ECS Entitas源码分析(四)___Entity

Entity介绍

Entity是ECS框架中三大基本概念之一 :Entity(实体)。ECS中所有的操作对象都是它。它本身对象并不带有任何属性与表达含义,它只有一个在它所有Context中唯一的ID(_creationIndex)。它所有的属性与含义都是通过挂在它身上的组件(Component)来组合表达的。所以说Entity本身不包含数据,它只是数据的搬运工。它也不管理任何数据,不对Component上的数据做任何的运算,只是提供对Conponent的增、删、查、改等方法,以及在自身Component发生变化时,通知其他的系统(如:Context,Group)。

一、Entity初始化

我们在介绍Context的文章中说过,Entity的创建必须通过Context的CreateEntity()方法。在这个方法中我们会对Entity进行初始化。

//判断对象池是否有空闲对象
if (_reusableEntities.Count > 0) {
    entity = _reusableEntities.Pop();
    //如果有,则拿出来,同时赋予新的ID
    entity.Reactivate(_creationIndex++);
} else {
    //如果没有,则创建,并且初始化
    entity = _entityFactory();
    entity.Initialize(_creationIndex++, _totalComponents, _componentPools,_contextInfo, _aercFactory(entity));
}

在上面的代码中我们可以看到,我们通过Initialize(int creationIndex, int totalComponents, Stack<IComponent>[] componentPools, ContextInfo contextInfo = null, IAERC aerc = null)方法对Entity进行初始化。

creationIndex:当前Context中Entity的唯一ID。
totalComponents:当前Context中Component的类型数量。用于初始化Componet数组的长度。
componentPools:Component栈类型的数组,作为当前Context中的组件对象池,将Component的类型序号作为数组下标。
contextInfo:当前Context的基本信息。
aerc:Entity的引用计数对象。

