20. Rooms,Areas,Flow
3.16,周一,上午依旧睡到十一点多。
看了一个文章,感觉 AI 时代这种手写代码实在没用,我只需要理解思路,做好架构,剩下全交给 AI 就好了。
就从这个教程开始实践吧。
Room 的定义
Room 就是场景。主菜单是 Room,选人是 Room,关卡本体也是 Room。
核心原则:同一时刻只让一个 Room 活动,更新和绘制都交给它。
Room 的基本形态:
Stage = Object:extend()
function Stage:new()
-- 初始化本 Room 需要的资源或状态
end
function Stage:update(dt)
-- 只处理本 Room 的更新逻辑
end
function Stage:draw()
-- 只处理本 Room 的绘制逻辑
end最小切换系统
最省事的做法是一个 current_room 加一个 gotoRoom:
function love.load()
-- 当前只维护一个活动 Room
current_room = nil
end
function love.update(dt)
-- 只更新当前 Room
if current_room then current_room:update(dt) end
end
function love.draw()
-- 只绘制当前 Room
if current_room then current_room:draw() end
end
function gotoRoom(room_type, ...)
-- 通过类名字符串创建 Room
current_room = _G[room_type](...)
endroom_type 是字符串,比如 'Stage',Lua 全局表 _G 能用字符串拿到类。
所以 gotoRoom('Stage') 实际就是 current_room = Stage(...)。
优点是简单直接,缺点是每次切换都新建 Room,状态不保留。
何时拆成多个 Room
以 Nuclear Throne 为例:主菜单 -> 选人 -> 游戏 -> 选被动 -> 回游戏。
看起来很多画面,但“加载界面”“死亡界面”只是覆盖层,逻辑仍在游戏里。
拆分标准:逻辑彻底不同才拆;只是临时状态就留在同一个 Room。
持久 Room 的需求
需要回到旧场景并保留状态时,就要持久 Room:
function love.load()
-- rooms 用来持久化缓存 Room 实例
rooms = {}
current_room = nil
end
function addRoom(room_type, room_name, ...)
-- 只在首次进入时创建
local room = _G[room_type](room_name, ...)
rooms[room_name] = room
return room
end
function gotoRoom(room_type, room_name, ...)
if current_room and rooms[room_name] then
-- 切换前后给 Room 留钩子
if current_room.deactivate then current_room:deactivate() end
current_room = rooms[room_name]
if current_room.activate then current_room:activate() end
else
current_room = addRoom(room_type, room_name, ...)
end
endactivate/deactivate 适合作为存档、读档、清理引用的挂载点。
《以撒》这种能回头走的游戏很适合:回去时房间仍保留之前被打过的状态。
转场的限制
房间切换有过渡动画时,往往要同时画两个 Room。
单一 current_room 不够,这套方案只是简化版。
Area 的职责
Area 负责对象管理:更新、绘制、移除。
Room 管流程,Area 管具体对象。
Area = Object:extend()
function Area:new(room)
-- 绑定所属 Room,持有对象列表
self.room = room
self.game_objects = {}
end
function Area:update(dt)
-- 倒序遍历,安全删除
for i = #self.game_objects, 1, -1 do
local obj = self.game_objects[i]
obj:update(dt)
if obj.dead then table.remove(self.game_objects, i) end
end
end
function Area:draw()
-- 统一绘制
for _, obj in ipairs(self.game_objects) do obj:draw() end
end倒序遍历是为了边删除边遍历时不漏掉元素。
GameObject 的基类
所有对象继承自 GameObject,统一位置、ID、timer:
GameObject = Object:extend()
function GameObject:new(area, x, y, opts)
local opts = opts or {}
-- 把 opts 字段直接挂到对象上
if opts then for k, v in pairs(opts) do self[k] = v end end
-- 基础通用字段
self.area = area
self.x, self.y = x, y
self.id = UUID()
self.dead = false
self.timer = Timer()
end
function GameObject:update(dt)
-- 默认携带计时器
if self.timer then self.timer:update(dt) end
endopts 允许创建对象时直接塞参数,省掉一堆赋值。
UUID 与随机
唯一 ID 用 UUID:
function UUID()
-- 使用 LÖVE 的随机数,避免固定种子
local fn = function(x)
local r = love.math.random(16) - 1
r = (x == "x") and (r + 1) or (r % 4) + 9
return ("0123456789abcdef"):sub(r, r)
end
return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
end用 love.math.random 是因为 LÖVE 已经播过种子,避免每次开局都生成同一串 ID。
往 Area 里塞对象
对象创建用字符串类名,和 gotoRoom 的思路一致:
function Area:addGameObject(game_object_type, x, y, opts)
-- 通过类名字符串创建对象
local game_object = _G[game_object_type](self, x or 0, y or 0, opts or {})
table.insert(self.game_objects, game_object)
return game_object
endRoom 里怎么用 Area
最朴素的组合:
function Stage:new()
-- Room 内持有一个 Area
self.area = Area(self)
self.area:addGameObject('Player', 100, 120)
end
function Stage:update(dt)
-- 交给 Area 统一更新
self.area:update(dt)
end
function Stage:draw()
-- 交给 Area 统一绘制
self.area:draw()
end以撒式结构
用一个 Game Room 统筹关卡生成,再创建多个 Room 实例:
function Game:new()
self.rooms = {}
for i = 1, 10 do
-- 预生成一批房间实例
self.rooms[i] = addRoom('Room', i)
end
-- 进入起始房间
gotoRoom('Room', 1)
end玩家移动就 gotoRoom('Room', next_id),每个房间都能记住之前发生的事。