Spawn and Move Players

Full workflows and code to spawn and move Player from user input

What you'll develop on this page

We will create a player prefab and have it spawn when the user inputs commands. The player will move according to user input.

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Spawning-and-Moving-Player

Spawning a player

Creating the Player prefab

We use many of the same steps as spawning an asteroid prefab to spawn a player prefab. First we make our player prefab.

  • Create a capsule GameObject in the Hierarchy of SubScene, name it "Player", and drag it into Scripts and Prefabs then delete the cylinder GameObject in the Hierarchy

  • Remove the Capsule Collider component

    • We will add DOTS Physics colliders in the next section

  • Add a cube to the Player hierarchy named "Visor"

    • (Double click on the Player prefab to get to the Player hierarchy)

    • Remove the Box Collider component

    • Change the position to (0, 0.5, 0.24)

    • Change the scale to (0.95, 0.25, 0.5)

  • Right click on the Assets, click "Create" > "Material"

    • Make the name "Black"

    • Make sure it is a Universal Render Pipeline/Lit shader

    • Double click Base Map and change the value to "000000"

  • Click on the Visor in the Player Hierarchy and drag "Black" to Mesh Renderer Material

  • Add a cylinder to the Player hierarchy named "Gun"

    • Remove the Capsule Collider component

    • Change the position to (0.5, 0, 0.5)

    • Change the rotation to (90, 0 , 0)

    • Change the scale to (0.25, 0.5, 0.25)

    • Change the material to "Black"

  • Finally let's add a camera

    • Right click in the Player prefab Hierarchy and create a Camera GameObject

    • Change the position to (0, 2, -5)

  • And make sure your Player prefab has a Position of (0, 0, 0)

  • Now we need to create a PlayerTag in our Scripts and Prefabs folder to put on our Player prefab

    • We use this tag to query for our player entity

    • Because we are putting this component data on a prefab before runtime we must make it an Authoring component with [GenerateAuthoringComponent]

  • Create PlayerTag and paste this code snippet into PlayerTag.cs:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct PlayerTag : IComponentData
{
}
  • Now we will put the PlayerTag component and the VelocityComponent on our Player prefab

  • Finally, we must add our Player prefab to the PrefabCollection GameObject in our ConvertedSubScene like we did with our Asteroid prefab

    • The first step here is to create the PlayerAuthoringComponent

      • code snippet for PlayerAuthoringComponent.cs below:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct PlayerAuthoringComponent : IComponentData
{
    public Entity Prefab;
}
  • Next we open our ConvertedSubScene, select PrefabCollection, click Add Component to add PlayerAuthoringComponent to the PrefabCollection GameObject and drag the Player prefab into the the Prefab field

    • Unity might ask you to save your Player prefab when navigating to the Sub Scene if you have not already, hit save

  • Save the Sub Scene, open SampleScene and reimport the ConvertedSubScene

We now have our Player prefab created and registered with our PrefabCollection so we can reference it in ECS.

Creating the spawning system

  • We are going to create InputSpawnSystem in Scripts and Prefabs that will take in user input and spawn a player prefab if the user presses the spacebar

    • AsteroidSpawnSystem spawns asteroids programmatically based on the GameSettingsComponents values

    • InputSpawnSystem will spawn entities based on user input

    • InputSpawnSystem will have a lot of similarities to AsteroidSpawnSystem because both systems need to set prefabs and use EntityCommandBuffer to record structural changes

    • InputSpawnSystem does not need GameSettingsComponent data so it will have a few less lines in OnCreate()

  • Create InputSpawnSystem and paste the code snippet below into InputSpawnSystem.cs:

using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using Unity.Burst;

public partial class InputSpawnSystem : SystemBase
{
    //This will be our query for Players
    private EntityQuery m_PlayerQuery;

    //We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
    private BeginSimulationEntityCommandBufferSystem m_BeginSimECB;

    //This will save our Player prefab to be used to spawn Players
    private Entity m_Prefab;

    protected override void OnCreate()
    {
        //This is an EntityQuery for our Players, they must have an PlayerTag
        m_PlayerQuery = GetEntityQuery(ComponentType.ReadWrite<PlayerTag>());

        //This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
        m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
    }
    
