这是基于LibGdx游戏开发系列的第二篇文章。阅读本文前,请确保你已经读过前面的第一部分.
由于本文包含了大量的内容,所以我会尽可能的切分段落来让文章容易理解消化。我们之前已经做了一个最基本的游戏世界,并且在这个世界里可以通过方向键和触摸动作来控制 Bob 前进后退。那么现在,就让我们为这个角色的移动增加一些更真实的动画和动作吧。
角色动画
为了让 Bob 动起来,我们决定用一个叫做 精灵动画 (sprite animation) 的简单技术来实现。所谓动画,不过就是一组连续的图片在一定时间内连续播放从而造成一种动画的错觉。下图所示的就是角色跑动时候的图片序列:
我曾经大量的玩 Star Guard 这个游戏,通过去分析游戏中角色跑动的过程然后使用 Gimp 来创建我自己的跑动角色。
构建一个动画是非常简单的。我们只需要把每张图片展现一定的时间,然后再展现下一张。当这个动画图片序列展现完毕后,我们重复这个过程即可,这样的流程我们称之为循环 (looping) 。在这里,我们需要去确定动画的帧长(每张图片显示的时间)。如果说一个动画的 FPS 是 60 FPS,那么就意味着我们的每过 1/60=0.016 秒就会渲染一帧新的动画。在这里,我们的动画总共只有5帧,考虑到一个每分钟180步的典型运动员的动作,我们可以算出最接近现实效果的帧长。
奔跑动画的计算
从上面的条件我们可以知道,典型的运动员每秒的步数是180/60=3步,所以我们的动画需要每秒播放3轮。又由于我们的动画每秒需要展示 3 * 5 = 15 帧从而模拟出一个较为真实的专业运动员跑步的姿态(非冲刺状态)。至此,我们可以算出这里的帧长为 1/15=0.066秒,也就是66毫秒。
图像优化
在把图片做成动画之前,我们需要对图片进行一些优化操作。在 star-assault-android 项目中的图片文件目前是存储在 assets/images 的目录下的,我们可以看到目前我们用到的图片有 block.png 以及 bob_01.png。 我们还需要其他的几张图片,连续命名为 bob_02 – 06.png,这些图片是用来做动画的帧序列的。
由于 LibGdx 底层是基于 OpenGL 的,所以用大量的图片作为纹理来渲染显然不是一个好的选择。而我们所使用的技术,是称为 纹理贴图集(Texture Atlas) 。 一个纹理贴图集就是指一张包含了所有动画所需图片的大图并且还带有一个说明了这些图片在纹理集中的偏移量和尺寸的描述信息。贴图集中的每一个独立的图片被称为纹理区域。如果有许多的小图,那么纹理贴图集也可以有许多页,每一页都会以单独一张图片的形式载入内存并且可以把图片从各个纹理区域中切出来。这一块的具体原理并不需要充分的去了解,只要懂得这样的优化操作可以让你的动画图片加载的更快更流畅就好了。
LibGdx 使用了一个称为 TexturePacker2 的工具去创建处理这些贴图集。这个工具可以在命令行下使用,也可以在程序中使用。如果你要用 Java 去调用这个程序,你可以用如下代码:
1 2 3 4 5 6 7 8 9 10 11 |
; html-script: false ] package net.obviam.starassault.utils; import com.badlogic.gdx.tools.imagepacker.TexturePacker2; public class TextureSetup { public static void main(String[] args) { TexturePacker2.process("/path-to-star-guard-assets-images/", "path-to-star-guard-assets-images", "textures.pack"); } } |
在这里,请确保你的项目的 libs 目录下已经包含了 gdx-tools.jar 这个库。你也可以通过修改 process 方法的参数来指定贴图集加载的目录。
处理图片的过程中有两个文件都会被处理,一个是 textures.png 另一个是 textures.pack。贴图集看起来应该是类似与下图的状态:
而其中的目录结构则看起来如下图:
至此,我们确定了动画应该怎么是什么样子的,动画的帧长并且还优化了素材图片,然后我们就该把动画加入到程序中去了。首先,我们把 Bob.java 当作一个最小的像素来修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
; html-script: false ] public class Bob { // ... omitted ... // float stateTime = 0; // ... omitted ... // public void update(float delta) { stateTime += delta; position.add(velocity.tmp().mul(delta)); } } |
可以看到,我们增加了一个叫做 stateTime 的新属性。这个属性会跟踪记录 Bob 在游戏中特定状态下的时间,我们也会使用这个来提供 Bob在游戏中所消耗的时间。这个时间刻度对于动画确定当前显示那张图片是非常重要的。如果你不太理解这个概念,请不要担心,你可以把动画的每一帧想象成一个特定状态。如此, Bob 的状态就会从 state_frame_1 到 state_frame_2 等等,每一个状态都持续 0.066秒,一但一个状态超过0.066秒,Bob 就进入到下一个状态中去。这里的动画类会知道当前状态需要提供哪一张图片,这也被称为关键帧。
修改最多的一个类是 WorldRenderer.java ,下面的代码包含了这些修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
; html-script: false ] public class WorldRenderer { // ... omitted ... // private static final float RUNNING_FRAME_DURATION = 0.06f; /** Textures **/ private TextureRegion bobIdleLeft; private TextureRegion bobIdleRight; private TextureRegion blockTexture; private TextureRegion bobFrame; /** Animations **/ private Animation walkLeftAnimation; private Animation walkRightAnimation; // ... omitted ... // private void loadTextures() { TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("images/textures/textures.pack")); bobIdleLeft = atlas.findRegion("bob-01"); bobIdleRight = new TextureRegion(bobIdleLeft); bobIdleRight.flip(true, false); blockTexture = atlas.findRegion("block"); TextureRegion[] walkLeftFrames = new TextureRegion[5]; for (int i = 0; i < 5; i++) { walkLeftFrames[i] = atlas.findRegion("bob-0" + (i + 2)); } walkLeftAnimation = new Animation(RUNNING_FRAME_DURATION, walkLeftFrames); TextureRegion[] walkRightFrames = new TextureRegion[5]; for (int i = 0; i < 5; i++) { walkRightFrames[i] = new TextureRegion(walkLeftFrames[i]); walkRightFrames[i].flip(true, false); } walkRightAnimation = new Animation(RUNNING_FRAME_DURATION, walkRightFrames); } private void drawBob() { Bob bob = world.getBob(); bobFrame = bob.isFacingLeft() ? bobIdleLeft : bobIdleRight; if(bob.getState().equals(State.WALKING)) { bobFrame = bob.isFacingLeft() ? walkLeftAnimation.getKeyFrame(bob.getStateTime(), true) : walkRightAnimation.getKeyFrame(bob.getStateTime(), true); } spriteBatch.draw(bobFrame, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.SIZE * ppuX, Bobddd.SIZE * ppuY); } // ... omitted ... // } |
- 行5——申明一个名为 RUNNING_FRAME_DURATION 的常量,用来定义跑动动画播放的每一帧长度。
- 行8-行11—— 一系列的TextureRegion 对象,用来表述 Bob 的不同状态。其中 bobFrame 用来指向当前状态的纹理贴图。
- 行14-行15—— 两个 Animation 对象用来做 Bob 跑动的动画。
然后是一个 TextureAtlas 用来加载贴图资源。
- 行19——修改后的 loadTextures() 函数。
- 行20——从资源文件中夹在贴图资源。这里的资源文件是一个TexturePacker2生成的 .pack 的包。
- 行21——把名为 “bob-01”的贴图赋值给 bobIdleLeft(注意,这里的名字就是真实的png图片的名字去掉后缀,详细可以参考 TexturePaker2的说明)。
- 行22-行23 —— 构造一个新的TextureRegion 对象(注意使用拷贝构造,因为这里需要的是一个全新的对象而不是一个引用),然后将这个对象的X轴翻转,这样我们就得到了一个一模一样但是左右镜像的 Bob 闲置状态贴图。翻转操作非常有用,可以让我们无需重复载入图片,仅仅通过拷贝已有对象就可以创建新的对象。
- 行24—— 把“block”贴图赋值给相应的变量。
- 行25-行28 —— 构建一个TextureRegion 对象数组用来制作后面的动画,我们知道这个动画有五帧分别称为:bob-02, bob-03, bob-04, bob-05 以及 bob-06 。我们用一个 for 循环来处理这些资源。
- 行29——这是向左走的动画构造的地方,第一个参数传入的是帧长也就是0.06秒,第二个参数传入的则是刚刚构造的贴图数组以便于顺序播放产生动画。
- 行31-行38——制作向右走动画,这部分的操作其实就是向左走动画的翻转,唯一需要注意的地方就是贴图对象记得使用拷贝构造全新的对象,而不是去修改原有的引用。
- 行40——修改后的 drawBob() 函数。
- 行42——根据当前 Bob 面向的方向来选择播放的动画。
- 行44——如果当前 Bob 已经是在行走状态,我们就根据他的面向的方向来判断需要处理的动画序列,然后对其进行处理,并将当前需要绘制的帧赋值给 bobFrame。
现在你可以运行下StarAssaultDesktop 这个 App 来看看 Bob 运动时的动画。具体的效果应该和下面的视频类似:
你可以从 https://github.com/obviam/star-assault 获得源码。同时你需要切换你的分支到 part2。
你可用如下命令直接 checkout 到源码
1 2 |
; html-script: false ] `git clone -b part2 git@github.com:obviam/star-assault.git` |
你也可以点击这里下载源码。