天天看点

ECS Entitas源码分析(五)___System

介绍

System是ECS的核心组成部分之一,它是个单纯的逻辑处理类,本身不包含任何状态,只是在特定的时间执行系统中的逻辑,在这些逻辑中改变相关Entity身上的Component的数据与状态,这是ECS系统中关键所在。Entitas中的System根据作用的不同给我们提供了5个接口类,分别为

IInitializeSystem

,

IExecuteSystem

,

IReactiveSystem

,

ICleanupSystem

以及

ITearDownSystem

而他们都继承自

ISystem

.同时Entitas还提供了一个Systems类,用来组合不同的System,控制System的执行顺序以及生命周期.

一 Systems与Feature

systems是用来控制所有system的执行,systems内部维护了4个不用的List用来保存不同类型的系统(

_initializeSystems

,

_executeSystems

,

_cleanupSystems

,

_tearDownSystems

) ,然后我们通过

Add(ISystem system)

方法添加到不同的List中. 同时systens继承了

IInitializeSystem

,

IExecuteSystem

,

ICleanupSystem

,

ITearDownSystem

,实现

Execute()

,

Cleanup()

,

Initialize()

,

TearDown()

等接口,在这些接口中驱动对应List中的system。所以我们只需要在外部调用这4个对应的接口来驱动这个Entitas的运行。

public Systems() {
    //初始化各个List
    _initializeSystems = new List<IInitializeSystem>();
    _executeSystems = new List<IExecuteSystem>();
    _cleanupSystems = new List<ICleanupSystem>();
    _tearDownSystems = new List<ITearDownSystem>();
}
           
//将不同的System添加到对应的列表中
public virtual Systems Add(ISystem system) {
    var initializeSystem = system as IInitializeSystem;
    if (initializeSystem != null) {
        _initializeSystems.Add(initializeSystem);
    }
    
    var executeSystem = system as IExecuteSystem;
    if (executeSystem != null) {
        _executeSystems.Add(executeSystem);
    }
    
    var cleanupSystem = system as ICleanupSystem;
    if (cleanupSystem != null) {
        _cleanupSystems.Add(cleanupSystem);
    }
    
    var tearDownSystem = system as ITearDownSystem;
    if (tearDownSystem != null) {
        _tearDownSystems.Add(tearDownSystem);
    }
    
    return this;
}
           

4个主要的执行方法

//驱动初始化系统执行Initialize()方法
public virtual void Initialize() {
    for (int i = 0; i < _initializeSystems.Count; i++) {
        _initializeSystems[i].Initialize();
    }
}

//驱动每帧执行的系统执行Execute()方法
public virtual void Execute() {
   for (int i = 0; i < _executeSystems.Count; i++) {
       _executeSystems[i].Execute();
   }
}

//驱动清理系统执行Cleanup()方法
public virtual void Cleanup() {
    for (int i = 0; i < _cleanupSystems.Count; i++) {
        _cleanupSystems[i].Cleanup();
    }
}

//驱动结束系统执行TearDown()方法
public virtual void TearDown() {
   for (int i = 0; i < _tearDownSystems.Count; i++) {
       _tearDownSystems[i].TearDown();
   }
}
           

Feature是代码生成器为我们生成的一个类,主要用于在编辑器模式下开启visual debugging时收集各个系统的运行数据,同时在Unity中展示,方便我们在开发时做调试和优化。所以在开启visual debugging时,Feature继承自DebugSystems,而DebugSystems又是继承自Systems的,在里面会做一些数据收集与展示的工作,在关闭visual debugging时,Feature直接继承自Systems,与Systems功能一致。当然这所有的不同都由代码生成器生成,无须我们手动修改。

二 4个寻常的系统,1个不寻常的系统

2.1 IExecuteSystem 每帧执行的系统

如果我们需要需要一个持续执行的system,继承

IExecuteSystem

,重写

void Execute()

方法,在这个方法里面写我们这个系统中需要每帧执行的逻辑。

