Use DOTS Physics for Prefabs, Players, and Bullets

Workflows and code to update previous ECS section to use DOTS Physics

What will be developed on this page

We will update our player, asteroids and bullets prefabs to run using DOTS Physics.

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Adding-Physics-To-Asteroids-Players-and-Bullets

Adding DOTS Physics to asteroids

First, some background:

The key "ingredients" of working with Unity Physics in this gitbook are: Physics Shape (This will drive how/if a prefab causes triggers/collisions) Physics Body (This will drive motion with things like gravity and linear/angular velocity) PhysicsCategoryNames (to help us define which prefabs interact with each other)

Since Unity Physics is purely based on DOTS, rigid bodies are represented with component data on the Entities. The simplified Physics Body and Physics Shape view that you have in the Editor is actually composed of multiple data components under the hood at runtime. This allows more efficient access and to save space for static bodies which do not require some of the data.

The current set of data components for a rigid body is as follows:

ComponentDescription

PhysicsCollider

The shape of the body. Needed for any bodies that can collide.

PhysicsWorldIndex

Shared component required on any Entity that is involved in physics simulation (body or joint). Its Value denotes the index of physics world that the Entity belongs to (0 for default).

PhysicsVelocity

The current linear and angular velocities of a dynamic body. Needed for any body that can move.

PhysicsMass

The current mass properties (center of mass and inertia) of a dynamic body. Assumed to be infinite mass if not present.

PhysicsDamping

The amount of damping to apply to the motion of a dynamic body. Assumed to be zero if not present.

PhysicsGravityFactor

The scalar for how much gravity should affect a dynamic body. Assumed to be 1 if not present.

PhysicsCustomData

Custom flags applied to the body. They can be used for certain collision event applications. Assumed to be zero if not present.

All physics bodies require components from Unity.Transforms in order to represent their position and orientation in world space. Physics ignores any scale of rigid bodies. Any scale applied to converted GameObjects is baked into a CompositeScale component (to preserve the scale of the render mesh at bake time) and the PhysicsCollider component (to approximate the scale of the physics geometry at bake time).

Dynamic bodies (i.e., those with PhysicsVelocity) require Translation and Rotation components. Their values are presumed to be in world space. As such, dynamic bodies are unparented during entity conversion.

Static bodies (i.e., those with PhysicsCollider but without PhysicsVelocity) require at least one of either Translation, Rotation, and/or LocalToWorld. For static bodies without a Parent, physics can read their Translation and Rotation values directly, as they are presumed to be in world space. World space transformations are decomposed from LocalToWorld if the body has a Parent, using whatever the current value is (which may be based on the results of the transform systems at the end of the previous frame). For best performance and up-to-date results, it is recommended that static bodies do not have a Parent.

From DOTS Physics Documentation

Run a Unity Sample and take a look at the DOTS Windows to better understand how DOTS Physics works.

You can usually consider the physics simulation as a monolithic process whose inputs are components described in core components and outputs are updated Translation, Rotation and PhysicsVelocity components. However, internally, the Physics step is actually broken down into smaller subsections, the output of each becomes the input to the next. Unity Physics currently gives you the ability to read and modify this data, if it is necessary for your gameplay use cases.

From DOTS Physics Modifying simulation behavior documentation

