回合制战斗设计



回合制战斗设计一 前端基本设定

回合制战斗设计一 前端基本设定


回合制战斗设计一 前端基本设定

前端需要考虑的问题
1.资源:     解析战斗配置,加载各种资源文件
2.人物动作     待命     普通攻击     技能性攻击     待命+buff效果     被攻击动作     被攻击+Buff效果
3.特效处理     伤害显示     技能效果     暴击等测试
人物的动作总量如果不多时可以放在一个swf里的buff效果和伤害显示,技能效果以及暴击等处理可以放在一个资源文件中。


回合制战斗设计二 动画资源处理

回合制战斗设计二 动画资源处理[本人原创,转载请注明出处]前端动画资源主要分为特效和人物动作两类。基础的人物动作有:攻击,被攻击,站立。特效又可分为:攻击特效和buff特效
按照播放的为单次播放还是循环播放来分:人物动作的攻击,被攻击,攻击特效等属于单次播放buff特效,人物站立属于循环播放,并且对于特效应该可以指定持续时间。
考虑设计一个IAnimate对象代表所有动画资源,故此接口有以下属性:function play():void;function stop():void;function set loop(value:Boolean):void;function set duration(value:int):void;
在我们继续往下写代码之前,我需要考虑一个问题,动画资源是什么样的?目前很多webgame和socialgame使用是Flash二维的动画,人物动作是由美术画好,导出为swf方式,再有程序加载。但是越来越多有开发商开始使用3d建模的方式来处理动画资源,使得整体的画面效果有这比较大提升,未来的主流应该3d方式的动画资源,因为有着更好的跨平台性,后面我会具体说明。先说一下3d建模美术动画资源是什么样的,美术工程师在3dmax的等中设计好美术资源之后,对人物的每一个动作会导出一个序列图。假设我们有一个人物naruto,攻击动作会由一系列的png图片按攻击动作次序导出。通过对这些序列图播放,我们就可以实现动画。就Flash而已,通过使用Flash的扩展脚本JSFL,可以实现将序列图组合到一个动作中去。具体的方法可以参考一下: http://bbs.9ria.com/archiver/tid-26288.html
另外多个序列图操作起来可能不方便,可以考虑将动作序列图拼接成一个动作图,用脚本(比如说python PIL库)是很容易做到的,但是要记录每个图的大小,并且按次序拼接。年度页游神仙道人物动作将单个人物的所有动作放在一个图里的,如果人物动作比较多的时候,并且不会都被使用情况下,建议还是分拆。区别主要是在资源加载处理上。
假设我们已经生成好一个动作的swf,比如naruto_attack.swfswf里是动作图(多个序列图合成一个图),此swf中需要实现一个数据接口,获得单张序列图的高度和宽度。IAnimateData:function get imageCount():Number;function get animateContent():BitmapData;//动作图
我们要做的事情就是将动作图切图,还原成多个序列的BitmapData,然后通过Timer,来播放这些动作。我们的攻击动作对象:public class Animate extends Sprite implements IAnimate{
     private var _data:IAnimateData;
     private var _bitmap:Bitmap;//实际显示对象
     private var _frames:Array;//存储序列图
     private var _frameRate:int=12;//播放速度
     private var _timer:Timer;
     private var _currentFrame:int;
     private var _loop:Boolean;
     private var _duration:int=-1;
    
     public function Animate(data:IAnimateData,frameRate:int):void{
          _bitmap=new Bitmap(null,"auto",true);
          this.addChild(_bitmap);
          _frames=[];
          _data=data;
          _frameRate=frameRate;
          _timer=new Timer(1000/frameRate);
          _timer.addEventListener(TimerEvent.TIMER,onEnterFrame,false,0,true);
          init();
     }
     public function init():void{
          var count:int=this._data.imageCount;
          var content:BitmapData=this._data.animateContent;
          var h:int=content.height;
          var w:int=content.width/count;
          var bitmapData:BitmapData;
          for(var i:int=0;i                bitmapData = new BitmapData(w, h, true, 0);
               bitmapData.copyPixels(content, new Rectangle(i*w, 0 , w, h), new Point());
               this._frames.push(bitmapData);
          }
     }
     public function onEnterFrame(e:TimerEvent):void{
          this._bitmap.bitmapData= this._frames[this._currentFrame++];
          if(this._duration>0){
               this._duration-=1;
               if(this._duration<=0){
                    this.stop();
               }
          }
          if (this._currentFrame == this._frames.length){
               this._currentFrame = 0;
               if (!this._loop && this._duration<0){
                    this.stop();
               }
          }
     }
....



回合制战斗设计三 人物动作处理之普通攻击

回合制战斗设计三 人物动作处理之普通攻击[本人原创,转载请注明出处]上一节我们设计了如何控制创建资源以及动画的播放处理,这里我们要把动作组合到人物对象中去。设定Soldier为人物对象,继承自Sprite,有如下属性var soldierName:String;var health:int;var maxHealth:int;var attack:IAnimate;//攻击var defensee:IAnimate;//防御var stand:IAnimate;//待命
我们设计Soldier是通过接受指令来控制自己的动作,这个指令是一个对象Commandfunction accpet(cmd:Command):void;//接受指令
Command对象的属性var name;//指令名称,比如attack
另外我们可以添加多个常量cmd在Command类中:static var ATTACK:String="attack";static var DEFENCE:String="defense";static var STAND:String="stand";
这样通过Command可以控制Solider动作了。我们分析一下,如果SoliderA攻击SoldierB改怎么处理。首先SoliderA接受了一个命令,攻击SoldierB,对于Command我们再加入一个target:SoldierCommand:var name;var target:Soldier;//被攻击者
我们知道,SoldierA的动作是攻击,然后是SoldierB的动作被攻击。这又有一个问题,A攻击之后,B的动作什么时候开始播放呢?通常设计就是在A的攻击动作播放完成之后,马上开始播放B的动作。或者在A的动作播放到最后2帧之前,开始播放B的动作。这样都可以达到一定流畅性。这两种方法本质上都是需要我们了解到A的动作播放细节,可以监听A播放时的事件。我们先采用简单实现方式,也就是A动作播放结束的时候,添加一个监听,开始调用B播放。对于待命动作,A和B都在动作结束之后回归到待命动作。由此,IAnimate 还需要添加一个listener,监听stop事件function addStopListener(onStop:Function):void对于Animate的实现:我们添加一下代码:var _onStop:Function;public function addStopListener(onStop:Function):void{
     this._onStop=onStop;
}

在stop()内添加: this._onStop && this._onStop(); this.onStop=null;//再次赋值为null
对于Soldier:添加一个_currentCmd:Command;//用于记录当前正在执行的命令添加一个onStop方法:function onStop():void{     if(_currentCmd!=null ){         accpet(new Command(Command.STAND));//始终回到待命状态         if(_currentCmd.name==Command.ATTACK){               _currentCmd.target.accpet(new Command(Command.DEFENSE));          }     }}
另外,accpet()方法可以对Command.STAND特殊处理一下,如果为STAND命令,_currentCmd始终为null这个一个攻击动画我们就完成了。

回合制战斗设计四 资源加载和Soldier初始化及攻击动画的实现

回合制战斗设计四 资源加载和Soldier初始化及攻击动画的实现[本人原创,转载请注明出处]如前文所述,Soldier下有多个动作资源需要加载。如果战斗人物比较多,就需要提前加载战斗相关的资源。
我们首先要构造资源的格式,一个动作的SWF的名字可以为:id_action.swf,比如naruto_attack.swf,表示为
naruto的攻击动作swf。
我事先使用神仙道资源做了一个naruto的3个动作,naruto_attack.swf,naruto_defense,naruto_stand.swf.
如何做这个资源swf呢?使用JSFL可以批量生成SWF的,并且要保持一致性。
目前这三个swf都是手动编辑的,内部都是导出BMP为AnimateData对象,并且实现IAnimateData接口。每个动作图片的大小和序列数量是不一样的,故实际上,每个动作都需要单独创建一个AnimateData对象,用来生成各自动作的SWF,这些使用脚本+JSFL是可以实现的,难度实际上也不大,后续如果有时间我会把这个脚本贡献出来,当然也欢迎牛人贡献一下。
BulkLoader资源加载如下:
bulkloader=new BulkLoader("fight");
bulkloader.addEventListener(BulkProgressEvent.COMPLETE,onComplete);
bulkloader.add("assets/naruto_defense.swf",{"id":"naruto_defense"});
bulkloader.add("assets/naruto_attack.swf",{"id":"naruto_attack"});
bulkloader.add("assets/naruto_stand.swf",{"id":"naruto_stand"});
bulkloader.start();
另外为了方便的获得资源中类,我定义两个一个AnimateLoader对象
AnimateLoader
var _loader:BulkLoader
public function AnimateLoader(loader:BulkLoader){
this._loader=loader;
}
public function getAssetClass(id:String):Class{
var domain:ApplicationDomain = this._loader.getMovieClip(id).loaderInfo.applicationDomain;
if(domain) return domain.getDefinition("AnimateData") as Class;//资源中对BMP进行导出
return null;
}
//这里在compete的时候,我生成了2个Soldier
function onComplete(e:BulkProgressEvent):void{
soldierA=new Soldier();
soldierA.init(animateLoader,"naruto");
this.addChild(soldierA);
soldierA.x=400;
soldierB=new Soldier(Soldier.RIGHT);
soldierB.init(animateLoader,"naruto");
soldierB.x=300;
this.addChild(soldierB);
}
考虑到人的朝向,Soldier添加direction来出来朝向问题
static var LEFT:int=1;//面朝左
static var RIGHT:int=-1;
vardirection:int;
init方法如下:
public function init(loader:AnimateLoader,id:String):void{
var AssetClass:Class;
var animate:Animate
for each(var action:String in actions){
AssetClass = loader.getAssetClass(id+"_"+action);
animate=new Animate(new AssetClass() as IAnimateData);
actionDict[action]=animate;
animate.scaleX=direction;
if(action==Command.STAND){
animate.loop=true;
}else{
animate.addStopListener(onStop);
}
}
accept(Command.create(Command.STAND));//默认为待命状态
}
actionDict就是普通的Object对象,用来保存所有动作引用。
对于Soldier动作名都由actions数组保存。
static var actions:Array=[Command.ATTACK,Command.DEFENCE,Command.STAND];
这里注意:
1 STAND需要loop为true
2 使用scaleX处理direction
3 对于非Stand动作,添加onStop的监听
有了Soldier A和B就能实现A攻击B
我们加一个键盘监听:
this.stage.addEventListener(KeyboardEvent.KEY_UP,onKeyUp);
function onKeyUp(e:KeyboardEvent):void{
var code:int = e.charCode;
var cmd:Command;
switch(code){
case 97://按键a
cmd=new Command(Command.ATTACK,soldierB);
soldierA.accept(cmd);
break;
default:
break;
}
}

这样就能实现一个普通攻击动画了,虽然目前现在看起来还是隔空打击。


回合制战斗设计五 自动回合的战斗设计

[本人原创,转载请注明出处]

前面我们已经可以控制两个玩家的交互动作了,下面我们可以考虑战斗了。
回合制一个基本特点是轮询机制
1.选择攻击者
2.选择被攻击者
选定这两个soldier之后,就可以构成一个攻击回合。
对于自动回合制,这部分处理通常在后端处理,对于交互性的可能要更为复杂点。
先考虑自动回合制,自动回合意味着前端仅仅播放动画,执行后端已经生成好的Command。
我们设定一个Battle对象用于处理战斗过程。
基本逻辑:Battle双方为A和B(左右各一个Soldier),其中一个Health<=0,回合结束,显示战斗结果。
1.初始化A,B站位(后面我们会加入阵型处理)
2.执行后端的命令
3.显示战斗结果
Battle中Soldier有两个,攻击方和被攻击方法,我们是A和B来区分
var soldierDict={};
soldierDict['A']=soldierA;
soldierDict['B']=soldierB;
soldierA和soldierB的生成请参考前面文章。
一系列的cmd数据我们定义为一个数据
var cmdTestData:Array=[
{"attacker":"A","target":"B","hurt":310,"cmd":"attack"},
{"attacker":"B","target":"A","hurt":90,"cmd":"attack"},
{"attacker":"A","target":"B","hurt":370,"cmd":"attack"},
{"attacker":"B","target":"A","hurt":360,"cmd":"attack"}//这个CMD执行完之后,B应该挂了
]
动画播是按次序的播放的,我们将Command数据解析成单个动作数据,即ActionItem数据ActionItem结构:private var mSoldiers:Array;//动作执行者,可能有多个。如果是群攻,防御者将为多个
private var mActionType:String;//动作类型
对于回合制战斗,一个Command通常包括两个ActionItem,一个是攻击者,另外一个是防守者。更为复杂的情况我们后面再讨论。//解析命令数据为ActionItempublic function interpret(data:Array):void{
               var cmdData:Object;
               var attacker:Soldier;
               var defenser:Soldier;
               var item:ActionItem;
               for each(cmdData in data){
                    attacker=soldierDict[cmdData["attacker"]];
                    defenser=soldierDict[cmdData["target"]];
                    item=new ActionItem([attacker],ActionItem.Atk);
                    this.actionItems.push(item);
                    item=new ActionItem([defenser],ActionItem.Def);
                    this.actionItems.push(item);
               }
          }}解析为之后需要执行这些ActionItem
