vanifatovvlad / morpeh

🎲 Fast and Simple Entity Component System (ECS) Framework for Unity Game Engine

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Morpeh

Morpeh

🎲 ECS Framework for Unity Game Engine.

Adapted for the rapid development mobile games like:
    πŸ“• Hyper Casual
    πŸ“— Idlers
    πŸ“˜ Arcades
    πŸ“š Other genres

Features:

  • Simple Syntax.
  • Plug & Play Installation.
  • No code generation.
  • Structure-Based and Cache-Friendly.
  • Built-in Events and Reactive Variables.
  • Single-threaded.

πŸ“– Table of Contents

✈️ Migration To New Version

English version: Migration Guide
Russian version: Π“Π°ΠΉΠ΄ ΠΏΠΎ ΠΌΠΈΠ³Ρ€Π°Ρ†ΠΈΠΈ

πŸ“– How To Install

Minimal Unity Version is 2019.4.*
Require Git + Git LFS for installing package.
Currently require Odin Inspector for drawing in inspector.

Open Package Manager and add Morpeh URL.

installation_step1.png
installation_step2.png

    β­ Master: https://github.com/scellecs/morpeh.git
    πŸš§ Dev: https://github.com/scellecs/morpeh.git#develop
    πŸ·οΈ Tag: https://github.com/scellecs/morpeh.git#2022.1.1

You can update Morpeh by Discover Window. Select Help/Morpeh Discover menu.

update_morpeh.png

πŸ“– Introduction

πŸ“˜ Base concept of ECS pattern

πŸ”– Entity

Container of components.
Has a set of methods for add, get, set, remove components.

var entity = this.World.CreateEntity();

ref var addedHealthComponent  = ref entity.AddComponent<HealthComponent>();
ref var gottenHealthComponent = ref entity.GetComponent<HealthComponent>();

bool removed = entity.RemoveComponent<HealthComponent>();
entity.SetComponent(new HealthComponent {healthPoints = 100});

bool hasHealthComponent = entity.Has<HealthComponent>();

πŸ”– Component

Components are types which include only data.
In Morpeh components are value types for performance purposes.

public struct HealthComponent : IComponent {
    public int healthPoints;
}

πŸ”– System

Types that process entities with a specific set of components.
Entities are selected using a filter.

public class HealthSystem : ISystem {
    public World World { get; set; }

    private Filter filter;

    public void OnAwake() {
        this.filter = this.World.Filter.With<HealthComponent>();
    }

    public void OnUpdate(float deltaTime) {
        foreach (var entity in this.filter) {
            ref var healthComponent = ref entity.GetComponent<HealthComponent>();
            healthComponent.healthPoints += 1;
        }
    }

    public void Dispose() {
    }
}

πŸ”– World

A type that contains entities, components caches, systems and root filter.

var newWorld = World.Create();

var newEntity = newWorld.CreateEntity();
newWorld.RemoveEntity(newEntity);

var systemsGroup = newWorld.CreateSystemsGroup();
systemsGroup.AddSystem(new HealthSystem());

newWorld.AddSystemsGroup(order: 0, systemsGroup);
newWorld.RemoveSystemsGroup(systemsGroup);

var filter = newWorld.Filter.With<HealthComponent>();

var healthCache = newWorld.GetCache<HealthComponent>();

πŸ”– Filter

A type that contains entities constrained by conditions With and/or Without.
You can chain them in any order and quantity.

var filter = this.World.Filter.With<HealthComponent>()
                              .With<BooComponent>()
                              .Without<DummyComponent>();

var firstEntityOrException = filter.First();
var firstEntityOrNull = filter.FirstOrDefault();

bool filterIsEmpty = filter.IsEmpty();
int filterLengthCalculatedOnCall = filter.GetFilterLength();

πŸ”– Cache

A type that contains components.
You can get components and do other operations directly from the cache, because entity methods look up the cache each time.
However, such code is harder to read.

var healthCache = this.World.GetCache<HealthComponent>();
var entity = this.World.CreateEntity();

ref var addedHealthComponent  = ref healthCache.AddComponent(entity);
ref var gottenHealthComponent = ref healthCache.GetComponent(entity);

bool removed = healthCache.RemoveComponent(entity);
healthCache.SetComponent(entity, new HealthComponent {healthPoints = 100});

bool hasHealthComponent = healthCache.Has(entity);

πŸ“˜ Getting Started

πŸ’‘ IMPORTANT
For a better user experience, we strongly recommend having Odin Inspector and FindReferences2 in the project.
All GIFs are hidden under spoilers.

After installation import ScriptTemplates and Restart Unity.

import_script_templates.gif

Let's create our first component and open it.

Right click in project window and select Create/ECS/Component.

create_component.gif

After it, you will see something like this.

