Home

Awesome

Twitter Follow Discord license openupm stars

Actors is a framework empowering developers to make better games faster on Unity.

[Due our shifting from Unity, not actively supported]

⭐ Why Actors ?

The framework favors simplicity and doesn't try to "abstract away" Unity workflows bringing first-class support for combining ECS with classic Unity component system.

How To Install - Projects - About - Changelog

📖 ECS Overview

🔖 Entities are containers for components. Entity ID is represented by ent structure.

💬 How to create an entity?

// Bare entity
ent e = Entity.Create();

Entities can hold information about unity objects as well!

// New entity with a new GO ( GameObject ).
// The GO prefab will be taken from the Resources folder by string ID.
ent e = Entity.Create("Obj Fluffy Unicorn");
// Access to the transform of Obj Fluffy Unicorn gameobject.
e.transform.position = new Vector3(0,0,0) ;

You can pool gameobject by adding true variable in the end of the method.

public GameObject prefabFluffyUnicorn;
// New entity with a new GO ( GameObject ).
// The GO will be created from the provided prefab.
// The GO will be pooled.
ent e = Entity.Create(prefabFluffyUnicorn, true);
// Access to the transform of Obj Fluffy Unicorn gameobject.
e.transform.position = new Vector3(0,0,0) ;
// New entity with a new GO ( GameObject ).
// The GO prefab will be taken from the Resources folder by string ID. 
// The GO will be pooled.
ent e = Entity.Create("Obj Fluffy Unicorn", true);
// Access to the transform of Obj Fluffy Unicorn gameobject.
e.transform.position = new Vector3(0,0,0) ;

Entity API is layer specific. You will find more info about layers below. If you want to create an entity on a different layer you need to use slightly different approach:

// we use LayerApp API to create entity inside of the LayerApp.
ent e = LayerApp.Entity.Create("Obj Fluffy Unicorn", true);
// Access to the transform of Obj Fluffy Unicorn gameobject.
e.transform.position = new Vector3(0,0,0) ;

🔖 Components are containers for data.

sealed class ComponentAlpaca
{
  public int fluffiness;
}
<details> <summary> To create a new component right click in project window and select Create->Actors->ECS Component </summary> <a href="https://gyazo.com/9b1735ba6c7b7844c222b9b439fc8dcb"><img src="https://i.gyazo.com/9b1735ba6c7b7844c222b9b439fc8dcb.gif" alt="Component creation" width="800"/></a> </details>

💡 For comfortable workflow a component is created together with special helper class. While you can live without that helper all examples are provided with use of it.

💬 How to add components?
There are several ways to add components depending on the context of your code. In case you want to setup your new entity use Set method. Use Set method only with newly created entities.

public void SomeMethod()
{    
     ent e = Entity.Create("Obj Bunny");
     // Add components
     var cCute       = e.Set<ComponentCute>();
     var cJumping    = e.Set<ComponentJumping>();
     var cConsumable = e.Set<ComponentConsumable>();
     var cPoo        = e.Set<ComponentCanPoo>();

     // Component Cute
     cCute.attractivness = float.PositiveInfinity;
     // Component Jumping
     cJumping.power = 100;
}

Use Get method when you unsure if entity has particular component. This method will check and add component if it doesn't exist.

public void SomeMethod()
{    
     // where e is some entity.
     // Add components
     var cCute       = e.Get<ComponentCute>();
     var cJumping    = e.Get<ComponentJumping>();
     var cConsumable = e.Get<ComponentConsumable>();
     var cPoo        = e.Get<ComponentCanPoo>();

     // Component Cute
     cCute.attractivness = float.PositiveInfinity;
     // Component Jumping
     cJumping.power = 100;

}

🔖 Systems in Actors are called Processors. Processors execute game logic with groups. Groups are containers for entities filtered by components mask.

  public class ProcessorAlpaca : Processor, ITick
  {
    // All entities that have ComponentAlpaca will be in this group.
    readonly Group<ComponentAlpaca> alpacas = default;
    
    public void Tick(float dt)
    {
      // iterate through entities in the group
      foreach (var alpaca in alpacas)
      {
        // retrieve a component
        var cAlpaca = alpaca.ComponentAlpaca();
      }
    }
  }
