Cocos Creator 3D 入门教程:快速上手,制作第一款游戏!

作者:CocosEngine
2019-11-09
3 1 0

快速上手制作你的第一个游戏

Cocos Creator 3D 编辑器的强大之处就是可以让开发者快速地制作游戏原型,下面我们将跟随教程制作一款名叫 《一步两步》 的魔性小游戏。这款游戏非常考验玩家的反应能力,玩家根据路况选择是要跳一步还是跳两步,“一步两步,一步两步,一步一步似爪牙似魔鬼的步伐”。

体验游戏(仅支持桌面鼠标操作):

https://gameu3d.github.io/MindYourStep_Tutorial/index.html

Cocos Creator 3D 下载地址:

https://forum.cocos.com/t/cocos-creator-3d-beta/82849

一、新建项目

如果您还不了解如何获取和启动 Cocos Creator 3D,请阅读 [安装和启动] 一节。

  1. 首先启动 Cocos Creator 3D,然后新建一个名为 MindYourStep 的项目,如果不知道如果创建项目,请阅读 [Hello World!]
  2. 新建项目后会看到如下的编辑器界面:

二、创建游戏场景

在 Cocos Creator 3D 中,游戏场景(Scene)是开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体,游戏场景中一般会包括以下内容:

  • 场景物体
  • 角色
  • UI
  • 以组件形式附加在场景节点上的游戏逻辑脚本

当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本,实现各种各样开发者设置的逻辑功能。所以除了资源以外,游戏场景是一切内容创作的基础。

现在,让我们来新建一个场景。

1. 在 资源管理器 中点击选中 asset 目录,点击 资源管理器 左上角的 加号 按钮,选择文件夹,命名为 Scenes

2. 点击先中 Scenes 目录(下图把一些常用的文件夹都提前创建好了),点击鼠标右键,在弹出的菜单中选择 场景文件。

3. 我们创建了一个名叫 New Scene 的场景文件,创建完成后场景文件 New Scene 的名称会处于编辑状态,将它重命名为 Main

4. 双击 Main,就会在场景编辑器和层级管理器中打开这个场景。

三、添加跑道

我们的主角需要在一个由方块(Block)组成的跑道上从屏幕左边向右边移动。我们使用编辑器自带的立方体(Cube)来组成道路。

1. 在 层级管理器 中创建一个立方体(Cube),并命名为 Cube

2. 选中 Cube,按 Ctrl+D 来复制出 3 个 Cube

3. 将 3 个 Cube 按以下坐标排列:第一个节点位置(0,-1.5,0),第二个节点位置(1,-1.5,0),第三个节点位置(2,-1.5,0)。

效果如下:


四、添加主角

1、创建主角节点

首先创建一个名字为 Player 的空节点,然后在这个空节点下创建名为 Body 的主角模型节点,为了方便,我们采用编辑器自带的胶囊体模型做为主角模型。

分为两个节点的好处是,我们可以使用脚本控制 Player 节点来使主角进行水平方向移动,而在 Body 节点上做一些垂直方向上的动画(比如原地跳起后下落),两者叠加形成一个跳越动画。

Player 节点设置在(0,0,0)位置,使得它能站在第一个方块上。效果如下:

2、编写主角脚本

想要主角影响鼠标事件来进行移动,我们就需要编写自定义的脚本。如果您从没写过程序也不用担心,我们会在教程中提供所有需要的代码,只要复制粘贴到正确的位置就可以了,之后这部分工作可以找您的程序员小伙伴来解决。下面让我们开始创建驱动主角行动的脚本吧。

(1)创建脚本

1. 如果还没有创建 Scripts 文件夹,首先在 资源管理器 中右键点击 assets 文件夹,选择 新建 -> 文件夹,重命名为 Scripts。

2. 右键点击 Scripts 文件夹,选择 新建 -> TypeScript ,创建一个 TypeScript 脚本,有关 TypeScript 资料可以查看[TypeScript 官方网站]

