本文主题:ECS框架的启动和循环,System的OnUpdate函数在哪里被调用,运行时disable一个System
先上个简约版的类图:
World:保存着一个EntityManager和一堆ComponentSystem,默认情况下游戏启动时会为我们创建一个World并通过反射把所有的ComponentSystem类加进去。你要定义多个World也可以,像守望先锋那样有个普通游戏时的World,被打死后的回放就用另外的World处理。
Entity:只有id和version的小结构体
IComponentData:空接口而已,表明是个ECS的组件
ComponentSystem:继承该类后,用Inject特性标明关注的IComponentData,重写OnUpdate方法就可以访问到所有Entity上的IComponentData了,当然你可以设置更加复杂的关系,比如需要有A组件且没有B组件:
public struct SampleGroup
{
public ComponentDataArray Acs;
public SubtractiveComponent Bcs;
public int Length;
}
甚至你还能指定某组件的值为x的集合:详细例子见:component特殊指定说明
ComponentGroup:在ComponentSystem里会针对大部分类型的Inject对象(比如上面的SampleGroup结构体)生成一个ComponentGroup,其包含了system所关注的组件信息。
关于ECS的源码,不需要反编译,从Unity菜单Window->Package Window里下载了Entities后就可以从以下目录找到:
C:\Users\Administrator\AppData\Local\Unity\cache\packages\packages.unity.com
为了调试方便,可以复制到项目的Packages目录里并把"com.unity.entities": "0.0.12-preview.19",修改为"com.unity.entities": "file:./com.unity.entities",这样就可以直接修改entities代码在Unity上调试了。
我们大致可以猜到会有个地方调用整个系统的初始化函数的,然后new一个默认的World把所有我们定义的System类注册进去,接着在主循环里每帧调用所有System的OnUpdate,所以我对着World类的构造函数右键-》查看所有引用就找到线索了。
具体细节:
AutomaticWorldBootstrap.Initialize(一般模式)或GameObjectEntity.OnEnable(Editor启动)->
DefaultWorldInitialization.Initialize->
public static void Initialize(string worldName, bool editorWorld)
{
var world = new World(worldName);
World.Active = world;
IEnumerable allTypes;
foreach (var ass in AppDomain.CurrentDomain.GetAssemblies())
{
allTypes = ass.GetTypes();
CreateBehaviourManagersForMatchingTypes(editorWorld, allTypes, world);
}
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(world);
}
static void CreateBehaviourManagersForMatchingTypes(bool editorWorld, IEnumerable allTypes, World world)
{
var systemTypes = allTypes.Where(t => t.IsSubclassOf(typeof(ComponentSystemBase)));
foreach (var type in systemTypes)
{
world.GetOrCreateManager(type);
}
}
为突出重点,删掉了不少代码,详细还是去看原文件的好。
上面的代码主要就是new一个World类并用AppDomain.CurrentDomain.GetAssemblies()遍历所有类,把继承自ComponentSystemBase的类都用World.Active.GetOrCreateManager(type)加进去。 ->
ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.Active)->
在这里给所有的System根据UpdateAfter、UpdateBefore和UpdateInGroup三种特性、是否支持多线程和读写权限等约束给System们排好序(这个排序逻辑也够写另外一章了),然后就按这个顺序调用所有ScriptBehaviourManager的Update方法,在调用我们定义System的OnUpdate方法前,ComponentSystem:UpdateInjectedComponentGroups就为我们“注入”该System需要用到的ComponentData,然后就可以愉快地玩耍了。
由于默认情况下World都会把我们所有的System给加进去,那么如果我们想运行时控制某个系统不执行应该怎么弄呢?我们只好从ComponentSystem和其父类的代码去找了,ComponentSystem调用Update前做了哪些操作:
internal sealed override void InternalUpdate()
{
if (Enabled && ShouldRunSystem())
{
if (!m_PreviouslyEnabled)
{
m_PreviouslyEnabled = true;
OnStartRunning();
}
BeforeOnUpdate();
try
{
OnUpdate();
}
finally
{
AfterOnUpdate();
}
}
else if (m_PreviouslyEnabled)
{
m_PreviouslyEnabled = false;
OnStopRunning();
}
}
第三行我们就看到Enabled字段了,在上面的类图里我们知道ComponentSystem也是继承自ScriptBehaviourManager的,所以我们可以通过World获取该System并disable掉它:
World.Active.GetOrCreateManager
下一章我们再看看ECS里组件的内存布局