Skip to content

OliverMead/dots-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 

Repository files navigation

DOTS Info

Please note that this document is a work in progress, and will be expanded in time.

Table of Contents

Preface, “Why?”

Unity is in the midst of a transformation that will fundamentally change the way games are written. While those changes are not in the mainstream distribution of the engine, it is imminent. This document aims to consolidate and summarise information on the requirements and use of this new Data-Oriented Technology Stack, and serve as a reference for project setup and development in the future.

It is still a good idea to learn the “classic”, object-oriented approach taken by the current series of learning materials that Unity provides. Much of the programming, concepts and techniques will still be useful and will still be necessary with some changes.

I make frequent reference to my efforts to rewrite the “Rollaball” Unity tutorial project to make use of DOTS. You may wish to inspect that project yourself at OliverMead/Rollaball-DOTS.

Required Packages

DOTS Packages

Installing the DOTS packages requires opening the Package Manger within the project (Select Window > Package Manager), then add by name (select from the drop-down “+” menu). Enter the following package names (one at a time):

  • com.unity.entities
  • com.unity.rendering.hybrid
  • com.unity.dots.editor
  • com.unity.physics ← The re-implementation of the physics engine built on DOTS

These packages are marked experimental, so be sure to save often and make use of good version control practices. Be sure to push changes to the remote repository often, and use branches when writing and testing new functionality before merging with parent branches.

Domain Reload

As noted in the entities documentation, Domain Reload occurs when entering play mode within the editor, and is especially slow when using DOTS. To disable this, enable the ”Enter Play Mode Settings” checkbox in the Editor section of the Project Settings, while leaving its child options unchecked.

Platform Packages

It is also necessary to install the DOTS based platform packages for Unity (The default tool-chains will not be replaced by default).

Install, in the same way as above, the com.unity.platforms.<platform> packages, where <platform> is the desired target, for example I will install com.unity.platforms.linux and com.unity.platforms.windows to produce binaries for Linux and Windows.

Builds

Building through the standard File > Build and Run or <ctrl+b> will not work with DOTS. You must create a “Classic Build Configuration” for each target platform through the asset manager:

+ > Build > <Platform> Classic Build Configuration

From now on you must build the project by selecting this configuration and using the options shown in the inspector.

Be sure to add the game’s scenes to the Scene List of the build configurations you create, as these will not be inherited from the regular build manager. You should also add a Live Link component in order to use that functionality of the editor.

Entity-Component-Systems

Comparison of Models

In the standard OOP model of game development, functionality is tied to individual instances of objects. Each Monobehaviour (the class that from which all standard scripts inherit) has its own Start(), Update(), similar and accompanying methods. The engine will run all of these sequentially.

In this model, each game object is treated as a collection of data (the entity), with this data organised into “components”. These components are like a struct in C, they are mutable collections of data, they do not normally have their own functionality (methods).

Transformation of Data

It is the job of a System to read and transform the data of the entities. For example you may have many entities with a Character component, each with an hp variable. This will include all players, enemies and NPCs. Characters may be poisoned during the game, adding a Poison component to their entity. This component will contain a value float rate to determine how much damage to deal each second, and a float duration to determine how long the character will be poisoned for.

You may define a StatusSystem, which manages status effects (in this case poisoning). It will operate on all of the entities with a Character and a Poison component, and update the hp variable based on the data related to the poison.

What will this look like?

Concrete example

We write Character.cs as a struct inheriting IComponentData.

using Unity.Entities;

// This tag allows us to set the fields in the editor like with a MonoBehaviour
[GenerateAuthoringComponent]
public struct Character : IComponentData {
    public float hp;
}

Poison.cs will also inherit IComponentData, but we don’t need the authoring component (since it would be added at runtime).

using Unity.Entities;

public struct Poison : IComponentData {
    public float rate;
    public float duration;
}

Finally the StatusSystem, whose OnUpdate() method will perform the transformation of data. This benefits from parallel execution through the C# Job System.

using Unity.Entities;
using UnityEngine;

public partial class StatusSystem : SystemBase {
    EndSimulationEntityCommandBufferSystem m_EndSimulationECBSystem;

    protected override void OnCreate() {
        base.OnCreate();
        // This is how we make changes to the EntityManager within a job
        m_EndSimulationECBSystem = World
            .GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }
    protected override void OnUpdate() {
        // only local variables are captured in the lambda unless otherwise specified
        float dT = Time.DeltaTime;

        // create an entity command buffer suitable for parallel scheduled jobs
        var ecb = m_EndSimulationECBSystem
            .CreateCommandBuffer().AsParallelWriter();

        Entities
            .WithName("Apply Poison Damage") // The name for the job (optional)
            .WithAll<Character, Poison>() // Only iterate over poisoned characters
            .ForEach(
                // define the lambda that transforms the data
                (int entityIndex, ref Character ch, ref Poison poison, in Entity entity) => {
                ch.hp -= poison.rate * dT;
                poison.duration -= dT;
                if (poison.duration <= 1) // remove the poison component if it has expired
                    ecb.RemoveComponent<Poison>(entityIndex, entity); // using the Entity Command Buffer
                })
            .WithBurst() // free performance enhancement from the 'Burst Compiler'
            .ScheduleParallel();

        // Tell the system what we need
        m_EndSimulationECBSystem.AddJobHandlerForProducer(this.Dependency);
    }
}

Some keywords are used in the lambda definition that relate to C#’s implementation:

  • ref creates a mutable reference to the given argument
  • in creates an immutable reference to the argument (here we are not modifying entity, only passing the reference to the EntityCommandBuffer in order to remove the component)

