跳转至内容

20. Rooms,Areas,Flow

3.16,周一,上午依旧睡到十一点多。

看了一个文章,感觉 AI 时代这种手写代码实在没用,我只需要理解思路,做好架构,剩下全交给 AI 就好了。

就从这个教程开始实践吧。

Room 的定义

Room 就是场景。主菜单是 Room,选人是 Room,关卡本体也是 Room。

核心原则:同一时刻只让一个 Room 活动,更新和绘制都交给它。

Room 的基本形态:

lua
Stage = Object:extend()

function Stage:new()
    -- 初始化本 Room 需要的资源或状态
end

function Stage:update(dt)
    -- 只处理本 Room 的更新逻辑
end

function Stage:draw()
    -- 只处理本 Room 的绘制逻辑
end

最小切换系统

最省事的做法是一个 current_room 加一个 gotoRoom

lua
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](...)
end

room_type 是字符串,比如 'Stage',Lua 全局表 _G 能用字符串拿到类。

所以 gotoRoom('Stage') 实际就是 current_room = Stage(...)

优点是简单直接,缺点是每次切换都新建 Room,状态不保留。

何时拆成多个 Room

以 Nuclear Throne 为例:主菜单 -> 选人 -> 游戏 -> 选被动 -> 回游戏。

看起来很多画面,但“加载界面”“死亡界面”只是覆盖层,逻辑仍在游戏里。

拆分标准:逻辑彻底不同才拆;只是临时状态就留在同一个 Room。

持久 Room 的需求

需要回到旧场景并保留状态时,就要持久 Room:

lua
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
end

activate/deactivate 适合作为存档、读档、清理引用的挂载点。

《以撒》这种能回头走的游戏很适合:回去时房间仍保留之前被打过的状态。

转场的限制

房间切换有过渡动画时,往往要同时画两个 Room。

单一 current_room 不够,这套方案只是简化版。

Area 的职责

Area 负责对象管理:更新、绘制、移除。

Room 管流程,Area 管具体对象。

lua
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:

lua
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
end

opts 允许创建对象时直接塞参数,省掉一堆赋值。

UUID 与随机

唯一 ID 用 UUID:

lua
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 的思路一致:

lua
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
end

Room 里怎么用 Area

最朴素的组合:

lua
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 实例:

lua
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),每个房间都能记住之前发生的事。