Skip to content

outerminds/Entia.Unity

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Entia.Unity

Entia.Unity is a full integration of the Entia framework for the Unity game engine. It consists of a code generator, inspectors, templates, tools and other conveniences that make the usage of the framework simple and accessible to Unity developers.

Entia is a free, open-source, data-oriented, highly performant, parallelizable and extensible Entity-Component-System (ECS) framework written in C# especially for game development. It takes advantage of the latest C#7+ features to represent state exclusively with contiguous structs. No indirection, no boxing, no garbage collection and no cache misses. See Entia.

Entia.Unity requires version 2018.3+ of the Unity game engine.


Content


Installation

  • Download the most recent stable version of Entia.Unity.
  • Extract the 'Entia.Unity.zip' package in a 'Plugins' folder in your Unity project (ex: 'Project/Assets/Plugins/Entia/').
  • Ensure that you have a .Net Core Runtime with version 2.0+ (required for the code generator to work).
  • Optionally install the packaged Visual Studio 2017 extension 'Entia.Analyze.vsix' to get Entia specific code analysis.

Tutorial

  • Create an empty Unity scene.

  • Click on the 'Entia/Generator/Generate' menu.

    • This will create a GeneratorSettings asset named 'Settings.asset' in your 'Entia' folder and will launch the generator.
    • The default values of the GeneratorSettings asset should satisfy most use-cases.
    • As long as the Automatic option is on in the GeneratorSettings, this is the only time that you will have to manually launch the generator or worry about it.

  • Define a couple components in the 'Assets/Scripts' folder.

using Entia;
using Entia.Core;
using Entia.Unity;

namespace Components
{
    // Components are simple structs that implement the empty 'IComponent' interface.
    public struct Velocity : IComponent { public float X, Y; }

    // Components may be empty to act as a tag on an entity.
    public struct IsFrozen : IComponent { }

    public struct Physics : IComponent
    {
        // Since structs can not have default values, the 'Default' attribute is
        // used by the framework to create default initialized instances.
        [Default]
        public static Physics Default => new Physics { Mass = 1f, Drag = 3f, Gravity = -2f };

        public float Mass;
        public float Drag;
        public float Gravity;
    }

    public struct Input : IComponent
    {
        // The 'Disable' attribute will make the field read-only in the Unity editor.
        [Disable]
        public float Direction;
        [Disable]
        public bool Jump;
    }

    public struct Motion : IComponent
    {
        [Default]
        public static Motion Default => new Motion { Acceleration = 2f, MaximumSpeed = 0.25f, JumpForce = 0.75f };

        public float Acceleration;
        public float MaximumSpeed;
        public float JumpForce;
    }
}
  • Create an empty GameObject named 'Player', add the newly defined components to it and tweak their values (you may addionally add a SpriteRenderer to the GameObject to visualize it).

    • The generator should've generated ComponentReference wrappers for your components in a 'Generated' folder.
    • You should be able to find them in the 'Add Component' dropdown of the GameObject.
    • Adding a component to a GameObject will automatically add the required EntityReference to it.

  • Define a couple systems that will use the components in the 'Assets/Scripts' folder.

    • When the generator detects the use of a queryable type, it will generate convenient extensions to unpack instances of that type.
    • Note that you may need to focus Unity to trigger the generator.
using Entia.Injectables;
using Entia.Queryables;
using Entia.Systems;
using UnityEngine;

namespace Systems
{
    // An 'IRun' system will be called on every 'Update'.
    // A system may implement as many system interfaces as needed.
    // See the 'Entia.Systems' namespace for more system interfaces.
    public struct UpdateInput : IRun
    {
        // Groups allow to efficiently retrieve a subset of entities that 
        // correspond to a given query represented by the generic types of the group.
        // This group will hold every entity that has a 'Components.Input' 
        // component and will give write access to it.
        public readonly Group<Write<Components.Input>> Group;

        public void Run()
        {
            foreach (var item in Group)
            {
                // The 'item.Input()' extension method is generated by the generator.
                ref var input = ref item.Input();
                input.Direction = Input.GetAxis("Horizontal");
                input.Jump = Input.GetKeyDown(KeyCode.UpArrow);
            }
        }
    }

    public struct UpdateVelocity : IRun
    {
        // Queries can also be defined as a separate type which is convenient 
        // for large queries.
        // The 'None' attribute means that all entities that have a 
        // 'Components.IsFrozen' component will be excluded from the query results.
        [None(typeof(Components.IsFrozen))]
        public readonly struct Query : IQueryable
        {
            // A queryable can only hold queryable fields. They can not hold 
            // components directly.
            public readonly Write<Components.Velocity> Velocity;
            public readonly Read<Components.Motion> Motion;
            public readonly Read<Components.Physics> Mass;
            public readonly Read<Components.Input> Input;
        }

