I have a character who shoots an arrow projectile. In order to have it feel more responsive, my strategy is to
- Spawn a dummy projectile (basically just a movable model) on the owner client side and store a reference to it.
- Have the server spawn a networked projectile, with Network Transform.
- Position the server-side networked projectile to compensate for latency. The goal is to position the server-side projectile to be in the same position it is on the owner client, so that future ticks should keep the projectile trajectories in sync.
- When the networked projectile spawns on a client, destroy any dummy projectiles. If the server-side projectile was properly synced, the objects should have been overlapping and there shouldn't be a noticeable effect.
Is my strategy right? If so, I'm having trouble with step 3, in that I'm not sure what time deltas to be using to calculate the latency compensation.
For steps 1 and 2, I have something like this on the AbilityUser class:
public class AbilityUser : NetworkBehaviour {
private NetworkedProjectile m_ProjectilePrefab;
private List<Projectile> m_DummyProjectiles = new List<Projectile>();
// called on TimeManager.OnTick
private void HandleTimeManagerTick() {
if (IsOwner) {
BuildAbilityCastData(out AbilityCastData castData); // based on a queued ability input from player
CastAbility(castData, false);
}
if (IsServer) {
CastAbility(default, true);
}
}
private void CastAbility(AbilityCastData castData, bool asServer, bool isReplaying = false) {
if (!castData.HasQueuedAbility) {
return;
}
if (isReplaying) {
return;
}
if (asServer) {
NetworkedProjectile networkedInstance = Instantiate(m_ProjectilePrefab); // networked projectile also has a projectile component
Spawn(networkedInstance.gameObject);
return;
}
Projectile dummyProjectile = Instantiate(m_ProjectilePrefab.DummyProjectilePrefab); // projectile is a simple motor component
m_DummyProjectiles.Add(dummyProjectile); // store these so we can destroy them later
}
My non-networked projectile component provides a simple motor:
public class Projectile : MonoBehaviour {
private Vector3 m_InitialPosition;
private float m_TimeElapsedSeconds;
public void Launch(Vector3 targetPosition, float travelTimeSeconds) {
m_InitialPosition = transform.position;
StartCoroutine(LaunchCo(targetPosition, travelTimeSeconds));
}
private IEnumerator LaunchCo(Vector3 targetPosition, float travelTimeSeconds) {
while (m_TimeElapsedSeconds < travelTimeSeconds) {
float t = m_TimeElapsedSeconds / travelTimeSeconds;
transform.position = Vector3.Lerp(m_InitialPosition, targetPosition, t);
m_TimeElapsedSeconds += Time.deltaTime; // this feels like it might not be the right time tick
yield return null;
}
}
public void AdjustTime(float amt) {
m_TimeElapsedSeconds += amt; // hopefully this causes the motor coroutine to start at the right position
}
}
So finally, my attempt at latency compensation:
[RequireComponent(typeof(Projectile))]
public class NetworkedProjectile : NetworkBehaviour {
public Projectile m_DummyProjectilePrefab;
private Projectile m_Projectile;
private void Awake() {
m_Projectile = GetComponent<Projectile>();
}
public override void OnStartClient() {
// send a message to the ability user to destroy dummy projectiles, this works fine
}
public override void OnStartServer() {
// there was some latency between when the client-side player issued the cast command and this gameobject started on the server
// if we adjust the position on the server, NetworkTransform will sync the client versions, and it should hopefully overlap with the dummy
// projectile spawned on the owner client
m_Projectile.AdjustTime((float)TimeManager.TickDelta); // this doesn't feel quite right?
}
}