原文地址:https://github.com/cocos-creator/docs-3d/blob/master/zh/getting-started/first-game/index.md
Cocos Creator 3D編輯器的強大之處就是可以讓開發者快速的製作遊戲原型。
下面我們將跟隨教程製作一款名叫 一步兩步 的魔性小遊戲。這款遊戲考驗玩家的反應能力,根據路況選擇是要跳一步還是跳兩步,“一步兩步,一步兩步,一步一步似爪牙似魔鬼的步伐”。
可以在 這裏 體驗一下游戲的完成形態。
新建項目
如果您還不瞭解如何獲取和啓動 Cocos Creator 3D,請閱讀 安裝和啓動 一節。
首先啓動 Cocos Creator 3D,然後新建一個名爲 MindYourStep 的項目,如果不知道如果創建項目,請閱讀 Hello World!。
新建項目後會看到如下的編輯器界面:
創建遊戲場景
在 Cocos Creator 3D 中,遊戲場景(Scene) 是開發時組織遊戲內容的中心,也是呈現給玩家所有遊戲內容的載體。遊戲場景中一般會包括以下內容:
場景物體
角色
UI
以組件形式附加在場景節點上的遊戲邏輯腳本
當玩家運行遊戲時,就會載入遊戲場景,遊戲場景加載後就會自動運行所包含組件的遊戲腳本,實現各種各樣開發者設置的邏輯功能。所以除了資源以外,遊戲場景是一切內容創作的基礎。現在,讓我們來新建一個場景。
在 資源管理器 中點擊選中 assets 目錄,點擊 資源管理器 左上角的加號按鈕,選擇文件夾,命名爲Scenes。
點擊先中Scenes目錄(下圖把一些常用的文件夾都提前創建好了),點擊鼠標右鍵,在彈出的菜單中選擇 場景文件
我們創建了一個名叫 New Scene 的場景文件,創建完成後場景文件 New Scene 的名稱會處於編輯狀態,將它重命名爲 Main。
雙擊 Main,就會在 場景編輯器 和 層級管理器 中打開這個場景。
添加跑道
我們的主角需要在一個由方塊(Block)組成的跑道上從屏幕左邊向右邊移動。我們使用編輯器自帶的立方體(Cube)來組成道路。
在 層級管理器 中創建一個立方體(Cube),並命名爲Cube。
選中Cube,按Ctrl+D來複製出3個Cube。
將3個Cube按以下座標排列:第一個節點位置(0,-1.5,0),第二個節點位置(1,-1.5,0),第三個節點位置(2,-1.5,0) 效果如下:
添加主角
創建主角節點
首先創建一個名字爲Player的空節點,然後在這個空節點下創建名爲Body的主角模型節點,爲了方便,我們採用編輯器自帶的膠囊體模型做爲主角模型。
分爲兩個節點的好處是,我們可以使用腳本控制Player節點來使主角進行水平方向移動,而在Body節點上做一些垂直方向上的動畫(比如原地跳起後下落),兩者疊加形成一個跳越動畫。將Player節點設置在(0,0,0)位置,使得它能站在第一個方塊上。
效果如下:
編寫主角腳本
想要主角影響鼠標事件來進行移動,我們就需要編寫自定義的腳本。如果您從沒寫過程序也不用擔心,我們會在教程中提供所有需要的代碼,只要複製粘貼到正確的位置就可以了,之後這部分工作可以找您的程序員小夥伴來解決。下面讓我們開始創建驅動主角行動的腳本吧。
創建腳本
如果還沒有創建Scripts文件夾,首先在 資源管理器 中右鍵點擊 assets 文件夾,選擇 新建 -> 文件夾,重命名爲Scripts。
右鍵點擊Scripts文件夾,選擇 新建 -> TypeScript,創建一個 TypeScript 腳本,有關TypeScript資料可以查看 TypeScript 官方網站。
將新建腳本的名字改爲PlayerController,雙擊這個腳本,打開代碼編輯器(例如VSCode)。
注意: Cocos Creator 3D 中腳本名稱就是組件的名稱,這個命名是大小寫敏感的!如果組件名稱的大小寫不正確,將無法正確通過名稱使用組件!
編寫腳本代碼
在打開的 PlayerController 腳本里已經有了預先設置好的一些代碼塊,如下所示:
import { _decorator, Component } from "cc";
const { ccclass, property } = _decorator;
@ccclass("PlayerController")
export class PlayerController extends Component {
start () {
// Your initialization 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 {
// 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按 在打開的網頁中點擊鼠標左鍵和右鍵,可以看到如下畫面:
添加角色動畫
從上面運行的結果可以看到單純對Player進行水平方向的移動是十分呆板的,我們要讓Player跳躍起來才比較有感覺,我們可以通過爲角色添加垂直方向的動畫來達到這個效果。有關 動畫編輯器 的更多信息,請閱讀 動畫編輯器
選中場景中的Body節點,編輯器下方 控制檯 邊上的 動畫編輯器,添加Animation組件並創建Clip,命名爲oneStep。
進入動畫編輯模式,添加position屬性軌道,並添加三個關鍵幀,position值分別爲(0,0,0)、(0,0.5,0)、(0,0,0)。
退出動畫編輯模式前前記得要保存動畫,否則做的動畫就白費了。
我們還可以通過 資源管理器 來創建Clip,下面我們創建一個名爲twoStep的Clip並將它添加到Body身上的
AnimationComponent
上,這裏爲了錄製方便調整了一下面板佈局。進入動畫編輯模式,選擇並編輯twoStep的clip,類似第2步,添加三個position的關鍵幀,分別爲(0,0,0)、(0,1,0)、(0,0,0)。
在
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並編輯位置來組成跑道顯然不是一個明智的做法,我們通過腳本完成跑道的自動創建。
遊戲管理器(GameManager)
一般遊戲都會有一個管理器,主要負責整個遊戲生命週期的管理,可以將跑道的動態創建代碼放到這裏。在場景中創建一個名爲GameManager的節點,然後在 assets/Scripts
中創建一個名爲GameManager的ts腳本文件,並將它添加到GameManager節點上。
製作Prefab
對於需要重複生成的節點,我們可以將他保存成 Prefab(預製) 資源,作爲我們動態生成節點時使用的模板。關於 Prefab 的更多信息,請閱讀 預製資源(Prefab)。我們將生成跑道的基本元素正方體(Cube)
製作成Prefab,之後可以把場景中的三個Cube都刪除了。
添加自動創建跑道代碼
我們需要一個很長的跑道,理想的方法是能動態增加跑道的長度,這樣可以永無止境的跑下去,這裏爲了方便我們先生成一個固定長度的跑道,跑道長度可以自己定義。跑道上會生成一些坑,跳到坑上就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;
}
}
在GameManager的inspector面板中可以通過修改roadLength的值來改變跑道的長度。預覽可以看到現在自動生成了跑道,不過因爲Camera沒有跟隨Player移動,所以看不到後面的跑道,我們可以將場景中的Camera設置爲Player的子節點。
這樣Camera就會跟隨Player的移動而移動,現在預覽可以從頭跑到尾的觀察生成的跑道了。
增加開始菜單
開始菜單是遊戲不可或缺的一部分,我們可以在這裏加入遊戲名稱、遊戲簡介、製作人員等信息。
添加一個名爲Play的按鈕
這個操作生成了一個Canvas節點,一個PlayButton節點和一個Label節點。因爲UI組件需要在帶有
CanvasComponent
的父節點下才能顯示,所以編輯器在發現目前場景中沒有帶這個組件的節點時會自動添加一個。創建按鈕後,將Label節點上的cc.LabelComponent
的String屬性從Button改爲Play。在Canvas底下創建一個名字爲StartMenu的空節點,將PlayButton拖到它底下。我們可以通過點擊工具欄上的2D/3D按 來切換到2D編輯視圖下進行UI編輯操作,詳細的描述請查閱 場景編輯。
增加一個背景框,在StartMenu下新建一個名字爲BG的Sprite節點,調節它的位置到PlayButton的上方,設置它的寬高爲(200,200),並將它的SpriteFrame設置爲
internal/default_ui/default_sprite_splash
。添加一個名爲Title的
Label
用於開始菜單的標題,修改Title的文字,並調整Title的位置、文字大小、顏色。
增加操作的Tips,然後調整PlayButton的位置,一個簡單的開始菜單就完成了
增加遊戲狀態邏輯,一般我們可以將遊戲分爲三個狀態:
使用一個枚舉(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;
}
初始化(Init):顯示遊戲菜單,初始化一些資源。
遊戲進行中(Playing):隱藏遊戲菜單,玩家可以操作角度進行遊戲。
結束(End):遊戲結束,顯示結束菜單。
添加對Play按鈕的事件監聽。爲了能在點擊Play按鈕後開始遊戲,我們需要對按鈕的點擊事件做出響應。在GameManager腳本中加入響應按鈕點擊的代碼,在點擊後進入遊戲的Playing狀態:
onStartButtonClicked() {
this.curState = GameState.GS_PLAYING;
}
然後在Play按鈕的Inspector上添加ClickEvents的響應函數。
現在預覽場景就可以點擊Play按鈕開始遊戲了。
添加遊戲結束邏輯
目前遊戲角色只是呆呆的往前跑,我們需要添加遊戲規則,來讓他跑的更有挑戰性。
角色每一次跳躍結束需要發出消息,並將自己當前所在位置做爲參數發出消息 在PlayerController中記錄自己跳了多少步
private _curMoveIndex = 0;
// ...
jumpByStep(step: number) {
// ...
this._curMoveIndex += step;
}在每次跳躍結束髮出消息:
onOnceJumpEnd() {
this._isMoving = false;
this.node.emit('JumpEnd', this._curMoveIndex);
}在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();
}
步數顯示
我們可以將當前跳的步數顯示到界面上,這樣在跳躍過程中看着步數的不斷增長會十分有成就感。
在Canvas下新建一個名爲Steps的Label,調整位置、字體大小等屬性。
在GameManager中引用這個Label
@property({type: LabelComponent})
public stepsLabel: LabelComponent = null;將當前步數數據更新到這個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世界更加的立體。接下來我們爲角色加上簡單的影子。
開啓陰影
在 層級管理器 中點擊最頂部的
Scene
節點,將planarShadows選項中的Enabled打鉤,並修改Distance和Normal參數點擊Player節點下的Body節點,將
cc.ModelComponent
下的ShadowCastingMode設置爲ON。
此時在場景編輯器中會看到一個陰影面片,預覽會發現看不到這個陰影,因爲它在模型的正後方,被膠囊體蓋住了。
調整光照
新建場景時默認會添加一個 DirctionalLight
,由這個平行光計算陰影,所以爲了讓陰影換個位置顯示,我們可以調整這個平行光的方向。在 層級管理器 中點擊選中 Main Light
節點,調整Rotation參數爲(-10,17,0)。
預覽可以看到影子效果:
總結
恭喜您完成了用 Cocos Creator 3D 製作的第一個遊戲!在 這裏 可以下載完整的工程,希望這篇快速入門教程能幫助您瞭解 Cocos Creator 3D 遊戲開發流程中的基本概念和工作流程。如果您對編寫和學習腳本編程不感興趣,也可以直接從完成版的項目工程中把寫好的腳本複製過來使用。
接下來您還可以繼續完善遊戲的各方各面,以下是一些推薦的改進方向:
爲遊戲增加難度,當角色在原地停留1秒就算失敗
改爲無限跑道,動態的刪除已經跑過的跑道,延長後面的跑道。
增加遊戲音效
爲遊戲增加結束菜單界面,統計玩家跳躍步數和所花的時間
用更漂亮的資源替換角色和跑道
可以增加一些可拾取物品來引導玩家“犯錯”
添加一些粒子特效,例如角色運動時的拖尾、落地時的灰塵
爲觸屏設備加入兩個操作按鈕來代替鼠標左右鍵操作
在此可下載完整工程:
https://github.com/cocos-creator/tutorial-mind-your-step-3d