Cocos Creator 3D 案例《彈彈樂》技術實現分享

 

《彈彈樂》是一款簡單的休閒物理彈跳類 3D 小遊戲,用手指輕輕划動屏幕來控制小球運動方向,跳中板心或是板邊可獲得不同分數,此外,留心獲取遊戲場景中設置的鑽石,可以爲玩家增加更多分數。

 

 

Cocos 引擎開發工程師放空將分享這款3D 小遊戲最基礎的完整開發流程,各位開發者可以在閱讀學習本篇教程後,繼續發揮創造力,將這款簡單的 3D 小遊戲進行拓展開發,變成一款更有趣的、可對外發布的小遊戲。

 

遊戲源碼

https://github.com/cocos-creator/demo-ball

 

結構說明

下圖是《彈彈樂》的草圖以及整體設計思路

在理出整體設計思路之後,就可以開始設計每個階段應該完成的目標,以便於提高代碼編寫的效率。

 

以下是我劃分的每個階段的開發任務:

 

遊戲初始化

  • 跳板初始化
  • 屏幕事件監聽,小球與普通板塊彈跳計算
  • 提供相機跟隨接口

 

遊戲核心邏輯編寫

  • 跳板複用邏輯編寫
  • 小球與不同板塊彈跳計算
  • 遊戲開始與結束邏輯編寫

 

遊戲豐富

  • 添加鑽石以及喫磚石表現
  • 添加跳板表現
  • 增加小球粒子以及拖尾表現

 

初期設計完成後,我們就可以開始整個遊戲場景的搭建。

 

整個遊戲一共就一個場景,一個主程序 Game,負責管理所有分支管理的 Manager 以及事件的監聽和派發;多個分支 Manager,負責管理跳板創建擺放或遊戲頁面等;一個全局配置模塊,負責存儲遊戲中使用的配置;獨立對象的運作腳本,負責自身行爲運作。

 

 

編寫遊戲內容邏輯

 

由於最終呈現出來的詳細步驟代碼太多,就不一一演示了,今天主要針對每個流程的幾個關鍵部分做個說明。整個遊戲的製作流程主要分爲以下幾點:

 

(1)跳板初始化

 

跳板初始化主要體現在 BoardManager 裏的 initBoard 和 getNextPos 兩個方法。在整個遊戲過程中,使用的板一共只有 5 個,後續的跳板生成都是通過複用的方式,不斷重新計算位置以及序號。跳板的生成也是嚴格根據上一個跳板的位置來計算,避免出現長距離位置偏移影響遊戲進行。

getNextPos(board: Board, count: number, out ?: Vec3) {    const pos: Vec3 = out ? out.set(board.node.position) : board.node.position.clone();    const o = utils.getDiffCoeff(count, 1, 2);    pos.x = (Math.random() - .5) * Constants.SCENE_MAX_OFFSET_X * o;    if (board.type === Constants.BOARD_TYPE.SPRINT) {        pos.y += Constants.BOARD_GAP_SPRINT;        pos.x = board.node.position.x;    }    if (board.type === Constants.BOARD_TYPE.SPRING) {        pos.y += Constants.BOARD_GAP_SPRING;    } else {        pos.y += Constants.BOARD_GAP;    }    return pos;}getDiffCoeff(e: number, t: number, a: number) {    return (a * e + 1) / (1 * e + ((a + 1) / t - 1));}

 

(2)屏幕事件監聽,小球與普通板塊彈跳計算

 

跳板初始化後,開始做小球的彈跳。整個遊戲的入口函數都設定在 Game 類上,Game 又添加在 Canvas 節點上,因此,Game 類所掛載的節點作爲全局對象的事件監聽節點來使用最合適不過。因爲主要接受該事件的對象是小球,所以,我們在小球裏做監聽的回調。

start () {    Constants.game.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);    Constants.game.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);    Constants.game.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);    this.updateBall();    this.reset();}onTouchStart(touch: Touch, event: EventTouch){    this.isTouch = true;    this.touchPosX = touch.getLocation().x;    this.movePosX = this.touchPosX;}onTouchMove(touch: Touch, event: EventTouch){    this.movePosX = touch.getLocation().x;}onTouchEnd(touch: Touch, event: EventTouch){    this.isTouch = false;}

