3.如何构建游戏世界

  1. 3.如何构建游戏世界
    1. 3.1 游戏世界由什么构成(Everything is a GameObject)
    2. 3.2 如何描述游戏对象
      1. 属性与行为
      2. 传统方式:面向对象继承
        1. 基础类:无人机(Drone)
        2. 派生类:武装无人机(ArmedDrone)
        3. 继承机制的局限
      3. 现代方式:组件化
        1. 核心设计:组件与对象的关系
        2. 组件化代码示例
        3. 组件化的优势:以无人机与武装无人机为例
        4. 主流引擎的组件化实践
      4. 核心要点总结
    3. 3.3 如何让游戏世界动起来(对象驱动 vs 组件驱动)
      1. 两种Tick模式:对象化 vs 组件化
        1. 对象化Tick(Object-based Tick)
        2. 组件化Tick(Component-based Tick)
      2. 现代引擎的选择:组件化Tick
    4. 3.4 对象间的交互:事件机制
      1. 传统交互的问题:硬编码耦合
      2. 事件机制:解耦交互逻辑
        1. 事件机制的实现逻辑
        2. 代码示例:事件机制实现
        3. 主流引擎的事件机制实践
    5. 3.5 如何管理游戏对象
      1. 场景管理的核心职责
      2. 空间分割策略:解决“查询效率”问题
        1. 无分割(No Division)
        2. 网格分割(Divided by Grid)
        3. 对象集群分割(Segmented by Object Clusters)
        4. 层次化分割(Hierarchical Segmentation)
      3. 常见空间管理数据结构
      4. 核心要点总结
    6. 3.6 时序的一致性问题
      1. 时序问题根源
      2. 核心目标:保证游戏确定性(Deterministic)
      3. 解决方案
    7. 3.7 Q&A
      1. 一个Tick过长如何处理?
      2. 渲染线程与逻辑线程如何同步?
      3. 动态对象的Scene Manage如何处理?
      4. 组件模式有什么缺点?
      5. Event机制如何调试?
      6. 物理系统和动画系统如何相互影响?

3.如何构建游戏世界


3.1 游戏世界由什么构成(Everything is a GameObject)

  • 我们可以思考游戏世界由什么构成,可以把游戏对象按不同类型区分

  • 动态物体:角色、载具(坦克/飞机/无人机)、投射物等。

  • 静态物体:地形、建筑、箱体、瞭望塔、路障等。

  • 环境要素:天空、天气、植被、雾、体积云等。

  • “看不见但关键”的辅助对象:空气墙、触发区、导航网格(NavMesh)、标尺/调试 Gizmo 等。

在现代商用引擎中,“世界里的万物”都抽象为游戏对象(GameObject/Actor),对象本身是“容器”,能力由组件提供。以 Unity 为例:每个 GameObject 至少有一个 Transform(位置/旋转/缩放),其它功能(渲染、物理、音频、脚本…)靠组件挂载。


3.2 如何描述游戏对象

描述游戏对象的核心是“拆分属性与行为”,传统继承机制耦合紧、扩展难,现代组件化通过“组合替代继承”实现灵活扩展,是主流引擎的首选方案

属性与行为

  • 任何游戏对象都可拆解为“属性(静态特征)”与“行为(动态能力)”,二者共同定义对象的“是什么”与“能做什么”。对应面向对象语言中的字段和函数。
  • 以“无人机(Drone)”为例:
    • 属性:形状(Shape,如模型网格形态)、位置(Position,用三维向量Vector3表示)、电量(Capacity of battery,浮点型float)、生命值(Health,浮点型float)——这些是对象的固有特征,不随交互主动变化,需通过外部行为修改;
    • 行为:移动(Move)、侦察(Scout)——这些是对象的动态能力,通过代码逻辑实现状态改变(如移动行为更新位置属性、侦察行为探测周围敌人)。

这种“属性+行为”的拆分思路,是所有对象描述方式的底层逻辑,区别在于“属性与行为的组织形式”。

