游戏从出现以来,一直提升着玩家的需求,游戏开发者不止是考虑简单的位置移动,例如将游戏中各种演员做成动画,就可以大大提升游戏的品质,在咱们的这个三国统帅的游戏中,小兵和将领是有各种动作的,这些动作对应动画,让游戏的互动感觉更加优秀,这次使用cocos2d-xna中的CCAnimate来实现角色动画。
首先我们要先准备游戏中所需要的动画资源,按照设计,游戏中至少有这样的角色和职业:主将、小兵(步兵、枪兵、骑兵、弓兵),它们的各种所扮演的角色因所属势力只是样子不一样。
角色的动画祯按照一个规则命名,这样就能方便的管理:
{id}_{n}表示的是正方向,而{id}f_{n}表示的是反方向。比如说
A1表示的是角色的id,按照编号的显示动作分别为:
0-3:攻击动作
4-5:行走动作
6-6:站立动作
7-8:死亡动作
在这个实验游戏中,我们有两个势力对阵,分别为义军和黄巾军,按照这样的规则制作了如下的资源:
A1:黄巾军步兵
A2:黄巾军枪兵
A3:黄巾军骑兵
A4:黄巾军弓兵
B1:义军步兵
B2:义军枪兵
B3:义军骑兵
B4:义军弓兵
另外还加了两个英雄:
Hero02:关羽
Hero11:张角
然后将他们的各种帧制作完成并命名完毕(这里也许你需要美术的帮助,我已将其完成,可以在最终的文件中浏览),用TexturePackerGUI打包。
图片包下载地址:http://files.cnblogs.com/nowpaper/SanguoCommander5_actors.rar
发布一下它,保存成一个plist和png图,命名为ActorsPack1,以后也许有Pack2,所以单独分开保存:
最终的plist文件放入工程Content下,可以建立一个例如plist的文件夹,并且将.plist文件的内容管线设置正确:
这张图并不正确,你应该建立子文件夹Images,并将ActorsPack1.png放入其中。
好了,下一步就可以开始对工程进行代码编写了。
从游戏本身的设计经验而言,最好的方式是数据驱动逻辑,所以,当我们描写一个角色动画类的时候,最先有一个比较明确的数据类,这里我们可以将游戏底层角色的复杂数据包含,并且做处理,例如角色最基本的特性、职业、类型等等:
enum ActorType { None, Soldier,Hero } enum ActorPro { None, Infantry, Pikeman, Cavalvy, Archer } enum ActorDir { None, Right, Left } class ActorData { public ActorData(string id) { ActorID = id; } //演员id public string ActorID { get; private set; } //演员分组 public string GroupID { get; private set; } //类型 public ActorType ActorType { get; private set; } //职业 public ActorPro ActorPro { get; private set; } //获得一个数据 public static ActorData getActorData(string id, string groupid, ActorType type, ActorPro pro) { ActorData data = new ActorData(id); data.GroupID = groupid; data.ActorType = type; data.ActorPro = pro; return data; } }
在最上面,我们定义了三个枚举,分别来表示类型、职业和方向,方向是用来做动画用的,而类型和职业则是基本数据的需求,大家可以注意到,设计了一个getActorData静态方法,用来方便的制作基本测试数据,因为按照游戏数据的处理惯例而言,这些数据都是通过表单的方式配置,程序读取配置解析成为程序数据,方便游戏设计师随时调整。
下一步就是建立动画类了,我们使用类继承的方式抽象动画类,建立ActorBase类,这个类只是管理角色最基本的动画,继承自CCSprite,通过CCAnimate行为来控制动画:
class ActorBase : CCSprite { public ActorData ActorData { get; private set; } private CCAnimate _action_attack; private CCAnimate _action_attack_flip; private CCAnimate _action_run; private CCAnimate _action_run_flip; private CCAnimate _action_stand; private CCAnimate _action_stand_flip; private CCAnimate _action_dead; private CCAnimate _action_dead_flip; public ActorDir ActorDir { get; set; } public ActorBase(ActorData data) { ActorData = data; //创建攻击动画 List _attackFrames = new List(); List _attackFrames_flip = new List(); for (int i = 0; i < 4; i++) { _attackFrames.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "_" + i + ".png")); _attackFrames_flip.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "f_" + i + ".png")); } _action_attack = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_attackFrames, 0.1f)); _action_attack_flip = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_attackFrames_flip, 0.1f)); //创建行走动画 List _runFrames = new List(); List _runFrames_flip = new List(); for (int i = 4; i < 6; i++) { _runFrames.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "_" + i + ".png")); _runFrames_flip.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "f_" + i + ".png")); } _action_run = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_runFrames, 0.1f)); _action_run_flip = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_runFrames_flip, 0.1f)); //创建站立动画 List _standFrames = new List(); List _standFrames_flip = new List(); for (int i = 6; i < 7; i++) { _standFrames.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "_" + i + ".png")); _standFrames_flip.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "f_" + i + ".png")); } _action_stand = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_standFrames, 0.2f)); _action_stand_flip = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_standFrames_flip, 0.2f)); //创建死亡动画 List _deadFrames = new List(); List _deadFrames_flip = new List(); for (int i = 7; i < 9; i++) { _deadFrames.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "_" + i + ".png")); _deadFrames_flip.Add(CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName(data.ActorID + "f_" + i + ".png")); } _action_dead = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_deadFrames, 0.3f)); _action_dead_flip = CCAnimate.actionWithAnimation(CCAnimation.animationWithFrames(_deadFrames_flip, 0.3f)); //初始化默认帧 base.initWithSpriteFrame(_standFrames[0]); } CCAction _currentAnimateAction; public void StateToRun() { if(ActorDir == Roles.ActorDir.Left) RunAnimateAction_RepeatForever(_action_run); else RunAnimateAction_RepeatForever(_action_run_flip); } //攻击状态 public void StateToAttack() { currentAnimateActionStop(); if (ActorDir == Roles.ActorDir.Left) RunAnimateAction_RepeatForever(_action_attack); else RunAnimateAction_RepeatForever(_action_attack_flip); } //死亡动画 public void StateToDead() { currentAnimateActionStop(); if (ActorDir == Roles.ActorDir.Left) _currentAnimateAction = runAction(_action_dead); else _currentAnimateAction = runAction(_action_dead_flip); } //站立动画 public void StateToStand() { if (ActorDir == Roles.ActorDir.Left) RunAnimateAction_RepeatForever(_action_stand); else RunAnimateAction_RepeatForever(_action_stand_flip); } //停止当前的动画 private void currentAnimateActionStop() { if (_currentAnimateAction != null) this.stopAction(_currentAnimateAction); } //播放循环动画的统一方法 private void RunAnimateAction_RepeatForever(CCAnimate action) { currentAnimateActionStop(); _currentAnimateAction = runAction(CCRepeatForever.actionWithAction(action)); } }
上述代码加了一些注释,希望能够帮助你的阅读,动画行为的制作流程是这样的:
首先我们要知道有那些帧,它们形成的集合变成CCAnimation的处理类,然后CCAnimate将其加载并形成特定的动画行为,有兴趣的配有可以看cocos2d的底层代码,CCSprite实际上是带了一个CCTexture来表示图像,CCAnimate是按照逻辑变化CCTexture。
在下面的代码中:StateToRun、StateToAttack、StateToDead、StateToStand等方法都是用来处理角色状态的动画,例如对应到当攻击的时候调用StateToAttack方法。
currentAnimateActionStop和RunAnimateAction_RepeatForever是为了方便处理动画的特定状态,因为诸如行走、站立、攻击,这一类的动画都是循环性质,统一代码比较方便。
那么我们就测试一下上面的代码看看直接的效果,还是为了方便,在本节中,我们直接将动画放在了开始界面这样浏览很方便了,打开SceneStart.cs,在构造函数中写如下代码:
//测试动画的角色 List id_buff = new List() { "B1","B2","B3","B4","Hero02","A1","A2","A3","A4","Hero11" }; for (int i = 0; i < 2; i++) { for (int j = 0; j < 5; j++) { var actor = new ActorBase(new ActorData(id_buff[i*5 + j])); actor.ActorDir = (ActorDir)(i + 1); actor.position = new CCPoint(64 + i * 64, 64 + j * 64); if(j % 2 ==1) actor.StateToRun(); else actor.StateToAttack(); this.addChild(actor); } } //
上面代码里,用一个List结构id_buff来描述ID,其实就是角色的前缀名,然后按照顺序排成两列,并且按照单数为行走动画,双数为攻击动画的形式显示。
上面是运行测试的效果,后面我们会将它们加到游戏界面中,让游戏真正的可以玩起来。
本篇例子工程:https://github.com/Nowpaper/SanguoCommander_cocos2dxna_Sample
本例子项目名为:SanguoCommander6