对于之前Animate和Soldier对象我们需要适当重构一下首先,我们将Animate中对动画的数据处理提取出来,放入到ActionData中,并在ActionData保存
ActionData:     private var mAnimateData:Object;
     private var mId:String;     private var mFrames:Array;
     public function ActionData(data:Object,id:String){
          this.mAnimateData=data;
          this.mId=id;
          this.mFrames=new Array();
          init();
     }
     private function init():void{
          var count:int=this.mAnimateData.imageCount;
          var content:BitmapData=this.mAnimateData.animateContent;
          var h:int=content.height;
          var w:int=content.width/count;
          var bitmapData:BitmapData;
          for(var i:int=0;i                bitmapData = new BitmapData(w, h, true, 0);
               bitmapData.copyPixels(content, new Rectangle(i*w, 0 , w, h), new Point());
               this.mFrames.push(bitmapData);
          }
     }
Animate只处理动画播放,同时添加一个帧播放结束的函数回调。重构后的主要代码如下:protected static var mActionDataDict:Object={};public function Animate(frameRate:int=12):void{
     this.mBitmap=new Bitmap(null,"auto",true);
     this.addChild(mBitmap);
     this.mFrameRate=frameRate;
}

protected function onTimer(e:TimerEvent):void{
     this.mBitmap.bitmapData= this.mFrames[this.mCurrentFrame++];
     if (this.mCurrentFrame == this.mFrames.length){
          this.mCurrentFrame = 0;
          if (!this.mLoop){
               this.stop();
          }
          //very important:flash是顺序执行的,最好是将onTimer执行结束之后再调用
          if(this.mFrameEndAction is Function){
               this.mFrameEndAction();
          }
     }
}
private function stop():void{
     if(this.mTimer){
          this.mTimer.stop();
     }
}

