Systems - OneYoungMean/Entitas-CSharp-OYM GitHub Wiki
有4种不同类型的系统:
IInitializeSystem:仅在开始执行一次 system.Initialize().
IExecuteSystem:每一帧都执行一次 system.Execute().
ReactiveSystem:仅当检测到组发生改变(替换,删除)后执行一次 system.Execute(Entity[]).
ICleanupSystem:每一帧仅在完成了其他系统以后执行一次 system.Cleanup().
我们建议你为单个的任务或者行为创建系统,并按定义的顺序执行他们。这有助你确保您的应用具有确定性。
var systems = new Systems()
.Add(new SomeInitializeSystem(contexts))
.Add(new SomeReactiveSystem(contexts))
.Add(new SomeExecuteSystem(contexts));
请注意,在创建系统的时候并没有编号,所有的系统都将以fluent interface-style. 规则加载。
这里我们提供了一个启动脚本应该具有的相关代码:
Use Entitas ;
Use UnityEngine ;
Public class GameController:MonoBehaviour {
Public Service Service = Service. Single ;
System _ system ;
Void Awake(){
Var contexts = context. sharedInstance ;
service. Initialization (context, this);
_systems = new GameSystems(contexts);
}
Void Start(){
_systems.Initialize();
}
Void Update(){
_systems.Execute();
_systems. Cleanup();
}
Void OnDestroy(){
_systems.TearDown();
}
}
初始化系统在程序的开始时运行一次。它们实现了IInitializeSystem
接口的定义方法Initialize()
。这是你设置游戏初始化状态的地方,与Unity的Start()
方法类似。
using Entitas;
public class MyInitSystem : IInitializeSystem {
public void Initialize() {
// Initialization code here
}
}
执行系统会在每一帧之后被调用,它们实现了IExecuteSystem
接口的定义方法Execute()
,这是一个你放置每一帧你需要执行的方法的地方。类似于Unity的Update()
方法。
using Entitas;
public class MyExecSystem : IExecuteSystem {
public void Execute() {
// per-frame code goes here
}
}
所有其他系统完成工作后,清理系统会在每个帧结束之后运行。它们实现了ICleanupSystem
接口的定义方法Cleanup()
。如果你需要创建一个仅存在于一帧的实体,那么这个方法会非常有用(详情参考下面的示例混合系统)。
public class MyCleanupSystem : ICleanupSystem {
public void Cleanup() {
// cleanup code here
// runs after every execute and reactive system has completed
}
}
Entitas同时提供了一个特殊的系统叫做反应系统,一个在底层用来监听组的系统。它将会实时监听与改变那些你所感兴趣的实体。想象一下,你在战场上有10000个战斗单位,但只有10在一帧内个改变了他们的位置,普通的IExecuteSystem需要更新所有10000个视图,而您可以使IReactiveSystem,它只会更新10个已更改单元的视图。这么高效:)。
此外,并不想其他系统那样,反应系统继承自基类ReactiveSystem<TEntity>
,而不是继承一个接口。Entitas为每一个你游戏当中的上下文生成一个实体。如果你的上下文是Game
, GameState
或者 Input
,Entitas生成的对应名字将会为GameEntity
, GameStateEntity
与InputEntity
。反应系统要求您提供其响应的特定上下文和关联实体类型。
在反应系统的基类当中,我们定义了一些必须实现的抽象方法。首先,您必须创建一个调用基础构造函数的构造函数,并为其提供适当的上下文,其次,你必须实现三个虚拟的方法: GetTrigger():返回一个收集器,它告诉系统要响应的事件。 Filter():对收集器返回的实体进行最终检查,负责告诉Execute()在运行前需要判定的条件。 Execute():是大部分游戏逻辑所在的位置。
注意:你不应该尝试将反应系统与执行系统结合起来,毕竟你不能同时继承一个类与一个接————它视为执行系统的特例。
using System.Collections.Generic;
using Entitas;
public class MyReactiveSystem : ReactiveSystem<MyContextEntity> {
public MyReactiveSystem (Contexts contexts) : base(contexts.MyContext) {
// pass the context of interest to the base constructor
}
protected override ICollector<MyContextEntity> GetTrigger(IContext<MyContextEntity> context) {
// specify which component you are reacting to
// return context.CreateCollector(MyContextMatcher.MyComponent);
// you can also specify which type of event you need to react to
// return context.CreateCollector(MyContextMatcher.MyComponent.Added()); // the default
// return context.CreateCollector(MyContextMatcher.MyComponent.Removed());
// return context.CreateCollector(MyContextMatcher.MyComponent.AddedOrRemoved());
// combine matchers with AnyOf and AllOf
// return context.CreateCollector(LevelMatcher.AnyOf(MyContextMatcher.Component1, MyContextMatcher.Component2));
// use multiple matchers
// return context.CreateCollector(LevelMatcher.MyContextMatcher, MyContextMatcher.Component2.Removed());
// or any combination of all the above
// return context.CreateCollector(LevelMatcher.AnyOf(MyContextMatcher.Component1, MyContextMatcher.Component2),
// LevelMatcher.Component3.Removed(),
// LevelMatcher.AllOf(MyContextMatcher.C4, MyContextMatcher.C5).Added());
}
protected override bool Filter(MyContextEntity entity) {
// check for required components
}
protected override void Execute(List<MyContextEntity> entities) {
foreach (var e in entities) {
// do stuff to the matched entities
}
}
}
要对来自多个上下文的实体更改做出反应,您需要使用多反应系统。首先,您需要声明一个接口,该接口将组合来自具有相同组件的多个上下文的实体,并且您需要通过部分类为实体类实现该接口。
public interface PositionViewEntity : IEntity, IPosition, IView {}
public partial class EnemyEntity : PositionViewEntity {}
public partial class ProjectileEntity : PositionViewEntity {}
然后创建一个从MultiReactiveSystem继承的系统并传递新接口。
public class ViewSystem : MultiReactiveSystem<PositionViewEntity, Contexts> {
public ViewSystem(Contexts contexts) : base(contexts) {}
protected override ICollector[] GetTrigger(Contexts contexts) {
return new ICollector[] {
contexts.Enemy.CreateCollector(EnemyMatcher.Position),
contexts.Projectile.CreateCollector(ProjectileMatcher.Position)
};
}
protected override bool Filter(PositionViewEntityentity) {
return entity.hasView && entity.hasPosition;
}
protected override void Execute(List<PositionViewEntity> entities) {
foreach(var e in entities) {
e.View.transform.position = e.Position.value;
}
}
}
Entitas功能类为您提供对系统组织的一些额外控制。使用它们将相关系统组合在一起。这具有在Unity层次结构中分离系统的可视调试对象的额外好处。现在,他们可以在逻辑组中进行检查,而不是一次性的完成。
这些功能还可以帮助您在项目中实施更广泛的范例规则。功能的执行顺序取决于它们的添加顺序,并且始终由Entitas遵守。你的系统分成InputSystems : Feature,GameLogicSystems : Feature并且RenderingSystems : Feature将会按顺序初始化它们,确保提供了一个游戏逻辑不干扰渲染的简单环境。
实现这些功能要求您实现构造函数。使用Add()中的方法将系统添加到功能部件中。这里添加它们的顺序定义了它们在运行时的执行顺序。可以在GameController中使用功能来一起实例化系统组。
using Entitas;
public class InputSystems : Feature
{
public InputSystems(Contexts contexts) : base("Input Systems")
{
// order is respected
Add(new EmitInputSystem(contexts));
Add(new ProcessInputSystem(contexts));
}
}
然后,在你的GameController.cs当中:
Systems createSystems(Contexts contexts) {
// order is respected
return new Feature("Systems")
// Input executes first
.Add(new InputSystems(contexts))
// Update
.Add(new GameBoardSystems(contexts))
.Add(new GameStateSystems(contexts))
// Render executes after game logic
.Add(new ViewSystems(contexts))
// Destroy executes last
.Add(new DestroySystem(contexts));
}
以下是一个在Game上下文当中运行反应系统的示例,在这个系统当中我们定义了两个组件,PositionComponent
负责存储2D的整数网格的坐标,ViewComponent
负责存储UnityGameobject需要可视化的实体。
using Entitas;
using UnityEngine;
[Game]
PositionComponent : IComponent {
int x;
int y;
}
[Game]
ViewComponent : IComponent {
GameObject gameObject;
}
下面示例如何让系统负责监听实体的' PositionComponent
当中发生的更改.,收集器收集其位置在前一帧(via entity.ReplacePosition(x, y))中已更改的所有实体,当然,这些实体通过过滤器来检查它们是否也添加了一个View(因此要移动一个GameObject)。同时具有更改位置和视图的实体将其视图GameObject移动到其新位置。
public class RenderPositionSystem : ReactiveSystem<GameEntity>
{
// ctor:构造函数
// ctor is called during GameController.CreateSystems()
// this system operates on the Game context so pass this to the base ctor
public RenderPositionSystem(Contexts contexts) : base(contexts.game) {
}
// our collector gathers entities whose Position component has changed
protected override Collector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
return context.CreateCollector(GameMatcher.Position);
}
// filter to ensure entity has a view component and that its position component has
// not been removed by another system since it was collected
protected override bool Filter(GameEntity entity)
{
return entity.hasView && entity.hasPosition;
}
// walk the list of entities and move their view GameObject
protected override void Execute(List<GameEntity> entities)
{
foreach (GameEntity e in entities)
{
var pos = e.gridPosition.position;
e.view.gameObject.transform.position = new Vector3(pos.x, pos.y, 0);
}
}
}
该系统既是Execute又是Cleanup系统。它的功能是监视Unity的Input类以获得鼠标点击,并创建相应的InputComponent添加的实体。一个单独的系统负责处理这些组件,然后,这些实体将在在清理阶段被销毁。
这种安排的优点是我们可以让多个独立的系统监听InputComponent并用它们做不同的事情。这些系统都不应该负责销毁它们处理的实体,因为我们以后可能会添加更多系统或删除现有系统。实体应该在下一帧之前销毁,因为我们永远不会再需要它。这就是Cleanup()介入的地方,允许创建实体的系统保留销毁它们的方式。
该系统还显示了如何使用组轻松查找附加了特定组件的实体并跟踪它们。这里我们添加了一个构造函数来设置对InputComponent附加实体组的引用。
using Entitas;
using UnityEngine;
public class EmitInputSystem : IExecuteSystem, ICleanupSystem {
readonly InputContext _context;
readonly IGroup<InputEntity> _inputs;
// get a reference to the group of entities with InputComponent attached
public EmitInputSystem(Contexts contexts) {
_context = contexts.input;
_inputs = _context.GetGroup(InputMatcher.Input);
}
// this runs early every frame (defined by its order in GameController.cs)
public void Execute() {
// check for unity mouse click
var input = Input.GetMouseButtonDown(0);
if(input) {
// perform a raycast to see if we clicked an object
var hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, 100);
if(hit.collider != null) {
// we hit an object, so this is a valid input.
// create a new entity to represent the input
// give it the position of the object we hit
var pos = hit.collider.transform.position;
_context.CreateEntity()
.AddInput((int)pos.x, (int)pos.y);
}
}
}
// ~~~~~~ OTHER SYSTEMS EXECUTE - PROCESS THE ENTITIES CREATED HERE ~~~~~ //
// all other systems are done so we can destroy the input entities we created
public void Cleanup() {
// group.GetEntities() always provides an up-to-date list
foreach(var e in _inputs.GetEntities()) {
_context.DestroyEntity(e);
}
}
}