2.2 ICleanupSystem 清理系统

ICleanupSystem

也是每帧执行的系统。不过从逻辑上区分这个system是为了我们在执行完所有的

IExecuteSystem

之后执行的逻辑所设立的,用来做一些数据的清理的作用.需要重写

void Cleanup()

方法; 将清理逻辑写在这个方法中.

2.3 IInitializeSystem 初始化系统

IInitializeSystem

正如它的名字一样,它是用来做初始化逻辑用的。需要重写

void Initialize();

方法。类似unity中的

void Start()

方法.

2.4 ITearDownSystem销毁系统

ITearDownSystem

需要重写

void TearDown();

方法。在这个方法里面执行退出销毁逻辑。

2.5 不寻常的ReactiveSystem 反应系统

ReactiveSystem

实际上是

IExecuteSystem

系统,所以它也需要重写

void Execute()

,但是对于我们使用者来说,我们自己重写的

void Execute()

不一定每帧都会调用,这究竟是怎么回事呢?我们来看看

ReactiveSystem

的实现:

我们继承

ReactiveSystem

后需要实现它的3个抽象方法:分别是:

ICollector<TEntity> GetTrigger(IContext<TEntity> context)

:创建一个收集器,收集我们需要的同时发生了改变的

Entity

.

bool Filter(TEntity entity)

:一个筛选方法,筛选收集器收集到的Entity。

void Execute(List<TEntity> entities)

:执行方法,对收集器收集到的同时通过了筛选方法筛选的

Entity

执行具体的逻辑。entities就是当前帧符合我们需求的Entity列表了。这个列表每帧都会清空。

上面这3个方法是我们使用

ReactiveSystem

的核心方法,每个

ReactiveSystem

都必须拥有这3个方法。

因为它自己也是一个

IExecuteSystem

系统,所以它也有自己的

void Execute()

方法,我们看一下这个方法里面做了些什么:

public void Execute() {
    if (_collector.count != 0) {
        foreach (var e in _collector.collectedEntities) {
            if (Filter(e)) {
                e.Retain(this);
                _buffer.Add(e);
            }
        }
        
        _collector.ClearCollectedEntities();
        
        if (_buffer.Count != 0) {
            try {
                Execute(_buffer);
            } finally {
                for (int i = 0; i < _buffer.Count; i++) {
                    _buffer[i].Release(this);
                }
                _buffer.Clear();
            }
        }
    }
}
           

这个方法也十分的简单,其中有2个变量:分别是

_collector

_buffer

_collector

GetTrigger

的方法的返回值,也就是这个系统的收集器。

_buffer

是存储当前帧中收集器收集到的,同时通过筛选方法的Entity集合。而这个方法也就是做这些事情。筛选Entity,然后调用

void Execute(List<TEntity> entities)

方法,将筛选好的列表传过去做逻辑处理。

那么

ReactiveSystem

中包含了变量是不是不符合ECS的规则了呢?

_collector

是系统初始化的时候创建的,不会随着系统的运行发生改变的。而

_buffer

只是相当与一个临时的变量,每一帧都会清理。也就是说每一帧开始的时候它的状态都是一样的。保存这个变量只是为了避免频繁的创建List。

MultiReactiveSystem

的用法和实现与

ReactiveSystem

类似,只是可以包含多个

Context

以及多个

Collector

总结

我们可以看出Entitas的System实现非常的简单。通过Systems类控制所有的系统的执行顺序,根据系统的逻辑功能,给系统划分了4种类型,分别有自己特殊的需要重写的方法,而所有系统的主要逻辑都集中在这些方法中。Entitas为了方便我们使用,提供了一种特殊的

ReactiveSystem

。它通过收集器,只关心满足条件的Entity,当没有满足条件的Entity时,它的

Execute

方法就不会执行,所以在我们使用者看来它就变成了一种触发式的系统了。

<上一篇>ECS Entitas源码分析(四)___Entity