#Predicted CharacterController with knockback

61 messages · Page 1 of 1 (latest)

patent lark
#

Currently i am trying to add knockback to my predicted CC. when a projectile hits a target "AddKnockback" is called locally to allow clients to predict knockback, knockback is used inside Replicate (RunInputs) to add to the character movement.

The problem is, at the moment the client does not see knockbacks and instead is reconciled to the servers position and only plays the end of the knockback effect.

ill include a video and the code. if anyone has any ideas please let me know, been struggling with this for a few days.

Onhit function (this is on a predicted projectile)

 private void OnTriggerEnter(Collider other)
    {

        if (other.gameObject == _owner.playerStats.gameObject) { return; }


        if (InstanceFinder.IsClientStarted)
        {
            //If client show visual effects, play impact audio.
            Instantiate(hitVfx, transform.position, transform.rotation);

            PlayerPredictedMovement victimMovement = other.gameObject.GetComponent<PlayerPredictedMovement>();
            if(victimMovement != null) { victimMovement.AddKnockback(.17f, transform.position); }
            
        }

        if (InstanceFinder.IsServerStarted)
        {
            PlayerStats victim = other.gameObject.GetComponent<PlayerStats>();
            

            if (victim != null) 
            {
                victim.TakeDamage(1);

               // PlayerPredictedMovement victimMovement = other.gameObject.GetComponent<PlayerPredictedMovement>();
               // if (victimMovement != null) { victimMovement.AddKnockback(.17f, transform.position); }
            }
                
            else { Debug.Log("No victim found" + other.gameObject); }


        }


        Destroy(gameObject);

    }
#
   public void AddKnockback(float pendingKnockbackForce, Vector3 knockbackOrigin)
    {
        Vector3 knockDirection = (transform.position - knockbackOrigin).normalized;
        knockDirection.y = 0;
        knockbackVelocity = knockDirection * pendingKnockbackForce;
    }
