Feel free to participate and help us improve GoCS!
💡 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.4or newer.
To import GoCS, open Packages/manifest.json and add this line under dependencies:
"dev.lazlo.gocs": "https://github.com/hitcache/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:
usingLazlo.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 interfacepublicinterfaceIInteractable:IComponent{// Attributes // (immutable, so only { get; })floatrange{get;}// Data // (mutable, so both { get; set; })boolisHovered{get;set;}boolisPressed{get;set;}// Events // (only { get; })// ("Event" is a minimal event wrapper class included in the GoCS API)EventonHoverEnter{get;}EventonHoverExit{get;}EventonPress{get;}EventonRelease{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 InterfacepublicclassGrabbable:BaseComponent,IInteractable{// Attributes// (Tip: use [SerializeField] to expose the range field to the inspector// while also implementing the interface's range property.)[SerializeField]privatefloat_range=5;publicfloatrange{get=>_range;set=>_range=value;}// DatapublicboolisHovered{get;set;}publicboolisPressed{get;set;}// EventspublicEventonHoverEnter{get;privateset;}publicEventonHoverExit{get;privateset;}publicEventonPress{get;privateset;}publicEventonRelease{get;privateset;}// InitializationprotectedoverridevoidAwake(){base.Awake();// Create events and link them to our handlersonPress=newEvent(Grab);onRelease=newEvent(LetGo);}// Extra LogicprivatevoidGrab(){transform.parent=Camera.main.transform;}privatevoidLetGo(){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))]publicclassHighlightable:BaseComponent,IInteractable{// Attributes[SerializeField]privatefloat_range=5;publicfloatrange{get=>_range;set=>_range=value;}// DatapublicboolisHovered{get;set;}publicboolisPressed{get;set;}// EventspublicEventonHoverEnter{get;privateset;}publicEventonHoverExit{get;privateset;}publicEventonPress{get;privateset;}publicEventonRelease{get;privateset;}// Extra Attributes[SerializeField]privateColor_color=Color.yellow;publicColorcolor{get=>_color;set=>_color=value;}// Extra DataprivateColornormalColor;// Extra CouplingprivateRendererrenderer;// InitializationprotectedoverridevoidAwake(){base.Awake();renderer=GetComponent<Renderer>();onHoverEnter=newEvent(Highlight);onHoverExit=newEvent(ResetColor);}// Extra LogicprivatevoidHighlight(){normalColor=renderer.material.color;renderer.material.color=color;}privatevoidResetColor(){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 classpublicclassInteractionSystem:BaseSystem{privateIInteractablehovered;privateIInteractablepressed;privatevoidUpdate(){if(pressed==null){varcamera=Camera.main;varray=newRay(camera.transform.position,camera.transform.forward);IInteractableinteractable=null;if(Physics.Raycast(ray,outvarhit,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.cspublicabstractclassBaseInteractable:BaseComponent,IInteractable{protectedoverridevoidAwake(){base.Awake();onHoverEnter=newEvent(OnHoverEnter);onHoverExit=newEvent(OnHoverExit);onPress=newEvent(OnPress);onRelease=newEvent(OnRelease);}// Attributes[SerializeField]privatefloat_range=5;publicfloatrange{get=>_range;set=>_range=value;}// DatapublicboolisHovered{get;set;}publicboolisPressed{get;set;}// EventspublicEventonHoverEnter{get;privateset;}publicEventonHoverExit{get;privateset;}publicEventonPress{get;privateset;}publicEventonRelease{get;privateset;}// Event HandlersprotectedvirtualvoidOnHoverEnter(){}protectedvirtualvoidOnHoverExit(){}protectedvirtualvoidOnPress(){}protectedvirtualvoidOnRelease(){}}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:
publicclassGrabbable:BaseInteractable{protectedoverridevoidOnPress(){transform.parent=Camera.main.transform;}protectedoverridevoidOnRelease(){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(outIHealthhealth,outIRegenableregenable)){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&®enable!=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.
classHealthRegenSystem:BaseSystem{voidUpdate(){foreach(var(health,regenable)inWorld.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:
classInteractionSystem:BaseSystem{// (Previous code...)voidOnDrawGizmos(){Gizmos.color=Color.magenta;foreach(varinteractableinWorld.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:
classHealthRegenSystem:BaseSystem{// Declare and initialize the SystemComponents helperSystemComponents<IHealth,IRegenable>components=newSystemComponents<IHealth,IRegenable>();// OnCreatedComponent is sent to all systems when a new component is createdpublicoverridevoidOnCreatedComponent(IComponentcomponent){// 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 destroyedpublicoverridevoidOnDestroyingComponent(IComponentcomponent){// Remove the component from our SystemComponentscomponents.Remove(component);}voidUpdate(){// Enumerate over the SystemComponents instead of using World.Queryforeach(var(health,regenable)incomponents){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))]interfaceIRegenable: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))]interfaceIDestructible: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:
classZoneSystem:BaseSystem{publicoverridevoidOnCreatedComponent(IComponentcomponent){if(componentisIDestructibledestructible){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:
interfaceIDamageable:IComponent{Event<int>onDamage{get;}}Your handlers signatures would then need to take that parameter:
classDamageable:IDamageable{publicEvent<int>onDamage{get;privateset;}publicfloathealth{get;set;}=100;protectedoverridevoidAwake(){base.Awake();onDamage=newEvent(OnDamage);}privatevoidOnDamage(intdamage){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:
enumElement{Fire,Water,Wind,Earth}structDamageEventArgs{publicintamount{get;}publicElementelement{get;}publicDamageEventArgs(intamount,Elementelement){this.amount=amount;this.element=element;}}interfaceIDamageable: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:
classDestructible:BaseComponent{publicEventonDestroy{get;privateset;}protectedoverridevoidAwake(){base.Awake();onDestroy=newEvent(OnDestroyHandler);}privatevoidOnDestroyHandler(){// ...}}Or, if you prefer, you can make the event propery { get; } only and initialize it inline, then use the AddHandler method in Awake:
classDestructible:BaseComponent{publicEventonDestroy{get;}=newEvent();protectedoverridevoidAwake(){base.Awake();onDestroy.AddHandler(OnDestroyHandler);}privatevoidOnDestroyHandler(){// ...}}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:
interfaceIInteractable:IComponent{// Default interface implementation (no event handling)EventonHoverEnter=>null;EventonHoverExit=>null;EventonPress=>null;EventonRelease=>null;}classGrabbable:BaseComponent,IInteractable{publicEventonPress{get;}=newEvent();publicEventonRelease{get;}=newEvent();// 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:
publicsealedclassCollisionProxy:BaseComponent{publicEvent<Collision>onEnter{get;}=newEvent<Collision>();publicEvent<Collision>onStay{get;}=newEvent<Collision>();publicEvent<Collision>onExit{get;}=newEvent<Collision>();privatevoidOnCollisionEnter(Collisioncollision){onEnter.Invoke(collision);}privatevoidOnCollisionStay(Collisioncollision){onStay.Invoke(collision);}privatevoidOnCollisionExit(Collisioncollision){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))]interfaceIDestructible:IComponent{floatrequiredForce{get;}EventonDestroy{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.
classDestructionSystem:BaseSystem{// Helper class to assign event handlersSystemEvents<Collision>collisionEvents=newSystemEvents<Collision>();publicoverridevoidOnCreatedComponent(IComponentc){if(c.gameObject.Has(outIDestructibledestructible,outCollisionProxycollidable)){// Add the system event handler on the proxycollisionEvents[collidable.onEnter]= collision =>OnCollision(destructible,collision);}}publicoverridevoidOnDestroyingComponent(IComponentc){if(c.gameObject.Has(outIDestructibledestructible,outCollisionProxycollidable)){// Remove the system event handler from the proxycollisionEvents[collidable.onEnter]=null;}}voidOnCollision(IDestructibledestructible,Collisioncollision){// Do the condition check and send back the event to the componentif(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:
publicoverridevoidOnCreatedComponent(IComponentc){components.Add(c.gameObject);if(c.gameObject.Has(outIDestructibledestructible,outCollisionProxycollidable)){collisionEvents[collidable.onEnter]= collision =>OnCollision(destructible,collision);}}To the single-line:
publicoverridevoidOnCreatedComponent(IComponentc){if(components.Add(c.gameObject,outIDestructibledestructible,outCollisionProxycollidable)){collisionEvents[collidable.onEnter]= collision =>OnCollision(destructible,collision);}}Thanks to these helpers, our system code stays very readable:
classDestructionSystem:BaseSystem{SystemComponents<IDestructible,ICollisionProxy>components=newSystemComponents<IDestructible,ICollisionProxy>();SystemEvents<Collision>collisionEvents=newSystemEvents<Collision>();publicoverridevoidOnCreatedComponent(IComponentc){if(components.Add(c.gameObject,outIDestructibledestructible,outCollisionProxycollidable)){collisionEvents[collidable.onEnter]= collision =>OnCollision(destructible,collision);}}publicoverridevoidOnDestroyingComponent(IComponentc){if(components.Remove(c.gameObject,outIDestructibledestructible,outCollisionProxycollidable)){collisionEvents[collidable.onEnter]=null;}}voidOnCollision(IDestructibledestructible,Collisioncollision){if(collision.impulse.magnitude>=destructible.requiredForce){destructible.onDestroy?.Invoke();}}}Important notes on event improvements by RealityStop (https://github.com/RealityStop/)
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.interfaceIPlayerWallet{// AttributesfloatWalletValue{get;}// EventsIHandledEvent<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.)
classPlayerWallet:BaseComponent,IPlayerWallet{// Attributesprivatefloat_WalletValue=0;publicfloatWalletValue{get{return_WalletValue;}privateset{_WalletValue=value;WalletValueChanged?.Invoke(value);}}//Events IHandledEvent<float>Pay{get;privateset}privateEvent<float>WalletValueChanged=newEvent<float>();IEmittedEvent<float>IPlayerWallet.WalletValueChanged=>WalletValueChanged;// Class codeprotectedoverridevoidAwake(){base.Awake();Pay=newEvent<float>(OnPay);}publicvoidOnPay(floatvalue){WalletValue-=value;}}Should you wish to have a bidirectional event, simply use the added IEvent, or the preexisting Event class from GoCS.
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 IComponentpublicabstractclassBaseNetworkComponent:NetworkBehaviour,IComponent{// Forward to BaseImplementationprotectedvirtualvoidAwake(){BaseImplementation.ComponentAwake(this);}protectedvirtualvoidOnEnable(){BaseImplementation.ComponentOnEnable(this);}protectedvirtualvoidOnDisable(){BaseImplementation.ComponentOnDisable(this);}protectedvirtualvoidOnDestroy(){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.publicabstractclassMyBaseSystem:ISystem,IWorldCallbackReceiver{// (Required) Forward lifecycle events to BaseImplementation:publicvoidStart(){BaseImplementation.SystemAwake(this);BaseImplementation.SystemOnEnable(this);}publicvoidStop(){BaseImplementation.SystemOnDisable(this);BaseImplementation.SystemOnDestroy(this);}// (Optional) Implement IWorldCallbackReceiver:publicvirtualvoidOnCreatedComponent(IComponentcomponent){}publicvirtualvoidOnDestroyingComponent(IComponentcomponent){}}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.publicabstractclassMyBaseComponent:MonoBehaviour{// Forward to BaseImplementation if this is a GoCS component.protectedvirtualvoidAwake(){if(thisisIComponentcomponent){BaseImplementation.ComponentAwake(this);}}protectedvirtualvoidOnEnable(){if(thisisIComponentcomponent){BaseImplementation.ComponentOnEnable(this);}}protectedvirtualvoidOnDisable(){if(thisisIComponentcomponent){BaseImplementation.ComponentOnDisable(this);}}protectedvirtualvoidOnDestroy(){if(thisisIComponentcomponent){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:
publicclassCharacter:MyBaseComponent{}publicclassPlayer:Character{}publicclassNPC: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:
publicclassCharacter:MyBaseComponent{}publicclassPlayer:Character{}publicclassNPC: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
- Thank you to https://github.com/RealityStop/gocs for the improvements to events in that fork which have been merged into this one.
- Thank you to https://github.com/Walter-Haynes/gocs for bug fixes in that fork which have been merged into this one.