[架构设计] Entity-Component-System (ECS)架构

介绍

ECS(Entity-Component-System)是一种游戏开发架构,它将游戏对象(Entity)分解成组件(Component)和系统(System),并在不同的数据集合中对它们进行处理。其中,组件是具有数据的对象,系统是根据组件来处理数据的对象,而实体是由组件构成的。

在ECS架构中,不同的组件包含不同的数据,系统只处理与其相关联的组件。这样,系统就能够高效地处理数据,而且可以轻松地添加和删除组件,从而灵活地管理游戏对象。

优缺点

ECS架构的优点:

  • 高性能:ECS可以利用数据布局、缓存友好性等特性来提高处理性能。
  • 可扩展性:ECS可以轻松地添加和删除组件,从而实现可扩展性。
  • 更容易的代码重用:ECS提供了组件和系统的分离,这样可以更容易地重用代码。
  • 更好的测试性:由于ECS中的系统和组件是分离的,所以可以更轻松地对它们进行单元测试。

ECS架构的缺点:

  • 学习曲线陡峭:ECS需要开发人员具备一定的编程技能和知识。
  • 可读性差:由于组件和系统分离,代码可能更加分散,可读性可能会降低。

总之,ECS是一种适用于大型游戏的高性能架构,可以提高游戏的可扩展性和性能。

实际应用

以游戏中的角色移动为例子来说明ECS的应用:

在ECS中,我们不再使用传统的面向对象模式,而是使用一种类似数据流的方式来处理游戏逻辑。在角色移动的场景中,我们可以将角色的数据分为三个部分:

  • 实体(Entity):实体是游戏对象的抽象,它仅仅是一个标识符,用于标识游戏中的一个对象。

  • 组件(Component):组件包含了实体的数据,例如角色的位置、速度、朝向等信息。

  • 系统(System):系统负责更新组件的状态,例如将角色的位置根据速度进行更新,处理输入等。

在ECS中,我们将实体看作一个空的容器,组件包含着实体的所有数据,系统根据组件的数据来进行逻辑处理。例如在角色移动的场景中,我们可以创建以下组件:

  • 位置组件(Position Component):包含角色的位置信息,例如x、y、z坐标等。

  • 移动组件(Movement Component):包含角色的速度、加速度、朝向等信息。

  • 输入组件(Input Component):包含角色的输入状态,例如玩家是否按下了方向键等。

在游戏运行时,系统会根据组件的数据来进行逻辑处理。例如位置系统会根据移动组件的数据来更新位置组件的数据,而输入系统则会根据玩家的输入来更新输入组件的数据。

使用ECS架构可以将游戏对象的数据分离出来,简化游戏逻辑的处理,提高游戏的性能和可维护性。

上代码

先来个简单的

以下是一个简单的 Unity ECS 示例,演示如何创建实体和组件,并在系统中使用这些组件进行操作:

首先,需要定义一个组件:


using Unity.Entities;

public struct Velocity : IComponentData
{
    
    
    public float Value;
}

然后,需要定义一个系统,这个系统会在每个帧上更新实体的速度:


using Unity.Entities;
using Unity.Transforms;

public class VelocitySystem : SystemBase
{
    
    
    protected override void OnUpdate()
    {
    
    
        float deltaTime = Time.DeltaTime;
        
        Entities.ForEach((ref Translation translation, in Velocity velocity) =>
        {
    
    
            translation.Value.y += velocity.Value * deltaTime;
        }).Schedule();
    }
}

这个系统依赖于两个组件:Translation 和 Velocity。在每个帧上,系统会遍历所有拥有这些组件的实体,并更新它们的速度。

最后,需要在场景中创建实体并添加组件:


using Unity.Entities;
using Unity.Transforms;

public class CreateEntity : MonoBehaviour
{
    
    
    EntityManager entityManager;

    void Start()
    {
    
    
        entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        
        Entity entity = entityManager.CreateEntity(typeof(Translation), typeof(Velocity));
        entityManager.SetComponentData(entity, new Translation {
    
     Value = new float3(0f, 0f, 0f) });
        entityManager.SetComponentData(entity, new Velocity {
    
     Value = 1f });
    }
}

在这个例子中,创建了一个实体,并添加了 Translation 和 Velocity 组件,其中 Velocity 的初始值为 1。这个实体将在每个帧上向上移动。

这是一个简单的 ECS 示例,它演示了如何使用 Unity ECS 创建实体和组件,并在系统中操作这些组件。在实际游戏中,可能会涉及到更复杂的组件和系统,但基本原理都是相同的。

再来个复杂一点的

个较为复杂的 ECS 代码示例,该代码实现了一个简单的飞机射击游戏。

首先,我们定义了一个组件类 Position,它表示游戏对象的位置:


public struct Position : IComponentData
{
    
    
    public float3 Value;
}

接下来,我们定义了一个组件类 Velocity,它表示游戏对象的速度:


public struct Velocity : IComponentData
{
    
    
    public float3 Value;
}

我们还定义了一个系统类 MovementSystem,它更新游戏对象的位置和速度:


[UpdateAfter(typeof(InputSystem))]
public class MovementSystem : SystemBase
{
    
    
    protected override void OnUpdate()
    {
    
    
        float deltaTime = Time.DeltaTime;

        Entities.ForEach((ref Position position, in Velocity velocity) =>
        {
    
    
            position.Value += velocity.Value * deltaTime;
        }).ScheduleParallel();
    }
}

在这个系统中,我们使用 Entities.ForEach 来循环遍历所有拥有 Position 和 Velocity 组件的游戏对象,并更新它们的位置。

接下来,我们定义了一个组件类 PlayerInput,它表示玩家的输入:


public struct PlayerInput : IComponentData
{
    
    
    public float2 Movement;
    public bool Fire;
}

我们还定义了一个系统类 InputSystem,它读取玩家的输入并更新 PlayerInput 组件:


public class InputSystem : SystemBase
{
    
    
    protected override void OnUpdate()
    {
    
    
        float2 movement = new float2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
        bool fire = Input.GetButton("Fire");

        Entities.ForEach((ref PlayerInput playerInput) =>
        {
    
    
            playerInput.Movement = movement;
            playerInput.Fire = fire;
        }).Schedule();
    }
}

在这个系统中,我们使用 Entities.ForEach 来循环遍历所有拥有 PlayerInput 组件的游戏对象,并更新它们的输入。

最后,我们定义了一个系统类 WeaponSystem,它根据玩家的输入来发射子弹:


public class WeaponSystem : SystemBase
{
    
    
    protected override void OnUpdate()
    {
    
    
        bool fire = false;

        Entities.ForEach((in PlayerInput playerInput) =>
        {
    
    
            fire = playerInput.Fire;
        }).WithoutBurst().Run();

        if (fire)
        {
    
    
            Entity bullet = EntityManager.Instantiate(prefabBullet);

            float3 position = EntityManager.GetComponentData<Position>(entityPlayer).Value;
            position.z += 1.0f;

            EntityManager.SetComponentData(bullet, new Position {
    
     Value = position });
            EntityManager.SetComponentData(bullet, new Velocity {
    
     Value = new float3(0.0f, 0.0f, 10.0f) });
        }
    }
}

在这个系统中,我们使用 Entities.ForEach 来循环遍历所有拥有 PlayerInput 组件的游戏对象,并根据输入来发射子弹。发射子弹时,我们使用 EntityManager.Instantiate 来实例化子弹预制件,并使用 EntityManager.SetComponentData 来设置它的位置和速度。

猜你喜欢

转载自blog.csdn.net/hhh314159/article/details/129277845