3. 将新建脚本的名字改为 PlayerController,双击这个脚本,打开代码编辑器,例如 VSCode。


注意:Cocos Creator 3D 中脚本名称就是组件的名称,这个命名是大小写敏感的!如果组件名称的大小写不正确,将无法正确通过名称使用组件!

(2)编写脚本代码

在打开的 PlayerController 脚本里已经有了预先设置好的一些代码块,如下所示:

import { _decorator, Component } from "cc";
const { ccclass, property } = _decorator;

@ccclass("PlayerController")
export class PlayerController extends Component {

/* class member could be defined like this */
// dummy = '';

/* use `property` decorator if your want the member to be serializable */
// @property
// serializableDummy = 0;

start () {
	// Your initialization goes here.
}

// update (deltaTime: number) {
// // Your update function goes here.
// }
}

这些代码就是编写一个组件(脚本)所需的结构,具有这样结构的脚本就是 Cocos Creator 3D 中的组件(Component),他们能够挂载到场景中的节点上,提供控制节点的各种功能,更详细的脚本信息可以查看 [脚本]

我们在脚本中添加对鼠标事件的监听,然后让 Player 动起来,将 PlayerController 中代码做如下修改:

import {_decorator, Component, Vec3, systemEvent, SystemEvent, EventMouse, AnimationComponent }from "cc"; 
const {ccclass, property } = _decorator; 

@ccclass("PlayerController")
export class PlayerController extends Component {
    /* class member could be defined like this */
    // dummy = '';

    /* use `property` decorator if your want the member to be serializable */
    // @property
    // serializableDummy = 0;

    // for fake tween
    private _startJump:boolean = false; 
    private _jumpStep:number = 0; 
    private _curJumpTime:number = 0; 
    private _jumpTime:number = 0.1; 
    private _curJumpSpeed:number = 0; 
    private _curPos:Vec3 = cc.v3(); 
    private _deltaPos:Vec3 = cc.v3(0, 0, 0); 
    private _targetPos:Vec3 = cc.v3(); 
    private _isMoving = false; 

    start () {
    // Your initialization goes here.
    systemEvent.on(SystemEvent.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._isMoving) {
            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, cc.v3(this._jumpStep, 0, 0)); 

        this._isMoving = true; 
    }

    onOnceJumpEnd() {
        this._isMoving = false; 
    }

    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); 
            }
        }
    }
}

现在我们可以把 PlayerController 组件添加到主角节点上。在 层级管理器 中选中  Player 节点,然后在 属性检查器 中点击 添加组件 按钮,选择 添加用户脚本组件 -> PlayerController ,为主角节点添加 PlayerController 组件。

为了能在运行时看到物体,我们需要将场景中的 Camera 进行一些参数调整,将位置放到(0,0,13),Color 设置为(50,90,255,255):

现在点击工具栏中心位置的 Play 按钮:在打开的网页中点击鼠标左键和右键,可以看到如下画面:

更多的预览功能,可以参考 [项目预览调试]

3、添加角色动画

从上面运行的结果可以看到单纯对 Player 进行水平方向的移动是十分呆板的,我们要让 Player 跳跃起来才比较有感觉,我们可以通过为角色添加垂直方向的动画来达到这个效果。有关 动画编辑器 的更多信息,请阅读 [动画编辑器]

(1)选中场景中的 Body 节点,编辑器下方 控制台 边上的 动画编辑器,添加 Animation 组件并创建 Clip,命名为 oneStep

(2)进入动画编辑模式,添加 position 属性轨道,并添加三个关键帧,position 值分别为(0,0,0)、(0,0.5,0)、(0,0,0)。

退出动画编辑模式前前记得要保存动画,否则做的动画就白费了。

(3)我们还可以通过 资源管理器 来创建 Clip,下面我们创建一个名为 twoStepClip 并将它添加到 Body 身上的 AnimationComponent 上,这里为了录制方便调整了一下面板布局。