    protected override void OnUpdate()
    {
        //Here we set the prefab we will use
        if (m_Prefab == Entity.Null)
        {
            //We grab the converted PrefabCollection Entity's PlayerAuthoringComponent
            //and set m_Prefab to its Prefab value
            m_Prefab = GetSingleton<PlayerAuthoringComponent>().Prefab;

            //we must "return" after setting this prefab because if we were to continue into the Job
            //we would run into errors because the variable was JUST set (ECS funny business)
            //comment out return and see the error
            return;
        }
        byte shoot;
        shoot = 0;
        var playerCount = m_PlayerQuery.CalculateEntityCountWithoutFiltering();

        if (Input.GetKey("space"))
        {
            shoot = 1;
        }

        if (shoot == 1 && playerCount < 1)
        {
            EntityManager.Instantiate(m_Prefab);
            return;
        }
    }
}
  • You will notice the similarities of InputSpawnSystem with AsteroidSpawnSystem, but also a few differences:

    • why aren't we using the EntityCommandBuffer in InputSpawnSystem?!

    • And what the heck is an EntityManager?!

  • We are not using the EntityCommandBuffer to demonstrate how to make structural changes during InputSpawnSystem's OnUpdate()

    • That's not to torture you, it's to teach you more ECS functionalities 💪

    • We will continue using the EntityCommandBuffer later on when we spawn bullets 😌

A World organizes entities into isolated groups. A world owns both an EntityManager and a set of Systems. Entities created in one world only have meaning in that world, but can be transfered to other worlds (with EntityManager.MoveEntitiesFrom). Systems can only access entities in the same world. You can create as many worlds as you like.

By default Unity creates a default World when your application starts up (or you enter Play Mode). Unity instantiates all systems (classes that extend ComponentSystemBase) and adds them to this default world. Unity also creates specialized worlds in the Editor. For example, it creates an Editor world for entities and systems that run only in the Editor, not in playmode and also creates conversion worlds for managing the conversion of GameObjects to entities. See WorldFlags for examples of different types of worlds that can be created.

Use World.DefaultGameObjectInjectionWorld to access the default world.

From ECS World documentation

  • You might have noticed that when we hit the "play" button the list of available worlds in our DOTS Windows changes

  • "Editor World" changes to "Default World"

You can also disable the default World creation entirely by defining the following global symbols:

  • #UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD disables generation of the default runtime World.

  • #UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_EDITOR_WORLD disables generation of the default Editor World.

  • #UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP disables generation of both default Worlds.

From ECS World documentation

  • A World owns a single EntityManager and a set of Systems

  • The EntityManager is what we use to interact with Entities if we are not using EntityCommandBuffer

The EntityManager provides an API to create, read, update, and destroy entities.

A World has one EntityManager, which manages all the entities for that World.

Many EntityManager operations result in structural changes that change the layout of entities in memory. Before it can perform such operations, the EntityManager must wait for all running Jobs to complete, an event called a sync point. A sync point both blocks the main thread and prevents the application from taking advantage of all available cores as the running Jobs wind down.

Although you cannot prevent sync points entirely, you should avoid them as much as possible. To this end, the ECS framework provides the EntityCommandBuffer, which allows you to queue structural changes so that they all occur at one time in the frame.

From ECS EntityManager documentation

  • Now that we've created our InputSpawnSystem let's hit "play" then "space bar" to spawn our Player prefab

  • Woo, all set! Right?

    • Not quite, why isn't the camera updated to the Player prefab's Camera GameObject?

  • The Camera is a GameObject but is being instantiated through ECS... a little wonky

  • To fix this we need to add HYBRID_ENTITIES_CAMERA_CONVERSION to the Player Settings

    • Navigate to "File", choose "Build settings", then "Player Settings", then "Player" on the left, expand the drop down menu for "Other Settings", and scroll down to "Scripting Define Symbols". Add HYBRID_ENTITIES_CAMERA_CONVERSION

      • Hit "Apply"

  • This will cause a warning on the Build Settings window

  • Wait until the warning on the Build Settings window goes away

  • Now hit "play" and press "space bar" again to see our new camera view

    • Through testing we have found that sometimes this change does not immediately "take"

    • In that case, go to "Assets" and choose "Reimport All"

We can now spawn our player from user input

  • We created our player prefab

  • We added a PlayerTag and VelocityComponent to the prefab

  • We created InputSystem which takes in inputs and spawns our player