using Morpeh;
using UnityEngine;
using Unity.IL2CPP.CompilerServices;

[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
[System.Serializable]
public struct HealthComponent : IComponent {
}

πŸ’‘ Don't care about attributes.
Il2CppSetOption attribute can give you better performance.

Add health points field to the component.

public struct HealthComponent : IComponent {
    public int healthPoints;
}

It is okay.

Now let's create first system.

Right click in project window and select Create/ECS/System.

create_system.gif

πŸ’‘ Icon U means UpdateSystem. Also you can create FixedUpdateSystem and LateUpdateSystem.
They are similar as MonoBehaviour's Update, FixedUpdate, LateUpdate.

System looks like this.

using Morpeh;
using UnityEngine;
using Unity.IL2CPP.CompilerServices;

[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
[CreateAssetMenu(menuName = "ECS/Systems/" + nameof(HealthSystem))]
public sealed class HealthSystem : UpdateSystem {
    public override void OnAwake() {
    }

    public override void OnUpdate(float deltaTime) {
    }
}

We have to add a filter to find all the entities with HealthComponent.

public sealed class HealthSystem : UpdateSystem {
    private Filter filter;
    
    public override void OnAwake() {
        this.filter = this.World.Filter.With<HealthComponent>();
    }

    public override void OnUpdate(float deltaTime) {
    }
}

πŸ’‘ You can chain filters by two operators With<> and Without<>.
For example this.World.Filter.With<FooComponent>().With<BarComponent>().Without<BeeComponent>();

The filters themselves are very lightweight and are free to create.
They do not store entities directly, so if you like, you can declare them directly in hot methods like OnUpdate().
For example:

public sealed class HealthSystem : UpdateSystem {
    
    public override void OnAwake() {
    }

    public override void OnUpdate(float deltaTime) {
        var filter = this.World.Filter.With<HealthComponent>();
        
        //Or just iterate without variable
        foreach (var entity in this.World.Filter.With<HealthComponent>()) {
        }
    }
}

But we will focus on the option with caching to a variable, because we believe that the filters declared in the header of system increase the readability of the code.

Now we can iterate all needed entities.

public sealed class HealthSystem : UpdateSystem {
    private Filter filter;
    
    public override void OnAwake() {
        this.filter = this.World.Filter.With<HealthComponent>();
    }

    public override void OnUpdate(float deltaTime) {
        foreach (var entity in this.filter) {
            ref var healthComponent = ref entity.GetComponent<HealthComponent>();
            Debug.Log(healthComponent.healthPoints);
        }
    }
}

πŸ’‘ Don't forget about ref operator.
Components are struct and if you want to change them directly, then you must use reference operator.

For high performance, you can use cache directly.
No need to do GetComponent from entity every time, which trying to find suitable cache.
However, we use such code only in very hot areas, because it is quite difficult to read it.

public sealed class HealthSystem : UpdateSystem {
    private Filter filter;
    private ComponentsCache<HealthComponent> healthCache;
    
    public override void OnAwake() {
        this.filter = this.World.Filter.With<HealthComponent>();
        this.healthCache = this.World.GetCache<HealthComponent>();
    }

    public override void OnUpdate(float deltaTime) {
        foreach (var entity in this.filter) {
            ref var healthComponent = ref healthCache.GetComponent(entity);
            Debug.Log(healthComponent.healthPoints);
        }
    }
}

We will focus on a simplified version, because even in this version entity.GetComponent is very fast.

Let's create ScriptableObject for HealthSystem.
This will allow the system to have its inspector and we can refer to it in the scene.

Right click in project window and select Create/ECS/Systems/HealthSystem.

create_system_scriptableobject.gif

Next step: create Installer on the scene.
This will help us choose which systems should work and in which order.

Right click in hierarchy window and select ECS/Installer.

create_installer.gif

Add system to the installer and run project.

add_system_to_installer.gif

Nothing happened because we did not create our entities.
I will show the creation of entities directly related to GameObject, because to create them from the code it is enough to write world.CreateEntity().
To do this, we need a provider that associates GameObject with an entity.

Create a new provider.

Right click in project window and select Create/ECS/Provider.

create_provider.gif

using Morpeh;
using Unity.IL2CPP.CompilerServices;

[Il2CppSetOption(Option.NullChecks, false)]
[Il2CppSetOption(Option.ArrayBoundsChecks, false)]
[Il2CppSetOption(Option.DivideByZeroChecks, false)]
public sealed class HealthProvider : MonoProvider<{YOUR_COMPONENT}> {
}

We need to specify a component for the provider.

public sealed class HealthProvider : MonoProvider<HealthComponent> {
}
Create new GameObject and add HealthProvider.

add_provider.gif

Now press the play button, and you will see Debug.Log with healthPoints.
Nice!

πŸ“– Advanced

πŸ“˜ Event system. Singletons and Globals Assets.

There is an execution order in the ECS pattern, so we cannot use standard delegates or events, they will break it.

ECS uses the concept of a deferred call, or the events are data.
In the simplest cases, events are used as entities with empty components called tags or markers.
That is, in order to notify someone about the event, you need to create an entity and add an empty component to it.
Another system creates a filter for this component, and if there are entities, we believe that the event is published.

In general, this is a working approach, but there are several problems.
Firstly, these tags overwhelm the project with their types, if you wrote a message bus, then you understand what I mean.
Each event in the game has its own unique type and there is not enough imagination to give everyone a name.
Secondly, it’s uncomfortable working with these events from MonoBehaviours, UI, Visual Scripting Frameworks (Playmaker, Bolt, etc.)

As a solution to this problem, global assets were created.

πŸ”– Singleton is a simple ScriptableObject that is associated with one specific entity.
It is usually used to add dynamic components to one entity without using filters.

πŸ”– GlobalEvent is a Singleton, which has the functionality of publishing events to the world by adding a tag to its entity.
It has 4 main methods for working with it:

  1. Publish (arg) - publish in the next frame, all systems will see this.
  2. IsPublished - did anyone publish this event
  3. BatchedChanges - a data stack where publication arguments are added.

πŸ”– GlobalVariable is a GlobalEvent that stores the start value and the last value after the changes.
It also has the functionality of saving and loading data from PlayerPrefs.

You can create globals by context menu Create/ECS/Globals/ in Project Window.
You can declare globals in any systems, components and scripts and set it by Inspector Window, for example:

public sealed class HealthSystem : UpdateSystem {
    public GlobalEvent myEvent;
    ...
}

And check their publication with:

public sealed class HealthSystem : UpdateSystem {
    public GlobalEvent myEvent;
    ...
    public override void OnUpdate(float deltaTime) {
        if (myEvent.IsPublished) {
            Debug.Log("Event is published");
        }
    }
}

And there is also a variation with checking for null:

public sealed class HealthSystem : UpdateSystem {
    public GlobalEvent myEvent;
    ...
    public override void OnUpdate(float deltaTime) {
        if (myEvent) {
            Debug.Log("Event is not null and is published");
        }
    }
}

🧨 Unity Jobs And Burst

πŸ’‘ Supported only in Unity. Subjected to further improvements and modifications.

You can convert Filter<T> to NativeFilter<TNative> which allows you to do component-based manipulations inside a Job.
Conversion of ComponentsCache<T> to NativeCache<TNative> allows you to operate on components based on entity ids.

Current limitations:

  • NativeFilter and NativeCache and their contents should never be re-used outside of single system tick.
  • NativeFilter and NativeCache cannot be used in-between UpdateFilters calls inside Morpeh.
  • NativeFilter should be disposed upon usage completion due to scellecs#107, which also means NativeFilter causes a single Allocator.TempJob NativeArray allocation.
  • Jobs can be chained only within current system execution, NativeFilter can be disposed only after execution of all scheduled jobs.

Example job scheduling:

public sealed class SomeSystem : UpdateSystem {
    private Filter filter;
    private ComponentsCache<HealthComponent> cache;
    ...
    public override void OnUpdate(float deltaTime) {
        using (var nativeFilter = this.filter.AsNative()) {
            var parallelJob = new ExampleParallelJob {
                entities = nativeFilter,
                healthComponents = cache.AsNative(),
                // Add more native caches if needed
            };
            var parallelJobHandle = parallelJob.Schedule(nativeFilter.length, 64);
            parallelJobHandle.Complete();
        }
    }
}

Example job:

[BurstCompile]
public struct TestParallelJobReference : IJobParallelFor {
    [ReadOnly]
    public NativeFilter entities;
    public NativeCache<HealthComponent> healthComponents;
        
    public void Execute(int index) {
        var entityId = this.entities[index];
        
        ref var component = ref this.healthComponents.GetComponent(entityId, out var exists);
        if (exists) {
            component.Value += 1;
        }
        
        // Alternatively, you can avoid checking existance of the component
        // if the filter includes said component anyway
        
        ref var component = ref this.healthComponents.GetComponent(entityId);
        component.Value += 1;
    }
}

πŸ“š Examples

πŸ”₯ Games

πŸ“˜ License

πŸ“„ MIT License

πŸ’¬ Contacts

βœ‰οΈ Telegram: olegmrzv
πŸ“§ E-Mail: benjminmoore@gmail.com
πŸ‘₯ Telegram Community RU: Morpeh ECS Development

About

🎲 Fast and Simple Entity Component System (ECS) Framework for Unity Game Engine

License:MIT License


Languages

Language:C# 100.0%