Breakout
项目结构
src/放 Lua 代码src/states/一个状态一个文件graphics/放纹理图集sounds/放 wav 音效fonts/放 ttf 字体lib/放 push 与 classmain.lua只留 LÖVE 回调Dependencies.lua集中所有requireconstants.lua存全局常量src/Util.lua放 quad 与table.slicetable.slice返回新区间,不改原表WINDOW_WIDTH与VIRTUAL_WIDTH分开PADDLE_SPEED的值是 200- 全局表
gTextures、gFrames、gFonts gSounds存所有音效gStateMachine在love.load里建renderHealth、renderScore放main.lua
状态机
- 八个状态覆盖全部界面
start进paddle-select或high-scoresserve是发球前的缓冲play是唯一持续推进的状态victory过关后回servegame-over可进enter-high-scoreStateMachine:change(name, params)- 状态表里存工厂函数
- 挡板选择到边界时箭头变灰
change先exit再enter- 每次切换都新建状态实例
enterParams用来跨状态传数据empty空状态兜住首帧BaseState提供五个空回调Class{__includes = BaseState}- 暂停用
paused字段,不进状态机
精灵表与 Quad
- 图集把几十张图打成一张纹理
newQuad描述图集内的矩形newQuad最后一个参数是总尺寸draw(texture, quad, x, y)只画一块- quad 不复制像素,只记录 UV
GenerateQuads按格子均匀切- 计数器从 1 开始,行优先
- 砖块 32×16,球 8×8
- 砖块只取前 21 个 quad
- 砖块索引
1 + (color-1)*4 + tier - 挡板宽 32、64、96、128 四档
- 挡板索引
size + 4*(skin-1) - 心 10×9,箭头 24×24
setDefaultFilter('nearest','nearest')防糊- 图集边长必须是格子整数倍
挡板与球
Paddle.dx只有水平分量- 不按键时
dx归零,没有惯性 - 左夹紧用
math.max(0, x) - 右夹紧要减
self.width size决定绘制宽度档位skin决定挡板颜色Ball宽高都是 8Ball:collides是 AABB- 左右上三面反弹并贴边
- 底边不判定,直接出界
serve状态球贴挡板中线- 按 Enter 才切到
play - 初速
dx取-200到200 - 初速
dy取-50到-60 wasPressed是课程自定义封装
关卡生成
LevelMaker.createMap(level)静态调用- 行数随机 1 到 5
- 列数随机 7 到 13 后取奇数
highestTier每 5 关涨一级highestColor封顶 5- 砖块 32 宽、16 高
- X 坐标先减 1 再乘 32
+8是左右边距- 少列时补
(13-numCols)*16居中 - Y 坐标直接用
y * 16 - 每行独立决定是否隔块
- 每行独立决定是否交替颜色
goto continue跳过本次生成- 标签写成
::continue:: - 全空时递归重掷
Brick有tier、color、inPlay
碰撞响应
- AABB 任一轴有间隙即不碰
- 碰撞函数只回答是否相交
- 挡板命中先抬到
paddle.y - 8 dy取反保证向上- 中线偏移决定
dx大小 - 偏移乘 8,基线 50
- 左移撞左半才向左加速
- 右移撞右半才向右加速
- 其余情况只反转
dy - 砖块用中心偏移算接触面
px、py是两轴穿透深度- 穿透浅的轴才是反弹轴
- 沿穿透深度推出球
- 命中后
break只处理一块 dy乘 1.02,封顶 150
生命、得分与粒子
- 满血 3 颗心
- 先画满心再补空心
- 心间距 11 像素
- 球过底边扣 1 血
- 归零进
game-over - 否则带数据回
serve - 得分
tier * 200 + color * 25 renderScore固定右上角- 分数过阈值回 1 血
- 阈值每次翻倍,封顶 100000
- 每块砖自带粒子系统
- 粒子寿命 0.5 到 1 秒
- Y 加速度只取正值
- 颜色从砖色渐变到透明
emit(64)一次喷 64 个- 粒子要在砖块之后渲染
关卡推进与存档
checkVictory遍历inPlay- 还有一块在场就没赢
- 胜利后按 Enter 进下一关
- 关卡号加一同时给生成器
- 挡板、血量、分数跨关保留
- 球不复用,重新随机皮肤
setIdentity('breakout')定存档目录- 存档目录随系统不同
getInfo判断文件是否存在- 首次启动写 10 条默认分
- 名字与分数各占一行
lines逐行读,交替解析- 三字母用 65 到 90 的码
string.char转成字符- 插入新分时后面的条目下移
- 从第 10 名往上比出插入位
- 写回时整表覆盖文件
容易混淆的问题
- 状态表里存的是类还是实例?
- 为什么
change之后旧状态的数据还在? init与enter谁先执行?- 图集过滤为什么要设
nearest? size变了碰撞盒会变吗?- 球撞砖为什么只处理一块?
px与py为什么要取绝对值?- 球速过快为什么会穿过去?
- 满血时还会消耗回血阈值吗?
- 存档为什么整表重写?