然後,小球根據一定比例的換算來做實際移動距離的計算。在 update 裏每幀根據衝刺等狀態對小球進行 setPosX,setPosY 調整,小球的上升與下降是通過擬重力加速減速來實現。

// Constantsstatic BALL_JUMP_STEP = [0.8, 0.6, 0.5, 0.4, 0.3, 0.2, 0.15, 0.1, 0.05, 0.03]; // 正常跳躍步長static BALL_JUMP_FRAMES = 20; // 正常跳躍幀數//Ball_tempPos.set(this.node.position);_tempPos.y += Constants.BALL_JUMP_STEP[Math.floor(this._currJumpFrame / 2)];this.node.setPosition(_tempPos);

 

(3)提供相機跟隨接口

 

相機的位置移動不是由自身來操控的,而是根據小球當前的位置來進行實時跟蹤。因此,相機只需要調整好設置接口,按照一定脫離距離去跟隨小球即可。

update() {    _tempPos.set(this.node.position);    if (_tempPos.x === this._originPos.x && _tempPos.y === this._originPos.y) {        return;    }    // 橫向位置誤差糾正    if (Math.abs(_tempPos.x - this._originPos.x) <= Constants.CAMERA_MOVE_MINI_ERR) {        _tempPos.x = this._originPos.x;        this.setPosition(_tempPos);    } else {        const x = this._originPos.x - _tempPos.x;        _tempPos.x += x / Constants.CAMERA_MOVE_X_FRAMES;        this.setPosition(_tempPos);    }    _tempPos.set(this.node.position);    // 縱向位置誤差糾正    if (Math.abs(_tempPos.y - this._originPos.y) <= Constants.CAMERA_MOVE_MINI_ERR) {        _tempPos.y = this._originPos.y;        this.setPosition(_tempPos);    } else {        const y = this._originPos.y - _tempPos.y;        if (this.preType === Constants.BOARD_TYPE.SPRING) {            _tempPos.y += y / Constants.CAMERA_MOVE_Y_FRAMES_SPRING;            this.setPosition(_tempPos);        } else {            _tempPos.y += y / Constants.CAMERA_MOVE_Y_FRAMES;            this.setPosition(_tempPos);        }    }}

核心邏輯

 

整個遊戲的節奏控制其實都是通過小球來的,小球通過彈跳位置決定什麼時候開始新板的生成,小球在遊戲過程中的得分決定了板子後續生成的豐富性(比如長板或者彈簧板)以及小球的死亡以及復活決定了遊戲的狀態等等;最後通過 UI 配合來完成遊戲開始結束復活的界面引導交互操作。

 

(1)跳板複用邏輯編寫

 

保持場景中的跳板就是初始化的數量,所以需要提前度量好板塊間的最小距離。那麼,屏幕最下方的板塊在什麼時機開始複用到屏幕最上方呢?舉個例子:假設當前場景的板上限是 5 塊,在數組裏的順序就是 0 - 4,按前面說的所有板在全顯示的情況下是會均勻分佈的,因此,屏幕的分割板就是在中間板的 2 號板,因此只要超過了 2,就代表小球已經跳過的屏幕的一半,這個時候就要開始清理無用的板了。

for (let i = this.currBoardIdx + 1; i >= 0; i--) {    const board = boardList[i];    // 超過當前跳板應該彈跳高度,開始下降    if (this.jumpState === Constants.BALL_JUMP_STATE.FALLDOWN) {        if (this.currJumpFrame > Constants.PLAYER_MAX_DOWN_FRAMES || this.currBoard.node.position.y - this.node.position.y > Constants.BOARD_GAP + Constants.BOARD_HEIGTH) {            Constants.game.gameDie();            return;        }        // 是否在當前檢測的板上        if (this.isOnBoard(board)) {            this.currBoard = board;            this.currBoardIdx = i;            this.activeCurrBoard();            break;        }    }}// 當超過中間板就開始做板複用for (let l = this.currBoardIdx - Constants.BOARD_NEW_INDEX; l > 0; l--) {    this.newBoard();}

 

(2)小球與不同板塊彈跳計算

 

上面的製作過程中,我們已經實現了在普通板上小球是一個乒乓球狀態,那麼遇到彈簧板或者衝刺板的時候,也可以用類似邏輯結構來繼續補充不同板子的不同處理。這裏的實現因爲結構已定較爲簡單,就不再多做說明,只需要在全局數據類里加上相應的相同配置即可。

 

(3)遊戲開始與結束邏輯編寫

 

遊戲開始以及結束都是通過 UI 界面來實現。定義一個 UIManager 管理類來管理當前 UI 界面,所有的 UI 打開與關閉都通過此管理類來統一管理,點擊事件的響應都直接回調給遊戲主循環 Game 類。

以上部分就基本完成了整個遊戲的邏輯部分。

 

 

遊戲豐富

 

接下來豐富一下游戲的真實表現力。

 

(1)添加鑽石以及喫磚石表現

 

因爲遊戲內的跳板數量限制,因此,我們可以大方的給每個跳板配置 5 個鑽石,通過隨機概率決定鑽石的顯示。

if (this.type === Constants.BOARD_TYPE.GIANT) {    for (let i = 0; i < 5; i++) {        this.diamondList[i].active = true;        this.hasDiamond = true;    }} else if (this.type === Constants.BOARD_TYPE.NORMAL || this.type === Constants.BOARD_TYPE.DROP) {    if (Math.random() > .7) {        this.diamondList[2].active = true;        this.hasDiamond = true;    }}

既然有了鑽石,那喫鑽石的時候,肯定也要有些表示,那就是掉落一些粒子來增加表現。由於遊戲設計過程中如果有很多對頻繁的創建和銷燬的話,對性能其實是很不友好的,因此,提供一個對象池在一款遊戲中是必不可少。

在這裏,我們就可以把散落的粒子存放在對象池裏進行復用。在這款遊戲的設計過程中,小球部分的計算量是很頻繁的,特別是在每幀需要更新的地方,想要去做性能優化的同學可以根據對象池的概念對小球裏的一些向量進行復用。

getNode(prefab: Prefab, parent: Node) {    let name = prefab.data.name;    this.dictPrefab[name] = prefab;    let node: Node = null;    if (this.dictPool.hasOwnProperty(name)) {        //已有對應的對象池        let pool = this.dictPool[name];        if (pool.size() > 0) {            node = pool.get();        } else {            node = instantiate(prefab);        }    } else {        //沒有對應對象池,創建他!        let pool = new NodePool();        this.dictPool[name] = pool;        node = instantiate(prefab);    }    node.parent = parent;    return node;}putNode(node: Node) {    let name = node.name;    let pool = null;    if (this.dictPool.hasOwnProperty(name)) {        //已有對應的對象池        pool = this.dictPool[name];    } else {        //沒有對應對象池,創建他!        pool = new cc.NodePool();        this.dictPool[name] = pool;    }    pool.put(node);}

 

(2)添加跳板表現、增加小球粒子以及拖尾表現

 

其實這兩點功能都基本類似,都是增加一些波動、拖尾粒子等來豐富表現,在這裏就不過多說明,具體的表現都寫在了 Board 類和 Ball 類相對應關鍵字的方法裏。

 

(3)增加音效和音樂

 

因爲是基礎教程,遊戲內的表現也不是很多,所以就選取了按鈕被點擊的音效和背景音樂來做效果。

playSound(play = true) {    if (!play) {        this.audioComp.stop();        return;    }    this.audioComp.clip = this.bg;    this.audioComp.play();}playClip() {    this.audioComp.playOneShot(this.click);}

 

以上就是本教程的全部內容,接下來看一下運行結果吧。

 

————/ END /————

 

 

 

我就知道你“在看”▼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章