1. 为什么“属性面板+装备穿戴”这个看似简单的东西值得单独开一篇
做MMO游戏做到第二十七篇,还在写UI的人,大概率都被同一件事折磨过:数值逻辑早就跑通了,服务端能算出攻击力、防御力、暴击率,可一旦要把这些数字摆到玩家面前,让玩家点一下装备就能看见面板变化,整个项目的“脏活”就全冒出来了。我见过太多个人项目死在“战斗核心写完了,但装备界面拖了三个月”这个阶段,不是因为难,而是因为这块代码天然夹在数据层、表现层和交互层之间,写得太紧会变成一团乱麻,写得太松又会出现“面板显示攻击力100,实际打出去只有80”的经典事故。这一篇要讲的,就是英雄属性展示和装备穿戴这两件事,它属于游戏客户端里最典型的“看起来毫无技术含量、做起来处处是坑”的模块,适合已经跑通了角色基础逻辑、准备做成长系统的个人开发者参考,也适合刚入行、想搞清楚数据驱动UI是怎么回事的新手跟着抄一遍。
先把核心背景交代清楚。MMO的角色成长系统里,“属性”和“装备”从来不是两件事。装备是属性的来源之一,属性是装备价值的体现,二者必须共用同一套计算链路。很多人在做这一块时走的第一条弯路,就是把面板上的属性直接存成字段,比如角色对象上挂一个attack = 100,捡到一把刀就attack += 20。这种写法在单机原型里能跑,但只要出现“装备卸下”“多件装备互相加成”“临时buff叠加”这几种情况,数值立刻开始漂移,因为你没有任何地方能回答“这个100到底是由哪几项构成的”。正确做法只有一条路:基础属性单独存,装备加成单独算,最终面板在每次相关变化时重新推导。这个原则听起来朴素,但它决定了后面所有的代码结构,本篇的框架也是围绕它搭的。
至于“英雄属性展示”这一层,很多人把它理解成“把数字填到文本组件里”,于是写成了一堆label.text = player.attack。这样的代码在需求变动面前毫无抵抗力:加一个“暴击伤害”词条,你要改五处UI;装备换了个品质要变色,你要再改五处;想做悬停对比,还得从头复制一遍计算逻辑。真正稳妥的写法是把“面板行”抽象成数据描述,让UI只负责渲染描述列表。这部分我会在第三节里展开讲,包括怎么设计属性行的数据结构、怎么处理数值变化时的局部刷新,而不是整块重建。
还有一点必须提前说:装备穿戴的校验绝对不能在点击事件里随手写两句。能不能穿、穿到哪个格子、穿完之后旧装备去哪儿、背包满了怎么办、穿戴过程中被其他操作打断怎么办,这些都属于业务规则,必须收敛到一个统一的入口里去判断。我在第四节会把这套校验逻辑拆成一张检查清单,你可以直接对着写。
最后提一下这一篇的范围。它只做“属性展示”和“穿戴装备的骨架”,不涉及美术表现、不涉及动画、不涉及网络同步的细节,服务端这边的落地会在后续单独展开。之所以这么切,是因为我踩过的教训是:一次想把“展示、交互、同步、特效”全做完,结果四天之后代码里全是互相调用的死结,最后只能推倒重来。控制粒度,是做这类系统时最值钱的习惯。
2. 属性系统的底层数据结构:把“来源”这件事显式表达出来
2.1 基础属性、装备加成与最终值的三层拆分
先说结论:角色属性一定要分成三层来存,基础值(Base)、附加列表(Modifiers)、最终值(Final)。基础值是角色自身成长带来的,比如等级加成、职业基础、天赋点分配;附加列表是外部来源的集合,每一条都记录“来自哪里、加什么、加多少”;最终值不进存档,每次需要展示或者参与战斗计算时,由前两层实时推导出来。
这么拆的理由很实在。第一,装备卸下的时候,你只需要从附加列表里移除对应来源的条目,基础值完全不动,不会出现“卸了装备攻击力变小、再穿上却回不到原值”的数值漂移。第二,多件装备叠加的时候,附加列表天然支持“同类词条相加、异类词条相乘”这类规则,你想加就加、想乘就乘,改一个函数就行。第三,任何诡异的数值问题都能靠“打出完整附加列表”瞬间定位,这比在几百行代码里找+=高效得多。
下面是我在这类项目里常用的数据结构,语言用伪代码表示,你按自己项目的技术栈翻译即可:
AttributeSet { base: Map<AttrType, float> // 基础值,只有升级、天赋等少数系统能改 modifiers: List<AttrModifier> // 附加条目列表,装备、buff 都往这里塞 } AttrModifier { sourceId: string // 来源唯一标识,比如装备实例ID、buff ID sourceType: enum // EQUIP / BUFF / TALENT / TEMP attr: AttrType // 影响哪个属性:攻击、防御、暴击率…… op: enum // ADD(加法) / MUL(乘法) / OVERRIDE(覆盖) value: float // 数值 priority: int // 同属性多来源时的结算顺序 }判断一个 Modifier 该不该存在于列表里,只看sourceId是否有效。这样一来,“穿戴装备”变成了向列表插入若干条带装备ID的 Modifier,“卸下装备”变成了按ID批量移除。整个过程是幂等的:同一件装备反复穿,只要插入前先按ID清一遍,就不会出现加成翻倍。
2.2 最终值的推导顺序:为什么加法先算、乘法后算
最终值的计算顺序不是随意的,它直接决定了数值体验。行业里常见的做法是先把所有加法条目求和,再用乘法条目逐条相乘,最后处理覆盖型条目。用公式表示大致是这样:
final = (base + Σ addValue) * Π (1 + mulValue) 若有 OVERRIDE 条目,则在全部结算完成后直接用覆盖值替换为什么要这样排?举个具体例子。角色基础攻击100,一件装备加20点固定攻击,另一件装备加10%攻击。如果先算乘法再算加法,结果是 100×1.1+20 = 130;如果先算加法再算乘法,结果是 (100+20)×1.1 = 132。后者更符合玩家直觉:百分比加成应该作用在“角色的整体攻击”上,而不是只作用在裸值上。绝大多数商业项目采用加法优先的顺序,就是为了让“百分比词条”的收益随固定值增长而增长,避免高等级时百分比词条变成废纸。
顺带说一句priority字段的用途。当同一条属性上出现多个乘法条目时,顺序理论上不影响结果,但浮点数乘法的精度误差会累积,尤其在数值频繁变化、每帧重算的情况下。给乘法条目一个固定顺序,可以让同一套装备在任意时刻算出的结果完全一致,避免“面板显示132,战斗日志里是131.9999”这种让人抓狂的显示问题。展示层做四舍五入之前,一定要确认最终值本身是稳定的。
2.3 变更通知:让属性变化主动找上UI
数据算对了,还得让界面知道该更新了。这里最省事也最容易踩坑的方案是“每帧重算”,也就是界面每帧读一次最终值填进去。原型阶段这么写确实快,但一旦面板行数上去了、文本组件变多了,每帧重算会带来明显的性能浪费,而且浮点数在帧间波动时会让文本无意义地闪烁。
我推荐的方案是脏标记+事件通知。属性系统提供两个接口:一个用于注册监听(onAttributeChanged(callback)),一个用于在 Modifier 列表或基础值发生实际变化后触发通知。关键在于“实际变化”:插入一条 Modifier 之后,重算该属性涉及的最终值,和旧值比较,只有产生差异才触发回调,这样能过滤掉大量无效刷新。回调里不做任何计算,只把“哪些属性变了”这个集合交给UI层,由UI层决定刷新哪几行。
function applyModifier(attrSet, modifier): attrSet.modifiers.add(modifier) affected = modifier.attr oldValue = cachedFinal[affected] newValue = recompute(attrSet, affected) cachedFinal[affected] = newValue if abs(newValue - oldValue) > EPSILON: emit("attribute_changed", affected, oldValue, newValue)这套机制在装备批量穿戴时尤其有用:一次穿三件装备,可能影响攻击、防御、暴击三个属性,通知只发三条,界面只刷三行,其余行完全不动。等你后面做 buff 系统、做套装效果,这套通知机制可以直接复用,不需要再改UI。
3. 属性面板的渲染设计:把“行”当成数据,而不是一堆文本组件
3.1 属性行描述结构:UI只认描述,不认逻辑
面板渲染这件事,我强烈建议做一层中间描述。具体说,属性系统只负责吐出一个列表,每一项描述“这一行显示什么”:
AttributeRow { key: string // 用于定位刷新,如 "atk" "def" "crit" label: string // 显示名,如"攻击力" value: number // 最终值 baseValue: number // 基础值,用于显示 "+XX" 的附加部分 format: enum // INT / PERCENT / FIXED_1 highlight: bool // 是否有临时加成,用于特殊颜色 order: int // 显示顺序,配置表决定 }UI 层拿到这个列表,做的事情只有三件:按order排序、按format格式化数字、按key建立索引以便局部刷新。整个过程里,UI 不知道攻击力是装备给的还是天赋给的,也不知道公式怎么算,这种“无知”恰恰是它最大的优点——需求变动时你只需要改数据侧,界面一行代码都不用碰。
这里有个实操细节值得单独说:附加部分的显示方式。玩家特别在意“我穿了装备之后涨了多少”,所以面板上最好显示成“攻击力 132(+32)”,其中括号里的部分来自附加列表的汇总。这个汇总值应该在属性行描述里直接给出来,而不是让UI自己拿最终值减基础值再格式化,否则百分比加成的情况下减法算出来的数跟玩家的心理预期完全对不上。
3.2 局部刷新与行复用:避免整块重建带来的卡顿和状态丢失
装备界面的刷新频率其实不低:穿一件、卸一件、切换对比、开背包,都可能触发面板更新。如果每次更新都把整个面板的行全部销毁重建,会带来两个问题。一是性能开销,行数一多、节点一多,瞬时卡顿很明显;二是状态丢失,比如某一行正处于高亮状态、某一行有滚动位置,重建之后全没了。
正确的做法是按 key 建索引 + 差异刷新。首次创建界面时,把所有行实例化并记录在Map<key, RowView>里。之后每次收到变更通知,只对变更集合里的 key 取出行、更新其数值和格式,未变化的行完全不碰。只有当“行集合本身发生变化”(比如某个属性被解锁、某个属性被隐藏)时,才做增删。
function refreshPanel(changedKeys): for key in changedKeys: if not rowMap.contains(key): createRow(key) // 行集合变化,才创建 continue row = rowMap[key] row.setValue(format(rowData[key])) row.setHighlight(rowData[key].highlight)还有一个容易被忽略的点:数字滚动或渐变效果要在刷新接口内部做节流。有些项目喜欢在数值变化时做个数字滚动动画,结果装备批量穿戴时同一行在一帧内被要求滚三次,动画直接错乱。稳妥做法是每次刷新只记录“目标值”,由行自己的更新循环平滑逼近,连续刷新时只更新目标,不重启动画。
3.3 悬停对比:复用同一套推导,不要另起炉灶
装备对比是属性面板之外玩家最常用的信息,也是重复代码的重灾区。常见错误写法是:鼠标悬停时另外写一段逻辑,手动算“穿上这件装备后攻击变多少”。这段逻辑一旦和主计算链路不一致,玩家就会看到两套数字。
我的做法是把“模拟穿戴”做成属性系统的一个纯函数能力:传入一个假的 Modifier 集合(即目标装备的加成条目),在属性集的一个副本上应用,返回重算后的最终值,整个过程不触碰真实状态,也不触发任何事件。悬停对比的UI只需要拿“模拟值”和“当前值”做差,展示成绿色上升或红色下降。这样做的好处是,对比数值和实际穿戴后的数值永远一致,因为走的是同一个计算函数,只是数据源不同。
注意:模拟计算一定要在副本上进行。我见过直接在真实属性集上加减、算完再还原的写法,一旦中间出现异常或被其他逻辑插入,角色属性就永久损坏,且极难排查。
3.4 面板行的排序与分组:别让玩家在一堆数字里找重点
属性一多,面板必然拥挤。这里有一个经验性的排序原则:核心战斗属性置顶,衍生属性居中,展示性属性垫底。攻击、防御、生命这类直接参与伤害计算的放最上面;暴击率、暴击伤害、命中、闪避这类次级判定属性放中间;移动速度、拾取范围这类功能性属性放下面。分组之间的间距要明显大于组内间距,让玩家的眼睛能快速分块。
排序本身不要写死在UI里,而应该由配置表驱动。每行描述带一个order值,UI按它排序。这样以后新增属性或者调整优先级,改配置就行,不需要重新编译界面逻辑。这一点在小团队里特别重要,因为改UI代码往往意味着要重新走一遍自测流程,而改配置的风险低得多。
4. 装备穿戴的完整校验链路:把规则收进一个入口
4.1 穿戴请求的六道检查:顺序不能乱
装备穿戴看起来就是一个点击操作,实际要过的检查不少。我把它整理成固定顺序的六道,顺序本身就是设计的一部分:
| 顺序 | 检查项 | 不通过时的处理 | 为什么排这个位置 |
|---|---|---|---|
| 1 | 请求是否重复(防连点) | 直接丢弃 | 最便宜,先挡掉无效请求 |
| 2 | 装备实例是否存在、是否在背包中 | 提示“物品不存在” | 数据合法性,后续检查的前提 |
| 3 | 装备是否满足穿戴条件(等级、职业等) | 提示具体不满足项 | 面向玩家的明确反馈 |
| 4 | 目标格子是否与装备部位匹配 | 自动纠正或提示 | 决定后面替换逻辑的目标 |
| 5 | 目标格子是否已有装备 | 走替换流程 | 影响后续步骤的分支 |
| 6 | 背包是否有空位容纳被替换的装备 | 提示“背包已满” | 最贵的检查放最后 |
把“背包空位”放到最后是有讲究的。这个检查要遍历背包找空格,成本相对高;更重要的是,如果先检查空位再检查等级,玩家会遇到“提示背包满,其实等级也不够”的双重误导。按成本从低到高、按信息明确度从高到低排列,玩家每次只会收到一个准确的原因。
4.2 替换与卸下:两个方向,同一套数据操作
穿戴的本质是“把装备从背包移到装备栏”,替换是“把新装备放进去、旧装备退回背包”,卸下是“把装备从装备栏移回背包”。三者在数据层其实是同一组操作的组合:移除旧的 Modifier 集合、插入新的 Modifier 集合、交换两个容器的条目引用。
关键在于操作的原子性。个人项目里常见的事故是:先把新装备从背包删了,插入 Modifier 时因为某个校验失败抛了异常,结果装备凭空消失。稳妥做法是先做所有可能失败的检查,全部通过后再执行数据变更,变更过程本身不再有失败分支。如果项目里有事务或者回滚机制,包一层更好;没有的话,就靠“检查前置”这个约束来保证安全。
function equip(player, equipItem, slot): // 阶段一:全部检查,任一失败直接返回,不改任何状态 if not validate(player, equipItem, slot): return fail(reason) // 阶段二:执行,不再有失败分支 oldItem = slot.item removeModifiers(player.attrs, equipItem.instanceId) addModifiers(player.attrs, equipItem.modifiers) slot.item = equipItem bag.remove(equipItem) if oldItem != null: bag.add(oldItem) removeModifiers(player.attrs, oldItem.instanceId) addModifiers(player.attrs, ...) // 若旧装备本身有被动效果需处理注意上面这段里,旧装备的 Modifier 移除必须在新装备插入之前或之后成对完成,不能只做一半。另外,如果某些装备带有“被替换时触发”的被动效果,那部分逻辑要单独挂在事件上,不要塞进这个函数,否则函数会越长越难读。
4.3 多槽位与部位映射:别让字符串硬编码满天飞
装备部位用字符串硬编码(slot === "weapon")在项目早期很省事,但随着部位增多、出现双武器、出现饰品双槽,这种写法会迅速失控。我的建议是建一张部位配置表,把“部位标识、允许的装备类型、默认格子、是否可空”全部数据化。
SlotConfig { slotId: "weapon_main" // 主手 acceptTypes: ["sword", "axe", "bow"] defaultEmpty: true pairedSlot: "weapon_off" // 若有副手槽,互相影响 }有了这张表,“这个装备能不能穿到那个格子”就变成了查表判断,“这个格子能不能为空”也变成查表判断。以后要加“戒指双槽”“披风槽”,只需要在配置里加条目,校验逻辑和界面生成逻辑都跟着数据走,几乎不用改代码。这类“提前花半小时做数据化”的投入,回报率在项目中期会高得吓人。
5. 实操中的真实坑位:从数值飘移到界面错乱的排查链路
5.1 坑一:卸下装备后属性回不到原值
现象:穿上一件加20攻击的武器,面板显示120;卸下后显示100,看起来正常;但穿上再卸下,显示变成98。反复几次,数值越来越低。
排查链路:先打日志,把每次操作前后的 Modifier 列表完整打印出来。如果列表里出现了同一件装备的两条记录,说明插入时没有按sourceId去重;如果列表条数正确但最终值不对,说明重算没有真正发生,或者缓存值没更新。我遇到过的真实原因属于第三种:移除时用的是“按属性值匹配”的方式删条目(找到第一个 value=20 的踢掉),结果误删了另一件同数值装备的加成,真正该删的那条还留着。
修复:所有 Modifier 的增删必须按sourceId精确操作,绝不允许按数值、按属性类型模糊匹配。这条规则要写进团队约定里,因为它的错误表现是缓慢漂移,极难在测试阶段被发现,等玩家反馈时已经积累了大量脏数据。
5.2 坑二:面板显示的和实际战斗用的不是同一个数
现象:面板写着攻击132,实际打怪伤害按120算。
排查链路:这类问题九成是“两套计算链路”造成的。先确认战斗计算有没有直接读基础值,或者读了一个更早缓存的旧快照。第二种可能是计算时机问题:战斗开始时把属性快照存进了战斗对象,之后换装备,快照没更新。第三种是格式问题,面板做了四舍五入,实际用的是浮点原值,数值差在临界点上看起来对不上。
修复:统一“最终值”的唯一出口,所有需要用到属性的地方(面板、战斗、技能伤害预览)全部走同一个获取函数,禁止任何地方自己拼公式。快照类设计必须明确生命周期,并在属性变更事件中标记快照失效。展示层格式化只发生在最后一步,计算全程用原值。
5.3 坑三:批量穿戴时界面闪烁、数字跳两次
现象:一次性穿上三件装备(比如一键换装),面板数字先跳到中间值再跳到最终值,肉眼可见地闪了一下。
排查链路:检查是不是每穿一件就触发了一次全量刷新。如果是,说明刷新接口没有做合并。另一种可能是事件是同步派发的,界面在第一次事件里就执行了重绘,第二次事件又重绘一次,中间状态被渲染出来了。
修复:引入“批量模式”。进入批量模式时挂起所有属性变更通知,把变更的 key 收集到一个集合里;退出批量模式时一次性派发。界面只收到一次通知,只刷一次。这套机制在后面做“套装穿戴”“buff批量结算”时同样适用,值得早点做进去。
提示:批量模式的挂起/恢复要成对出现,最好用 try-finally 保证异常时也能恢复,否则一次异常就会让后续所有属性变更彻底静默,表现为“穿装备面板没反应”,排查起来非常费劲。
5.4 坑四:换装过程中被其他操作打断导致装备丢失
现象:玩家点穿戴的瞬间恰好触发了自动拾取或者背包整理,结果新装备既不在背包里也没穿在身上。
排查链路:这是典型的竞态问题。个人项目里往往没有锁机制,多个操作在同一帧内交错执行,中间状态被覆盖。排查时先梳理所有会改动背包或装备栏的入口,看它们之间有没有互斥保护。
修复:至少做到两点。一是穿戴流程内部不要有让出执行权的异步等待,把“检查+执行”放在同一个同步块里完成。二是给背包和装备栏加一个简单的操作标记,正在执行换装时,其他改动操作直接排队或拒绝。原型阶段不必上复杂的锁,一个布尔标记加上明确的拒绝提示就够用了。
5.5 坑五:属性面板的数值格式五花八门
现象:攻击力显示 132.0,暴击率显示 0.0500000007,防御显示 132.00001。
排查链路:这类问题几乎全部来自浮点精度和格式化缺失。百分比属性的存储用小数(0.05),乘法累加之后尾数必然带误差;整数属性用了浮点存储,除法之后也会带尾巴。
修复:在属性行描述里带一个明确的format枚举,由渲染层统一格式化。整数属性用取整,百分比属性用“保留一位小数的百分比”,所有数值在展示前做一次精度收敛(比如乘以1000取整再除回去),消除浮点尾数。千万别在计算侧做取整,那会破坏后续的乘法链。
6. 把面板和装备串起来的工程细节:顺序、缓存与可测性
6.1 界面打开时机与数据初始化顺序
很多界面问题不在于逻辑,而在于时序。属性面板打开时,如果先创建UI行、再去请求数据,中间会有一帧空白或者显示默认值,玩家看到的就是“数字闪了一下才出来”。我的做法是先取数据、再建行、最后统一显示:打开面板的流程是拉取属性行描述列表、按 key 创建或更新行、数据全部就位后一次性把面板设为可见。这样界面出现的第一帧就是完整正确的。
装备界面还有一层顺序:装备栏的槽位视图依赖槽位配置,槽位配置又依赖角色的职业。所以初始化必须按“角色数据 → 部位配置 → 槽位视图 → 装备实例绑定”这个顺序来,任何一步提前都会引发空引用或者显示错位。这套顺序我建议直接写进注释里,因为过两个月你自己都会忘。
6.2 缓存与失效:什么时候该重算,什么时候该直接读
属性最终值要不要缓存?我的实践是缓存,但明确失效条件。缓存的好处是面板每帧刷新、战斗每帧取属性时不做重复计算。失效条件只有三个:基础值变化、Modifier 列表变化、影响计算的配置变化(比如公式调整)。
具体实现上,属性集内部维护一个dirty集合,记录哪些属性需要重算。被查询时,如果该属性在 dirty 集合里,先重算、写缓存、移出集合,再返回。这种“惰性重算”比“变更即全量重算”更省,因为批量变更时中间态不会被重复计算。
function getFinal(attrSet, attr): if attrSets.dirty.contains(attr): attrSet.cached[attr] = recompute(attrSet, attr) attrSets.dirty.remove(attr) return attrSet.cached[attr]开发期可以额外加一个自检开关:每隔一段时间把所有属性全量重算一遍,和缓存值对比,不一致就打日志。这个开关在排查“显示不对”类问题时极其有效,因为它能第一时间告诉你“是缓存脏了”还是“公式错了”。
6.3 可测性:给属性计算留出纯函数入口
属性计算这块逻辑,是最值得写单元测试的地方,也是最容易被个人项目忽略的地方。原因很简单:它是纯数据变换,输入明确、输出明确,测试成本极低,而它一旦出错影响面极广。
我的习惯是单独抽出一个纯函数computeFinal(base, modifiers, config),不依赖任何全局状态、不读角色对象、不触发事件。这样测试可以这样写:给一个基础值100、两个加法和一个乘法的 Modifier,断言输出等于132。再补几个边界用例:空 Modifier 列表、同一来源重复添加、百分比超过100%、整数溢出。这套测试写下来不超过一页代码,但能挡住后面80%的数值事故。
装备校验同理。validateEquip(player, item, slot)也应该尽量做成纯函数,把需要的数据作为参数传入,而不是直接读全局角色。这样你可以用构造出来的假数据测试“等级不够”“部位不符”“背包满”等各种分支,而不必真的去造一个完整角色。
6.4 配置驱动的度:哪些该进表,哪些该留在代码里
最后聊聊配置化。属性行描述、部位配置、装备类型映射,这三类东西强烈建议进配置表,因为它们会频繁调整且不影响逻辑。而计算顺序、事件派发时机这类“逻辑本身”,留在代码里更合适,强行配置化只会让调试变得困难。
我的判断标准是:这个值变化时,是否需要同步改变其他逻辑?如果不需要,进表;如果需要,说明它和逻辑强耦合,留在代码里并写成常量。比如“加法先于乘法”这条规则,如果允许配置,就意味着公式行为会随配置变化,测试和排查成本成倍上升,不值得。
7. 写给准备动手的你:几条省时间的实践建议
第一条,先做“数据正确”,再做“界面好看”。我见过不少项目急着把面板做得花里胡哨,结果数值计算三天两头出问题,返工时连UI一起推倒。正确的顺序是:先把三层属性结构、计算函数、事件通知跑通,用最丑的纯文本界面验证数值,确认穿脱装备、多件叠加、模拟对比全都正确,再去做视觉表现。数值对了,界面怎么改都不会白费。
第二条,给属性变更日志留一个开关。开发期打开它,每次变更打印“来源、属性、旧值、新值”,后期出任何数值问题,第一反应都是打开日志复现一遍。这条日志在我参与的项目里救过太多次,它的成本几乎为零,价值却极高。
第三条,装备穿戴的失败一定要给出精确原因。不要统一弹“操作失败”,而要明确到“等级不足,需要15级”或者“背包已满,请先清理”。玩家和测试都无法从模糊提示里判断问题,最终压力全部回到开发者身上。
注意:错误提示本身也要走统一的提示通道,不要在业务逻辑里直接调用界面弹窗。否则你以后想做“提示合并”“提示队列”时,会发现几十处调用点散落各处。
第四条,面板行数超过十五行时,考虑分组折叠或者分页。这不是性能问题,是阅读体验问题。玩家打开面板是为了快速确认关键数值,不是为了做数学题。
第五条,也是最实在的一条:这一块代码请尽量写成“数据进、视图出”,别让视图反过来修改数据。我自己的习惯是界面上任何操作都只发“意图”(我要穿这件),由业务层校验并执行,执行结果回流到视图。这条边界守住了,后面接网络同步、接回放、接自动化测试都不会太痛;守不住,等你想加服务端校验的时候,会发现客户端界面里藏着一堆直接改数据的逻辑,全部要重写。这套结构做完之后,后面加套装效果、加附魔、加装备强化,都是在附加列表里加条目的事,改动面小得多。