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
来进行各种状态修改
与逻辑计算
。比如,想要一个英雄既能变成汽车又能变成飞机,我们可以设计出 Wheel
和 Wing
两个组件,存储数据的同时也表明不同实体的对应功能或身份。当然对应着的是处理该组件的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})");
}
}
}
扩展阅读
最后更新于 2024-01-02 17:14:08 并被添加「」标签,已有 1300 位童鞋阅读过。
本站使用「署名 4.0 国际」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处
此处评论已关闭