In the diagram above (presented at Unity's DOTS Physics talk at their Unite conference) you can see what takes place between Build Physics World and Export Physics world. The simulation first runs through the Collision World and Dynamics World.

Although we will use DOTS Physics as the simulation backend in this gitbook, it is also possible to use Havok Physics as the back-end; check out the Unite talk above if you're interested in learning more.

The main run-time components in the list above are the components that are worked on in DOTS Physics simulations. These components are read from BuildPhysicsWorld then written to in ExportPhysicsWorld.

So in terms of the actual main runtime systems, we've got the BuildPhysicsWorld, StepPhysicsWorld, ExportPhysicsWorld, and then this final sort of catch-all EndFramePhysics system. And one common feature of all these is that they have this sort of final job handle exposed. So, the idea is that if you have some job that you want to execute -- let's say after you built the Physics World -- you may want to be querying against the static geometry, and that can happen in parallel with the simulation because you know that the stuff that you're querying against isn't going to be moving. You just need to make sure you not only update after the BuildPhysicsWorld but that your job has a dependency on the BuildPhysicsWorld final job handle. As I alluded to before, the two key products of the BuildPhysicsWorld step are this CollisionWorld and the DynamicsWorld.

Video transcription from 14:55 "Overview of physics in DOTS - Unite Copenhagen" video

As you probably understood from reading the video transcription above, it is possible to take advantage of how DOTS Physics sets up for simulation and schedule jobs, which have dependencies on any of those main run-time systems.

In the next section we will implement TriggerEventConversionSystem which uses the FinalSimulationJobHandle of the StepPhysicsWorld. The output of this system is then added as an InputDependency to the EndFramePhysics system.

So there's also some more advanced stuff in here depending upon how deep you want to go. If you think about cutting up the Physics system into (1) Broad Phase (just getting overlapping pairs), (2) Narrow Phase (where you're generating contact points), and the (3) Solver (where you're integrating and applying constraints and so forth), we have different custom job types where you can actually sort of inject in-between these systems to make modifications. This is really important if you're doing, let's say, a racing game. for example. You might need to do some modifications at a very low level in order to achieve the types of behaviors that you would expect in those scenarios. For example, you can create an IBodyPairsJob that would execute in between the Broad Phase and Narrow phase, so you could maybe filter out pairs of bodies that otherwise would overlap because something about your game state has changed. Or, between the Narrow Phase and the Solver, you can modify contact points. So you could say, "oh, I happen to know the bodies that have this particular tag. I need to modify the normals of the contact to some value," and so that's something that you can do before it's passed the Solver. Anytime after the Solver has completed, but before the end of your frame, you can schedule collision event jobs or trigger event jobs, so that's what you would expect. It's just responding to different contact or overlap events and having different aspects of gameplay or audio effects or things like that play out.

Video transcription from 16:42 "Overview of physics in DOTS - Unite Copenhagen"

TriggerEventConversionSystem uses the ITriggerEventsJob interface (pretty hardcore!) We take the output of the Solver's trigger events and update them to "stateful" trigger events (more on that in the next section).

Quick recap:

  • DOTS Physics takes in relevant ECS component data for BuildPhysicsWorld

  • Physics Shape and Physics Body are actually collections of those components

    • PhysicsCollider, PhysicsVelocity, PhysicsMass, PhysicsDamping, PhysicsGravityFactor, PhysicsCustomData

  • Between BuildPhysicsWorld and ExportPhysics world there are intermediate results that we can plug into

    • Like we will with TriggerEventConversionSystem in the next section

  • At the end, the results of the simulation are written in ExportPhysicsWorld to the relevant components

    • Our simulation back-end is Unity DOTS Physics

But what about Physics Category Names that was mentioned at the beginning of this section?

So, before with classic GameObject-based Physics, an individual GameObject could only belong to one layer. We're using these layers for a lot of other things.The UI system uses it for ray-casting, the rendering system uses it for culling, and so you can very quickly run out of layers to set up collision behaviors that you would need. It's also problematic that if something is a member of a particular layer it's opting into all the behaviors of that layer. So by saying "this collider represents a body part" it then has all the characteristics of all body parts. So if you had a particular character where you said well this specific body part on this character it needs to behave like a body part for the most part but it also needs to react to this other thing, or it needs to ignore this other thing that's different from body parts. So in order to express this now we have these collision filters where you can specify the categories an object belongs to and the categories it's going to collide with. So instead of being a member in a single category, a single layer, you can be a member in any number of these 32 categories. Respectively, you can collide with any number of those 32 categories. So when we have an overlapping pair of bodies we basically are comparing "does the membership of this one body intersect with the collision response of the other body" and then vice versa. If they both have a match then we're gonna generate a collision response and if you've opted into collision events you would get those as well.

Video transcription from 24:05 "Overview of physics in DOTS - Unite Copenhagen"

Physics Category Names are how we can define what objects collide with each other and whether or not they raise events.

Now let's implement:

  • Very first step to Set up Physics is to add the following to our manifest.json:

    "com.unity.physics": "0.50.0-preview.43",
  • Next, we will add our physics Category Names

  • Within the Assets folder right click, choose "Create", select "DOTS", "Physics", and "Physics Category Names"

    • Don't see "DOTS" when you right-click? You might need to Assets > Reimport All again 😩

  • Then add the following categories to the list:

    • Trigger

    • Asteroid

    • Bullet

    • Player

  • These categories are named after our 3 prefabs as well as a "trigger" category

    • Trigger will be used when a bullet passes through an asteroid or player (i.e. this event will raise a trigger)

  • Now let's open our Asteroid Prefab and add a Physics Shape component and Physics Body component to the prefab

  • Update Physics Shape

    • Shape Type = Sphere

    • Friction = 0

    • Restitution = 1

    • Belongs to (under Collision Filter) = Asteroid

      • you may need to first select "nothing" to clear the selections, and then select Asteroid

    • Collides With = Asteroid, Bullet, Player

  • Update Physics Body

    • Gravity Factor = 0

  • Remove the VelocityComponent (Script) from the prefab

There is nothing inherently wrong with having undefined Physics Categories selected in "Belongs To" and "Collides With" in under "Collision Filter" in Physics Shape. Just to make this gitbook a bit cleaner, we decided to specify our defined categories. Rest assured, there would be no weird behavior if undefined physics categories were left selected.

  • Now we will update AsteroidSpawnSystem to initialize the PhysicsVelocity component rather than the VelocityComponent

  • Paste this code snippet below into AsteroidSpawnSystem.cs:

using System.Diagnostics;
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using Unity.Burst;
using Unity.Physics;

public partial class AsteroidSpawnSystem : SystemBase
{
    //This will be our query for Asteroids
    private EntityQuery m_AsteroidQuery;

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

    //This will be our query to find GameSettingsComponent data to know how many and where to spawn Asteroids
    private EntityQuery m_GameSettingsQuery;

    //This will save our Asteroid prefab to be used to spawn Asteroids
    private Entity m_Prefab;

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

        //This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
        m_BeginSimECB = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();

        //This is an EntityQuery for the GameSettingsComponent which will drive how many Asteroids we spawn
        m_GameSettingsQuery = GetEntityQuery(ComponentType.ReadWrite<GameSettingsComponent>());

        //This says "do not go to the OnUpdate method until an entity exists that meets this query"
        //We are using GameObjectConversion to create our GameSettingsComponent so we need to make sure 
        //The conversion process is complete before continuing
        RequireForUpdate(m_GameSettingsQuery);
    }
    
    [BurstCompile]
    protected override void OnUpdate()
    {
        //Here we set the prefab we will use
        if (m_Prefab == Entity.Null)
        {
            //We grab the converted PrefabCollection Entity's AsteroidAuthoringComponent
            //and set m_Prefab to its Prefab value
            m_Prefab = GetSingleton<AsteroidAuthoringComponent>().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;
        }

        //Because of how ECS works we must declare local variables that will be used within the job
        //You cannot "GetSingleton<GameSettingsComponent>()" from within the job, must be declared outside
        var settings = GetSingleton<GameSettingsComponent>();

        //Here we create our commandBuffer where we will "record" our structural changes (creating an Asteroid)
        var commandBuffer = m_BeginSimECB.CreateCommandBuffer();

        //This provides the current amount of Asteroids in the EntityQuery
        var count = m_AsteroidQuery.CalculateEntityCountWithoutFiltering();

        //We must declare our prefab as a local variable (ECS funny business)
        var asteroidPrefab = m_Prefab;

        //We will use this to generate random positions
        var rand = new Unity.Mathematics.Random((uint)Stopwatch.GetTimestamp());

        Job
        .WithCode(() => {
            for (int i = count; i < settings.numAsteroids; ++i)
            {
                // this is how much within perimeter asteroids start
                var padding = 0.1f;

                // we are going to have the asteroids start on the perimeter of the level
                // choose the x, y, z coordinate of perimeter
                // so the x value must be from negative levelWidth/2 to positive levelWidth/2 (within padding)
                var xPosition = rand.NextFloat(-1f*((settings.levelWidth)/2-padding), (settings.levelWidth)/2-padding);
                // so the y value must be from negative levelHeight/2 to positive levelHeight/2 (within padding)
                var yPosition = rand.NextFloat(-1f*((settings.levelHeight)/2-padding), (settings.levelHeight)/2-padding);
                // so the z value must be from negative levelDepth/2 to positive levelDepth/2 (within padding)
                var zPosition = rand.NextFloat(-1f*((settings.levelDepth)/2-padding), (settings.levelDepth)/2-padding);
                
                //We now have xPosition, yPostiion, zPosition in the necessary range
                //With "chooseFace" we will decide which face of the cube the Asteroid will spawn on
                var chooseFace = rand.NextFloat(0,6);
                
                //Based on what face was chosen, we x, y or z to a perimeter value
                //(not important to learn ECS, just a way to make an interesting prespawned shape)
                if (chooseFace < 1) {xPosition = -1*((settings.levelWidth)/2-padding);}
                else if (chooseFace < 2) {xPosition = (settings.levelWidth)/2-padding;}
                else if (chooseFace < 3) {yPosition = -1*((settings.levelHeight)/2-padding);}
                else if (chooseFace < 4) {yPosition = (settings.levelHeight)/2-padding;}
                else if (chooseFace < 5) {zPosition = -1*((settings.levelDepth)/2-padding);}
                else if (chooseFace < 6) {zPosition = (settings.levelDepth)/2-padding;}

                //we then create a new translation component with the randomly generated x, y, and z values                
                var pos = new Translation{Value = new float3(xPosition, yPosition, zPosition)};

                //on our command buffer we record creating an entity from our Asteroid prefab
                var e = commandBuffer.Instantiate(asteroidPrefab);

                //we then set the Translation component of the Asteroid prefab equal to our new translation component
                commandBuffer.SetComponent(e, pos);

                //We will now set the PhysicsVelocity of our asteroids
                //here we generate a random Vector3 with x, y and z between -1 and 1
                var randomVel = new Vector3(rand.NextFloat(-1f, 1f), rand.NextFloat(-1f, 1f), rand.NextFloat(-1f, 1f));
                //next we normalize it so it has a magnitude of 1
                randomVel.Normalize();
                //now we set the magnitude equal to the game settings
                randomVel = randomVel * settings.asteroidVelocity;
                //here we create a new VelocityComponent with the velocity data
                var vel = new PhysicsVelocity{Linear = new float3(randomVel.x, randomVel.y, randomVel.z)};
                //now we set the velocity component in our asteroid prefab
                commandBuffer.SetComponent(e, vel);

            }
        }).Schedule();

        //This will add our dependency to be played back on the BeginSimulationEntityCommandBuffer
        m_BeginSimECB.AddJobHandleForProducer(Dependency);
    }
}
  • Navigate to SampleScene, reimport ConvertedSubScene and hit "play"

    • Is it wonky? Here's a couple (potentially) helpful debug tips:

      • If the asteroids seem to overpopulate the screen, then check to make sure that you didn't delete the Destroy Tag component off the asteroid prefab

      • If you've lost the camera and/or sight of the player, you might need to check to make sure HYBRID_ENTITIES_CAMERA_CONVERSION is still added 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". If it's not already there, add HYBRID_ENTITIES_CAMERA_CONVERSION

