Making the Character Move
This time around, I will be focusing on the character movement. I will be using Psyshock and Anna.
Psyshock
Psyshock is one of Latios’ modules. As described in the documentation:
Psyshock Physics is a physics and spatial query module focused on user control. While most physics engines provide out-of-the-box simulation, Psyshock instead provides direct access to the underlying algorithms so that you can craft the perfect physics simulation for your game and not waste any computation on things you don’t need. Psyshock’s Collision Layers can be built directly from Entity Queries, removing all the archetype guesswork out of collisions and triggers.
Anna

Not This Anna
Anna is a simple out-of-the-box rigid body physics engine built on Psyshock’s UnitySim APIs. It is intended to be general-purpose, striking a balance between performance, flexibility, and most importantly, ease-of-use.
Despite Anna being a pretty recent AddOn for Latios and being in early development, it is already very powerful and easy to use.
Before I go any further, I need to add a new scripting define to the project to enable Anna : LATIOS_ADDON_ANNA
and bootstrap it in the LatiosBootstrap
class.
// Easy !
Latios.Anna.AnnaBootstrap.InstallAnna(world);
I’ll create a new subscene just to hold Anna’s settings.

The settings Subdivisions Per Axis
have been chosen based on these recommendations from Psyshock’s documentation:
Lastly, you should only ever raise the subdivisions of the x-axis when you are having trouble increasing y and z and need more parallelism. This is because the x-axis already benefits from sorting, and the subdivisions don’t offer much additional benefits.
Here’s some examples:
- A top-down game with a large mostly-flat world
- (1, 1, 8) or (2, 1, 5) or similar
- A 2D side-scrolling platformer
- (3, 4, 1) or similar
- An arena shooter with lots of verticality and lots of bullets
- (2, 3, 3) or (1, 4, 4) or similar
- A massive space battle
- (1, 12, 12) or similar

The Subdivisions Per Axis
are set to (1, 1, 8)
because I want to have a top-down game with a large mostly-flat world.
Since I’m having a hard time visualizing the World Bounds, I’ve written this small editor script to help me out:
namespace Survivors.Editor.Anna
{
[CustomEditor(typeof(AnnaSettings))]
public class AnnaSettingsEditor : UnityEditor.Editor
{
void OnSceneGUI()
{
var t = target as AnnaSettings;
if (!t)
return;
var bounds = t.worldBounds;
Handles.color = Color.green;
Handles.DrawWireCube(bounds.center, bounds.size);
}
}
}
Setup
Making a base level
Still using KayKit’s assets, I’m picking a floor tile and a wall tile.
Then, I’m creating a new subscene with an exceptionally original name : Level
.
Finally, I’m adding a root game object that will hold two scripts.

Let’s break it down:
Custom Collider
is a Psyshock authoring script. I’ve set theCollider Type
toCompound
and checkedGenerate From Children
so that it will generate a compound collider from the children of the game object (obviously).Collision Tag Authoring
is an Anna authoring script. Set toInclude environment Self Only
. It will add anEnvironmentCollisionTag
to the baked entity, automatically generate anEnvironmentCollisionLayer
, add it to thesceneBlackBoardEntity
(so we can query it) and update it inBuildEnvironmentCollisionLayerSystem
.
Anna can generate colliders recursively but, since I have a compound collider, I don’t need to do that.
Here is the kind of setup I have for a floor tile:

I’m adding an empty parent to have the pivot point in a corner. It will save me some level design time with the help of snapping.
It is a similar setup for the wall tile.
I’ve added a “Wall Corner” and, since I don’t want to use MeshColliders
, I’ve made two child BoxColliders
that should get baked by the Custom Collider
script.
A simple system we can add to debug and visualize the colliders and the collision layers :
namespace Survivors.Play.Systems.Debug
{
[RequireMatchingQueriesForUpdate]
public partial struct PhysicsDebugSystem : ISystem
{
LatiosWorldUnmanaged m_world;
EntityQuery m_Query;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_world = state.GetLatiosWorldUnmanaged();
m_Query = state.Fluent()
.With<EnvironmentCollisionTag>()
.Build();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var layer = m_world.sceneBlackboardEntity
.GetCollectionComponent<EnvironmentCollisionLayer>().layer;
state.Dependency = PhysicsDebug.DrawLayer(layer).ScheduleParallel(state.Dependency);
foreach (var (collider, transformAspect) in SystemAPI.Query<RefRO<Collider>, TransformAspect>()
.WithAll<PlayerTag>())
{
var t = transformAspect.worldTransform;
PhysicsDebug.DrawCollider(in collider.ValueRO, in t, Color.green);
}
foreach (var (collider, transformAspect) in SystemAPI.Query<RefRO<Collider>, TransformAspect>()
.WithAll<EnvironmentCollisionTag>())
{
var t = transformAspect.worldTransform;
PhysicsDebug.DrawCollider(in collider.ValueRO, in t, Color.red);
}
}
}
}

