【CSDN 編者按】過去一周,不少人被《羊了個羊》這款遊戲虐的不輕,有多少個「再玩一把」的念頭,就有多少次被打入深淵的淒涼,甚至還有人評價道:「什麼事都可以過去,除了《羊了個羊》第二關」。因此,有用戶抱怨是「程序員故意挖坑製作死關卡」。然而在本文作者老王一探究竟以後,才發現並非程序員挖坑,而是該遊戲的本身,就有很多「天然的坑」。
昨天有朋友和我說:「最近有個叫《羊了個羊》的遊戲爆火,就是太難玩了,你能復刻一個不?」話說上次玩休閒遊戲還是在幾年前,但是朋友之託必須赴湯蹈火啊,二話不說,開整!然而,衝動是魔鬼,直到此時此刻,老王也沒能親手玩一局原版遊戲,不知道是遊戲入口設計得太隱蔽還是網絡加載太慢,無論手機端還是PC端,遊戲都停留在如下界面。
所以本次遊戲的復刻,完全是基於各視頻網站雲觀摩的結果,好在遊戲的玩法不是特別難理解。復刻使用的開發工具是Godot Engine(使用其它工具開發原理也是相似的),目前項目已經開源到了GitCode:Godot版《羊了個羊》https://gitcode.net/hello_tute/SheepASheep。
接下來我將通過臨摹遊戲的方式推測一下這個小遊戲的實現原理,本文主要面向對遊戲開發有興趣的朋友,歡迎大家多提寶貴意見。
先說說玩法
第一眼看到《羊了個羊》,老王首先想到當年的《連連看》,不過有網友爆料,該遊戲「借鑑」了《3tiles》。瞄了眼《3tiles》,是比較相似。說心裡話,這個遊戲的玩法並沒有什麼過於出眾的地方,算是個中規中矩的「低卡路里」休閒遊戲。
之所以成為話題作品,主要就是因為它的第2關極其低的通關率,一下子激起了眾多玩家的挑戰欲望。而時至今日這個「低通關率」也被網絡上的眾多玩家揭秘,第2關其實大概率上本身就是個死局。是程序員故意挖坑設了死局麼?先賣個關子,我們先聊聊遊戲的開發,然後您自己就會有答案了。
實現概要
遊戲的整體很簡單,但其中有幾個實現的重點需要注意:
牌堆數據結構的實現
如何檢測和更新可拾取的牌
先做個小定義,一個牌堆中可被拾取的牌以下將簡稱其為:「窗口牌」。
牌堆的結構及其數據結構
最初,我還真被這複雜的牌堆結構蒙住了,但仔細研究一番發現,無論多麼複雜的牌堆,其實都是由如下三種牌堆模式組合拼湊而成的。
藍圈圈出的牌堆模式A:上面1張牌只擋住下面1張牌;同時下面的牌僅被上面1張牌擋住。只要上面的1張牌被取走,下面的牌就成為窗口牌;
紅圈圈出的牌堆模式C:上面1張牌可以擋住下面4張牌;同時下面的牌可能被上面4張牌擋住,一張牌只有它上面的4張牌都被取走,它自己才成為窗口牌。
雖然上圖中體現不是很明顯,但不難猜想出,第三種牌堆模式B 的存在,那就是:
上面1張牌可以擋住下面2張牌;同時下面的牌可能被上面2張牌擋住,一張牌只有它上面的2張牌都被取走,它自己才成為窗口牌。
對於牌堆模式A,有些朋友會迫不及待地用「隊列」或「棧」實現它,這樣做有兩個缺點:
邏輯上牌堆模式A的窗口牌也可能是2維的,如果用隊列實現就限制了它的靈活性;
牌堆模式B和C都不好用隊列實現,所以想追求數據結構的統一,還要另求他法。
實際上無論牌堆模式A、B還是C,都不過是3維數組結構,上圖中模式A看起來特殊,無非是它的x,y維度都為1罷了。而三種牌堆的區別也無非就是當一張窗口牌被取走,檢查牌堆是否出現新的窗口牌的方法罷了。
牌堆模式A
牌堆模式B
牌堆模式C
牌堆的數據結構
我將其定義為MContainerBase基類
最基礎的牌堆就是一個 x*y*z的三維數組,我們可以使用一切方法構造想要的排隊形狀:柱形、條形、甚至金字塔形。這都不會影響後面程序的實現。
項目中為了增加這個「大方塊」的多樣性,我還給它設置了如下的「遮罩」,這就是遊戲中CSDN文字的由來。當然我們還可以通過「遮罩」來自由定義窗口牌,這部分就請大家自由發揮了。
如何檢測和更新可拾取的牌
三種牌堆模式分別派生自MContainerBase,並對應着如下三種檢測方式:
牌堆模式A
僅檢測自己正上方是否有牌
牌堆模式B
檢測自己上方兩方位是否有牌
牌堆模式C
檢測自己上方四方位是否有牌
在Godot中,這三種牌堆模式還可以通過場景節點製作成預製體,這樣關卡設計師就可以輕鬆地製作出美觀的關卡了。
如何生成新關卡
簡單了解遊戲規則後,我們就不難推導出,每個關卡能被通過的一個必要條件就是每一種圖案的總數,必須能被3整除。實現方法如下:
其中字典initial_tiles 的key對應着每一種圖案,後面的value對應着這一關該圖案出現的「對數」(此處1對等於3個)。按照value乘以3的數量存入數組tiles(下文稱之為:待發牌池),然後把待發牌池中的元素打亂順序,等待「發牌」。
關於遊戲中的坑
很多朋友抱怨:「程序員故意挖坑製作死關卡」。其實不然,他無須故意挖坑,因為這個遊戲本身就有很多「天然的坑」,如果不使勁填坑,它們自然而然就屬於你了。而這裡就隱藏了幾個可致命的坑:乍一看,待發牌池中所有的圖案都可以被3整除那麼一定可以通關?那可不一定:
只有桌面牌堆中牌的數量和待發牌池牌數一致,所有的牌才能「落地」,而遊戲中桌面牌堆到底有多少(層)本身就是個迷。並且如果沒猜錯的話,在每一局設計者先要確保牌堆形狀好看,然後再使堆牌數和待發池的牌數一致。二者哪怕差1個,也會造成死局。
上文說了,桌面牌數和待發牌池的牌數一致只是過關的必要而非充分條件。即使該條件滿足,如果相對於牌桌上的牌數以及圖案數量,窗口牌數太少,也會造成死局。比如下面這個極端的例子:假設遊戲共有 15種花色,而牌桌上只有這個模式A牌堆,它有90張牌。那麼玩家只要在連續7次拾牌時沒有遇到3個相同圖案的牌,就「必死無疑」了。
其實這個遊戲,一方面要控制關卡的難度,另一方面又要保證能通關本身就是一個相當困難的問題(至少老王沒有想出辦法)。而設計者反其道而行之,(可能)沒有花力氣去設計算法,把坑留給玩家,得到了極低的通關率,反而製造了話題並形成爆款。如此說來,這確實是個抖機靈的「設計」。但老王認為這種「設計」在遊戲策劃中是不宜被借鑑的,就像現在市面上泛濫的懸疑劇,開始埋坑無數,吊足觀眾胃口,最後爛尾不了了之一樣,長此以往觀眾(玩家)對於懸疑劇(遊戲)的信任感就被消費殆盡了。
洗牌道具的實現
洗牌的實現原理很簡單,把當前桌面的牌記錄在一個數組tiles中,當需要洗牌時,先打亂一下數組中牌的順序,然後讓桌面上每一張牌到tiles中重新取一個值。再來個眼花繚亂點的動畫,還真挺像那麼回事兒。
遮罩文件的讀取
這裡要夸一下Godot Engine,它的很多功能真是方便,比如下面這個str2var它可以簡單粗暴地直接把字符串轉換成對象類型。
對象間的通信
這個小遊戲中存在大量的對象間的通信需求:牌和牌之間、牌和牌堆之間、牌和關卡之間、牌堆和關卡之間。為了快速實現遊戲,我大量使用了Godot Engine的Group機制,不得不說Group是Godot Engine最贊的設計之一。
總結
小遊戲《羊了個羊》,從策劃和開發的角度來看並不困難,然而「瑕疵」竟然能夠成為「噱頭」,也讓人不得不感慨「遊戲世界真的一切皆有可能啊」。
作者簡介:
開發遊戲的老王,高校教師、技術專欄作者、獨立遊戲開發者