传统方式:面向对象继承

早期引擎常用“类继承”组织对象的属性与行为,核心逻辑是“子类复用父类特征,新增自身专属功能”,典型案例为“无人机→武装无人机”的扩展:


基础类:无人机(Drone)

class Drone  // 父类:定义无人机基础特征
{
public:
    /* 父类属性:子类可直接继承 */
    Vector3 position;  // 位置(三维坐标)
    float health;      // 生命值
    float fuel;        // 电量(等价于燃料)
    
    /* 父类行为:子类可直接复用 */
    void move() { /* 移动逻辑:根据速度更新position */ }
    void scout() { /* 侦察逻辑:探测周围敌人并返回信息 */ }
};

派生类:武装无人机(ArmedDrone)

武装无人机在无人机基础上新增“弹药”属性与“开火”行为,通过继承实现功能扩展:

class ArmedDrone : public Drone  // 子类:继承Drone并新增功能
{
public:
    /* 子类新增属性:父类无此特征 */
    float ammo;  // 弹药量(控制开火次数)
    
    /* 子类新增行为:父类无此能力 */
    void fire() { /* 开火逻辑:消耗ammo、生成子弹对象 */ }
};

继承机制的局限

  • 共同父类难以找到:随着游戏世界越来越复杂,有些东西它并没有那么清晰的父子关系。比如我把坦克和船合到一起就会造出水陆两栖坦克,而坦克本身是派生于车辆,而巡逻艇是派生于船的,那水陆两栖坦克,那么它的父亲是车辆还是船呢?
  • 耦合紧:子类与父类强绑定,修改父类属性(如将fuel改为battery)会影响所有子类,可能引发连锁bug;
  • 扩展难:新增对象类型(如“侦察无人机”“运输无人机”)需创建新派生类,导致类数量激增,维护成本升高;
  • 灵活性差:无法动态组合功能(如给普通无人机临时加装武器,需修改类结构而非实时添加组件)。

现代方式:组件化

组件化的核心是“拆分独立功能组件,通过组合实现对象特征”——不再让对象继承自父类,而是让对象作为“组件容器”,每个组件负责一个单一功能(如变换、模型、战斗),通过挂载/卸载组件灵活调整对象能力。

铲子车组装不同铲子
枪组装不同消音器

核心设计:组件与对象的关系

  • 组件基类(ComponentBase):所有组件的统一父类,定义核心接口(如帧更新函数tick()),确保组件行为可被引擎统一调度;
  • 游戏对象(GameObject):作为“组件聚合体”,自身不实现具体功能,仅负责组件的添加、删除与调度(如调用所有组件的tick());
  • 组件分类:按功能拆分独立组件,常见类型包括:
    • 变换组件(TransformComponent):管理对象位置、旋转、缩放,是所有对象的基础组件;
    • 模型组件(ModelComponent):加载并渲染对象外观(如无人机的3D模型),包含网格、材质等数据;
    • 动力组件(MotorComponent):管理移动能力与能量(如电量消耗、移动速度),实现move()方法;
    • 战斗组件(CombatComponent):管理攻击能力(如弹药、开火逻辑),实现fire()方法;
    • AI组件(AIComponent):管理决策逻辑(如侦察、追击),实现对象的自主行为。

组件化代码示例

// 1. 组件基类:统一接口
class ComponentBase 
{
public:
    virtual void tick() = 0;  // 纯虚函数:强制子类实现帧更新
    virtual ~ComponentBase() {}  // 虚析构:确保子类资源正确释放
};

// 2. 游戏对象:组件容器
class GameObject 
{
private:
    vector<ComponentBase*> components;  // 存储挂载的所有组件
public:
    // 添加组件
    void addComponent(ComponentBase* comp) { components.push_back(comp); }
    // 帧更新:调度所有组件
    void tick() 
    {
        for (auto comp : components) { comp->tick(); }
    }
};