(4)进入动画编辑模式,选择并编辑 twoStepclip,类似第 2 步,添加三个 position 的关键帧,分别为(0,0,0)、(0,1,0)、(0,0,0)。

(5) 在 PlayerController 组件中引用动画组件,我们需要在代码中根据跳的步数不同来播放不同的动画。

首先需要在 PlayerController 组件中引用 Body 身上的 AnimationComponent

@property({type: AnimationComponent})

public BodyAnim: AnimationComponent = null;

然后在属性检查器中将 Body 身上的 AnimationComponent 拖到这个变量上。

在跳跃的函数 jumpByStep 中加入动画播放的代码:

if (step === 1) {
	this.BodyAnim.play('oneStep');
} else if (step === 2) {
	this.BodyAnim.play('twoStep');
}

点击 Play 按钮,点击鼠标左键、右键,可以看到新的跳跃效果:

五、跑道升级

为了让游戏有更久的生命力,我们需要一个很长的跑道来让 Player 在上面一直往右边跑,在场景中复制一堆 Cube 并编辑位置来组成跑道显然不是一个明智的做法,我们通过脚本完成跑道的自动创建。

1、游戏管理器(GameManager)

一般游戏都会有一个管理器,主要负责整个游戏生命周期的管理,可以将跑道的动态创建代码放到这里。在场景中创建一个名为 GameManager 的节点,然后在  assets/Scripts 中创建一个名为 GameManager 的 ts 脚本文件,并将它添加到 GameManager 节点上。

2、制作 Prefab

对于需要重复生成的节点,我们可以将他保存成 Prefab(预制)资源,作为我们动态生成节点时使用的模板。关于 Prefab 的更多信息,请阅读 [预制资源(Prefab)]

我们将生成跑道的基本元素 正方体(Cube)制作成 Prefab,之后可以把场景中的三个 Cube 都删除了。

3、添加自动创建跑道代码

我们需要一个很长的跑道,理想的方法是能动态增加跑道的长度,这样可以永无止境的跑下去,这里为了方便我们先生成一个固定长度的跑道,跑道长度可以自己定义。跑道上会生成一些坑,跳到坑上就 GameOver 了。

GameManager 脚本中代码替换成以下代码:

import { _decorator, Component, Prefab, instantiate, Node, CCInteger} from "cc";
const { ccclass, property } = _decorator;

enum BlockType{
    BT_NONE,
    BT_STONE,
};

@ccclass("GameManager")
export class GameManager extends Component {

    @property({type: Prefab})
    public cubePrfb: Prefab = null;
    @property({type: CCInteger})
    public roadLength: Number = 50;
    private _road: number[] = [];

    start () {
        this.generateRoad();
    }

    generateRoad() {

        this.node.removeAllChildren(true);

        this._road = [];
        // startPos
        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));
            }
        }

        for (let j = 0; j < this._road.length; j++) {
            let block: Node = this.spawnBlockByType(this._road[j]);
            if (block) {
                this.node.addChild(block);
                block.setPosition(j, -1.5, 0);
            }
        }
    }

    spawnBlockByType(type: BlockType) {
        let block = null;
        switch(type) {
            case BlockType.BT_STONE:
                block = instantiate(this.cubePrfb);
                break;
        }

        return block;
    }

    // update (deltaTime: number) {
    // // Your update function goes here.
    // }
}

GameManager 的 inspector 面板中可以通过修改 roadLength 的值来改变跑道的长度。预览可以看到现在自动生成了跑道,不过因为 Camera 没有跟随 Player 移动,所以看不到后面的跑道,我们可以将场景中的 Camera 设置为 Player 的子节点。

这样 Camera 就会跟随 Player 的移动而移动,现在预览可以从头跑到尾的观察生成的跑道了。

六、增加开始菜单

