<kbd id="9plqc"><label id="9plqc"></label></kbd>

        <th id="9plqc"></th>
        1. <center id="9plqc"><video id="9plqc"></video></center>
          <sub id="9plqc"><form id="9plqc"><pre id="9plqc"></pre></form></sub>
          <nav id="9plqc"><form id="9plqc"><legend id="9plqc"></legend></form></nav>
          程序員用12小時復刻《羊了個羊》,代碼已開源! 您所在的位置:網(wǎng)站首頁 算命微信小程序源代碼 程序員用12小時復刻《羊了個羊》,代碼已開源!

          程序員用12小時復刻《羊了個羊》,代碼已開源!

          2025-07-18 06:59| 來源: 網(wǎng)絡(luò)整理| 查看: 265

          【CSDN 編者按】過去一周,不少人被《羊了個羊》這款游戲虐的不輕,有多少個“再玩一把”的念頭,就有多少次被打入深淵的凄涼,甚至還有人評價道:“什么事都可以過去,除了《羊了個羊》第二關(guān)”。因此,有用戶抱怨是“程序員故意挖坑制作死關(guān)卡”。然而在本文作者老王一探究竟以后,才發(fā)現(xiàn)并非程序員挖坑,而是該游戲的本身,就有很多“天然的坑”。

          作者 | 開發(fā)游戲的老王 責編 | 張紅月 出品 | CSDN(ID:CSDNnews)

          昨天有朋友和我說:“最近有個叫《羊了個羊》的游戲爆火,就是太難玩了,你能復刻一個不?”話說上次玩休閑游戲還是在幾年前,但是朋友之托必須赴湯蹈火啊,二話不說,開整!然而,沖動是魔鬼,直到此時此刻,老王也沒能親手玩一局原版游戲,不知道是游戲入口設(shè)計得太隱蔽還是網(wǎng)絡(luò)加載太慢,無論手機端還是PC端,游戲都停留在如下界面。 在這里插入圖片描述

          所以本次游戲的復刻,完全是基于各視頻網(wǎng)站云觀摩的結(jié)果,好在游戲的玩法不是特別難理解。復刻使用的開發(fā)工具是Godot Engine(使用其它工具開發(fā)原理也是相似的),目前項目已經(jīng)開源到了GitCode:Godot版《羊了個羊》https://gitcode.net/hello_tute/SheepASheep。

          接下來我將通過臨摹游戲的方式推測一下這個小游戲的實現(xiàn)原理,本文主要面向?qū)τ螒蜷_發(fā)有興趣的朋友,歡迎大家多提寶貴意見。

          羊了羊

          先說說玩法

          第一眼看到《羊了個羊》,老王首先想到當年的《連連看》,不過有網(wǎng)友爆料,該游戲“借鑒”了《3tiles》。瞄了眼《3tiles》,是比較相似。說心里話,這個游戲的玩法并沒有什么過于出眾的地方,算是個中規(guī)中矩的“低卡路里”休閑游戲。

          之所以成為話題作品,主要就是因為它的第2關(guān)極其低的通關(guān)率,一下子激起了眾多玩家的挑戰(zhàn)欲望。而時至今日這個“低通關(guān)率”也被網(wǎng)絡(luò)上的眾多玩家揭秘,第2關(guān)其實大概率上本身就是個死局。是程序員故意挖坑設(shè)了死局么?先賣個關(guān)子,我們先聊聊游戲的開發(fā),然后您自己就會有答案了。

          實現(xiàn)概要

          游戲的整體很簡單,但其中有幾個實現(xiàn)的重點需要注意:

          牌堆數(shù)據(jù)結(jié)構(gòu)的實現(xiàn) 如何檢測和更新可拾取的牌 先做個小定義,一個牌堆中可被拾取的牌以下將簡稱其為:“窗口牌”。

          牌堆的結(jié)構(gòu)及其數(shù)據(jù)結(jié)構(gòu)

          在這里插入圖片描述 最初,我還真被這復雜的牌堆結(jié)構(gòu)蒙住了,但仔細研究一番發(fā)現(xiàn),無論多么復雜的牌堆,其實都是由如下三種牌堆模式組合拼湊而成的。

          藍圈圈出的牌堆模式A:上面1張牌只擋住下面1張牌;同時下面的牌僅被上面1張牌擋住。只要上面的1張牌被取走,下面的牌就成為窗口牌; 紅圈圈出的牌堆模式C:上面1張牌可以擋住下面4張牌;同時下面的牌可能被上面4張牌擋住,一張牌只有它上面的4張牌都被取走,它自己才成為窗口牌。

          雖然上圖中體現(xiàn)不是很明顯,但不難猜想出,第三種牌堆模式B 的存在,那就是:

          上面1張牌可以擋住下面2張牌;同時下面的牌可能被上面2張牌擋住,一張牌只有它上面的2張牌都被取走,它自己才成為窗口牌。

          對于牌堆模式A,有些朋友會迫不及待地用“隊列”或“棧”實現(xiàn)它,這樣做有兩個缺點:

          邏輯上牌堆模式A的窗口牌也可能是2維的,如果用隊列實現(xiàn)就限制了它的靈活性; 牌堆模式B和C都不好用隊列實現(xiàn),所以想追求數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一,還要另求他法。

          實際上無論牌堆模式A、B還是C,都不過是3維數(shù)組結(jié)構(gòu),上圖中模式A看起來特殊,無非是它的x,y維度都為1罷了。而三種牌堆的區(qū)別也無非就是當一張窗口牌被取走,檢查牌堆是否出現(xiàn)新的窗口牌的方法罷了。

          牌堆模式A 在這里插入圖片描述 牌堆模式B

          在這里插入圖片描述 牌堆模式C 在這里插入圖片描述 牌堆的數(shù)據(jù)結(jié)構(gòu)

          我將其定義為MContainerBase基類

          #MContainerBase extends Node2D class_name MContainerBase func _ready(): add_to_group(name) add_to_group("game") var Mask = FileReader.read(mask_file,null) box.resize(size_x) for i in range(size_x): box[i] = [] box[i].resize(size_y) for j in range(size_y): box[i][j] = [] box[i][j].resize(size_z) for k in range(size_z): if Mask == null or Mask[i][j] == 1: box[i][j][k] = add_tile(i,j,k,get_parent().distribute_face()) else: box[i][j][k] = null for x in range(size_x): for y in range(size_y): for z in range(size_z): check_is_on_top(x,y,z)

          最基礎(chǔ)的牌堆就是一個 xyz的三維數(shù)組,我們可以使用一切方法構(gòu)造想要的排隊形狀:柱形、條形、甚至金字塔形。這都不會影響后面程序的實現(xiàn)。

          項目中為了增加這個“大方塊”的多樣性,我還給它設(shè)置了如下的“遮罩”,這就是游戲中CSDN文字的由來。當然我們還可以通過“遮罩”來自由定義窗口牌,這部分就請大家自由發(fā)揮了。

          # S形遮罩 [ [0,0,0,0,0], [0,0,0,0,0], [1,1,1,0,1], [1,0,1,0,1], [1,0,1,1,1], ]

          在這里插入圖片描述 如何檢測和更新可拾取的牌

          三種牌堆模式分別派生自MContainerBase,并對應著如下三種檢測方式:

          牌堆模式A

          僅檢測自己正上方是否有牌

          #1 Cover 1 extends MContainerBase func check_is_on_top(x,y,z): if has_tile(x,y,z): if not has_tile(x,y,z + 1) : (box[x][y][z] as MTile).set_is_on_top(true)

          牌堆模式B

          檢測自己上方兩方位是否有牌

          #1 Cover 2 extends MContainerBase func check_is_on_top(x,y,z): if has_tile(x,y,z): if z%2 == 0: if not has_tile(x,y,z + 1) and not has_tile(x - 1 ,y,z + 1): (box[x][y][z] as MTile).set_is_on_top(true) else: if not has_tile(x,y,z + 1) and not has_tile(x + 1 ,y,z + 1): (box[x][y][z] as MTile).set_is_on_top(true)

          牌堆模式C

          檢測自己上方四方位是否有牌

          #1 Cover 4 extends MContainerBase func check_is_on_top(x,y,z): if has_tile(x,y,z): if z%2 == 0: if not has_tile(x,y,z + 1) and not has_tile(x - 1 ,y,z + 1) and not has_tile(x,y - 1 ,z + 1) and not has_tile(x - 1,y - 1,z + 1): (box[x][y][z] as MTile).set_is_on_top(true) else: if not has_tile(x,y,z + 1) and not has_tile(x + 1 ,y,z + 1) and not has_tile(x,y + 1 ,z + 1) and not has_tile(x + 1,y + 1,z + 1): (box[x][y][z] as MTile).set_is_on_top(true)

          在Godot中,這三種牌堆模式還可以通過場景節(jié)點制作成預制體,這樣關(guān)卡設(shè)計師就可以輕松地制作出美觀的關(guān)卡了。 在這里插入圖片描述 在這里插入圖片描述

          如何生成新關(guān)卡

          簡單了解游戲規(guī)則后,我們就不難推導出,每個關(guān)卡能被通過的一個必要條件就是每一種圖案的總數(shù),必須能被3整除。實現(xiàn)方法如下:

          var tiles = [] export var initial_tiles = { 0:10, 1:10, 2:10, 3:10, 4:10, 5:10, 6:10, 7:10, 8:10, 9:10, 10:10, 11:10, 12:10, 13:10, 14:10, 15:10 } func _init(): for key in initial_tiles: var num = initial_tiles[key]*3 for i in range(0,num): tiles.append(key) tiles.shuffle()

          其中字典initial_tiles 的key對應著每一種圖案,后面的value對應著這一關(guān)該圖案出現(xiàn)的“對數(shù)”(此處1對等于3個)。按照value乘以3的數(shù)量存入數(shù)組tiles(下文稱之為:待發(fā)牌池),然后把待發(fā)牌池中的元素打亂順序,等待“發(fā)牌”。

          關(guān)于游戲中的坑

          很多朋友抱怨:“程序員故意挖坑制作死關(guān)卡”。其實不然,他無須故意挖坑,因為這個游戲本身就有很多“天然的坑”,如果不使勁填坑,它們自然而然就屬于你了。而這里就隱藏了幾個可致命的坑:乍一看,待發(fā)牌池中所有的圖案都可以被3整除那么一定可以通關(guān)?那可不一定:

          只有桌面牌堆中牌的數(shù)量和待發(fā)牌池牌數(shù)一致,所有的牌才能“落地”,而游戲中桌面牌堆到底有多少(層)本身就是個迷。并且如果沒猜錯的話,在每一局設(shè)計者先要確保牌堆形狀好看,然后再使堆牌數(shù)和待發(fā)池的牌數(shù)一致。二者哪怕差1個,也會造成死局。

          上文說了,桌面牌數(shù)和待發(fā)牌池的牌數(shù)一致只是過關(guān)的必要而非充分條件。即使該條件滿足,如果相對于牌桌上的牌數(shù)以及圖案數(shù)量,窗口牌數(shù)太少,也會造成死局。比如下面這個極端的例子:假設(shè)游戲共有 15種花色,而牌桌上只有這個模式A牌堆,它有90張牌。那么玩家只要在連續(xù)7次拾牌時沒有遇到3個相同圖案的牌,就“必死無疑”了。 在這里插入圖片描述 其實這個游戲,一方面要控制關(guān)卡的難度,另一方面又要保證能通關(guān)本身就是一個相當困難的問題(至少老王沒有想出辦法)。而設(shè)計者反其道而行之,(可能)沒有花力氣去設(shè)計算法,把坑留給玩家,得到了極低的通關(guān)率,反而制造了話題并形成爆款。如此說來,這確實是個抖機靈的“設(shè)計”。但老王認為這種“設(shè)計”在游戲策劃中是不宜被借鑒的,就像現(xiàn)在市面上泛濫的懸疑劇,開始埋坑無數(shù),吊足觀眾胃口,最后爛尾不了了之一樣,長此以往觀眾(玩家)對于懸疑劇(游戲)的信任感就被消費殆盡了。

          洗牌道具的實現(xiàn)

          洗牌的實現(xiàn)原理很簡單,把當前桌面的牌記錄在一個數(shù)組tiles中,當需要洗牌時,先打亂一下數(shù)組中牌的順序,然后讓桌面上每一張牌到tiles中重新取一個值。再來個眼花繚亂點的動畫,還真挺像那么回事兒。 在這里插入圖片描述

          func shuffle_tiles(): tiles.shuffle() tiles_index = -1 func redistribute_face() -> int: tiles_index += 1 return tiles[tiles_index]

          遮罩文件的讀取

          這里要夸一下Godot Engine,它的很多功能真是方便,比如下面這個str2var它可以簡單粗暴地直接把字符串轉(zhuǎn)換成對象類型。

          class_name FileReader static func read(path,default_data): var data = default_data var file = File.new() file.open(path,File.READ) var content :String = file.get_as_text() if not content.empty(): data = str2var(content) file.close() return data

          對象間的通信

          這個小游戲中存在大量的對象間的通信需求:牌和牌之間、牌和牌堆之間、牌和關(guān)卡之間、牌堆和關(guān)卡之間。為了快速實現(xiàn)游戲,我大量使用了Godot Engine的Group機制,不得不說Group是Godot Engine最贊的設(shè)計之一。

          在這里插入圖片描述 在這里插入圖片描述

          總結(jié)

          小游戲《羊了個羊》,從策劃和開發(fā)的角度來看并不困難,然而“瑕疵”竟然能夠成為“噱頭”,也讓人不得不感慨“游戲世界真的一切皆有可能啊”。

          作者簡介:

          開發(fā)游戲的老王,高校教師、技術(shù)專欄作者、獨立游戲開發(fā)者,CSDN博客地址:https://blog.csdn.net/ttm2d



          【本文地址】

          公司簡介

          聯(lián)系我們

          今日新聞

          推薦新聞

          專題文章
            CopyRight 2018-2019 實驗室設(shè)備網(wǎng) 版權(quán)所有
            黄色免费网站在线看,韩国精品在线观看,韩国美女一区二区,99国产热 观塘区| 太仆寺旗| 益阳市| 仁怀市| 龙海市| 布拖县| 杭锦后旗| 无为县| 日土县| 浠水县| 谷城县| 菏泽市| 寿宁县| 鄂伦春自治旗| 灵台县| 绿春县| 会理县| 阿拉善右旗| 沾化县| 财经| 林周县| 张家港市| 于田县| 襄垣县| 贵定县| 林西县| 罗山县| 广宁县| 綦江县| 汾西县| 常山县| 郯城县| 安乡县| 洛阳市| 化州市| 临沭县| 敦化市| 吉林省| 西乡县| 工布江达县| 灌阳县| http://444 http://444 http://444 http://444 http://444 http://444