/ Ch17 运行时游戏玩法

第17章:运行时游戏玩法系统 — 零基础讲义

讲义说明本讲义基于 Game Engine Architecture Vol.II 第17章(Runtime Gameplay Systems, p.542-657)。

学习目标

  1. 理解运行时游戏对象的创建/更新/销毁管理
  2. 掌握对象句柄(Handle)防野指针的机制
  3. 了解脚本系统(Lua/Blueprint)如何嵌入C++引擎

17.1 游戏对象管理

17.1.1 对象句柄——安全的指针替代

17.1.1.1 为什么不能用裸指针

游戏对象频繁创建销毁(子弹、粒子、敌人)。裸指针在对象被销毁后变成悬空指针——访问它就是"未定义行为"(意味着"任何事都可能发生"——崩溃是最幸运的结果,更坏的是静默数据损坏)。

// 对象句柄 = 索引 + 代数 = 安全引用
struct GameObjectHandle {
    uint32_t index;       // 对象池中的槽位
    uint32_t generation;  // 代数(对象删除后代数递增)
};

GameObject* Resolve(GameObjectHandle handle) {
    GameObject& slot = objectPool[handle.index];
    if (slot.generation != handle.generation)
        return nullptr;       // 代数不匹配→对象已被删除→安全返回空
    return &slot;
}

// 使用示例
GameObjectHandle enemyHandle = SpawnEnemy();
// ... 战斗中敌人被击杀(对象销毁,代数递增) ...
GameObject* enemy = Resolve(enemyHandle);
if (enemy == nullptr) {
    // 安全处理:敌人已死,不做操作
}

17.1.2 属性系统与反射

17.1.2.1 编辑器如何知道对象有什么属性?

C++不原生支持反射(无运行时类型信息查询)。但编辑器需要在属性面板显示"位置/血量/模型路径"。Unreal 用 UHT(Unreal Header Tool)——编译前扫描带 UPROPERTY() 宏的代码,生成反射数据。Unity 用 C# 自带反射。


17.2 世界加载与流式传输

17.2.1 三种加载策略

17.2.1.1 策略对比

策略做法加载时间内存占用适用
全量加载整个关卡一次性进内存长(几十秒)竞技场/小关卡
分区加载按区域分块,接近时加载中(分段加载)大型线性关卡
流式加载连续异步加载/卸载无感知可控开放世界

17.3 实时更新

17.3.1 安全的增删操作

17.3.1.1 延迟批量处理

// 游戏更新循环中的对象管理
void GameLoop::Update(float dt) {
    // ① 先处理上帧积累的"待创建"请求(批量,不在遍历中插入)
    for (auto& createReq : pendingCreations) {
        activeObjects.push_back(CreateObject(createReq));
    }
    pendingCreations.clear();

    // ② 更新所有活跃对象
    for (auto& obj : activeObjects) {
        obj->Update(dt);
    }

    // ③ 处理上帧积累的"待销毁"请求(批量,不在遍历中删除)
    for (auto& destroyReq : pendingDestructions) {
        RemoveFromActiveList(destroyReq.handle);
        DestroyObject(destroyReq.handle);
    }
    pendingDestructions.clear();
}

17.4 脚本系统

17.4.1 C++的重与脚本的轻

17.4.1.1 迭代速度是核心价值

修改C++→编译(数分钟)→启动游戏→测试。脚本修改→保存→即刻生效(秒级)。设计师一天可能迭代数百次——脚本的即时性是他们工作的生命线。

// Lua脚本嵌入C++引擎
// C++侧注册API给脚本使用
lua_register(L, "SpawnEnemy", Lua_SpawnEnemy);
lua_register(L, "PlayMusic", Lua_PlayMusic);

// Lua脚本(设计师编写,即时修改即可测试)
function OnPlayerEnterBossRoom()
    SpawnEnemy("DragonBoss", 1000, 500)   -- 血量1000 位置(500)
    PlayMusic("boss_battle_theme.ogg")
    ShowBossHealthBar()
end

17.4.2 可视化脚本

17.4.2.1 Blueprint:让非程序员写逻辑

Unreal的Blueprint拖线连接节点——没有一行代码,但能实现完整游戏逻辑。这是游戏行业的重大变革:降低了参与门槛,设计师直接实现玩法,不再依赖程序员翻译需求。


本章核心洞察

🔑 对象管理 = 池化 + 句柄 + 延迟操作;脚本 = 设计师的C++
运行时对象管理的一切复杂性源于"更新循环中不能安全地增加/删除元素"。答案永远是延迟批量处理。脚本系统则解决了一个完全不同的问题——让非程序员能快速迭代游戏逻辑。

📝 课后练习题(含答案)

题1对象句柄如何防止野指针?

句柄 = 槽位索引 + 代数。对象被删除后代数递增。解析句柄时检查代数是否匹配——不匹配返回nullptr——不会出现指向已释放内存的悬空指针。

题2为什么需要脚本?

C++编译慢(分钟级),脚本即时生效(秒级)。设计师需要快速迭代("Boss血量调到1500试试"),脚本热重载支持这个工作流。且脚本降低编写游戏逻辑的门槛。


🤖 qAagent问答区

实践C++和脚本的分工边界是什么?

性能敏感的 → C++: 渲染、物理、AI核心算法、动画蒙皮。迭代频繁的 → 脚本: Boss AI决策、任务流程、UI交互、数值配置。简单判断:这个逻辑今天会改10次吗?是→脚本。需要每帧处理5000个对象吗?是→C++。