跨平台游戏引擎Cocos creator基础教程
# 跨平台游戏引擎Cocos creator基础教程
本文讲述跨平台游戏引擎Cocos的基础教程。Cocos Creator 深度支持各大主流平台,游戏可以快速发布到 Web、iOS、Android、Windows、Mac,以及各个小游戏平台。在 Web 和小游戏平台上提供了纯 JavaScript 开发的引擎运行时,以获得更好的性能和更小的包体。
# 1. 安装开发工具Cocos Creator 3
Cocos Creator是cocos项目的开发工具,当前最新版本V3.0.0-Preview.1。
推荐使用最新版本,最新版本同时支持2D和3D游戏游戏开发,已经将Cocos Creator 2D和Cocos Creator 3D整合到一起,所以Creator 3D 后续不会再发布独立版本。
Cocos Creator 3.0手册 (opens new window)
# 1.1 注册账号
访问官网 (opens new window)注册账号
# 1.2 下载并安装Creator Dashboard
下载Creator Dashboard
,下载地址详见https://www.cocos.com/creator
安装Creator Dashboard并启动,会提示登录,使用注册好的账号登录后进入如下界面:
# 1.3 下载并安装Creator Editor
运行Creator Dashboard后, 点击界面左侧的Editor
菜单,再点击右下角的Download
按钮
选择指定版本的Editor, 点击右侧的下载按钮即可。
还需要安装的其它环境:
- nodejs
- git
目前不支持linux开发环境
# 2. 核心概念
游戏场景包含以下内容:
- 物体
- 角色
- UI
- 组件
可以是一个Typescript脚本
当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本
# 3. 教程
Creator和Editor都设置为中文 js脚本关联外部编辑器,如vscode https://www.bilibili.com/video/BV1sA411Y7x4?p=3
# 4. 动态加载精灵节点的图片
# 4.1 加载网络图片
var sprite = this.node.getComponent(cc.Sprite)
cc.loader.load("http://localhost/666.png", function (err, texture) {
cc.log(texture);
//可以自定义宽和高, 如texture.height = 200;
sprite.spriteFrame = new cc.SpriteFrame(texture);
})
# 4.2 加载本地resources目录下的图片
let that = this
let sprite = that.node.getChildByName('avator').getComponent(cc.Sprite)
sprite.spriteFrame = new cc.SpriteFrame(cc.url.raw('resources/prop_666.png'));
# 5. 碰撞
# 5.1 碰撞的种类
碰撞分为2种(对应2种碰撞组件):
- 非物理碰撞
即没有物理效果的碰撞,只能监听到碰撞的事件。
该方案仅适合简单的场景。 - 物理碰撞 有物理效果的碰撞,即碰撞过程中会显示出物理性质,如重力、碰撞后的反弹。
# 5.2 物理碰撞
在使用cocos creator为节点添加物理碰撞组件时,会自动添加一个刚体。
- 碰撞组件
- 刚体
刚体用来反应物理特性。刚体是绑到碰撞组件上的。
刚体分为4种类型。
# 5.2.1 刚体穿透
注意不能使用setPosition类似方式移动物体,否则会出现刚体穿透的现象。应该以物理的方式移动物体,详见力与冲量 (opens new window)。
# 5.2.2 移动物体
以物理的方式
移动物理碰撞系统中的物体, 有3种方法:ApplyForce、ApplyLinearImpulse、SetLinearVelocity
- ApplyForce - 力,循序渐进
F=ma, 即力=质量*加速度
。
该方法的使用方法详见如下案例
:
已知一个物体的初速度vel,和物体质量body->GetMass(),你要设定他t秒后的速度要变为desiredVel的话,可以计算出需要的力 f=(v2-v1)*m/t。 完整代码如下(为c++代码):
b2Vec2 vel = body->GetLinearVelocity();
float m = body->GetMass();// the mass of the body
float t = 1.0f/60.0f; // the time you set
b2Vec2 desiredVel = b2Vec2(10,10);// the vector speed you set
b2Vec2 velChange = desiredVel - vel;
b2Vec2 force = m * velChange / t; //f = mv/t
body->ApplyForce(force, body->GetWorldCenter() );
- ApplyLinearImpulse(冲量) - 速度,叠加
冲量的计算公式I=FΔt=mΔv,I代表冲量。
由如上公式可以得出: m一定,那么施加冲量,则速度一定会改变,这里Δv是在原速度的基础上进行改变的。
与ApplyForce不同,ApplyLinearImpulse不会产生力,而是直接影响刚体的速度。通过ApplyLinearImpulse方法添加的速度会与刚体原有的速度叠加,产生新的速度。
补课: 几个重要公式:
- I=FΔt
- F=ma
- a=Δv/Δt
- I=mΔv(由如上几个公式可以得出)
冲量是描述力对物体作用的时间累积效应的物理量。力的冲量是一个过程量。在谈及冲量时,必须明确是哪个力在哪段时间上的冲量。 由F=ma,a=Δv/Δt,设Δv=v2-v1,Δt=t2-t1可得 Δp=mv2-mv1=FΔt 即可说:物体所受合外力的冲量就是该物体的动量(p=mv)变化量。 动量和冲量都是矢量,一定要注意方向。
- SetLinearVelocity - 一触即发
setLinearVelocity与ApplyImpulse一样,直接影响刚体的速度。
和ApplyImpulse的区别: setLinearVelocity添加的速度会覆盖刚体原有的速度。中间没有加速度的概念,新速度和原速度没有关系,直接定义新速度。
不过,在SetLinearVelocity方法不会自动唤醒sleeping的刚体,所以在调用该方法之前,记得将刚体 body.wakeUp()一下。
# 5.2.3 碰撞监听
可以通过cocos creator提供的碰撞回调函数来监听碰撞事件,如onBeginContact
等。
需要注意回调函数的输入参数contact
是引用类型
的对象,每次发生碰撞则使用该对象存储最新的碰撞信息(而不是每碰撞一次, new出一个contact对象)。所以当您要存储历史的碰撞点信息时,需要注意该点, 否则您可能发现存储的所有历史碰撞点的坐标都相同。
验证方法: 存储历史碰撞点(获取当前碰撞点的方法:contact.getWorldManifold().points[0])到全局数组中,最终发现该数组的所有元素的坐标信息都相同。解决方法: new一个cc.v2对象push到数组中, 而不是将contact.getWorldManifold().points[0]对象直接push到数组。
# 5.2.4 碰撞分组
默认情况下, 所有节点的分组名称都是default
。
所属不同分组的节点之间发生碰撞才会进行碰撞检测,所以需要按实际情况配置碰撞分组。
# 5.2.5 碰撞组件的参考坐标系
和组件cc.Graphics
类似,指定碰撞组件的坐标点时, 参考坐标系并不是
当前节点的父节点坐标系
(按本地坐标系的定义,您可能会这么认为, 但实际上非也),而是当前节点坐标系
。
所以,您在可移动的节点上创建的碰撞组件会随着当前节点的移动而移动。
# 5.2.6 动态创建碰撞体
可以通过程序动态创建碰撞体,但是建议指定碰撞体的坐标点时使用整数,而不使用小数,免得发生不可预料的异常。
如下展示一个真实的异常案例
如下代码做了2件事:
- 根据顶点动态创建碰撞体
- 根据顶点连线,从而分析出碰撞体的边界线
// 如下顶点的定义创建碰撞体失败,
let objArr =[{"x":-292,"y":1,"z":0},{"x":-173,"y":180,"z":0},{"x":-172,"y":182,"z":0},{"x":-171.8490380034824,"y":183.61515573074465,"z":0},{"x":-171.25946101609992,"y":185.17405815031,"z":0},{"x":-170.66988402871743,"y":186.73296056987536,"z":0},{"x":-162,"y":208,"z":0},{"x":-141,"y":461,"z":0},{"x":-320,"y":462,"z":0}];
// 如下顶点的定义创建碰撞体成功,与前者定义的区别是将第3个顶点的x坐标由-171.8490380034824改成了-171.8490380034823
// let objArr =[{"x":-292,"y":1,"z":0},{"x":-173,"y":180,"z":0},{"x":-172,"y":182,"z":0},{"x":-171,"y":183.61515573074465,"z":0},{"x":-171.25946101609992,"y":185.17405815031,"z":0},{"x":-170.66988402871743,"y":186.73296056987536,"z":0},{"x":-162,"y":208,"z":0},{"x":-141,"y":461,"z":0},{"x":-320,"y":462,"z":0}];
for (let index = 0; index < objArr.length; index++) {
const element = objArr[index];
let tmp = this.convertSpace(this.node.parent, this.pathDrawNode, cc.v2(element.x, element.y));
if(index == 0){
this.graphic.moveTo(tmp.x, tmp.y);
}else{
this.graphic.lineTo(tmp.x, tmp.y);
this.graphic.strokeColor = cc.Color.RED;
this.graphic.lineWidth = 10;
this.graphic.stroke();
}
}
let pointArr :cc.Vec2[] = [];
for (let index = 0; index < objArr.length; index++) {
const element = objArr[index];
pointArr.push(cc.v2(element["x"], element["y"]));
}
let collider = this.node.parent.addComponent(cc.PhysicsPolygonCollider);
collider.friction = 0;
collider.restitution = 0;
collider.points = pointArr;
collider.apply();
如上代码绘制出了碰撞体的边界路线,但是碰撞体却创建失败, 浏览器控制台报错如下:
CCPolygonSeparator.js:337 Uncaught TypeError: Cannot read property 'x' of undefined
at Area (CCPolygonSeparator.js:337)
at Right (CCPolygonSeparator.js:219)
at Reflex (CCPolygonSeparator.js:207)
at ConvexPartition (CCPolygonSeparator.js:77)
at ConvexPartition (CCPolygonSeparator.js:154)
at ConvexPartition (CCPolygonSeparator.js:154)
at Object.ConvexPartition (CCPolygonSeparator.js:154)
at cc_PhysicsPolygonCollider._createShape (CCPhysicsPolygonCollider.js:56)
at cc_PhysicsPolygonCollider.__init (CCPhysicsCollider.js:172)
at CCClass.pushDelayEvent (CCPhysicsManager.js:165)
错误截图如下:
按如上代码注释的提示,启用成功时的顶点变量的定义,则创建碰撞体成功,运行成功的效果如下:
通过不断验证发现,如下
任意
测试用例都可以创建成功:
- 修改第3个顶点的x坐标
将-171.8490380034824改为-171.8490380034823或-171.8490380034825或-171都可以成功- 修改第3个顶点的y坐标
将183.61515573074465改为183可以成功- 修改第4个顶点
修改方法同第3个顶点- 所有顶点坐标改为整数
总结:
- 指定碰撞体的坐标点时采用整数表示,别使用小数。
通过如上几个测试用例,无法确定创建碰撞体失败的真正原因,但是推测可能是因为顶点坐标表示为小数,因为精度问题(别再追问为什么,我也不知道,只是感觉)导致引擎内部的一些bug。 - 不支持创建凹多边形碰撞体
虽然官网 (opens new window)有提到支持创建凹多边形的碰撞体。但实际上支持是有问题的,详见冰冰的解释cocos creator不支持创建凹多边形碰撞体 (opens new window)。 - 建议使用try catch
听其它同学讲,在正常使用的情况下,还是偶尔会报如上异常,所以为了不让程序因异常停止运行,加入一个try catch语句做个保护。
# 6. 分辨率适配
若不做分辨率适配,那么可能会造成背景图片的横向显示不全或竖向显示不全。为了能够完整显示背景图片,可以进行如下几步操作,设计分辨率和背景图片的分辨率保持一致,同时开启适配高度模式
// 设计分辨率要和背景图片的大小保持一致,否则会造成图片显示不全。将图片的宽和高设置为设计分辨率即可
cc.view.setDesignResolutionSize(texture.width, texture.height, cc.ResolutionPolicy.FIXED_HEIGHT);
# 7. 顶点坐标系
cocos creator的顶点坐标系有2种坐标系,分别为世界坐标系
、本地坐标系
。
cocos creator的坐标系是与
OpenGL坐标系
和cocos2d-x坐标系
的定义完全相同。与标准屏幕坐标系
不同。下图对比了2者的区别:
此处先做个铺垫: 下一章节即将讲解的cocos creator 纹理坐标系
与标准屏幕坐标系
是一致的,都是以左上角为原点。
若想进一步了解详细内容,请参考Cocos Creator 坐标系 (opens new window)和坐标系和节点变换属性 (opens new window)
# 7.1 锚点Anchor
锚点是用来代表当前节点位置的一个参考点,那么以锚点的坐标来代表所属节点的坐标。
# 7.2 世界坐标系
世界坐标系用来描述节点在场景中的绝对位置。
世界坐标系中的世界
可以理解为场景
, 那么世界坐标系可以理解为以场景
的左下角为原点的坐标系。
取得节点的世界坐标(即当前节点的本地坐标系原点的世界坐标):
# 7.3 本地坐标系
本地坐标系用来描述节点相对于父节点的相对位置。 场景中的任何一个节点都有一个属于自己的独立的坐标系,该坐标系的原点为该节点的锚点。每个子节点的position属性的取值是在父节点的坐标系下的坐标值。
获取节点的本地坐标,对应方法: node.getPosition()。
注意,该方法是以父节点锚点
为原点的。
注意,本地坐标系还有另一种叫法:
节点坐标系
。
因为本地坐标系上的坐标点是相对于父节点的,所以坐标点在屏幕上的位置会随着父节点的移动而移动。
# 7.4 坐标系转换
本地坐标转换为世界坐标
convertToWorldSpace(以节点左下角为原点)或convertToWorldSpaceAR(以锚点为原点)
世界坐标转换为本地坐标
convertToNodeSpace(以节点左下角为原点)或convertToNodeSpaceAR(以锚点为原点)
需要指定转换为哪个节点的本地坐标。
某节点的本地坐标转换为另一个节点的本地坐标
使用世界坐标作为桥梁,实现不同节点的本地坐标的转换。
# 7.5 获取刚体的坐标
使用刚体自带的方法比通过节点获取刚体坐标更快: 使用这些 API 来获取世界坐标系下的旋转位移会比通过节点来获取相关属性更快,因为节点中还需要通过矩阵运算来得到结果,而这些 api 是直接得到结果的。
# 7.5.1 获取刚体世界坐标值
// 直接获取返回值
var out = rigidbody.getWorldPosition();
// 或者通过参数来接收返回值
out = cc.v2();
rigidbody.getWorldPosition(out);
# 7.5.2 局部坐标与世界坐标转换
// 世界坐标转换到局部坐标
var localPoint = rigidbody.getLocalPoint(worldPoint);
// 或者
localPoint = cc.v2();
rigidbody.getLocalPoint(worldPoint, localPoint);
// 局部坐标转换到世界坐标
var worldPoint = rigidbody.getWorldPoint(localPoint);
// 或者
worldPoint = cc.v2();
rigidbody.getLocalPoint(localPoint, worldPoint);
// 局部向量转换为世界向量
var worldVector = rigidbody.getWorldVector(localVector);
// 或者
worldVector = cc.v2();
rigidbody.getWorldVector(localVector, worldVector);
var localVector = rigidbody.getLocalVector(worldVector);
// 或者
localVector = cc.v2();
rigidbody.getLocalVector(worldVector, localVector);
如上代码段中有提到向量,向量是有方向的线段,相关教程请前往
这里提供一个向量在游戏中的具体应用,请前往cocos creator教程之向量的妙用 (opens new window)
# 8. 纹理uv坐标系
纹理uv坐标系的原点在左上角,u轴是向右,v轴是向下,范围是0~1。
# 8.1 纹理坐标的含义
纹理位图
是用来表示物体图案的二维数组,数组的每一个元素都存储了一个颜色值,称为纹理元素。 纹理元素在表示纹理的数组中的二维下标(即它在位图中的二维坐标)称为纹理坐标,一般以字母表示为(u,v),也称为实际纹理坐标。假设位图的宽、高分别为w、h,显然, 0 ≤ u ≤ w,0 ≤ v ≤ h 因为在一个图形显示系统中往往存在多幅不同的纹理,它们的宽、高也不尽相同,用实际纹 理坐标表示纹理元素的位置在计算上很难统一,所以经常使用相对纹理坐标[设为(u′,v′)]代替实际纹理坐标,u′、v′分别是u、v所占宽、高的百分比: u′ = u / w,v′ = v / h 因此,在D3D中用两个0~1的浮点值(U,V)来设置一个点的纹理坐标,U是横轴、V是纵轴。纹理的左上角为(0,0),右下角为(1,1)。
# 8.2 纹理的示例教程
- 初探精灵中的网格渲染模式-Sprite 组件的渲染模式Mesh (opens new window)
- 应用 Sprite 渲染模式 Mesh 和 cc.Graphics ,实现画线纹理 (opens new window)
- 画线纹理之连接优化 (opens new window)
- 画线纹理之绳子 (opens new window)
# 9. 切图
切图有多种方法:
# 9.1 mesh结合切耳法
实现过程详见文档使用mesh和切耳法实现多边形切图或计算多边形面积 (opens new window)
该文档有讲解纹理的应用。
该文档的对应工程源码地址为meshTexture (opens new window)
该工程的creator版本为2.2.2
切耳法的核心代码如下:
if (this.vertexes.length >= 3) {
// 计算顶点索引
let ids = [];
const vertexes = [].concat(this.vertexes);
// 多边形切割,未实现相交的复杂多边形,确保顶点按顺序且围成的线不相交
let index = 0, rootIndex = -1;
while (vertexes.length > 3) {
const p1 = vertexes[index];
const p2 = vertexes[(index + 1) % vertexes.length];
const p3 = vertexes[(index + 2) % vertexes.length];
const v1 = p2.sub(p1);
const v2 = p3.sub(p2);
if (v1.cross(v2) >= 0) {
// 是凸点
let isIn = false;
for (const p_t of vertexes) {
if (p_t !== p1 && p_t !== p2 && p_t !== p3 && this._testInTriangle(p_t, p1, p2, p3)) {
// 其他点在三角形内
isIn = true;
break;
}
}
if (!isIn) {
// 切耳朵,是凸点,且没有其他点在三角形内
ids = ids.concat([this.vertexes.indexOf(p1), this.vertexes.indexOf(p2), this.vertexes.indexOf(p3)]);
vertexes.splice(vertexes.indexOf(p2), 1);
rootIndex = index;
} else {
index = (index + 1) % vertexes.length;
if (index === rootIndex) {
cc.log('循环一圈未发现');
break;
}
}
} else {
index = (index + 1) % vertexes.length;
if (index === rootIndex) {
cc.log('循环一圈未发现');
break;
}
}
// 感谢 @可有 修复
if (index > (vertexes.length - 1)) index = (vertexes.length - 1);
}
ids = ids.concat(vertexes.map(v => { return this.vertexes.indexOf(v) }));
mesh.setIndices(ids);
if (this.renderer.mesh != mesh) {
// mesh 完成后再赋值给 MeshRenderer , 否则模拟器(mac)会跳出
this.renderer.mesh = mesh;
}
} else {
}
# 9.2 mesh结合poly2tri库
核心代码如下:
// 计算顶点索引
const ids = [];
// 多边形切割 poly2tri,支持简单的多边形,确保顶点按顺序且不自交
const countor = this.vertexes.map((p) => { return { x: p.x, y: p.y } });
const swctx = new poly2tri.SweepContext(countor, { cloneArrays: true });
swctx.triangulate();
const triangles = swctx.getTriangles();
triangles.forEach((tri) => {
tri.getPoints().forEach(p => {
const i = countor.indexOf(p as any);
ids.push(i);
});
})
mesh.setIndices(ids);
详细教程请前往使用mesh和poly2tri实现多边形切图或计算多边形面积 (opens new window)
该教程的源码地址为使用mesh和poly2tri实现多边形切图或计算多边形面积 (opens new window)