        public readonly Group<Query> Group;

        // Resources hold world-global data.
        // 'Entia.Resources.Time' is a library resource that holds 
        // Unity's 'Time.deltaTime'.
        public readonly Resource<Entia.Resources.Time>.Read Time;

        public void Run()
        {
            ref readonly var time = ref Time.Value;
            foreach (ref readonly var item in Group)
            {
                // Unpack the group item using generated extensions.
                ref var velocity = ref item.Velocity();
                ref readonly var motion = ref item.Motion();
                ref readonly var physics = ref item.Physics();
                ref readonly var input = ref item.Input();

                var drag = 1f - physics.Drag * time.Delta;
                velocity.X *= drag;
                velocity.Y *= drag;

                var move = (input.Direction * motion.Acceleration) / physics.Mass;
                velocity.X += move * time.Delta;

                if (input.Jump) velocity.Y += motion.JumpForce / physics.Mass;
                velocity.Y += physics.Gravity * time.Delta;

                // Clamp horizontal velocity.
                if (velocity.X < -motion.MaximumSpeed)
                    velocity.X = -motion.MaximumSpeed;
                if (velocity.X > motion.MaximumSpeed)
                    velocity.X = motion.MaximumSpeed;
            }
        }
    }

    public struct UpdatePosition : IRun
    {
        // 'Unity<Transform>' is a Unity-specific query that gives access 
        // to any Unity component associated with the entity.
        // Note that it will not work for custom 'MonoBehaviour' types.
        public readonly Group<Unity<Transform>, Write<Components.Velocity>> Group;

        public void Run()
        {
            foreach (ref readonly var item in Group)
            {
                // Unpack the group item using generated extensions.
                var transform = item.Transform();
                ref var velocity = ref item.Velocity();

                var position = transform.position;
                position += new Vector3(velocity.X, velocity.Y);

                // Fake a floor.
                if (position.y < 0)
                {
                    position.y = 0;
                    velocity.Y = 0;
                }

                transform.position = position;
            }
        }
    }
}
using Entia.Core;
using Entia.Nodes;
using Entia.Unity;
using static Entia.Nodes.Node;

namespace Controllers
{
    public class Main : ControllerReference
    {
        // This 'Node' represents the execution behavior of systems.
        public override Node Node =>
            // The 'Sequence' node executes its children in order.
            Sequence(nameof(Main),
                // This node holds a few useful Unity-specific library systems.
                Nodes.Default,
                // Any number of systems can be added here.
                System<Systems.UpdateInput>(),
                System<Systems.UpdateVelocity>(),
                System<Systems.UpdatePosition>()
            );
    }
}
  • Create an empty GameObject named 'World' and add your newly defined controller to it.

    • This will automatically add the required WorldReference on the GameObject.

  • Press Play and appreciate your moving and jumping player entity.

  • For more details, please see Entia or consult the wiki.


Integration

  • References

    • Most of the integration with the Unity game engine is done through what are called references. These are convenient MonoBehaviour wrappers that act as constructors and visualizers for Entia elements. After initialization, references are only debug views for what is going on the Entia side and are not strictly required. Other than ControllerReference (which is where you define your execution graph), you will never have to define references yourself since the code generator will do all the boilerplate work.
  • Profiler

    • Unity's profiler works with Entia out of the box.

    • Summary information can also be found in the WorldReference editor.

Generator

A lightweight code generator comes packaged with Entia.Unity to make the integration with the Unity game engine seamless. It generates corresponding references for every component and resource that you define such that they can be inspected and adjusted in the editor just like regular MonoBehaviour components. Additionally, it will generate convenient extensions for your systems to simplify their usage.

Most of the time you will not have to worry about the generator, but it is useful to know that it is triggered when a relevant C# script is saved or imported by the Unity editor. It can also be manually triggered using the menu 'Entia/Generator/Generate' or in the GeneratorSettings asset.

The generator uses Roslyn to run before the Unity compiler does such that it does not depend on the compiled assembly. This prevents many typical and unpleasant generator bugs (example: you delete a type and the Unity compiler can't run because some generated code depends on it and since the generator depends on a successful compilation, it can't run either, forcing you to manually modify generated code).

  • The generator is smart enough to detect most renaming scenarios and renames the .meta files associated with the generated MonoBehaviour wrappers such that your links will not be lost.
    • Renaming a type through refactor is detected as long as all modified files are saved.
    • Renaming a file is detected.
    • Renaming or changing the namespace of a type is detected as long as the file is not renamed at the same time.
  • The generator exists only in the Unity editor and will never encumber your builds.