Dynamically Changing Ghosts Between Interpolated and Predicted

Code and workflows to update ghosts between interpolated and predicted

What you'll develop on this page

We will update Asteroids to be "predicted" when within a specified distance from the player. Although difficult to tell from the gif the Asteroids close to the player are moving smoother than those far away.

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Changing-Between-Interpolated-and-Predicted

Updating Asteroids Based on Proximity to Player

Currently all the Asteroids are interpolated. That means that the server updates their movement, and then snapshots are sent to the clients to update their position.

You might have noticed that the Asteroids movement is not as "smooth" as it was in our Physics section. The reason is that the client does not get as many update s because of the amount of Asteroids. The server has to send a snapshot for every single Asteroid.

By interpolating all the asteroids, we reduce the computation requirements needed on the client, because they run less physics. So it is a trade-off, do we reduce computation and increase bandwidth?

The answer, like most things in engineering, it depends! For a large scale game, it probably is unnecessary to predicted the physics of everything on the map, especially when the player cannot see/interact with the predicted objects. So in this section we will implement a system that changes which Asteroids are interpolated vs. predicted based on a player's proximity.

  • First let's make a ClientSettingsAuthoringComponent.cs in Authoring/ to create an authoring component where we will store the radius where Asteroids switch form interpolated to predicted

using Unity.Entities;

[GenerateAuthoringComponent]
public struct ClientSettings : IComponentData
{
    public float predictionRadius;
    public float predictionRadiusMargin;

}
  • Let's add this component in ConvertedSubScene on the GameSettings GameObject

  • Let's set predictionRadius to 5 and predictionRadiusMargin to 1

  • Now let's create AsteroidSwitchPredictionSystem.cs in Client/Systems

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Collections;
using Unity.Burst;

[UpdateInGroup(typeof(ClientSimulationSystemGroup))]
public partial class AsteroidSwitchPredictionSystem : SystemBase
{
    private NativeList<Entity> m_ToPredicted;
    private NativeList<Entity> m_ToInterpolated;
    private GhostSpawnSystem m_GhostSpawnSystem;
    protected override void OnCreate()
    {
        RequireSingletonForUpdate<ClientSettings>();
        m_ToPredicted = new NativeList<Entity>(16, Allocator.Persistent);
        m_ToInterpolated = new NativeList<Entity>(16, Allocator.Persistent);
        m_GhostSpawnSystem = World.GetExistingSystem<GhostSpawnSystem>();
    }
    protected override void OnDestroy()
    {
        m_ToPredicted.Dispose();
        m_ToInterpolated.Dispose();
    }
    protected override void OnUpdate()
    {
        var spawnSystem = m_GhostSpawnSystem;
        var toPredicted = m_ToPredicted;
        var toInterpolated = m_ToInterpolated;
        for (int i = 0; i < toPredicted.Length; ++i)
        {
            if (EntityManager.HasComponent<GhostComponent>(toPredicted[i]))
                spawnSystem.ConvertGhostToPredicted(toPredicted[i], 1.0f);
        }
        for (int i = 0; i < toInterpolated.Length; ++i)
        {
            if (EntityManager.HasComponent<GhostComponent>(toInterpolated[i]))
                spawnSystem.ConvertGhostToInterpolated(toInterpolated[i], 1.0f);
        }
        toPredicted.Clear();
        toInterpolated.Clear();

        var settings = GetSingleton<ClientSettings>();
        if (settings.predictionRadius <= 0)
            return;

        if (!TryGetSingletonEntity<PlayerCommand>(out var playerEnt) || !EntityManager.HasComponent<Translation>(playerEnt))
            return;
        var playerPos = EntityManager.GetComponentData<Translation>(playerEnt).Value;

        var radiusSq = settings.predictionRadius*settings.predictionRadius;
        Entities
            .WithNone<PredictedGhostComponent>()
            .WithAll<AsteroidTag>()
            .ForEach((Entity ent, in Translation position) =>
        {
            if (math.distancesq(playerPos, position.Value) < radiusSq)
            {
                // convert to predicted
                toPredicted.Add(ent);
            }
        }).Schedule();
        radiusSq = settings.predictionRadius + settings.predictionRadiusMargin;
        radiusSq = radiusSq*radiusSq;
        Entities
            .WithAll<PredictedGhostComponent>()
            .WithAll<AsteroidTag>()
            .ForEach((Entity ent, in Translation position) =>
        {
            if (math.distancesq(playerPos, position.Value) > radiusSq)
            {
                // convert to interpolated
                toInterpolated.Add(ent);
            }
        }).Schedule();
    }
}
  • Now let's hit play and check out the difference for Asteroids that are close to the player

  • The Asteroids that are closer to the player are moving in a smoother fashion! (because they are predicted)

  • What is great is that this runs on the client side, so the client can make a decision of how much prediction to do

    • You can imagine you can have settings where lower powered devices run less prediction

We now predict Asteroids that are within our prediction radius

  • We created the ClientSettingsAuthoringComponent and added it to the GameSettings GameObject in ConvertedSubScene

  • We created AsteroidSwitchPredictionSystem to use these settings to change nearby Asteroids to predicted from interpolated (and switch back)

Github branch link:

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Changing-Between-Interpolated-and-Predicted'

Last updated