HELLO,大家好,我是麒麟子。作为Cocos社区高产用户,今天又给大家带来了一个看起来很酷,但实际上大多数人用不到的DEMO。
不知道大家是否记得梦幻西游、问道、英雄无敌、仙剑奇侠传、神仙道、神曲OL。
不知道大家是否最近在玩自走棋(哎哟,不错哟,最近特别火)
然而,他们从技术上讲,没有本质的区别,他们的战斗都是回合制。
回合制的游戏就像棋牌一样,每一个回合,同一时间,只有一个人能操作。 等这个人表演完。再交给下一个。
DEMO内容:
这个DEMO展示了一个战斗场景,玩家可以控制一个英雄去攻击目标。 英雄有6个技能,当玩家点击某个技能的时候,英雄会冲到目标跟前,翻云覆云一番,再退回自己的位置。
有在线演示可以看哦:https://qilinzi.ukylin.net/?lesson=08
当然,还有,源码:https://gitee.com/qilinzi/qlz_ccc_tips
一、动作与特效
在这个例子中,英雄的动作和特效是在同一张图上的。显然,这样的方式不适合像传奇这样的MMORPG。 但是对于不换装的游戏,是完全没有问题的。还能省下不少DrawCall。
值得注意的是,我们在这个例子中,并没有使用Cocos Creator中的Animation来编辑英雄动画。 因为一个游戏有上百上千种动画,如果一个个手工编辑的话,是要死人的。 SO。。。 我们自己手写了一个。 请看大屏幕。
//动画信息配置
var AnimConfig = { }
AnimConfig['0001'] = {
'attack':{frames:8,fps:8},
'attacked':{frames:1,fps:8},
'combat_idle':{frames:4,fps:8},
'idle':{frames:4,fps:8},
'ride_idle':{frames:4,fps:8},
'ride_run':{frames:8,fps:8},
'run':{frames:8,fps:8},
'rush':{frames:1,fps:8},
'spell1':{frames:8,fps:8},
'spell2':{frames:8,fps:8},
'spell3':{frames:14,fps:8},
'spell4':{frames:8,fps:8},
'spell5':{frames:4,fps:8},
'spell6':{frames:10,fps:8},
}
AnimConfig['0003'] = {
'attack':{frames:8,fps:8},
'attacked':{frames:1,fps:8},
'combat_idle':{frames:4,fps:8},
'idle':{frames:4,fps:8},
'ride_idle':{frames:4,fps:8},
'ride_run':{frames:8,fps:8},
'run':{frames:8,fps:8},
'rush':{frames:1,fps:8},
'spell1':{frames:8,fps:8},
'spell2':{frames:8,fps:8},
'spell3':{frames:14,fps:8},
'spell4':{frames:8,fps:8},
'spell5':{frames:4,fps:8},
'spell6':{frames:10,fps:8},
}
export default class NewClass {
public static getRoleInfo(roleId){
return AnimConfig[roleId];
}
}
上面的类用于配置我们对应角色的动画,每一个动画,有动画名,帧数,帧率 三个属性。 每一个角色都有一个编码,如0001,0003。
import AnimConfig from './08_anim_config';
const {ccclass, property} = cc._decorator;
@ccclass
export default class NewClass extends cc.Component {
@property
roleId:string = '0001';
@property
defaultAnim:string = 'idle';
// LIFE-CYCLE CALLBACKS:
private _spriteFrames:Array = [];
private _lastStartTime = 0;
private getAnimInfo(animName:string):any{
var info = AnimConfig.getRoleInfo(this.roleId);
return info[animName];
}
onLoad () {
}
playAnim(animName:string){
var aniInfo = this.getAnimInfo(animName);
if(!aniInfo){
return null;
}
this.defaultAnim = animName;
this._lastStartTime = Date.now();
var folder = 'roles/' + this.roleId + '/';
var arr = [];
for(var i = 0; i < aniInfo.frames; ++i){
var url = folder + animName + '/frame' + i;
arr.push(url);
}
cc.loader.loadResArray(arr,cc.SpriteFrame,function(err,arr){
this._spriteFrames = arr;
}.bind(this));
}
start () {
this.playAnim(this.defaultAnim);
}
update (dt) {
if(!this._lastStartTime || !this._spriteFrames.length){
return;
}
var fps = this.getAnimInfo(this.defaultAnim).fps;
var index = Math.floor((Date.now() - this._lastStartTime) / 1000 * fps);
if(index > this._spriteFrames.length){
this.playAnim('combat_idle');
return;
}
index %= this._spriteFrames.length;
this.node.getComponent(cc.Sprite).spriteFrame = this._spriteFrames[index];
}
}
在上面的代码中,playAnim被调用的时候,我们首先获取到动画的信息,然后使用cc.loader.loadResArray来加载动画所需要使用到的图片。待加载完毕后,放入对象变量中缓存。
update里,我们根据时间计算出当前帧。
通过这样的方式,我们就可以基于配置和文件命名规则来实现大量的角色动画和NPC动画。 减少动画编辑的工作量。
二、攻击过程
回合制游戏,最大的特点就是玩家不需要控制角色位置。 所以,近身攻击是需要系统主动移动位置到目标跟前的。 攻击完毕后,又要移回来。 我们在这里,使用了cc.Action组合来做。先欣赏一下代码。
onCastSpell(event){
var animName = event.target.__meta;
if(this._isSpelling){
return;
}
this._isSpelling = true;
var arr = [];
//切换成冲刺动画,并移动到目标跟前
arr.push(cc.spawn(cc.moveTo(0.3,this.target.node.position.sub(cc.v2(120,0))),cc.callFunc(function(){
this.hero.playAnim('rush');
},this)) );
//播放攻击动画
arr.push(cc.callFunc(function(){
this.hero.playAnim(animName);
},this));
var animInfo = AnimConfig.getRoleInfo(this.hero.roleId)[animName];
var playTime = animInfo.frames / animInfo.fps;
//等待攻击完成
arr.push(cc.delayTime(0.5 + playTime));
//移回原来位置
arr.push(cc.moveTo(0.1,this.hero.node.position));
arr.push(cc.callFunc(function(){
this._isSpelling = false;
},this));
var act = cc.sequence(arr);
this.hero.node.runAction(act);
}
1、冲刺到目标面前
为了实现冲刺到目标面前,我们需要在切换动画的同时,移动角色位置。 Cocos Creator提供了cc.spawn,构建出能够同时执行多个Action的组合Action。cc.callFunc我们理解为自定义Action,你可以在回调函数里编写你期望的逻辑。
2、攻击
我们只需要简单的使用cc.callFunc写一个自定义Action,进行攻击动作的播放即可。
3、等待播放完成
由于我们自己写的动画播放类,还未处理播放完成事件,所以在这里,我们使用了一个cc.delayTime来做等待,等待的时间,是根据动画帧率*帧数 + 一个固定值
4、回到原来位置
这个用cc.moveTo即可实现
5、把它们串起来
我们把所有的Action放入一个数组中,再使用cc.sequence,即可以构建出一个按顺序执行的cc.Action。 并把这个Action交给目标节点执行即可
6、其它
大家会看到一个this._isSpelling的变量,和这个相关的代码,均是为了防止用户在攻击过程中,多次点击出现BUG。
三、技能图标与池
技能图标的实现相当简单,就是把他们放到了一个Layout里面,只不过,我们是动态添加的节点。 在这里,麒麟子使用了一套常用的辅助函数
public static removeItemToPool(listRoot){
for(var i = 0; i < listRoot.childrenCount; ++i){
listRoot.children[i].active = false;
}
};
public static addItemFromPool(listRoot){
for(var i = 0; i < listRoot.childrenCount; ++i){
var child = listRoot.children[i];
if(child.active == false){
child.active = true;
return child;
}
}
var newChild = cc.instantiate(listRoot.children[0]);
listRoot.addChild(newChild);
return newChild;
};
removeItemToPool 会把所有的layout子节点标记为active = false;
addItemFromPool 会从layout子节点中选择一个active为false的节点,若没有可用的,则复制0号节点来用。
上面这个套路,麒麟子用了很久了,既简单明了,又能够控制对象池。 大家在做背包,或者其它大量子元素界面的时候可以试试。可以很任性的随意刷新,完全不用担心效率和内存问题。
四、结束语
有在线演示:https://qilinzi.ukylin.net/?lesson=08
源码:https://gitee.com/qilinzi/qlz_ccc_tips
大家朋什么不明白的,或者想了解但麒麟子没有写的。在本博客中留言即可,也可以直接在交流群里@麒麟子,或者私聊麒麟子。 谢谢大家的支持!