搓搓小手,开始激动的开发小游戏。
会js就行,别的随缘学习。
JS入门就跳过了。
一. CocosCreator 入门
时长占比最大的手机游戏端引擎。
文档 Introduction · Cocos Creator
1. 生命周期
- onLoad 加载成功时
- onEnable 当组件的
enabled
属性从false
变为true
时,或者所在节点的active
属性从false
变为true
时,会激活onEnable
回调。倘若节点第一次被创建且enabled
为true
,则会在onLoad
之后,start
之前被调用。 - start
start
回调函数会在组件第一次激活前,也就是第一次执行update
之前触发。start
通常用于初始化一些中间状态的数据,这些数据可能在 update 时会发生改变,并且被频繁的 enable 和 disable。 - update 游戏开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在
update
回调中。 - lateUpdate
update
会在所有动画更新前执行,但如果我们要在动效(如动画、粒子、物理等)更新之后才进行一些额外操作,或者希望在所有组件的update
都执行完之后才进行其它操作,那就需要用到lateUpdate
回调。 - onDisable 当组件的
enabled
属性从true
变为false
时,或者所在节点的active
属性从true
变为false
时,会激活onDisable
回调。 - onDestroy 当组件或者所在节点调用了
destroy()
,则会调用onDestroy
回调,并在当帧结束时统一回收组件。
2. 节点
节点有子父关系,子对于父相当于相对定位。子坐标的计算全都基于父节点的本地坐标系。
(1)最简单的就是空节点,可以作为容器承接其他节点,进行统一管理。
(2)3D对象
(3)UI节点
ui节点会自动生成根节点canvas,而且canvas会自动在屏幕中间展示。并且有适配方案。
(4)2D节点
(5)除了常见的渲染节点外,很多时候也会创建逻辑节点用来挂载脚本。
3. quick Start
脚本
GameManger.Ts
import { _decorator, Component, Prefab, instantiate, Node, Label, CCInteger, Vec3 } from 'cc';
import { PlayerController } from "./PlayerController";
const { ccclass, property } = _decorator;
// 赛道格子类型,坑(BT_NONE)或者实路(BT_STONE)
enum BlockType {
BT_NONE,
BT_STONE,
};
enum GameState {
GS_INIT,
GS_PLAYING,
GS_END,
};
@ccclass("GameManager")
export class GameManager extends Component {
// 赛道预制
@property({ type: Prefab })
public cubePrfb: Prefab | null = null;
// 赛道长度
@property({ type: CCInteger })
public roadLength: Number = 50;
private _road: BlockType[] = [];
// 主界面根节点
@property({ type: Node })
public startMenu: Node | null = null;
// 关联 Player 节点身上 PlayerController 组件
@property({ type: PlayerController })
public playerCtrl: PlayerController | null = null;
// 关联步长文本组件
@property({ type: Label })
public stepsLabel: Label | null = null!;
start() {
this.curState = GameState.GS_INIT;
this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this);
}
init() {
// 激活主界面
if (this.startMenu) {
this.startMenu.active = true;
}
// 生成赛道
this.generateRoad();
if (this.playerCtrl) {
// 禁止接收用户操作人物移动指令
this.playerCtrl.setInputActive(false);
// 重置人物位置
this.playerCtrl.node.setPosition(Vec3.ZERO);
// 重置已经移动的步长数据
this.playerCtrl.reset();
}
}
set curState(value: GameState) {
switch (value) {
case GameState.GS_INIT:
this.init();
break;
case GameState.GS_PLAYING:
if (this.startMenu) {
this.startMenu.active = false;
}
if (this.stepsLabel) {
this.stepsLabel.string = '0'; // 将步数重置为0
}
// 会出现的现象就是,游戏开始的瞬间人物已经开始移动
// 因此,这里需要做延迟处理
setTimeout(() => {
if (this.playerCtrl) {
this.playerCtrl.setInputActive(true);
}
}, 0.1);
break;
case GameState.GS_END:
break;
}
}
generateRoad() {
// 防止游戏重新开始时,赛道还是旧的赛道
// 因此,需要移除旧赛道,清除旧赛道数据
this.node.removeAllChildren();
this._road = [];
// 确保游戏运行时,人物一定站在实路上
this._road.push(BlockType.BT_STONE);
// 确定好每一格赛道类型
for (let i = 1; i < this.roadLength; i++) {
// 如果上一格赛道是坑,那么这一格一定不能为坑
if (this._road[i - 1] === BlockType.BT_NONE) {
this._road.push(BlockType.BT_STONE);
} else {
this._road.push(Math.floor(Math.random() * 2));
}
}
// 根据赛道类型生成赛道
let linkedBlocks = 0;
for (let j = 0; j < this._road.length; j++) {
if (this._road[j]) {
++linkedBlocks;
}
if (this._road[j] == 0) {
if (linkedBlocks > 0) {
this.spawnBlockByCount(j - 1, linkedBlocks);
linkedBlocks = 0;
}
}
if (this._road.length == j + 1) {
if (linkedBlocks > 0) {
this.spawnBlockByCount(j, linkedBlocks);
linkedBlocks = 0;
}
}
}
}
spawnBlockByCount(lastPos: number, count: number) {
let block: Node | null = this.spawnBlockByType(BlockType.BT_STONE);
if (block) {
this.node.addChild(block);
block?.setScale(count, 1, 1);
block?.setPosition(lastPos - (count - 1) * 0.5, -1.5, 0);
}
}
spawnBlockByType(type: BlockType) {
if (!this.cubePrfb) {
return null;
}
let block: Node | null = null;
switch (type) {
case BlockType.BT_STONE:
block = instantiate(this.cubePrfb);
break;
}
return block;
}
onStartButtonClicked() {
// 点击主界面 play 按钮,开始游戏
this.curState = GameState.GS_PLAYING;
}
checkResult(moveIndex: number) {
if (moveIndex < this.roadLength) {
console.log('跳到了坑上');
if (this._road[moveIndex] == BlockType.BT_NONE) {
this.curState = GameState.GS_INIT;
}
} else {
console.log('跳过了最大长度');
this.curState = GameState.GS_INIT;
}
}
onPlayerJumpEnd(moveIndex: number) {
if (this.stepsLabel) {
// 因为在最后一步可能出现步伐大的跳跃,但是此时无论跳跃是步伐大还是步伐小都不应该多增加分数
this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex);
}
// 检查当前下落道路的类型,获取结果
this.checkResult(moveIndex);
}
// update (deltaTime: number) {
// // Your update function goes here.
// }
}
PlayerController.Ts
import { _decorator, Component, Vec3, input, Input, EventMouse, Animation, SkeletalAnimation } from 'cc';
const { ccclass, property } = _decorator;
@ccclass("PlayerController")
export class PlayerController extends Component {
@property({type: Animation})
public BodyAnim: Animation|null = null;
@property({type: SkeletalAnimation})
public CocosAnim: SkeletalAnimation|null = null;
// for fake tween
private _startJump: boolean = false;
private _jumpStep: number = 0;
private _curJumpTime: number = 0;
private _jumpTime: number = 0.3;
private _curJumpSpeed: number = 0;
private _curPos: Vec3 = new Vec3();
private _deltaPos: Vec3 = new Vec3(0, 0, 0);
private _targetPos: Vec3 = new Vec3();
private _curMoveIndex = 0;
start () {
}
reset() {
this._curMoveIndex = 0;
}
setInputActive(active: boolean) {
if (active) {
input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
} else {
input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this);
}
}
onMouseUp(event: EventMouse) {
if (event.getButton() === 0) {
this.jumpByStep(1);
} else if (event.getButton() === 2) {
this.jumpByStep(2);
}
}
jumpByStep(step: number) {
if (this._startJump) {
return;
}
this._startJump = true;
this._jumpStep = step;
this._curJumpTime = 0;
this._curJumpSpeed = this._jumpStep / this._jumpTime;
this.node.getPosition(this._curPos);
Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0));
if (this.CocosAnim) {
this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; //跳跃动画时间比较长,这里加速播放
this.CocosAnim.play('cocos_anim_jump'); //播放跳跃动画
}
if (this.BodyAnim) {
if (step === 1) {
this.BodyAnim.play('oneStep');
} else if (step === 2) {
this.BodyAnim.play('twoStep');
}
}
this._curMoveIndex += step;
}
onOnceJumpEnd() {
if (this.CocosAnim) {
this.CocosAnim.play('cocos_anim_idle');
}
this.node.emit('JumpEnd', this._curMoveIndex);
}
update (deltaTime: number) {
if (this._startJump) {
this._curJumpTime += deltaTime;
if (this._curJumpTime > this._jumpTime) {
// end
this.node.setPosition(this._targetPos);
this._startJump = false;
this.onOnceJumpEnd();
} else {
// tween
this.node.getPosition(this._curPos);
this._deltaPos.x = this._curJumpSpeed * deltaTime;
Vec3.add(this._curPos, this._curPos, this._deltaPos);
this.node.setPosition(this._curPos);
}
}
}
}
4. 资源释放
不同场景的资源默认不会自动释放,如果都不释放的话内存所占会越来月高,最简单的方法就是再场景上勾上自动释放资源。
当然也可以防止特定资源被自动释放
具体操作 场景资源 · Cocos Creator
5. 导入资源
(1)资源工作流:
三种导入的方式:直接添加 复制 或者拖进来
不推荐直接在 操作系统的文件管理器 对资源文件进行操作,如有操作,请同步处理相应的 .meta
文件,如下建议:
- 关闭正在使用的编辑器,避免因为文件锁定或资源名称相同导致更新失败。
- 删除,重命名,移动资源时,请连同
.meta
文件一起删除,重命名,移动。 - 复制资源时如果连同
.meta
文件一起复制,将直接使用复制进来的.meta
文件,而不是再生成新的.meta
文件;如果只复制了资源文件,则会生成对应名称的新的.meta
文件。
(2)图像资源
常用的图片资源类型:
纹理贴图资源(Texture)
纹理贴图资源是一种用于程序采样的资源,如模型上的贴图、精灵上的 UI。当程序渲染 UI 或者模型时,会使用纹理坐标获取纹理颜色,然后填充在模型网格上,再加上光照等等一系列处理便渲染出了整个场景。
----Texture2D
Texture2D 是纹理贴图资源的一种,通常用于 3D 模型的渲染,如模型材质中的反射贴图、环境光遮罩贴图等等。
在将图像资源 导入 到 Creator 后,即可在 属性检查器 面板将其设置为 texture 类型,texture 类型便是 Texture2D 纹理资源。
----genMipmaps
为了加快 3D 场景渲染速度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的序列,这样的贴图被称为 mipmap。mipmap 中每一个层级的小图都是原图的一个特定比例的缩小细节的复制品,当贴图被缩小或者只需要从远距离观看时,mipmap 就会转换到适当的层级。
当 Texture2D 的 Mip Filter 属性设置为 nearest 或者 linear 时,会在两个相近的层级之间插值,自动生成 mipmap(仅对非压缩格式生效)。因为渲染远距离物体时,mipmap 贴图比原图小,提高了显卡采样过程中的缓存命中率,所以渲染的速度得到了提升。同时因为 mipmap 的小图精度较低,从而减少了摩尔纹现象,可以减少画面上的锯齿。另外因为额外生成了一些小图,所以 mipmap 需要额外占用约三分之一的内存空间。
精灵帧资源(SpriteFrame)
Sprite 组件剪裁相关设置详解
和图片裁剪相关的 Sprite 组件设置有以下两个:
-
Trim
勾选后将在渲染 Sprite 图像时去除图像周围的透明像素,我们将看到刚好能把图像包裹住的约束框。取消勾选,Sprite 节点的约束框会包括透明像素的部分。 -
Size Mode
用来将节点的尺寸设置为原图或原图裁剪透明像素后的大小,通常用于在序列帧动画中保证图像显示为正确的尺寸。有以下几种选择:-
TRIMMED
选择该选项,会将节点的尺寸(size)设置为原始图片裁剪掉透明像素后的大小。 -
RAW
选择该选项,会将节点尺寸设置为原始图片包括透明像素的大小。 -
CUSTOM
自定义尺寸,用户在使用 矩形变换工具 拖拽改变节点的尺寸,或通过修改Size
属性,或在脚本中修改width
或height
后,都会自动将Size Mode
设为CUSTOM
。表示用户将自己决定节点的尺寸,而不需要考虑原始图片的大小。
-
立方体贴图
TextureCube 为立方体纹理,常用于设置场景的 天空盒。立方体贴图可以通过设置全景图 ImageAsset 为 TextureCube 类型获得,也可以在 Creator 中制作生成。
合成图集
在游戏中使用多张图片合成的图集作为美术资源,有以下优势:
- 合成图集时会去除每张图片周围的空白区域,加上可以在整体上实施各种优化算法,合成图集后可以大大减少游戏包体和内存占用
- 多个 Sprite 如果渲染的是来自同一张图集的图片时,这些 Sprite 可以使用同一个渲染批次来处理,大大减少 CPU 的运算时间,提高运行效率。
(3)预制件(Prefab)
预制件用于存储一些可以复用的场景对象,它可以包含节点、组件以及组件上的数据。由预制件生成的实例既可以继承模板的数据,又可以有自己定制化的数据修改。
二. 脚本使用
(1)编译环境 跳过
(2)组件装饰器
1.executeInEditMode
默认情况下,所有组件都只会在运行时执行,也就是说它们的生命周期回调在编辑器模式下并不会触发。executeInEditMode
允许当前组件在编辑器模式下运行,默认值为 false
。
2. requireComponent
requireComponent
参数用来指定当前组件的依赖组件,默认值为 null
。当组件添加到节点上时,如果依赖的组件不存在,引擎会自动将依赖组件添加到同一个节点,防止脚本出错。该选项在运行时同样有效。
3. executionOrder
executionOrder
用来指定脚本生命周期回调的执行优先级。小于 0 的脚本将优先执行,大于 0 的脚本将最后执行。排序方式如下:
- 对于同一节点上的不同组件,数值小的先执行,数值相同的按组件添加先后顺序执行
- 对于不同节点上的同一组件,按节点树排列决定执行的先后顺序
该优先级设定只对 onLoad
、onEnable
、start
、update
和 lateUpdate
有效,对 onDisable
和 onDestroy
无效。
4. 属性装饰器
import { _decorator, Component, Node, Color, RealCurve, Gradient } from 'cc';
const { ccclass, property, type, integer, float } = _decorator;
@ccclass('HelloWord')
export class HelloWord extends Component {
//加了注释会在cocos中图形化展示
@type(Node)
testNode: Node | null = null;
@integer
testInter = 2
@float
testFloat = 3.6
//颜色选择器
// @property(Color)
// color: Color
//曲线选择器
// @property(RealCurve)
// realCurve: RealCurve = new RealCurve();
//渐变色选择
// @property(Gradient)
// gradient = new Gradient();
start() {
console.log("hello world");
}
update(deltaTime: number) {
}
}
一般情况下,属性是否显示在 属性检查器 中取决于属性名是否以 _
开头。如果是以 _
开头,则不显示。
如果要强制显示在 属性检查器 中,可以设置 visible
参数为 true:
@property({ visible: true })
private _num = 0;
(3)在脚本中访问节点
1. 获得组件所在的节点
start() {
let node = this.node;
node.setPosition(0.0, 0.0, 0.0);
}
2. 获得其它组件
如果你经常需要获得同一个节点上的其它组件,这就要用到 getComponent
这个 API,它会帮你查找你要的组件。
import { _decorator, Component, Label } from 'cc';
const { ccclass, property } = _decorator;
@ccclass("test")
export class test extends Component {
private label: any = null
start() {
this.label = this.getComponent(Label);
let text = this.name + 'started';
// Change the text in Label Component
this.label.string = text;
}
}
下面这俩等价
this.node.getComponent(Label) === this.getComponent(Label)
3. 查找子节点
有时候,游戏场景中会有很多个相同类型的对象,像是炮塔、敌人和特效,它们通常都有一个全局的脚本来统一管理。如果用 属性检查器 来一个一个将它们关联到这个脚本上,那工作就会很繁琐。为了更好地统一管理这些对象,我们可以把它们放到一个统一的父物体下,然后通过父物体来获得所有的子物体:
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass("CannonManager")
export class CannonManager extends Component {
start() {
let cannons = this.node.children;
//...
}
}
4. 节点常用api
(1)active属性 是否禁用该节点及其子节点。
(2)更改父节点
this.node.parent = parentNode;
this.node.removeFromParent(false);
parentNode.addChild(this.node);
(3)更改节点位置
-
使用
setPosition
方法:this.node.setPosition(100, 50, 100);
this.node.setPosition(new Vec3(100, 50, 100));
-
设置
position
变量:this.node.position = new Vec3(100, 50, 100)
(4)更改节点旋转
this.node.setRotation(90, 90, 90);
或通过欧拉角设置本地旋转:
this.node.setRotationFromEuler(90, 90, 90);
(5)更改节点缩放
this.node.setScale(2, 2, 2);
(6)新版本的计时器
比js那俩更灵活一些
(7)组件执行优先级
大体分为两种方案:要么设置一个总的脚本去控制其他脚本的执行顺序,
要么在组件类上添加注释设置执行的order
(4)场景的加载和切换
在 Cocos Creator 中,我们使用场景文件名(不包含扩展名)来索引指代场景。并通过以下接口进行加载和切换操作:
director.loadScene("MyScene");
Asset Bundle 提供的 loadScene
只会加载指定 bundle 中的场景,并不会自动运行场景,还需要使用 director.runScene
来运行场景。
引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。我们使用以下接口:
director.addPersistRootNode(myNode);
上面的接口会将 myNode
变为常驻节点,这样挂在上面的组件都可以在场景之间持续作用,我们可以用这样的方法来储存玩家信息,或下一个场景初始化时需要的各种数据。 需要注意的是,目标节点必须为位于层级的根节点,否则设置无效。
如果要取消一个节点的常驻属性:
director.removePersistRootNode(myNode);
需要注意的是上面的 API 并不会立即销毁指定节点,只是将节点还原为可在场景切换时销毁的节点。
加载场景时,可以附加一个参数用来指定场景加载后的回调函数:
director.loadScene("MyScene", onSceneLaunched);
上一行里 onSceneLaunched
就是声明在本脚本中的一个回调函数,在场景加载后可以用来进一步的进行初始化或数据传递的操作。
由于回调函数只能写在本脚本中,所以场景加载回调通常用来配合常驻节点,在常驻节点上挂载的脚本中使用。
同样的我们也可以预加载场景
director.loadScene
会在加载场景之后自动切换运行新场景,有些时候我们需要在后台静默加载新场景,并在加载完成后手动进行切换。那就可以预先使用 preloadScene
接口对场景进行预加载:
director.preloadScene("table", function () {
console.log('Next scene preloaded');
});
之后在合适的时间调用 loadScene
,就可以真正切换场景。
director.loadScene("table");
就算预加载还没完成,你也可以直接调用 director.loadScene
,预加载完成后场景就会启动。
(5)事件系统
Cocos Creator 引擎提供了 EventTarget
类,用以实现自定义事件的监听和发射,在使用之前,需要先从 'cc'
模块导入,同时需要实例化一个 EventTarget
对象。
import { EventTarget } from 'cc';
const eventTarget = new EventTarget();
这玩意熟悉发布订阅模式的随便虐
监听 on once 卸载 off 发布 emit
内置事件(这是新的得记住)
输入系统事件
节点事件
三, 功能模块
1. 图像渲染
(1). 渲染管线
Cocos Creator 3.1 的内置渲染管线包括 builtin-forward(前向渲染管线)和 builtin-deferred(延迟渲染管线)。渲染管线可通过编辑器主菜单中的 项目 -> 项目设置 -> 项目数据 -> 渲染管线 进行设置,设置完成之后 重启编辑器 即可生效。
延迟的兼容性不是特别好,对于卡通材质的绘制也有问题,实验阶段只能说是。
(2)相机
(3) 光照
光学度量单位(Photometric Unit) 是用来计算光的强弱(大小)和方向的一门科学:
-
光通量(Luminous Flux)
单位 流明(lm),单位时间内光源所发出或者被照物体所接收的总光能。改变光源大小不会影响场景照明效果。
-
亮度(Luminance)
单位 坎德拉每平方米(cd/m2),单位面积光源在给定方向上,在每单位面积内所发出的总光通量。改变光源大小会影响场景照明效果。
-
照度(Illuminance)
单位 勒克斯(lux 或 lx),每单位面积所接收到的光通量。该值受光的传播距离影响,对于同样光源而言,当光源的距离为原先的两倍时,照度减为原先的四分之一,呈平方反比关系。
在真实世界中,由于描述光源的重要物理参数不一样,我们通常用 光通量(Luminous Flux) 和 亮度(Luminance) 来描述生活中常见的带有照明面积的光源,用 照度(Illuminance) 来描述太阳光。
光源的类型:
平行光:
平行光又称为方向光(Directional Light),是最常用的一种光源,模拟了无限远处的光源发出的光线,常用于实现太阳光。
球面光:
球面光会向所有方向均匀地发散光线,接近于蜡烛产生的光线。物体受到的光照强度会随着跟光源距离的增大而减弱,当距离超过设置的光照影响范围,则光照强度为 0。
在实际应用中可用于模拟火把、蜡烛、灯泡等光源,照亮四周一定距离内的环境。
聚光灯:
聚光灯 是由一个点向一个方向发射一束锥形光线,类似于手电筒或舞台照明灯产生的光线。与其他光源相比,聚光灯多了 SpotAngle
属性,用于调整聚光灯的光照范围
环境光
在生活中,错综复杂的光线与凹凸不平的物体表面相互反射,使得整个环境都被照亮,仿佛被一层光均匀笼罩,这个光一般称为 环境光,也称为 漫射环境光。
因为环境光可以均匀地照亮场景中的所有物体,常用于解决模型背光面全黑的问题,一般需要配合其他类型的光源一起使用。例如场景中只有一个平行光,那么在模型的背光源处会显得非常暗,加入环境光则可以提升模型背部的亮度,显得更加美观。
(4)阴影
在 3D 世界中,光与影一直都是极其重要的组成部分,它们能够丰富整个环境,质量好的阴影可以达到以假乱真的效果,并且使得整个世界具有立体感。
Creator 3.0 目前支持 Planar 和 ShadowMap 两种阴影类型。
(5)光照贴图
烘焙系统会对光源稳定的静态物体所受到的光照和阴影等进行预先计算,计算产生的结果存放在一张纹理贴图中,这张贴图我们称之为 光照贴图。
生成的光照贴图 Creator 会在运行时自动处理并使用。在光源固定的场景中,使用光照贴图代替实时的光照计算,可以减少资源消耗,从而提高场景运行效率。
2. 网格
(1)MeshRenderer
(网格渲染器)组件用于显示一个静态的 3D 模型。通过 Mesh 属性设置模型网格,通过 Materials 属性控制模型的显示外观。
在 属性检查器 中点击 添加组件 -> Mesh -> MeshRenderer 即可添加 MeshRenderer 组件。
(2)蒙皮网格渲染器组件(SkinnedMeshRenderer)
蒙皮网格渲染器组件(SkinnedMeshRenderer)主要用于渲染蒙皮模型网格。
导入模型资源 后,若模型网格中带有蒙皮信息,在使用模型时,SkinnedMeshRenderer 组件便会自动添加到模型节点上。