跳到正文

容易混淆的问题 ​

这些概念单独看都清楚,凑在一起就容易写反。

容易混淆区别
状态机 / 状态栈同时一个状态 vs 可以叠多层
update / render 范围只更新栈顶,渲染全部
HP / currentHP上限 vs 当前值,血条两端分别取
mapX / x格子编号 vs 像素坐标,后者由前者推出
isDown / wasPressed按住持续为真 vs 只触发一帧
value / max会变 vs 不变
push / pop压入并 enter vs exit 后移除
遇敌判定时机每走一格一次 vs 每帧一次
存档 / 存对象存数据,读档重建实例
Panel 内缩位置 +2,宽高 -4

更新与渲染 ​

状态栈里 update 只发给栈顶,render 从底到顶全画。写反的两种后果:全更新则暂停菜单下面的角色继续走,全渲染只画栈顶则菜单背后是空白,看起来像换了场景。想把「只画栈顶」用在某个特殊状态上,别改 StateStack,让那个状态自己画一张不透明背景即可。

弹栈也不会自动恢复下层的状态。战斗状态压入时把场地音乐 pause 了,弹出时就得有人 resume。这个「有人」要么是战斗状态的 exit,要么是场地状态的某个回调,二者选一个,别两边都写,否则音乐会被恢复两次。

  • 每次压栈新建实例;复用旧实例会带着光标位置、淡入进度、算好的对手
  • pop 之后不要再持有那个状态的引用去改它,栈里已经没有它
  • 空栈时 update 与 pop 都要能安全返回,否则 states[0] 是 nil

两套坐标 ​

mapX 在 tween 开始时就变成目标格,x 要 0.5 秒后才到。所以:

  • 判断地形、遇敌、NPC 占位,用 mapX、mapY
  • 判断画面上的位置、和鼠标或摄像机换算,用 x、y

用 x 去查瓦片,会在移动途中查到两格之间的错误编号。

lua
-- 正确:格子查询
local tile = level.grassLayer.tiles[entity.mapY][entity.mapX]

-- 危险:像素当格号
local tile = level.grassLayer.tiles[entity.y][entity.x]

第二行不会报错,只会返回 nil,然后在下一行「索引 nil 的 id」时崩掉。崩点在判断地形的地方,原因在坐标混用,隔了好几层。

概率的写法 ​

math.random(10) == 1 是十分之一。写成 math.random(10) < 1 则永远为假,因为返回的是 1 到 10 的整数。想改百分比,用浮点更不容易错:

lua
if love.math.random() < 0.1 then
    -- 10%
end

还有一个隐藏假设:这段代码只在移动完成时调用一次。放进 update 里就变成每帧 10%,一秒能触发好几次。

随机源也值得分清:

  • love.math.random 启动时已设种子,直接可用
  • math.random 在 Lua 5.1 下序列固定,要 math.randomseed(os.time())
  • 两套随机混用,调参结果会对不上

概率判断也别和地形判断拆开写。if isGrass and random() < 0.1 与 if random() < 0.1 and isGrass 结果一样,但后者会先掷骰,调试时打印日志会误导你以为「每一步都判定过」。

面板的边界 ​

Panel 画内框时位置加 2、宽高减 4。只减宽高不减位置,边框会跑到右下角;只加位置不减宽高,内框会溢出外框。

  • Textbox 的 maxLines 用 (height - 2 * padding) 除以 (lineHeight + linePadding) 再取整
  • 少减一个 padding,最后一行会被切掉半截;不取整,行数会算多一行
  • getWrap 的宽度传 width - 2 * padding,传整个框宽会让文字压到边框上

战斗数值 ​

血条上端取 max = HP,数值取 value = currentHP。两个都是数字,混用时不会报错,只会让进度条显示错误的长度。

  • 伤害下限是 1,靠 math.max(1, attack - defense);去掉下限,防御高的双方永远打不完
  • 速度相同先手属于对手,是 else 分支的结果,不是显式规则
  • 想改成玩家优先,在比较里用 >=,而不是在别处补判断
  • 经验阈值按等级平方增长,觉得升级太快就调 expToLevel 的系数

存档 ​

table 里如果有函数或类实例,写不进文件。存档只存数字、字符串、布尔和嵌套表,读回来用定义表重建对象。忘了重建,读档后第一次调用 pokemon:calculateStats() 就会报「尝试索引 nil」。

调试坏档时先看文件本身:love.filesystem.read('save.lua') 打印出来,比在 pcall 里猜错误快。另外,love.filesystem 的读写是同步 IO,别把存档放进 love.update,一帧写一次会直接掉帧。