如何打造一款三消類遊戲

Shopee技術團隊發表於2022-01-06
本文首發於微信公眾號“Shopee技術團隊”。

摘要

樣式繁多的“消消樂”遊戲想必大家都不陌生,通關祕籍就是將三個或更多相同的元素配對消除,通常我們稱這類遊戲為“三消”遊戲。Shopee 購物平臺內嵌的三消遊戲 Shopee Candy 也受到了不少使用者的喜愛,這篇文章將帶你從專案起源、遊戲架構和專案工具集等方面瞭解如何打造一款這樣的三消小遊戲。

1. 專案起源

1.1 遊戲簡介

Shopee Candy 是一款面向多地區市場的三消類休閒 H5 遊戲,使用者可以在遊戲中獲得 Shopee Coins、商家購物券等優惠獎勵,既可以增強使用者粘性,激勵使用者消費,也可以為商家引流。同時,分享領獎、好友排行榜等機制增強了遊戲的社交性,起到了為平臺拉新的作用。此外,H5 遊戲簡單、輕量、高適配的特性與 Shopee 使用者的使用習慣非常契合,可以即點即玩,參與門檻低的同時兼顧了趣味性。

2020 年 6 月,Shopee Candy 在 Shopee 全市場上線,推出 iOS 和安卓正式版本。自上線以來,Shopee Candy 在日常和大促活動中表現耀眼,使用者的活躍度和線上時長屢創新高。

Shopee Candy 在風格上使用了繽紛可愛的糖果元素主題;玩法上主要是在限制操作步數的同時,設定收集元素數量作為通關條件。關卡內,使用者通過交換相鄰糖果,使三個或以上相同顏色的糖果連線起來,便可觸發消除;根據消除糖果的數量和形狀還可以生成有特殊消除能力的道具元素;模式上有關卡模式及無盡模式等。

1.2 專案簡介

隨著專案不斷髮展,Shopee Candy 的功能迭代可以被清晰地劃分為四類。首先是各類業務功能模組(道具商城、任務系統和兌換商店等);其次是負責消除邏輯演算法,計算分數、關卡進度的演算法庫(Algorithm SDK)和負責消除效果的動畫系統;最後是服務遊戲的各種工具,包括解放設計關卡生產力的 Map Editor 關卡編輯器,可以量化關卡難度的 Score Runner 跑分器以及能進行操作覆盤的 Replayer 回放工具等。

2. 遊戲架構

2.1 演算法庫(Algorithm SDK)

作為一款元素消除種類豐富的三消遊戲,Shopee Candy 的演算法部分非常重要和複雜。早在專案之初,演算法和動畫是耦合在一起的。隨著產品的上線和消除種類的增加,我們慢慢發現專案維護成本越來越高;同時演算法本身是獨立的,跟動畫和業務沒有依賴,所以將演算法部分抽離了出來。

2.1.1 Algorithm SDK 實現

Algorithm SDK 的實現主要有三大部分:Map、Operator 以及 Logic Processing。

地圖(Map)

Map 管理了遊戲關卡中的地圖物件和元素物件。我們根據每個元素的特性對元素進行了上、中、下三層的管理,這種元素分層的架構模式能夠滿足不同特效新元素的加入。每一層元素相互制約、相互影響,在消除流程中相輔相成,完成遊戲中華麗的消除效果。

export default class Grid {
  public middle: CellModel;
  public upper: Upper;
  public bottom: Bottom;

  constructor(info: ICellInfo) {
    const { type } = info;

    this.upper = new Upper(/* ... */);
    this.middle = new CellModel(/* ... */);
    this.bottom = new Bottom(/* ... */);
  }
}

操作(Operator)

Operator 對演算法的所有可行操作進行管理,是整個 Algorithm SDK 與外部通訊的橋樑,負責接收外部的交換、雙擊等操作訊號,進行相應演算法的呼叫。在 Shopee Candy 主流程的呼叫中,Operator 會收集動畫資料,並以此與動畫系統進行通訊。

// 元素交換
export function exchange(startPos, endPos): IAnimationData {
  // ... logic process
  // returns animation data
}

// 元素雙擊
export function doubleClick(pos): IAnimationData {
  // ... logic process
  // returns animation data
}

邏輯運算(Logic Processing)

Algorithm 對 Algorithm SDK 的所有邏輯運算進行管理,包括:開局有解保證、有解檢測、消除、掉落等,是整個 Algorithm SDK 的核心。

流程中元素消除多次迴圈時,可能會出現邏輯執行用時過長的問題,導致使用者操作時丟幀。為了避免這種情況,我們將邏輯運算分成了多段,讓計算非同步化,提前將資料傳送到動畫執行,後續操作分別在不同幀中完成。

2.1.2 Algorithm SDK 單元測試

在實現上我們已經做到了儘可能的分離和解耦,但是對於龐大的演算法庫來說,光是常規的 Code Review 遠遠不夠,前端測試就顯得非常重要了。

單元測試介紹

