控制台遊戲專題又來了,這次是你們沒有見過的船新版本

控制台遊戲專題又來了,這次是你們沒有見過的船新版本

來自專欄遊戲開發入門指南——Unity+43 人贊了文章

容我組織一下語言先。

《Baba is You》是去年Nordic Game Jam公布的一款益智解謎類遊戲,而且還一舉奪得了冠軍的位置。

名字感覺有點誰占誰便宜的味道,但不要誤會,baba只是遊戲中主角的名字。而且,之所以用了「xx is xx」這樣一個不常用於遊戲名的主系表結構,是因為這跟遊戲的基本規則是密切相關的

遊戲預定於今年上steam。

···道理我都懂,可這跟這次的文章有啥關係?

你肯定已經猜出來了。沒錯,本期有一位童鞋將《Baba is You》這款還未正式發行的遊戲在控制台上復刻了一下,並參考原作的試玩版製作了十餘個關卡。

粗看,這就是一款普通的推箱子遊戲。但不同的是,遊戲將具體的規則也具象化成了一個個可以推動的「箱子」,並直接放在了場景中。玩家需要仔細思考所有的可能性,才能打破僵局通過關卡。即是說,這實際上是一款「披著推箱子遊戲外皮的腦洞遊戲」

可能這麼說有點抽象,我配合具體的關卡演示來說明一下:

這一關,我用常規思考方式想了半天愣是沒想明白該如何過關。

首先,主角被困在了場景中間的凸字區域的上半部分內,無法通過改變牆的屬性穿牆出去。

然後,初始狀態下有1個球必被用來填河,這樣才能讓主角移動至凸字區域的下半部分。這樣就只剩1個球了,而要想到達勝利條件——星星,至少需要兩個物體來填河。「星is贏」倒是可以被推動,但這樣一來規則又被改變了,即使能到達星星,也完全沒意義了。

後來經童鞋點化才回過神來:我們為啥不能改動勝利規則呢?腦洞,腦洞啊。

一下子就過了。

再比如這一關。粗看感覺完全無從下手,男AI在場景中傻不拉幾地晃蕩,關鍵的星星又是在牆和火的中間,勝利條件「星is贏」也是靠牆貼著的不能被推動。

腫么辦啊?

誒對了,如果晃蕩的是火本身,能否打開僵局?

這裡有一個bug,火成為AI後移動會增加自己的數量以及額外的牆體

體會到前面說的「腦洞」的真正含義沒?

這位童鞋的控制台版可以說已經是極為還原的實現了大部分的功能,但美中不足的還是有一些BUG和邏輯上的問題。

例如:

這是控制台版的第一關。當我們移開了牆的「停」這個規則,牆就如我們所願,是可以穿過去的。然而當我們移開了球的「推」這個規則,球卻不能穿過,彷彿球這裡多了個「停」的規則一樣。

我們來交換一下牆和球的規則:

可以看到一開始成功交換了球和牆的規則,牆變成可推動的物體,球則會阻擋玩家行動。但是當把兩個規則推開後出現了相同的問題(還會出現一個渲染層的BUG)。

因此,接下來我們就著這些問題再稍微說說實現這樣一款遊戲會遇到的一些難點。沒錯,本篇文章,實際上是「披著作品介紹外皮的技術向文章」

而且,難度是4星哦!

本篇難度:☆

1.複合邏輯

如上所述,本遊戲的主要賣點,是規則是可以隨著操作而變化

而這裡的看似簡單、一個字就能概括的規則,其實有可能是多個邏輯的疊加。

用原版遊戲舉個例子:

操作玩家進入岩漿中會被殺死,但是你推著石頭進入岩漿石頭卻不會有事。所以這裡岩漿上的「KILL」規則其實也是一個複合邏輯,一條是「會殺死進入其區域的物體」,另一條是定義這個被殺死的物體是「誰」,在原版遊戲里這個會被殺死的物體被指定為玩家操作的物體。

因此當我們想要實現原版遊戲里的規則或者我們想自己定義新的規則時,需要把這個規則的邏輯拆分清楚。

還有一種情況。當一個主體同時關聯了兩種邏輯(比如「you」和「push」)時,兩種邏輯這時候需要分別獨立生效,如果只單獨的用一種標記去標識物體的類型勢必會造成邏輯的錯誤。

那麼在代碼里該怎麼實現呢?

可能有聰明的同學想到了可以把邏輯標識符(例如一個枚舉類型的變數)放在容器里去管理,在聲明物體類型時用遍歷這個容器的方法來識別物體的實例。這樣確實能夠實現,但是多次遍歷會更多的佔用性能,再一個代碼也不太美觀。