// 3. 具体组件实现
class TransformComponent : public ComponentBase 
{
public:
    Vector3 position;
    void tick() { /* 更新位置(如跟随父对象移动) */ }
};

class MotorComponent : public ComponentBase 
{
public:
    float battery;
    void tick() { /* 消耗电量、更新移动状态 */ }
    void move() { /* 具体移动逻辑 */ }
};

class CombatComponent : public ComponentBase 
{
public:
    float ammo;
    void tick() { /* 检查弹药状态 */ }
    void fire() { /* 开火逻辑 */ }
};

组件化的优势:以无人机与武装无人机为例

通过“挂载不同组件”实现功能差异,无需创建新类,灵活性大幅提升,二者的组件对比如下:

组件类型 普通无人机(Drone) 武装无人机(ArmedDrone)
Transform(变换) ✅ (必选:管理位置) ✅ (必选:管理位置)
Model(模型) ✅ (必选:显示外观) ✅ (必选:显示外观)
Motor(动力) ✅ (必选:移动与电量) ✅ (必选:移动与电量)
AI(人工智能) ✅ (基础:侦察逻辑) ✅ (进阶:战斗决策)
Combat(战斗) ❌ (无武器) ✅ (有弹药、开火功能)

这种方式的核心优势是“解耦与灵活”——新增功能只需添加组件,删除功能只需移除组件,完全贴合现代游戏“快速迭代玩法”的需求。

主流引擎的组件化实践

  • Unity:以MonoBehaviour为行为组件基类,搭配Transform(变换)、Animator(动画)、Collider(碰撞)等内置组件,开发者可自定义组件挂载到GameObject上;
  • Unreal Engine:以UObject为基础对象类,AActor为游戏对象类,UActorComponent为组件基类(如UStaticMeshComponent静态模型组件、UCharacterMovementComponent移动组件),支持可视化拖拽组合。

核心要点总结

  • 游戏世界中的所有事物都是游戏对象
  • 游戏对象可以采用组件化方式进行描述

3.3 如何让游戏世界动起来(对象驱动 vs 组件驱动)

Tick是游戏世界的“最小时间单位”,通过“帧更新”驱动所有对象状态变化;对象化Tick直观但低效,组件化Tick并行高效,是现代引擎的核心选择

Tick概念:每隔1/30s执行一次。那我们就可以每次Tick调用GameObject的所有组件,使得游戏世界能够动起来。

两种Tick模式:对象化 vs 组件化

对象化Tick(Object-based Tick)

  • 核心逻辑:每个游戏对象(GameObject)独立管理自身组件的更新——对象的tick()函数按固定顺序调用内部所有组件的tick(),更新逻辑与对象强绑定。

    // 示例:对象化Tick的更新顺序
    无人机GO.tick() → Transform.tick() → Motor.tick() → AI.tick()
    士兵GO.tick()   → Transform.tick() → Motor.tick() → AI.tick()
    
  • 优缺点

    • 优点:逻辑直观(更新顺序与对象绑定,易理解)、调试简单(问题可直接定位到具体对象);
    • 缺点:无法并行处理(必须按对象顺序更新,多核CPU利用率低)、缓存命中率低(组件数据分散在不同对象中,CPU读取时频繁“跳读”)。

组件化Tick(Component-based Tick)

  • 核心逻辑:按“组件类型”批量更新——引擎先收集所有同类型组件(如所有Motor组件、所有AI组件),再批量调用它们的tick(),更新顺序由组件类型决定,与对象无关。

    // 示例:组件化Tick的更新顺序
    所有Motor组件.tick() → 无人机.Motor.tick() → 士兵.Motor.tick() → 坦克.Motor.tick()
    所有AI组件.tick()   → 无人机.AI.tick()   → 士兵.AI.tick()   → 坦克.AI.tick()
    
  • 优缺点

    • 优点:可并行处理(同类型组件无依赖,可分配到多核CPU同时执行)、减少缓存未命中(同类型组件数据集中存储,CPU读取时“连续读”)、效率更高;
    • 缺点:逻辑分散(更新顺序与对象解绑,需跟踪组件所属对象)、调试成本略高(需定位到组件而非对象)。