很多人說前端測試浪費時間且收效甚微,常規的前端業務確實會經常變動,包括 Shopee Candy 的業務檢視也是經常變化。但是得益於演算法庫的分離和獨立,我們可以對它進行無 UI、純邏輯的單元測試。

單元測試應用

人工測試只能保證操作後程式碼不報錯以及佈局不錯亂,但無法發現消除元素數量或分數不正確等多種情況。專案中使用者的一次移動或者雙擊的操作,控制同樣的佈局下,最後得出相同的結果。這個結果包括了最終地圖的資料、使用者獲得的分數、收集或者銷燬元素的數量等,保證在多次迭代中的穩定性。

describe('BOMB', () => {
    it('Exchange each other should be the same crush', () => {
      const source = { row: 0, col: 3 };
      const target = { row: 1, col: 3 };
      const wrapper = mapDecorator(operator);
      const data1 = wrapper({ key: CRUSH_TYPE.BOMB }, source, target);
      const data2 = wrapper({ key: CRUSH_TYPE.BOMB }, target, source);
      // 地圖比較
      expect(JSON.stringify(data1.map)).equal(JSON.stringify(data2.map)).equal('xxx');
      // 分數比較
      expect(data1.score).equal(data2.score).equal(150);
      // 步數比較
      expect(data1.passStep).equal(data2.passStep).equal(14);
    });
});

2.2 動畫系統(Animation)

動畫與演算法分離後,動畫單獨作為一個系統,這樣有以下優點:

  • 高內聚低耦合:演算法與動畫均具有較高複雜度,分離後降低了系統複雜度,同時模組更加內聚;
  • 高效率:演算法的執行不用等待動畫,提高了計算效率;
  • 高靈活性:演算法去掉對動畫的依賴後,可以很好地支援跳過 Bonus、跑分器等需求。

2.2.1 方案設計

分離動畫系統後,需要與演算法建立通訊機制,來保證演算法執行的消除結果有對應的動畫播放。通訊的實現方式如下:

  • 建立事件機制,演算法與動畫通過事件進行相互通訊;
  • 定義動畫資料結構,通過定義不同的動畫型別來區分動畫,例如消除和下落動畫,同時定義完整的動畫資訊,動畫系統解析後播放對應動畫。

針對動畫的播放,我們引入了一套「動畫佇列」的流程。將演算法解析後的動畫資料新增到佇列中,遞迴播放佇列,直至佇列為空,結束動畫播放。

從動畫佇列中播放單個動畫時,為了確保各個元素動畫的播放彼此之間不相互影響,動畫系統採用「策略模式」進行設計,根據動畫型別執行不同的消除策略,將元素的動畫「內聚」到各自的策略方法中。

//策略配置
const animStrategyCfg: Map<TElementType, IElementStrategy> = new Map([
    [AElement, AStrategy],
    [BElement, BStrategy],
    [CElement, CStrategy],
]);

//獲取策略
function getStrategy(elementType):IElementStrategy{
    return animStrategyCfg.get(elementType);
}

//執行策略
function executeStrategy(elementType){
    const strategy = getStrategy(elementType);
    return strategy.execute();
}

演算法負責計算消除邏輯,動畫系統負責播放對應動畫,除了播放龍骨等特效,還會操作棋盤元素的大小、位置和顯示。正常情況下動畫播放結束後棋盤的狀態和演算法狀態應該是一致的,但極少數情況下可能會由於裝置效能等原因,造成時序、定時器異常等問題,進而導致兩者狀態不一致,比如元素不顯示或位置錯位等。

所以,動畫結束後,需要「兜底邏輯」實時獲取演算法狀態,校驗修正棋盤狀態,使之與其匹配,避免展示上的錯誤。同時,為了避免效能問題,這裡並非全量校驗修正,而是隻針對容易出錯的中層元素。

2.2.2 解決回撥地獄

遊戲引擎自帶的動畫庫採用回撥方式執行動畫完成後的邏輯,在動畫較為複雜的情況下,常常會出現回撥巢狀的寫法,使得邏輯難以理解和維護。為了解決回撥地獄問題,我們在動畫庫的原型鏈上封裝了 promise 方法,這樣就可以使用 async/await 同步的寫法。

function animation(callback: Function) {
    tweenA.call(() => {
        tweenB.call(() => {
            tweenC.call(() => {
                sleep.call(callback);
            });
        });
    });
}
async function animation() {
    await tweenA.promise();
    await tweenB.promise();
    await tweenC.promise();
    await sleep.promise();
}

上面展示了從回撥寫法改成同步寫法的效果,可以看到同步寫法更加直觀,解決了回撥地獄的問題,更易於程式碼的維護。

3. 專案工具集

前面介紹了 Shopee Candy 的演算法庫和動畫系統,實際上我們團隊還做了很多工具。

3.1 Map Editor

在 Shopee Candy 遊戲建立之初,我們需要製作一個既能靈活配置關卡也能測試關卡可玩性與通關率的工具。

在日常的工作中,通過拖拽、鍵盤快捷鍵的方式可以快速配置關卡棋盤元素。

