Legend of Zelda
俯视视角与四向移动
- 俯视视角取代横版卷轴
- 正交投影与等距投影的差别
- 同时改变 X 和 Y,而不是只靠重力
direction只有up、down、left、right- 动画键名拼接:
walk-left、sword-up - 走路图集每格 16×32,持剑 32×32
offsetX、offsetY补偿图集留白- 玩家碰撞盒 16×22,比格子矮
PLAYER_WALK_SPEED = 60EntityWalkState的bumped标志- 边界钳制代替图块碰撞
- 四向分支用
elseif固定优先级 - 同时按两个方向只走一个轴
- 状态机:
walk、idle、swing-sword - 子类先调父类
EntityWalkState.update - 敌人只有
walk与idle - 撞墙后随机换向或待机
实体定义与动画数据
ENTITY_DEFS是全局常量表- 全大写命名表示只读数据
- 键:
player、skeleton、slime、bat、ghost、spider - 每个动画含
frames、interval、texture texture缺省回落到entitiesEntity:createAnimations批量造AnimationAnimation的timesPlayed记录播了几圈refresh()让单次动画从头再来- 实体字段:
x、y、width、height、health offsetX、offsetY只影响绘制GAME_OBJECT_DEFS用同样的思路描述物件states表按名字取帧号solid决定物件是否挡路defaultState决定初始状态- 数据与代码分离,改动不进逻辑层
房间与图块生成
- 房间固定 22×11 格
TILE_SIZE = 16MAP_RENDER_OFFSET_X让地图居中留边- 双层循环按行填
tiles[y][x] - 四个角单独判定
- 四条边从
TILE_*_WALLS随机取 - 内部从
TILE_FLOORS随机取 TILE_EMPTY = 19- 图块编号从 1 起,左到右、上到下
- Python + Pillow 脚本标注图集编号
GenerateQuads把图集切成gFrames- 房间不做图块碰撞,只做边界
Room:render先地砖、再门、再物件、再实体renderOffset与adjacentOffset叠加
地牢生成与协程
- 10×10 房间网格,起点
(5, 10) - 队列加
head下标做 BFS visited用"x,y"字符串当键- 方向数组先洗牌再遍历
- 每个方向 40% 概率跳过
- 越界与已访问双重检查
- 生成完毕再统一
generateDoorways - 队列铺得宽,栈容易生成蛇形
coroutine.wrap返回可反复调用的函数coroutine.yield吐出phase、rooms、visitedphase:visit、push、doorways、done- 按 Enter 单步推进生成过程
math.randomseed(os.time())换种子- 死胡同软锁是已知问题,不算 bug
- 房间太少时再扫一遍补足
相机与房间切换
cameraX、cameraY是translate的偏移shifting标志锁住整间房的更新- 撞门时
Event.dispatch('shift-left') Event.on给四个方向各绑一次- 位移量是整屏:
VIRTUAL_WIDTH、VIRTUAL_HEIGHT nextRoom由currentRoom坐标加减得出adjacentOffsetX把下一间画在屏幕外Timer.tween(1, ...)同时补相机与玩家finishShifting归零并把nextRoom换成当前房- 收尾时重置玩家
direction与坐标 - 切换期间只更新玩家动画
love.graphics.push/pop包住平移- 进新房间前先把它的门置为打开
- 到达后关闭旧房间的门并复位开关
攻击、受击与无敌帧
Hitbox只是带尺寸的矩形对象- hitbox 是能被打中的身体
- hurtbox 是能造成伤害的判定
- 敌人身体兼作两者
- 剑的 hurtbox 只在
swing-sword状态存在 - 左右挥剑 8×16,上下挥剑 16×8
- 剑盒贴在
player.x ± width entity:collides(hitbox)复用 AABB- 命中一次
damage(1) - 挥满一圈靠
timesPlayed > 0回idle - 挨打
damage(1)后goInvulnerable(1.5) - 无敌期间每 0.06 秒切一次半透明
health == 0切game-over- 血量上限 6,一颗心 2 点
- 挥剑期间锁住移动
- 挥剑没有冷却,可以连挥
道具、库存与存档
- 罐子做成
GameObject,solid = true - 新增状态:
lift、carry、throw - 举着罐子时禁掉
swing-sword - 罐子每帧跟随玩家坐标绘制
- 抛出后带
dx、dy变成投射物 - 撞墙、撞敌人、飞满四格都碎
- 掉落物用
onConsume回调 - 一颗心补 2 点,上限 6
- 宝箱开出回旋镖,写进库存
- 库存起步就是几个布尔字段
- 按
E发射回旋镖 - 回旋镖飞满四格后折返并追踪玩家
- 飞行中按时间旋转贴图
- 存档要序列化房间坐标、血量、库存
love.filesystem.write落盘love.filesystem.getInfo判断存档是否存在
触发、门与遮罩
GameObject由定义表构造onCollide默认是空函数- 开关两态:
unpressed、pressed - 帧号取自
states[state].frame - 踩下开关打开房间所有门
Doorway位置按方向硬编码- 一扇门由四块图块拼成
open布尔决定用哪组图块编号- 碰墙时把玩家临时推进墙内再判定
- 撞到门后吸附到门中心
- 模板缓冲遮住拱门区域
stencil(fn, 'replace', 1)写入遮罩setStencilTest('less', 1)只放行未写值的像素- LÖVE 12 换成
setStencilState加setColorMask - 遮罩测试结束必须清掉
- 按
S画出遮罩矩形
容易混淆的问题
- hitbox 与 hurtbox:一个能被击中,一个造成伤害
- 图块索引与像素坐标:前者从 1 起,后者从 0 起
cameraX与adjacentOffsetX:前者平移画布,后者挪动下一间房currentRoom与nextRoom:补间结束才交换,不是撞门那一刻- 状态机与 tween:前者换行为,后者只改数值
coroutine.yield与return:前者保留调用栈,后者结束函数TILE_EMPTY与越界索引:前者是合法编号,后者会取到nil- 一颗心与 2 点血:界面单位与数据单位,差一个系数
solid与onCollide:一个只是数据标记,一个才是真正的行为looping = false与单次播放:数据想停,代码里被or true吃掉