This is a custom fork of GoCS, found at https://github.com/lazlo-bonin/gocs
Please read the original readme on GoCS to better understand the slight changes to this fork.
This fork is intended to give the interface/component developer the ability to encode whether an event is an incoming event that needs to be handled by the component, or an emitted (output) event that can be reacted to by systems. In addition, a simple Message bus is added for inter-system communication (which is rarely needed, but handy, such as disabling the camera controls on a game mode change, for example).
To quote Lazlo (the original developer) on this:
In the ideal world, this access control would be better — the component should be able to explicitly define whether an event can be subscribed externally, or published externally, or both.
This fork provides the developer with the option of specifying this information.
// This example demonstrates an emitted event, that only the actual component can invoke,
// as well as a handled event that it consumes.
interface IPlayerWallet
{
// Attributes
float WalletValue {get;}
// Events
IHandledEvent<float> Pay { get; }
IEmittedEvent<float> WalletValueChanged { get; }
}
This can be implemented easily using the full Event class from GoCS, which has been modified to fulfill these interfaces. Here's an oversimplified example that demonstrates how to fulfill the interface requirements. We make use of expression-bodied members and explict interface implementation to avoid confusion when filling the class details. (Because the class needs to be able to Invoke the event, it needs a backing GoCS Event. Using explicit interface implementation allows us to name our backing field the same name as the property and prevent confusion. The explicit property will only be used by externals using the interface.)
class PlayerWallet : BaseComponent, IPlayerWallet
{
// Attributes
private float _WalletValue = 0;
public float WalletValue
{
get {return _WalletValue;}
private set { _WalletValue = value; WalletValueChanged?.Invoke(value); }
}
//Events
IHandledEvent<float> Pay {get; private set}
private Event<float> WalletValueChanged = new Event<float>();
IEmittedEvent<float> IPlayerWallet.WalletValueChanged => WalletValueChanged;
// Class code
protected override void Awake()
{
base.Awake();
Pay = new Event<float>(OnPay);
}
public void OnPay(float value)
{
WalletValue -= value;
}
}
Should you wish to have a bidirectional event, simply use the added IEvent
, or the preexisting Event
class from GoCS.
To import GoCS, open Packages/manifest.json
and add this line under dependencies
:
"dev.lazlo.gocs": "https://github.com/RealityStop/gocs.git"
Open Packages/manifest.json
and remove this line under dependencies
:
"dev.lazlo.gocs": "https://github.com/lazlo-bonin/gocs.git"
Return to Unity and let it update your project (cringe at the wall of errors), then add
"dev.lazlo.gocs": "https://github.com/RealityStop/gocs.git"
in place of the original line. Then return to Unity and let it reimport.
💡 GoCS (pronounced go-cee-ess) is a design pattern for Unity. It's inspired by entity component system (ECS), but it works with the existing Unity GameObject + Component architecture, hence the name. It's also more flexible and less strict than ECS. As such, it's easy to get started using the concepts and API you're already familiar with.
🏁 The goal of GoCS is to help you write code that is readable, reusable and maintanable. It elegantly scales to any project size, from small to big. If you ever found yourself asking "where should I put this code?", then GoCS is here to help.
🔀 To be clear, GoCS is entirely unrelated to Unity's new data oriented tech stack (DOTS).
🎓 Before you continue, if you're a bit confused with acronyms and concepts like ECS, OOP, IOC or DOTS, you should start with this basic introduction: Intro: OOP vs ECS in Unity
GoCS helps solve some common headaches when developing Unity projects:
- You use OOP, but your component inheritance hierarchy quickly becomes messy
- You use ECS, but you are constantly writing boilerplate code for simple tasks
- You need an event system, but you're unsure how to implement them properly
In tech lingo, GoCS excels at combining encapsulation, composition and inversion of control.
Benefit | OOP | ECS | GoCS |
---|---|---|---|
Encapsulation | ✅ | ⛔ | ✅ |
Scalability | ⛔ (Inheritance) | ✅ (Composition) | ✅ (Both) |
Inversion of Control | ⛔ | ⛔ | ✅ |
High Performance | ⛔ | ✅ (DOTS) | ⛔ |
GoCS is not for you if you need the high performance benefits of multi-threading, burst compiling, and memory layouting. For those case, you should use DOTS.
GoCS is a very lightweight framework imported from the Unity Package Manager.
GoCS requires Unity 2018.4 or newer.
To import GoCS, open Packages/manifest.json
and add this line under dependencies
:
"dev.lazlo.gocs": "https://github.com/lazlo-bonin/gocs.git"
To update GoCS, open Packages/manifest.json
and remove the dev.lazlo.gocs
entry under lock
at the end of the file.
All the GoCS API is under the Lazlo.Gocs
namespace:
using Lazlo.Gocs;
GoCS is shared under the MIT License. This means you're free to use and redistribute it in your games and other projects, even commercially. For the full license, see LICENSE.md.
GoCS is designed to be simple to implement. Once you wrap your head around the core concepts below, you should be able to start using it with only the basics of C# and Unity.
GoCS is similar to traditional ECS: it's organized as Entities, Components and Systems.
Concept | Purpose | Related API |
---|---|---|
⛓ Entities | Empty shells that link Components together | GameObject |
🧩 Components | Define data for runtime and authoring for the editor | IComponent + BaseComponent |
⚙️ Systems | Perform common logic on batches of components | BaseSystem |
GoCS is different from traditional ECS in two ways:
- Components are separated in two layers: the Interface and the Class(es). Systems only operate on component interfaces. But component classes can add extra logic, data, attributes or even inheritance and couplings if they need to. (Don't worry if that sounds abstract for now, it'll become clearer with examples!)
- Systems are in charge of event dispatch. In other words, they decide when to send events back to the components. This is what's called "inversion of control": your components don't need to handle their own event conditions, they only need to handle their event reactions.
Let's say we have a first person game where the player can interact with objects in the world.
We want to use GoCS to create a setup where all the code to hover and click objects is handled by a common System, so that our Components only need to implement the reactions to those events.
Let's start by creating a component interface that defines interactable objects.
The component interface is responsible for declaring:
- 🎛 Attributes: Immutable (read-only) properties needed by the system.
- 📦 Data: Mutable (read+write) properties needed by the system.
- ⚡️ Events: Callbacks that the system can trigger or register.
// Component Interfaces must implement the IComponent interface
public interface IInteractable : IComponent
{
// Attributes
// (immutable, so only { get; })
float range { get; }
// Data
// (mutable, so both { get; set; })
bool isHovered { get; set; }
bool isPressed { get; set; }
// Events
// (only { get; })
// ("Event" is a minimal event wrapper class included in the GoCS API)
Event onHoverEnter { get; }
Event onHoverExit { get; }
Event onPress { get; }
Event onRelease { get; }
}
The component class is responsible for providing attribute values and handling events.
In GoCS, you can define more than one component class per component interface. That's very powerful, because they will all reuse the same logic from the system without having to resort to inheritance. Therefore, each of these classes stays simple and focused on a single responsibility.
To demonstrate that, we'll create two different interactable classes from the same interactable interface.
The grabbable component allows the player to move an object around by clicking and holding it. It does so by simply changing the object's transform parent to the main camera.
// Component Classes are derived from the BaseComponent class
// They must also implement their matching Component Interface
public class Grabbable : BaseComponent, IInteractable
{
// Attributes
// (Tip: use [SerializeField] to expose the range field to the inspector
// while also implementing the interface's range property.)
[SerializeField]
private float _range = 5;
public float range
{
get => _range;
set => _range = value;
}
// Data
public bool isHovered { get; set; }
public bool isPressed { get; set; }
// Events
public Event onHoverEnter { get; private set; }
public Event onHoverExit { get; private set; }
public Event onPress { get; private set; }
public Event onRelease { get; private set; }
// Initialization
protected override void Awake()
{
base.Awake();
// Create events and link them to our handlers
onPress = new Event(Grab);
onRelease = new Event(LetGo);
}
// Extra Logic
private void Grab()
{
transform.parent = Camera.main.transform;
}
private void LetGo()
{
transform.parent = null;
}
}
The highlightable component changes the object's color when it gets hovered, and sets it back to its normal color afterwards.
This component makes use of GoCS' ability to add extra attributes, data and couplings to component classes when you need to.
[RequireComponent(typeof(Renderer))]
public class Highlightable : BaseComponent, IInteractable
{
// Attributes
[SerializeField]
private float _range = 5;
public float range
{
get => _range;
set => _range = value;
}
// Data
public bool isHovered { get; set; }
public bool isPressed { get; set; }
// Events
public Event onHoverEnter { get; private set; }
public Event onHoverExit { get; private set; }
public Event onPress { get; private set; }
public Event onRelease { get; private set; }
// Extra Attributes
[SerializeField]
private Color _color = Color.yellow;
public Color color
{
get => _color;
set => _color = value;
}
// Extra Data
private Color normalColor;
// Extra Coupling
private Renderer renderer;
// Initialization
protected override void Awake()
{
base.Awake();
renderer = GetComponent<Renderer>();
onHoverEnter = new Event(Highlight);
onHoverExit = new Event(ResetColor);
}
// Extra Logic
private void Highlight()
{
normalColor = renderer.material.color;
renderer.material.color = color;
}
private void ResetColor()
{
renderer.material.color = normalColor;
}
}
The system is a class responsible for implementing the reusable logic and for triggering events.
Here, we'll create a basic system that uses a raycast to find the object below the player's cursor at the center of the screen.
It stores and compares the currently hovered and pressed objects across frames to determine when they change and trigger the related events accordingly.
// Systems are derived from the BaseSystem class
public class InteractionSystem : BaseSystem
{
private IInteractable hovered;
private IInteractable pressed;
private void Update()
{
if (pressed == null)
{
var camera = Camera.main;
var ray = new Ray(camera.transform.position, camera.transform.forward);
IInteractable interactable = null;
if (Physics.Raycast(ray, out var hit, Mathf.Infinity, (LayerMask)~0))
{
interactable = hit.collider.gameObject?.GetComponentInParent<IInteractable>();
if (interactable != null && hit.distance > interactable.range)
{
interactable = null;
}
}
if (interactable != hovered)
{
if (hovered != null)
{
hovered.onHoverExit?.Invoke();
hovered.isHovered = false;
}
hovered = interactable;
if (hovered != null)
{
hovered.onHoverEnter?.Invoke();
hovered.isHovered = true;
}
}
if (hovered != null && Input.GetMouseButtonDown(0))
{
pressed = hovered;
pressed.onPress?.Invoke();
pressed.isPressed = true;
}
}
if (pressed != null && Input.GetMouseButtonUp(0))
{
pressed.onRelease?.Invoke();
pressed.isPressed = false;
pressed = null;
}
}
}
Because GoCS allows inheritance in component classes, you can reduce the amount of boilerplate even further.
For example, you could create a BaseInteractable
class from which Grabbable
and Highlightable
inherit.
// BaseInteractable.cs
public abstract class BaseInteractable : BaseComponent, IInteractable
{
protected override void Awake()
{
base.Awake();
onHoverEnter = new Event(OnHoverEnter);
onHoverExit = new Event(OnHoverExit);
onPress = new Event(OnPress);
onRelease = new Event(OnRelease);
}
// Attributes
[SerializeField]
private float _range = 5;
public float range
{
get => _range;
set => _range = value;
}
// Data
public bool isHovered { get; set; }
public bool isPressed { get; set; }
// Events
public Event onHoverEnter { get; private set; }
public Event onHoverExit { get; private set; }
public Event onPress { get; private set; }
public Event onRelease { get; private set; }
// Event Handlers
protected virtual void OnHoverEnter() { }
protected virtual void OnHoverExit() { }
protected virtual void OnPress() { }
protected virtual void OnRelease() { }
}
Then, the derived classes become extremely simple and eloquent, which is the the ultimate goal of GoCS!
For example, Grabbable
now only needs a dozen lines of code:
public class Grabbable : BaseInteractable
{
protected override void OnPress()
{
transform.parent = Camera.main.transform;
}
protected override void OnRelease()
{
transform.parent = null;
}
}
Neat! ✨
In ECS patterns, you'll typically want systems to operate on GameObjects that share a set of components.
GoCS provides helpers to make that easy. These helpers use out
parameters and the tuple deconstruction syntax to keep the code readable.
For the following examples, we'll pretend we want a health regeneration system that slowly regens the health of game objects that have both IHealth
and IRegenable
components attached.
On individual GameObjects, you an use the Has
and Get
to fetch sets of components.
Has
will only return true if all the components are found on the given object:
if (gameObject.Has(out IHealth health, out IRegenable regenable))
{
health.value += regenable.healthPerSecond * Time.deltaTime;
}
Get
will always return a tuple of components, but some of them may be null if they are not on the object. Only use it if you know for certain that all components are present.
var (health, regenable) = gameObject.Get<IHealth, IRegenable>();
if (health != null && regenable != null)
{
health.value += regenable.healthPerSecond * Time.deltaTime;
}
Typically, systems will want to run a foreach
loop on all game objects that share a set of components. To do that, you can use World.Query
. Unlike FindObjectsOfType
, this method is optimized for speed and allocates zero byte of memory.
class HealthRegenSystem : BaseSystem
{
void Update()
{
foreach (var (health, regenable) in World.Query<IHealth, IRegenable>())
{
health.value += regenable.healthPerSecond * Time.deltaTime;
}
}
}
If you need to use a world query in the while in edit-mode, you must pass true
to the forceNative
argument of the World.Query
method. This will make it revert to the slower FindObjectsOfType
in the background, which is required because the components are only properly cached during play mode.
For example, if we wanted to draw a gizmo for the range of each of our interactables, we could add this code to the interaction system:
class InteractionSystem : BaseSystem
{
// (Previous code...)
void OnDrawGizmos()
{
Gizmos.color = Color.magenta;
foreach (var interactable in World.Query<IInteractable>(forceNative: true))
{
Gizmos.DrawWireSphere(interactable.transform.position, interactable.range);
}
}
}
For even better performance, you can use the SystemComponents
helper generic class.
This class will keep a extremely fast and lean registry of components, but you must add and remove components to it manually.
GoCS makes this easy with the OnCreatedComponent
and OnDestroyingComponent
callbacks in BaseSystem
. Let's rewrite our health regen system with this approach:
class HealthRegenSystem : BaseSystem
{
// Declare and initialize the SystemComponents helper
SystemComponents<IHealth, IRegenable> components = new SystemComponents<IHealth, IRegenable>();
// OnCreatedComponent is sent to all systems when a new component is created
public override void OnCreatedComponent(IComponent component)
{
// Add the component to our SystemComponents
// (The API takes care of making sure it has the right components for us)
components.Add(component);
}
// OnDestroyingComponent is sent to all systems before an existing component is destroyed
public override void OnDestroyingComponent(IComponent component)
{
// Remove the component from our SystemComponents
components.Remove(component);
}
void Update()
{
// Enumerate over the SystemComponents instead of using World.Query
foreach (var (health, regenable) in components)
{
health.value += regenable.healthPerSecond * Time.deltaTime;
}
}
}
You can also use multiple SystemComponents in a single system if you need to.
GoCS includes a benchmark of the different query methods in the samples. This benchmark tests a two-component query over 10,000 game objects at every frame.
Method | API | Time (lower is better) | Allocation (lower is better) |
---|---|---|---|
Native Query | World.Query(true) |
🛑 36.28ms | 🔴 195.2 KB |
Managed Query | World.Query() |
❇️ 0 byte | |
System Query | SystemComponents |
✅ 0.16ms | ❇️ 0 byte |
Obviously, using system components is the fastest alternative, clocking in at almost 200x faster than Unity's FindObjectsOfType
.
Sometimes, components only make sense when coupled with other components.
For example, an IRegenable
component might always require a IHealth
component.
There are a few ways you can go about this.
The most simple is to require a component to appear with another with Unity's built-in [RequireComponent]
attribute:
[RequireComponent(typeof(IHealth))]
interface IRegenable : IComponent { }
This will force the component to be added while in the editor. But if you don't need to configure any property on the required component, you might not want to pollute your game objects while authoring in the editor. This is often the case when using GoCS event proxies (see section below). In those cases, you can use GoCS' custom [RuntimeRequireComponent]
attribute:
[RuntimeRequireComponent(typeof(CollisionEventProxy))]
interface IDestructible : IComponent { }
Finally, if you don't want to (or can't) specify dependencies on the components themselves, you can require components directly from the system, during the OnCreatedComponent
phase. For example, if IDestructible
components always need a matching CollisionEventProxy
component, you could do so using the GetOrAddComponent
helper:
class ZoneSystem : BaseSystem
{
public override void OnCreatedComponent(IComponent component)
{
if (component is IDestructible destructible)
{
destructible.gameObject.GetOrAddComponent<CollisionProxy>();
}
}
}
If you need to pass in arguments to GoCS events, you can use the generic version of Event.
For example, if you need an damage event that passes the amount of damage, you could write:
interface IDamageable : IComponent
{
Event<int> onDamage { get; }
}
Your handlers signatures would then need to take that parameter:
class Damageable : IDamageable
{
public Event<int> onDamage { get; private set; }
public float health { get; set; } = 100;
protected override void Awake()
{
base.Awake();
onDamage = new Event(OnDamage);
}
private void OnDamage(int damage)
{
health -= damage;
}
}
And when invoking the event, you'll need to pass the damage argument:
damageable.onDamage.Invoke(5);
If you need more than one event argument, you can create an arguments struct (or class).
For example, if you wanted to add an elemental type to your damage event, you could write:
enum Element
{
Fire,
Water,
Wind,
Earth
}
struct DamageEventArgs
{
public int amount { get; }
public Element element { get; }
public DamageEventArgs(int amount, Element element)
{
this.amount = amount;
this.element = element;
}
}
interface IDamageable : IComponent
{
Event<DamageEventArgs> onDamage { get; }
}
Events can have zero, one or multiple handlers.
Unlike C# events, GoCS events can be triggered from outside their parent class. This is the trick that lets systems manage the event dispatch.
There are two styles you can use to declare events. These two styles are functionally identical. It's just a matter of preference!
On one hand, you can make the event property { get; private set; }
and use the constructor that specifies the handler in Awake
. For example:
class Destructible : BaseComponent
{
public Event onDestroy { get; private set; }
protected override void Awake()
{
base.Awake();
onDestroy = new Event(OnDestroyHandler);
}
private void OnDestroyHandler()
{
// ...
}
}
Or, if you prefer, you can make the event propery { get; }
only and initialize it inline, then use the AddHandler
method in Awake
:
class Destructible : BaseComponent
{
public Event onDestroy { get; } = new Event();
protected override void Awake()
{
base.Awake();
onDestroy.AddHandler(OnDestroyHandler);
}
private void OnDestroyHandler()
{
// ...
}
}
By convention, GoCS allows event properties to stay null (uninitialized) if your doesn't need them. To avoid errors, your systems should therefore always use the null-coalesce operator ?.
before invoking events:
destructible.onDestroy?.Invoke();
Whenever Unity will start supporting C# 8 (currently planned for the 2020 cycle), you'll be able to use the new default interface implementations feature to further reduce the amount of boilerplate for events.
Indeed, the component interface will be able to define a null getter for events by default, meaning they assume their implementation don't care about the event unless they specifically override them.
For example:
interface IInteractable : IComponent
{
// Default interface implementation (no event handling)
Event onHoverEnter => null;
Event onHoverExit => null;
Event onPress => null;
Event onRelease => null;
}
class Grabbable : BaseComponent, IInteractable
{
public Event onPress { get; } = new Event();
public Event onRelease { get; } = new Event();
// No need to implement onHoverEnter and onHoverExit!
}
Unity's event system is limited because it does not allow external objects to add event handlers.
For example, you cannot listen to a collider's OnCollisionEnter
event unless you create a MonoBehaviour script on the same GameObject.
This is problematic for GoCS, because your Systems don't live on the same GameObject as your Components, and yet they are responsible for event dispatch!
To fix that issue, GoCS introduces something called event proxies. Proxies are small components packaged with GoCS that just forward the built-in Unity messages like OnCollisionEnter
, OnTriggerEnter
, OnTransformParentChanged
, etc. to normal GoCS events that we can then use in our systems.
For example, here is how the collision proxy is implemented behind the scenes:
public sealed class CollisionProxy : BaseComponent
{
public Event<Collision> onEnter { get; } = new Event<Collision>();
public Event<Collision> onStay { get; } = new Event<Collision>();
public Event<Collision> onExit { get; } = new Event<Collision>();
private void OnCollisionEnter(Collision collision)
{
onEnter.Invoke(collision);
}
private void OnCollisionStay(Collision collision)
{
onStay.Invoke(collision);
}
private void OnCollisionExit(Collision collision)
{
onExit.Invoke(collision);
}
}
Let's say you wanted to implement a procedural destruction system with GoCS. When objects with the IDestructible
component collide with enough force, then the DestructionSystem
system should break them to pieces and send back an onDestroy
event. We'll need to to use CollisionProxy
for that purpose.
First, your component must require the proxy:
[RuntimeRequireComponent(typeof(CollisionProxy))]
interface IDestructible : IComponent
{
float requiredForce { get; }
Event onDestroy { get; }
}
Then, your system must add and remove listeners to the collision events in its OnCreatedComponent
and OnDestroyingComponent
phases.
To do that, you must use a SystemEvents
helper.
class DestructionSystem : BaseSystem
{
// Helper class to assign event handlers
SystemEvents<Collision> collisionEvents = new SystemEvents<Collision>();
public override void OnCreatedComponent(IComponent c)
{
if (c.gameObject.Has(out IDestructible destructible, out CollisionProxy collidable))
{
// Add the system event handler on the proxy
collisionEvents[collidable.onEnter] = collision => OnCollision(destructible, collision);
}
}
public override void OnDestroyingComponent(IComponent c)
{
if (c.gameObject.Has(out IDestructible destructible, out CollisionProxy collidable))
{
// Remove the system event handler from the proxy
collisionEvents[collidable.onEnter] = null;
}
}
void OnCollision(IDestructible destructible, Collision collision)
{
// Do the condition check and send back the event to the component
if (collision.impulse.magnitude >= destructible.requiredForce)
{
destructible.onDestroy?.Invoke();
// Here you could also add common code for destruction VFX...
}
}
}
You'll often also want to use a SystemComponents
for performance at the same time as registering SystemEvents
. Because this is a very common pattern with GoCS, there are overloads that combine Add
and Has
in one call for convenience.
For example, if you also had:
SystemComponents<IDestructible, ICollisionProxy> components = // ...
Then you could rewrite:
public override void OnCreatedComponent(IComponent c)
{
components.Add(c.gameObject);
if (c.gameObject.Has(out IDestructible destructible, out CollisionProxy collidable))
{
collisionEvents[collidable.onEnter] = collision => OnCollision(destructible, collision);
}
}
To the single-line:
public override void OnCreatedComponent(IComponent c)
{
if (components.Add(c.gameObject, out IDestructible destructible, out CollisionProxy collidable))
{
collisionEvents[collidable.onEnter] = collision => OnCollision(destructible, collision);
}
}
Thanks to these helpers, our system code stays very readable:
class DestructionSystem : BaseSystem
{
SystemComponents<IDestructible, ICollisionProxy> components = new SystemComponents<IDestructible, ICollisionProxy>();
SystemEvents<Collision> collisionEvents = new SystemEvents<Collision>();
public override void OnCreatedComponent(IComponent c)
{
if (components.Add(c.gameObject, out IDestructible destructible, out CollisionProxy collidable))
{
collisionEvents[collidable.onEnter] = collision => OnCollision(destructible, collision);
}
}
public override void OnDestroyingComponent(IComponent c)
{
if (components.Remove(c.gameObject, out IDestructible destructible, out CollisionProxy collidable))
{
collisionEvents[collidable.onEnter] = null;
}
}
void OnCollision(IDestructible destructible, Collision collision)
{
if (collision.impulse.magnitude >= destructible.requiredForce)
{
destructible.onDestroy?.Invoke();
}
}
}
GoCS come packaged with BaseComponent
and BaseSystem
base classes to get you started quickly.
Both of these classes are derived from MonoBehaviour
.
However, if you need your components or systems to derive from other root classes, you don't have to use those provided by GoCS. You only have to implement the IComponent
and ISystem
interfaces. The API provides a BaseImplementation
static helper class that makes this process easy and future-proof.
Let's say you are making an online game with UNET and thus need to use NetworkBehaviour
instead of MonoBehaviour as your base component type.
To make it GoCS-compatible, you could create your own BaseNetworkComponent
class:
// Derive from NetworkBehaviour and implement IComponent
public abstract class BaseNetworkComponent : NetworkBehaviour, IComponent
{
// Forward to BaseImplementation
protected virtual void Awake()
{
BaseImplementation.ComponentAwake(this);
}
protected virtual void OnEnable()
{
BaseImplementation.ComponentOnEnable(this);
}
protected virtual void OnDisable()
{
BaseImplementation.ComponentOnDisable(this);
}
protected virtual void OnDestroy()
{
BaseImplementation.ComponentOnDestroy(this);
}
}
Systems follow the same principle: you can implement your own ISystem
via BaseImplementation
helpers.
However, if you want to receive the OnCreatedComponent
and OnDestroyingComponent
callbacks, you also need to implement the IWorldCallbackReceiver
class. This interface is optional in case you don't need those callbacks in your system and want minimize its initialization cost. BaseSystem
always implements IWorldCallbackReceiver
.
Finally, systems also don't theoretically need to inherit from MonoBehaviour
or even UnityEngine.Object
. They could live in singletons, scriptable objects, or any other kind of class you have in your architecture. As long as you call Awake + OnEnable when the system "starts" and OnDisable + OnDestroy when the system "stops", everything should work.
// Implement ISystem as a non-UnityEngine.Object class.
public abstract class MyBaseSystem: ISystem, IWorldCallbackReceiver
{
// (Required) Forward lifecycle events to BaseImplementation:
public void Start()
{
BaseImplementation.SystemAwake(this);
BaseImplementation.SystemOnEnable(this);
}
public void Stop()
{
BaseImplementation.SystemOnDisable(this);
BaseImplementation.SystemOnDestroy(this);
}
// (Optional) Implement IWorldCallbackReceiver:
public virtual void OnCreatedComponent(IComponent component) { }
public virtual void OnDestroyingComponent(IComponent component) { }
}
It's perfectly fine to mix GoCS with OOP. In fact, that's one of its main benefits! You should think of GoCS as just another tool in your development toolbox, not as a pattern you have to use everywhere.
For example, if you have a player controller class that works well as a self-contained, encapsulated object, then you should keep it that way. No need to split it into entities, components and systems unless it makes sense to you. As the saying goes, if it ain't broke, don't fix it.
Likewise, if you use an entire other architectural pattern for other parts of your codebase, for example model-view-component (MVC) for your GUI code, you don't need to convert that. GoCS can happily coexist in parallel of all your existing codebase.
GoCS is also designed to let you progressively add it to an existing OOP architecture.
There's no downside and very minimal overhead to having every component in your project derive from BaseComponent, even if you don't use Systems with them. But if for whatever reason you don't want all your components to derive from IComponent, you could create a more flexible base component class that will only use GoCS on derived classes that implement IComponent. This way, you can reuse it as a common base for classes that are GoCS-aware and classes that aren't. For example:
// Don't implement IComponent yet, let the derived classes do that.
public abstract class MyBaseComponent: MonoBehaviour
{
// Forward to BaseImplementation if this is a GoCS component.
protected virtual void Awake()
{
if (this is IComponent component)
{
BaseImplementation.ComponentAwake(this);
}
}
protected virtual void OnEnable()
{
if (this is IComponent component)
{
BaseImplementation.ComponentOnEnable(this);
}
}
protected virtual void OnDisable()
{
if (this is IComponent component)
{
BaseImplementation.ComponentOnDisable(this);
}
}
protected virtual void OnDestroy()
{
if (this is IComponent component)
{
BaseImplementation.ComponentOnDestroy(this);
}
}
}
You can then start adding GoCS at any level in your inheritance hierarchy.
Let's say you have the following inheritance hierarchy:
public class Character : MyBaseComponent { }
public class Player : Character { }
public class NPC : Character { }
Later on in your development process, you realize that NPCs should be interactable to start a conversation. But you don't want player characters to be interactable, because that wouldn't make sense. Instead of rearchitecting your existing classes (that work just fine as is!), you can tackle on GoCS by simply adding component interfaces like IInteractable lower in the chain:
public class Character : MyBaseComponent { }
public class Player : Character { }
public class NPC : Character, IInteractable { } // Minor change, big benefits!
All NPC would have to implement is the members needed by the IInteractable component interface, for example its interaction range and events. With minimal changes to your existing code, you now benefit from the full InteractionSystem
we created earlier.
The GoCS package comes bundled with optional commented examples.
If you're using Unity 2019 or newer, you can import them in your project directly from the Package Manager 2.0 interface.
If you're on Unity 2018, you can find them under the Samples~/
directory of the package and copy them to your project manually.
See: CONTRIBUTING.md
See: API Reference
GoCS is not yet guaranteed to be production ready. There may be breaking API changes in the future. In that case, since GoCS uses Semantic Versioning, a new major version number will be used.
I'm open to suggestions and appreciate bug reports. If you have any, just create a new issue on Github!
- Alvaro Salvagno for early feedback and testing
- Tor Vesteergard for various optimization tips