System
A system a piece of logic of a game. It can be thought as an entity processor since a typical system will query a subset of entities and process their components in some way. To accomplish this, a system can claim parts of the world as injectables by declaring public
fields of an IInjectable
type. All types under the namespace Entia.Injectables
are such injectables.
A system has to implement at least one of the ISystem
interfaces found under the namespace Entia.Systems
. Each one of these system interfaces will be executed during a specific execution phase.
A system may also be reactive if it implements at least one of the system interface IReact<T>
. The type T
is a message type and the IReact<T>
method will be called immediately when a message of that type is emitted.
A system must:
- be a
struct
- implement at least one of
ISystem
interfaces which can be found under the namespaceEntia.Systems
- have its
public
instance fields have injectables types - store state as
private
instance fields
A system should:
- have its
public
instance injectable fields bereadonly
. - not refer to another system (to share logic between systems, use utility functions)
- not have fields that store another Entia type (such as a component, a system, a message, a queryable, etc.) except for an entity or injectable.
using System.Collections.Generic;
using Entia;
using Entia.Injectables;
using Entia.Queryables;
using Entia.Systems;
namespace Components
{
public struct Health : IComponent { public float Current, Maximum; }
}
namespace Messages
{
public struct OnBirth : IMessage { public Entity Entity; }
public struct OnDeath : IMessage { public Entity Entity; }
public struct DoKill : IMessage { public int Count; }
}
namespace Systems
{
// The 'IRun' interface is one of the system interfaces that
// inherit from the 'ISystem' interface.
// It executes during the 'Phases.Run' phase.
public struct Death : IRun
{
// Queries all entities that have a 'Health' component.
public readonly struct Query : IQueryable
{
public readonly Entity Entity;
public readonly Write<Components.Health> Health;
}
// Require a couple injectables.
public readonly AllEntities Entities;
public readonly Emitter<Messages.OnDeath> OnDeath;
public readonly Group<Query> Group;
public void Run()
{
foreach (ref readonly var item in Group)
{
// Unpack the group item.
var entity = item.Entity;
ref var health = ref item.Health.Value;
if (health.Current <= 0)
{
// Emits a on death message such that other
// systems can be notified.
OnDeath.Emit(new Messages.OnDeath { Entity = entity });
Entities.Destroy(entity);
}
}
}
}
// These interfaces are all system interfaces.
// The 'IInitialize' interface executes during the 'Phases.Initialize' phase.
// The 'IRun' interface executes during the 'Phases.Run' phase.
// The 'IReact<T>' interface execute immediatly when a message of type
// 'T' is emitted.
public struct Killer :
IInitialize, IRun,
IReact<Messages.OnBirth>, IReact<Messages.OnDeath>
{
// An injectable that gives write access to 'Health' components.
public readonly Components<Components.Health>.Write Healths;
// An injectable that queues all 'DoKill' messages.
public readonly Receiver<Messages.DoKill> DoKill;
// A system state must be private.
// This set will keep track of the entities have been born.
HashSet<Entity> _live;
public void Initialize()
{
// The set must be initialized or else it will be 'null'.
_live = new HashSet<Entity>();
}
public void Run()
{
var enumerator = _live.GetEnumerator();
// Dequeues all 'DoKill' messages.
while (DoKill.TryPop(out var message))
{
for (int i = 0; i < message.Count && enumerator.MoveNext(); i++)
{
ref var health = ref Healths.GetOrDummy(
enumerator.Current,
out var success);
if (success) health.Current = 0;
}
}
}
// Called immediately after an 'OnBirth' message is emitted.
public void React(in Messages.OnBirth message) =>
_live.Add(message.Entity);
// Called immediately after an 'OnDeath' message is emitted.
public void React(in Messages.OnDeath message) =>
_live.Remove(message.Entity);
}
}