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_damage 和 extra_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 来判断展示时机。
总结
开发中没有银弹,所有的方案都有利有弊,大家会发现当方案越来越灵活时,对于填数据的人的要求也越来越高。方案一是有手就能填,方案二则需要了解一些占位数据的写法,方案三已经是直接在写代码了,方案的选择需要根据自身情况实际应用。
这篇文章旨在抛砖引玉,如果大家有什么更好的方案,欢迎在评论区提出!