第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(0, -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个小时。