``` knockback function
#
private ReplicateData CreateReplicateData()
    {
        if (!base.IsOwner)
            return default;

        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");
        Quaternion playerRotationTarget = RotatePlayerToMouse();


        ReplicateData md = new ReplicateData(horizontal, vertical, playerRotationTarget, knockbackVelocity, velocity);


        return md;
    }

#
 private ReplicateData _lastCreatedInput = default;

    [Replicate]
    private void RunInputs(ReplicateData data, ReplicateState state = ReplicateState.Invalid, Channel channel = Channel.Unreliable)
    {
        if (!IsOwner && !IsServerInitialized)
            return;

        if (state.IsFuture())
        {
            uint lastCreatedTick = _lastCreatedInput.GetTick();

            uint thisTick = data.GetTick();
            if ((data.GetTick() - lastCreatedTick) <= 1)
            {
                data.Vertical = _lastCreatedInput.Vertical;
                data.Horizontal = _lastCreatedInput.Horizontal;
            }
        }

        else if (state == ReplicateState.ReplayedCreated)
        {

            _lastCreatedInput.Dispose();
            
            _lastCreatedInput = data;
        }


        Vector3 moveInput = new Vector3(data.Horizontal, 0, data.Vertical).normalized;

        Vector3 movement = moveInput * (moveSpeed * (float)TimeManager.TickDelta);

        velocity = movement;

       // characterController.Move(velocity + data.KnockbackVelocity);
        characterController.Move(velocity + knockbackVelocity);
        transform.rotation = data.Rotation;

        knockbackVelocity *= .92f;
        if (knockbackVelocity.magnitude < .01f) { knockbackVelocity = Vector3.zero; }

    }


    private void TimeManager_OnTick()
    {
        RunInputs(CreateReplicateData());
    }
    private void TimeManager_OnPostTick()
    {
        CreateReconcile();

    }

#
    public override void CreateReconcile()
    {
        transform.GetPositionAndRotation(out Vector3 position, out Quaternion rotation);
        ReconcileData rd = new ReconcileData(position, rotation, knockbackVelocity, velocity);
        ReconcileState(rd);
    }

    [Reconcile]
    private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
    {
    transform.SetPositionAndRotation(data.Position, data.Rotation);
        velocity = data.Velocity;
        knockbackVelocity = data.KnockbackVelocity;

    }
#

version 4.4.6

cobalt trench
#

How are you "activating" an attack?

patent lark
cobalt trench
#

Are you using predicted spawns?

patent lark
#

it seems like the value is being updated locally, but is instantly overridden by the server

patent lark
# cobalt trench Are you using predicted spawns?
    private void ClientFire()
    {
        if(!IsOwner) { return; }

        Vector3 position = firePoint.transform.position;
        Vector3 direction = firePoint.transform.forward;

        /* Spawn locally with 0f passed time.
         * Since this is the firing client
         * they do not need to accelerate/catch up
         * the projectile. */
        SpawnProjectile(position, direction, 0f);
        //Ask server to also fire passing in current Tick.
        ServerFire(position, direction, base.TimeManager.Tick);
    }

Yeah, here is the fire script

#
    private void Move()
    {
        //Frame delta, nothing unusual here.
        float delta = Time.deltaTime;

        //See if to add on additional delta to consume passed time.
        float passedTimeDelta = 0f;
        if (_passedTime > 0f)
        {
            /* Rather than use a flat catch up rate the
             * extra delta will be based on how much passed time
             * remains. This means the projectile will accelerate
             * faster at the beginning and slower at the end.
             * If a flat rate was used then the projectile
             * would accelerate at a constant rate, then abruptly
             * change to normal move rate. This is similar to using
             * a smooth damp. */

            /* Apply 8% of the step per frame. You can adjust
             * this number to whatever feels good. */
            float step = (_passedTime * 0.1f);
            _passedTime -= step;

            /* If the remaining time is less than half a delta then
             * just append it onto the step. The change won't be noticeable. */
            if (_passedTime <= (delta / 2f))
            {
                step += _passedTime;
                _passedTime = 0f;
            }
            passedTimeDelta = step;
        }

       

        //Move the projectile using moverate, delta, and passed time delta.
        transform.position += _direction * (projectileSpeed * (delta + passedTimeDelta));
    }
``` and projectile movement here
#

the projectile is synced nicely, but the knockback value on client seems like it is ignored. or instantly reconciled by server

cobalt trench
#

Is your knockback velocity in your reconcile data?

#

I see that it is.. Nevermind.

patent lark
cobalt trench
#

I'm wondering if you're suffering from the default data problem too 😄

#

Can you try something for me?

#

Add a boolean named IsRealData in your ReplicateData struct and set it to true in the constructor.

#

Return at the top of your Replicate function if IsRealData is false.

patent lark
#

do i need to set it to false anywhere?

cobalt trench
#

The default value should be false so no.

patent lark
#

ok testing now

#

still have the same results

#

If I use the editor as client only, and then update the knockback velocity from the inspector. i can see the character snap very quickly and the knockbackvelocity is instantly set back to 0

#

its like the local value of knockbackvelocity is instantly reconciled

cobalt trench
#

Are you setting knockback velocity anywhere else?

patent lark
# cobalt trench The default value should be false so no.

i tried to remove knockbackvelocity from the reconcile, and now the client can use the knockbackvelocity locally from the inspector. but OnHit the knockbackvelocity is never updated it stays at 0. it just looks like its partially working because its reconciling its position on the server

patent lark
cobalt trench
#

Try disabling reconciliation altogether. See if it solves the problem on the client.

patent lark
#