public function init(loader:AnimateLoader,id:String):void{
     this.mTypeId=id;
     var AssetClass:Class;
     for each(var action:String in ActionItem.Actions){
          if(!mActionDataDict[id+"_"+action]){//保存动画资源
               AssetClass = loader.getAssetClass(id+"_"+action);
               mActionDataDict[id+"_"+action]=new ActionData(new AssetClass(),id+"_"+action);
          }
     }
}
protected function getActionData(action:String):ActionData{
     return mActionDataDict[this.mTypeId+"_"+action] as ActionData;
}
//播放某个动作
public function play(action:String,loop:Boolean=false):void{
     if (this.mTimer){
          this.mTimer.stop();
     } else {
          this.mTimer = new Timer(1000 / this.mFrameRate);
          this.mTimer.addEventListener(TimerEvent.TIMER, this.onTimer, false, 0, true);
     };
     this.mCurrentFrame=0;
     this.mLoop=loop;
     this.mCurrentAction=action;
     var adata:ActionData=getActionData(action);
     this.mFrames=adata.frames;
     this.mTimer.start();
}
public function set frameEndAction(value:Function):void{
     this.mFrameEndAction=value;
}

Soldier继承Animate对象:public class Soldier extends Animate{
     public static var LEFT:int=1;//面朝左
     public static var RIGHT:int=-1;     
     public function Soldier(name:String,direction:int=1,rate:int=12){
          this.mDirection=direction;
          this.soldierName=name;
          this.scaleX=this.scaleX*this.mDirection;
          this.name=name;
          super(rate);
     }
     override public function init(loader:AnimateLoader,id:String):void{
          super.init(loader,id);
          standby();//战士默认的动作是待命状态
     }
     public function standby():void{
          play(ActionItem.Standby,true);//默认动作,代码状态始终播放
     }
}

