Entitas|01 基础介绍
下载与安装
Release Entitas 1.13.0 · sschmid/Entitas-CSharp · GitHub 下载最新的ECS安装包
解压到Asset目录
随便打开一个脚本,会自动编译Unity项目
然后又如下设置
首先点击Auto Import
然后把Project Path改为自己的项目名称,一般来说就是这个
现在可以点击生成了,会根据Contexts的内容自动生成上下文
默认就两个Game和Input
-
Attribute:一个标签
比如有GameAttribute之后,我们就可以在代码里这么写
这是一个标准组件的写法
[Game] public class PlayerName : IComponent { public string name; }
-
ComponentsLookUp:会存放对应上下文里的所有组件,这个类是用来作为查找工具的
可以看看源码
public static class GameComponentsLookup { public const int TotalComponents = 0; public static readonly string[] componentNames = {}; public static readonly System.Type[] componentTypes = {}; }
目前啥也没有,组件数量为0,因为我们确实没有写任何组件
-
Context:上下文,是一个分布类
-
Entity:游戏实体,任何运行在游戏里的数据都可以算作是一个实体,这是一个分布类
-
Matcher:匹配器,用来筛选Entity的,这是一个分布类
Hello World
创建一个Log组件
[Game]
public class LogComponent : IComponent
{
public string msg;
}
这是一个标准组件写法,组件在ECS里,只有信息,而没有逻辑
同时标签[Game]说明,这个组件位于GameContext下
注意写完后需要Generate,不然不会自动生成代码
创建一个系统
ECS有很多种系统,这里选择继承ReactiveSystem,代表反应系统,意思就是当你关心的实体产生变换时,这个系统会有反应
哪些实体是我们关心的呢?
可以发现这是一个泛型类,而泛型填入我们所需要关心的实体类型,这里关心的是GameEntity
public class LogSystem : ReactiveSystem<GameEntity>
{
public LogSystem(IContext<GameEntity> context) : base(context)
{
}
public LogSystem(ICollector<GameEntity> collector) : base(collector)
{
}
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
throw new System.NotImplementedException();
}
protected override bool Filter(GameEntity entity)
{
throw new System.NotImplementedException();
}
protected override void Execute(List<GameEntity> entities)
{
throw new System.NotImplementedException();
}
}
最上面是两个构造方法,我们不关心,主要是下面三个方法
-
GetTrigger:可以认为是初步筛选,因为Game上下文内有非常多种类的GameEntity,我们这个系统不可能关心所有的实体,这一步就是收集我们关心的实体
public class LogSystem : ReactiveSystem<GameEntity> { protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context) { return context.CreateCollector(GameMatcher.Log); } }
注意,必须生成过代码,才有GameMatcher.Log
-
Filter:过滤器,可以认为是再次筛选,因为可能又很多的Log实体,我们还可以对他们再次细分
我们这里的要求很简单,就只要拥有log组件就可以了
hasLog方法是自动生成的,等于hasLogComponent,用于判读实体是否拥有Log组件
public class LogSystem : ReactiveSystem<GameEntity> { protected override bool Filter(GameEntity entity) { return entity.hasLog; } }
-
Execute:真正的执行逻辑,传入一个数组,这个数组里是所有符合上述两步筛选的Entity
public class LogSystem : ReactiveSystem<GameEntity> { protected override void Execute(List<GameEntity> entities) { foreach (var entity in entities) { Debug.Log(entity.log.msg); } } }
初始化系统
在构造方法里获取到Game上下文,并在初始化的时候创建一个实体,并给实体添加一个Log组件
public class InitializeSystem : IInitializeSystem
{
public GameContext context;
public InitializeSystem(Contexts contexts)
{
context = contexts.game;
}
public void Initialize()
{
context.CreateEntity().AddLog("hello world");
}
}
创建一个功能
一个功能,比如战斗功能,可能有很多系统,比如攻击啊,收集啊,连击啊,等等
所以一个功能会包含多个 系统,我们可以以功能划分系统
当然这个功能我们只添加了一个初始化系统和Log系统
public class LogFeature : Feature
{
public LogFeature(Contexts contexts): base("LogFeature")
{
Add(new InitializeSystem(contexts));
Add(new LogSystem(contexts));
}
}
功能入口
public class Test : MonoBehaviour
{
private Systems systems;
void Start()
{
var context = Contexts.sharedInstance;
systems = new Feature("Systems").Add(new LogFeature(context));
systems.Initialize();
}
void Update()
{
systems.Execute();
systems.Cleanup();
}
}
systems相当于所有Feature管理器
我们先创建一个总的管理器,然后添加了一个LogFeature
在LogFeature里又有2个子系统
然后调用总系统初始化,这一步会执行所有继承自InitializeSystem的Initialize方法
然后Update里去调用所有系统的Execute和CleanUp
此时运行,可以发现打印出了想要的结果
交互组件
实现如下需求
- 右键点击屏幕时,在当前位置生成一张图片
- 左键点击屏幕时,所有图片向此位置移动
Input上下文内的组件
[Input, Unique]
public class LeftMouseComponent :IComponent
{
}
[Input, Unique]
public class RightMouseComponent :IComponent
{
}
[Input]
public class MouseDownComponent :IComponent
{
public Vector2 position;
}
[Input]
public class MousePositionComponent :IComponent
{
public Vector2 position;
}
[Input]
public class MouseUpComponent :IComponent
{
public Vector2 position;
}
- [Unique]标签,限定此组件为全局唯一,并且可以通过InputContext.isLeftMouse = true来创建唯一Entity,通过InputContext.LeftMouseEntity访问
InputSystem
public class InputSystem :IInitializeSystem,IExecuteSystem
{
private InputContext context;
private InputEntity leftMouseEntity;
private InputEntity rightMouseEntity;
public InputSystem(Contexts contexts)
{
context = contexts.input;
}
public void Initialize()
{
context.isLeftMouse = true;
context.isRightMouse = true;
leftMouseEntity = context.leftMouseEntity;
rightMouseEntity = context.rightMouseEntity;
}
public void Execute()
{
Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
if (Input.GetMouseButtonDown(0))
leftMouseEntity.ReplaceMousePosition(mousePosition);
if (Input.GetMouseButtonDown(1))
rightMouseEntity.ReplaceMousePosition(mousePosition);
}
}
Game上下文内的组件
// 当前GameObject所在的位置
[Game]
public sealed class PositionComponent : IComponent
{
public Vector2 value;
}
// 当前GameObject朝向
[Game]
public class DirectionComponent : IComponent
{
public float value;
}
// GameObject显示的图片
[Game]
public class ViewComponent : IComponent
{
public GameObject gameObject;
}
// 显示图片的名称
[Game]
public class SpriteComponent : IComponent
{
public string name;
}
// GameObject是否是Mover的标志
[Game]
public class MoverComponent : IComponent
{
}
// 移动的目标
[Game]
public class MoveComponent : IComponent
{
public Vector2 target;
}
// 移动完成标志
[Game]
public class MoveCompleteComponent : IComponent
{
}
CreateMoverSystem
public class CreateMoverSystem : ReactiveSystem<InputEntity>
{
readonly GameContext gameContext;
public CreateMoverSystem(Contexts contexts) : base(contexts.input)
{
gameContext = contexts.game;
}
// 收集有RightMouse和MouseDown的InputEntity
protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)
{
return context.CreateCollector(InputMatcher.AllOf(InputMatcher.RightMouse, InputMatcher.MousePosition));
}
// 第二过滤,直接返回true也无所谓
protected override bool Filter(InputEntity entity)
{
return entity.hasMousePosition;
}
// 执行,每次按下右键,设置Mover标志,添加Position、Direction,并添加表现该Entity的图片名称
protected override void Execute(List<InputEntity> entities)
{
foreach (InputEntity e in entities)
{
GameEntity mover = gameContext.CreateEntity();
mover.isMover = true;
mover.AddPosition(e.mousePosition.position);
mover.AddDirection(Random.Range(0, 360));
mover.AddSprite("Sprite");
}
}
}
CommandMoveSystem
public class CommandMoveSystem : ReactiveSystem<InputEntity>
{
readonly IGroup<GameEntity> movers;
// 获取拥有Mover标志Entity的组
public CommandMoveSystem(Contexts contexts) : base(contexts.input)
{
movers = contexts.game.GetGroup(GameMatcher.AllOf(GameMatcher.Mover));
}
// 过滤左键点击,和右键点击那个System一样
protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)
{
return context.CreateCollector(InputMatcher.AllOf(InputMatcher.LeftMouse, InputMatcher.MousePosition));
}
protected override bool Filter(InputEntity entity)
{
return entity.hasMousePosition;
}
// 在Entity上设置移动命令Move
protected override void Execute(List<InputEntity> entities)
{
foreach (InputEntity e in entities)
{
GameEntity[] movers = this.movers.GetEntities();
foreach (GameEntity entity in movers)
entity.ReplaceMove(e.mousePosition.position);
}
}
}
AddViewSystem
public class AddViewSystem : ReactiveSystem<GameEntity>
{
// 为了好看,所有ViewGameObject都放在该父节点下
readonly Transform viewContainer = new GameObject("Game Views").transform;
readonly GameContext context;
public AddViewSystem(Contexts contexts) : base(contexts.game)
{
context = contexts.game;
}
// 创建Sprite的过滤器
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Sprite);
}
// 第二次过滤,没有View,没有关联上GameObject的情况
protected override bool Filter(GameEntity entity)
{
return entity.hasSprite && !entity.hasView;
}
// 创建一个View的GameObject,并进行关联
protected override void Execute(List<GameEntity> entities)
{
foreach (GameEntity e in entities)
{
GameObject go = new GameObject("Game View");
go.transform.SetParent(viewContainer, false);
e.AddView(go); // Entity关联GameObject
go.Link(e); // GameObject关联Entity
}
}
}
RenderSpriteSystem
public class RenderSpriteSystem : ReactiveSystem<GameEntity>
{
public RenderSpriteSystem(Contexts contexts) : base(contexts.game)
{
}
// 过滤拥有Sprite的Entity
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Sprite);
}
protected override bool Filter(GameEntity entity)
{
return entity.hasSprite && entity.hasView;
}
// 在这里的时候Entity已经创建了关联的节点,所以只要添加Sprite的渲染就OK了。
// 所以当然也要注意,在添加程序组的时候要先添加AddViewSystem,在添加该System。
// 不然GameObject都没有创建就执行该代码肯定报错的。
protected override void Execute(List<GameEntity> entities)
{
foreach (GameEntity e in entities)
{
GameObject go = e.view.gameObject;
// 先获取SpriteRenderer组件,没有获取到再添加,大家还记得只要改变Sprite的内容就会执行这边的代码吧?
SpriteRenderer sr = go.GetComponent<SpriteRenderer>();
if (sr == null) sr = go.AddComponent<SpriteRenderer>();
sr.sprite = Resources.Load<Sprite>(e.sprite.name);
}
}
}
RenderPositionSystem
public class RenderPositionSystem : ReactiveSystem<GameEntity>
{
public RenderPositionSystem(Contexts contexts) : base(contexts.game)
{
}
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Position);
}
protected override bool Filter(GameEntity entity)
{
return entity.hasPosition && entity.hasView;
}
protected override void Execute(List<GameEntity> entities)
{
foreach (GameEntity e in entities)
{
e.view.gameObject.transform.position = e.position.value;
}
}
}
RenderDirectionSystem
public class RenderDirectionSystem : ReactiveSystem<GameEntity>
{
readonly GameContext context;
public RenderDirectionSystem(Contexts contexts) : base(contexts.game)
{
context = contexts.game;
}
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Direction);
}
protected override bool Filter(GameEntity entity)
{
return entity.hasDirection && entity.hasView;
}
protected override void Execute(List<GameEntity> entities)
{
foreach (GameEntity e in entities)
{
float ang = e.direction.value;
e.view.gameObject.transform.rotation = Quaternion.AngleAxis(ang - 90, Vector3.forward);
}
}
}
MoveSystem
public class MoveSystem : IExecuteSystem, ICleanupSystem
{
readonly IGroup<GameEntity> moves;
readonly IGroup<GameEntity> moveCompletes;
const float speed = 4f;
// 获取有移动目标Move组和完成移动MoveComplete组
public MoveSystem(Contexts contexts)
{
moves = contexts.game.GetGroup(GameMatcher.Move);
moveCompletes = contexts.game.GetGroup(GameMatcher.MoveComplete);
}
// 拥有目标的Mover每帧执行
public void Execute()
{
foreach (GameEntity e in moves.GetEntities())
{
// 计算下一个GameObject的位置,并替换
Vector2 dir = e.move.target - e.position.value;
Vector2 newPosition = e.position.value + dir.normalized * speed * Time.deltaTime;
e.ReplacePosition(newPosition);
// 计算下一个方向
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
e.ReplaceDirection(angle);
// 如果距离在0.5f之内,则判断为移动完成,移除Move命令,并添加移动完成标志
float dist = dir.magnitude;
if (dist <= 0.5f)
{
e.RemoveMove();
e.isMoveComplete = true;
}
}
}
// 清除所有MoveComplete,MoveComplete暂时没有作用
public void Cleanup()
{
foreach (GameEntity e in moveCompletes.GetEntities())
{
e.isMoveComplete = false;
}
}
}
GameFeature
public class GameFeature : Feature
{
public GameFeature(Contexts contexts) : base("GameFeature")
{
Add(new AddViewSystem(contexts));
Add(new RenderSpriteSystem(contexts));
Add(new RenderPositionSystem(contexts));
Add(new RenderDirectionSystem(contexts));
}
}
需要注意的是,ViewSystem和SpriteSystem同时监听Sprite组件
但ViewSystem的过滤器是这样的
protected override bool Filter(GameEntity entity)
{
return entity.hasSprite && !entity.hasView;
}
而SpriteSystem的过滤器是这样的
protected override bool Filter(GameEntity entity)
{
return entity.hasSprite && entity.hasView;
}
当执行SpriteSystem时,必须已经拥有view了,如果反过来那么永远不会触发SpriteSystem
因此,SpriteSystem必须后于ViewSystem添加
同理Position和Direction也是如此
InputFeature
public class InputFeature : Feature
{
public InputFeature(Contexts contexts) : base("InputFeature")
{
Add(new InputSystem(contexts));
Add(new CreateMoverSystem(contexts));
Add(new CommandMoveSystem(contexts));
}
}
运行入口
public class Enter : MonoBehaviour
{
public Systems systems;
private void Start()
{
var contexts = Contexts.sharedInstance;
systems = new Feature("Systems")
.Add(new MoveSystem(contexts))
.Add(new GameFeature(contexts))
.Add(new InputFeature(contexts));
systems.Initialize();
}
private void Update()
{
systems.Execute();
systems.Cleanup();
}
}
运行到指定位置时切换图片
public class ChangeSpriteSystem : ReactiveSystem<GameEntity>
{
public ChangeSpriteSystem(Contexts contexts) : base(contexts.game)
{
}
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.MoveComplete);
}
protected override bool Filter(GameEntity entity)
{
return entity.isMoveComplete && entity.isMover;
}
protected override void Execute(List<GameEntity> entities)
{
foreach (var entity in entities)
{
entity.ReplaceSprite("Sprite2");
}
}
}
ReactiveSystem解析
创建如下简单组件和系统
[Game]
public class DebugComponent : IComponent
{
public string msg;
}
public class InputSystem : IExecuteSystem
{
private Contexts contexts;
private GameEntity entity;
private int count = 0;
public InputSystem(Contexts contexts)
{
this.contexts = contexts;
}
public void Execute()
{
if (Input.GetMouseButtonDown(0))
{
count++;
entity ??= contexts.game.CreateEntity();
entity.ReplaceDebug($"左键点击了{count}次");
}
}
}
public class DebugSystem : ReactiveSystem<GameEntity>
{
public DebugSystem(Contexts contexts) : base(contexts.game)
{
}
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Debug);
}
protected override bool Filter(GameEntity entity)
{
return true;
}
protected override void Execute(List<GameEntity> entities)
{
foreach (var entity in entities)
{
Debug.Log(entity.debug.msg);
}
}
}
很简单,就是当我们左键鼠标时会答应日志
GetTrigger
很容易发现GetTrigger是在父类的构造函数中执行的
public abstract class ReactiveSystem<TEntity> : IReactiveSystem, IExecuteSystem, ISystem
where TEntity : class, IEntity
{
private readonly ICollector<TEntity> _collector;
protected ReactiveSystem(IContext<TEntity> context)
{
this._collector = this.GetTrigger(context);
}
}
这里的TEntity泛型我们很明显能知道应该是GameEntity(或者其他上下文)
推测一下,这个ICollector<GameEntiry>应该就是某一类GameEntity的收集
继续看GetTrigger方法,这个方法被我们重写了,看方法名,很明显就是创建了某一个Collector
public class DebugSystem : ReactiveSystem<GameEntity>
{
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Debug);
}
}
GameMachter
我们先观察这个参数,GameMatcher.Debug
public sealed partial class GameMatcher {
static IMatcher<GameEntity> _matcherDebug;
public static IMatcher<GameEntity> Debug
{
get {
if (_matcherDebug == null)
{
var matcher = (Matcher<GameEntity>)Matcher<GameEntity>.AllOf(GameComponentsLookup.Debug);
matcher.componentNames = GameComponentsLookup.componentNames;
_matcherDebug = matcher;
}
return _matcherDebug;
}
}
}
通过一个静态方法创建
public class Matcher
{
private int[] _allOfIndices;
public static IAllOfMatcher<TEntity> AllOf(params int[] indices) => (IAllOfMatcher<TEntity>) new Matcher<TEntity>()
{
_allOfIndices = Matcher<TEntity>.distinctIndices((IList<int>) indices)
};
}
这个方法的参数就是组件的ID,可以是一个数组
这些内容是自动生成的
public static class GameComponentsLookup {
public const int Asset = 0;
public const int Board = 1;
public const int Debug = 2; // 当前的组件
public const int GameItem = 3;
public const int Positon = 4;
public const int View = 5;
public const int TotalComponents = 6;
public static readonly string[] componentNames = {
"Asset",
"Board",
"Debug",
"GameItem",
"Positon",
"View"
};
public static readonly System.Type[] componentTypes = {
typeof(AssetComponent),
typeof(BoardComponent),
typeof(DebugComponent),
typeof(GameItemComponent),
typeof(PositonComponent),
typeof(ViewComponent)
};
}
并且在这里,IMatcher和IAllOfMatcher都是一个接口而已,Matcher类实现了这两个接口,所以可以转换
这个方法其实很简单,直接返回了一个new Matcher<GameEntity>(),并修改了属性_allOfIndices
这只是一个数组而已,继续看方法
public class Matcher
{
private static readonly HashSet<int> _indexSetBuffer = new HashSet<int>();
private static int[] distinctIndices(IList<int> indices)
{
foreach (int index in (IEnumerable<int>) indices)
Matcher<TEntity>._indexSetBuffer.Add(index);
int[] array = new int[Matcher<TEntity>._indexSetBuffer.Count];
Matcher<TEntity>._indexSetBuffer.CopyTo(array);
Array.Sort<int>(array);
Matcher<TEntity>._indexSetBuffer.Clear();
return array;
}
}
这个操作就很简单了,输入的一个组件ID数组,这一步的目的就是给这个数组去重,然后排序,并返回
Collector
了解了参数GameMatcher,我们继续看GetTrigger方法
这一部分是个扩展方法
public static class CollectorContextExtension
{
public static ICollector<TEntity> CreateCollector<TEntity>(this IContext<TEntity> context,IMatcher<TEntity> matcher)
{
return context.CreateCollector<TEntity>(new TriggerOnEvent<TEntity>(matcher, GroupEvent.Added));
}
}
TriggerOnEvent是一个简单的封装类,GroupEvent只是一个枚举类型而已
public enum GroupEvent : byte
{
Added,
Removed,
AddedOrRemoved,
}
public struct TriggerOnEvent<TEntity> where TEntity : class, IEntity
{
public readonly IMatcher<TEntity> matcher;
public readonly GroupEvent groupEvent;
public TriggerOnEvent(IMatcher<TEntity> matcher, GroupEvent groupEvent)
{
this.matcher = matcher;
this.groupEvent = groupEvent;
}
}
再继续看创建方法
public static class CollectorContextExtension
{
public static ICollector<TEntity> CreateCollector<TEntity>(this IContext<TEntity> context,
params TriggerOnEvent<TEntity>[] triggers)
{
IGroup<TEntity>[] groups = new IGroup<TEntity>[triggers.Length];
GroupEvent[] groupEvents = new GroupEvent[triggers.Length];
for (int index = 0; index < triggers.Length; ++index)
{
groups[index] = context.GetGroup(triggers[index].matcher);
groupEvents[index] = triggers[index].groupEvent;
}
return (ICollector<TEntity>) new Collector<TEntity>(groups, groupEvents);
}
}
这里的长度其实都是1
先看GetGroup方法, 这个方法属于context,在这里我们显然应该去GameContext里找
public sealed partial class GameContext : Entitas.Context<GameEntity>
{
private readonly Dictionary<IMatcher<TEntity>, IGroup<TEntity>> _groups
= new Dictionary<IMatcher<TEntity>, IGroup<TEntity>>();
public IGroup<TEntity> GetGroup(IMatcher<TEntity> matcher)
{
IGroup<TEntity> group;
if (!this._groups.TryGetValue(matcher, out group))
{
group = (IGroup<TEntity>) new Group<TEntity>(matcher);
foreach (TEntity entity in this.GetEntities())
group.HandleEntitySilently(entity);
this._groups.Add(matcher, group);
for (int index1 = 0; index1 < matcher.indices.Length; ++index1)
{
int index2 = matcher.indices[index1];
if (this._groupsForIndex[index2] == null)
this._groupsForIndex[index2] = new List<IGroup<TEntity>>();
this._groupsForIndex[index2].Add(group);
}
if (this.OnGroupCreated != null)
this.OnGroupCreated((IContext) this, (IGroup) group);
}
return group;
}
}
看到这里我们应该很明白了
首先Mather只是一个Key,如果我们有两个组件,Debug和Input,那么就会存在3种Mather
Mather<GameEntity>{ Debug }
Mather<GameEntity>{ Input }
Mather<GameEntity>{ Debug,Input }
此时_gourps里就存在了3种分类,而我们可以根据传入的mather,直接获取对应的group,group里存放这所有符合条件的GameEntity
最后再看Collector的构造函数
public class Collector
{
private GroupChanged<TEntity> _addEntityCache;
public Collector(IGroup<TEntity>[] groups, GroupEvent[] groupEvents)
{
this._groups = groups;
this._groupEvents = groupEvents;
this._addEntityCache = new GroupChanged<TEntity>(this.addEntity);
this.Activate();
}
public void Activate()
{
for (int index = 0; index < this._groups.Length; ++index)
{
IGroup<TEntity> group = this._groups[index];
switch (this._groupEvents[index])
{
case GroupEvent.Added:
group.OnEntityAdded -= this._addEntityCache;
group.OnEntityAdded += this._addEntityCache;
break;
case GroupEvent.Removed:
group.OnEntityRemoved -= this._addEntityCache;
group.OnEntityRemoved += this._addEntityCache;
break;
case GroupEvent.AddedOrRemoved:
group.OnEntityAdded -= this._addEntityCache;
group.OnEntityAdded += this._addEntityCache;
group.OnEntityRemoved -= this._addEntityCache;
group.OnEntityRemoved += this._addEntityCache;
break;
}
}
}
}
这里的关键作用就是当目标group发生变化时,执行相应操作了
其中GroupChanged是一个委托
public delegate void GroupChanged<TEntity>(
IGroup<TEntity> group,
TEntity entity,
int index,
IComponent component)