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

分类栏目:cocos教程

75

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

跟随教程制作一款名叫《一步两步》的魔性小游戏,从新建项目到完整工程代码都有,帮助新手开发者了解 Cocos Creator 3D 游戏开发流程中的基本概念和工作流程。


五、跑道升级

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

1、游戏管理器(GameManager)

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

2、制作 Prefab

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

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

Image title


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 的子节点。

Image title


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

五、增加开始菜单

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

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

Image title


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

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

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

Image title


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

Image title


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

Image title


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

Image title


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

Image title


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;


Image title


增加状态切换代码,并修改 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 的响应函数。

Image title


现在预览场景就可以点击 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;
}


在GameManager的init函数调用reset来重置PlayerController的属性。


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