开始菜单是游戏不可或缺的一部分,我们可以在这里加入游戏名称、游戏简介、制作人员等信息。

1、添加一个名为 Play 的按钮

这个操作生成了一个 Canvas 节点,一个 PlayButton 节点和一个 Label 节点。因为 UI 组件需要在带有 CanvasComponent  的父节点下才能显示,所以编辑器在发现目前场景中没有带这个组件的节点时会自动添加一个。

创建按钮后,将 Label 节点上的 cc.LabelComponentString 属性从 Button 改为 Play

2、在 Canvas 底下创建一个名字为 StartMenu 的空节点,将 PlayButton 拖到它底下。我们可以通过点击工具栏上的 2D/3D 按钮来切换到 2D 编辑视图下进行 UI 编辑操作,详细的描述请查阅 [场景编辑]

3、增加一个背景框,在 StartMenu 下新建一个名字为 BGSprite 节点,调节它的位置到 PlayButton 的上方,设置它的宽高为(200,200),并将它的 SpriteFrame 设置为 internal/default_ui/default_sprite_splash

4、添加一个名为 TitleLabel 用于开始菜单的标题。

5、修改 Title 的文字,并调整 Title 的位置、文字大小、颜色。

6、增加操作的 Tips,然后调整 PlayButton 的位置,一个简单的开始菜单就完成了。

7、增加游戏状态逻辑,一般我们可以将游戏分为三个状态:

  • 初始化(Init):显示游戏菜单,初始化一些资源。
  • 游戏进行中(Playing):隐藏游戏菜单,玩家可以操作角度进行游戏。
  • 结束(End):游戏结束,显示结束菜单。

使用一个枚举(enum)类型来表示这几个状态。

enum BlockType{
	BT_NONE,
	BT_STONE,
};

enum GameState{
	GS_INIT,
	GS_PLAYING,
	GS_END,
};

GameManager 脚本中加入表示当前状态的私有变量

private _curState: GameState = GameState.GS_INIT;

为了在开始时不让用户操作角色,而在游戏进行时让用户操作角色,我们需要动态的开启和关闭角色对鼠标消息的监听。所以对 PlayerController 做如下的修改:

start () {
    // Your initialization goes here.
    //systemEvent.on(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this);
}

setInputActive(active: boolean) {
    if (active) {
        systemEvent.on(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this);
    } else {
        systemEvent.off(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this);
    }
}

然后需要在 GameManager 脚本中引用 PlayerController,需要在 Inspector 中将场景的 Player 拖入到这个变量中。

@property({type: PlayerController})
public playerCtrl: PlayerController = null;

为了动态的开启\关闭开启菜单,我们需要在 GameManager 中引用 StartMenu 节点,需要在 Inspector 中将场景的 StartMenu 拖入到这个变量中。

@property({type: Node})
public startMenu: Node = null;

增加状态切换代码,并修改 GameManger 的初始化方法:

start () {
    this.curState = GameState.GS_INIT;
}

init() {
    this.startMenu.active = true;
    this.generateRoad();
    this.playerCtrl.setInputActive(false);
    this.playerCtrl.node.setPosition(cc.v3());
}

set curState (value: GameState) {
    switch(value) {
        case GameState.GS_INIT:
            this.init();
            break;
        case GameState.GS_PLAYING:
            this.startMenu.active = false;
            setTimeout(() => { //直接设置 active 会直接开始监听鼠标事件,做了一下延迟处理
                this.playerCtrl.setInputActive(true);
            }, 0.1);
            break;
        case GameState.GS_END:
            break;
    }
    this._curState = value;
}

8、添加对 Play 按钮的事件监听。

为了能在点击 Play 按钮后开始游戏,我们需要对按钮的点击事件做出响应。在 GameManager 脚本中加入响应按钮点击的代码,在点击后进入游戏的 Playing 状态:

onStartButtonClicked() {
    this.curState = GameState.GS_PLAYING;
}