Moving a Player

  • We are going to create InputMovementSystem in Scripts and Prefabs to take in "WASD" and mouse input to change the rotation and velocity of our player

    • We could implement this functionality into the existing InputSpawnSystem (and name the system something else) but we are separating to make the code easier to understand

  • Create InputMovementSystem and paste the code snippet below into InputMovementSystem.cs:

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;

public partial class InputMovementSystem : SystemBase
{
    protected override void OnCreate()
    {
        //We will use playerForce from the GameSettingsComponent to adjust velocity
        RequireSingletonForUpdate<GameSettingsComponent>();
    }

    protected override void OnUpdate()
    {
        //we must declare our local variables to be able to use them in the .ForEach() below
        var gameSettings = GetSingleton<GameSettingsComponent>();
        var deltaTime = Time.DeltaTime;

        //we will control thrust with WASD"
        byte right, left, thrust, reverseThrust;
        right = left = thrust = reverseThrust = 0;

        //we will use the mouse to change rotation
        float mouseX = 0;
        float mouseY = 0;

        //we grab "WASD" for thrusting
        if (Input.GetKey("d"))
        {
            right = 1;
        }
        if (Input.GetKey("a"))
        {
            left = 1;
        }
        if (Input.GetKey("w"))
        {
            thrust = 1;
        }
        if (Input.GetKey("s"))
        {
            reverseThrust = 1;
        }
        //we will activate rotating with mouse when the right button is clicked
        if (Input.GetMouseButton(1))
        {
            mouseX = Input.GetAxis("Mouse X");
            mouseY = Input.GetAxis("Mouse Y");

        }

        Entities
        .WithAll<PlayerTag>()
        .ForEach((Entity entity, ref Rotation rotation, ref VelocityComponent velocity) =>
        {
            if (right == 1)
            {   //thrust to the right of where the player is facing
                velocity.Value += (math.mul(rotation.Value, new float3(1,0,0)).xyz) * gameSettings.playerForce * deltaTime;
            }
            if (left == 1)
            {   //thrust to the left of where the player is facing
                velocity.Value += (math.mul(rotation.Value, new float3(-1,0,0)).xyz) * gameSettings.playerForce * deltaTime;
            }
            if (thrust == 1)
            {   //thrust forward of where the player is facing
                velocity.Value += (math.mul(rotation.Value, new float3(0,0,1)).xyz) * gameSettings.playerForce * deltaTime;
            }
            if (reverseThrust == 1)
            {   //thrust backwards of where the player is facing
                velocity.Value += (math.mul(rotation.Value, new float3(0,0,-1)).xyz) *  gameSettings.playerForce * deltaTime;
            }
            if (mouseX != 0 || mouseY != 0)
            {   //move the mouse
                //here we have "hardwired" the look speed, we could have included this in the GameSettingsComponent to make it configurable
                float lookSpeedH = 2f;
                float lookSpeedV = 2f;

                //
                Quaternion currentQuaternion = rotation.Value; 
                float yaw = currentQuaternion.eulerAngles.y;
                float pitch = currentQuaternion.eulerAngles.x;

                //MOVING WITH MOUSE
                yaw += lookSpeedH * mouseX;
                pitch -= lookSpeedV * mouseY;
                Quaternion newQuaternion = Quaternion.identity;
                newQuaternion.eulerAngles = new Vector3(pitch,yaw, 0);
                rotation.Value = newQuaternion;
            }
        }).ScheduleParallel();
    }
}
  • Hit "play" and once the game is loaded press "space bar" to spawn your player, hold down the right button and move your mouse to change rotation and hit "w" to add forward thrust

  • We didn't need to make a second MovementSystem for our player entity because the MovementSystem works on any entity that has a Translation and VelocityComponent

    • Nice benefit of ECS

  • It is a bit fast, let's change the Player Force to 10

  • Go into ConvertedSubScene and change Player Force from 50 to 10

  • Save and return to SampleScene and reimport ConvertedSubScene and hit play and try again

We can now move our player from user input

  • We updated our InputSystem to read more user inputs

  • We can adjust the rotation and velocity of our player entity with user inputs

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Spawning-and-Moving-Player

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Spawning-and-Moving-Player'

Last updated