一、动画系统
参考动画系统
注意:Cocos Creator 自带的动画编辑器适用于制作一些不太复杂的、需要与逻辑进行联动的动画,例如 UI 动画。如果要制作复杂的特效、角色动画、嵌套动画,可以考虑改用 Spine 或者 DragonBones 进行制作。
二、DragonBones
参考
DragonBones 组件参考
example-cases 范例中的 DragonBones(GitHub | Gitee)
Laya 动画系列三 骨骼动画
1.换装示例
通过替换插槽的显示对象,将下图绿色框中的武器替换为红色框中的刀。
@ccclass
export default class Helloworld extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
@property
text: string = 'hello';
@property(dragonBones.ArmatureDisplay)
public robot: dragonBones.ArmatureDisplay = null;
@property(dragonBones.ArmatureDisplay)
public knife: dragonBones.ArmatureDisplay = null;
private _rightDisplayIndex = 0;
private _rightDisplayNames = ["weapon_1004_r", "weapon_1004d_r"];
private _rightDisplayOffset = [{ x: 0, y: 0 }, { x: -60, y: 100 }];
start() {
this.knife.node.active = false;
}
left() {
let armature = this.robot.armature();
let slot = armature.getSlot("weapon_hand_l");
slot.displayIndex = slot.displayIndex == 0 ? 4 : 0;
}
right() {
this._rightDisplayIndex++;
this._rightDisplayIndex %= this._rightDisplayNames.length;
let armature = this.robot.armature();
let slot = armature.getSlot("weapon_hand_r");
const displayName = this._rightDisplayNames[this._rightDisplayIndex];
let factory = dragonBones.CCFactory.getInstance();
factory.replaceSlotDisplay(this.knife.getArmatureKey(), "weapon", "weapon_r", displayName, slot);
let offset = this._rightDisplayOffset[this._rightDisplayIndex];
slot.parent.offset.x = offset.x;
slot.parent.offset.y = offset.y;
armature.invalidUpdate();
}
}
2.至2021年7月6号,最新的版本已经三年没更新了
参考https://docs.egret.com/dragonbones/cn/docs/update/update561
版本:5.6.1
发布日期:2018-8-22
三、Spine
参考
Spine 组件参考
Spine API
官方 Spine 用户指南
http://zh.esotericsoftware.com/spine-api-reference
版本参考http://zh.esotericsoftware.com/spine-changelog,至2021年7月6号最新的版本是
## 4.0.02
2 Jul 2021
Creator 中的骨骼动画资源是由 Spine 编辑器 导出的,目前支持 JSON 和 二进制 两种数据格式。Creator2.3版本以上,支持Spine3.8以上版本。
Spine可以导出为JSON或者二进制格式。JSON可读性强、便于检查和修改但体积大;二进制可读性差,修改成本高,但文件体积小,加载速度快。
1.spine相关API
参考ULUA中SPINE动画的切换以及委托的使用方法
官方提供的连续播放SPINE动画方法
//先开始播放一个动画,然后使用AddAnimation往后面去添加动画
OBJ.AnimationState:SetAnimation(0, "animation1", false)
OBJ.AnimationState:AddAnimation(0, "animation2", false, 0)
关于SetAnimation 的三个参数,可以看下原型实现
public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop)
第二个参数是动画的名字,第三个参数是动画是否循环,很好理解。
第一个参数trackIndex是指通道序号。所谓通道Track,就是把动画分层,让角色在同一时间可以播放几个Spine动画。高层级的通道会覆盖低层级的Track:越大的通道数字就拥有越高的优先级。我觉得和PS里面的图层概念很像。
为了方便理解这个通道的概念,可以修改上面的代码。
OBJ.AnimationState:SetAnimation(0, “animation1”, false)
OBJ.AnimationState:SetAnimation(1, “animation2”, false)
这个时候就看不到播放的animation1了,这样写两个动画会同时播放,动画2的层级较高,会直接覆盖掉动画1。
可以用通道实现动画的叠加:比如我们有一个跑步的动画,依靠通道,我们可以实现在跑步的同时,让人能够挥手,或者扭头,用高级别的通道去覆盖低级别上的动画。
//利用不同的通道,边跑边射击
run () {
this.spine.setAnimation(0, 'run', true);
this._hasStop = false;
},
shoot () {
this.spine.setAnimation(1, 'shoot', false);
},
2.官方文档的格布林换装示例
这个比dragonbones要简单一些:
start() {
this._skinIdx = 0;
this.parts = ["left-arm", "left-hand", "left-shoulder"];
this.parts = ["left-arm"];
},
change() {
if (this._skinIdx == 0) {
console.log("换女孩的蓝手");
this._skinIdx = 1;
for (let i = 0; i < this.parts.length; i++) {
// 通过名称查找 slot。这里对每个 slot 的名称进行了比较。
// 返回一个 sp.spine.Slot 对象。
let goblin = this.goblin.findSlot(this.parts[i]);
let goblingirl = this.goblingirl.findSlot(this.parts[i]);
// 通过 slot 和 attachment 的名字来设置 attachment。
// Skeleton 优先查找它的皮肤,然后才是 Skeleton Data 中默认的皮肤。
let attachment = goblingirl.getAttachment();
goblin.setAttachment(attachment);
}
} else if (this._skinIdx == 1) {
console.log("换成自己的手");
this._skinIdx = 0;
// 按名称查找皮肤,激活该皮肤。这里对每个皮肤的名称进行了比较。
// 注意:设置皮肤不会改变 attachment 的可见性。
// 返回一个 sp.spine.Skin 对象。
this.goblin.setSkin('goblin');
//设置 slot 到起始动作
this.goblin.setSlotsToSetupPose();
}
}
//spine.js L6440
var Slot = (function () {
function Slot(data, bone) {
this.deform = new Array();
if (data == null)
throw new Error("data cannot be null.");
if (bone == null)
throw new Error("bone cannot be null.");
this.data = data;
this.bone = bone;
this.color = new spine.Color();
this.darkColor = data.darkColor == null ? null : new spine.Color();
this.setToSetupPose();
}
Slot.prototype.getSkeleton = function () {
return this.bone.skeleton;
};
Slot.prototype.getAttachment = function () {
return this.attachment;
};
Slot.prototype.setAttachment = function (attachment) {
if (this.attachment == attachment)
return;
this.attachment = attachment;
this.attachmentTime = this.bone.skeleton.time;
this.deform.length = 0;
};
Slot.prototype.setAttachmentTime = function (time) {
this.attachmentTime = this.bone.skeleton.time - time;
};
Slot.prototype.getAttachmentTime = function () {
return this.bone.skeleton.time - this.attachmentTime;
};
Slot.prototype.setToSetupPose = function () {
this.color.setFromColor(this.data.color);
if (this.darkColor != null)
this.darkColor.setFromColor(this.data.darkColor);
if (this.data.attachmentName == null)
this.attachment = null;
else {
this.attachment = null;
this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName));
}
};
return Slot;
}());
spine.Slot = Slot;
3.上面换装示例使用素材的goblins.json
参考
Spine2D-JSON数据格式
"bones": [
{ "name": "root" },
{ "name": "hip", "parent": "root", "x": 0.65, "y": 114.41, "color": "ffcf00ff" },
{
"name": "torso",
"parent": "hip",
"length": 85.83,
"rotation": 93.93,
"x": -6.42,
"y": 1.98,
"color": "ffcf00ff"
},
{
"name": "neck",
"parent": "torso",
"length": 18.38,
"rotation": -1.52,
"x": 81.68,
"y": -6.35,
"color": "ffcf00ff"
},
...
"slots": [
{ "name": "left-shoulder", "bone": "left-shoulder", "attachment": "left-shoulder" },
{ "name": "left-arm", "bone": "left-arm", "attachment": "left-arm" },
{ "name": "left-hand-item", "bone": "left-hand", "attachment": "spear" },
{ "name": "left-hand", "bone": "left-hand", "attachment": "left-hand" },
{ "name": "left-foot", "bone": "left-foot", "attachment": "left-foot" },
{ "name": "left-lower-leg", "bone": "left-lower-leg", "attachment": "left-lower-leg" },
{ "name": "left-upper-leg", "bone": "left-upper-leg", "attachment": "left-upper-leg" },
{ "name": "neck", "bone": "neck", "attachment": "neck" },
{ "name": "torso", "bone": "torso", "attachment": "torso" },
{ "name": "pelvis", "bone": "pelvis", "attachment": "pelvis" },
{ "name": "right-foot", "bone": "right-foot", "attachment": "right-foot" },
{ "name": "right-lower-leg", "bone": "right-lower-leg", "attachment": "right-lower-leg" },
{ "name": "undie-straps", "bone": "pelvis", "attachment": "undie-straps" },
{ "name": "undies", "bone": "pelvis", "attachment": "undies" },
{ "name": "right-upper-leg", "bone": "right-upper-leg", "attachment": "right-upper-leg" },
{ "name": "head", "bone": "head", "attachment": "head" },
{ "name": "eyes", "bone": "head" },
{ "name": "right-shoulder", "bone": "right-shoulder", "attachment": "right-shoulder" },
{ "name": "right-arm", "bone": "right-arm", "attachment": "right-arm" },
{ "name": "right-hand-thumb", "bone": "right-hand", "attachment": "right-hand-thumb" },
{ "name": "right-hand-item", "bone": "right-hand", "attachment": "dagger" },
{ "name": "right-hand", "bone": "right-hand", "attachment": "right-hand" },
{ "name": "right-hand-item2", "bone": "right-hand", "attachment": "shield" }
],
attachment:附件名称
4.渲染模式示例SpineBatch
渲染模式,默认 REALTIME 模式。(v2.0.9 中新增)
- REALTIME 模式,实时运算,支持 Spine 所有的功能。
- SHARED_CACHE 模式,将骨骼动画及贴图数据进行缓存并共享,相当于预烘焙骨骼动画。拥有较高性能,但不支持动作融合、动作叠加,只支持动作开始和结束事件。至于内存方面,当创建 N(N>=3) 个相同骨骼、相同动作的动画时,会呈现内存优势。N 值越大,优势越明显。综上 SHARED_CACHE 模式适用于场景动画,特效,副本怪物,NPC 等,能极大提高帧率和降低内存。
- PRIVATE_CACHE 模式,与 SHARED_CACHE 类似,但不共享动画及贴图数据,且会占用额外的内存,仅存在性能优势,如果大量使用该模式播放动画可能会造成卡顿。当想利用缓存模式的高性能,但又存在换装的需求,因此不能共享贴图数据时,那么 PRIVATE_CACHE 就适合你。
let mode = dragonBones.ArmatureDisplay.AnimationCacheMode.SHARED_CACHE;
if (!this.isCache) mode = dragonBones.ArmatureDisplay.AnimationCacheMode.REALTIME;
this.sp0.setAnimationCacheMode(mode);
变灰:
let material = this.grayMaterial;
if (!this.isGray) {
material = this.normalMaterial;
}
this.sp0.setMaterial(0, material);
this.sp0.markForRender(true);
Use Tint 是否开启染色效果,默认关闭。(v2.0.9 中新增)
this.sp0.useTint = this.isTint;
没搞懂染色是啥意思……
Enable Batch 是否开启动画合批,默认关闭。(v2.0.9 中新增)
开启时,能减少 Drawcall,适用于大量且简单动画同时播放的情况。
关闭时,Drawcall 会上升,但能减少 CPU 的运算负担,适用于复杂的动画。
this.sp0.enableBatch = this.isBatch;
开启合批,如果渲染大量相同纹理,且结构简单的骨骼动画,开启合批可以降低drawcall,否则请不要开启,cpu消耗会上升。
5.顶点效果示例SpineMesh
https://docs.cocos.com/creator/api/zh/classes/VertexEffectDelegate.html
-
clear
清空顶点效果 -
initJitter
设置顶点抖动效果 -
initSwirlWithPow
设置顶点漩涡效果 -
initSwirlWithPowOut
设置顶点漩涡效果 -
getJitterVertexEffect
获取顶点抖动效果 -
getSwirlVertexEffect
获取顶点漩涡效果 -
getVertexEffect
获取顶点效果 -
getEffectType
获取效果类型
start () {
this._swirlTime = 0;
this._maxEffect = 3;
this._index = 0;
this._bound = cc.size(this.skeleton.node.width, this.skeleton.node.height);
this._swirlEffect = new sp.VertexEffectDelegate();
this._swirlEffect.initSwirlWithPowOut(0, 2);
this._jitterEffect = new sp.VertexEffectDelegate();
this._jitterEffect.initJitter(20, 20);
},
switchEffect () {
this._index++;
if (this._index >= this._maxEffect) {
this._index = 0;
}
switch (this._index) {
case 0:
this.skeleton.setVertexEffectDelegate(null);
break;
case 1:
this.skeleton.setVertexEffectDelegate(this._jitterEffect);
break;
case 2:
this.skeleton.setVertexEffectDelegate(this._swirlEffect);
break;
}
},
update (dt) {
if (this._index == 2) {
this._swirlTime += dt;
let percent = this._swirlTime % 2;
if (percent > 1) percent = 1 - (percent -1 );
let bound = this._bound;
let swirlEffect = this._swirlEffect.getSwirlVertexEffect();
swirlEffect.angle = 360 * percent;
swirlEffect.centerX = bound.width * 0.5;
swirlEffect.centerY = bound.height * 0.5;
swirlEffect.radius = percent * Math.sqrt(bound.width * bound.width + bound.height * bound.height);
}
}
6.挂点示例SpineAttach
点击 生成挂点 按钮后,层级管理器 中 Spine 组件所在节点的下方,会以节点树的形式生成所有骨骼。
注意:Spine 挂点完成后,即可删除 层级管理器 中无用的骨骼节点,以减少运行时的计算开销。注意目标骨骼节点的父节点都不可删。
generateSomeNodes () {
let attachUtil = this.skeleton.attachUtil;
let boneNodes = attachUtil.generateAttachedNodes(this.greenBoneName);
let boneNode = boneNodes[0];
if (boneNode) {
let targetNode = cc.instantiate(this.targetPrefab);
targetNode.color = cc.color(0, 255, 0);
boneNode.addChild(targetNode);
}
// console.log(attachUtil._attachedNodeArray);
// console.log(attachUtil._boneIndexToNode);
},
destroySomeNodes () {
let attachUtil = this.skeleton.attachUtil;
attachUtil.destroyAttachedNodes(this.greenBoneName);
// console.log(attachUtil._attachedNodeArray);
// console.log(attachUtil._boneIndexToNode);
},
注:这里如果TS中this.skeleton.attachUtil
报错,可以在其上面一行添加//@ts-ignore
进行忽略。
7.碰撞检测示例SpineCollider
也是利用挂点,添加碰撞盒实现的。
关于polygonCollider,点击Editor按钮后进入编辑模式:
8.控制示例SpineBoy
spineBoy.setAnimation(0, 'walk', true);//trues时为一直循环播放
spineBoy.setMix('walk', 'jump', 0.2);//从walk转换到run动画的过度时间
关于setMix可以参考spine动画融合与动画叠加
下图为动作切换时的情况(setAnimation),左红线为切换动作时刻,两个红线之间为动作切换时间,这之间动画播放为A与B的混合,A和B所占的权重是不断变化的,从A占的权重从100到0,B占的权重从0到100(alpha值变化)。右红线之后为动作B完全播放。
事件:
this.spine.setCompleteListener(this._spineEventHandler);
private _spineEventHandler(trackEntry: any) {
var animationName = trackEntry.animation ? trackEntry.animation.name : "";
console.log("_animationEventHandler", trackEntry, animationName);
if (animationName === 'shoot') {
// this.spine.clearTrack(1);
}
}
四、扩展阅读
2d手游美术实现方案分析
我所理解的打击感 逐帧分析过几十款游戏的开发者经验分享