在奇葩的 Json 世界裡冒險! - 《沒有勇者的世界》開發日誌 (2)
這次的開發日誌主要介紹遊戲的探索系統,以及實現這個開放式探索系統用的一些技術。放了一些代碼截圖,希望不會吐槽致死(苦笑)。
這篇日誌沒有題圖了,因為沒法有。正式開學了,一個月只放一天假了,碰不到電腦了。為什麼安卓就找不到可以上傳文件的瀏覽器啊?
為了照顧各位看官的閱讀體驗,今後會把策劃的內容放在文章前面,把程序的內容放到後面,這樣非程序猿大概就不至於被文章里突然出現的代碼給弄懵圈了……
如果沒有看過上一篇日誌,可以先圍觀圍觀,以免不知道這篇在講什麼:用手機開發手游,聽起來沒什麼不對的 -《沒有勇者的世界》開發日誌 (1)
地圖結構
基於純文字與選項的探索系統早就不是什麼新鮮玩意了。玩家通過探索來獲取物品、攻略副本,然後通過選擇選項來處理探索中的各種事件……嗯,一切都是這樣的熟悉。
首先說說遊戲中一個地圖的結構。
玩家在探索頁可以看到已經解鎖的地圖,或者更通俗的名字,副本。每個地圖都由幾個場景組成,而每個場景又包含了許多事件。
例如一張森林的地圖,可能就會包含森林邊緣、森林深處、森林中心三個場景(可能還會有森林神廟之類的隱藏場景),玩家把三個場景依次探索結束後就算通關。
每個場景的難度和事件都是獨立配置的。例如玩家只會在森林邊緣找到普通的木頭,面對蛇、蛤一類的小野怪。但是玩家進入森林中心後,就可以找到稀有的靈木,怪物也會變得頗為強大。
事件
我把探索過程中的普通事件分為了 5 類:
戰鬥(battle):可能會由於玩家的選擇觸發戰鬥,也可能直接是強制戰鬥。
材料(material):獲取地圖內的材料資源。探索的目的之一就是收集材料,然後用於鍛造裝備。獎勵(bonus):「獲取材料」以外的正面事件。例如獲得金錢,恢復生命,等等。陷阱(trap):受到傷害、損失金錢等負面事件。不僅僅是陷阱。取這個名字只是因為我不知道怎麼形容這種事件……選擇(select):完全根據玩家的選擇來決定好壞的事件。
將事件分類後,就可以通過調整各個類型的權重,來簡單地控制場景的難度、長度、資源豐富度等等。
例如,提高材料類與戰鬥類的權重,就可以讓玩家更快速地獲得材料與怪物掉落,而降低權重就可以使得這個場景資源匱乏。
而選擇類的講究就更多了,或者說,它才是探索系統的中心。
在「多彩野果」事件中,玩家會找到各種各樣的野果:紅色的,深綠的,灰色的,黃綠色的,甚至還有黑白相間的……玩家可以選擇吃掉它們或丟掉它們,而吃掉之後會有三種結果:恢復生命、損失生命,以及什麼也不會發生。
玩家剛開始會很懵逼,不知道該不該吃,但是在經歷多次實驗後,他們才會發現一些規律:偏紅色的野果會恢復生命,偏綠色的什麼也不會發生,而黑色、白色那些亂七八糟的就會損失生命。
玩家需要在一次次的嘗試與失敗後獲得經驗教訓,從而提高生存幾率。玩家必須要熟悉這張地圖的選項才能存活得更久、更好,這延長了一張地圖的平均遊戲時間,而玩家也不至於很快感到厭煩,因為他們還沒徹底搞清楚「選擇其它選項會發生什麼」,探索新選項的新鮮感與類 Roguelike 的隨機性會給玩家提供繼續玩下去的動力。
此外,這樣的選擇事件為老玩家與新玩家之間提供了明顯的「經驗差距」——這並不是因為誰的數值高,而是老玩家的經驗更豐富、全面。而老玩家通常也樂意向新玩家提供這些經驗,這種分享對於雙方是互利的:老玩家可以體驗到他人的認同感以及裝逼的快感,新玩家則能獲得情報,提高自己的遊戲水平。
其實,在玩家討論遊戲的時候,也是一種變相的宣傳——至少我就因為周圍一圈同學日復一日地討論皇室戰爭才被拉進了坑(現在已經出來了)。
其實這個設計思路在上一篇開發日誌就已經提及過了,它也被我一直貫徹在遊戲的各個系統中。
事件重用
為了減少工作量,大多數的事件都被多個場景,甚至多個地圖重複使用。所有事件都可以按照重複使用的範圍分類:整個遊戲通用、特定環境內通用、某一地圖通用,等等。
例如上文的野果事件是植物環境通用的(也就是說,所有出現了植物的場景都可能發生這個事件),而「走在平地上突然摔了一跤」這個事件則是整個遊戲通用的。
這就帶來了一個新的問題:有的事件可以出現的場景太少,而全遊戲通用的事件又會在整個遊戲過程中一直出現,甚至會讓玩家感到厭煩。
這個問題可以通過調節事件出現的概率簡單地解決。下圖是某草原地圖中「獎勵」類的隨機池(冒號後面是權重):
不同範圍的事件的發生率如下:
整個遊戲通用的事件:1/16
只在存在植物的場景發生的事件: 3/16只在草原發生的事件:5/16當前場景的專屬事件:7/16
平衡了各個事件的出現次數後,還有一個問題:事件的數據也得隨著場景變化。例如,在新手村觸發的治癒事件能恢復 5 點生命,但在玩家歷經千辛萬苦,打最終 BOSS 的時候,如果治癒事件還是只能恢復 5 點生命,可就有點尷尬了。
當然,一個非常簡單粗暴的解決方案就是應用比例,例如「恢復最大生命的 20%」。雖然這是個解決方案,但大範圍地使用卻並不是一個精明的主意,這會讓玩家非常鬱悶的:剛開始,我在新手村被陷阱坑三次就會死,現在這遊戲玩了這麼久了,在新手村 TM 還是坑三次就會死!
讓難度自適應玩家的數據,帶給玩家的體驗會非常糟糕。這樣他們一直不能釋放難度帶來的壓力,並且進步所帶來的進展感也會變得非常之弱。
這是我的解決方案:
首先,定義一個標準傷害,玩家在受到一定次數的次這個傷害後就會死亡。玩家的存活能力與最大生命值也會隨著難度增大而增長,根據這些數據的估計值,大致可以計算出標準傷害(y)隨難度(x,0 ~ 10)變化的函數。
例如,難度 1 時玩家最大生命為 100,讓玩家受到 16 次標準傷害後死亡,那麼標準傷害就是 100÷16≈6.3。
難度 9 時玩家最大生命預計為 265,讓玩家受到 24 次標準傷害後死亡(讓玩家的生存能力緩慢增加),那麼標準傷害就是 265÷24≈11。
需要適應難度的傷害值都可以藉助數值調整器來乘以標準傷害。例如下面的事件(第一個 damage 指定事件類型為「造成傷害」,第二個 damage 是標準傷害調整器):
"action": {n "damage": {n "damage": 3n }n}n
它會對玩家造成 3 倍的標準傷害,所以實際的傷害在難度為 1 時是 6.3×3≈19,在難度為 9 時則是 11×3=33。
除此之外,還有 in 調整器,它在難度為 0 的時候 0,在難度為 10 的時候才乘以 1。out 調整器與此相反,隨著難度增加,乘數從 1 降到 0。把它們運用在事件與分支的權重上,就可以起到「隨著難度增加,有的事件不會再發生,但又出現了新的事件」的效果。
Json 與 Mod
預防針:從這裡開始程序相關的東西就比較多了。
遊戲的所有數據都使用 Json 來存儲:物品、事件、地圖、對話、調整器……(Gson 是個好東西)
在讀取遊戲數據時,遊戲會優先讀取 SD 卡中的 Json 文件。SD 卡里沒有找到的時候,再讀取遊戲內置的數據。也就是說,玩家可以通過往 SD 卡里添加與遊戲內路徑相同的 Json,來替換、添加、刪除遊戲內容,這也就暫且就算是 Mod 了吧。
為了簡化 Json,少打點字,以及帶來更多的語法糖,Json 格式被前前後後改了三次……
還是以造成傷害為例,簡單介紹一下目前的 Json 格式是怎麼來的。最基本的,造成 3 點傷害(不使用調整器)。
"action": {n "type": "damage",n "value": 3n}n
為了少寫幾個字(用手機在不同文件間複製、粘貼就夠煩人了),動作種類(type)就被直接改成了 Key。
於是格式就簡化了下面這樣。
"action": {n "damage": 3n}n
接著就是隨機值。下面的例子會造成 5 ~ 10 點傷害。
"action": {n "damage": {n "min": 5,n "max": 10n }n}n
看吧,這個也太麻煩了,於是隨機數又被簡化成了下面這樣。包含兩個數字的數組會返回兩個數字之間的隨機值,而包含一堆字元串的數組會返回數組中的一個隨機字元串,等等。
"action": {n "damage": [5, 10]n}n
最後就是調整器。調整器原來也是用 type 和 value 的,但是又被我簡化成了一個鍵值對,也就是下面這個樣子。
它會造成 1.5 ~ 3 倍標準傷害。遊戲里的事件傷害也基本都是這個格式,隨機的倍數傷害。
"action": {n "damage": {n "damage": [1.5, 3]n }n}n
而事件、隨機池等可以直接寫文件路徑來實現文件引用。如果以「..」開頭的話,會以當前場景的文件夾作為根目錄,否則會以 mod 文件夾作為根目錄。
因此,玩家只需要在隨機池裡添加新文件的引用,就可以添加新事件、新物品什麼的了。
並且,以當前場景文件夾作為根目錄也實現了通用的事件與場景私有的物品之間的交互(就像調用被子類複寫的方法一樣)。例如,整個遊戲通用的開箱子事件可以通過文件引用來使用當前場景的掉落物,這樣同一個事件在不同的場景里就可以提供不同的物品了。
這個通用事件的一些分支使用了當前所處場景的掉落物表(..bonus/level/掉落物等級)
總之,搞了這麼一大堆千(喪)奇(心)百(病)怪(狂)的語法糖,目標就是讓純 Json 可以實現與程序類似的效果,同時節省時間,降低 Mod 製作門檻。
我希望將 Mod 做成類似《逗比人生》這種「由玩家提供內容」的形式。玩家可以提供事件的點子,甚至直接做成 Mod,然後我就挑優秀的 Mod 進行整合。畢竟遊戲走奇葩風格,思維開放性也就會很強,事實上我一個人的腦洞也遠遠不夠。以後我大概還得寫個官方的開發文檔啥的?
Gson 與 TypeAdapter
簡單說說這一段的代碼吧。上面的小標題已經劇透了,用的是 Gson 與 TypeAdapter(實際上也就 Gson 能這樣玩吧)。
下面就簡單地說一下瞎改 Json 的過程中的一些難點以及碰到的一些坑。
為了讓「花式數字表示法」同時兼容 int、float、double 等類型,實際上這裡使用的不是基礎類型,而是 Number。
Number 類在反序列化時可以接受各種類型的數字,在序列化時也會直接轉成對應的數字,而不會出什麼被取整了的問題。
動事件里大量的 Number 數據類型。
說最後一個問題。解析文件引用的字元串時,出現了奇怪的情況。
這是 Json 的原始數據:
"item": "select/public/路中央的寶箱"n
在這個值傳入 Adapter 時,字元串莫名其妙地變成了 "select",斜杠後面的文本消失了。
下圖是還原的情況,直接用 "select/public" 這個字元串要求解析為 String,但是 TypeAdapter 獲得的 Json 數據只有 "select"。
經過反覆測試,在直接傳入字元串時,Gson 會忽略掉左斜杠後的內容,原因不明。暫且只能推測左斜杠(/)在某些情況下就像右斜杠()一樣起到轉義作用,但是實際上並沒有找到相關資料。
以下是測試的部分結果:
輸入:"select/public"n輸出:"select"nn輸入:"select//public"n輸出:"select"nn輸入:"select/public"n輸出:編譯器報錯:Unexpected /n(這可能是 AIDE 的鍋?)nn輸入:"/select/public"n輸出:com.google.gson.stream.MalformedJsonExceptionn(Expected value at line 1 column 1 path $)n
解決方法是將字元串先用 toJsonTree 轉換為 JsonElement,再進行解析,這樣才能正常傳入數據。(MDZZ)
結語與吐槽
出乎意料,又寫了五千字……
目前由於美術風格還沒確定,UI 與動畫製作也還繼續擱置著。不過話說回來,在 Demo 階段把 UI 做得很好對我這種時間少、沒電腦的人來說確實蠻傻的。做得太精細了,改一點界面就會有強烈的「哇,好浪費啊」的感覺,所以 UI 暫且保持無圖黑白吧。
純手機編程的效率也確實坑,兩周才做了這麼點東西,如果放電腦上做速度能快一倍。但是現在高三了,一個月也只有一天假,並且那天假我選擇摸魚。
說來在上一次的開發日誌里,有不少人問我的學業啊……我並不是很想在文章里說我成績怎麼樣什麼的的,現在成績就比一本線高七八十,雖然考不上什麼名校,但也算是湊合吧(笑),大家就別在意啦~
學校讓我去參加 NOIP,然而我在四川,強省。看著成都綿陽那些競賽班大佬,我就只想去大學校園二日遊了(苦笑)。我一個業餘的要我怎麼才能和學了幾年的專業大佬同台競技啊!
下一篇日誌在 9 月底咯,大家可以關注一下這個專欄~再見!
推薦閱讀:
※從零開始手敲次世代遊戲引擎(三十六)
※從零開始手敲次世代遊戲引擎(二十六)
※陳灼:我在2K的八年(序)
※準備玩到手殘么?200款遊戲,70家廠商,遊戲業決戰最後百天