Debugging the colliders. Everything looks fine.
Capsule Collider
I’ll add a CapsuleCollider
to the little barbarian and a Anna Rigid Body Authoring
component.
I’ll lock the rotation on all axes for now.

Capsule Collider and Rigid Body
Sanity Check
Let’s move up the barbarian authoring Game Object, save the scenes and run the game to see if everything is working as expected.
Barbarian bouncing on the floor. Cute.
Input and Player Movement
I’ve been reading this part of the documentation entitled “How To Make an N – 1 Render Loop” here. From what I understand, it means that explicitly ordering your systems in a random fashion would be suboptimal.
Input System
So, first, I’ll move the EscapeKeySystem
in a root super system and add a PlayerInputSuperSystem
like this:
namespace Survivors.Bootstrap.RootSystems
{
[UpdateInGroup(typeof(PreTransformSuperSystem))]
public partial class InputRootSystem : RootSuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddManagedSystem<EscapeKeySystem>();
GetOrCreateAndAddManagedSystem<PlayerInputSuperSystem>();
}
}
}
Here is the PlayerInputSuperSystem
:
namespace Survivors.Bootstrap.RootSystems.SuperSystems
{
public partial class PlayerInputSuperSystem : SuperSystem
{
EntityQuery m_shouldUpdateQuery;
protected override void CreateSystems()
{
m_shouldUpdateQuery = Fluent
.With<PauseRequestedTag>()
.Build();
GetOrCreateAndAddManagedSystem<PlayerInputSystem>();
}
public override bool ShouldUpdateSystem()
{
return m_shouldUpdateQuery.IsEmptyIgnoreFilter;
}
}
}
Upon creating the sub systems, I’m creating a query that will be used in ShouldUpdateSystem()
to check if the game is paused or not and, thus, if the input system should run or not.
Next, I’ll create an authoring script to add to the scene blackboard entity.
namespace Survivors.Play.Authoring
{
public class InputAuthoring : MonoBehaviour
{
class InputAuthoringBaker : Baker<InputAuthoring>
{
public override void Bake(InputAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.None);
AddComponent<PlayerInputState>(entity);
}
}
}
public struct PlayerInputState : IComponentData
{
// We don't need more right now
public float2 Direction;
}
}
Finally, I’ll create the PlayerInputSystem
that will read the input and update the PlayerInputState
component.
namespace Survivors.Play.Systems.Input
{
[RequireMatchingQueriesForUpdate]
public partial class PlayerInputSystem : SubSystem
{
InputSystem_Actions m_inputActions;
EntityQuery m_Query;
protected override void OnCreate()
{
m_inputActions = new InputSystem_Actions();
m_inputActions.Enable();
m_Query = Fluent
.With<PlayerInputState>()
.Build();
}
protected override void OnDestroy()
{
m_inputActions.Disable();
}
protected override void OnUpdate()
{
var move = m_inputActions.Player.Move.ReadValue<Vector2>();
if (math.length(move) > 0.01f) move = math.normalize(move);
sceneBlackboardEntity.SetComponentData(new PlayerInputState
{
Direction = move
});
}
}
}
Player Movement System
Now, to use the input state to move the player, I’ll need an authoring script to:
- set a
PlayerTag
and aMovementSettings
component. - add a
PlayerMovementSystem
to thePlayerMotionSuperSystem
in thePreAnnaRootSystem
(because I’ll move the player with RigidBody).
namespace Survivors.Play.Authoring
{
public class PlayerAuthoring : MonoBehaviour
{
[SerializeField] private MovementSettings movementSettings;
private class PlayerAuthoringBaker : Baker<PlayerAuthoring>
{
public override void Bake(PlayerAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<PlayerTag>(entity);
AddComponent(entity, authoring.movementSettings);
}
}
}
public struct PlayerTag : IComponentData { }
[Serializable]
public struct MovementSettings : IComponentData
{
public float moveSpeed;
// not yet used
public float rotationSpeed;
public float speedChangeRate;
}
}
namespace Survivors.Play.Systems.Player
{
[RequireMatchingQueriesForUpdate]
public partial struct PlayerMovementSystem : ISystem
{
LatiosWorldUnmanaged m_world;
EntityQuery m_Query;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_world = state.GetLatiosWorldUnmanaged();
m_Query = state.Fluent()
.With<PlayerInputState>()
.Build();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var playerInputState = m_world.sceneBlackboardEntity.GetComponentData<PlayerInputState>();
var move = playerInputState.Direction;
foreach (var (rb, movementSettings) in SystemAPI.Query<RefRW<RigidBody>, RefRO<MovementSettings>>()
.WithAll<PlayerTag>())
{
var currentVelocity = rb.ValueRO.velocity.linear;
var desiredVelocity = new float3(move.x, 0f, move.y) * movementSettings.ValueRO.moveSpeed;
// We don't want to change the gravity force
desiredVelocity.y = currentVelocity.y;
currentVelocity =
currentVelocity.MoveTowards(desiredVelocity, movementSettings.ValueRO.speedChangeRate);
rb.ValueRW.velocity.linear = currentVelocity;
}
}
}
}
Where does this MoveTowards
function come from ? It’s a simple extension method I wrote that mimics Vector3.MoveTowards(Vector3 current, Vector3 target, float maxDistanceDelta)
.
namespace Survivors.Utilities
{
public static class Float3Extensions
{
public static float3 MoveTowards(this float3 current,
float3 target,
float maxDistanceDelta)
{
var a = target - current;
var magnitude = math.length(a);
if (magnitude <= maxDistanceDelta || magnitude < math.EPSILON) return target;
return current + a / magnitude * maxDistanceDelta;
}
}
}
Now, we can add it to some system hierarchy.
namespace Survivors.Bootstrap.RootSystems
{
[UpdateBefore(typeof(AnnaSuperSystem))]
public partial class PreRigidBodyRootSystem : RootSuperSystem
{
EntityQuery m_shouldUpdateQuery;
protected override void CreateSystems()
{
m_shouldUpdateQuery = Fluent.With<PauseRequestedTag>()
.Build();
GetOrCreateAndAddManagedSystem<PlayerMotionSuperSystem>();
}
public override bool ShouldUpdateSystem()
{
return m_shouldUpdateQuery.IsEmptyIgnoreFilter;
}
}
}
namespace Survivors.Bootstrap.RootSystems.SuperSystems
{
public partial class PlayerMotionSuperSystem : SuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddUnmanagedSystem<PlayerMovementSystem>();
}
}
}
Issue
If we play the game and press pause…
The PlayerMovementSystem
will not run, it will nor update the RigidBody velocity but the physics simulation will still run and the player will be able to move.
To solve this, I first tried to override AnnaSuperSystem
and used the ShouldUpdateSystem()
method to check if the game is paused or not.
Sadly, when Anna’s simulation is paused, a lot of errors are thrown in the console.
A wild guess : most of the operations run by Anna are using multithreaded Pyshsock jobs that may or may not be completed when the simulation is paused, thus the errors complaining about “disposed arrays” and “layers” being “different”.
Update Mar 30 2025: It was a bug “around collision layer allocation lifecycles”
Hacking
I don’t know if this is the best way to do it but I added a new authoring script that adds a SavedRigidBodyState
component to the player (and future enemies).
namespace Survivors.Play.Authoring.Anna
{
[AddComponentMenu("Survivors/Anna/AnnaRigidBodyExtension")]
[RequireComponent(typeof(AnnaRigidBodyAuthoring))]
public class AnnaRigidBodyExtensionAuthoring : MonoBehaviour
{
class AnnaRigidBodyExtensionBaker : Baker<AnnaRigidBodyExtensionAuthoring>
{
public override void Bake(AnnaRigidBodyExtensionAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<SavedRigidBodyState>(entity);
}
}
}
/// <summary>
/// Saved state of a rigid body.
/// </summary>
public struct SavedRigidBodyState : IComponentData
{
public UnitySim.Velocity Velocity;
public float InverseMass;
public half CoefficientOfFriction;
public half CoefficientOfRestitution;
}
}
Then, I’m creating a Root Super System that will run before the AnnaSuperSystem
like the PreRigidBodyRootSystem
. It will update when the pause is requested. Using ISystemStartStop
, it will save the rigid body state in OnStartRunning
and restore it in OnStopRunning
.
namespace Survivors.Bootstrap.RootSystems
{
[UpdateBefore(typeof(AnnaSuperSystem))]
public partial class PreAnnaRootSystem : RootSuperSystem
{
EntityQuery m_shouldUpdateQuery;
protected override void CreateSystems()
{
m_shouldUpdateQuery = Fluent.With<PauseRequestedTag>()
.Build();
GetOrCreateAndAddUnmanagedSystem<PreAnnaPauseSystem>();
}
public override bool ShouldUpdateSystem()
{
return !m_shouldUpdateQuery.IsEmptyIgnoreFilter;
}
}
}
Registration
namespace Survivors.Play.Systems.PreAnna
{
public partial struct PreAnnaPauseSystem : ISystem, ISystemStartStop
{
EntityQuery m_shouldUpdateQuery;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_shouldUpdateQuery = state.Fluent().Without<PauseRequestedTag>()
.Build();
}
public void OnStartRunning(ref SystemState state)
{
var world = state.GetLatiosWorldUnmanaged();
var ecb = world.syncPoint.CreateEntityCommandBuffer();
foreach (var (rb, last, entity) in
SystemAPI.Query<RefRO<RigidBody>,
RefRW<SavedRigidBodyState>>()
.WithEntityAccess())
{
last.ValueRW.Velocity = rb.ValueRO.velocity;
last.ValueRW.InverseMass = rb.ValueRO.inverseMass;
last.ValueRW.CoefficientOfFriction = rb.ValueRO.coefficientOfFriction;
last.ValueRW.CoefficientOfRestitution = rb.ValueRO.coefficientOfRestitution;
ecb.RemoveComponent<RigidBody>(entity);
}
}
public void OnStopRunning(ref SystemState state)
{
var world = state.GetLatiosWorldUnmanaged();
var ecb = world.syncPoint.CreateEntityCommandBuffer();
foreach (var (last, entity)
in SystemAPI.Query<RefRO<SavedRigidBodyState>>()
.WithEntityAccess())
ecb.AddComponent(entity, new RigidBody
{
velocity = last.ValueRO.Velocity,
inverseMass = last.ValueRO.InverseMass,
coefficientOfFriction = last.ValueRO.CoefficientOfFriction,
coefficientOfRestitution = last.ValueRO.CoefficientOfRestitution
});
}
}
}
System
Barbarian paused. No more movement.
More Level Design and a proper Top-Down Camera
Camera
I’ll have a simple Cinemachine setup: a Cinemachine Camera with a Cinemachine Spline Dolly. I’ll setup only one knot for the spline (for now).

