用Java制作一款Steam上的游戏

用Java制作一款Steam上的游戏

哈咯,大家好,又见面啦,今天我们要介绍一下如何用Java来制作一款游戏,并将其放到Steam上售卖,啧啧啧,看到标题之后,你的想法是不是如下图啊?哈哈哈

嗯,我知道传统认为,Java并不适合用来做游戏,马上我们将会谈到Java不适合做游戏的原因,随后我也会一一解释如何解决这些问题,随着时间的推移,以前很多认为不可能完成的事,现在也都已经不再是障碍咯,表刻舟求剑哦。

先来看例子,Java做的知名游戏至少有以下两款:

Minecraft(俺的世界)

Spiral Knights on Steam(螺旋骑士)

前者创造了M$历史上至今为止最大一笔收购,后者放在Steam上售卖,打开该游戏的界面可以看到该游戏要求Java至少是1.5:

好了,这说明了一点,就是Java是可以用来做游戏滴,关键看制作者的能力如何,人笨不要怪刀钝。

使用Java的优点不用解释了,应该很多人都知道,开发快,IDE支持好,网络支持好,类库多,跨平台等等,下面主要说一下传统用Java开发游戏的障碍,为什么Java并没有被用来制作游戏的几个常见理由:

  1. Java的程序运行需要预先在各个操作系统上安装虚拟机;
  2. Java有GC停顿,会阻碍客户体验;
  3. 没有流行的游戏引擎;

先说第一个,这个的解决依赖于大概14年时候Java推出的JavaFX新一代图形控件,该控件有一个native compiling的工具,可以将JavaFX的代码打成不同操作系统上的独立运行的包,比如打成Windows上的exe文件,打成MacOSX上的dmg文件,用户拿到之后,双击就可以运行啦。而在最新版本的Java中,JavaFX已经集成进Java,成为Java的一部分啦,所以不需要单独下载JavaFX,只需要下载Java,JDK之后,就能使用JavaFX,以及native compiling工具,是不是很方便呢?Native Compiling打包可以参考专栏之前写的一篇文章:JavaFX的几个新特性,让Swing彻底过时

有一点可能需要注意,打包的时候,选择一下all,而不是特定的exe或者dmg这些,这样会生成一大堆文件,你把你需要用到的部分挑出来,那些不用的直接丢弃便可。

至于哪些是你需要用到的,你自己摸索啦。

这样就可以发布特定平台相关的应用,是不是很方便呢?

然后第二个,GC停顿,这个是一个很大的topic,Java的GC里面讲究甚多,这里只能简单说明一下,Java传统的GC算法呢,在启动GC的时候,会暂停整个虚拟机的执行,然后等GC完成之后,再继续执行,而人对于超过一定时间的停顿,是可以感知到的,一旦GC停顿超过该感知的界限,玩游戏的人的体验就会变差,嗯,这么说有些过于感性了,我们来一点数字。

一般游戏的帧数是20帧到60帧之间,少数会冲到90帧,帧数意思就是每一秒游戏画面刷新的次数,60帧的意思就是一秒钟内游戏画面刷新60次,为什么游戏的帧数会在20-60之间呢?

因为低于20帧,人就能看出画面的停顿,超过60帧,帧与帧之间的停顿时长,基本上就超出了人可以感知的范围了,也就是说,如果两个不同画面的停顿超过1/20秒,人看到的就是一幅一幅不同的图片,而不是动画,而要让人看到动画的效果,不同两个画面之间的停顿,至少要在1/20秒也就是50ms以内,而人视觉感知的极限,就是1/60秒,也就是16或者17ms,如果停顿时间短于16ms比如是10ms,那么人在视觉上是很难感知到的,也就是说,一秒刷新60次,跟一秒刷新90次,在人看来,几乎没啥差别,所以就能看到iphone或者android手机的广告,60帧如丝般顺滑,blablabla,简而言之,就是我们要把刷新画面做到一秒刷新20次以上,60次最佳。

那这个时候我们就能看出来啦,GC停顿如果超过50ms,客户就能感受到GC,如果低于16ms,完美。

这里说一下,虽然超过50ms就能感知到,但是一般情况下,只要这种50ms级别的停顿不频繁发生,客户体验并不差,比如10min触发一次50ms的停顿,这有关系吗?玩个游戏又不是搞导弹拦截,偶尔来个50ms的卡顿会死人还是会怀孕啊?而且网游里面,公网的延迟经常超过50ms这个量级,尤其是手游的破网络。

那肿么做到呢?

有技巧,首先要修改GC的策略,在新版本的Java中,加入了新的GC策略G1,该策略将会在Java9中成为缺省的GC策略,该策略允许设置目标停顿时间,解释一下目标停顿时间的意思,就比如我们设置目标停顿时间为10ms,那么GC会尽可能在10ms内结束,如果完不成该目标,则暂停GC,程序恢复执行,等下一次GC继续,如此反复,所以G1策略的总停顿时间,会超过CMS策略,但是每一次GC停顿,都会控制在一定时长以内,但是不能保证一定在该时长以内结束,只是虚拟机会尽量完成这个目标,这是一。在打包时候,ant build file里面用fx:deploy, fx:platform和fx:jvmarg来设置JVM参数,which包括了GC策略等参数。

