在CocosCreator3D发布之前,虽然CocosCreator2.x版本已经有了3D的功能,但是这些3D功能并不能完全支持一个纯3D的游戏开发,更多的只是辅助2D游戏,提升2D游戏的表现。那现在CocosCreator3D已经发布了1.0.0正式版,也就意味着我们可以使用它来进行纯3D游戏开发,并且如果是有着creator使用经验的同学,上手会非常的快速。这里带来一个简单的3D跑酷小游戏,目标平台是微信小游戏,希望通过这个游戏的开发,能够快速掌握并上手CocosCreator3D。
下载地址:https://www.cocos.com/creator3d
(本文使用的是Cocos Creator 3D v1.0.0版本)
下载完成后解压安装即可,没有什么太多的注意事项。
唯一需要注意的就是Cocos Creator 3D需要的nodejs版本,如果你的nodejs版本比较低的话,安装后可能会出现一些编辑器的bug或者是打不开编辑器。如果碰到这种情况,可以升级下nodejs,重新安装即可
另外需要准备的工具即是微信web开发者工具
安装完成之后,打开CocosCreator3D,就可以看到熟悉的Dashboard界面了,与Creator2D非常的相似。
这里我们选择New Project
,新建一个项目
项目打开后我们可以看到,整个编辑器的界面和Creator2D非常的相似
具体详细的编辑器面板介绍,包括操作介绍可以参考官方文档,如果你是具有一定的3D游戏开发基础的,可以忽略。CocosCreator3D的编辑器绝大部分是参考了Unity以及UE去设计的,对应的操作也基本相同
##Canvas
在Scene节点下创建一个Canvas节点,我们用它来作为UI的根节点
和Creator2D不同的地方是,在Creator3D中,场景默认为3D场景,因此也没有默认的去创建Canvas节点。如果这个场景你有一些UI或者是2D的渲染需要显示的话,需要手动创建Canvas节点。
在Scene节点下新建一个3D节点,我们选择3D Object
中的Plane
控件,顾名思义,这就是一个平板模型,只有一个平面的模型。
在层级管理器中选择这个节点,在右边的属性检查器中,点击AddComponent
按钮,给它添加一个刚体组件,也就是RigidBodyComponent组件
接着,我们再点击AddComponent
按钮,给它添加一个碰撞组件,这里我们使用BoxColliderComponent组件
这时我们的碰撞组件的大小已经与我们的模型相吻合了。如果是其他的一些自定义模型,在添加了碰撞组件后,碰撞组件的大小并不会与模型非常的吻合,你发现有需要调整的地方,这时你可以通过鼠标拖动画面中我们看到的几个绿点来调整碰撞组件的大小。
碰撞组件的位置目前这个版本还不能在场景编辑器中调整,需要在右边的属性检查器中,通过改变数值进行调整。
由于我们这个游戏使用的都是基础的模型,所以基本上不怎么需要去调整碰撞组件的大小。
去掉RigidComponent组件
上的UseGravity
属性,这样可以让这个刚体不受重力的影响。
同时我们勾选上IsKinematic
属性,这样可以让这个物体的状态不受物理模拟所影响。类似于UE中的Static Actor。
这样我们就完成了一个基本的地面节点的创建。
我们把这个节点的Z轴缩放改为0.5
然后将它的命名改为NormalPlane,拖入到我们的资源管理器中,生成一个Prefab资源,留待后续关卡编辑使用
刚体组件和碰撞组件,我们可以参考看下官方文档
在Scene节点下面,我们再创建一个Sphere球形模型,选择3D Object中的Sphere即可,
同样我们给这个节点添加RigidComponent组件
和一个SphereColliderComponent组件
,SphereColliderComponent组件和BoxColliderComponent组件一样,都是碰撞组件,只是一个是盒形碰撞,一个是球形碰撞
注意这里的UseGravity
属性和IsKinematic
属性使用默认的值,不需要像Plane那样去设置。
这个小球就是我们游戏中,玩家去操控的对象了。我们需要控制这个小球,完成一系列的跑酷关卡。
接下来,我们调整一下小球的坐标,将Y轴坐标
设置为0.5
,这样让我们的小球处于地面的上方,并且更改名称为Charactor
最后我们设置一下RigidComponent组件
上的LinearDamping
属性,这是线速度的衰减系数,可以让我们的小球就像真实物理世界的那样,逐渐因为一些阻力停下来
这时我们调整一下默认摄像机节点Camera
的位置以及角度,预览一下可以看到小球停留在地面上静止不动
接下来我们创建两个脚本CharactorMovement和CharactorController,分别用来实现Charactor小球的运动以及控制。
先来看CharactorMovement这个脚本,这个脚本主要是要实现与小球的运动相关的代码
在资源管理器中,我们右键选择create
-> TypeScript
来创建脚本
由于小球是由物理进行运动模拟的,因此对于小球的运动,我们不能直接给予它位置上的改变,我们必须通过给予冲量Impluse或者是持续给予一个力Force来对小球进行速度上的改变,从而达到移动小球的目的。
如果是给予力Force的话,参考的物理公式就是牛顿定律:F = m * a
如果是给予冲量Impluse的话,参考的物理公式就是动能定律
这里我们给予小球冲量来改变速度即可
const { ccclass, property, requireComponent } = _decorator;
@ccclass("CharactorMovement")
@requireComponent(RigidBodyComponent)
首先使用requireComponent装饰器,定义我们的CharactorMovement组件必须要依赖于一个RigidComponent组件,毕竟我们的CharactorMovement组件是操作同一个节点上的RigidComponent组件的,有了这个装饰器,可以保证挂载了CharactorMovement的节点上肯定会有一个RigidComponent组件。如果没有RigidComponent组件,编辑器就会自动给这个节点添加一个RigidComponent组件,用来保证CharactorMovement组件的顺利运行。
接着我们给CharactorMovement定义一个私有属性_rigidBody
private _rigidBody: RigidBodyComponent = null;
然后我们在start
方法中获取节点上的刚体组件并赋值给_rigidBody
属性
this._rigidBody = this.node.getComponent(RigidBodyComponent);
接下来我们实现一些运动的接口
//给予小球向前移动的冲量
addFrontImpluse (power: number) {
if (this._jumping) {
return;
}
this._rigidBody.applyImpulse(new Vec3(power, 0, 0));
this.playMovementAudio();
}
//给予小球向左移动的冲量
addLeftImpluse (power: number) {
if (this._jumping) {
return;
}
this._rigidBody.applyImpulse(new Vec3(0, 0, -power / 2));
this.playMovementAudio();
}
//给予小球向右移动的冲量
addRightImpluse (power: number) {
if (this._jumping) {
return;
}
this._rigidBody.applyImpulse(new Vec3(0, 0, power / 2));
this.playMovementAudio();
}
//给予小球向后移动的冲量
addBackImpluse (power: number) {
if (this._jumping) {
return;
}
this._rigidBody.applyImpulse(new Vec3(-power, 0, 0));
this.playMovementAudio();
}
//给予小球向上的冲量,也就是跳起
addTopImpluse (power: number) {
if (this._jumping) {
return;
}
this._rigidBody.applyImpulse(new Vec3(0, power, 0));
this.playJumpAudio();
}
//给予小球一个冲量,这里是为了方便其他组件进行调用预留的接口
addImpluse (vector: Vec3) {
this._rigidBody.applyImpulse(vector);
}
这样子通过刚体组件的applyImpulse
方法,我们实现了给予小球一个冲量并且让小球运动起来。
你可以在start
方法中调用一下addFrontImpluse
或者是其他的方法测试一下,随便给一个参数值,就可以看到小球移动起来的样子。
这里的方法中我们使用了一个this._jump
属性,这是一个boolean
类型的属性,用于标示这个小球有没有在空中。如果小球正在空中,那么我们就无法对小球进行任何操作,只有小球在地面上,才能对小球进行操作。
private _jumping: boolean = false;
接下来我们在update
方法中,对小球是否跳起做判断
update (deltaTime: number) {
// Your update function goes here.
if (Math.abs(this.node.position.y - 0.5) < 0.01) {
this._jumping = false;
}
else {
this._jumping = true;
}
}
小球的高度是0.5
,也就是当小球在地面上的时候,它的Y轴坐标
应该是0.5
,当小球跳起或者是跌落到地面下时,它的高度就发生了改变。我们通过Math.abs(this.node.position.y - 0.5) < 0.01
去判断小球的状态,这段代码意味着小球只要偏离地面的高度大于0.01,那么我们就认为小球在空中,将_jump
属性设置为false,这样我们在applyImpluse
前对小球的状态进行一下判断,就可以知道需不需要给小球施加冲量了
以上,我们就完成了小球的CharactorMovement组件的编写
CharactorController组件主要需要实现对于小球的控制,也就是对我们的触摸操作进行处理
首先我们先定义一下组件的一些属性
@property(Node)
reciveInputNode: Node = null;
@property(CharactorMovement)
movement: CharactorMovement = null;
@property
movePower: number = 0;
@property
jumpPower: number = 0;
private _pressTime: number = 0;
private _pressStart: boolean = false;
private _enableController: boolean = true;
reciveInputNode
属性是用来注册接受触摸事件的节点
movement
属性是为了让我们将CharactorMovement组件引用进来的属性
movePower
和jumpPower
是小球移动和跳起时的冲量力度
私有属性_pressTime
和_pressStart
是为了实现小球的蓄力跳跃使用的,_pressTime
是用来记录触摸的持续时间,而_pressStart
是为了标记触摸是否开始了。
_enableController
属性是为了控制是否启用了触摸
接下来,我们需要在start
方法中注册一下我们的触摸监听事件,以及初始化属性,和creator2D也是类似的
事件这一块可以参考官方文档
start () {
// Your initialization goes here.
if (this.reciveInputNode) {
this.reciveInputNode.on(SystemEventType.TOUCH_START, this._touchStart, this);
this.reciveInputNode.on(SystemEventType.TOUCH_MOVE, this._touchMove, this);
this.reciveInputNode.on(SystemEventType.TOUCH_END, this._touchEnd, this);
}
this._pressTime = 0;
this._pressStart = false;
}
接下来我们需要按个实现_touchStart
,_touchMove
,_touchEnd
这几个事件回调函数
_touchStart (event: EventTouch) {
//如果this._enableController为false则不可以操作,直接return出去
if (!this._enableController) {
return;
}
var touches: Touch[] = event.getTouches();
//多点触摸无效
if (touches.length > 1) {
return;
}
else if (this._pressStart === false) {
//有效触摸,将_pressStart标记为true,同时重置触摸时间_pressTime
this._pressStart = true;
this._pressTime = 0;
}
else
this._pressStart = false;
}
_touchMove (event: EventTouch) {
if (!this._enableController) {
return;
}
var touches: Touch[] = event.getTouches();
if (touches.length > 1) {
return;
}
else {
var touch: Touch = touches[0];
//获取触摸的起始点位置
var startPos: Vec2 = touch.getStartLocationInView();
//获取触摸的当前位置
var nowPos: Vec2 = touch.getLocationInView();
//通过向量减法,计算出方向向量
var direction: Vec2 = nowPos.subtract(startPos);
//当触摸开始后,触摸滑动的距离大于20像素时,我们认为玩家进行了一次移动操作
//这个20的值可以提取出来作为属性值进行配置,方便调试
if (this._pressStart && direction.length() > 20) {
//获取方向值,并通过方向值的判断,让小球的movement调用不同的移动方法
var directionType = this._toDirection(direction);
switch (directionType) {
case EControllerDirection.LEFT:
this.movement.addLeftImpluse(this.movePower);
break;
case EControllerDirection.RIGHT:
this.movement.addRightImpluse(this.movePower);
break;
case EControllerDirection.FRONT:
this.movement.addFrontImpluse(this.movePower);
break;
case EControllerDirection.BACK:
this.movement.addBackImpluse(this.movePower);
break;
}
console.log(directionType);
//调用完移动方法后,结束触摸,将_pressStart重置为false,触摸时间同样也进行重置
this._pressStart = false;
this._pressTime = 0;
}
}
}
_touchEnd (event: EventTouch) {
if (!this._enableController) {
return;
}
console.log("touchend", event);
var touches: Touch[] = event.getTouches();
if (touches.length > 1) {
return;
}
else if (this._pressStart && this._pressTime > 0) {
//当触摸结束时间回调时,我们的_pressStart和_pressTime没有在_touchMove中重置掉时
//也就意味着并没有发生滑动触摸,玩家一致保持着长按状态
//这时我们认为玩家进行了一次蓄力跳跃的操作
var touch: Touch = touches[0];
var startPos: Vec2 = touch.getStartLocationInView();
var nowPos: Vec2 = touch.getLocationInView();
var direction: Vec2 = nowPos.subtract(startPos);
//设定一个误差20,和前面_touchMove中的判断值一致
//这个值也是为了防止错误判断,因为虽然玩家可能一致长按,但这是对于玩家的操作感觉来说的
//有些时候可能轻轻的动了一下手指,玩家觉得没有移动手指,但实际上我们发现Touch可能就移动了几个像素
//这时我们也要判断为长按中,否则会带来很差的操作体验
if (direction.length() < 20) {
//最长的蓄力时间为2秒,这个2秒,也就是2.0这个数值,也可以提取为属性,方便配置
var trulyTime = math.clamp(this._pressTime, 0.0, 2.0);
var trulyPower = this.jumpPower * trulyTime / 2.0;
//调用小球的跳起移动方法,给予小球冲量进行跳跃
this.movement.addTopImpluse(trulyPower);
}
}
this._pressStart = false;
this._pressTime = 0;
}
//方向判断,根据方向向量与(1,0)向量的夹角进行判断
_toDirection (direction: Vec2): number {
var angle = Vec2.angle(direction, new Vec2(1,0));
var degree = math.toDegree(angle);
if (degree < 50) {
return EControllerDirection.RIGHT;
}
else if (degree > 130) {
return EControllerDirection.LEFT;
}
else if (Math.abs(degree - 90) < 40) {
if (direction.y > 0) {
return EControllerDirection.BACK;
}
else {
return EControllerDirection.FRONT;
}
}
}
方向判断如下图所示:
通过夹角的度数,以及y轴的方向(正负值),我们就可以判断出方向
得到的方向我们使用了一个枚举类型来表示,因此我们需要在脚本中事先申明一下这个枚举类型
var EControllerDirection = Enum({
FRONT : 0,
BACK : 1,
LEFT : 2,
RIGHT : 3
})
你也可以通过下面的方法去定义这个枚举
enum EControllerDirection2 {
FRONT = 0,
BACK = 1,
LEFT = 2,
RIGHT = 3
}
enum EControllerDirection3 {
FRONT = 0,
BACK = 1,
LEFT = 2,
RIGHT = 3
}
const EControllerDirection4 = Object.freeze({
FRONT: { name: "FRONT", value: v3(1,0,0) },
BACK: { name: "BACK", value: v3(-1,0,0) },
LEFT: { name: "LEFT", value: v3(0,0,-1) },
RIGHT: { name: "RIGHT", value: v3(0,0,1) },
});
都是等价的。
需要注意的是最后一种方法,使用时,对于变量的类型声明需要处理一下:
var _test: ({name: string,value : Vec3}) = EControllerDirection4.FRONT;
最后我们在update
方法中对_pressTime
做一下计时
update (deltaTime: number) {
// Your update function goes here.
if (this._pressStart) {
this._pressTime += deltaTime;
}
}
我们再做一个接口,用来控制是否开启触摸控制,留待后续使用
disableController () {
this._enableController = false;
}
这样我们的CharactorController
组件也就完成了
我们将编写好的CharactorMovement
以及CharactorController
脚本,通过拖拽的方式,挂载到Charactor
节点上去,也就是我们的小球上
通过拖动节点到属性检查器中的方式,我们给几个我们定义好的属性赋一下值
最终运行一下,我们就可以体验一下小球的操作了。