ECS架构与实际应用

2017年一场《守望先锋》的技术分享让我们认识到了ECS架构,ECS近年来已然成为游戏开发中比较热门的一种架构模式`。

Entity-Component-System(ECS)是一种架构模式,用于将游戏对象(实体)拆分为组件,并使用系统来处理这些组件的行为和逻辑。ECS架构模式的优点在于它提供了一种高度模块化可扩展的方式来管理游戏对象和行为。

什么是ECS

  • E Entity 实体,本质上是存放组件的容器,实体中通常只有唯一的标识符(ID)
  • C Component 组件,游戏所需的所有数据结构,组件只能存放数据,不能实现任何处理状态相关的函数
  • S System 系统,根据组件数据处理逻辑状态的管理器,只有函数,没有变量,System之间的执行顺序需要严格制定

组件放数据,系统来处理,让数据与逻辑进行解耦

与传统的“类-继承”奉行的“我是什么”不同,基于组件化的ECS架构更强调的是“我有什么”,是一种组合优先的编程模式。使用组合而非继承,会使你的代码更具灵活性。

比如英雄联盟游戏的玩法,我们会构建出一个英雄的Entity实体类,它更像一个空盒子,可以在创建英雄Entity实例的时候赋予它一个ID作为唯一标识。当我们将这个实体放到world下,也许什么也看不见,什么也做不了,这是因为它现在还什么数据都没有。此时就需要根据游戏的需求,来设计出不同的组件填充到这个实体当中。注意,应尽可能地保证组件设计上的扁平化,会让你的模块结构更加清晰,也大大增加了CPU缓存命中的概率。

举个例子,常见的组件包括而不仅限于:

渲染组件 :英雄的顶点、材质等数据,保证我们能正确地渲染到world中
位置组件 :记录着实体在这个world的真实位置
特效组件 :不同的时机,可能会需要播放不同的粒子特效以增强视觉感受
技能组件 :角色技能

此外,根据策划的各种奇葩需求,还可以衍生出不同的功能性组件,本质上都是数据的集合,之后会交由System来进行各种状态修改逻辑计算。比如,想要一个英雄既能变成汽车又能变成飞机,我们可以设计出 WheelWing 两个组件,存储数据的同时也表明不同实体的对应功能或身份。当然对应着的是处理该组件的System,一个 FlightSystem 可以去关注那些持有 Wing 的实体。确切点说,FlightSystem 其实只需要关注 Wing 组件就足够了,它不应该关心是哪个实体持有这个组件,只要能修改 Wing 的状态就足矣。就实现了我们经常说的解耦

将复杂的游戏拆解成不同的逻辑处理单元 (System) ,而每个逻辑处理单元只关心那些向它注册监听的数据,其他数据一概不管。并且最主要的是,System 是不保存状态的Component 才是状态的真正持有者。随着时间的推移需求也会大量增加,组件持有的数据会过于复杂。可以将一个复杂的模块拆解成若干个相对简单的单元

ECS优缺点

  • 性能优化,因为数据都被统一存放到Component中,所以如果能够在内存中以合理的方式将所有Component聚合到连续的内存中,这样可以大幅度提升cpu cache命中率,游戏对象越多,性能提升越明显。
  • 扩展性强,一个实体可以随意增减组件,一个或多个组件中有绑定的系统
  • 面向数据编程,数据都放在了Component中

基于ECS的游戏架构

在《守望先锋》分享所知,它们游戏中光 System 就上百个。

然而,很多System关心的组件只有一个(如输入事件),于是就有了Singleton Component

ECS也并非尽善尽美,随着系统不断地开发与扩展,会发现难免有时 System 要处理的数据过于复杂,如果一味细化 System 的种类,虽能保证模块与组件数据之间的解耦,但却无形中增加了 System 的维护成本,或者将组件相互关联,这就使得组件不是扁平化管理了。其实架构是为人服务的,在理解ECS架构后,不同的项目可以在ECS基础之上进行修改也是可以的。

示例demo

using System.Collections;
using System.Collections.Generic;

// 实体
public class Entity
{
    private Dictionary<Type, object> components = new Dictionary<Type, object>();

    public void Add<T>(T component)
    {
        components[typeof(T)] = component;
    }

    public T Get<T>()
    {
        return (T)components[typeof(T)];
    }

}

// 组件
public struct PositionComponent
{
    public float X;
    public float Y;
}

public struct MovementComponent
{
    public float Speed;
}

public struct RenderComponent
{
    public string Sprite;
}

// 系统
using System.Collections;
using System.Collections.Generic;

public class PlayerMovementSystem
{
    public void Update(Entity entity)
    {
        // 获取实体的组件
        var position = entity.Get<PositionComponent>();
        var movement = entity.Get<MovementComponent>();

        // 处理玩家的输入,并更新位置
        // 这里只是一个示例,实际游戏中需要根据具体输入处理逻辑
        if (Input.GetKey(KeyCode.W))
        {
            position.Y += movement.Speed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.S))
        {
            position.Y -= movement.Speed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.A))
        {
            position.X -= movement.Speed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.D))
        {
            position.X += movement.Speed * Time.deltaTime;
        }
    }
}

public class EnemyMovementSystem
{
    public void Update(Entity entity)
    {
        // 获取实体的组件
        var position = entity.Get<PositionComponent>();
        var movement = entity.Get<MovementComponent>();

        // 简单示例:随机移动敌人
        var randomX = UnityEngine.Random.Range(-1f, 1f);
        var randomY = UnityEngine.Random.Range(-1f, 1f);
        position.X += movement.Speed * randomX * Time.deltaTime;
        position.Y += movement.Speed * randomY * Time.deltaTime;
    }
}

// 游戏世界
using System.Collections;
using System.Collections.Generic;

public class Word
{
    private List<Entity> entities = new List<Entity>();

    public void AddEntity(Entity entity)
    {
        entities.Add(entity);
    }

    public void Update()
    {
        foreach (var entity in entities)
        {
            if (entity.Has<PlayerMovementSystem>())
            {
                var playerSystem = entity.Get<PlayerMovementSystem>();
                playerSystem.Update(entity);
            }
            else if (entity.Has<EnemyMovementSystem>())
            {
                var enemySystem = entity.Get<EnemyMovementSystem>();
                enemySystem.Update(entity);
            }
        }

        // 渲染实体,这里只是一个示例
        // 实际游戏中可能需要使用更高级的渲染系统
        foreach (var entity in entities)
        {
            var position = entity.Get<PositionComponent>();
            var render = entity.Get<RenderComponent>();
            UnityEngine.Debug.Log($"Rendering entity with sprite: {render.Sprite} at ({position.X}, {position.Y})");
        }
    }
}

扩展阅读

ECS架构优缺点 ecs框架思想
实体组件系统(ECS)

此处评论已关闭