Battle对象的作用1.     初始化战场2.     初始化战斗成员3.     初始化战斗数据,生成ActionItem序列4.     执行战斗序列5.     显示战斗结果关键是第四步,如何按顺序的执行战斗序列,我们通过在Animate中的frameEndAction,来判断一个战斗序列是否执行结束。通过添加匿名函数:     s.frameEndAction=function():void{
          s.frameEndAction=null;
          s.standby();
          doNextAction();
     }可能达到我们的目的。
Battle:/*** main参数为当前战斗舞台,Battle本身不继承Sprite,是一个普通AS类**/public function Battle(main:Sprite,a:Soldier,b:Soldier):void{
     soldierDict['A']=a;
     soldierDict['B']=b;
     soldierLayer=new Sprite();
     main.addChild(soldierLayer);
     this.soldierLayer.addChild(a);
     this.soldierLayer.addChild(b);
     this.actionItems=new Array();
}
public function start():void{
     interpret(this.cmdDataArr);//解析ActionItem序列
     var item:ActionItem=actionItems.shift();
     doAction(item);
}
//执行ActionItem序列public function doAction(item:ActionItem):void{
     var s:Soldier=item.soldiers.shift();
     s.frameEndAction=function():void{          ...//如上
     }
     s.play(item.type);
     if(item.soldiers.length>0){
          for each(s in item.soldiers){
               s.frameEndAction=function():void{
                    ...//如上
               }
               s.play(item.type);
          }
     }
}
//执行下一个序列public function doNextAction():void{
     if(actionItems.length==0) return;
     var item:ActionItem=this.actionItems.shift();
     doAction(item);
}
在主程序中:在资源价值完毕后,onComplete(e:BulkProgressEvent):soldierA=new Soldier("A",Soldier.RIGHT);
soldierA.init(animateLoader,"naruto");
soldierA.x=300;
soldierB=new Soldier("B");
soldierB.init(animateLoader,"naruto");
soldierB.x=500;
var battle:Battle=new Battle(this,soldierA,soldierB);
battle.start();
即可看到一个简单回合战斗过程。

你可能感兴趣的:(cocos2d-x,游戏开发)