<details> <summary> To create a new processor right click in project window and select Create->Actors->ECS Processor. </summary> <a href="https://gyazo.com/e550c2a6fa035c0d877e5491414febcf"><img src="https://i.gyazo.com/e550c2a6fa035c0d877e5491414febcf.gif" alt="Add processor" width="800"/></a> </details>

🔖 Layers are containers for your processors and entry point of the scene. You can think about layers as ECS worlds, but they are much more then that. In fact, everything in Actors controlled by layers. Every unity scene must hold no more than one layer.

using Pixeye.Actors;

public class LayerGame : Layer<LayerGame>
{
  protected override void Setup()
  {
    // the order affects on execution
    Add<ProcessorAlpaca>();
    Add<ProcessorDamage>();
    Add<ProcessorAnimations>();
  }
}
<details> <summary> To create a new layer right click in project window and select Create->Actors->Layer </summary> <a href="https://gyazo.com/8bf54796394aead4cead1ad059fabdf9"><img src="https://i.gyazo.com/8bf54796394aead4cead1ad059fabdf9.gif" alt="Layers="000"/></a> </details>

📖 Scenes Overview

The framework favors multiscene architecture in Unity. The concept is simple: you have one main scene that can be only changed for another scene and subscenes that can be added/removed when needed. Normally scene will have a logic layer of the framework. This will be explained further.