然后在 Play 按钮的 Inspector 上添加 ClickEvents 的响应函数。

现在预览场景就可以点击 Play 按钮开始游戏了。

七、添加游戏结束逻辑

目前游戏角色只是呆呆的往前跑,我们需要添加游戏规则,来让他跑的更有挑战性。

1、角色每一次跳跃结束需要发出消息,并将自己当前所在位置做为参数发出消息。在 PlayerController 中记录自己跳了多少步:

private _curMoveIndex = 0;
// ...
jumpByStep(step: number) {
    // ...
    
    this._curMoveIndex += step;
}

在每次跳跃结束发出消息:

onOnceJumpEnd() {
    this._isMoving = false;
    this.node.emit('JumpEnd', this._curMoveIndex);
}

2. 在 GameManager 中监听角色跳跃结束事件,并根据规则判断输赢,增加失败和结束判断,如果跳到空方块或是超过了最大长度值都结束:

checkResult(moveIndex: number) {
    if (moveIndex <= this.roadLength) {
        if (this._road[moveIndex] == BlockType.BT_NONE) { //跳到了空方块上
            this.curState = GameState.GS_INIT;
        }
    } else { // 跳过了最大长度
        this.curState = GameState.GS_INIT;
    }
}

监听角色跳跃消息,并调用判断函数:

start () {
    this.curState = GameState.GS_INIT;
    this.playerCtrl.node.on('JumpEnd', this.onPlayerJumpEnd, this);
}
    
// ...
onPlayerJumpEnd(moveIndex: number) {
    this.checkResult(moveIndex);
}

此时预览,会发现重新开始游戏时会有判断出错,是因为我们重新开始时没有重置 PlayerController 中的 _curMoveIndex 属性值。所以我们在 PlayerController 中增加一个 reset 函数:

reset() {
    this._curMoveIndex = 0;
}

GameManagerinit 函数调用 reset 来重置 PlayerController 的属性。

init() {
    \\ ...
    this.playerCtrl.reset();
}

八、步数显示

我们可以将当前跳的步数显示到界面上,这样在跳跃过程中看着步数的不断增长会十分有成就感。

1、在 Canvas 下新建一个名为 StepsLabel,调整位置、字体大小等属性。

2、在 GameManager 中引用这个 Label

@property({type: LabelComponent})
public stepsLabel: LabelComponent = null;

3、将当前步数数据更新到这个 Label 中,因为我们现在没有结束界面,游戏结束就跳回开始界面,所以在开始界面要看到上一次跳的步数,因此我们在进入 Playing 状态时,将步数重置为 0

set curState (value: GameState) {
    switch(value) {
        case GameState.GS_INIT:
            this.init();
            break;
        case GameState.GS_PLAYING:
            this.startMenu.active = false;
            this.stepsLabel.string = '0'; // 将步数重置为0
            setTimeout(() => { //直接设置 active 会直接开始监听鼠标事件,做了一下延迟处理
                this.playerCtrl.setInputActive(true);
            }, 0.1);
            break;
        case GameState.GS_END:
            break;
    }
    this._curState = value;
}

在响应角色跳跃的函数中,将步数更新到 Label 控件上

onPlayerJumpEnd(moveIndex: number) {
    this.stepsLabel.string = '' + moveIndex;
    this.checkResult(moveIndex);
}

九、光照和阴影

有光的地方就会有影子,光和影构成明暗交错的 3D 世界。接下来我们为角色加上简单的影子。

1、开启阴影

(2) 在 层级管理器 中点击最顶部的 Scene 节点,将 planarShadows 选项中的 Enabled 打钩,并修改 DistanceNormal 参数。

(2)点击 Player 节点下的 Body 节点,将 cc.ModelComponent 下的 ShadowCastingMode 设置为 ON

此时在场景编辑器中会看到一个阴影面片,预览会发现看不到这个阴影,因为它在模型的正后方,被胶囊体盖住了。