public void Initialize(int creationIndex, int totalComponents, Stack<IComponent>[] componentPools, ContextInfo contextInfo = null, IAERC aerc = null) {
    //激活Entity,设置标识ID
    Reactivate(creationIndex);
 
    //初始化组件数组
    _totalComponents = totalComponents;
    _components = new IComponent[totalComponents];
    _componentPools = componentPools;

    _contextInfo = contextInfo ?? createDefaultContextInfo();
    _aerc = aerc ?? new SafeAERC(this);
二、Component的创建

创建Component时,我们根据Component在ComponentsLookup里生成的索引,先在_componentPools对象池中查看是否有空闲的Component,有则直接Pop()出来使用,没有则创建。所有的Entity都是共用一个Component对象池。

//创建Component
public IComponent CreateComponent(int index, Type type) {
    var componentPool = GetComponentPool(index);
    return componentPool.Count > 0
        ? componentPool.Pop()
        : (IComponent)Activator.CreateInstance(type);
}

public T CreateComponent<T>(int index) where T : new() {
    var componentPool = GetComponentPool(index);
    return componentPool.Count > 0 ? (T)componentPool.Pop() : new T();
}

根据Component索引获得对象池

public Stack<IComponent> GetComponentPool(int index) {
    var componentPool = _componentPools[index];
    if (componentPool == null) {
        componentPool = new Stack<IComponent>();
        _componentPools[index] = componentPool;
    }
    return componentPool;
}
三、Component的增删查改

Entity的Component都保存在_components数组中,_components的长度是所有Component类型数量的长度,也就是 ComponentsLookup.TotalComponents,这样我们就可以将Component的索引当作数组的下标,我们再查找Component或者判断是否有某个Component时,都可以通过下标快速判断。极大的提高查找效率。
当Entity的组件发生变化是时,通过代理方法统一的在Context中处理,减少了不同模块之间的耦合度。提高了代码的可读性。

1、添加组件

判断组件是否重复,给_components对应的下标赋值,同时通知组件被添加。

public void AddComponent(int index, IComponent component) {
    if (!_isEnabled) {
        throw new EntityIsNotEnabledException(
            "Cannot add component '" +
            _contextInfo.componentNames[index] + "' to " + this + "!"
        );
    }
    
    if (HasComponent(index)) {
        throw new EntityAlreadyHasComponentException(
            index, "Cannot add component '" +
            _contextInfo.componentNames[index] + "' to " + this + "!",
            "You should check if an entity already has the component " +
            "before adding it or use entity.ReplaceComponent()."
        );
    }
    
    _components[index] = component;
    _componentsCache = null;
    _componentIndicesCache = null;
    _toStringCache = null;
    
    if (OnComponentAdded != null) {
        OnComponentAdded(this, index, component);
    }
}

2、组件移除

public void RemoveComponent(int index) {
    if (!_isEnabled) {
        throw new EntityIsNotEnabledException(
            "Cannot remove component '" +
            _contextInfo.componentNames[index] + "' from " + this + "!"
        );
    }
    
     if (!HasComponent(index)) {
        throw new EntityDoesNotHaveComponentException(
            index, "Cannot remove component '" +
                   _contextInfo.componentNames[index] + "' from " + this + "!",
            "You should check if an entity has the component " +
            "before removing it."
        );
    }
    
     replaceComponent(index, null);
}

3、组件修改
主要是修改_components数组对应下标的值,如果新值为空,那么就相当于remove操作,那么就需要就旧的值Push到_componentPools对象池中。然后就是通知代理组件修改了。

public void ReplaceComponent(int index, IComponent component) {
    if (!_isEnabled) {
        throw new EntityIsNotEnabledException(
            "Cannot replace component '" +
            _contextInfo.componentNames[index] + "' on " + this + "!"
        );
    }
    if (HasComponent(index)) {
        replaceComponent(index, component);
    } else if (component != null) {
        AddComponent(index, component);
    }
}

void replaceComponent(int index, IComponent replacement) {
    var previousComponent = _components[index];
    if (replacement != previousComponent) {
        _components[index] = replacement;
        _componentsCache = null;
        if (replacement != null) {
            if (OnComponentReplaced != null) {
                OnComponentReplaced(
                    this, index, previousComponent, replacement
                );
            }
        } 
        else
        {
            _componentIndicesCache = null;
            // TODO VD PERFORMANCE
            _toStringCache = null;
            if (OnComponentRemoved != null) {
                OnComponentRemoved(this, index, previousComponent);
            }
        }
        GetComponentPool(index).Push(previousComponent);
   } 
   else
   {
     if (OnComponentReplaced != null) {
            OnComponentReplaced(
                this, index, previousComponent, replacement
            );
        }
    }
}

4、组件查询
之前有说过,组件的查询非常的简单高效,就是判断数组的下标。

public IComponent GetComponent(int index) {
    if (!HasComponent(index)) {
        throw new EntityDoesNotHaveComponentException(
            index, "Cannot get component '" +
                   _contextInfo.componentNames[index] + "' from " + this + "!",
            "You should check if an entity has the component " +
            "before getting it."
        );
    }
    return _components[index];
}

public bool HasComponent(int index) {
   return _components[index] != null;
}
四、Entity的删除

Entity的删除是调用Destroy()方法,但是在这个方法里并不是做真正的删除操作,它只是发送代理通知,真正的删除操作是InternalDestroy()方法,但是我们不需要自己这个方法,我们在外部调用Destroy()方法。

public void Destroy() {
    if (!_isEnabled) {
        throw new EntityIsNotEnabledException("Cannot destroy " + this + "!");
    }
    if (OnDestroyEntity != null) {
        OnDestroyEntity(this);
    }
}

public void InternalDestroy() {
    _isEnabled = false;
    RemoveAllComponents();
    OnComponentAdded = null;
    OnComponentReplaced = null;
    OnComponentRemoved = null;
    OnDestroyEntity = null;
}
五、代码生成器扩展Entity

当我们自定义了Component后,我们使用代码生成器生成代码,这是Entitas会为我们扩展Entity类。添加一些我们方便访问这个组件的方法。方便我们在Entity中直接使用。
例如我们定义一个AssetComponent组件如下:

using Entitas;
public class AssetComponent : IComponent
{
    public string assetName;
}

那么代码生成器就会为我们生成一个这样的扩展类:

public partial class GameEntity {
    public AssetComponent asset { get { return (AssetComponent)GetComponent(GameComponentsLookup.Asset); } }
    public bool hasAsset { get { return HasComponent(GameComponentsLookup.Asset); } }
    
    public void AddAsset(string newAssetName) {
        var index = GameComponentsLookup.Asset;
        var component = (AssetComponent)CreateComponent(index, typeof(AssetComponent));
        component.assetName = newAssetName;
        AddComponent(index, component);
    }
    
    public void ReplaceAsset(string newAssetName) {
        var index = GameComponentsLookup.Asset;
        var component = (AssetComponent)CreateComponent(index, typeof(AssetComponent));
        component.assetName = newAssetName;
        ReplaceComponent(index, component);
    }
    
    public void RemoveAsset() {
        RemoveComponent(GameComponentsLookup.Asset);
    }
}

我们就可以通过AddAssetReplaceAsset等方法直接添加、修改和判断对应的组件了。
同时还会根据我们给组件不同的标签,为我们生成不同特性的方法,使用起来十分的方便。

六、总结

Entity通过自身添加不同的Component表达不同的含义,所以它就是一个Component的载体,提供各种各样Component的操作方法。同时,在自身Component发生改变时,会通过代理方法将改变通知到Context中,然后在Context中与改变对应的Group,以达到快速分组的功能。

<上一篇>ECS Entitas源码分析(三)___Component

<下一篇>ECS Entitas源码分析(五)___System

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值