本文乃Siliphen原創,轉載請注明出處
目錄
概述
游戲整體流程
游戲框架設計
單一職責的類
主要流程控制類
核心玩法模塊
UI:
游戲世界:
本文項目的代碼組織結構
作者項目實踐總結
場景只有一個入口腳本
盡量少在節點上掛載腳本
構建游戲世界
ECS 設計
消除物
棋盤地圖
邏輯計算和顯示分離
消除的實現
查找聯通分量
逐步由內向外擴張的消除動畫
掉落的實現
合并的實現
道具的實現
本文的完整實現源碼工程
概述
《消滅星星》是一個爆款休閑游戲,累計用戶5億+。
目前(2023.08.06)在 App Store 上39.6萬個評分,評分4.6,益智解謎類第7名。
參考鏈接:??App?Store 上的“消滅星星全新版?”
本文講解用 Cocos Creator 實現一款加強效果版的《消滅星星》的核心流程和算法。
本文實現的游戲效果如下:
?
可在這個地址運行體驗下本文實現的版本:Cocos Creator | 消滅星星
文本末尾給出完整實現的源碼工程。
游戲整體流程
游戲執行一輪玩家操作的流程:等待玩家點擊操作 -> 用戶點擊 -> 消除-> 掉落 -> 合并 -> 等待玩家點擊操作
以上流程是游戲玩家操作一次,游戲執行一輪的分解動作循環。
整個游戲的組成:游戲由N關組成,一關由N輪玩家操作組成。
游戲框架設計
單一職責的類
可以把一輪中的每個動作都獨立成一個控制類,每個控制類只負責一種動作,比如A類只負責消除控制,B類只負責掉落控制。
這是敏捷開發中的重要原則:單一職責。一個類的功能越是單一,它就越內聚、越和其他系統解耦合。
每種控制類在它負責的單一動作執行完成后,用回調通知其他系統,它已經完成,可以進行下一步操作。
比如:消除控制系統處理消除完成后,用一個 onComplete 回調通知外界,已經完成了消除這個動作。
掉落控制系統監聽消除控制系統的 onComplete ,處理消除后下一步的掉落控制。
主要流程控制類
從調用先后順序開始依次如下:
類名 | 作用 |
UiTouch | 處理用戶觸摸輸入 |
Eliminate | 處理消除。消除上下左右連色的實體。 |
Fall | 消除后會留下空位,控制消除實體掉落下來。 |
Merge | 掉落后,如果有空的列,那么向左邊靠攏合并起來。 |
核心玩法模塊
核心玩法分成2大部分:UI、游戲世界
UI:
包括:按鈕、彈窗、分數顯示、玩家輸入 等部分。是所有用戶界面的集合。
這個部分的開發有點類似于做 APP。可以用 MVC 等 APP 常用開發模式。
游戲世界:
這是游戲開發獨有的部分。處理游戲世界中游戲實體的行為、游戲實體之間的關系和交互、游戲世界的規則等。
游戲核心玩法的開發主要關注這部分。
因為游戲世界不可能簡單分為幾個層,比如,什么顯示層,邏輯層,數據層等。
有可能實體之間的關系和交互很復雜,MVC 等傳統 APP 開發模式并不適用。一般大型游戲會采用 ECS 等設計。
本文項目的代碼組織結構
如下圖:
作者項目實踐總結
場景只有一個入口腳本
一個場景只掛一個入口腳本,各種節點的引用使用 find、node.getChildByPath 等去查找。
就像 C/C++ 語言有一個唯一入口函數 main 。
這樣做的好處是:在代碼中初始化各個系統,有明確的初始化順序。
在多個節點上掛多個腳本,默認情況下有個問題,哪個腳本先執行哪個腳本后執行。有時候執行順序是非常重要的。
編輯器可以指定節點上掛的腳本的執行順序,但這是額外的維護負擔。不如在代碼中指定的維護性好。
盡量少在節點上掛載腳本
少掛載腳本的好處是:
- 降低腳本Missing情況的維護成本。
- 節約性能。
- 提高項目移植性。比如移植到其他引擎上。
想象一個情況,一個場景中有很多節點,很多節點都掛有腳本。出于某些原因,腳本和節點的掛載關系丟失了。
編輯器節點上要么不顯示腳本,要么腳本顯示為丟失(Missing)。
場景簡單還好,重新手動拖腳本到節點上。場景復雜,那就很麻煩。絕大部分情況,只是知道節點Missing了腳本,但不知道Missing的是哪個腳本。
為什么有時候會Missing腳本?原因很多,可能有如下幾種:
* 用戶誤操作。比如 破壞了 *.meta 文件。
* 多人協作 *.meta 文件沖突,導致腳本丟失。
* 引擎版本多次低級高級來回切換。
* 一些說不清楚的,莫名其妙的情況。
構建游戲世界
《消滅星星》的游戲世界只有2個實體:消除物、棋盤地圖。
棋盤沒有畫面表現。棋盤是消除物的容器,棋盤限定了消除物計算規則和運動規則。
后面的查找消除算法和掉落控制,都是作用在棋盤上計算的。
ECS 設計
本項目使用類似 ESC 的設計,非嚴格意義上的 ECS ,是如下定義:
Entity 是 Componet 的容器。Component 只有數據,沒有邏輯。System 沒有數據,只有邏輯。
實體和游戲世界的交互實現,實體和實體之間的交互實現,都放在 System 中。
這種設計的好處是:高擴展性。高維護性。易于移植到其他引擎。易于引擎升級。
消除物
定義如下:
// 消除物實體
export class Elimination
{// 類型IDpublic kindId = "" ;public presentaion = new EliminationPresentaion() ;}// 消除物表現組件
export class EliminationPresentaion
{// 根節點public node : Node = null ;// 動畫public amin : Animation = null ; }
Elimination 是消除物實體類。EliminationPresentaion 是消除物實體的表現組件類。
實體類只是組件類的容器。實體類和組件類都只有定義,沒有邏輯。
棋盤地圖
棋盤數據本質是個二維數組。定義如下:
// 地圖數據
export class MapData
{// 單件/* */public static ins = new MapData() ;// 數據網格public grid = new Array< Array< Cell > >() ;// 地圖大小public size = new Size();// 是否是有效地坐標public isValid(coord : Vec2) : boolean{if( coord.x < 0 || coord.y < 0 || coord.x >= this.size.x || coord.y >= this.size.y ) return false ;return true ;}}// 地圖單元格
export class Cell
{// 消除物public elimination : Elimination = null ;// 坐標public coord = new Vec2();// 在世界空間中單元格的位置。public pt = new Vec3(); }
二位數組對應的位置如下圖:
左下角的索引是(0,0),右上角是(9,9)。
邏輯計算和顯示分離
先計算好結果后再播放達到這個結果的過渡動畫。邏輯計算和播放顯示動畫的分離可以讓代碼結構更清晰,維護性更高。
后面的處理都是先在內存中計算好地圖狀態:消除后地圖哪些單元格為空,掉落后消除物實體都落在哪個單元格上 等。
計算好地圖狀態后再處理畫面顯示:播放消除動畫,播放掉落動畫等。
消除的實現
先看下文本實現的消除效果:
?大部分《消滅星星》的實現都是點擊后瞬間一起消除。
本文做了不一樣的效果,從點擊的消除物開始逐步由內向外擴張的消除。
不管是瞬間消除,還是某種控制動畫消除,第一步都是“查找相鄰的同類消除物”。
查找聯通分量
術語“查找聯通分量”很多《數據結構》的書都會有介紹。此處,我們用來查找相鄰的同類消除物。
使用深度優先搜索(DFS)實現,輸出一顆樹。樹的根結點是玩家點擊的那個消除物。
為什么要輸出一棵樹?因為要按照樹的層次進行消除才能實現逐步由內向外擴張的動畫。
具體實現可查看工程源碼的 ConnectionFind.find 函數。
這里為了講解算法原理,用偽代碼說明算法的核心思想。
// start 是點擊的消除物
dfs( start )
{// 結果數據結構,用 Map 表示一棵樹。key 是一個被發現的消除物,value 是這個消除物的父節點。let ret = new Map< Elimination , Elimination >() ; // 創建一個棧 stack q ;let q = new Stack() ; // 訪問記錄。該數據結構是為了防止重復訪問那些已經訪問過的消除物let visit = Set< Elimination >() ;q.push( start ) ; // 起始點入棧ret.set( start , null ) ; // 點擊的消除物是根結點,根結點沒有父節點。for( ; q.count > 0 ; ) // 棧不為空就一直循環{let t = q.pop() ; // 出棧一個節點let list = expand( t ) ; // 查找出棧節點上下左右4個方向相鄰的同樣的節點foreach( let t2 in list ) // 所有查找出來的節點入棧{if( visit.has( t2 ) ) continue ; // 跳過訪問過的消除物q.push( t2 ) ; ret.add( t2 , t ) ; // 發現一個行節點t2,它的父節點是t。visit.add( t2 ) ;} // end for} // end forreturn ret ;
}
《消滅星星》最難的算法就是這個“查找聯通分量”了。
如果一下子不理解也沒關系,可以反復琢磨下本文作者的偽代碼和具體實現。
或者是查閱數據結構或算法的書籍,深入、詳細的學習下。加油!:)
逐步由內向外擴張的消除動畫
在上一步中,我們獲得了一顆消除物節點樹。是一個鍵值對數據結構,key 表示發現的節點,value 表示發現的節點的父節點。
這里,我們處理這棵樹結構為按照樹的層次劃分的數據結構:let levels = new Array< Array< Elimination > >()
levels[ 0 ] 表示樹第 1 層的節點集合。樹根只有一個起始節點。
levels[ 1 ] 表示樹第 2 層的節點集合。
... 以此類推
間隔一層層的整體消除即可。
如何把 Map< Elimination , Elimination > 處理成 Array< Array< Elimination > > 的層次結構呢?
遍歷這個 Map,對每個 key 向上查找,直到查到 null 遇到根結點為止。就可以得知當前 key 所在的層次。
按照層次放入對應的 Array 數組容器中即可。
具體實現查看源碼工程的類 SeqCtrl。
掉落的實現
消除后,棋盤地圖的一些被消除的消除物所在的單元格會被設為空。上面的消除物會掉落下來。
從棋盤底部向上一行行遍歷,遇到一個消除物后,向下查找一個空位,如果能找到一個空位,就把這個消除物設置到那個空位上。
先設置棋盤的邏輯狀態。后計算被移動的消除物的新的顯示位置,做一個移動動畫即可。
具體實現查看源碼工程的類 Fall。
合并的實現
本文實現的合并效果如下圖:
?
合并的處理在掉落之后。
遍歷棋盤最底部的那一行,遍歷順序從左到右。
因為之前已經執行了掉落,最底部的一行有空位的話,就說明有棋盤地圖有一列為空。
如果發現了一個空位,就說明需要合并,向后查找一個非空列,整體移動那一列的消除物到空位即可。
具體實現查看源碼工程的類 Merge。
道具的實現
經典的消滅星星有3個道具:指定一個消除物替換為另一個指定的消除物、九宮格炸彈,全體消除物隨機變換。
九宮格炸彈
具體實現查看源碼工程的類 PropBombNine、TouchPropBombNine
全體消除物隨機變換
遍歷整個棋盤地圖,隨機替換消除物即可。
具體實現查看源碼工程的類 PropChangeAll。
單點替換
這個道具的實現相對以上2個比較特殊,耦合了點擊操作。
先要設置觸摸模式為使用道具,然后玩家點擊后,如果點擊的是一個消除物,
就在這個消除物的上方顯示替換UI,供玩家選擇變換后的消除物。
具體實現查看源碼工程的類 PropChangeOne、TouchPropChangeOne
本文的完整實現源碼工程
源碼工程下載地址:Cocos Store
作者創作不易,您的支持讓我創造出更多更好的作品。?:)