我們有更好的方法,使用C#的一個特性:位域。

[Flags] public enum LogicType { //沒有附加任何邏輯,只是顯示在遊戲界面里 Null = 1, //其他邏輯 Win = 1 << 1, You = 1 << 2, Stop = 1 << 3, Push = 1 << 4, Kill = 1 << 5, Sink = 1 << 6, AI = 1 << 7, Subject = 1 << 8, Object = 1 << 9 }

位域就是在使用枚舉變數時加入位運算,來達到組合多個狀態的目的。其使用方法是在聲明枚舉上加上[Flags]。如此一來我們就有了一個好用的工具去處理混合邏輯的問題。

我們可以使用位運算的「或」去合併多個邏輯:

static void Test() { GameObject obj = new GameObject(1, 2); obj.logic_type = LogicType.Push | LogicType.You; obj.logic_type = LogicType.Stop | LogicType.Sink | LogicType.Win; }

同時也可以很方便的檢查枚舉中是否包含某一個邏輯:

public bool HasLogic(LogicType logic) { return (logic_type & logic) != 0; }

也可以去掉混合邏輯的枚舉其中單獨一個的邏輯:

public void RemoveLogic(LogicType type) { logic_type = logic_type & (~type); }

2.遊戲內物體的類型與關係

粗略一想,遊戲內的所有物體大致能分成兩類:

1.能附加規則的實體,即所有不是字的圖標類物體。這一類物體的通用規律是他們與場景內其他遊戲物體交互的邏輯是可以改變的,而且能附加的規則可以是多個。

2.能通過改變位置來修改規則字類型的物體。這一類物體又可以細分為三種不同的類型,一種是名詞,即含有代指圖標類型物品信息的詞。一種是動詞,即包含邏輯規則信息的詞。還有一種就是特殊的「is」,用來連接兩種片語成邏輯關係。其中能作為主體的只能是名詞(仔細想想動詞做主體有沒有意義),而作為客體的既可以是名詞,又可以是動詞。當名詞與名詞進行邏輯連接代表的意思就是將所有主體詞指代的遊戲物體變換成客體所指代的遊戲物體。

這在代碼里我們完全可以全部用一個類來聲明:

public class GameObject : ICloneable { //在地圖上的坐標 public int x; public int y; //邏輯與類型信息 public LogicType logic_type; public ObjectType object_type; public ConsoleColor color; //顯示默認兩個空格,因為漢字佔用兩個位元組。 public string Icon = " "; //指代的物體圖標,僅名詞 public string contect_Icon; //附加的邏輯關係,僅動詞 public LogicType effect_logic; //除String外所有欄位都是值類型,因此可以直接使用淺表拷貝 public object Clone() { return MemberwiseClone() as GameObject; } public GameObject(int x, int y) { this.x = x; this.y = y; logic_type = LogicType.Null; } public bool HasLogic(LogicType logic) { return (logic_type & logic) != 0; } public void RemoveLogic(LogicType type) { logic_type = logic_type & (~type); } }

我們在實例時候通過類型信息來區別賦值。

//類型枚舉,因為類型唯一故不需要使用位域特性 public enum ObjectType { eneity, verb, noun }

相信有同學已經想到了,只有作為實體的物體才能修改規則邏輯信息,而文字類型的物體其在遊戲中的規則一律是固定不變的「push」,在後面的代碼設立邏輯規則的時候一定要注意。

3.邏輯關係的先後順序

這個問題比較頭疼,其原因是混合邏輯後,你很難在代碼里用一套通用的規則來實現。原版里就有這樣的問題:

可以看到:當給牆同時附加了「you」和「push」之後再移動,牆的移動會因為兩個規則一起生效而錯位。如果我們用同樣的邏輯在控制台里實現,可能會出現更嚴重的由邏輯衝突引起的BUG,比如只有頭前的兩個能動或者全都動不了。因此我們在自己實現時要想辦法解決這個問題。

我們知道,在程序運行時一定都是按步驟有先後順序的(這裡先不考慮回調或多線程),因此哪怕是牆附加了「you」的規則,在移動時也是一個一個先後移動的。因此我們可以在檢測一個物體能不能被推動時遞歸檢測移動方向上的所有物體是否可以移動:

//返回是否能改變坐標,若能並移動 static bool CanMove(GameObject obj, Direction dir) { switch (dir) { case Direction.Left: if (obj.x == 0 || !CheckDirectionLogic(obj, dir)) { return false; } obj.x -= 1; break; case Direction.Right: if (obj.x == map_width - 1 || !CheckDirectionLogic(obj, dir)) { return false; } obj.x += 1; break; case Direction.Up: if (obj.y == 0 || !CheckDirectionLogic(obj, dir)) { return false; } obj.y -= 1; break; case Direction.Down: if (obj.y == map_height - 1 || !CheckDirectionLogic(obj, dir)) { return false; } obj.y += 1; break; } return true; }

static bool CheckDirectionLogic(GameObject caller, Direction dir) { bool just_one = true; var next_objs = GetObjectsByDir(caller.x, caller.y, dir); if (next_objs.Count == 0) { return true; } if(next_objs.Count>1) { just_one = false; } foreach (var obj in next_objs) { if (obj.HasLogic(LogicType.Stop)) { return false; } if (obj.HasLogic(LogicType.Push)) { return CanMove(obj, dir); } } return true; }

就像擊鼓傳花一樣:當移動方向上的最後一個物體也能移動時,先讓最後一個移動,然後逆向按著順序一個個移動。

還有一種情況是,邏輯順序的先後都是合理的,取決於你自己想要達到的邏輯生效的順序。

原版是「push」的規則生效在前。

控制台版則是「win」的規則生效在前。

在代碼里以此對邏輯的枚舉變數檢測的順序決定了其之後的調用順序。

4.顯示的層級順序

由於我們實現的是一個基於控制台的遊戲,因此在我們自定義的二維數組地圖中每個坐標只能顯示一個物體,而不能向原版一樣圖層重疊顯示。

所以,我們需要自己定義一個顯示先後順序的規則。我們的地圖是在遍歷裝有所有物體的容器後依次列印的。

public static void DrawALL() { foreach (var obj in all_objs) { if (obj.Icon != "") { buffer[obj.x, obj.y] = obj.Icon; } color_buffer[obj.x, obj.y] = obj.color; } }

並且在顯示之前會與之前上一次顯示的圖像做一個對比,只修改有變化的而沒有變化則不去管。

public void RefreshDoubleBuffer() { for(int i=0;i<_height;i++) { for(int j=0;j<_width;j++) { if(Buffer[i,j]!=BackGroundBuffer[i,j]) { Console.SetCursorPosition((i + 1) * 2, j + 1); Console.ForegroundColor = ColorBuffer[i, j]; Console.Write(Buffer[i, j]); } } } }

這樣寫的目的主要是為了防止每次操作都重新遍歷列印一次全部圖像而帶來的閃屏問題。然而這樣就帶來了另一個新的問題:顯示的順序與我們期望的不符。

導致這個問題的原因,是我們的人物在遍歷的順序中排中間,所以往遍歷順序較前的方向移動時,會覆蓋原本的圖像,而往遍歷順序較後的方向移動則會被後面的圖像覆蓋。

解決這個問題的方法是我們在每次調用顯示函數的時候先給所有物體排個序,讓希望顯示順序較前的物體在容器里的下標位置靠後。

public static void DrawALL() { all_objs.Sort((a, b) => a.logic_type.CompareTo(b.logic_type)); foreach (var obj in all_objs) { if (obj.Icon != "") { buffer[obj.x, obj.y] = obj.Icon; } color_buffer[obj.x, obj.y] = obj.color; } }

可以自己定義排序的規則。這裡用的具體方法是用每個物體自身的邏輯類型的枚舉變數來進行排序。還記得前面對邏輯類型的定義么?邏輯類型為NULL的枚舉值轉換為int的值是最低的,因此在顯示中所有能「動」的物體在顯示中的順序就會比較靠前。

好了,技術講解到此完畢。

可以再次看到,控制台遊戲極力精簡了畫面等一切表現層面的元素,但對於關鍵邏輯的實現是一個都不能含糊——這決定了其天生就是極其鍛煉遊戲開發能力本身的

放上相關的工程連接:

原版:github.com/tank1018702/

控制台:tank1018702/C-001

控制台(BUG調整後):tank1018702/C-001

我很欣慰地發現,童鞋們的主觀能動性是一屆賽一屆的強,願意主動嘗試新的、有創意和有挑戰的遊戲類型——而這,其實是作為一個開發者,而不是一個需求實現器所最需要具備的素質。

還等什麼,趕緊就地開始遊戲開發之旅吧!


推薦閱讀:

為什麼電腦正在運行的文件無法刪除?
PhotoShop CS2 9.0 解決 組織或序列號丟失或無效。應用程序無法繼續|PhotoShop
育肥期保健的實施程序|搜豬網
農業行政處罰程序和文書製作
如何在blockstack上寫一個區塊鏈「微博」程序?

TAG:遊戲 | 遊戲開發 | 程序 |