现代引擎的选择:组件化Tick

由于现代游戏对性能要求极高(如开放世界需同时更新数千个对象),组件化Tick的“并行高效”优势成为关键。主流引擎(Unity、Unreal)均以组件化Tick为核心,同时提供对象化Tick的兼容接口(如Unity的Update()本质是MonoBehaviour组件的Tick),平衡效率与开发便捷性。


3.4 对象间的交互:事件机制

传统硬编码交互(switch-case)耦合紧、难维护,事件机制通过“发送者→事件→接收者”的解耦模式,实现灵活、可扩展的对象交互

传统交互的问题:硬编码耦合

以“炸弹爆炸(Bomb:explode())”为例,传统方式通过switch-case判断对象类型,直接处理所有交互逻辑,代码如下:

void Bomb::explode() 
{
    switch(go_type)  // 根据对象类型分支处理,耦合所有对象逻辑
    {
        case GoType::HUMAN:    /* 处理士兵:减少生命值、播放受伤动画 */ break;
        case GoType::DRONE:    /* 处理无人机:减少电量、停止移动 */ break;
        case GoType::TANK:     /* 处理坦克:减少护甲、播放爆炸特效 */ break;
        case GoType::STONE:    /* 处理石头:破碎、生成碎片 */ break;
        default: break;
    }
}

这种方式的核心问题的是“强耦合”:

  • 炸弹需知道所有对象的处理逻辑,新增对象(如直升机)需修改炸弹代码,违反“开闭原则”;
  • 逻辑集中在一个函数中,代码量大、易出错,维护成本随对象类型增加而急剧升高。

事件机制:解耦交互逻辑

事件机制的核心是“发送者只负责‘通知’,接收者自己负责‘响应’”,通过三个角色实现解耦:

  • 事件发送者(Sender):触发事件的对象(如炸弹),仅需构造并发布事件,无需关心谁会响应;
  • 事件(Event):交互的“消息载体”,包含交互所需的关键参数(如爆炸伤害、爆炸位置);
  • 事件接收者(Receiver):响应事件的对象(如士兵、无人机),需提前“订阅”事件,在事件触发时执行自身逻辑。

事件机制的实现逻辑

  1. 定义事件:封装交互所需参数,如爆炸事件包含炸弹ID、伤害值、位置;
  2. 事件管理器:统一管理“事件订阅”与“事件分发”,维护“事件类型→接收者列表”的映射;
  3. 发送者发布事件:炸弹爆炸时,通过事件管理器发布“爆炸事件”;
  4. 接收者响应事件:士兵、无人机等对象提前订阅“爆炸事件”,在事件触发时执行自身逻辑(如士兵减血、无人机减电量)。

代码示例:事件机制实现

// 1. 定义爆炸事件(消息载体)
struct ExplodeEvent 
{
    int bomb_id;       // 炸弹ID
    float damage;      // 爆炸伤害
    Vector3 position;  // 爆炸位置
};

// 2. 事件管理器(订阅与分发)
class EventManager 
{
private:
    unordered_map<string, vector<function<void(ExplodeEvent)>>> listeners;
public:
    // 订阅事件:接收者注册响应函数
    void subscribe(const string& event_type, function<void(ExplodeEvent)> callback) 
    {
        listeners[event_type].push_back(callback);
    }
    // 发布事件:发送者触发事件,管理器分发
    void publish(const string& event_type, ExplodeEvent event) 
    {
        for (auto& callback : listeners[event_type]) 
        {
            callback(event);  // 调用所有接收者的响应函数
        }
    }
};

