我是如何从0开始,在23天里完成一款Android游戏开发的 – Part4~7
第 9 天这是一款第一人称视角射击游戏,但它绝不老套
在与人们谈论起这款游戏的时候,为它定义一个明确的分类确实很难。虽然可以将它看作一款传统的街机游戏,但与那些到处移动自己的飞船、直线开火 的街机游戏不同的是——你的位置是固定的并且可以按照指令向任意方向开枪。经过仔细回想,我从来没有见过一模一样的游戏,所以不要试图把它归到那些现有的 分类中。我能给出的最为贴切的描述是:与太空入侵者类似,但是游戏中没有飞船。这样的描述把人们完全搞晕了。
今天我重写了制造外星人的代码,并且把外星人的颜色都改成了白色。这样就可以在游戏运行过程中用同一个子画面更新外星人的颜色,假设你没有发现 外星人是单色的。除了 boss 之外,我都打算这样处理。boss 会增加一些发光的动态效果之类的。谁知道呢,也许会有艺术家发现游戏的潜力并且创作出给力的效果。如果发现这种情况请联系我。
我考虑在敌人将要“升级”的时候给出提示,像是“外星人已经升级”这样带有金属感的文字会与外星人一起出现,
爆炸的代码也进行了统一。一开始是通过一个 2D“像素”数组展示敌人被炸成碎片。现在这段代码进行了优化,各种外星人都使用了同样的效果,只有 boss 除外。boss 会分阶段炸成碎片。这个设计我还需要仔细考虑。
无论如何,如果你对玩这个游戏感兴趣,这个.apk 文件就是迄今为止的全部成果。虽然没有什么特色,但是你能看到我一直在努力。
第 10 天让游戏玩起来更有意思
我已经开始怀疑这个概念是否真的具有较长时间的可玩性。如果外星人可以左右移动游戏会变得更好玩一点。我新增了两种攻击队形看上去感觉好多了。 我还增加了击毁外星人爆炸时镜头抖动的效果,这样游戏感觉更鲜活了。重新填充弹药的功能也实现了,现在如果你把十发容量的弹夹打空就不得不重新填装。在使 你中断射击重新填充弹药的同时可玩性也增加了。我把一些外星人丑陋的品红色替换成冷色调的蓝白色效果好看多了。
我想如果设定成玩家只操控屏幕的下半部分——有护盾遮挡的地方,屏幕的上半部分就可以一直看到敌人的情况而不会被玩家的手指挡到。这种情况是经 常出现的。一开始护盾可能有半个屏幕大,随着敌人的攻击护盾逐渐被打掉直到枪口裸露没有任何防护。虽然这意味着可能要画一把枪却不失为一个好主意,我可能 会试试。
星空之战的名字已经取好了。我尝试了其他 20 几个名字,但由于太空主题的游戏太多很难找到一个免费的名字。
第 11 天增加外星人种类、攻击方式,第一级 boss 出现
第一个 boss 出现在第 10 关出现,它的外形就是个大方块。我称它为 Borg,虽然它和 Borg 一点不像。它会不断发射外星人,你必须打很多次才能消灭它,我为干掉 boss 的场景设置了一个大的震动。boss 是由 10×10 的像素积木组合而成爆炸效果很炫。
我还加入了个新蛇形移动攻击,外星人落下时会左右移动并然后下降不断重复直至到达底部。我在这个攻击模式中稍稍加快了移动速度,这样玩起来更具有挑战性。
我还画了一些好看的外星人并给它们起了名字,每种类型都由 Java 类的名字命名。分别是:Hairy、Glider、Worker 和 Eater。
这是今天做的.jar 文件,只要你安装了 java 就可以在 windows、linux、mac 上运行。在 linux 上我执行的命令是:
java -jar stardust.jar
为了保证正常的游戏效果,请注意窗口的高度不能小于 800。我在 1680×1050 的台式机上运行良好,但在 1280×800 的笔记本上由于任务栏占据了一定空间窗口被垂直压缩,因此我需要点击外星人的下方进行射击。我想如果要发布 PC 版的就必须解决这个问题。
第 12 天(第一部分):新游戏名 DRONE INVADER
名字终于选好了。备选名字有很多,但只有6、7 个是可用的。这一个看起来最符合游戏的主题。全新的主题也做好了,同样选用了 Ruslan 字体。
今天弄懂了 Java 里 Comparable 和 Comparator 的区别。我改动了子弹部分的代码,以便同时发射多个子弹(激光碎片)。子弹不必接触到外星人才能打中,只需朝着一个方向发射,子弹便会自动攻击外星人。这 样就可以简单地根据子弹发射时刻的轨迹判断先击中了哪个外星人,从而取代全碰撞检测。这个方法对除 Boss 级外的所有级别都适用。 因为 Boss 会吐出其他外星人,他应该排在数组里的第一个,这样子弹会飞过所有外星人直接打中 boss。如果子弹直接打中 boss,会保留之前的处理。但如果只是向某个方向射击,会按照y坐标轴对外星人排序,然后打中最近的那个。
新特性会稍稍改变游戏体验,现在玩起来更容易也更有趣了。射击外星人不再会因为手指点击出现误操作。游戏从“精确碰撞”变为“射击测试”,弹卡是 10 发的,以防你瞬间摧毁一切。
我想我应该把外星人的运动变得更有挑战性一些,这样游戏不至于太容易。
第 12 天(第二部分)
我开始喜欢上这个游戏了,而且是特别喜欢。我决定在睡前再做一会儿,在 16、26 这样的关卡添加了一个大月亮,外星人就藏在月亮后面。月亮慢慢移动穿过屏幕,在 Boss 出现的时候正好从屏幕移出。这让游戏难度陡然增加,因为很难判断是否有外星人躲在月亮后面。由于月亮是圆形,这增加了操作长方形难度。我调查了精确像素检 测方法,每当射中一个外星人像素会逐个显示,特别是当月亮一直在转的时候。然而这种方法恐怕会让游戏变慢。原来的矩形被我减少了 12%,用矩形检测取而代之。虽然并不是精确像素,但是工作得也很好。这种方式可以让子弹穿过月亮打中有月亮做掩护的外星人。
我对现在的游戏体验非常满意。有趣,有挑战性,而且很吸引人。现在我需要更多 boss,更多的外星人类型和能量升级。如果不升级能量想升到 60 级是非常难的,所以我试着增加足够的内容,起码在 100 级之前没有重复的 boss 出现。我觉得增加能量升级会容易点,看看增加这些会以后游戏会变成什么样。
我第一次感觉这会是一个很棒的游戏,会从一般的太空射击游戏里脱颖而出。
第 13 天:盾牌、新 Boss
我加入了一个新 boss,现在有两个 boss。以下是目前外星人的名字:Worker、Eater、Hairy、Glider。Boss 叫 Worker Boss(看起来像是更大的 Worker,而且会吐出很多小 Worker)和 Borg(它是立方形的,被摧毁以后变成许多大立方块)。
我还添加了盾牌。以前我曾经用 Inkscape 画过这种漂亮的盾牌标志,还从某个 YT 教程里获得了灵感。我试着照着教程做(教程使用的是 Adobe Illustrator,不是 Inkscape),但是失败了。我开始观察一些喜欢的盾牌,注意到盾牌只是由一些分支、曲线或梯度构成。我把 Hairy 放上去,看起来很不错。这个还可以用作 Android 标志。
不管怎样,这个盾牌可以随时引入并且持续 20 秒。后面的一些升级可以使盾牌持续更长时间。如果外星人碰到盾牌会加速损耗能量,这样即使盾牌消失你仍然可以干掉它们。
盾牌的图形看起来很像力场,使用 Gimp 再配合手动修改可以让它变得更好看。我想要一个漂亮的曲线而不是直线,因为直线好像不能添加梯度阴影效果。也许有一些技巧可以做到,但是目前我还不知道。最后,我结合了不同角度的多重线性梯度,出来的效果非常棒。
我试着在这个盾牌标志上面添加一些荧光效果,但是看上去有些太刺眼了。我会把它留在飞行过程中吃到能量升级时使用。
有了这些新图形,游戏看起来更完整了。我还在考虑在哪里放置分数倍增器比较合适,还有是不是需要显示当前攻击波。
我还在想玩家需要在玩之前买一些升级装备,但这需要一些硬币,或者类似的钱,目前,快速游玩显然还不支持。也许可以在一些外星人后面留一些水晶或者一种类似随机升级的装备会出现。再或者你经历了多少波,就得到多少硬币。
第 14 天:完成所有低级外星人图形
所有 7 种低级外星人都完成了.我刚刚做好了 Catcher,Humaniod 和 Scorpio 是昨天晚上做的。我还做了一些基本图形放在商店里,玩家可以从那儿买到升级产品。
关于外星人 boss 我又有了一些新想法。其中一种像蛇,身体由多段组成,需要分段消灭。
第二种外星人 boss 自己也可以制造 boss。如果你不能及时摧毁它,它就会放出另一个 boss。第二种 boss 吐出普通外星人并向下移动(以便为下一个留出空间)。
第三种 boss 是一种特别的生物,它能够自我分裂。当它被打中时,会分裂成两个相同的外星人。每一个分裂出的外星人能量是有母外星人的一半。外星人会一直分裂,直到变成一堆能量为 1 的外星人。打死那些外星人就可以结束游戏。
第四种 boss 是……好吧,让我留一些惊喜给你们,玩的时候就知道了。我敢肯定对一些玩家而言,第四种 boss 是非常讨厌的,除非他们发现这种 boss 的规律。
安排音乐时间
今天剩下的时间里,我浏览了一些免费音乐。有许多音乐网站,但成千上百的音乐逐个听过去并不好玩。多数免费的音乐网站都是垃圾。我在 Reddit 的 gamedev 上找到了一些推荐的网址,同样在 gamedev.stackexhange.com 上可以看见,还有一些独立的网站。除了 Jamendo, 这个有点贵,大多数免网站真的很糟糕。
我通常会自己创作音乐,用 MilkyTracker 或者一些其它破解程序。这个习惯是从 Amiga500 来的,在 .it 或 .s3m 调制器的时代这让我感觉得心应手。但这次我觉得没有时间这么做。通常创作一个好听的音频要花我3、4 周。然而,我可以再利用一些以前的作品,但我已经把它们都用到以前的游戏中了——说实话,没有一个适合用到这次的游戏。我还是找到了一些 Kevin MacLeod 的音乐。这些音乐相当不错,我决定就用他了。
是花上几个小时去听那些免费音乐,还是集中精力自己做,似乎真的取决于你对时间的估计。
第 15 天: Android“后退”按钮、主菜单、固定坐标 bug
还记得第 11 天屏幕坐标和鼠标点击射击不到外星人的问题吗?是的,那都是我的错。幸运的是这让我及时发现了很多下载游戏的 Android 用户屏幕分辨率并不是 800×400。在那之前我是这样直接转换触摸坐标到实际坐标:
float x = Gdx.input.getX () - 240f;float y = 400 - Gdx.input.getY ();
这不是正确的做法。简单恰当的办法是通过 GDX 进行转换 :
Vector3 touchPos; touchPos.set(Gdx.input.getX (), Gdx.input.getY (), ); camera.unproject (touchPos);
在 Android 上处理“返回”按钮
大多数网上的例子在处理“返回”按钮时都谈到重载 KeyDown 方法。不幸的是这种办法要求使用 Stage,我没有这么做。我知道现在的代码里复制了很多 Actor 和 Stage,但那不重要。在下一个项目里我才会使用 Stage。
幸运的是,我找到了解决办法。只要在 Game 子类的 create ()函数里添加下面函数:
Gdx.input.setCatchBackKey (true);
然后在 render ()方法中检查否已经按下“返回”按钮:
if (Gdx.input.isKeyPressed (Keys.BACK)) { Gdx.app.exit (); }
由于 render ()每秒钟会被调用很多次,你可能需要一个 boolean 标记变量来检测“返回”按钮是否已释放。
if (backReleased && Gdx.input.isKeyPressed (Keys.BACK)) { backReleased = false; Gdx.app.exit (); } else { backReleased = true; }
现在可以进入游戏,进入商店菜单,然后返回主菜单。当然,菜单只显示选项,还没有真正实现功能。
使用9-patch 处理动态大小的按钮和容器
译注:9-patch 一个对 png 图片做处理的工具,能够为生成一个“*.9.png”的图片实现部分拉升。
我还学会了如何使用9-patch 创建漂亮的按钮。有一次,我意识到不得不像绘制 10 个大小不同的选项按钮,但样子基本上一模一样只有里面的内容不同。我甚至参考了 Gdx 按钮,但最终还是决定自己 DIY 一个。在我游戏里,按钮有一些特殊需求,在一个文本按钮里要结合了 2 张图、4 个文本以及 2 种不同字体。
无论如何,我得画一个包括所有按钮尺寸和其他的东西的 46×46 9-patch 图片,然后写一些代码定制其他覆盖在图片上面的东西。我在构造函数里通过 TextureRegion 从大皮肤里提取9-patch。减掉了一个皮肤开关。
通过这种处理使我得以有各种不同的选择来填充主菜单,同时我还加入了滚动字幕给出玩法提示。我真的很喜欢这个概念,但很少有游戏使用它。有的游戏只显在一开始的时候有个提示。也许他们不想让玩家看主菜单时分心吧。
下面是购买强化道具的商店菜单:
强化道具
关于道具我又有了一些新点子。一种是可以暂时让外星人减速,另一种是在短时间内积分 x5。我正在考虑移除之前商店里的“双倍积分”道具。有些玩家真的很能得高分,所以这可能是一个坏主意。
另一方面,在下次装弹前能增加射速的道具可能会大受欢迎,所以我正在加入。
我希望商店能保持只有 7 个道具,这样就能刚好在一个屏幕内显示。但现在我不肯定所有可能的升级……拭目以待吧。
第 16 天:从 GDX 游戏中录制影片
视频地址:www.油Tube.com/embed/RUy177pvT8I?rel=0
我曾想过在 油Tube 上传游戏视频,然后用 recordmydesktop 程序录制,但结果一团糟。由于 libGDX 和 RMD 不同步,我在屏幕上看到的是一堆零件,诸如被切掉了一半的精灵等等。我搜索了一下发现了几篇有用的文章。基本上都是将每帧做成一个 PNG 文件然后组成视频。可以想见这么做会耗费大量的磁盘空间,这对我不是大问题。我发现了一个很有用的帖子:
http://www.wendytech.de/2012/07/opengl-screen-capture-in-real-time/
然而,他们的代码有一些问题。出于某种原因,当我用半透明精灵叠加在背景上时,由此产生的 PNG 文件在那块区域会出现半透明像素。这样生成的视频会有很多乱七八糟的东西。我尝试了不同的设置,甚至改变渲染代码,但问题依旧。现在,只要一个简单的处理 步骤——使用 ImageMagick(加入黑色背景)就可以解决这个问题。所以我想,如果无论如何都要做这步处理,我可能还要在 ImageMagick 中做垂直翻转。所以我关掉了代码中的Y轴翻转,这使得它更有效率,从而没有必要在每一帧中分配w *h*4 个字节的内存。在 800×480 的屏幕上,每一帧大约需要 1.5MB!
同时,处理帧率(跳帧)的代码没有怎么优化。处理过程跳过了几个文件号,这没什么问题。但同时还给每帧还创建了对应的 ScreenShot 对象,这完全没有必要。譬如你正在录制 30fps 的视频而游戏运行速率是 60fps,你花了一半的时间在创建完全用不到的对象上。
最后,FPS 处理代码似乎没有释放像素图。所以如果你运行了很长的时间,RAM 会被吃光。
所以,我从 ScreenShot 类里提取出了全部的 FPS 代码,剩下的代码只负责处理连续视频。我还注意到一些变量有初始化但从未使用过。现在 ScreenShot 类变得更加直观并且易于理解:
public class ScreenShot implements Runnable { private static int fileCounter = ; private Pixmap pixmap; @Override public void run () { saveScreenshot (); } public void prepare () { getScreenshot (, , Gdx.graphics.getWidth (), Gdx.graphics.getHeight (), false); } public void saveScreenshot () { FileHandle file = new FileHandle ("/tmp/shot_"+ String.format ("%06d", fileCounter++) + ".png"); PixmapIO.writePNG (file, pixmap); pixmap.dispose (); } public void getScreenshot (int x, int y, int w, int h, boolean flipY) { Gdx.gl.glPixelStorei (GL10.GL_PACK_ALIGNMENT, 1); pixmap = new Pixmap (w, h, Pixmap.Format.RGBA8888); Gdx.gl.glReadPixels (x, y, w, h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, pixmap.getPixels ()); }
好了,全部就这么多。我在渲染循环中的每个渲染结尾加上了:
ScreenShot worker = new ScreenShot (); worker.prepare (); // grab screenshot executor.execute (worker); // delayed save in other thread
考虑到完整性,在 Screen 的子类添加了 executor:
private ExecutorService executor; ... executor = Executors.newFixedThreadPool (25);
现在,在我的酷睿 2 已经赶不上帧率了。这是好消息,一方面因为游戏速度变慢我能够录下更好的视频,另一方面能更好地记录截图以供稍后导出视频。所以我添加了一个截图热键。在 按住S键时开始录制,当你只是记录了一些有趣的片段,松开S键让 PNG writer 赶上进度。当 CPU 的负荷恢复到正常,意味着 PNG 都生成好了,你可以再次开始录制。
这种方式创建的视频很容易编辑。只要删除不需要的 PNG 文件,用剩下的压制视频即可。而且这种方法也很容易与音乐同步,因为可以随意添加或删除帧。
用截图生成 油Tube 视频
由于 Android 屏幕默认分辨率是 480×800,而最接近 油Tube 的分辨率是 1280 x720。因此需要将图像缩放到 432×720 ,以保持宽高比。这样两边会多出很多未使用的面积。你可以把你的 logo、广告贴上去,甚至可以并排显示两个视频。我决定用另一段视频填补空白,那是我用一台手持设备拍摄的,所以图像更小只有 372×620。
现在,我创建了一个大小 1280×720 包含了 logo 的静态图像。现在我把它混合进游戏,并垂直翻转。在 Linux 上,我使用这样的命令:
for i in shot*png; do echo $i; convert $i -flip -filter Lanczos -resize 372x620 temp1.png; composite temp1.png back.png -geometry +126+56 $i; done
一旦所有的图像都准备就绪,就可以运行 MEncoder 来导出视频。油Tube 建议 720p 的视频采用H.264 格式和 5000 以上的比特率 。他们还建议两个B帧(RGB)。这里是执行的命令:
mencoder mf://shot*.png -mf w=1080:h=720:fps=25:type=png -ovc x264 -audiofile music.mp3 -oac copy -o movie.avi -x264encopts bitrate=5000:bframes=2:subq=6:frameref=3:pass=1:nr=2000
这样就生成了一个质量过硬的 油Tube 游戏视频。在这篇文章的开始,你可以看到我的成果。至于音频,我只是提取了一些游戏的音轨并没有捕捉实际游戏中的音频。
第 17 天:Android 图标、完成道具
我喜欢 Android 允许(甚至建议)图标不是圆角矩形。这样可以赋予游戏自己的个性风格。起初,我考虑过给这游戏做一个特殊的图标,但我真的非常非常喜欢这个画着外星人像素 图形的盾。我用 Inkscape 制作,这样就可以输出任意大小的图片(而不像在 GIMP 下制作的其他一些图形)。献上 Drone Invaders 官方图标:
丰富的道具
下面的视频显示所有收藏的强化道具:
http://www.油Tube.com/embed/SZ73G0n6cm4?rel=0
我准备了原子弹,但名字还没有最终确定。也许会叫核弹、钚炸弹、智能炸弹或完全不同的东西。它会摧毁屏幕上的一切。Boss 能抵挡一两个,但遇到三个炸弹一样完蛋。在系统内部,每个 Boss 有 20 点血而炸弹有 8 点的伤害。普通攻击就是 1 点伤害,除非你升级激光。
其次,有3 路散弹。射击三次仍然要更换弹夹。这是一个非常强大的道具,有了它,真是人挡杀人佛当杀佛,清理掉一波波的怪物和 boss。
第三,自动重装填。正如名字那样,你的激光会自动加载。所以可以自由地射击,射击,再射击。
第四,减速。它只是减缓外星人的移动速度,其他一切速度正常。在前 20 关这玩意儿相当废柴。但越到后来,你就越觉得它有用。
第五,双倍积分。在道具作用期间,获得的点数翻一倍。我仍然在考虑是否要在达到某个分数的时候给予奖励,但达到高分仍是一件很酷的事情。
第 18 天:外星人图形与圆形冲突、完美的子弹轨迹
今天我受够了“射击月亮”bug。有时候外星人即使在屏幕中出现,也可能射不中。我做了大量测试,在屏幕上布满外星人并且设置月亮半透明以定位 这个 bug 的原因。我发现测试击中区域的坐标偏移了一个 bit 位,但即使解决了这个问题原先的 bug 依然存在。外星人图形不能简单用圆形覆盖,否则玩家要么射不到外星人,要么会射到隐蔽在月亮下的外星人。
所以我决定使用圆形检查。由于月亮比外星人大很多,能够很容易地检查外星人图形边缘的四个点是否都在圆形月亮内。为了测试,我使用 libGDX 内置的 ShapeRender 类,具体的实现代码如下:
shapeRenderer.setProjectionMatrix (camera.combined); shapeRenderer.begin (ShapeType.Circle); shapeRenderer.setColor (1, 1, 1, 1); shapeRenderer.circle (sMoon.getX () + 119, sMoon.getY () + 116, 167); shapeRenderer.end ();
上面的代码加在 SpriteBatch 完成以后,沿着月亮表面画白色的圆圈。类似地,给外星人边界画上长方形。
测试一个点是否在圆内的高效方法不是计算平方根(速度较慢)而是比较距离的平方。libGDX 的内置函数 Circle.contains (x,y)恰好实现了这个功能,所以我使用了这个函数进行检查。事实证明这个方法非常有效。我为半径长度增加了一些像素值,因为所有外星人之间会有一些间 隔。改动后的结果令我非常满意。
完美的子弹轨迹
在这个游戏中,子弹是从距离屏幕下方 50 像素值的地方发射的。我使用了函数 atan2 让子弹旋转着击中目标,但我的代码中有一些错误,在没有射中目标时错误会经常出现。为了理解这部分内容,请注意在这个游戏所有的射击都采用了 HitScan 策略。
译注:HitScan 与射击目标相对,指的是射击出的子弹不针对任何目标而是摧毁子弹运行轨迹上的任何物体。
在没有射中目标时,现在的代码将子弹轨迹延伸到屏幕尽头,而以前的代码把尽头设置得太远。由于子弹的飞行使用了中间位置,结果看上去有很大的跳 跃并且在子弹射出屏幕之前只能看到2、3 个点。通过把结束点设置到屏幕的边缘来解决了这个问题,现在你能清楚地看到子弹在飞行。
这时又暴露出另外一个问题:子弹有时候距离玩家接触的屏幕点只有 10 到 20 个像素点。导致这个问题有三个原因。第一个问题,我使用了子弹的X坐标和Y坐标。由于这个坐标位于屏幕底部的角落。通过把子弹的中心坐标加上一半的宽和高 解决了这个问题。但仍有一些子弹没有射中。第二个问题,我忘记设置原点,所以子弹围绕着左下角进行旋转。这个问题也解决了,但仍有一些朝屏幕左边射射出的 子弹没有射中。
第三个问题,我意识到当子弹旋转时宽度和高度是在变化的,所以子弹的中心点需要在旋转后需要重新计算。解决了这个问题,子弹就能正确地从玩家触摸的地方射击。修改后的代码如下:
// 子弹飞行 LaserBullet lb = new LaserBullet (tUI, 65, 64, 20, 40); lb.setPosition (, -450); lb.setOrigin (10, 20); lb.setRotation ( (float)(Math.atan2(-x, 450f+y) * 180f / Math.PI) ); Rectangle r = lb.getBoundingRectangle (); x = (int)(x - r.width * 0.5f); y = (int)(y - r.height * 0.5f); lb.target.set(x, y); bullets.add (lb); Tween.to (lb, SpriteTweenAccessor.POSITION_XY, delay) .target (x, y) .start (tweenManager);
第 19 天:每日挑战和任务
每日挑战是收集 5 个字母,操作方式和道具一样。一旦收集了所有字母,就可以得到一些用于购买道具的游戏币。这是一个通过玩游戏获取硬币的简单方法,这个灵感是受到“地铁跑酷”(Subway Surfers)的启发。
任务由许多子任务组成,通过完成这些子任务可以赚取硬币。硬币可以用于购买升级道具和消费物质,如盔甲、炸弹等等。每天的任务由三部分组成,你必须完成所有三项子任务才能获得奖励。
我发现使用内置的文本换行来显示任务比较简单。然而行高会显得过大,而且直接修改代码没有办法减小行高。因此我选择编辑由 BMFont 生成的 .fnt 文件,进行如下调整:
lineHeight=33
变成
lineHeight=23
在开始生成位图时,我在字母的四周增加了 5 个像素的阴影,所以现在需要把高度减少了 10 像素(上面减少 5 像素,下面减少 5 像素)。
在为此查找文档时,我发现了一些先前遗漏的问题:在为游戏选择字体时,可能数字看起来效果不是很好。数字 1 看起来很修长,而数字 11 看起来很奇怪。要解决这个问题,可以为图中的字体设置固定宽度。
font.setFixedWidthGlyphs ("0123456789");
这样效果看起来会非常好。但由于已经决定使用修长字体,因而没有采用固定宽度。
第 20 天:周挑战、用户数据持久化、Java 日期灾难
周挑战是在一周内收集特定数目的星星,从而获得一些优异的奖励,如 8 个原子弹、5 个盔甲等等。我用 Gimp 做了一个很棒的金色星星并在尝试了不同的闪烁和星光效果,但是这些看上去效果不是特别好。所以我想到了强化道具的粒子效果,对它进行改变直到满足星星的要 求。星星有了自己的闪烁节奏,而且可以在屏幕上同时显示星星和强化道具。
我还添加了玩家数据的加载和保存。这个比我想象中要简单。我以为必须学习一些 Android 的数据存储 API,但 libGDX 提供了简单键值存储类。只要调用以下代码进行初始化:
Preferences prefs = Gdx.app.getPreferences ("DroneInvaders");
然后使用 get (“key”, defaultValute)和 set (key,value)进行值的读写。
我唯一遇到的麻烦是时间问题。为了持续跟踪天挑战和周挑战,必须存储最后玩游戏的时间。当玩家开始游戏,系统比较这个时间并重新设置一些计数 器。理论上我可以阻止玩家将系统日历修改到过去的时间,但是我不想这么做。当时间回滚时,我所做的是设置新的每日挑战和周挑战并且重置星星和搜集到的字母 个数。
为了实现这个功能,必须获取上一次玩游戏的时间并计算与当前的时间差。是否是同一天、一天前或几天前都会影响计算结果。我在谷歌上搜索到很多讨 论这个问题的网站以及 StackOverflow 问题。大多数答案很好笑。许多程序员简单地用相差的秒数来计算时间差,然后除以 60*60*24 得到天数,完全忽略了夏令时和闰秒。有人会争辩说,对一个游戏来说这个差别影响不大。但是我不喜欢每年收到 2 次大量的 bug 报告。另一些家伙简单地通过从开始到结束日期一天天累加天数。这些循环看起来是正确的,但是计算结果还是会丢失了部分时间。比如一个对象在 1 月 1 号上午 5 点存储了,然后你在 1 月 2 好晚上 23 点计算时间差,在第一个时间点上加上 1 天仍然比第二个时间点少。但是按他们的计算方法,实际增加了 2 天。
在这种情况下,我使用的一个技巧是总是设置前一次游戏的日期为早上 10 点,而设置最后一次游戏的日期为下午 5 点。尽管夏令时总是在晚上改变,但是这个设置是安全的。因为即使如果有一天有人决定夏令时的变化发生在中午,在这之间同样也有 7 个小时。