Am I Misunderstanding Client Prediction?

Hello! I’ve been working on a Netcode for Entities project and I’m struggling to implement certain gameplay mechanics that I’d like to be client predicted. When I can get them to work, the implementation I came up with doesn’t feel quite right. I’ll give you a few examples of what I’m trying to do below and the kinds of things I am running into.

Before I get into it, here are some basic questions that may be related - Does prediction only work on input or command data? Further, does input/command data need to be written to every simulation tick?

Example 1: Point and Click Movement
The end goal is to have a system that allows a player to click anywhere in the game world and move their character to the selected position. I’d want this system to be client predicted so the player immediately begins to move towards the destination. I did get this to work, but my solution just seems “okay” so I’d be curious to know if there is a better way to do this.

On the client, have a target position component that I set whenever the user clicks into the game world.

[GhostComponent(PrefabType = GhostPrefabType.PredictedClient)]
public struct ClientTargetPosition : IComponentData
{
    public float3 Value;
}

Also on the client, I have a system that runs in the GhostInputSystemGroup which sets an IInputComponentData every frame - seems like it doesn’t work if I only set this whenever the user clicks.

[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct InputTargetPosition : IInputComponentData
{
    [GhostField(Quantization = 0)] public float3 Value;
}

Then finally my move system is a predicted system that moves the LocalTransform towards the value stored in the input component.

Again, this all seems to work fine, I’d just be curious if there is any way to optimize this so maybe the input component doesn’t need to get written to every frame or otherwise. Using command or input data seems like the correct approach over an RPC as users could spam-click, so having a constant and reliable communication stream of input seems to make sense to me.

Example 2: Time-Based Attack System
This one I seem to be struggling with a bit more as I’m trying to “predict” non-input data. End goal is to have a player target another player. Once the target player is within range, the attacking player can apply an attack to the targeted player. At that time, a cooldown timer begins where the attacking player cannot execute any more attacks. If the attacking player keeps the targeting player selected, the attacking player will automatically attack as soon as the cooldown expires. I would also want this to be predicted so that as soon as an attack is executed, damage effects can be spawned into the world, etc.

I initially tried using a GhostComponent + GhostField for a countdown timer that I would tick down and reset in a predicted system, however the server values just override the client values in the GhostUpdateSystem. To my understanding, this is the correct behavior as GhostFields basically just display the values from the server and can’t be set directly. The result in the game is that whenever the timer expires, the client performs an attack, but then the GhostUpdateSystem keeps setting the cooldown timer back to a value that will apply the attack, so the client preforms the attack multiple frames in succession until the tick where the server cooldown is reset gets received by the client.

Another implementation I tried is something I came across in the Asteroids sample project, where rather than using a countdown timer, the server tick where the cooldown is complete is stored. However, this has the same effect as the timer implementation.

public struct MeleeAttackCooldown : IComponentData
{
    [GhostField] public NetworkTick Value;
}

So what would be the best way to implement a predicted system like this? Am I going to need to use some command data or input data for this as well?

Example 3: Destroy Entity System
Because entities cannot be destroyed client-side, I’ve implemented an entity destruction system that can have the effect of predicting entity destruction, let me know what you think of it and if there are any ways I can improve. Use case for this would be dealing the final blow to an enemy which should make them immediately disappear.

When I need to destroy an entity, I add a DestroyEntityTag to the entity in a predicted system. Then I have another predicted system that will destroy the entity on the server side, or move the entity offscreen client side.

This is another one that seems to work fine, but the main issue I see with this is that I’m unsure how it would react to handling the case where a client predicts the destruction of an entity, but the server rejects the destruction.

foreach (var (transform, entity) in SystemAPI.Query<RefRW<LocalTransform>>().WithAll<DestroyEntityTag, Simulate>().WithEntityAccess())
{
    if (state.World.IsServer())
    {
        ecb.DestroyEntity(entity);
    }
    else
    {
        transform.ValueRW.Position = new float3(1000f, 1000f, 1000f);
    }
}

Anyways, still just a bit unsure if I am thinking about prediction correctly so any feedback or guidance related to anything mentioned above would be tremendously appreciated. Happy to post any more code if you want to have a look. Thank you!!

Prediction is based on command data. As such, user is responsible to sample every frame the input (either Input or new InputSystem) and convert that to either ICommandData and push into the command buffer or using IInputComponentData (as you did) that does that work for you.

You can avoid writing the to the IInputComponent every frame if there is no change in the target position. But that will not change the fact the input is buffered and transmitted to the server every tick (because this is the way it should work).

I think the problem is the misunderstanding of how things work. Let’s clarify. So, Netcode is server authoritative right? As such the server is the one responsible to do all the simulation (no the client). The only thing the client is authoritative about is their input (i.e pressed up, moving to X etc).

When using ghost, data is replicated from server to client at Network TickRate (that is by default identical to the simulation tick rate, so say 60hz). The data sent is are the component marked with [GhostField] attribute (as you did).

When the client receive the data from the server these fields are reset to the value the server has at tick X, no matter what the client set locally (because server is the authority).

If the ghost is set as Predicted, client can also predict/simulate the ghost ands its components and change these values inside the prediction group, using the same logic the server does, so that when the new values are received from the server (that is in the past) they usually match (predicted correctly).

If the ghost is Interpolated or OwnerPredicted and the client is not the owner, the client will not predict any data but just interpolate (eventually) the value of the fields (in case of integer there is not interpolation of course).

This is why you may see the value being set every frame to the server value.

For timers in general, the best approach (and more robust) is to use a terget tick when an action should occur instead of using time (that can pass slightly different on server and client because of the catch-up mechanism and partial ticks).

If you need to embed the TargetTick as part of a command or being replicated via ghost fields depends on your game logic, so depending on the case either are correct approaches.

Predicting ghost destruction is tricky. And you discovered one way to do it is queue the destruction of the ghost on the client (and potentially disabling the entity and its rendering or moving it away), and wait for a confirmation on the server (the despawn of the entiity). Because client does that in the future, either the client predict that correctly (and the server will send at a certain point the despawn) or will not. There is not guarantee that you either receive the Tick X from the server (and so you may received the despawn later). So you need to put some ticks guard/range on the queue despawn on the client side to check if you mispredict (say current tick + 10) and if after that point no despawn has been received, you undo the despawn on the client.
Being the ghost still present, the data is still updated (so you have the newest data from the server). There may be some glitches and tuning to do but mostly the logic looks like that.

Thank you for the detailed response @CMarastoni it’s helpful to confirm some of my assumptions and fill in some of the gaps I still had.

Awesome - I was able to get this setup fairly easily and it is a much better solution than what I had before where I had a client only component constantly writing to the input component. The systems are now much cleaner and it’s one less thing to go wrong.

Makes sense to me! I was able to change the attack system to work based off the target network tick stored in a command data component. Seems to work with prediction and client/server seem to be well synched.

Out of curiosity, what would be a use case for using a ghost field over a command? What are the general steps to get this working as I haven’t quite figured that out yet? Doesn’t have to be directly related to the timer tick example as this is something I may want to use for other features.

Your suggestions on entity destruction all sound good to me! I’m thinking about a more robust solution for this that could make the misprediction delay based off the avg client ping plus a 5-10 tick buffer.

Thanks again!

Yes, this is a robust one, or you can already use the InterpolationTick as baseline (because it is already behind by the client RTT plus a couple of ticks)