One knot for the spline
The Spline is a child of an empty game object that will be the target of the camera.
I’m adding a quick MonoBehaviour to move the target and register it in the GameLifetimeScope
.
namespace Survivors.GameScope.MonoBehaviours
{
[AddComponentMenu("Survivors/Cinemachine Behaviour")]
public class CinemachineBehaviour : MonoBehaviour
{
[SerializeField] Transform target;
[SerializeField] CinemachineSplineDolly dolly;
public void SetTargetPosition(Vector3 position)
{
target.position = position;
}
}
}
Something that will be useful here and later is to have a system that tracks the player’s position and set it in a component to the Scene Blackboard Entity.
namespace Survivors.Play.Authoring
{
public class PlayerPositionAuthoring : MonoBehaviour
{
class PlayerPositionAuthoringBaker : Baker<PlayerPositionAuthoring>
{
public override void Bake(PlayerPositionAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.None);
AddComponent<PlayerPosition>(entity);
}
}
}
public struct PlayerPosition : IComponentData
{
public float3 Position;
}
}
I could have tow systems that will run in the LateSimulationSystemGroup
.
One will update the PlayerPosition
component and the other will set the target position of the camera (in that order). The later will have the CinemachineBehaviour
injected by VContainer.
namespace Survivors.Bootstrap.RootSystems
{
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
public partial class LateSimulationRootSystem : RootSuperSystem
{
protected override void CreateSystems()
{
GetOrCreateAndAddUnmanagedSystem<PlayerPositionUpdater>();
GetOrCreateAndAddManagedSystem<CinemachineTargetUpdater>();
}
}
}
namespace Survivors.Play.Systems.BlackBoard
{
public partial struct PlayerPositionUpdater : ISystem
{
LatiosWorldUnmanaged m_world;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_world = state.GetLatiosWorldUnmanaged();
state.RequireForUpdate<PlayerTag>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var playerPosition = float3.zero;
foreach (var transformAspect in SystemAPI.Query<TransformAspect>()
.WithAll<PlayerTag>())
playerPosition = transformAspect.worldTransform.position;
m_world.sceneBlackboardEntity.SetComponentData(new PlayerPosition
{
Position = playerPosition
});
}
}
}
namespace Survivors.Play.Systems.Camera
{
public partial class CinemachineTargetUpdater : SubSystem
{
CinemachineBehaviour m_cinemachine;
[Inject]
public void Construct(CinemachineBehaviour cinemachine)
{
m_cinemachine = cinemachine;
}
protected override void OnCreate()
{
RequireForUpdate<PlayerTag>();
}
protected override void OnUpdate()
{
var playerPosition = sceneBlackboardEntity.GetComponentData<PlayerPosition>();
m_cinemachine.SetTargetPosition(playerPosition.Position);
}
}
}
Far from a blockbuster but it works.
Level Design
I’ll skip the level design part as I’m not the most qualified person to talk about it.
Here is where I’m stopping for now. It can already make a good test case for soon to come pathfinding and enemy AI.

Some post processing, some lights. Done.
Closing Words
I hope you enjoyed this second part of this walkthrough. Next, I’ll be working on adding more animations to the player character.
Oh, and here is the code : https://github.com/clandais/Latios-Survivors-Articles/tree/Part2