容易混淆的问题
这一讲把坐标、时间和数据结构各拆成两套,混淆几乎都发生在两套之间的边界上。
下标与坐标
Lua 的下标从 1 起,屏幕像素从 0 起,于是 gridX = 1 对应 x = 0。所有换算集中在 (gridX - 1) * 32,只写这一处。
-- 网格:1..8
local gridX = 3
-- 棋盘像素:0..224
local px = (gridX - 1) * 32 -- 64
-- 屏幕像素:棋盘偏移 240 之后
local screenX = px + (VIRTUAL_WIDTH - 272) -- 304| 说法 | 取值 | 用途 |
|---|---|---|
| 网格坐标 | 1–8 | 数组下标、匹配、相邻判断 |
| 棋盘像素 | 0–224 | 补间目标、Tile:render |
| 屏幕像素 | 240–496 | 鼠标命中、调试 |
| 虚拟分辨率 | 512×288 | 交给 push 放大 |
gridX 是逻辑,改它不会动画面;x 是画面,改它不会动逻辑。交换时先改 gridX 再补间 x,两者短暂不一致正是动画存在的空间。
棋盘偏移也是常错的一处:Board:init 收的 240 和 16 只加在渲染与命中判断里,Tile 自己不存这两个数。漏加时整块棋盘贴着左上角,看起来像「图集切错了」。
尺寸与缩放
newQuad 的宽高是裁剪窗口,love.graphics.draw 的 sx/sy 才是缩放。把 32 写进缩放参数,方块会变成 32 倍大小。
| 数字 | 出处 | 含义 |
|---|---|---|
| 32 | newQuad 的宽高 | 从图集里裁出的一格 |
| 32 | (gridY - 1) * 32 | 两格之间的像素间距 |
| 512×288 | VIRTUAL_WIDTH | 逻辑分辨率,push 放大 |
| 1280×720 | 窗口尺寸 | 实际像素,不参与布局 |
像素画必须先用 setDefaultFilter('nearest', 'nearest') 再加载图片。顺序反了,32×32 的图会被线性插值糊掉,看起来像「缩放没对齐」,其实是采样方式错了。
另一个混淆是「一格」和「一次绘制」:棋盘 8 格宽只有 256 像素,屏幕上看着大是因为 push 把 512×288 拉到了 1280×720,乘的是 2.5 倍,不是 32 倍。
时间与顺序
Timer.every 是周期,Timer.after 是一次性,Timer.tween 是插值。三者都进同一个默认组,由同一次 Timer.update(dt) 推进。
Timer.every(1, tick) -- 每秒一次,直到移除
Timer.after(1, function() end) -- 一秒后一次
Timer.tween(0.25, def):finish(nextFn) -- 补间结束才跑 nextFn| 混淆点 | 区别 |
|---|---|
:finish 与 Timer.after | 前者跟着本段结束,后者从创建时起算 |
Timer.update 调两次 | 所有动画与倒计时双倍速 |
| 暂停时清空计时器 | 应停止调用 Timer.update,而不是 clear |
Timer.clear() 的位置 | 必须在 change 之前,否则新状态计时器被清 |
自己累加 dt 时记得取模 % interval,直接清零会让每帧的余数丢掉,倒计时越走越慢。
补间和计时器共用同一个组,Timer.clear() 会连正在跑的补间一起清掉。只想停倒计时,用 :remove() 单独摘,别动整组。
- 判定在
update开头、推进在update末尾,归零到切状态差一帧 - 条件写
timer <= 0,== 0会被跨帧的小数跳过
返回值与状态
calculateMatches 返回 false 而不是空表,调用方才能写 if matches then。空表在 Lua 里是真值,if {} then 永远成立——这是最容易踩的一句话。
| 名字 | 看着像 | 实际是 |
|---|---|---|
false | 空结果 | 「没有匹配」 |
{} | 空结果 | 真值,条件永远成立 |
self.matches | 临时变量 | removeMatches 的输入 |
canInput | 输入锁 | 从没被置 false,等于没锁 |
pauseInput | 与上面重复 | 真的在挡输入 |
removeMatches 依赖 self.matches 这个字段,所以算匹配与删匹配必须按顺序调用。先删再算,删的是上一轮的结果;matches 又是「组的数组」,十字交叉进两组、中心格计两次分,以为是重复计数而去重,反而少给玩家分数。
empty状态的方法全是空的,它只兜住第一帧,不代表「没有状态」- 状态之间不共享变量,分数与关卡号靠
params传,params.score or 0兜住nil - 目标是
score >= scoreGoal,写成>会让踩线的分数卡住一关
选中与相邻
同一格再按一次 Enter 是取消选中,不是交换。距离为 0 走取消分支,为 1 才尝试交换,其余情况播 error 并清空。
| 曼哈顿距离 | 含义 | 行为 |
|---|---|---|
| 0 | 同一格 | 取消选中 |
| 1 | 上下左右相邻 | 补间后尝试交换 |
| 2 | 斜角或隔一格 | 播 error 清空 |
判定顺序写反,最典型的症状是「按同一格反而报错」:先查距离、后查相等,距离 0 永远过不了相邻判断。
消除后的棋盘
removeMatches 之后棋盘是半空的,tiles[y][x] 里混着 nil。这时任何遍历都可能出事:# 不可靠、ipairs 提前停、calculateMatches 读到 nil.color 直接崩。
getFallingTiles 在返回补间表之前就把数据填满了,画面才慢慢追。所以「掉落中」只是视觉状态,数据始终是完整的 8×8,递归判分才敢直接重入。分数则必须在置空之前结算,顺序写错就会一直加 0 分。