2、调整光照

新建场景时默认会添加一个 DirctionalLight,由这个平行光计算阴影,所以为了让阴影换个位置显示,我们可以调整这个平行光的方向。

在 层级管理器 中点击选中 Main Light 节点,调整 Rotation 参数为(-10,17,0)。

预览可以看到影子效果:

十、添加主角模型

用胶囊体当主角显的有点寒碜,所以我们花(低)重(预)金(算)制作了一个 Cocos 主角。

1、导入模型资源

从原始资源导入模型、材质、动画等资源不是本篇基础教程的重点,所以这边直接使用已经导入工程的资源。

[项目工程] 中 assets 目录下的 Cocos 文件夹拷贝到你自己工程的 assets 目录下。

2、添加到场景中

在 Cocos 文件中已经包含了一个名为 Cocos 的 Prefab,将它拖到场景中 Player 下的 Body 节点中。

此时会发现模型有些暗,可以加个聚光灯,以突出它锃光瓦亮的脑门。

3、添加跳跃动画

现在预览可以看到主角初始会有一个待机动画,但是跳跃时还是用这个待机动画会显得很不协调,所以我们在跳跃过程中换成跳跃的动画。在 PlayerController 类中添加一个引用模型动画的变量:

@property({type: SkeletalAnimationComponent})
public CocosAnim: SkeletalAnimationComponent = null;

然后在 Inspector 中要将 Cocos 节点拖入这个变量里。在 jumpByStep 函数中播放跳跃动画

jumpByStep(step: number) {
    if (this._isMoving) {
        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, cc.v3(this._jumpStep, 0, 0));

    this._isMoving = true;

    this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; //跳跃动画时间比较长,这里加速播放
    this.CocosAnim.play('cocos_anim_jump'); //播放跳跃动画
    if (step === 1) {
        //this.BodyAnim.play('oneStep');
    } else if (step === 2) {
        this.BodyAnim.play('twoStep');
    }

    this._curMoveIndex += step;
}

onOnceJumpEnd 函数中让主角变为待机状态,播放待机动画。

onOnceJumpEnd() {
    this._isMoving = false;
    this.CocosAnim.play('cocos_anim_idle');
    this.node.emit('JumpEnd', this._curMoveIndex);
}


预览效果:


十一、最终代码

PlayerController.ts

import { _decorator, Component, Vec3, systemEvent, SystemEvent, EventMouse, AnimationComponent, SkeletalAnimationComponent } from "cc";
const { ccclass, property } = _decorator;

@ccclass("PlayerController")
export class PlayerController extends Component {

    @property({ type: AnimationComponent })
    public BodyAnim: AnimationComponent = null;
    @property({ type: SkeletalAnimationComponent })
    public CocosAnim: SkeletalAnimationComponent = 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 = cc.v3();
    private _deltaPos: Vec3 = cc.v3(0, 0, 0);
    private _targetPos: Vec3 = cc.v3();
    private _isMoving = false;
    private _curMoveIndex = 0;

    start() {
    }

    reset() {
        this._curMoveIndex = 0;
    }

    setInputActive(active: boolean) {
        if (active) {
            systemEvent.on(SystemEvent.EventType.MOUSE_UP, this.onMouseUp, this);
        } else {
            systemEvent.off(SystemEvent.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._isMoving) {
            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, cc.v3(this._jumpStep, 0, 0));

        this._isMoving = true;

        this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; //跳跃动画时间比较长,这里加速播放
        this.CocosAnim.play('cocos_anim_jump'); //播放跳跃动画
        if (step === 1) {
            //this.BodyAnim.play('oneStep');
        } else if (step === 2) {
            this.BodyAnim.play('twoStep');
        }

        this._curMoveIndex += step;
    }

    onOnceJumpEnd() {
        this._isMoving = false;
        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);
            }
        }
    }
}

GameManager.ts