其次,我们要减少GC的时长,肿么做?将对象pool起来,也就是说,需要复用对象,不能频繁生成并销毁对象,这样非常消耗GC资源,会明显增加GC停顿时长。举个例子,如果我们做的是一个射击游戏,那么发射出去的子弹,我们需要pool起来,比如把子弹对象都放在一个list里面,子弹出了边界之后,标记对象为无效状态,但是并不从list中remove掉,这样因为有list的引用在,该子弹对象不会被GC掉,下一次发射的子弹,先找一遍有没有标记为无效状态的对象,如果用,则使用该对象,如果找不到,再增加一个新的对象,放入list,这样子弹对象数量随着游戏的进行,会趋近于一个定值,而因为有list这个引用在,所以对象不会被GC掉,这样GC的压力就小了很多,即便触发了full gc,其执行时间也在一定范围之内,有助于我们实现目标GC停顿时间。

再其次,多线程并发执行,先将代码通过MVC分离之后,m也就是model的部分可以并发执行,这个技巧客观滴说,很多游戏都做不到,包括钢铁雄心,到现在还在跑单线程,玩到后面卡得一塌糊涂,尤其是当苏联有了上千个师的时候,还有一些大厂的后台,居然跑的也是单线程,智商实在是感人,不过游戏后台这个技巧将来留给Vert.x部分再说。

最后,渲染时候尽可能调用底层的API,也就是View的部分,请使用诸如Canvas之类的控件,在安卓上,就用SurfaceView,在IOS上好一点,有SpriteKit可以使用,而不是其它的控件,比如ImageView等,因为普通的ImageView需要换Image的时候,需要重新生成一个Image对象,乖乖,图像对象可是很大一个东东,频繁生成销毁这种对象,GC不卡才见鬼。

做到了这几点之后,一般GC停顿实测,MacBook Pro 15年版,基本上GC在30ms左右,如果是网吧的Windows机器,基本上都在15ms以内完成,也就意味着,能够实现我们的目标啦。游戏的本质其实就是一个UI线程不停滴刷新屏幕,不要打断这个UI线程的这么一个过程,其本质跟Vert.x的Eventloop线程有一定相似之处,扯远了。

本来想写完的,但是写到这里发现,东西还挺多,那就先这样吧,下一篇我们再解释如何制作一个简单的游戏引擎,然后发布到Steam上去。

最后来一个最爱的哈拉鱼表情,萌死了

bye

------------------------------

更新一下,这一篇其实主要是各种可行性的分析,有哪些blockers,是否可以解决,怎么解决,下一篇开始我们介绍如何实现,就是上代码了,然后回答评论区提出的两个问题:

1)图形引擎,JavaFX有一个图形引擎Prism,其封装的是DirectX或OpenGL接口,比以前Java用的什么Java2D,Java3D,还有LWJGL,那是要强太多了,不管是什么游戏,只要在Windows以及MacOSX上,其实底层用的也都是DirectX或者OpenGL的api,这都是很成熟的图形接口了,Prism只是照搬其它引擎的经验罢了,Prism在渲染的时候,会先尝试调用DirectX或者OpenGL,如果操作系统没有这两个,则使用Java2D/3D,所以这能解释为什么以前用Java写的GUI会卡,而我用JavaFX写出来的不卡,我要是换成Java2D/3D估计也卡。然后一般Windows&MacOSX都有OpenGL或DirectX啦,这都是OS的一部分了,说白了,其它游戏引擎最后调用的也都是OpenGL/DirectX,JavaFX也调用的是这两个,渲染起来能有多大本质区别?其它游戏引擎底层能用这两个做硬件加速,Prism有什么做不了的?还不是一样的?当然经过封装之后,实现细节对用户透明了,我们也不需要关心到底是怎么实现的,知道有这么一回事就行了。

FX架构如下图:

2)字体,这个说到底就是缺省字体对中文支持不好的缘故,因为字体font这个东西,跟不同国家语言有关系,不是所有的字体都能覆盖所有语言的,因为符号太多,甚至可以这么说,没有任何一个字体,能够覆盖地球上所有的语言,因为太多了,所以一般都是各个地区自己搞,而很不幸,Java是Unix或者是Solaris的直系吧,反正很多界面都是照搬Unix那种的UI,贼丑,对中文也不友好,因为大天朝就没几个正儿八经用Unix的,会用Unix的也不见得需要看中文,所以一直以来都支持得一般般,好,不抱怨过去了,说说解决方案,这个问题在JavaFX中也解决了,JavaFX支持读取jar中的字体文件,包括.ttf这些,并在渲染时使用这些字体,所以问题就很容易解决了,你需要做的就是去网络上找一个你看着顺眼的.ttf文件,然后下下来,打包时候放进jar里面去,然后用Font.loadFont方法读出来,然后用的时候,setFont就好了,或者用css,这个问题已经不是问题了,表刻舟求剑,Java被Oracle收购之后,还是做了很多有益的事,其中GUI的变革相当值得称道,为了市场做出了很多妥协,不像SUN当年那么装逼,扯远了,字体的解决方案参考How to embed .ttf fonts is JavaFx 2.2?

以上

编辑于 2017-08-27 14:55