概要

Entity管理核心为EntityManager,Show和Hide接口会被lua层所调用。
其中Show的实现类似UI模块,首先检查对象池中是否有可用GameObject资源(EntityInstance),如果有,则使用之,并显示,否则先LoadAsset,加载完毕后注册于对象池,而后显示。

对象池在此处封装为EntityGroup,内部包含一个维护EntitiyInstance的对象池,指代GameObject对象资源,以及一个Entity链表,指代当前正在使用的GameObject。
此处对象池和其他模块的对象池类似,可以配置自动释放时间,大小等参数。

最终生成 的GameObject上会挂载一个EntityLogic + Entity

关于Preload的坑:
如果只是Show/Hide来实现Preload,Preload后的Entity可能会被自动释放掉。所以实际项目中禁用了自动释放相关的代码,完全手动管理。

实现上的代码细节

ShowEntity

直接进入EntityManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void ShowEntity(int entityId, string entityAssetName, string entityGroupName, int priority, object userData)
{
EntityInstanceObject entityInstanceObject = entityGroup.SpawnEntityInstanceObject(entityAssetName);
if (entityInstanceObject == null)
{
int serialId = ++m_Serial;
m_EntitiesBeingLoaded.Add(entityId, serialId);
m_ResourceManager.LoadAsset(entityAssetName, priority, m_LoadAssetCallbacks, ShowEntityInfo.Create(serialId, entityId, entityGroup, userData));
return;
}

InternalShowEntity(entityId, entityAssetName, entityGroup, entityInstanceObject.Target, false, 0f, userData);
}

这里的处理和UIManager十分相似:

对象池(EntityGroup)尝试获取EntityInstance

  • 如果成功,直接进入InternalShowEntity,显示之
  • 如果失败,就走LoadAsset逻辑,尝试加载对应资源
    加载成功会构造对应的EntityInstance,注册于对象池,然后再进入InternalShowEntity。
    Anyway,这里暂时吧资源加载放一边,进入InternalShowEntity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void InternalShowEntity(int entityId, string entityAssetName, EntityGroup entityGroup, object entityInstance, bool isNewInstance, float duration, object userData)
{
IEntity entity = m_EntityHelper.CreateEntity(entityInstance, entityGroup, userData);

EntityInfo entityInfo = EntityInfo.Create(entity);
m_EntityInfos.Add(entityId, entityInfo);
entityInfo.Status = EntityStatus.WillInit;
entity.OnInit(entityId, entityAssetName, entityGroup, isNewInstance, userData);
entityInfo.Status = EntityStatus.Inited;
entityGroup.AddEntity(entity);
entityInfo.Status = EntityStatus.WillShow;
entity.OnShow(userData);
entityInfo.Status = EntityStatus.Showed;
}

在entity.OnInit中:

1
m_EntityLogic = gameObject.AddComponent(entityLogicType) as EntityLogic;

这里会根据之前传入的Logic type来构造Component,这样就将对应的logic lua脚本挂到了Entity上。
另外,这里entity实际上是个Component脚本,并非GameObject,所以真正生成的gameObject上挂了两个脚本:

  • Entity
  • Logic

所以这里可能有点迷惑的地方就是这个Entity并非直接指代Unity中的GameObject,本体是GameObject上挂的一个c#脚本,其内部引用了EntityLogic,来执行真正的游戏业务逻辑。
之所以这么设计是为了方便对象池管理,后文提到。

对象管理

这里Entity精髓在于它利用了底层的通用对象池来管理Entity对象。
回到EntityManager,这里的EnttiyGroup可以看做一个对象池,不同的Group分别管理:

1
2
3
4
5
6
7
public void ShowEntity(int entityId, string entityAssetName, string entityGroupName, int priority, object userData)
{
EntityGroup entityGroup = (EntityGroup)GetEntityGroup(entityGroupName);
......
EntityInstanceObject entityInstanceObject = entityGroup.SpawnEntityInstanceObject(entityAssetName);
......
}