We now have updated our asteroid prefab to run using DOTS Physics

  • We added the Physics package

  • We created our Physics Category Names

  • We added a Physics Shape and Physics Body to our Asteroid prefab

  • We updated our AsteroidSpawnSystem to initialize asteroids with PhysicsVelocity

  • We removed the VelocityComponent from the Asteroid prefab

Adding DOTS Physics to Player

  • We need to update our Player prefab similar to how we updated our Asteroid prefab

  • Now let's open our Player Prefab and add a Physics Shape component and Physics Body component to the prefab

  • Update Physics Shape

    • Shape Type = Capsule

      • Notice how when we initially switch to Capsule the shape tries to encompass all GameObjects; we will update this so only the capsule portion is contained within the Physics shape

    • Radius = 0.5

    • Center = (0, 0, 0,)

    • Friction = 0

    • Restitution = 1

    • Belongs to = Player

    • Collides With = Asteroid, Bullet, Player

  • Update Physics Body

    • GravityFactor = 0

  • Remove the VelocityComponent from the prefab

  • Now we need to update our InputMovementSystem so we update the PhysicsVelocity rather than the VelocityComponent when we add thrust

  • 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;
using Unity.Physics;

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 PhysicsVelocity velocity) =>
        {
            if (right == 1)
            {   //thrust to the right of where the player is facing
                velocity.Linear += (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.Linear += (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.Linear += (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.Linear += (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();
    }
}
  • After you have updated the InputMovementSystem, navigate to SampleScene, reimport ConvertedSubScene and hit "play"

  • You might notice that you are unable to spawn bullets

    • This is because our .ForEach() in InputSpawnSystem runs a query on entities with a VelocityComponent

    • Because our player entity no longer has a VelocityComponent, it does not appear as an entity in the .ForEach()

    • Not to worry, we will update this when we add DOTS Physics to the bullet (next section)

  • Also, it's a bit tight in the scene. Let's update our Game Settings to have a width, height and depth of 40

  • Return to SampleScene, reimport ConvertedSubScene, and hit "play"

Our Player prefab is now updated with DOTS Physics

  • We updated our Player prefab with a Physics Shape and Physics Body

  • We updated InputMovementSystem to adjust PhysicsVelocity (rather than the VelocityComponent)

Adding DOTS Physics to Bullet prefabs

  • We need to update our Bullet prefab similar to how we updated our Asteroid and Player prefabs

  • So let's open our Bullet Prefab and add a Physics Shape component and Physics Body component to the prefab

  • Update Physics Shape

    • Shape Type = sphere

    • Friction = 0

    • Restitution = 1

    • Collision Response = Raise Trigger Events

    • Belongs to = Bullet

    • Collides With = Trigger, Asteroid, Bullet, Player

  • Update Physics Body

    • Gravity Factor = 0

  • Remove the VelocityComponent from the prefab

  • Now we need to update our InputSpawnSystem to initialize the bullet PhysicsVelocity instead of its VelocityComponent (which we just deleted)

  • 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;
using Unity.Physics;

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_PlayerPrefab;

    //This will save our Bullet prefab to be used to spawn Players
    private Entity m_BulletPrefab;

    //We are going to use this to rate limit bullets per second
    //We could have included this in the game settings, no "ECS reason" not to
    private float m_PerSecond = 10f;
    private float m_NextTime = 0;

    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>();

        //We need the GameSettingsComponent to grab the bullet velocity
        //When there is only 1 instance of a component (like GamesSettingsComponent) we can use "RequireSingletonForUpdate"
        RequireSingletonForUpdate<GameSettingsComponent>();
    }
    
    protected override void OnUpdate()
    {
        //Here we set the prefab we will use
        if (m_PlayerPrefab == Entity.Null || m_BulletPrefab == Entity.Null)
        {
            //We grab the converted PrefabCollection Entity's PlayerAuthoringCOmponent
            //and set m_PlayerPrefab to its Prefab value
            m_PlayerPrefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
            m_BulletPrefab = GetSingleton<BulletAuthoringComponent>().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, selfDestruct;
        shoot = selfDestruct = 0;
        var playerCount = m_PlayerQuery.CalculateEntityCountWithoutFiltering();

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

        //If we have pressed the space bar and there is less than 1 player, create a new player
        //This will be false after we create our first player
        if (shoot == 1 && playerCount < 1)
        {
            var entity = EntityManager.Instantiate(m_PlayerPrefab);
            return;
        }

        var commandBuffer = m_BeginSimECB.CreateCommandBuffer().AsParallelWriter();
        //We must declare our local variables before the .ForEach()
        var gameSettings = GetSingleton<GameSettingsComponent>();
        var bulletPrefab = m_BulletPrefab;

        //we are going to implement rate limiting for shooting
        var canShoot = false;
        if (UnityEngine.Time.time >= m_NextTime)
        {
            canShoot = true;
            m_NextTime += (1/m_PerSecond);
        }

        Entities
        .WithAll<PlayerTag>()
        .ForEach((Entity entity, int entityInQueryIndex, in Translation position, in Rotation rotation,
                in PhysicsVelocity velocity, in BulletSpawnOffsetComponent bulletOffset) =>
        {
            //If self destruct was pressed we will add a DestroyTag to the player entity
            if(selfDestruct == 1)
            {
                commandBuffer.AddComponent(entityInQueryIndex, entity, new DestroyTag {});
            }
            //If we don't have space bar pressed we don't have anything to do
            if (shoot != 1 || !canShoot)
            {
                return;
            }
            
            // We create the bullet here
            var bulletEntity = commandBuffer.Instantiate(entityInQueryIndex, bulletPrefab);
            
            //we set the bullets position as the player's position + the bullet spawn offset
            //math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
            //think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
            var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
            commandBuffer.SetComponent(entityInQueryIndex, bulletEntity, newPosition);


            // bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
            // adding to the players physics Velocity makes sure that it takes into account the already existing player velocity (so if shoot backwards while moving forwards it stays in place)
            var vel = new PhysicsVelocity {Linear = (gameSettings.bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocity.Linear};

            commandBuffer.SetComponent(entityInQueryIndex, bulletEntity, vel);

        }).ScheduleParallel();
        
        m_BeginSimECB.AddJobHandleForProducer(Dependency);
    }
}
  • Once you have updated InputSpawnSystem navigate back to SampleScene, reimport ConvertedSubScene, then hit "play"

  • Great, now our prefabs work with Unity DOTS Physics!

    • DOTS Physics is accomplishing that what VelocityComponent and MovementSystem were doing before: taking in ECS data, running a simulation, then writing out the results

  • Because we set our bullet Collision Response to "Raise Trigger Events," the bullets do not collide with asteroids or players

  • We can now delete VelocityComponent and MovementSystem from our project completely

    • No gif here to show you how; we believe in you 💪

Our bullet prefab is now updated with DOTS Physics

  • we updated our Bullet prefab with a Physics Shape and Physics Body component

  • We updated the InputSpawnSystem to initialize PhysicsVelocity

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Adding-Physics-To-Asteroids-Players-and-Bullets

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Adding-Physics-To-Asteroids-Players-and-Bullets'

Last updated