游戏物体拆解

GameObject

现代游戏引擎中,将物体统一称为GameObject(GO),主要包括如下:

  • 可交互动态物(DynamicGameObject)

  • 静态物(staticGameObject)

  • 环境(地形系统、天空、植被系统...)

  • 其他物体(TriggerArea、navigationMesh,ruler)

对GO的描述:

  1. Property:包括几何模型、位置、Transform等等

  2. Behavior:行为逻辑

早期引擎大多都是按照这样的方式,使用面向对象的思维,但是面向对象无法解决繁杂交错的派生关系,所以便有了组件化(ComponentBase)

组件模块

如图。每个派生类中都有一个Tick()函数,方便更新

Object-based Tick

在每一个Tick,依次更新每一个GO的Component

Component-based Tick

但是在现代引擎中,一般是按照系统依次Tick,来提高效率

GO之间如何交互

事件机制:在下一个Tick时接收事件(订阅广播)

组件模式有何缺点?

效率不如class,每次Tick需要查询组件(这一点在ECS架构中可以进行数据层面的优化)

// 传统组件模式的性能问题示例
public class GameObject {
    private List<Component> components;
    
    public void Update() {
        foreach(var component in components) {
            component.Update(); // 缓存不友好,虚函数调用开销
        }
    }
}

// ECS架构优化(Unity DOTS)
public class MovementSystem : SystemBase {
    protected override void OnUpdate() {
        Entities.ForEach((ref Translation translation, in Velocity velocity) => {
            translation.Value += velocity.Value * Time.deltaTime;
        }).ScheduleParallel(); // 数据导向,批量处理
    }
}

组件之间也需要设计通讯系统

如何管理GO

  • 标识物体:使用UID与物体的世界坐标

  • 不管理GO、直接广播事件:在少量GO时可以使用,但是会导致n平方问题

  • 分治管理:画格子,在格子中广播事件,但是当GO分布不均匀时导致问题

  • 四叉树/八叉树管理:记录事件节点

  • BVH(BoundingVolumeHierarchies):现代引擎常用的boundingBox

动态物体如何处理?

每种管理方式都有各自的更新数据的方式,会根据不同的游戏需求去设计不同的空间管理来减少性能开销,比如静态的单机游戏使用四叉树或者八叉树,多人的开放世界游戏使用BVH

处理复杂情况

GOTick时序问题:

一般是父节点先Tick

面对复杂情况时,由于不同的Component一般是在不同的CPU上并行执行,如果此时GO相互发送消息,容易出现逻辑混乱,影响游戏的确定性,所以需要一个管理事件的中心作为中转来做到同步(类似于同步锁?)同时也需要preTick/postTick解决Tick依赖时序

并且许多组件可能出现循环依赖的问题,比如Animation和physics

主流的方法是采用插值的方式,某几帧是动画,将动画的输出作为物理的输入

Tick时间过长怎么办?

传递步长,下一帧补偿

分帧处理

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

会先更新逻辑帧再更新渲染