实际上对于EntityGroup,内部维护了两个东西,

  • m_InstancePool :对象池,这个运行时的类型是ObjectPoolManager.ObjectPool,EntityInstanceObject内部有个Target指向真正的GameObject
  • m_Entities : 当前使用的Entity链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private sealed class EntityGroup : IEntityGroup
{
private readonly IObjectPool<EntityInstanceObject> m_InstancePool;
private readonly GameFrameworkLinkedList<IEntity> m_Entities;

//LoadAsset时被触发
public void RegisterEntityInstanceObject(EntityInstanceObject obj, bool spawned)
{
m_InstancePool.Register(obj, spawned);
}

//show时被触发
public void AddEntity(IEntity entity)
{
m_Entities.AddLast(entity);
}
}

可以这么理解:

  • EntityInstance指代了真正的GameObject,可以看做真正的资源,这个资源被对象池自动管理。
  • Entity则可以看做当前正在使用的GameObject。

举个栗子:
如果hide了,Entity是会被从这个链表中移除的,如果又show了一个,可能依然会用对象池中已经有的EntityInstance来造一个Entity。
而正是因为Entity会被经常移入移除,所以用链表维护。

此外,如果lua设置hide某个Entity了,由于对象池内部机制,时间超过了AutoReleaseInterval之后,EntityInstance是会被自动释放的。

一些问题的进一步讨论

1.同一帧show/hide会有什么问题

TL;DR

Entity模块已经考虑到了这种情况,对于正在加载的Entity会打个特殊标记,加载完毕如果发现已经失效,就正常释放。

此外,hide需要用CClientEntityMgr:HideEntityWithId(entityId)接口,CClientEntityMgr:HideEntity(entityLogic)的问题后文讨论

代码细节

这里分两种情况:

show的时候对象池中没有资源,需要新加载
对象池中已经preload了资源,可以直接show

1.show的时候对象池中没有资源,需要新加载

依然是正常进入ShowEntity,发现没有资源,然后异步加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void ShowEntity(int entityId, string entityAssetName, string entityGroupName, int priority, object userData)
{
EntityInstanceObject entityInstanceObject = entityGroup.SpawnEntityInstanceObject(entityAssetName);
if (entityInstanceObject == null)
{
......
int serialId = ++m_Serial;
//注意这一句
m_EntitiesBeingLoaded.Add(entityId, serialId);
m_ResourceManager.LoadAsset(entityAssetName, priority, m_LoadAssetCallbacks, ShowEntityInfo.Create(serialId, entityId, entityGroup, userData));
return;
}
public bool IsLoadingEntity(int entityId)
{
return m_EntitiesBeingLoaded.ContainsKey(entityId);
}
}

这里会对正在异步加载的Entity放到一个叫m_EntitieseBeingLoadedDict中,而判断某个Entity是否在异步加载就是通过这个来判断IsLoadingEntity。

不过接着立即调用了hide,此时如果发现这个Entity只是一个正在被加载的Entity,就将其从Dict中移除,然后加入m_EntitiesToReleaseOnLoad这么个HashSet中,表示加载完成就Release之,不会走真正的Hide逻辑:

1
2
3
4
5
6
7
8
9
10
11
public void HideEntity(int entityId, object userData)
{
if (IsLoadingEntity(entityId))
{
m_EntitiesToReleaseOnLoad.Add(m_EntitiesBeingLoaded[entityId]);
m_EntitiesBeingLoaded.Remove(entityId);
return;
}
.....
InternalHideEntity(entityInfo, userData);
}

然后异步加载完成了,如果发现这个Entity实际上并不需要,就直接干掉,不会走InternalShow逻辑:

1
2
3
4
5
6
7
8
9
10
private void LoadAssetSuccessCallback(string entityAssetName, object entityAsset, float duration, object userData)
{
if (m_EntitiesToReleaseOnLoad.Contains(showEntityInfo.SerialId))
{
m_EntitiesToReleaseOnLoad.Remove(showEntityInfo.SerialId);
m_EntityHelper.ReleaseEntity(entityAsset, null);
return;
}
...
}

2.对象池中已经preload了资源,可以直接show

这种就比较简单了,因为不走异步加载资源逻辑,正常show/hide即可

2. 如何Preload

TODO