原文位址: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 tweenprivate _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) {// endthis.node.setPosition(this._targetPos);this._startJump = false;this.onOnceJumpEnd();
} else {// tweenthis.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元件
動畫元件
,我們需要在代碼中根據跳的步數不同來播放不同的動畫。
首先需要 在
中引用Body身上的PlayerController元件
。AnimationComponent
然後在 屬性檢查器 中将Body身上的@property({type: AnimationComponent})public BodyAnim: AnimationComponent = null;
拖到這個變量上。AnimationComponent
- 在跳躍的函數
中加入動畫播放的代碼:jumpByStep
點選Play按鈕,點選滑鼠左鍵、右鍵,可以看到新的跳躍效果:if (step === 1) {this.BodyAnim.play('oneStep'); } else if (step === 2) {this.BodyAnim.play('twoStep'); }
跑道更新
為了讓遊戲有更久的生命力,我們需要一個很長的跑道來讓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 = [];// startPosthis._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元件需要在帶有
的父節點下才能顯示,是以編輯器在發現目前場景中沒有帶這個元件的節點時會自動添加一個。建立按鈕後,将Label節點上的CanvasComponent
的String屬性從Button改為Play。cc.LabelComponent
- 在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)類型來表示這幾個狀态。
GameManager腳本中加入表示目前狀态的私有變量enum BlockType{BT_NONE,BT_STONE, };enum GameState{GS_INIT,GS_PLAYING,GS_END, };
為了在開始時不讓使用者操作角色,而在遊戲進行時讓使用者操作角色,我們需要動态的開啟和關閉角色對滑鼠消息的監聽。是以對PlayerController做如下的修改:private _curState: GameState = GameState.GS_INIT;
然後需要在GameManager腳本中引用PlayerController,需要在Inspector中将場景的Player拖入到這個變量中。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中引用StartMenu節點,需要在Inspector中将場景的StartMenu拖入到這個變量中。@property({type: PlayerController})public playerCtrl: PlayerController = null;
@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; } }
此時預覽,會發現重新開始遊戲時會有判斷出錯,是因為我們重新開始時沒有重置PlayerController中的_curMoveIndex屬性值。是以我們在PlayerController中增加一個reset函數:start () {this.curState = GameState.GS_INIT;this.playerCtrl.node.on('JumpEnd', this.onPlayerJumpEnd, this); }// ...onPlayerJumpEnd(moveIndex: number) {this.checkResult(moveIndex); }
在GameManager的init函數調用reset來重置PlayerController的屬性。reset() {this._curMoveIndex = 0; }
init() { \\ ...this.playerCtrl.reset(); }
步數顯示
我們可以将目前跳的步數顯示到界面上,這樣在跳躍過程中看着步數的不斷增長會十分有成就感。
- 在Canvas下建立一個名為Steps的Label,調整位置、字型大小等屬性。
- 在GameManager中引用這個Label
@property({type: LabelComponent})public stepsLabel: LabelComponent = null;
- 将目前步數資料更新到這個Label中 因為我們現在沒有結束界面,遊戲結束就跳回開始界面,是以在開始界面要看到上一次跳的步數,是以我們在進入Playing狀态時,将步數重置為0。
在響應角色跳躍的函數中,将步數更新到Label控件上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'; // 将步數重置為0setTimeout(() => { //直接設定active會直接開始監聽滑鼠事件,做了一下延遲處理this.playerCtrl.setInputActive(true); }, 0.1);break;case GameState.GS_END:break; }this._curState = value; }
onPlayerJumpEnd(moveIndex: number) {this.stepsLabel.string = '' + moveIndex;this.checkResult(moveIndex); }
光照和陰影
有光的地方就會有影子,光和影使得3D世界更加的立體。接下來我們為角色加上簡單的影子。
開啟陰影
- 在 層級管理器 中點選最頂部的
節點,将planarShadows選項中的Enabled打鈎,并修改Distance和Normal參數Scene
- 點選Player節點下的Body節點,将
下的ShadowCastingMode設定為ON。cc.ModelComponent
此時在場景編輯器中會看到一個陰影面片,預覽會發現看不到這個陰影,因為它在模型的正後方,被膠囊體蓋住了。
調整光照
建立場景時預設會添加一個
DirctionalLight
,由這個平行光計算陰影,是以為了讓陰影換個位置顯示,我們可以調整這個平行光的方向。在 層級管理器 中點選選中
Main Light
節點,調整Rotation參數為(-10,17,0)。
預覽可以看到影子效果:
總結
恭喜您完成了用 Cocos Creator 3D 制作的第一個遊戲!在 這裡 可以下載下傳完整的工程,希望這篇快速入門教程能幫助您了解 Cocos Creator 3D 遊戲開發流程中的基本概念和工作流程。如果您對編寫和學習腳本程式設計不感興趣,也可以直接從完成版的項目工程中把寫好的腳本複制過來使用。
接下來您還可以繼續完善遊戲的各方各面,以下是一些推薦的改進方向:
- 為遊戲增加難度,當角色在原地停留1秒就算失敗
- 改為無限跑道,動态的删除已經跑過的跑道,延長後面的跑道。
- 增加遊戲音效
- 為遊戲增加結束菜單界面,統計玩家跳躍步數和所花的時間
- 用更漂亮的資源替換角色和跑道
- 可以增加一些可拾取物品來引導玩家“犯錯”
- 添加一些粒子特效,例如角色運動時的拖尾、落地時的灰塵
- 為觸屏裝置加入兩個操作按鈕來代替滑鼠左右鍵操作
在此可下載下傳完整工程:
https://github.com/cocos-creator/tutorial-mind-your-step-3d