转自:https://blog.csdn.net/zhanghefu/article/details/22753973
回合制战斗设计一 前端基本设定
回合制战斗设计一 前端基本设定
回合制战斗设计一 前端基本设定
前端需要考虑的问题
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<count;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<count;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();
即可看到一个简单回合战斗过程。