💡 The main (or it's also called active in Unity) scene name font style is bold inside the Hierarchy window.

public class ProcessorAlpaca : Processor, ITick
{
  public void Tick(float dt)
  {
      // Example of how to change the main scene.
      if (Input.GetKeyDown(KeyCode.Alpha1))
        SceneMain.ChangeTo(1);                       // by index
    
      if (Input.GetKeyDown(KeyCode.Alpha2))
        SceneMain.ChangeTo("Scene Alpaca Fields");   // by name

      // Example of how to work with subscenes.
      if (Input.GetKeyDown(KeyCode.Alpha1))
      {
        SceneSub.Add("Alpaca Shop");    //  by name
        SceneSub.Add(2);                //  by index
      }
      if (Input.GetKeyDown(KeyCode.Alpha2))
      {
        SceneSub.Remove("Alpaca Shop"); //  by name
        SceneSub.Remove(2);             //  by index
      }
  }
}

💡 Don't forget to add scene in the build settings of your game.

Note that you can have only one copy of each scene. In case you try to add scene twice the framework will ignore the call.

📖 Layers Overview

As already said, layers are very important part of the framework. Each layer represents a unity scene and holds several important modules of the framework. When the layer is removed or changed everything that is inside of the layer will be cleaned up. Another important thing is that any actor or monocached components will start working only after initialization and setup of the layer. The concepts of actor and monocached will be discussed further.

📘 Obj

Obj is a module that controls gameobject instantiating. Entities and actors with prefabs are created through obj module. 💬 How to instantiate gameobjects?
🔖 From string: the prefab will be taken from your resources folder. You provide the path. The result will be cached so next time you use the string it will take prefab from the cache. Transform of the created gameobject will be returned.

Obj.Create("Obj Alpaca");

🔖 From prefab: you directly provide a prefab. Transform of the created gameobject will be returned.

GameObject PrefabAlpaca; 
Obj.Create(PrefabAlpaca);

🔖 Pooling gameobjects: you can take a gameobject from a pool. Transform of the created gameobject will be returned. // create new gameobject from prefab.

Obj.Create(Pool.Entities,"Obj Alpaca");

💬 How to release gameobjects?

 var transform = Obj.Create("Obj Alpaca");
 transform.gameObject.Release();

In case you used pool you must provide the pool ID.

 var transform = Obj.Create(Pool.Entities, "Obj Alpaca");
 transform.gameObject.Release(Pool.Entities);

💡 You don't need to manually release gameobjects created by entities/actors. Those objects are controlled by entities and will be released with them. You also don't need to define pool id in the entity.Release method as entity already knows if it is pooled.

📘 Time

Time is a module that controls time flow. Each layer has it's own time and timescale.

 public class ProcessorAlpaca : Processor, ITick
  {
    public void Tick(float dt)
    {
      if (Input.GetKeyDown(KeyCode.Space))
        Time.scale = 0.5f;
    }
  }

📘 Engine

Engine is a centralized update processor based on Unity update. Think of it as a one Unity update that controls them all. Engine work inside of a layer and will be stopped if the layer they work on would be destroyed. You are not forced to use custom updates but every processor and framework module work via engine.

Why use this module?

💬 How to use custom updates? First, you need to add a special interface for using updates.

If you are using those interfaces with processors then this is all you need to provide.

public class ProcessorAlpaca : Processor, ITick, ITickFixed
  {
    public void Tick(float dt)
    {
    }

    public void TickFixed(float dt)
    {
      
    }
  }

In case you are using custom class or monocached/actor class you will need to manually add your object to the engine.

public class ActorAlpaca : Actor, ITick
  {
    protected override void HandleEnable() => Engine.Add(this);     // add this to ticks.
    protected override void HandleDisable() => Engine.Remove(this); // remove this from ticks.

    public void Tick(float dt)
    {
    }
  }

💡 Don't forget to remove update from engine when object is destroyed/deactivated.

📘 Observers

Observers allows to handle changes of a variable. It's very handy for working with UI but can be used in game logic too. Routines work inside of a layer and will be stopped if the layer they work on would be destroyed.

💬 How to use observers?
You need to register the variable you are looking on and the callback.

public class ProcessorAlpaca : Processor, ITick
  {
    public int level;

    public ProcessorAlpaca()
    {
      Observer.Add(this, x => x.level,levelNext => Debug.Log($"Alpaca's level has changed {levelNext}!"));
    }

    public void Tick(float dt)
    {
      if (Input.GetKeyDown(KeyCode.A))
      {
        level++;
      }
    }
  }

💡 Processors, Actors, and Monocached objects know about the layer and can directly access Observer.

In case you are working out of Processor, Actor, or Monocached scope you can access Observer by pointing to a specific layer. Note that layer must be active.

// Take Observer of the LayerApp. This is just an example.
Layer<LayerApp>.Observer.Add(this, x => x.level,
        levelNext => Debug.Log($"Alpaca's level has changed {levelNext}!"));

💬 How to release observers? Observers work inside of ECS so when you register a new observer you will get an entity of the observer. To release observer all you have to do is to release the provided entity.

public int level;
public ent levelObserver;

public ProcessorAlpaca()
{
  levelObserver = Observer.Add(this, x => x.level,
  levelNext => Debug.Log($"Alpaca's level has changed {levelNext}!"));
}

public void Tick(float dt)
{
  if (Input.GetKeyDown(KeyCode.A))
  {
     levelObserver.Release();
  }
}

💬 How to add comparers? In order to check values, comparers are provided. If you want to check custom type you need a custom comparer.

  // custom value
  public struct AlpacaPower
  {
    public int power;
  }
  
  // comparer
  sealed class EqualityAlpacaPower : IEqualityComparer<AlpacaPower>
  {
    internal static EqualityAlpacaPower Default = new EqualityAlpacaPower();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool Equals(AlpacaPower x, AlpacaPower y)
    {
      return x.power == y.power;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public int GetHashCode(AlpacaPower obj)
    {
      return obj.power;
    }
  }

Once, somewhere at the very beginning of your game register the comparer. Note that comparer must be provided before you start observing the value.

Comparers.Add(EqualityAlpacaPower.Default);

Now you can use your custom values!

public class ProcessorAlpaca : Processor, ITick
{
   public AlpacaPower alpacaPower;
   public ProcessorAlpaca()
   {
     Observer.Add(this, x => alpacaPower, 
        alpacaPowerNext => Debug.Log($"Alpaca's level has changed {alpacaPowerNext.power}!"));
   }
   public void Tick(float dt)
   {
      if (Input.GetKeyDown(KeyCode.A))
      {
        alpacaPower.power++;
      }
    }
}

🔖 Comparers for types added by default:

📘 Buffers

Buffers are iterators for structs. Buffers perform simple actions that don't require composition and ECS stuff. Buffers designed to be fast and despite their dynamic nature, they don't copy structs every time buffer grows. Instead, buffer uses an array of indexes to refer to the struct.

💬 How to use buffers?

Create new struct. I prefer to call them segments.

  public struct SegmentShoot
  {
    public Transform obj;
    public Vector3 position;
    public float speed;
  }

Register new buffer via layer setup.

using Pixeye.Actors;

public class LayerGame : Layer<LayerGame>
{
  protected override void Setup()
  {
    // the order affects on execution
    Add<ProcessorShoot>(); // we will use this processor for our buffer.
    Add<Buffer<SegmentShoot>>(); // register new buffer.
  }
}

Add a new segment to the buffer.

 ref var segShoot = ref Layer.GetBuffer<SegmentShoot>().Add();
 segShoot.obj             =  bullet;
 segShoot.position        = position;
 segShoot.speed           = 10f;

If you need to cache the pointer to the segment in the buffer than use slightly different method:

 var pointer = 0;
 ref var segShoot = ref Layer.GetBuffer<SegmentShoot>().Add(out pointer);
 segShoot.obj             =  bullet;
 segShoot.position        = position;
 segShoot.speed           = 10f;

Use Processor to work with your buffer. You are not forced to use processors but it's a convenient way.

public class ProcessorShoot : Processor, ITick
{
    public void Tick(float dt)
    {
      var shoots     = Layer.GetBuffer<SegmentShoot>(); // receive buffer.
      foreach (var pointer in shoots) // remember that we get a pointer, not the element of the buffer.
      {
        ref var source   = ref shoots[pointer];
        ref var position = ref source.position;
        position.x += source.speed * dt;
        if (position.x >= 100)
        {
            shoots.RemoveAt(pointer); // remove buffer element by pointer.
        }
      }
    }
}

💡 Remember that you receive pointer when iterating buffers. Use the pointer both to get struct or to remove struct from the buffer.
💡 Custom particles or objects that are created and destroyed a lot are ideal candidates for buffers.

📘 Routines

Routines are custom coroutines that can work outside of Unity scope and more performant than native Unity coroutines. Routines work inside of a layer and will be stopped if the layer they work on would be destroyed. Routines have similar to Coroutines API so it will be very easy to start using them. There are two types of routines:

public class ProcessorAlpaca : Processor, ITick
{
  IEnumerator CoHangWithAlpaca()
  {
     yield return Layer.Wait(2.0f);
     yield return Layer.WaitFrame;
     Debug.Log("Stop hanging around");
  }

  public void Tick(float dt)
  {
      if (Input.GetKeyDown(KeyCode.Alpha1))
      {
        var routine = Layer.Run(CoHangWithAlpaca()); // run routine and get routine call.
        routine.Stop(); // stop the routine
        routine = Layer.RunUnscaled(CoHangWithAlpaca()); // run routine that ignores time scale.
      }
  }
}

🔖 Wait for are delayed actions based on coroutines.

if (Input.GetKeyDown(KeyCode.Alpha1))
{
   Layer.WaitFor(1.0f, () => { Debug.Log("Hello!"); });
}

🔖 Wait is used to wait for a period of time or to finish another routine.

IEnumerator CoWorld()
    {
      yield return Layer.Run(CoHello()).Wait();
      Debug.Log("World !");
    }
IEnumerator CoHello()
    {
      Debug.Log("Hello");
      yield return Layer.Wait(1.0f);
    }

📖 Monocached Overview

Monocached classes are basic monobehavior classes in the world of Actors. They are intialized after the layer and use special Setup method for Awaking. It's better to create monocomponents inherited from monocached.

💬 How to use monocached?

public class MonoAlpaca : Monocached
{
    // use instead of the start method.
    protected override void Setup()
    {
    }
    // use instead of the enable method.
    protected override void HandleEnable()
    {
    }  
    // use instead of the disable method.
    protected override void HandleDisable()
    {
    }    
}

💡 Actors will be initialized after the layer.

📖 Actor Overview

The Actor is a monobehavior class inherited from the monocached that allows communicating between unity game objects and ecs. Most of the time you will inherit from actors when defining new objects.

The actor class is for setting your entity. You can use it as view class if you like MVC patterns and define logic inside of the actor.

💬 How to use actors?

public class ActorAlpaca : Actor
  {
    // you are not forced to use attribute.
    [FoldoutGroup("Components", true)]
    public ComponentHealth componentHealth;
    public ComponentDamager componentDamager;
    
    // use instead of the start method to initialize the entity.
    protected override void Setup()
    {
      entity.Set(componentHealth);
      entity.Set(componentDamager);
    }
  }

💡 Actors will be initialized after the layer.

<details> <summary> Actor in the inspector </summary> <a href="https://gyazo.com/742384979f2cf627c5b89c93a854cf38"><img src="https://i.gyazo.com/742384979f2cf627c5b89c93a854cf38.gif" alt="Actor="000"/></a> </details>

🔖 IsPooled
Actors on scene may be pooled. To do this you need to toggle IsPooled variable in the actor's inspector panel.

💡 Pooled actors must be unity prefabs. This won't work for plain objects in the scene.

📖 Advanced <a name="Advanced"></a>

📘 ECS entity childs

You can child an entity. Child entities will be released along with the parent.

var e = Entity.Create();
var eChild = Entity.Create();
eChild.SetParent(e);
// you can remove the parent from a child.
eChild.Unparent();

📘 ECS moving entities to another layer

You can move entities to different layers. This is a relatively complex operation where entity will be removed from the current layer ECS systems and migrated to the new layer ECS. If an entity is with gameobject then gameobject will be moved to the layer's scene.

// assuming we create alpaca in the LayerGame
var e = Entity.Create("Obj Alpaca");
e.MoveTo<LayerField>();

📘 ECS Events

You can perform actions when entities are added or removed from the groups.
💬 How to use ecs events?
You need to use the HandleEcsEvents method and iterate through added/removed stack of the group.

public class ProcessorTest : Processor, ITick
  {
    Group<ComponentHealth> objects;

    public override void HandleEcsEvents()
    {
      foreach (var e in objects.added)
      {
        Debug.Log($"{e} was added!");
      }

      foreach (var e in objects.removed)
      {
        Debug.Log($"{e} was removed!");
      }
    }

    public void Tick(float dt)
    {
      if (Input.GetKeyDown(KeyCode.Space))
      {
        var entity = Entity.Create();
        entity.Set<ComponentHealth>();
      }

      if (Input.GetKeyDown(KeyCode.Q))
      {
        if (objects.length > 0)
          objects[0].Release();
      }
    }
  }

📘 ECS Signals

ECS Signals are special events that are send through all processors on the layer. This is a very powerful concept and I don't recommend to use it until you really understand what are you doing. These events can be changed or even stopped on their route.

💬 How they work?
Ecs signals are deterministic and sent in order. See example below:

 public class LayerBattle : Layer<LayerBattle>
  {
    protected override void Setup()
    {
      Add<ProcessorReceiverA>(); // receive the event ( second receiver )
      Add<ProcessorSender>();    // send the event.
      Add<ProcessorReceiverB>(); // receive the event ( first receiver ) 
    }
  }

💬 How to subscribe on ecs signal?
Inherit from Processor<T>. T type is your event.

public class ProcessorReceiverA : Processor<SignalGameStarted>
  {
    public override void ReceiveEcs(ref SignalGameStarted signal, ref bool stopSignal)
    {
      Debug.Log(signal.message);
      stopSignal = true; // you can stop signal from moving on to the next processor.
    }
  }

💬 How to send ecs signal?

public class ProcessorSender : Processor, ITick
  {
    public void Tick(float dt)
    {
      if (Input.GetKeyDown(KeyCode.Space))
      {
        SignalGameStarted signal;
        signal.message = "hello world";
        Ecs.Send(signal);
      }
    }
  }

📘 Signals

Signals are pub/sub message system.

💬 How to add signals?
Inherit your class from IReceive<T> interface and add signal to the layer.

public class ActorAlpaca : Actor, IReceive<SignalGameStarted>
  {
    // add signal to layer that actor belongs to.
    protected override void HandleEnable() => Layer.AddSignal(this);    
    // remove signal from layer when actor is disabled/destroyed. 
    protected override void HandleDisable() => Layer.RemoveSignal(this);

    public void HandleSignal(in SignalGameStarted arg)
    {
      Debug.Log(arg.message);
    }
  }

💬 How to send signals?

public class ProcessorAlpaca : Processor, ITick
  {
    public void Tick(float dt)
    {
      // will send a signal to the layer where ProcessorAlpaca registered.
      if (Input.GetKeyDown(KeyCode.Space))
      {
        SignalGameStarted signal;
        signal.message = "hello world!";
        Layer.Send(signal);
      }
      
      // will send a signal to all layers.
      if (Input.GetKeyDown(KeyCode.Q))
      {
        SignalGameStarted signal;
        signal.message = "hello world!";
        Every.Layer.Send(signal);
      }
    }
  }

📘 Box

Box is a small wrapper to take assets from the resources foler.

🔖 Box.Load is used to load asset as Resources.Load.

var data = Box.Load<DataObject>("Data Hero Alpaca");
var sprites = Box.LoadAll<Sprite>("Tex Hero Alpaca"); // take elements from array.

🔖 Box.Get is used to load and cache asset. When you load the asset for the first time it will be saved in dictionary. Next time you will get it from the dict.

Box.Get<DataObject>("Data Hero Alpaca");

📘 Group Bindings

You can bind a group with an id and later get this group by this id. This is very useful when you need to work with several groups and want to filter them. For example you would want to raycast against entities that are in a group of allies and a group of objects.

💬 How to bind a group?
You should use a special bind attribute.

   const int tagPlayers = 0; // just an example
    
   [Bind(tagPlayers)]
   Group<ComponentPlayer> players;

   public void Tick(float dt)
   {
      var group = Ecs.Group(tagPlayer); // get a group by id.
   }

💡 Note that group bindings refer to the layer where group was registered.

📘 Settings

You can cusotomize some parametres through the SettingsFramework.json. If you don't have one just create it inside of your resources folder.

{
  "//": "Vsync",
  "Vsync": 0,
  "//": "Fixed FPS, 50 by default",
  "FpsPhysics": 50,
  "//": "FPS, -1 for fast as possible",
  "Fps": -1,
  "//": "How much entities do you need at start",
  "SizeEntities": 5000,
  "//": "Amount of component types. Must be multiple of 32",
  "SizeComponents": 128,
  "//": "Namespace for your components. Remove if you don't use namespaces",
  "Namespace": "Pixeye.Source",
  "//": "Extra Debug ( works only in the debug mode )",
  "DebugNames": false,
  "//": "Plugins. Put them in Rersources folder",
  "Plugins": [
  ],
  "//": "Pause on losing focus",
  "PauseOnFocusLost": true
}

📖 How to Install <a name="HowToInstall"></a>

Git Installation

OpenUPM Installation

The package is available on the openupm registry. It's recommended to install it via openupm-cli.

openupm add com.pixeye.ecs

Manual Installation

How To Update

<details> <summary> Press Tools->Actors->Update Actors to get a new update. </summary> <a href="https://gyazo.com/e64645a5eacba87e6714da2e848cb88d"><img src="https://i.gyazo.com/e64645a5eacba87e6714da2e848cb88d.gif" alt="Updating" width="800"/></a> </details>

💡 If you install via OpenUPM registry, you can update the package using the Unity package manager.

📖 Gallery <a name="Gallery"></a>

This is a list of known projects that uses Actors by far. If you want to add a project, feel free to contact me :)

Until We Die | Steam

Steam

Snake Demo | Github link

Rocket Science Snake Demo | Github link

Actors Benchmark | Github link

2D Roguelike Actors | Github link

📖 About <a name="About"></a>

💬 How to help

💬 Credits

Developed by Dmitry Mitrofanov and every direct or indirect contributors to the GitHub.
Recurring contributors (2020): Dmitry Mitrofanov @PixeyeHQ, Alex Ohezin @Allexeee.

📘 License

Actors is licensed under the MIT License, see LICENSE for more information.