Match 3
精灵表与 Quad
- 精灵表与 texture atlas 是同一回事
love.graphics.newImage把整张图读进显存love.graphics.newQuad(x, y, w, h, 尺寸)- 最后两个参数是整图宽高,不是这块的尺寸
- 本作所有 tile 统一 32×32
GenerateQuads用行列双层循环切图texture:getWidth()除以 tile 宽得列数#quads取数量,math.random(#quads)随机取- 每帧重掷随机 ID 会闪成噪声
- 随机 ID 必须缓存进 table
- 本作按颜色分表
gFrames['tiles'][color][variety] - 18 种颜色 × 6 种花样 = 108 个 quad
- 图集共 9 行 × 12 列
- 先画偏移 2 像素的深色阴影再画本体
setDefaultFilter('nearest', 'nearest')保住像素边
棋盘与 Tile
Board:init(x, y)记棋盘左上角像素位置self.tiles[y][x]是二维数组,行优先tiles[1]是最上面一行- Lua 下标从 1 起,像素坐标从 0 起
Tile:init(x, y, color, variety)gridX/gridY是 1–8 的离散逻辑坐标x/y是连续的像素坐标- 像素坐标由
(gridX - 1) * 32推出 - 逻辑判断只读
gridX/gridY - 动画只改
x/y - 棋盘 8×8,放在
VIRTUAL_WIDTH - 272 - 开局
while self:calculateMatches() do重建 - 重建保证开局没有现成三连
Board:render遍历 64 格调Tile:render- 空格用
nil表示
计时器与补间
- 手写计时器累加
dt,超阈值后取模 - 多个间隔就要多套变量与判断
Timer = require 'lib.knife.timer'Timer.every(interval, callback)周期执行Timer.after(delay, callback)只执行一次Timer.tween(duration, definition)平滑插值Timer.update(dt)每帧必须调用- 回调是匿名函数,被当作数据处理
- 一帧跨过多个间隔时用
while补发 - 补间通式
base + (dest - base) * t / d - 定义表
[target] = { x = endX } :finish(fn)在本段结束后再跑下一步:ease(fn)换缓动函数:limit(n)限制执行次数Timer.clear()清空默认组- 标题配色计时器离开前
:remove() :remove()拆掉单个计时器
交换与匹配
- 光标
boardHighlightX/Y从 0 开始 - 选中框用
line矩形画 math.max(0, ...)与math.min(7, ...)夹边界- 第一次 Enter 选中
highlightedTile - 再按同一格取消选中
- 曼哈顿距离大于 1 判为不相邻
- 先换
gridX/gridY再写回tiles - 像素位置交给
Timer.tween(0.1, ...) - 补间结束才调
calculateMatches - 只判横竖,不判对角线
- 逐行追踪
colorToMatch与matchNum - 换色时回溯收集整组
if x >= 7 then break剪掉尾两格- 行尾单独再查一次
- 再按列扫一遍就是竖向匹配
- 返回
false而不是空表 - 十字形会同时进两组
消除与连锁掉落
removeMatches把命中格置nilnil同时是空格标记getFallingTiles逐列从底往上扫space标记是否已遇到空位spaceY记最下面那个空位- 上方 tile 下移并改写
gridY - 下移后
y = spaceY回头重扫 - 顶部剩下的空洞生成新 tile
- 新 tile 起点
y = -32在屏幕外 - 函数返回
tweens表而不是自己动 - 旧块下落与新块入场同一张补间表
Timer.tween(0.25, tilesToFall):finish里递归calculateMatches- 递归就是连锁反应
canInput在动画期间为false- 无匹配才恢复输入
计分与关卡
- 一组得分
#match * 50 - 3 连 150,4 连 200,5 连 250
- 十字形两组各算一次
self.score在enter里从params继承scoreGoal = level * 1.25 * 1000- 第 1 关 1250,第 2 关 2500
- 达标切
begin-game并level + 1 timer = 60在init里给Timer.every(1, ...)每秒减一- 剩 5 秒起播
clock提示音 - 归零切
game-over并带走score - 切状态前
Timer.clear() params.score or 0兜默认值- 计分只发生在
calculateMatches里
状态机与过渡
- 状态钩子
enter/update/render/exit states表存返回实例的工厂函数change依次exit、新建、enterassert拦住拼错的状态名empty状态兜住第一次切换- 四条主线
start、begin-game、play、game-over - 全屏白矩形铺在最上层
transitionAlpha从 0 补到 1:finish里才真正换状态- 新状态从 alpha 1 淡回 0
BeginGameState的关卡字三段位移Timer.after(1, ...)做中间停顿board经params传给play复用levelLabelY从 -64 出发Timer.clear()必须在change之前
像素调色板
- Dawnbringer 32 是 32 色像素调色板
- 抖动 dithering 交替像素造中间色
- 32 色也能造出连续渐变错觉
- 限定调色板保证色彩协调
- 可选颜色少就少犯错
- NES 主调色板 54 色,每块用 4 色一组
- SNES 用 15 位色,共 32768 色
- 调色板交换能换配色不换贴图
- 同一精灵多个配色省贴图内存
- 本作 18 色 × 6 花样就是变体堆叠
setColor收 0–1 浮点- 代码里写
217/255便于对照十六进制 - 4 色 Game Boy 限制适合当练习
容易混淆的问题
- 网格下标 1 起,像素坐标 0 起
gridX/gridY管逻辑,x/y管动画- 棋盘像素要再加 240 和 16 才是屏幕位置
- Quad 尺寸是裁剪窗口,不是绘制缩放
- 32×32 是图集格子,
push再放大到窗口 - 数组先换位,像素后补间
Timer.every周期触发,Timer.after只触发一次:finish跟着本段结束,Timer.after从创建时起算Timer.clear()放在change之前false是「没匹配」,空表是「匹配了但长度为零」