目前棋盤元素已發展到 30 多個,各個元素之間存在複雜的互斥共存規則:

  • 共存:A 元素和 B 元素可以同時出現在一個格子上;
  • 互斥:A 元素和 B 元素不能同時出現在一個格子上;
  • 大互斥:A 元素和 B 元素不能同時出現在一個棋盤上。

面對這麼多元素之間的關係,單純依靠策劃在配置關卡的時候人工處理互斥關係是不合適的,
所以需要一個關係互斥表去維護元素之間的關係。我們通過 Map Editor 服務端拉取這個表格的資料,將它返回給 Web 端,在做好關係限制的同時給出一定的錯誤反饋。

3.2 Score Runner

在上線前期,遇到的問題之一就是關卡策劃速度追不上使用者通關的速度,經常出現使用者催更關卡的情況。一款三消遊戲動輒幾千關的設計量,對於任何團隊來說都是研發工作中的一個巨大難點。其中,策劃關卡最頭疼和耗時是關卡的難度測試和調整,每一次調整都要人工重複試玩多次,然後統計通關率。Score Runner 正是一個可以解決圖中紅色枯燥費時流程的工具。

Score Runner 實現

先來看其中一關的佈局,場上可以消除的操作有多種可能性,如果把這些可能性看成一張網狀結構的資料,模擬使用者的操作無外乎就只有這些可能性。

那麼,下一步操作呢?在這些可能性操作後面有不同的佈局,就會有更多的不同的可能性。流程大致如下:

  • 遍歷地圖獲取每一種操作的可能性;
  • 根據每一種操作,繼續獲取下一步的可能性直到最後一步(通過還是失敗);
  • 得到所有的操作步驟,就能得出最多和最少通關分數。

從圖中可以看到,每一種可能性都能走到最後結束,但是實際上很多可能性使用者是不會去操作的,比如得分少的,沒有消除意義的操作等,對於通關率來說不科學,而且對於計算效率來說非常低下。

於是我們加入了聰明度策略,對可能性的消除的資料集做計算得出操作權重順序。以最高權重的操作為最佳可能性,對當前龐大的可能性網狀結構進行剪枝,得出更符合的通關率。

例:

關卡平均通關率(100次)通關記錄(%)
34165%63/62/71
34268%65/63/76
34360%56/57/67
34460%63/64/56
34547%46/47/47
34651%51/52/49
34750%50/52/42
34847%51/38/51
34963%65/61/62

通過分析平均通關成功率,可以減少策劃對於關卡難度的驗證時間。

3.3 Replayer

在 Score Runner 之後,我們使用 Replayer 對跑分過程進行回放,以驗證跑分的正確性。

回放時,面臨的首要問題就是保證每次隨機的結果與跑分時是一致的。如何保證?

隨機種子是這個問題的答案。隨機種子是一種以隨機數作為物件的,以真隨機數(種子)為初始條件的隨機數。簡單來說就是設定固定的種子,輸出的結果、順序完全相同的偽隨機方法。

為了接入隨機種子,我們採用了新的隨機數策略,該策略可以對隨機種子進行設定,且我們每一次隨機數都是基於上一次隨機數結果作為種子計算得出的結果。

這樣的策略保證了整一局遊戲中的每一次隨機數都被記錄,每一次隨機的結果可以隨時拿到。

同時這一策略也有以下收穫:

  • 邏輯演算法執行的可回溯性;
  • 斷線重連後隨機數的可追溯性;
  • 覆盤線上使用者整局遊戲步驟的可行性;
  • 極少資料儲存量。

4.未來規劃

本文從專案起源、遊戲架構和專案工具集等方面介紹了 Shopee Candy 這款遊戲。

“Rome wasn't built in a day”,經過“需求——重構——上線”的迴圈才能造就更加完善的專案,後續我們將從以下幾方面對 Shopee Candy 進行完善和優化:

1)針對新元素實現配置化開發,減少開發成本

目前新元素的開發還是需要投入較多的人力,計劃結合演算法庫(Algorithm SDK)與 Map Editor 實現元素屬性分類、分層等配置化開發,僅需新增配置化演算法及元素動畫即可實現新元素的開發。

2)針對低端裝置提供更流暢的遊戲體驗

Shopee Candy 是一款強遊戲性專案,特別關注遊戲效能和操作手感。由於市面上移動裝置效能參差不齊,則更加需要關注低端裝置的使用者體驗。後續會針對低端裝置制定邏輯和檢視效能的優化策略,為使用者提供更加流暢的遊戲體驗。

3)操作行為驗證,避免作弊現象

前端依靠混淆或者加密等手段增加了破解成本,但無法完全防範作弊行為。目前,專案正在開發操作行為驗證服務,結合現有的 Replyer 功能對可疑結算行為進行二次操作校驗,從而保障遊戲的公平性。

4)利用機器學習進行關卡設計

目前,Shopee Candy 已經開發了上千關卡。為提高關卡配置效率,後續計劃結合機器學習和 Score Runner 統計的通關率進行模型訓練,僅需提供少量的人工配置即可自動生成關卡。

本文作者

Shopee Games - Candy 前端團隊。

相關文章