// 3. 发送者:炸弹
class Bomb 
{
private:
    EventManager& event_manager;
public:
    void explode() 
    {
        ExplodeEvent event = {1, 100.0f, Vector3(0, 0, 0)};
        event_manager.publish("ExplodeEvent", event);  // 仅发布事件,不关心响应
    }
};

// 4. 接收者:士兵、无人机
class Soldier 
{
public:
    Soldier(EventManager& em) 
    {
        // 订阅事件,定义自身响应逻辑
        em.subscribe("ExplodeEvent", [this](ExplodeEvent e) 
        {
            this->takeDamage(e.damage);
        });
    }
    void takeDamage(float d) { /* 士兵受伤逻辑 */ }
};

class Drone 
{
public:
    Drone(EventManager& em) 
    {
        em.subscribe("ExplodeEvent", [this](ExplodeEvent e) 
        {
            this->loseBattery(e.damage * 0.5f);
        });
    }
    void loseBattery(float v) { /* 无人机电量减少逻辑 */ }
};

主流引擎的事件机制实践

  • Unity:提供SendMessage()(发送消息给对象所有组件)、EventSystem(UI事件),以及通过delegateevent实现的自定义事件。例如通过SendMessage()实现简单交互:
    using UnityEngine;
    public class Bomb : MonoBehaviour 
    {
        void Explode() 
        {
            // 发送“ApplyDamage”消息,参数5.0,所有含该函数的组件响应
            gameObject.SendMessage("ApplyDamage", 5.0f);
        }
    }
    public class Soldier : MonoBehaviour 
    {
        public void ApplyDamage(float damage) 
        {
            Debug.Log("士兵受击:" + damage);
        }
    }
    
  • Unreal Engine:通过DECLARE_EVENT_*系列宏定义事件(如DECLARE_EVENT_OneParam带一个参数的事件),支持蓝图与C++交互,适配可视化开发需求。


3.5 如何管理游戏对象

场景管理的核心是“高效管理对象+快速查询交互”,通过空间分割策略解决“大海捞针”问题,场景图实现对象层级维护,保障大场景流畅运行

游戏对象是处于游戏场景中的,游戏对象通过唯一id来进行识别:unique ID(uid),游戏对象处于特定位置。

继续上述的例子,当坦克发生爆炸时,所有接收到事件的对象都会进行处理。但游戏场景中存在大量对象时,这样的处理方式难以适用(每个游戏对象都可能和其它游戏对象互动,此时算法复杂度为n^2)。因此就需要根据游戏对象在场景中的分布进行管理,以优化场景对象的查找。

场景管理的核心职责

场景(Scene)是游戏世界的“容器”,场景管理模块的核心职责是:

  1. 对象生命周期管理:加载/卸载场景内的游戏对象(如进入关卡时加载敌人、离开时卸载非必要建筑),控制内存占用;
  2. 高效对象查询:快速定位目标对象(如“查找爆炸范围内的所有士兵”),避免遍历所有对象导致的性能损耗;
  3. 层级关系维护:通过场景图(Scene Graph)管理对象的父子关系(如“士兵手持武器”,武器随士兵移动),简化变换同步逻辑。

空间分割策略:解决“查询效率”问题

当场景内对象数量庞大(如开放世界有上万个对象),遍历所有对象查询会严重拖慢性能,此时需通过“空间分割”缩小查询范围,常见策略如下:

无分割(No Division)

  • 逻辑:不划分空间,查询时遍历所有对象;
  • 适用场景:对象极少的场景(如测试场景、小型UI界面);
  • 缺点:对象数量多时效率极低,如同“大海捞针”。

网格分割(Divided by Grid)

  • 逻辑:将场景划分为均匀网格,查询时仅遍历目标网格内的对象;
  • 适用场景:对象分布均匀的场景(如平原、沙漠);
  • 优点:实现简单,查询效率随网格密度提升。

