Hey Filip cheers for getting back to me.
And it is not a problem, i have put a pin in this for now. But if you want to take a look, ill upload my entire project, and not just the assets, package, and project folders. I have tidied up my code as well for the dynamic and kinematic test. Iāll post everything below if you want to have a crack at it.
My samples are under :
\Assets\Samples\Dynamic and \Assets\Samples\Kinematic
Dynamic character code -
using System.Collections.Generic;
using System.Linq;
using Coherence.Toolkit;
using Coherence.Toolkit.Bindings.TransformBindings;
using Coherence.Toolkit.Bindings.ValueBindings;
using Unity.Mathematics;
using UnityEngine;
namespace Samples.CSP.BasicDynamicPlayer
{
public class SimulationState
{
public long SimulationFrame;
public Vector3 Position;
public Quaternion Rotation;
public Vector3 Velocity;
public Vector3 AngularVelocity;
public bool NaiveTestForReconcile(SimulationState other)
{
return math.distance(Position, other.Position) > 0.1f;
}
public void ApplyTo(Rigidbody rb)
{
rb.position = Position;
rb.rotation = Rotation;
rb.transform.position = Position;
rb.transform.rotation = Rotation;
rb.velocity = Velocity;
rb.angularVelocity = AngularVelocity;
}
}
public class InputData
{
public float Horizontal;
public float Vertical;
private const float MoveThreshold = 0.01f;
public bool IsMoving => math.abs(Horizontal) >= MoveThreshold || math.abs(Vertical) >= MoveThreshold;
public void CopyTo(ref CoherenceInput coInput)
{
coInput.SetAxis("Horizontal", Horizontal);
coInput.SetAxis("Vertical", Vertical);
}
public void CopyFrom(CoherenceInput coInput)
{
Horizontal = coInput.GetAxis("Horizontal");
Vertical = coInput.GetAxis("Vertical");
}
}
public class BasicDynamicPlayer : MonoBehaviour
{
private const int STATE_CACHE_SIZE = 1024;
private SimulationState[] simulationHistory = new SimulationState[STATE_CACHE_SIZE];
private InputData[] inputHistory = new InputData[STATE_CACHE_SIZE];
private Rigidbody cachedRigidbody;
private CoherenceInput coInput;
private CoherenceSync coSync;
private InputData cachedMovement;
public Vector3 syncedTestPosition;
public Vector3 SyncedTestVelocity;
public Vector3 SyncedTestAngularVelocity;
public Quaternion SyncedTestRotation;
public float Speed = 10f;
[OnValueSynced(nameof(Reconcile))]
public long InputSimulationFrame;
public long LastCorrectFrame;
private void Awake()
{
cachedRigidbody = GetComponent<Rigidbody>();
coSync = GetComponent<CoherenceSync>();
coInput = GetComponent<CoherenceInput>();
if (coSync == null && coInput == null)
{
Debug.LogError("Missing Sync and/or Input components");
gameObject.SetActive(false);
return;
}
// set up bindings for syncing position, rotation, velocity, and angular velocity from the simulator
var positionBinding = coSync.Bindings.First(b => b is PositionBinding) as PositionBinding;
positionBinding.OnNetworkSampleReceived += (sample,_, _) => syncedTestPosition = (Vector3)sample;
var velocityBinding = coSync.Bindings.First(b => b is Vector3Binding) as Vector3Binding;
velocityBinding.OnNetworkSampleReceived += (sample, _, _) => SyncedTestVelocity = (Vector3)sample;
var angularVelocityBinding = coSync.Bindings.First(b => b is Vector3Binding) as Vector3Binding;
angularVelocityBinding.OnNetworkSampleReceived += (sample, _, _) => SyncedTestAngularVelocity = (Vector3)sample;
var rotationBinding = coSync.Bindings.First(b => b is RotationBinding) as RotationBinding;
rotationBinding.OnNetworkSampleReceived += (sample, _, _) => SyncedTestRotation = (Quaternion)sample;
}
private void Update()
{
if (coSync.HasInputAuthority)
{
cachedMovement = GetMoveData();
cachedMovement.CopyTo(ref coInput);
}
}
private void FixedUpdate()
{
InputData movement = cachedMovement == null ? new InputData() : cachedMovement;
long simulationFrame = coInput.CurrentSimulationFrame;
if (coSync.HasStateAuthority || coSync.HasInputAuthority)
{
if (!coSync.HasInputAuthority)
{
movement.CopyFrom(coInput);
InputSimulationFrame = simulationFrame;
}
MovePlayer(cachedRigidbody, movement);
}
if (coSync.HasInputAuthority)
{
CacheSimulationState(CurrentSimulationState(simulationFrame), simulationFrame);
CacheInputState(movement, simulationFrame);
}
}
private InputData GetMoveData()
{
return new InputData()
{
Horizontal = Input.GetKey(KeyCode.A) ? -1 : Input.GetKey(KeyCode.D) ? 1 : 0,
Vertical = Input.GetKey(KeyCode.S) ? -1 : Input.GetKey(KeyCode.W) ? 1 : 0
};
}
private void MovePlayer(Rigidbody rb, InputData md)
{
Vector3 ballDirection = new Vector3(md.Vertical, 0, -md.Horizontal);
rb.AddTorque(ballDirection * Speed, ForceMode.VelocityChange);
}
private void CacheSimulationState(SimulationState state, long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
simulationHistory[cacheIndex] = state;
}
private void CacheInputState(InputData state, long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
inputHistory[cacheIndex] = state;
}
public SimulationState CurrentSimulationState(long inSimulationFrame)
{
return new SimulationState
{
Position = transform.position,
Rotation = transform.rotation,
Velocity = cachedRigidbody.velocity,
AngularVelocity = cachedRigidbody.angularVelocity,
SimulationFrame = inSimulationFrame
};
}
public SimulationState CurrentSimulationState()
{
return new SimulationState
{
Position = transform.position,
Rotation = transform.rotation,
Velocity = cachedRigidbody.velocity,
AngularVelocity = cachedRigidbody.angularVelocity,
SimulationFrame = 0
};
}
private SimulationState GetSimulationState(long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
return simulationHistory[cacheIndex];
}
private InputData GetInputState(long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
return inputHistory[cacheIndex];
}
private void Reconcile(long oldInputSimulationFrame, long newInputSimulationFrame)
{
InputSimulationFrame = newInputSimulationFrame;
InputData cachedInputData = GetInputState(InputSimulationFrame);
SimulationState cachedStateData = GetSimulationState(InputSimulationFrame);
if (cachedInputData == null || cachedStateData == null)
{
transform.position = syncedTestPosition;
LastCorrectFrame = InputSimulationFrame;
return;
}
SimulationState serverState = new SimulationState()
{
Position = syncedTestPosition,
Rotation = SyncedTestRotation,
Velocity = SyncedTestVelocity,
AngularVelocity = SyncedTestAngularVelocity,
SimulationFrame = InputSimulationFrame
};
if (cachedStateData.NaiveTestForReconcile(serverState))
{
transform.position = serverState.Position;
long rewindFrame = InputSimulationFrame;
long simulationFrame = coInput.CurrentSimulationFrame;
while (rewindFrame <= simulationFrame)
{
// Determine the cache index
long rewindCacheIndex = rewindFrame % STATE_CACHE_SIZE;
// Obtain the cached input and simulation states.
InputData rewindCachedInputState = GetInputState(rewindCacheIndex);
SimulationState rewindCachedSimulationState = GetSimulationState(rewindCacheIndex);
// If there's no state to simulate, for whatever reason,
// increment the rewindFrame and continue.
if (rewindCachedInputState == null || rewindCachedSimulationState == null)
{
++rewindFrame;
continue;
}
// Process the cached inputs.
MovePlayer(cachedRigidbody, rewindCachedInputState);
Physics.Simulate(Time.fixedDeltaTime);
// Replace the simulationStateCache index with the new value.
SimulationState rewoundSimulationState = CurrentSimulationState();
rewoundSimulationState.SimulationFrame = rewindFrame;
simulationHistory[rewindCacheIndex] = rewoundSimulationState;
// Increase the amount of frames that we've rewound.
++rewindFrame;
}
}
LastCorrectFrame = InputSimulationFrame;
}
}
}
Kinematic character code -
using Coherence.Toolkit;
using Coherence.Toolkit.Bindings.TransformBindings;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
namespace Samples.CSP.BasicKinematicPlayer
{
public class SimulationState
{
public long SimulationFrame;
public Vector3 Position;
public void ApplyTo(Transform transform)
{
transform.position = Position;
}
public bool NaiveTestForReconcile(SimulationState other)
{
return math.distance(Position, other.Position) > 0.01f;
}
}
public class InputData
{
public float Horizontal;
public float Vertical;
private const float MoveThreshold = 0.01f;
public bool IsMoving => math.abs(Horizontal) >= MoveThreshold || math.abs(Vertical) >= MoveThreshold;
public void CopyTo(ref CoherenceInput coInput)
{
coInput.SetAxis("Horizontal", Horizontal);
coInput.SetAxis("Vertical", Vertical);
}
public void CopyFrom(CoherenceInput coInput)
{
const int numInputsToBuffer = 0;
Horizontal = coInput.GetAxis("Horizontal", coInput.CurrentSimulationFrame - numInputsToBuffer);
Vertical = coInput.GetAxis("Vertical", coInput.CurrentSimulationFrame - numInputsToBuffer);
}
}
public class BasicKinematicPlayer : MonoBehaviour
{
private const int STATE_CACHE_SIZE = 1024;
private SimulationState[] simulationHistory = new SimulationState[STATE_CACHE_SIZE];
private InputData[] inputHistory = new InputData[STATE_CACHE_SIZE];
private CoherenceInput coInput;
private CoherenceSync coSync;
private InputData cachedMovement;
[OnValueSynced(nameof(Reconcile))]
public long InputSimulationFrame;
public long LastCorrectFrame;
public float Speed = 1f;
public Vector3 syncedTestPosition;
void Awake()
{
coSync = GetComponent<CoherenceSync>();
coInput = GetComponent<CoherenceInput>();
var positionBinding = coSync.Bindings.First(b => b is PositionBinding) as PositionBinding;
positionBinding.OnNetworkSampleReceived += (sample, _, _) => syncedTestPosition = (Vector3)sample;
}
private void Update()
{
if (coSync.HasInputAuthority)
{
cachedMovement = GetMoveData();
cachedMovement.CopyTo(ref coInput);
}
}
private InputData GetMoveData()
{
return new InputData()
{
Horizontal = Input.GetKey(KeyCode.A) ? -1 : Input.GetKey(KeyCode.D) ? 1 : 0,
Vertical = Input.GetKey(KeyCode.S) ? -1 : Input.GetKey(KeyCode.W) ? 1 : 0
};
}
private void FixedUpdate()
{
InputData movement = cachedMovement == null ? new InputData() : cachedMovement;
long simulationFrame = coInput.CurrentSimulationFrame;
if (coSync.HasStateAuthority || coSync.HasInputAuthority)
{
if (!coSync.HasInputAuthority)
{
movement.CopyFrom(coInput);
InputSimulationFrame = simulationFrame;
}
MovePlayer(movement);
}
if (coSync.HasInputAuthority)
{
CacheSimulationState(CurrentSimulationState(simulationFrame), simulationFrame);
CacheInputState(movement, simulationFrame);
}
}
public SimulationState CurrentSimulationState(long inSimulationFrame)
{
return new SimulationState
{
Position = transform.position,
SimulationFrame = inSimulationFrame
};
}
public SimulationState CurrentSimulationState()
{
return new SimulationState
{
Position = transform.position,
SimulationFrame = 0
};
}
private void CacheSimulationState(SimulationState state, long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
simulationHistory[cacheIndex] = state;
}
private void CacheInputState(InputData state, long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
inputHistory[cacheIndex] = state;
}
private SimulationState GetSimulationState(long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
return simulationHistory[cacheIndex];
}
private InputData GetInputState(long simulationFrame)
{
long cacheIndex = simulationFrame % STATE_CACHE_SIZE;
return inputHistory[cacheIndex];
}
private void MovePlayer(InputData md)
{
Vector3 movement = new Vector3(md.Horizontal, 0, md.Vertical);
transform.position += movement * Speed;
}
private void Reconcile(long oldInputSimulationFrame, long newInputSimulationFrame)
{
InputSimulationFrame = newInputSimulationFrame;
InputData cachedInputData = GetInputState(InputSimulationFrame);
SimulationState cachedStateData = GetSimulationState(InputSimulationFrame);
if (cachedInputData == null || cachedStateData == null)
{
transform.position = syncedTestPosition;
LastCorrectFrame = InputSimulationFrame;
return;
}
SimulationState serverState = new SimulationState
{
Position = syncedTestPosition,
SimulationFrame = InputSimulationFrame
};
if (cachedStateData.NaiveTestForReconcile(serverState))
{
transform.position = serverState.Position;
long rewindFrame = InputSimulationFrame;
long simulationFrame = coInput.CurrentSimulationFrame;
while (rewindFrame <= simulationFrame)
{
// Determine the cache index
long rewindCacheIndex = rewindFrame % STATE_CACHE_SIZE;
// Obtain the cached input and simulation states.
InputData rewindCachedInputState = GetInputState(rewindCacheIndex);
SimulationState rewindCachedSimulationState = GetSimulationState(rewindCacheIndex);
// If there's no state to simulate, for whatever reason,
// increment the rewindFrame and continue.
if (rewindCachedInputState == null || rewindCachedSimulationState == null)
{
++rewindFrame;
continue;
}
// Process the cached inputs.
MovePlayer(rewindCachedInputState);
// Replace the simulationStateCache index with the new value.
SimulationState rewoundSimulationState = CurrentSimulationState();
rewoundSimulationState.SimulationFrame = rewindFrame;
simulationHistory[rewindCacheIndex] = rewoundSimulationState;
// Increase the amount of frames that we've rewound.
++rewindFrame;
}
}
LastCorrectFrame = InputSimulationFrame;
}
}
}