In the Editor

In the Unity Editor, create objects and materials as normal. You can still use prefabs, but some unity components (separate to DOTS Components) should be exchanged for their counterparts in the new packages. The packages expose DOTS Components to the editor in the form of an Authoring Component, a MonoBehaviour which tells the editor how to create the Component based on the settings you give the inspector. In the concrete example, I used the [GenerateAuthoringComponent] tag to generate this MonoBehaviour automatically, but you can also define your own - it must implement IConvertGameObjectToEntity as well as extending MonoBehaviour.

Physics

The old physics system is not compatible with DOTS, that means unity components like Colliders and RigidBody are not to be used.

Physics Shape

The counterpart to Colliders is the Physics Shape authoring Component. Add it to the GameObject, set up the shape of the collider, and set the collision response behaviour (Collide or trigger normally suffice).

Physics Body

This authoring component replaces RigidBody. Set the Motion Type appropriately for the object:

  • Dynamic - standard RigidBody behaviour
  • Kinematic - like RigidBody with the isKinematic flag checked
  • Static - The object does not move

The Unity Input System

Unity has a very useful input package (com.unity.inputsystem), which lets the developer define simple methods like OnMove(InputValue) to create gameplay. One fatal flaw is that it is (at time of writing) fundamentally incompatible with entities.

The (almost criminal) workaround I have found is to create an “InputProxy” empty game object, which will not be converted to an entity, and apply the Player Input component to that empty. Then attach an InputProxy MonoBehaviour to the empty, having all the callback methods store the movement data in the fields of a static class (in my case named InputCapture). You can then use that data within a player movement system (see the UpdateLocation method).

While the linked examples are limited to only movement data, this can be expanded to include more data as per your requirements.

I encourage any person reading this, who knows of an easier way to go about combining DOTS and the Input System, to submit a pull request or otherwise contact me.

Capturing Trigger Events (Physics)

Capturing trigger events using standard Unity’s Colliders is simple:

  • Mark the trigger object’s collider to be a trigger
  • Add a collider to the object you’d like to have react to the trigger
  • Define the OnTriggerEnter(Collider) method in a MonoBehaviour attached to that object.

DOTS affords us no such luxury or convenience. It is up to the developer to define a job to respond to TriggerEvent occurrences. This is rather complex, but the setup code is readily reusable. I will be referring to CollisionSystem.cs in the Rollaball-DOTS project. As noted at the top of that file, it is adapted from a Physics sample provided by Unity Technologies, which you may also wish to inspect.

State

TriggerEvents are not stateful. Therefore we cannot determine, simply by inspecting the event, whether the Colliders first intersected on the current frame or on any frame previously.

So I define an enumeration - EventOverlapState with fields Enter, Stay and Exit - and a structure StatefulTriggerEvent, which will have all the same data as a TriggerEvent, plus a field holding the state.

This structure will be used with a Dynamic Buffer Component (glorified list associated with an entity) later, so it implements IBufferElementData, and will need to be compared against other instances, so implements IComparable<StatefulTriggerEvent>.

A C# Job to Collect Events

Event types have their own Job Interfaces. Create an implementation of the right interface, schedule it at the appropriate time, and it will “receive” events.

The CollisionSystem in this example has a member structure CollectTriggerEvents implementing ITriggerEventsJob. The Execute method creates a StatefulTriggerEvent from the given event, and adds it to the list.

It is the role of the surrounding system to schedule this job and make use of the events it captures.

The Dynamic Buffer Component

TriggerEventBufferAuth is an authoring component which will give a GameObject’s entity a DynamicBuffer<StatefulTriggerEvent> component, which the developer will access in the same way as other components, and iterate over in the same way as a list. Once an entity has this component, the CollisionSystem will update the buffer every frame with the data of any TriggerEvents involving the entity.

It is important, so that no events are missed, that any system which reads this buffer be updated every frame (by using the [ExecuteAlways] class attribute), and perhaps after the physics systems (by using the [UpdateAfter(typeof(EndFramePhysicsSystem))] class attribute), but I am not sure this is necessary.

CollisionSystem

To summarise this system, it performs the following operations:

  • Clear all trigger event buffers (see the job that has .WithName("Clear_Trigger_Event_Buffers"))
  • Move current frame triggers to previous (see FrameStartEventMove() in CollisionSystem.cs and SwapTriggerEventStates() in the Unity sample, their definitions are the same).
  • Collect trigger events for the frame (creating the job instance as teCollectJob and scheduling it)
  • Collect entities with a trigger event buffer (job assigned to collectBuffers variable)
  • Update the States of the trigger events and add the trigger events to the respective buffers (both part of the job named "Convert_Trigger_Event_Stream_to_Dynamic_Buffers").

Camera Following an Entity

The real question here is: how do you make a GameObject follow the translation of a given Entity? See FollowEntity. This is a MonoBehaviour which keeps a reference to the Entity Manager, and a copy of our desired entity (remember the entity is only a key to look up component data, so it is passed by value). The LateUpdate method can then update the GameObject’s transform in much the same way as it would if it were following another GameObject.

The difference here is that you cannot assign an entity to follow within the editor like you would a GameObject. Instead, create an authoring component (here ObjectFollowAuth) for the entity you would like your GameObject to follow, assigning the follower field within the editor. This script will add the FollowEntity MonoBehaviour to the follower, setting the correct Entity field.

Cinemachine

Using Cinemachine is possible in this way by creating an empty GameObject, which will be the follower of the entity. Then set this empty GameObject as the follow target and look-at target for the Cinemachine virtual camera.