遊戲開發中的細節——“描述”


3樓貓 發佈時間:2022-10-01 08:20:08 作者:mnikn Language

The devil is in the details
大部分的教程都會教你怎麼實現一個功能,不過卻很少從工程的角度去分析和解釋該怎麼去實現一些必須要有但是又難以管理的細節。
這次我們討論的主題是——描述。在遊戲中,想要表達一個技能、一項物品有什麼功能效果,肯定會出現一大段字來描述,這篇文章重點關注的就是這段字要怎麼顯示的問題。
單純一看這個問題好像很簡單,所有遊戲引擎都會有顯示文本的功能,直接把值顯示上去不就好了嗎?別急!我們要做的是從工程角度上去思考,就以實際案例《欺詐之地》這一個卡牌遊戲為例吧。

方案1

我們先拿一張攻擊牌,看看它的描述:
我們先設計下這張卡牌的數據格式,這裡就以 json 為例吧:
{ "id": "big_hammer", "name": "大錘", "type": "attack", "desc": "當你有任意連擊點時,造成2額外傷害,獲得2連接點", // 額外傷害 "extra_damage": 2, // 連擊點 "extra_combo_point": 2 }
按照這結構,好像我們直接用 desc 顯示就行了,但是如果我們在後期調試數值時,修改 extra_damageextra_combo_point
的值,desc 並不會隨著值得變化而更改描述,尤其是當你有成百上千張卡牌時,每張卡牌都在描述裡寫死數值就不太實際了。

方案二

我們在代碼里根據不同的佔位拿對應的值去展示,desc 為了表示數值佔位就改為:"當你有任意連擊點時,造成{{extra_damage}}額外傷害,獲得{{extra_combo_point}}連接點"
在代碼裡我們直接用正則匹配 {{}},拿 {{}} 裡面的內容當字段名讀取,這裡為了方便就先用 javascript 來演示,至於像 c# 的靜態語言就各顯神通(例如反射)自行處理吧:
const skillData = { "id": "big_hammer", "name": "大錘", "type": "attack", "desc": "當你有任意連擊點時,造成{{extra_damage}}額外傷害,獲得{{extra_combo_point}}連接點", "extra_damage": 2, "extra_combo_point": 2 } ​ let realDesc = skillData.desc; // 匹配 {{}} 裡的值,做一個值的映射表 const valueMap = realDesc.match(/\{\{\w+\}\}/g).reduce((r, key) => { r[key] = skillData[key.substring(2,key.length-2)]; return r; }, {}); ​ // 根據映射表替換文本 Object.keys(valueMap).forEach((key) => { realDesc = realDesc.replace(key, valueMap[key]); }); console.log(realDesc);
這個方案能夠解決大部分固定數據的情況,但是當你的展示數據是動態時,就有點不夠用了。

方案三

方案二處理簡單的展示方式沒什麼問題,直到我們遇到了這一張卡牌:
你會發現這張牌的描述需要依賴於其他牌的名字,假設這幾張牌的數據如下:
[{ "id": "sal_dagger", "name": "薩兒的匕首", "type": "strategy", "desc": "手牌中加入{{reward_cards[0]}}或者{{reward_cards[1]}}", "reward_cards": ["big_hammer", "hold_knife"] }, { "id": "big_hammer", "name": "大錘" }, { "id": "hold_knife", "name": "握刀" }]
從數據設計的角度出來 reward_cards 一般只存放對應卡牌的 id,而 desc 裡面則需要根據 id 讀取技能的名字再展示,這種情況就需要我們的數據擁有執行表達式的能力了。
我們改造下 desc 的描述為:"手牌中加入:var{skills[current_skill.reward_cards[0]].name}或者:var{skills[current_skill.reward_cards[1]].name}",這裡我們用一個特殊的關鍵字 :var 來標記這個數據來自外部,而 :var 括號裡面則是表達式的內容,我們需要在外部傳 一些變量例如 skills
current_skill 來讓表達式裡面的代碼使用。
示例代碼如下:
const skills = { "sal_dagger": { "id": "sal_dagger", "name": "薩兒的匕首", "type": "strategy", "desc": "手牌中加入:var{skills[currentSkill.reward_cards[0]].name}或者:var{skills[currentSkill.reward_cards[1]].name}", "reward_cards": ["big_hammer", "hold_knife"] }, "big_hammer": { "id": "big_hammer", "name": "大錘" }, "hold_knife": { "id": "hold_knife", "name": "握刀" } } ​ const currentSkill = skills["sal_dagger"] let realDesc = currentSkill.desc; realDesc.match(/:var{[^}]+}/g).forEach((exp) => { realDesc = realDesc.replace(exp, eval(exp.substring(5, exp.length - 1))); }); console.log(realDesc);
有了這樣一個機制我們在填寫描述時基本上是可以為所欲為了,任何動態的數據都可以通過外部傳過來展示,同時可以自行進一步實現其他關鍵字例如 :visible 來判斷展示時機。

總結

開發中沒有銀彈,所有的方案都有利有弊,大家會發現當方案越來越靈活時,對於填數據的人的要求也越來越高。方案一是有手就能填,方案二則需要了解一些佔位數據的寫法,方案三已經是直接在寫代碼了,方案的選擇需要根據自身情況實際應用。
這篇文章旨在拋磚引玉,如果大家有什麼更好的方案,歡迎在評論區提出!

© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com