import { _decorator, Component, Prefab, instantiate, Node, LabelComponent, CCInteger } from "cc";
import { PlayerController } from "./PlayerController";
const { ccclass, property } = _decorator;

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;
    @property({ type: CCInteger })
    public roadLength: Number = 50;
    private _road: number[] = [];
    @property({ type: Node })
    public startMenu: Node = null;
    @property({ type: PlayerController })
    public playerCtrl: PlayerController = null;
    private _curState: GameState = GameState.GS_INIT;
    @property({ type: LabelComponent })
    public stepsLabel: LabelComponent = null;

    start() {
        this.curState = GameState.GS_INIT;
        this.playerCtrl.node.on('JumpEnd', this.onPlayerJumpEnd, this);
    }

    init() {
        this.startMenu.active = true;
        this.generateRoad();
        this.playerCtrl.setInputActive(false);
        this.playerCtrl.node.setPosition(cc.v3());
        this.playerCtrl.reset();
    }

    set curState(value: GameState) {
        switch (value) {
            case GameState.GS_INIT:
                this.init();
                break;
            case GameState.GS_PLAYING:
                this.startMenu.active = false;
                this.stepsLabel.string = '0'; // 将步数重置为0
                setTimeout(() => { //直接设置 active 会直接开始监听鼠标事件,做了一下延迟处理
                    this.playerCtrl.setInputActive(true);
                }, 0.1);
                break;
            case GameState.GS_END:
                break;
        }
        this._curState = value;
    }

    generateRoad() {

        this.node.removeAllChildren();

        this._road = [];
        // startPos
        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));
            }
        }

        for (let j = 0; j < this._road.length; j++) {
            let block: Node = this.spawnBlockByType(this._road[j]);
            if (block) {
                this.node.addChild(block);
                block.setPosition(j, -1.5, 0);
            }
        }
    }

    spawnBlockByType(type: BlockType) {
        let block = null;
        switch (type) {
            case BlockType.BT_STONE:
                block = instantiate(this.cubePrfb);
                break;
        }

        return block;
    }

    onStartButtonClicked() {
        this.curState = GameState.GS_PLAYING;
    }

    checkResult(moveIndex: number) {
        if (moveIndex <= this.roadLength) {
            if (this._road[moveIndex] == BlockType.BT_NONE) { //跳到了空方块上
                this.curState = GameState.GS_INIT;
            }
        } else { // 跳过了最大长度
            this.curState = GameState.GS_INIT;
        }
    }

    onPlayerJumpEnd(moveIndex: number) {
        this.stepsLabel.string = '' + moveIndex;
        this.checkResult(moveIndex);
    }

    // update (deltaTime: number) {
    // // Your update function goes here.
    // }
}

十二、总结

希望这篇快速入门教程能帮助您了解 Cocos Creator 3D 游戏开发流程中的基本概念和工作流程。如果您对编写和学习脚本编程不感兴趣,也可以直接从完成版的项目工程中把写好的脚本复制过来使用。

接下来您还可以继续完善游戏的各方各面,以下是一些推荐的改进方向:

  • 为游戏增加难度,当角色在原地停留 1 秒就算失败
  • 改为无限跑道,动态的删除已经跑过的跑道,延长后面的跑道。
  • 增加游戏音效
  • 为游戏增加结束菜单界面,统计玩家跳跃步数和所花的时间
  • 用更漂亮的资源替换角色和跑道
  • 可以增加一些可拾取物品来引导玩家“犯错”
  • 添加一些粒子特效,例如角色运动时的拖尾、落地时的灰尘
  • 为触屏设备加入两个操作按钮来代替鼠标左右键操作

相关地址

下载完整工程:

https://github.com/cocos-creator/tutorial-mind-your-step-3d

仓库地址:

https://github.com/cocos-creator/docs-3d/tree/master/zh/getting-started/first-game

社区地址:

https://forum.cocos.com/