232 lines
6.5 KiB
C#
232 lines
6.5 KiB
C#
using UnityEngine;
|
|
using Mirror;
|
|
using System.Collections.Generic;
|
|
using System.Collections;
|
|
using CustomExtensions;
|
|
|
|
public class Player : NetworkBehaviour
|
|
{
|
|
|
|
private float timer;
|
|
private int currentTick;
|
|
private float minTimeBetweenTicks;
|
|
private const float SERVER_TICK_RATE = 30f;
|
|
private const int BUFFER_SIZE = 1024;
|
|
|
|
// Client specific
|
|
private StatePayload[] client_stateBuffer;
|
|
private InputPayload[] inputBuffer;
|
|
private StatePayload latestServerState;
|
|
private StatePayload lastProcessedState;
|
|
private float horizontalInput;
|
|
private float verticalInput;
|
|
|
|
private StatePayload[] stateBuffer;
|
|
private Queue<InputPayload> inputQueue;
|
|
|
|
void Start()
|
|
{
|
|
minTimeBetweenTicks = 1f / SERVER_TICK_RATE;
|
|
|
|
stateBuffer = new StatePayload[BUFFER_SIZE];
|
|
inputBuffer = new InputPayload[BUFFER_SIZE];
|
|
inputQueue = new Queue<InputPayload>();
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
horizontalInput = Input.GetAxis("Horizontal");
|
|
verticalInput = Input.GetAxis("Vertical");
|
|
timer += Time.deltaTime;
|
|
|
|
while (timer >= minTimeBetweenTicks)
|
|
{
|
|
timer -= minTimeBetweenTicks;
|
|
if(isServer){
|
|
HandleTick();
|
|
}else if(isLocalPlayer){
|
|
HandleTickClient();
|
|
}
|
|
currentTick++;
|
|
}
|
|
}
|
|
public void OnClientInput(InputPayload inputPayload){
|
|
if(isServer){
|
|
m_OnClientInput(inputPayload);
|
|
}else{
|
|
CmdOnClientInput(inputPayload);
|
|
}
|
|
}
|
|
|
|
[Command]
|
|
public void CmdOnClientInput(InputPayload inputPayload){
|
|
m_OnClientInput(inputPayload);
|
|
}
|
|
|
|
public void m_OnClientInput(InputPayload inputPayload)
|
|
{
|
|
inputQueue.Enqueue(inputPayload);
|
|
}
|
|
|
|
IEnumerator SendToClient(StatePayload statePayload)
|
|
{
|
|
yield return new WaitForSeconds(0.02f);
|
|
|
|
OnServerMovementState(statePayload);
|
|
}
|
|
|
|
void HandleTick()
|
|
{
|
|
// Process the input queue
|
|
int bufferIndex = -1;
|
|
while(inputQueue.Count > 0)
|
|
{
|
|
InputPayload inputPayload = inputQueue.Dequeue();
|
|
|
|
bufferIndex = inputPayload.tick % BUFFER_SIZE;
|
|
|
|
StatePayload statePayload = ProcessMovement(inputPayload);
|
|
stateBuffer[bufferIndex] = statePayload;
|
|
}
|
|
|
|
if (bufferIndex != -1)
|
|
{
|
|
StartCoroutine(SendToClient(stateBuffer[bufferIndex]));
|
|
}
|
|
}
|
|
|
|
void HandleTickClient()
|
|
{
|
|
if (!latestServerState.Equals(default(StatePayload)) &&
|
|
(lastProcessedState.Equals(default(StatePayload)) ||
|
|
!latestServerState.Equals(lastProcessedState)))
|
|
{
|
|
HandleServerReconciliation();
|
|
}
|
|
|
|
int bufferIndex = currentTick % BUFFER_SIZE;
|
|
|
|
// Add payload to inputBuffer
|
|
InputPayload inputPayload = new InputPayload();
|
|
inputPayload.tick = currentTick;
|
|
inputPayload.inputVector = new Vector3(horizontalInput, 0, verticalInput);
|
|
inputBuffer[bufferIndex] = inputPayload;
|
|
|
|
// Add payload to stateBuffer
|
|
stateBuffer[bufferIndex] = ProcessMovement(inputPayload);
|
|
|
|
// Send input to server
|
|
StartCoroutine(SendToServer(inputPayload));
|
|
}
|
|
|
|
void HandleServerReconciliation()
|
|
{
|
|
lastProcessedState = latestServerState;
|
|
|
|
int serverStateBufferIndex = latestServerState.tick % BUFFER_SIZE;
|
|
float positionError = Vector3.Distance(latestServerState.position, stateBuffer[serverStateBufferIndex].position);
|
|
float rotationError = CustomExtensions.QuaternionExtensions.Difference(latestServerState.rotation, stateBuffer[serverStateBufferIndex].rotation);
|
|
|
|
if (positionError > 0.001f || rotationError > 0.001f)
|
|
{
|
|
Debug.Log($"We have to reconcile bro, Errors\npos:{positionError}, rot:{rotationError}");
|
|
// Rewind & Replay
|
|
transform.position = latestServerState.position;
|
|
transform.rotation = latestServerState.rotation;
|
|
|
|
// Update buffer at index of latest server state
|
|
stateBuffer[serverStateBufferIndex] = latestServerState;
|
|
|
|
// Now re-simulate the rest of the ticks up to the current tick on the client
|
|
int tickToProcess = latestServerState.tick + 1;
|
|
|
|
while (tickToProcess < currentTick)
|
|
{
|
|
int bufferIndex = tickToProcess % BUFFER_SIZE;
|
|
|
|
// Process new movement with reconciled state
|
|
StatePayload statePayload = ProcessMovement(inputBuffer[bufferIndex]);
|
|
|
|
// Update buffer with recalculated state
|
|
stateBuffer[bufferIndex] = statePayload;
|
|
|
|
tickToProcess++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void m_OnServerMovementState(StatePayload serverState)
|
|
{
|
|
latestServerState = serverState;
|
|
}
|
|
|
|
public void OnServerMovementState(StatePayload serverState){
|
|
if(isServer){
|
|
RpcOnServerMovementState(serverState);
|
|
}else{
|
|
m_OnServerMovementState(serverState);
|
|
// CmdOnServerMovementState(serverState);
|
|
}
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void RpcOnServerMovementState(StatePayload serverState){
|
|
m_OnServerMovementState(serverState);
|
|
Debug.Log(serverState.position + ":" + transform.position);
|
|
|
|
}
|
|
|
|
IEnumerator SendToServer(InputPayload inputPayload)
|
|
{
|
|
yield return new WaitForSeconds(0.02f);
|
|
|
|
OnClientInput(inputPayload);
|
|
}
|
|
|
|
StatePayload ProcessMovement(InputPayload input)
|
|
{
|
|
// Should always be in sync with same function on Client
|
|
transform.position += input.inputVector * 5f * minTimeBetweenTicks;
|
|
transform.Rotate(input.inputVector);
|
|
|
|
return new StatePayload()
|
|
{
|
|
tick = input.tick,
|
|
position = transform.position,
|
|
rotation = transform.rotation
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
public struct InputPayload
|
|
{
|
|
public int tick;
|
|
public Vector3 inputVector;
|
|
|
|
public override string ToString()
|
|
{
|
|
return JsonUtility.ToJson(this);
|
|
}
|
|
|
|
public static InputPayload Parse(string json){
|
|
return JsonUtility.FromJson<InputPayload>(json);
|
|
}
|
|
}
|
|
|
|
public struct StatePayload
|
|
{
|
|
public int tick;
|
|
public Vector3 position;
|
|
public Quaternion rotation;
|
|
|
|
public override string ToString()
|
|
{
|
|
return JsonUtility.ToJson(this);
|
|
}
|
|
|
|
public static StatePayload Parse(string json){
|
|
return JsonUtility.FromJson<StatePayload>(json);
|
|
}
|
|
}
|