本文作者:BigBear
多年游戏行业研发经验
精通Unreal、CocosCreator游戏引擎
参与过多款手游、端游项目的研发
Cocos Creator入门实战:桌球小游戏
本篇主要是希望能够通过Cocos Creator实现一个桌球小游戏,从而能够让大家更好的了解以及运用Creator的物理系统 由于游戏比较简单,同时代码量也极少,因此就集中在一篇文章里面了。因此会长一些,有兴趣的同学麻烦耐心食用
开始之前
在开始桌球小游戏之前,我们需要对creator有一定了解以及熟悉,对js语法有一定的了解。在开始同样还是希望大家能够仔细的阅读一遍官方文档,以便理解。
所涉及到的知识点参考:
- 物理系统
- UI系统
- 监听和发射事件
- 动作列表
- 预制体Prefab
- 图集资源
ps:本项目所使用的Cocos Creator版本为v2.0.9
开启物理系统
为了尽量达到真实的效果,我们对于桌球的运动均采用物理模拟来实现。在Cocos Creator中物理系统是默认关闭的,我们可以通过下面的代码来开启物理系统
代码语言:javascript复制let physicsManager = cc.director.getPhysicsManager();
physicsManager.enabled = true;
同时我们也可以通过设置debugDrawFlags来开启一些调试信息的显示,方便我们开发预览
代码语言:javascript复制cc.director.getPhysicsManager().debugDrawFlags = cc.PhysicsManager.DrawBits.e_aabbBit |
cc.PhysicsManager.DrawBits.e_pairBit |
cc.PhysicsManager.DrawBits.e_centerOfMassBit |
cc.PhysicsManager.DrawBits.e_jointBit |
cc.PhysicsManager.DrawBits.e_shapeBit
;
这里我建议将物理系统的开启以及各个属性的设置统一开发为一个组件,这样可以方便我们在属性面板中管理物理系统的属性以及各个调试开关,同时这样的一个物理系统组件比较独立,在日后其他需要使用到物理系统的场景或者是其他项目中,都可以很方便的进行复用。
创建一个叫做"PhysicsManager.js"的脚本,代码如下:
代码语言:javascript复制
/**
* 物理引擎管理组件
*/
cc.Class({
extends: cc.Component,
properties: {
active: {
default: true,
tooltip: '是否启用物理引擎',
},
aabb:{
default: true,
tooltip: '是否显示包围盒',
},
pair: {
default: true,
},
centerOfMass: {
default: true,
tooltip: '是否显示中心点'
},
joint: {
default: true,
tooltip: '是否显示关节连接线'
},
shape: {
default: true,
tooltip: '是否填充形状'
},
gravity: {
default: cc.v2(0,-960),
tooltip: '重力'
}
},
onEnable() {
//开启或关闭物理系统
let physicsManager = cc.director.getPhysicsManager();
if (physicsManager.enabled && this.active) {
cc.warn('The physical system is enabled!');
}
physicsManager.enabled = this.active;
if (!this.active) {
return;
}
//设置物理系统的重力属性
physicsManager.gravity = this.gravity;
//设置调试标志
let drawBits = cc.PhysicsManager.DrawBits;
if (CC_PREVIEW) {
physicsManager.debugDrawFlags =
(this.aabb && drawBits.e_aabbBit) |
(this.pair && drawBits.e_pairBit) |
(this.centerOfMass && drawBits.e_centerOfMassBit) |
(this.joint && drawBits.e_jointBit) |
(this.shape && drawBits.e_shapeBit);
} else {
physicsManager.debugDrawFlags = 0;
}
},
onDisable() {
let physicsManager = cc.director.getPhysicsManager();
physicsManager.debugDrawFlags = 0;
physicsManager.enabled = false;
}
});
编写完脚本后,我们将这个组件挂载到Canvas节点下。在属性面板中我们可以看到:
可视化的编辑,非常的方便。勾上Active开启物理系统,这里调试开关我们开启Shape即可,由于我们这个桌游游戏demo是一个俯视的视角,因此Gravity重力我们设置为(0,0),让小球不会受到重力的影响向下坠去。
桌面节点
在Canvas下面创建一个子节点作为我们的桌面节点。
- 给它添加一个cc.Sprite组件并且给予它显示的spriteFrame
- 添加RighdBody组件
- 由于桌面是静止不动的,因此我们将Type设置为Static
- 去掉AwakeOnLoad,因为我们并不需要它处于唤醒状态
- 添加四个边的碰撞组件
- 注意不是使用碰撞组件,而是物理组件中的Collider组件
- 勾选Editing选项可在场景编辑器中改变碰撞体的大小和位置
- 添加六个袋口的碰撞组件
- 这里用的碰撞体为CircleCollider
- 袋口碰撞体的tag我们设置为1
- 这里是为了在碰撞回调中与四周的边区分开来,便于判断
最终的节点碰撞体应调整为这样的效果:
小球
搞定完桌面我们来制作小球。同样,小球也是需要使用到物理系统的
- 红球 摆放好后我们可以将这10个小球节点都放到一个空节点下,这样我们就可以将这个10个小球做成一个prefab。方便我们动态生成。
ok,到这里我们的红球就做好了。我们可以将红球节点在场景中复制10个出来,摆放为金字塔的形状,注意摆放时红球与红球之间不要发生碰撞,否则游戏一开始红球就会散开。
白球与红球基本是一样的,只需要将spriteframe更换为白球的纹理即可。
球杆
在白球节点下创建一个子节点“Cue”,“Cue”节点为一个空节点。
在这个空节点下再创建一个子节点,并添加Sprite组件,使用球杆的问题。调整节点的x坐标,使得球杆与白球的位置如下
这样设计节点的层级关系是为了方便进行球杆的角度计算,当然会有其他不同的做法,这里只是其中的一种。
同样的,我们将白球和球杆做成一个预制体,方便我们后面的动态创建
做到这里整个节点树应该是如下的一个结构
接下来我们就要在这个基础上开发我们的游戏逻辑的脚本组件了
球杆脚本Cue.js
球杆脚本我们挂到“Cue”节点下,它的功能我们主要需要实现这几个:
- 监听鼠标事件,通过鼠标的移动控制球杆的方向
- 监听鼠标左键的按下以及抬起事件,通过按下的时间控制球杆的力度
- 鼠标左键按下时,球杆做向后的移动,松开鼠标左键,球杆做向前击打白球的动作
- 球杆击打时,向白球发送自定义事件,使得白球进行运动
- 接受白球发送过来的自定义事件,使得白球运动过程中,球杆不能操作并隐藏显示
明确了需求,并对需求进行功能拆分后,我们可以开始做手编码了。
- 首先要做的就是在start方法中对上面提到的一些事件进行监听的注册
start () {
//鼠标移动系统事件
cc.Canvas.instance.node.on(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this);
//鼠标左键按下系统事件
cc.Canvas.instance.node.on(cc.Node.EventType.MOUSE_DOWN, this.onMouseDown, this);
//鼠标左键抬起系统事件
cc.Canvas.instance.node.on(cc.Node.EventType.MOUSE_UP, this.onMouseUp, this);
//白球停止的自定义事件
cc.Canvas.instance.node.on("wball-sleep", this.onwballSleep, this);
},
注册完事件后,我们定一个cc.Node类型的属性,将cue的子节点,也就是显示球杆纹理的那个节点引用进来
代码语言:javascript复制 properties: {
//显示球杆纹理的节点
cue : cc.Node
},
接下来我们挨个实现这些事件的回调。
- 鼠标移动的事件回调
onMouseMove (event) {
//按下鼠标时,球杆方向不再移动。球杆隐藏时操作无效
if (this._mouseDown || this.node.opacity != 255) {
return;
}
//获取鼠标的当前位置坐标
var loc = event.getLocation();
this._mousePosition = loc;
//将坐标转换到父节点的坐标系下
loc = this.node.parent.convertToNodeSpaceAR(loc);
//计算与(-1,0)向量的夹脚,改夹脚即为球杆需要转动的角度
var angle = loc.signAngle(cc.v2(-1,0));
angle = cc.misc.radiansToDegrees(angle);
//设置球杆的角度
this.node.rotation = angle;
},
通过这段代码,我们即可让球杆跟随鼠标进行转动,从而控制击球的方向
- 鼠标左键按下的事件回调
onMouseDown (event) {
//球杆隐藏时操作无效
if (this.node.opacity != 255) {
return;
}
//将按下鼠标的标记设置为true
this._mouseDown = true;
//使球杆向后移动,每秒向后移动50个像素
//这里可以将-50这个值提升为组件属性,暴露到属性面板中方便配置调试
this.cue.runAction(cc.repeatForever(cc.moveBy(1, cc.v2(-50, 0))));
},
这里的速度属性值可以提取出来,在属性面板中进行配置。这样做会好一些,我这里完全是偷懒写死了
- 鼠标左键抬起的事件回调
onMouseUp (event) {
//球杆隐藏时操作无效
if (this.node.opacity != 255) {
return;
}
//计算球杆向后移动的像素,通过这个值来计算击球的力度
var force = this.cue.x - 182;
//停止球杆向后移动的动作
this.cue.stopAllActions();
//使用序列动作,先执行
this.cue.runAction(cc.sequence(
cc.moveTo(0.1, cc.v2(-182,0)).easing(cc.easeSineOut()),
cc.callFunc(() => {
//将按下鼠标的标记设置为false
this._mouseDown = false;
//创建自定义事件"cue",并派发出去
//事件有两个参数,一个是force,通过这个值,白球可以计算击球力度
//另一个值为cue,是一个cc.vec2坐标,记录按下时的鼠标位置,这是提供给白球进行角度计算的
var customEvent = new cc.Event.EventCustom("cue", true);
customEvent.force = force;
customEvent.cue = this._mousePosition
this.node.dispatchEvent(customEvent);
//隐藏球杆
this.node.opacity = 0;
})
));
},
这里force计算中的- 182
也是可以提取为参数进行配置的,这里再次偷懒写死
- 白球停止的自定义事件回调
onwballSleep () {
//白球停止时,显示球杆
this.node.opacity = 255;
},
ok,这样子我们就完成了球杆Cue.js脚本的代码编写
白球脚本 wball.js
白球需要做的事情就相对比较简单了:
- 监听击球事件“cue”,通过击球力度和坐标计算出白球被击打后的线速度
- 当白球停止运动时,也就是刚体的awake状态为false时,发送事件,告知球杆节点显示并可以击球了
因为要监听击球事件,因此首先是在start中对事件进行注册
代码语言:javascript复制 start () {
//监听击球事件“cue”
this.node.on("cue", this.onCue, this);
//白球是否停止的标记,主要是用于使停止事件只发送一次
//白球的初始状态是停止的,因此设置为true
this._sleep = true;
},
接下来是击球事件的回调实现
代码语言:javascript复制 onCue (event) {
if (this && this.node.parent) {
//白球没有停止,因此_sleep为false
this._sleep = false;
//计算白球运动的方向向量
var direction = this.node.parent.convertToNodeSpaceAR(event.cue);
direction = direction.sub(this.node.position);
direction = direction.normalize();
//根据方向和力度,计算并给予白球线速度
this.node.getComponent(cc.RigidBody).linearVelocity = direction.mul(-Math.pow(1.016, Math.abs(event.force)));
}
},
最后是白球停止时的事件派发
代码语言:javascript复制 update (dt) {
if (!this.node.getComponent(cc.RigidBody).awake && !this._sleep) {
//白球从运动到停止,状态切换时,标记设置为true,并发送白球停止的事件
this._sleep = true;
cc.Canvas.instance.node.emit("wball-sleep");
}
},
ok,搞定。基本上到这里,你已经可以控制白球去撞击其他的红球了。
这里有一个小技巧,可以看到白球停止的事件在球杆中我们是注册到了Canvas节点上。由于Cocos Creator目前的事件传递的机制是冒泡传递,因此会有一些情况下我们的事件不太好进行派发。而这样通过Canvas节点,我们就可以实现在场景中很方便的进行事件的监听处理以及派发。 需要注意的是,通过Canvas节点的话,意味着如果有场景切换的话,事件监听就会被注销掉,因此如果这个事件监听需要跨场景存在的话,可以自定义一个全局的cc.EventTarget对象用于事件的监听以及派发
球桌脚本 table.js
最后的脚本是我们的球桌脚本,这个脚本主要负责几个事情:
- 球入袋的逻辑,红球入袋的记分以及胜负判定;白球入袋的重置白球位置逻辑
- 开始游戏或是重新开始的游戏逻辑:重置记分以及重置白球红球的数量位置
最主要的代码是实现入袋的逻辑,这一部分需要我们实现onBeginContact方法 onBeginContact在组件所属节点的碰撞体发生碰撞时会进行调用 这个方法有三个参数 contact, selfCollider, otherCollider 第一个参数contact,是碰撞的信息 第二个参数selfCollider,是当前组件所属节点被碰撞到的Collider组件 第三个参数otherCollider,是非this节点,也就是发生碰撞另一个节点的Collider组件 代码如下:
代码语言:javascript复制 onBeginContact(contact, selfCollider, otherCollider) {
//如果Collider的组件tag为1时,也就是小球碰撞到代表袋口的碰撞体时
if (selfCollider.tag === 1) {
//如果是与白球发生碰撞
if (otherCollider.node.name === "wball") {
//发送通知重新生成白球,重置位置
this.node.emit("wball", otherCollider.node);
}
else {
//红球数减1
this._ballNum--;
this.ballLabel.string = "剩余" this._ballNum "球";
//红球数小于等于0时,获得胜利
if (this._ballNum <= 0) {
this.gameUI.active = false;
this.winUI.active = true;
}
}
//将小球节点从场景树上移除
otherCollider.node.removeFromParent(true);
}
},
至于重新生成白球和红球的逻辑就更简单了,这里就不详细说了,届时你会利用上之前我们所做的白球,红球的prefab
至此,我们的桌球demo基本上算是完成了核心的部分了,剩下的就是一些UI上的逻辑了。比较简单就不一一叙述了。
希望通过这个小游戏的制作,可以帮助大家了解Creator以及了解Creator物理系统的应用