so something with this must be off

        Vector3 moveInput = new Vector3(data.Horizontal, 0, data.Vertical).normalized;

        Vector3 movement = moveInput * (moveSpeed * (float)TimeManager.TickDelta);

        velocity = movement;

       // characterController.Move(velocity + data.KnockbackVelocity);
        characterController.Move(velocity + knockbackVelocity);
        transform.rotation = data.Rotation;

        knockbackVelocity *= .92f;
        if (knockbackVelocity.magnitude < .01f) { knockbackVelocity = Vector3.zero; }

this is inside replicate

#

also without reconciliation my client moves much faster than the server. maybe that is the underlying cause here

cobalt trench
#

I think the issue is that you're reducing the knockback velocity in Replicate.

patent lark
delicate atlas
#

i have similar issues in my prediction model (though i dont know if its the same issue).

ill have some non-owned forces (like a grenade) applied to my prediction rigidbody (on both client and server, as per CSP), but then on my client, on the next frame or so after the force, a reconcile from the server immediately resets that velocity, until a few frames later when the server has also applied those non-owned forces and sent the latest reconcile

i havent had time to make a simpler case for this yet though, so i dont know if the issue is on my side or not. i was hoping that the new CreateLocalReconciles option would help here actually!

patent lark
patent lark
patent lark
#

got this working finally

patent lark
merry star
#

Remember that a predicted CC undergoes replays many times, so anything that applies a force to a CC needs to apply it once per replay, aka multiple times per collision or event. This usually means that the object applying the force is predicted as well (e.g: using NetworkCollider functions)

delicate atlas
delicate atlas
patent lark
#

apply knockback for clients locally and on server

pass knockback value in along with inputs in CreateReplicateData

in RunInputs use data.KnockbackValue to apply the knockback.

dont reconcile knockback

patent lark
#
    private ReplicateData CreateReplicateData()
    {
        if (!base.IsOwner)
            return default;

        //Build the replicate data with all inputs which affect the prediction.
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");
        Quaternion playerRotationTarget = RotatePlayerToMouse();

        ReplicateData md = new ReplicateData(horizontal, vertical, playerRotationTarget, velocity, dashForce, knockbackVelocity);

        return md;
    }
#
        Vector3 moveInput = new Vector3(data.Horizontal, 0, data.Vertical).normalized;

        Vector3 movement = (moveInput * moveSpeed) * tickDelta;
        velocity = movement;


        knockbackVelocity *= .99f;
        if (knockbackVelocity.magnitude < .001f) { knockbackVelocity = Vector3.zero; }

        dashForce *= .98f;
        if (dashForce.magnitude < .001f) { dashForce = Vector3.zero; }


        velocity += data.DashForce + data.KnockbackVelocity;

        //characterController.Move(velocity + data.DashForce + data.KnockbackVelocity);     // USE THIS INSTEAD IF COMBINING ALL MOVEMENT FORCES CAUSES ISSUES ON RECONCILIAITON
        characterController.Move(velocity);
        transform.rotation = data.Rotation;
    }``` ^ replicate or RunInputs
#
    [Reconcile]
    private void ReconcileState(ReconcileData data, Channel channel = Channel.Unreliable)
    {
        transform.SetPositionAndRotation(data.Position, data.Rotation);
        velocity = data.Velocity;


       // knockbackVelocity = data.KnockbackVelocity;  // this is an input dont reconcile this or dashForce

    }
delicate atlas
#

that makes sense! ill probably update my approach to do something like this

patent lark
#

took me like 5 days to figure this out. seems so simple

#

i had like a realization today at work. got home and it worked first try

#

knockback is an input.

delicate atlas
#

feels amazing when that happens :)

patent lark
#

seriously

patent lark
delicate atlas
#

for sure. we're pretty deep in this project so our solution will probably need a lot of complexity sweats

twilit gorge
#

I also tried removing NetworkTransform script and turn on the PredictedObject prediction implementation, but it seems that it does't make any difference

patent lark
#

collider rollback is different from CSP, and if you're using CSP you need to get rid of network transform

#

use a NOB with prediction enabled