对象集群分割(Segmented by Object Clusters)

  • 逻辑:按对象密度聚类分割(高密度区域网格更细,低密度区域网格更粗);
  • 适用场景:对象分布不均的场景(如城市+荒野,城市内对象密集、荒野内稀疏);
  • 优点:平衡查询效率与内存占用,避免均匀网格在低密度区域的资源浪费。

层次化分割(Hierarchical Segmentation)

  • 逻辑:递归划分空间(2D用四叉树Quadtree,3D用八叉树Octree),从根节点逐层定位目标区域;
  • 适用场景:大场景(如开放世界、MMO游戏);
  • 优点:查询效率极高,可快速定位到小范围区域,是现代开放世界引擎的首选。

常见空间管理数据结构

二分树、四叉树(八叉树)、BVH包围盒、场景图(Scene Graph)等

核心要点总结

  • 万物皆对象
  • 游戏对象可采用组件化方式描述
  • 游戏对象状态通过 tick 循环更新
  • 游戏对象通过事件机制相互交互
  • 游戏对象在场景中通过高效策略进行管理

3.6 时序的一致性问题

时序问题根源

  • 多核并行Tick:同类型组件并行更新,导致对象交互顺序不可控(如两AI并行算位置,结果与单核不同);
  • 直接事件通信:对象间即时发事件(如“分手信直接送门口”),易形成混乱事件链,帧内执行顺序无序;
  • 组件循环依赖:如“动力→动画→物理→动力”的相互影响,无明确时序易出“动画未更、物理已算”的偏差(致穿模/卡顿)。

核心目标:保证游戏确定性(Deterministic)

  • 关键作用:支撑精彩回放(仅存玩家输入,靠复现逻辑生成画面,需相同输入出相同结果);避免多人/多设备运行结果不一致。

解决方案

  • 邮局模式:事件先存队列(邮局),隔帧统一分发,避免并行通信顺序问题;
  • Pre Tick/Post Tick:拆分Tick为“预准备(如动力更速度)→主逻辑(如动画切换)→后同步(状态缓存)”,解循环依赖;
  • 父子对象规则:绑定对象(如人车)按“父先Tick、子后Tick”执行,确保子对象读取父对象最新状态(防错位)。


3.7 Q&A

一个Tick过长如何处理?

  1. 传入步长拆分:为每个Tick设置合理步长,分摊单次计算压力。
  2. 跳过Tick(谨慎使用):直接跳过部分Tick可快速缓解,但可能导致逻辑断层或画面卡顿。
  3. 优化算法瓶颈:针对大量计算场景,采用分帧处理拆分任务,减少单次Tick的计算量。

渲染线程与逻辑线程如何同步?

  • 逻辑线程:优先完成各系统的逻辑计算(如角色状态、数值变化)。
  • 渲染线程:基于逻辑线程的计算结果,准备渲染数据(如坐标、材质)。
  • 核心原则:逻辑线程需在渲染线程启动前完成计算,确保渲染数据的准确性。

动态对象的Scene Manage如何处理?

  • 核心思路:根据游戏类型选择适配的空间结构(如网格划分、四叉树)。
  • 最终目标:减少动态对象的更新范围和频率,降低系统运行代价。

组件模式有什么缺点?

  • 数据组织低效:需通过GameObject作为中间载体访问组件或接口,层级嵌套增加访问成本。
  • 组件通讯成本高:跨组件交互需额外设计通讯机制,易出现耦合或响应延迟。

Event机制如何调试?

  • 核心方式:输出详细Log日志,覆盖事件触发、传递、执行全流程。
  • 优势:非技术人员也可通过日志定位问题,支持可视化工具解析日志数据。

物理系统和动画系统如何相互影响?

  1. 早期方案(布娃娃模式):动画系统完全服从物理系统,仅依赖物理引擎驱动角色运动,视觉效果较生硬。
  2. 现代方案(插值融合):通过权重分配融合两个系统的结果,公式为 实际效果 = a × 物理系统结果 + (1 - a) × 动画系统结果(a为0-1之间的权重值)。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