翻譯:瘋狂的技術宅
原文: https://www.smashingmagazine....
本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章前端
據說過文字冒險遊戲嗎? 若是你的年齡足夠大的話(就像我同樣),那麼你可能據說過、甚至玩過「back in the day」。在本文中,我將向你展現編寫的整個過程。這不只僅是一個文本冒險遊戲,而是一個能讓你和你的朋友們一塊兒玩的,能夠進行任何劇情的文本冒險遊戲引擎。 沒錯,咱們將經過在添加多人遊戲功能來增長它的趣味性。node
文字冒險是最先的 RPG 形式的遊戲之一,回到尚未圖形畫面的時代,你只能經過閱讀 CRT 顯示器上黑色背景下的描述,而且依賴本身的想象力來推進遊戲劇情的發展。程序員
若是要懷舊的話,可能世界上第一個文字冒險遊戲名叫 Colossal Cave Adventure(也許是叫 Adventure)。面試
文字冒險遊戲 back in the day 的畫面json
上圖是你實際看到的遊戲畫面,這與咱們如今的頂級 AAA 冒險遊戲相差甚遠。 儘管如此,可是他們玩起來卻頗有趣,並會很容易的消磨你幾百個小時的時間,由於只有你本身本身坐在顯示器前,試圖找到打穿它的途徑。segmentfault
能夠理解的是,多年以來,文字冒險已經被更好的視覺效果所取代,特別是在過去幾年裏,遊戲的協做性越強,你能夠和朋友們一塊兒玩。 這是原始的文字冒險遊戲所缺乏的,同時也是我想在本文中提到的功能。windows
可能你已經從標題中猜到了,本文的重點在於建立一個文字冒險引擎,而且讓你和朋友們一塊兒玩,使你可以與他們進行協做,就像在玩「龍與地下城」這個遊戲同樣。數組
在建立引擎時,聊天服務器和客戶端的工做了至關大。 在本文中,我將向你展現設計思路、解釋引擎背後的架構、客戶端如何與服務器交互以及這個遊戲的規則。服務器
爲了讓你對個人目標又一個直觀的感覺,先上一張圖:微信
遊戲客戶端的 UI 設計
這就是咱們的目標。 一旦達成這個目標,將會獲得截圖而不是簡單和骯髒的模型。 因此,須要瞭解這個過程。首先要介紹的就是總體設計;而後介紹我將用來編碼的相關工具;最後我將向你展現一些核心代碼(固然,還有指向完整代碼庫的連接)。
但願到最後,你可以本身創造一個新的文字冒險遊戲,並與朋友一塊兒樂在其中!
在設計階段,我將描述這個遊戲的總體藍圖。 我會盡力不讓你以爲無聊,不過我認爲在給你展現第一行代碼以前,頗有必要先搞清楚幕後的一些工做。
我想接下來介紹的這四個組件可以提供至關多的細節:
遊戲引擎或遊戲服務器將會是REST API,並提供全部必需的功能。
我選擇REST API只是由於(對於這種類型的遊戲)HTTP形成的延遲以及他的異步特性不會形成任何麻煩。 可是,咱們必須爲聊天服務器採用不一樣的路線。 在開始定義 API 以前,先須要定義引擎的功能。 因此,讓咱們來看看吧。
特性 | 描述 |
---|---|
加入遊戲 | 玩家能夠經過指定的遊戲ID來加入遊戲。 |
建立一個新遊戲 | 玩家還能夠建立新的遊戲實例。 引擎應該返回一個ID,以便其餘人可使它來加入遊戲。 |
返回場景 | 此功能應返回玩家所在的當前場景。 基本上,它將返回描述,包含全部相關信息(可能的操做、其中的對象等)。 |
與場景互動 | 這將是最複雜的一個,由於它將從客戶端獲取命令並執行該操做——例如移動,攻擊,獲取,查看,讀取等等。 |
檢查庫存 | 雖然這是與遊戲互動的一種方式,但它與場景並無直接關係。 所以,檢查每一個玩家的庫存將被視爲不一樣的操做。 |
咱們須要一種用來測量遊戲中距離的方法,由於在遊戲中玩家能夠採起的核心行動之一就是移動。 咱們須要用這個數字做爲時間的衡量標準,來簡化遊戲的玩法。 考慮到這一類型的遊戲具備基於回合的動做,例如戰鬥,使用實際時鐘對時間進行測量可能不是最好的。 因此咱們將使用距離來測量時間(意味着距離爲 8 比距離爲 2 將須要更多的時間,從而容許咱們作一些事情,例如爲持續必定數量的「距離點」的玩家添加效果)。
考慮運動的另外一個緣由是否是一我的在玩這個遊戲。 爲簡單起見,引擎不會讓玩家隨意組隊(雖然這對將來多是一個有趣的改進)。 該模塊的初始版本只容許我的朝着大多數參與者決定的地方移動。所以,必須以協商一致的方式進行移動,這意味着每一步行動都將等待大多數人在行動以前提出請求。
戰鬥是這種遊戲另外一個很是重要的方面,咱們不得不考慮將它添加到引擎中,不然咱們最終會失去一些樂趣。
說實話,這並不須要從新發明輪子。基於回合制的組隊對戰已經存在了幾十年,因此在這裏只實現這個機制的一個簡單版本。咱們將把它與「龍與地下城」中的「主動性」這個概念混合起來,產生一個隨機數使戰鬥更有活力。
換句話說,就是參與戰鬥的每一個人的行動順序將會被隨機化,其中包括敵人。
最後(雖然我將在下面詳細介紹這一點),你能夠用設置的「攻擊力」值的物品。這些是你在戰鬥中可使用的道具;若是一個道具沒有這個屬性的話只能對敵人形成 0 點傷害。當你試圖用這樣的道具進行戰鬥時,咱們可能會添加一條消息,這樣你就能知道本身要作的事情是毫無心義的。
如今來看看客戶端怎樣基於前面定義的功能與服務器進行交互(目前還沒考慮端點,不過立刻就會講到這個):
客戶端與服務器之間的交互
客戶端和服務器之間的初始交互(從服務器的角度來看)是一個新遊戲的開始,其步驟以下:
遊戲的動做指令
一旦知足了先決條件,玩家就能夠開始遊戲,經過聊天室分享他們的想法,並推進故事的發展。上圖顯示了所需的四個步驟。
如下步驟將做爲遊戲循環的一部分來運行,這意味着它們將會不斷重複,一直到遊戲結束。
這應該屬於第二步,但爲了清楚起見,我把它做爲額外步驟。主要區別在於第二步能夠被認爲是這個循環的開始,而這一步考慮到你已經開始進行遊戲了,所以,服務器須要瞭解這個動做將影響誰(單個或全部玩家)。
做爲額外步驟,雖然不是流程的一部分,但服務器將通知客戶端與它們相關的狀態的更新狀況。
存在這個額外重複步驟的緣由是玩家能夠從其餘玩家的動做中得到更新。回想從一個地方移動另外一個地方的需求;正如我以前所說那樣,一旦大多數玩家選擇了方向,那麼全部玩家都會移動(不須要全部球員的輸入)。
不過 HTTP(前面已經提到服務器爲REST API)不容許這種類型的行爲。因此,咱們的選擇是:
根據個人經驗,我傾向於選擇選項 2。實際上,我會(在本文中)使用Redis來實現這種行爲。
下圖演示了服務之間的依賴關係。
客戶端應用程序與遊戲引擎之間的交互
我將把這個模塊的設計細節留給開發階段(本文不涉及這一部分)。話雖如此,咱們仍能夠決定一些事情。
咱們能夠肯定的一件事是服務器的限制集合,這將簡化咱們的工做。若是咱們正確地玩牌,最終可能會有一個提供強大界面的服務,從而容許咱們去進行擴展甚至修改實現,以提供更少的限制,而不會影響到遊戲。
這就是聊天服務器。畢竟,它不會很複雜。在開始編碼以前還有不少工做要作,可是對於本文來講已經足夠了。
這是最後一個須要編碼的模塊,它將是最笨重的一個模塊。根據經驗來看,我更喜歡讓客戶端笨重,使服務器輕巧。這樣爲服務器開發新的客戶端會更加容易。
這是咱們最終應該採用的架構。
最終架構
咱們要實現的ClI客戶端很簡單,不會實現任何很是複雜的東西。實際上,必需要解決的最複雜的部分是 UI,由於它是一個基於文本的界面。
客戶端應用程序必須實現的功能以下:
稍後將詳細介紹客戶端的內部結構和設計。與此同時,讓咱們完成設計階段的最後一部分:遊戲文件。
這是它變得有趣的地方,由於到次爲止,我已經涵蓋了基本的微服務定義。其中一些可能會基於 REST,而另一些可能會使用套接字,但本質上它們都是同樣的:你定義並對它們編碼,而後它們提供服務。
我不打算對這個特定的組件作任何編碼,但咱們仍然須要設計它。基本上咱們是在實現一種協議來定義遊戲、它內部的場景以及一切。
若是你想想,文本冒險的核心基本上是一組相互鏈接的房間,裏面是你能夠與之互動的「事物」,全部這些都與一個引人入勝的故事聯繫在一塊兒。如今咱們的引擎不會處理最後一部分,這部分將取決於你。
如今回到相互鏈接的房間,對我來講這就像一個圖結構,若是咱們還添加了前面提到的距離或移動速度的概念,還須要一個加權圖。這只是一組節點,它們具備權重(或只是一個數字 —— 不要糾結它的名稱),表明了它們之間的路徑。下面是一個示意圖(我喜歡經過觀察進行學習,因此只看圖,好嗎?):
這是一個加權圖 —— 就是這樣。我相信你已經弄明白了,但爲了完整起見,讓我告訴你一旦咱們的引擎準備就緒,你將會作些什麼。
一旦開始設置遊戲,你將建立地圖(就像你在下圖中左側看到的那樣)。而後將其轉換爲加權圖,如圖所示。引擎將可以接收它並讓你按正確的順序進行瀏覽。
一個地牢的示例圖
經過上面的加權圖,能夠確保玩家不能從入口一會兒走到左翼。他們必須經過這二者之間的節點,這樣作會消耗時間,能夠用鏈接的權重來測量。
如今,進入「有趣」的部分。來看看地圖在 JSON 格式中的樣子。這個JSON將包含不少信息:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
它看起來有不少內容,可是若是你把它視爲一個簡單的遊戲描述,就會明白這是一個含有六個房間的地牢,每一個房間都與其餘房間相互鏈接,如上圖所示。
你的任務是穿越並探索它。你會發現有兩個地方能夠找到武器(不管是在廚房仍是在黑暗的房間,只要破壞掉椅子就能獲得)。你也將面對一扇上鎖的門,因此,一旦找到鑰匙(位於相似辦公室的房間內),就能夠打開並用你收集到的武器和BOSS展開一場大戰。
你能夠幹掉它而獲勝,也能夠被它殺死而輸掉。
如今讓咱們更詳細地瞭解整個 JSON 結構及其中的三個部分。
這裏包含節點之間的關係。基本上這一部分會直接轉換爲咱們以前看到的圖。
這部分的結構很是簡單。它是一個節點列表,其中每一個節點都包含如下屬性:
本節包含常規設置和條件。特別是在上面的示例中,此部分包含輸贏條件。換句話說,在這兩個條件下,咱們會讓遊戲知道何時結束。
爲了簡單起見,我添加了兩個條件:
這一部分佔了 JSON 文件很大的篇幅,也是最複雜的部分。在這裏描述冒險中全部區域及其內部全部房間。
每一個房間都有一把鑰匙,使用咱們以前定義的 ID。每一個房間都有一個描述,一個物品列表,一個出口(或門)列表和一個非玩家角色(NPC)列表。在這些屬性中,惟一應該被強制定義的屬性是描述,由於引擎須要這個屬性才能讓你明白所看到的內容。若是有什麼東西須要展現,它們只能在那裏。
讓咱們來看看這些屬性能爲遊戲作些什麼。
這一項並不像想象的那麼簡單,由於你看到的房間可能會根據不一樣的狀況而變化。例如:若是你查看第一個房間的描述,就會注意到在默認狀況下,你將看不到任何東西,除非你有一個點亮的火炬。
所以,拾取物品並使用它們,可能會觸發影響遊戲中其餘部分的全局條件。
這些表明了你能夠在房間內找到的全部東西。每一個項目都會共享與 graph 節點相同的 ID 和名稱。
它們還有「目標」屬性,該屬性指示一旦拾取該道具應放在哪裏。這是有意義的,由於你手上只能裝備一個道具,而在揹包中能夠存放不少的道具。
最後,其中一些道具可能會觸發其餘操做或者狀態更新,具體取決於玩家決定用它們作什麼。其中一個例子就是從入口處點燃的火把。若是你拿着一個,將在遊戲中觸發狀態更新,這反過來將使遊戲向你顯示下一個房間的不一樣描述。
道具也能夠有「子道具」,一旦原始道具被銷燬(例如經過「分解」操做)就會發揮做用。一個道具能夠被分解爲多個,並在「subitems」元素中定義。
本質上,此元素只是一個新道具的數組,其中還包含能夠觸發其建立的一組操做。基本上能夠根據你對原始道具執行的操做建立不一樣的子道具。
最後,有些物品會有「傷害」屬性。因此若是你用某個道具擊中 NPC,該值用於從中減去生命。
出口是與道具分開的實體,由於引擎須要知道你是否可以根據其狀態去遍歷它們。不然被鎖定的出口沒法讓你經過,除非你把它的狀態改成已解鎖。
最後,NPC 將成爲另外一個列表的一部分。它們是有狀態信息的項目,引擎將使用這些狀態信息來了解每一個項目的行爲方式。在咱們的例子中定義的是 「hp」,它表明健康狀態,還有「damage」,就像武器同樣,每次命中將從玩家的健康情況中減去相應的值。
這就是我創造的地牢。內容不少,未來我可能會考慮寫一個編輯器,來簡化 JSON 文件的建立。但就目前而言尚未必要。
你可能尚未意識到,這樣在文件中定義遊戲是有很大好處的,可以像超級任天堂時代那樣切換 JSON 文件。只需加載一個新文件就能開始另外一個遊戲。很是簡單!
感謝你能讀到這裏。但願你能喜歡我所經歷的設計過程,並將想法變爲現實。我正在努力實現這一目標。咱們之後可能會意識到,今天定義的內容可能會不起做用,出現這種狀況時,咱們將不得不回溯並修復它。
我敢確定,有不少方法能夠對這裏提出的想法進行改善,並建立一個地獄的引擎。可是這須要在本文中添加的更多的內容,爲了避免讓讀者感到無聊,因此就先這樣吧。