图1 游戏最终效果
我们都看过动画片,看过电影,玩过游戏,里面都有各种绚丽的动画,实际上动画的绘制机制概括说来就是以一定的速度连续播放静态的图片,这样人眼就能识别出动画。一般我们人眼能够分辨的动画速度为10——20帧/秒,我们称之为FPS(Frame Per Second),低于10帧就会变得很卡,高于20就会变成快动作,因此,控制好这个数值对游戏整体的流畅度至关重要。
那么总结起来,要想在游戏中绘制动画需要具备以下几个必要元素:
1. 动画的原始图片
2.动画序列数据
3.动画刷新机制(下一帧)
常规的动画播放方法1.实现第一步(动画的原始图片)
常规的播放2D动画方法的方法实际就是对上述过程的一个分解:在游戏中,精灵的图片经常是被放置到一张图片上,下图就是一个典型的2D游戏精灵图片:
图2 角色原始图片
2.实现第二步(设定动画序列)
有了原始图片之后,我们就可以把这些图片按照一定的尺寸进行分割,切割成的每个块我们称之为“帧”,同时为每一帧进行编号,为每帧分配唯一的索引号,索引号可以从任意数字开始,可连续也可以随机,但要注意不能有重复的索引号,分配好的索引号可参照下图所示:
表1 游戏帧序列
0 1 2 3 4 5 6 7 8 9 10 11
有了这个索引,我们就可以为每个动画设定动画序列了,比如,我们可以分别定义精灵的上、下、左、右行走的动画序列,每个动画的序列如下面代码所示:
final static int ACTION_UP[]={0,1,2};
final static int ACTION_DOWN[]={6,7,8};
final static int ACTION_LEFT[]={9,10,11};
final static int ACTION_RIGHT[]={3,4,5};
final static int ACTION_STAND[]={7};
数组中的每个元素值就是上面表格中的数值。
3.实现第三步(切换并播放动画)
由于所有的图片块都被放置到了一个数组中保存起来,因此有个动画数据之后,我们就可以把数据所对应的图片绘制到屏幕上了,然后,通过线程来控制一帧一帧的切换,这就是游戏中动画处理的一般过程。
上面这种播放动画的方式的优点是开发简单,便于切换和控制,但是也有如下的一些缺点:
1.存在重复的图片资源
我们可以从原始图片上看到,如果要实现精灵向下走的动画,需要使用4帧图片,但是这4帧图片中只有精灵脚步的图片存在差异,剩下的都大致相同,这就是一种图片上的浪费。而我们的手机内存目前还比较小,因此这种浪费应该是要尽量避免的。
2.动画数据直接存在于代码中
上面的例子中,我们直接把动画的序列以数组的形式保存到了代码中,也就是说,把对动画数据的处理交给了程序员来完成,而程序员的主要工作应该是处理游戏中的算法逻辑,因此,比较好的解决方法是把动画的处理交给美工和策划来完成。因此,上面这种方法也存在缺点。
3.帧图片要求规整
如果采用上面的方法绘制动画,要求每一帧必须按照指定的长度和宽度进行分割,否则就是出现绘制错误,这在一定程度上也限制了程序的灵活性。
4. 难易实现复杂的动画效果
由于帧的尺寸人为的被限定,同时每一个动作就必须要一帧进行显示,如果要想实现复杂的动画序列就意味着要存在大量的帧序列,而原始图片势必要随之变得很大,进而增加了对内存的占用,而在这种情况下,帧数越多,重复的内容也会随之不断增多,显然这也不适合手机这种资源有限的设备的特点。
上面说了这些缺点,那么该如何解决呢?下面就为你介绍一下目前比较流行的一种动画处理方式,也是本文的重点——使用动画编辑工具。
作者介绍李建,乐成数字通信学院 高级讲师。层就职于国内数家SP,CP公司,具有丰富的软件、游戏开发经验。并从事多年教学工作,具有丰富的教学经验。目前主要从事OPhone、J2ME开发和教学方面的工作。
动画编辑器的原理动画编辑器就是一种软件工具,它的作用就是通过软件对原始图片进行切割,然后在软件中把切割好的图片块拼成一帧一帧的图片,再选择相应的图片来组成动画序列,最后将数据导出以文件。
动画编辑器的使用目前,很多的手机游戏公司都在使用动画编辑器来制作动画,而不同的公司使用的工具也不尽相同,下图为一个功能比较强大且使用很广泛的动画编辑工具,界面如下图所示:
图3 动画编辑器界面
接下来我将一步一步的演示如何使用这个工具来制作动画:
1. 首先要建立一个工程,这个同时点击“”按钮引入一张图片,如下图:
图4 角色原始图片
从图中我们看到,原始图片不再是规则的帧序列,精灵图片被一块块的分解了。
2. 下面我们要对图片进行分割了,选中左边栏的“”,然后在图片上进行切割,把乌龟的各个零部件都用方块圈出来,最后的效果如下图:
图5 分割原始图片
3. 接下来就可以用这些“零件”拼图了。首先我们在右上方的窗口中点击“”按钮,接下来就可以看到右侧窗口变成如下图所示的界面:
图6 frame窗口
4. 接下来双击上图中的“1-Frame”,就会出现如下图所示界面:
图7 拼图界面
5. 接下来你就可以在左边的窗口中按住鼠标左键选择你要的乌龟的零部件图片并将其拖入右边窗口的坐标系中,一般将其放置到第一象限,一个拼好的帧图片如下图所示:
图8 帧的拼图
在拼图中,你可以使用左边的一些按钮来实现各个图块的位置和对齐,如图:
图9 布局排列按钮
可以点击“”来回到帧列表界面,同时可以使用“”按键增加新的帧。按照上述方法可以拼出乌龟的上、下、左、右行走等各个帧序列,拼好的效果如下图:
图10 拼好的各个帧
6. 有了帧图片就可以利用它们来组成动画了,方法如下:
菜单中选择“Action——Edit”,效果如下图:
图11 增加动画
图12 动画编辑界面
7. 接下来在“Action Edit”窗口中通过“”按钮增加一个动画,然后就可以从Frame列表中选择相应的帧来组成相应动画了,如下图所示:
图13 设置动画
这样我们就制作好了一个乌龟向下行走的动画,同时你可以点击“”按钮来观察这个动画,并根据效果对动画进行调整,非常方便。
现在我们已经完成了整个动画的制作过程,下面就要将数据进行导出,过程如下图所示:
图14 导出数据
上面介绍了如何使用动画编辑器来设定动画,相信你已经掌握了这个工具的使用了。但是,用这个工具制作的动画并不能直接显示到手机屏幕上,还需要我们通过代码的方式来对其进行处理。下面来介绍一下如何通过代码来绘制响应的动画。
动画播放类——Animation的实现:要想实现对动画的播放必须要有一个动画的解析类,也就是需要对“.sprite”数据文件进行解析,这里我们将这个类命名为SpriteX,这个类的作用是解析数据文件并提供动画的绘制方法,因该类内容较多,此处没有贴出源代码,请参考附件。
精灵类——Role的实现每个精灵类都需要有个SpriteX类的对象用来处理该精灵的动画,因此需要在这个类中定义如下属性:
SpriteX spx;
精灵类的构造方法如下:
这里面的“view”为视图类的对象,稍后会有介绍。
注意:精灵图片需要放置到“res/drawable”文件夹,动画数据要放到“assets”文件夹下面。
精灵类的绘制方法如下:
设计屏幕类
定义好精灵类只是实现了这个项目的第一步,我们还不能在屏幕上看到精灵的动画,我们需要一个视图来把精灵显示出来,接下来我们定义视图类,在这里我们自定义一个类GameView来实现这个功能。作为一个自定义的视图类,这个类需要继承View,关于这个类请参考api文档。这个类用来处理精灵的绘制,按键处理等功能。其构造方法如下:
精灵对象创建完毕后,我们就可以将其绘制到屏幕上了,这里需要重写View类中的onDraw()方法,此方法会在该类对象被Activity调用时自动调用,其方法如下:
动画的刷新
在GameView类中需要处理动画的刷新,也就是每隔一段时间来播放动画的下一帧,因此我们用线程来实现这个功能,线程的run方法如下所示:
在这里要注意给线程适当的休眠时间,用来控制FPS,这样有利于得到更好的游戏体验。
注意:SPLASH_RATE就是每个线程的间隔时间
定义Activity最后我们定义一个Activity的子类用来运行该游戏,在Activitiy中我们需要创一个GameView对象,然后通过setContentView()方法进行设定,代码如下:
到此,我们已经完成了所有的功能,你可以运行模拟器并观察其效果了。
总结使用动画编辑工具的方法可以非常方便的开发复杂的动画效果,尽可能的节约有限的内存资源并且使程序员和策划、美工的工作相分离,是现在手机端2D游戏开发普遍采用的一种高效、方便的方法。
我们介绍了常见的各种游戏特效的实现,你现在可以很轻松的实现各种游戏中所需要的特效,但是,你可能已经意识到了,我们的游戏一般都需要进行碰撞检测,比如前面的火柴棍小人,我们需要检测子弹和敌人之间的碰撞;碰撞检测通常是游戏开发的难点,作为引擎必然少不了碰撞检测部分,这里我们还是按照cocos2d的构架,使用Box2d作为物理引擎,下面我们将通过在Ophone平台实现一个小游戏,来对Box2d物理引擎进行学习。
Box2d
Box2D是一个用于游戏的2D刚体仿真库,它可以使物体的运动更加真实,让游戏场景看起来更具交互性。2D物理引擎能增强游戏世界中物体如多边形(砖块,三角形,多边形)的动作的真实感从而提高游戏质量。该引擎通过用户设定的参数如重力,密度,摩擦,弹性等参数计算碰撞,角度,力和动力等。这些计算需要大量的数学,物理等知识,如果有兴趣也可以下载其源码来研究。
Box2d同时也提供了各种语言环境的实现,由于Ophone平台使用JAVA作为变成语言,所以我们将选择使用Box2d的java版JBox2d,这也将产生一个问题,JBox2D是用processing库来处理图像显示,所以Ophone平台上则不适用,在Ophone平台上的图像渲染主要包括两种:Canvas和Opengl ES,因此我们可以任选其中一种,这里为了配合我们的引擎实现,选择通过Opengl ES来作为渲染部分,这部分就需要我们自己来实现,其实我们也可以不使用其图像渲染部分,因为我们主要是使用Box2d来做物理检测,稍后我们会通过一个实例游戏来介绍。
另外,比较优秀的2D物理引擎还有Chipmunk,对于谁好谁坏,我们这里不去评价,如果要使用Chipmunk作为物理引擎会比Box2d稍微苦难一些,因为Chipmunk目前没有Java版本,所以只能通过JNI方式来使用,这就需要使用NDK来开发原生的C程序,使用C语言来做,效率要高很多,但是开发,调试的难度也将增加,有机会我们将可以介绍如何使用NDK来编写C程序,并同时整合Chipmunk物理引擎。
这里只是我们对Box2d的一个简单介绍,让大家明白其用处,关于更多详细信息,大家可以参考其官方网站http://www.box2d.org/,图12-1则是cocs2d中演示的Box2d物理引擎效果,学完这部分内容,你也可以很轻松将其运行在Ophone平台上。
图12-1中这每个方块都具有重力,摩擦力,碰撞检测规则,他们都处于同一个世界场景中,不必眼红iPhone开发者,下面就给大家看一下,我们在Ophone平台提供的示例物理小游戏。
在学习使用Box2D引擎之前,我们需要了解一下一些常用的概念:
刚体(rigid body)
一块十分坚硬的物质,它上面的任何两点之间的距离都是完全不变的。它们就像钻石那样坚硬。我们用物体(body)来代替刚体。
形状(shape)
一块严格依附于物体(body)的 2D 碰撞几何结构(collision geometry)。形状具有摩擦(friction)和恢复(restitution)的材料性质。
约束(constraint)
一个约束(constraint)就是消除物体自由度的物理连接。在 2D 中,一个物体有 3 个自由度。如果我们把一个物体钉在墙上(像摆锤那样),那我们就把它约束到了墙上。这样,此物体就只能绕着这个钉子旋转,所以这个约束消除了它 2 个自由度。
接触约束(contact constraint)
一个防止刚体穿透,以及用于模拟摩擦(friction)和恢复(restitution)的特殊约束。你永远都不必创建一个接触约束,它们会自动被 Box2D 创建。
关节(joint)
它是一种用于把两个或多个物体固定到一起的约束。Box2D 支持的关节类型有:旋转,棱柱,距离等等。关节可以支持限制(limits)和马达(motors)。
关节限制(joint limit)
一个关节限制(joint limit)限定了一个关节的运动范围。例如人类的胳膊肘只能做某一范围角度的运动。
关节马达(joint motor)
一个关节马达能依照关节的自由度来驱动所连接的物体。例如,你可以使用一个马达来驱动一个肘的旋转。
世界(world)
一个物理世界就是物体,形状和约束相互作用的集合。Box2D 支持创建多个世界,但这通常是不必要的。
这里先给大家介绍就是让大家明白Box2d包括哪些内容,稍后对框架的介绍时就能更加容易理解,当然对于这些具体的功能,我们会在后面跟着示例代码一起学习。
Ophone Box2d
首先分析一下我们在Ophone平台上的Box2dDemo需要实现什么功能,首先我们将整个屏幕构建成一个盒子,然后再盒子中设置各种障碍,当我们触摸屏幕上任意位置时,就释放一个当前选择的物体,然后该物体将受到重力等因素的影响开始运动,直到最后静止下来。运行效果如图12-2所示。
屏幕中间的长条则是我们设置的障碍,而圆形和矩形都是我们在点击屏幕时要释放的物体,前面我们说过,JBox2d中的图形部分在Ophone中不能用,所以我们会专门介绍如何通过Opengl ES来对图形图像进行渲染,另外,该示例中的这些物体都是通过纹理映射来将图片映射到四边形上。为了大家能掌握图形系统相关内容,我们还实现了一个功能,玩家可以自己设置障碍,只需要点击Menu中,选择"编辑模式"就可以进入障碍编辑状态,如图12-3所似。
当障碍编辑完成之后再次选择编辑模式,则恢复到游戏中,此时我们所编辑的这些障碍都会正常的运行。当然该过程我们并没有使用引擎来完成,目的在于让大家更清楚渲染的原理,以及代码能够更多的重用,也就是说,大家可以直接拷贝代码到需要的游戏中去即可。同时大家也能掌握很多Opengl ES的相关知识。
Ophone平台如何使用JBox2d
要使用JBox2d我们首先需要获得其源码或者jar包,这个就不用多说了,知道其官方网站下载即可,这里我们下载了一个完整版本jbox2d-2.0.1-full.jar,让后将其放入我们所建立OphoneBox2d工程的lib文件夹下,JBox2d中大致包含了如图12-4所示的一些包:
图12-4 JBox2d结构图
其中org.jbox2d.collision比较重要,主要负责处理碰撞相关,包括对一些多边形的实现,这里所说的多边形主要是一些数据,比如多边形的位置,大小,重力,形状,质量等属性;org.jbox2d.common包主要用来设置一些全局的属性(Setting.java),调试时所使用的颜色(Color3f.java),以及其他的一些数学相关的内容,因为我们说了Box2d他主要不是来做渲染的,但是有时候我们需要知道所设置的这些物体是否正确,进行调试,就需要绘制这些简单的图形,并显示出来,供我们调试;org.jbox2d.dynamics包主要负责动力学相关的内容,下面是常见的功能包描述。
org.jbox2d.collision包
AABB:AABB坐标
OBB:OBB坐标
ContactID:接触ID
ContactPoint:接触点
ManifoldPoint:繁殖点
Segment:线段
Shape:外形基类
ShapeDef:外形定义基类
CircleDef:圆外形定义
CircleShape:圆外形
FilterData:碰撞过滤器
MassData:质量运算器
PolygonDef:多边开定义
PolygonShape:凸多边形
org.jbox2d.common包
Color3f:调试绘图颜色
Settings:全局设置
Mat22:2*2 矩阵
Sweep:碰撞描述
Vec2:向量(x ,y)
XForm:坐标转换,平移或旋转
标准的版本中还会存在Mat33表示3*3的矩阵和Vec3向量(x,y,z),该java版本中没有出现这些。
org.jbox2d.dynamics包
Body:刚体或叫物体
BodyDef:刚体定义
BoundaryListener:世界边界侦听
ContactFilter:继承这个类用来获取过滤碰撞
ContactListener:继承这个类用来获取碰撞结果
DebugDraw:调试绘图,用于调试
DestructionListener:关节或外形销毁时处理方法
World:物理世界
org.jbox2d.dynamics.contacts
Contact:管理两个外形接触
ContactEdge:接触边用来连接多个物体和接触到一个接触表
ContactResult:记录接触结果
org.jbox2d.dynamics.Joints
DistanceJoint:距离校正器
DistanceJointDef:距离连接定义
GearJoint:齿轮
GearJointDef:齿轮连接定义
Joint:连接基类
JointDef:连接定义基类
JointEdge:用于组合刚体或连接到一起.刚体相当于节点,而连接相当于边
MouseJoint:鼠标连接
MouseJointDef:鼠标连接定义
PrismaticJoint:棱柱连接
PrismaticJointDef:棱柱连接定义
PulleyJoint:滑轮连接
PulleyJointDef:滑轮连接定义
RevoluteJoint:旋转连接
RevoluteJointDef:旋转连接定义
org.jbox2d.testbed:主要是一些用来测试的程序
添加JBox2d到Ophone项目中
要在工程中使用JBox2d库,需要将JBox2d添加到工程中,添加方法如下:
右键单击工程,选择"Properties",进入项目Properties界面。
选择"Java Build Path",选择"Libraries"选项卡。
在点击"Add Jars..."按钮,添加Jar。
选择当前工程中我们之前放入lib文件夹中的jbox2d-2.0.1-full.jar文件,如图12-5所示,单击"确定"按钮即可。
OphoneBox2d框架
现在工程的结构展开应该如图12-6所示。
其中实现该工程的文件如下:
Box2dTest:工程Activity,入口
GameGLSurfaceView:游戏GLSurfaceView
GLRenderer:Opengl es渲染器
DrawObject:使用Opengl ES来绘制常用图形(矩形,圆形)
PhysicsWorld:物理世界场景
OphoneBox2d实现
开始分析代码之前,我们先确定一下需要准备的资源图片,从图12-2所示,我们可以看出,多少需要一个矩形和一个圆形的图片(当然也可直接指定颜色绘制矩形和圆形),这里我们将使用图片来进行纹理映射,该工程所需要的纹理图片如图12-7所示。
Box2dTest实现
该类继承自Activity,将作为本程序的入口,授予我们是通过Opengl ES来渲染的,所以构建需要构建一个GLSurfaceView对象作为Opengl ES的窗口,然后通过setContentView函数来设置显示该窗口视图。然后分别在onPause和onResume函数中调用GLSurfaceView类的GLSurfaceView。当然这也是所有Opengl ES程序的渲染基础框架,所有的Opengl ES程序窗口都由GLSurfaceView来实现。具体实现入代码清单12-1所示。
代码清单12-1:Box2dTest.java
复制到剪贴板 Java代码
public class Box2dTest extends Activity { private GLSurfaceView mGLView; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //构建GLSurfaceView视图 mGLView = new GameGLSurfaceView(this); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); //设置GLSurfaceView视图 setContentView(mGLView); } @Override protected void onPause() { super.onPause(); mGLView.onPause(); } @Override protected void onResume() { super.onResume(); mGLView.onResume(); } //创建按钮 public boolean onCreateOptionsMenu(Menu menu) { menu.add(Menu.NONE, 1, Menu.NONE, "编辑模式"); menu.add(Menu.NONE, 2, Menu.NONE, "选择模型"); return true; } //按钮事件处理 public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case 1: //切换编辑模式 ((GameGLSurfaceView) mGLView).toggleEdit(); return true; case 2: //选择模型 ((GameGLSurfaceView) mGLView).toggleModel(); return true; } return false; } }
前面我们说了,可以通过Menu来切换模式和选择模型,因此我们在onCreateOptionsMenu中添加了两个菜单选项,其中"编辑模式"用来切换编辑状态和游戏状态,"选择模型"则用于选择我们触摸屏幕时所释放的模型,这里主要包括矩形和圆形,因此在菜单事件处理函数onOptionsItemSelected中,我们分别对两个菜单选项进行了处理,具体实现位于GameGLSurfaceView中。
GameGLSurfaceView实现
要实现一个用于显示Opengl ES窗口程序的视图,Ophone为我们提供了GLSurfaceView类,因此我们的GameGLSurfaceView类可以继承自GLSurfaceView很轻松的实现Opengl ES视图,该视图中有一个重要的部分,那就是Renderer,每一个Opengl ES窗口视图都需要一个渲染器来负责渲染。本例中的渲染器GLRenderer直接通过继承自Renderer来实现,GLSurfaceView中可以通过setRenderer来设置一个自定义的渲染器。具体实现入代码清单12-2所示。
代码清单12-2:GLSurfaceView.java
复制到剪贴板 Java代码
public class GameGLSurfaceView extends GLSurfaceView { GLRenderer mRenderer; public GameGLSurfaceView(Context context) { super(context); mRenderer = new GLRenderer(context); setRenderer(mRenderer); } public void toggleEdit() { mRenderer.toggleEdit(); } public void toggleModel() { mRenderer.switchModel(); } public boolean onTouchEvent(final MotionEvent event) { mRenderer.setSize(this.getWidth(), this.getHeight()); //线程通信 queueEvent(new Runnable() { public void run() { mRenderer.touchEvent(event.getX(), event.getY(), event.getAction()); } }); // sleep try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } return true; } }
该窗口类的实现非常简单,基本上将所有需要处理的情况都转交给了Renderer,这里一个需要值得注意的地方,我们在 GameGLSurfaceView.onTouchEvent()中使用了queueEvent()方法。queueEvent()主要用于在UI线程和渲染线程(Renderer)间通信。当然也可以用其他的Java线程通信技术, 如synchronized方法,AsyncTask,Handler等,但queueEvent是最简单的线程通信方法。另外Android中View的线程通信还可以使用postInvalidate()函数来实现重绘操作。
总结
本文主要介绍了开源2D物理引擎Box2d的一些功能特征,以及如何在Ophone平台上使用Box2d的java版JBox2d;同时我们完成了使用该物理引擎实现的小游戏的框架,接下来我们就将通过完整实现该游戏的过程来学习使用JBox2d作为游戏开发中的物理引擎部分,同时大家可以考虑如何使用JNI来使用Box2d开发原生程序,加油哦,下一篇文章我们就将做出一个物理效果很酷的游戏来了。由于分享经验的心情急切,难免会出现一些疏忽或错误,还望不吝赐教!
<item name="tabWidgetStyle">@android:style/Widget.TabWidget</item> <style name="Widget.TabWidget"> <item name="android:textAppearance">@style/TextAppearance.Widget.TabWidget</item> <item name="ellipsize">marquee</item> <item name="singleLine">true</item> </style> <style name="TextAppearance.Widget.TabWidget"> <item name="android:textSize">14sp</item> <item name="android:textStyle">normal</item> <item name="android:textColor">@android:color/tab_indicator_text</item> </style>