I’ve been trying to make a generic class for handling serverAuth, that includes client prediction and reconciliation, but when i try to test it, unity throws a huge error. I’m using the latest netcode for gameobjects version and almost the latest unity version.
error:
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP: (0,0): error - System.NullReferenceException: Object reference not set to an instance of an object.|| at
Mono.Cecil.ImportGenericContext.TypeParameter(String type, Int32 position)|| at
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportMethodSpecification(MethodReference method, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportMethod(MethodReference method, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportReference(MethodReference method, IGenericParameterProvider context)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.GetWriteMethodForParameter(TypeReference paramType, MethodReference& methodRef)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomAttribute rpcAttribute, UInt32 rpcMethodId)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)|| at
System.Collections.Generic.List`1.ForEach(Action`1 action)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.Process(ICompiledAssembly compiledAssembly) at
Mono.Cecil.ImportGenericContext.TypeParameter(String type, Int32 position)|| at
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportTypeSpecification(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportType(TypeReference type, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportMethodSpecification(MethodReference method, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportMethod(MethodReference method, ImportGenericContext context)|| at
Mono.Cecil.DefaultMetadataImporter.ImportReference(MethodReference method, IGenericParameterProvider context)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.GetWriteMethodForParameter(TypeReference paramType, MethodReference& methodRef)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomAttribute rpcAttribute, UInt32 rpcMethodId)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.ProcessNetworkBehaviour(TypeDefinition typeDefinition, String[] assemblyDefines)|| at
System.Collections.Generic.List`1.ForEach(Action`1 action)|| at
Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP.Process(ICompiledAssembly compiledAssembly)
Both scripts have a lot of commented code because i’ve tried a lot of things
ServerAuthHandler:
using System;
using System.Collections.Generic;
using Unity.Netcode;
using Unity.VisualScripting;
using UnityEngine;
/*
///<summary>
///A function to be called when an input is polled to be sent to the server.
///</summary>
///<returns>
///A class implementing IInputData, containing the input information to be sent to the server.
///</returns>
public delegate TInputData GetInputDataDelegate<TInputData>() where TInputData : IInputData;
///<summary>
///The function needs to process the input and apply it to the scene, while also returning the new state.
///</summary>
///<returns>
///A class implementing IStateData, containing the new state after the changes.
///</returns>
public delegate TStateData ProcessInputDelegate<TInputData, TStateData>(TInputData input) where TInputData : IInputData where TStateData : IStateData;
///<summary>
///The function needs to calculate the error between the two states and determine if a reconciliation is necessary.
///</summary>
///<returns>
///A bool that is true if a reconciliation is needed, and false if it isn't.
///</returns>
public delegate bool CalculateErrorDelegate<TStateData>(TStateData serverState, TStateData clientState) where TStateData : IStateData;
///<summary>
///The function needs to load the state into the scene.
///</summary>
public delegate void LoadStateDelegate<TStateData>(TStateData state) where TStateData : IStateData;
*/
/// <summary>
/// A class for handling serverauth, it includes client prediction and reconciliation
/// </summary>
/// <typeparam name="InputData">A struct that inherits INetworkSerializable and contains all the information needed to determine the next state</typeparam>
/// <typeparam name="StateData">A struct that inherits INetworkSerializable and contains all the information about the current state</typeparam>
public abstract class ServerAuthHandler<InputData, StateData> : NetworkBehaviour where InputData : struct, INetworkSerializable where StateData : struct, INetworkSerializable
{
float timer;
int currentTick;
public float timePerTick;
public bool logReconciliationWarning;
const float tickRate = 60f;
const int bufferSize = 1024;
/*
public GetInputDataDelegate<IInputData> FGetInputData;
public ProcessInputDelegate<IInputData, IStateData> FProcessInput;
public CalculateErrorDelegate<IStateData> FCalculateError;
public LoadStateDelegate<IStateData> FLoadState;
*/
//Client
InputPayload[] inputBuffer;
StatePayload[] cStateBuffer;
StatePayload lastServerState;
StatePayload lastProcessedState;
//Server
Queue<InputPayload> inputQueue;
StatePayload[] sStateBuffer;
/*
public static ServerAuthHandler CreateInstance<TInputData, TStateData>(
GetInputDataDelegate<TInputData> GetInputData,
ProcessInputDelegate<TInputData, TStateData> GetStateData,
CalculateErrorDelegate<TStateData> CalculateError,
LoadStateDelegate<TStateData> LoadState,
bool isServer)
where TInputData : IInputData
where TStateData : IStateData
{
if (GetInputData == null || GetStateData == null || CalculateError == null || LoadState == null)
{
Debug.LogError("One or more delegates are null!");
return null;
}
ServerAuthHandler instance = MultiplayerManager.instance.serverAuthHandlers.AddComponent<ServerAuthHandler>();
instance.FGetInputData = GetInputData as GetInputDataDelegate<IInputData>;
instance.FProcessInput = GetStateData as ProcessInputDelegate<IInputData, IStateData>;
instance.FCalculateError = CalculateError as CalculateErrorDelegate<IStateData>;
instance.FLoadState = LoadState as LoadStateDelegate<IStateData>;
instance.isServer = isServer;
return instance;
}
*/
// Start is called once before the first execution of Update after the MonoBehaviour is created
public void Start()
{
timePerTick = 1 / tickRate;
inputBuffer = new InputPayload[bufferSize];
cStateBuffer = new StatePayload[bufferSize];
inputQueue = new Queue<InputPayload>();
sStateBuffer = new StatePayload[bufferSize];
}
// Update is called once per frame
public void Update()
{
timer += Time.deltaTime;
while (timer >= timePerTick)
{
if (MultiplayerManager.isServer) SHandleTick(); else CHandleTick();
timer -= timePerTick;
currentTick++;
}
}
void CHandleTick()
{
//Reconciliation
if (!lastServerState.Equals(default(StatePayload)) && (lastProcessedState.Equals(default(StatePayload)) || !lastServerState.Equals(lastProcessedState)))
{
//ARREGLAR !lastServerState.Equals(lastProcessedState) SIEMPRE == TRUE
//Debug.Log(!lastServerState.Equals(default(StatePayload)) + "---" + lastProcessedState.Equals(default(StatePayload)) + "---" + !lastServerState.Equals(lastProcessedState));
HandleServerReconciliation();
}
//Prediction and sending
int bufferIndex = currentTick % bufferSize;
InputData input = GetInputData();
InputPayload payload = new()
{
tick = currentTick,
data = input
};
inputBuffer[bufferIndex] = payload;
cStateBuffer[bufferIndex] = GetStatePayload(payload);
OnClientInputServerRPC(payload);
}
[ServerRpc(RequireOwnership = false)]
void OnClientInputServerRPC(InputPayload payload)
{
inputQueue.Enqueue(payload);
}
void HandleServerReconciliation()
{
lastProcessedState = lastServerState;
int serverStateBufferIndex = lastServerState.tick % bufferSize;
if (CalculateError(lastServerState.data, cStateBuffer[serverStateBufferIndex].data))
{
if (logReconciliationWarning) Debug.LogWarning("Reconciliating");
LoadState(lastServerState.data);
cStateBuffer[serverStateBufferIndex] = lastServerState;
int tickToProcess = lastServerState.tick + 1;
while (tickToProcess < currentTick)
{
cStateBuffer[tickToProcess % bufferSize] = GetStatePayload(inputBuffer[tickToProcess % bufferSize]);
tickToProcess++;
}
}
}
void SHandleTick()
{
int bufferIndex = -1;
while (inputQueue.Count > 0)
{
InputPayload payload = inputQueue.Dequeue();
bufferIndex = payload.tick % bufferSize;
sStateBuffer[bufferIndex] = GetStatePayload(payload);
}
if (bufferIndex != -1)
{
OnServerStateClientRPC(sStateBuffer[bufferIndex]);
}
}
[ClientRpc]
void OnServerStateClientRPC(StatePayload payload)
{
lastServerState = payload;
}
StatePayload GetStatePayload(InputPayload inputPayload)
{
StatePayload statePayload = new()
{
tick = inputPayload.tick,
data = ProcessInput(inputPayload.data)
};
return statePayload;
}
[Serializable]
public struct InputPayload : INetworkSerializable
{
public int tick;
public InputData data;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref tick);
serializer.SerializeValue(ref data);
}
}
[Serializable]
public struct StatePayload : INetworkSerializable
{
public int tick;
public StateData data;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref tick);
serializer.SerializeValue(ref data);
}
}
public abstract bool CalculateError(StateData serverState, StateData clientState);
public abstract void LoadState(StateData state);
public abstract StateData ProcessInput(InputData input);
public abstract InputData GetInputData();
}
/*public struct InputData : INetworkSerializable
{
public Vector2 movement;
public bool jump;
public bool sprint;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref movement);
serializer.SerializeValue(ref jump);
serializer.SerializeValue(ref sprint);
}
}
public struct StateData : INetworkSerializable
{
public Vector3 position;
public Vector3 velocity;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref position);
serializer.SerializeValue(ref velocity);
}
}*/
///<summary>
///The class needs to be serializable, which means having all variables be public and overriding the NetworkSerialize function to provide serialization for each variable.
///</summary>
//public class IInputData : INetworkSerializable {public virtual void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {}}
///<summary>
///The class needs to be serializable, which means having all variables be public and overriding the NetworkSerialize function to provide serialization for each variable.
///</summary>
//public class IStateData : INetworkSerializable {public virtual void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter {}}
PlayerController:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;
using System.IO;
using Unity.Netcode;
public class PlayerController : ServerAuthHandler<InputData, StateData>
{
public float sensitivity = 20f;
public Transform cameraTransform;
public int speed;
public float jumpForce;
public float maxPosError;
public float maxVelError;
new void Start()
{
base.Start();
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
if (!IsOwner) Destroy(cameraTransform.gameObject);
/*
serverAuthHandler = ServerAuthHandler.CreateInstance<InputData, StateData>(GetInputData, GetStateData, CalculateError, LoadStateData, MultiplayerManager.isServer);
serverAuthHandler.FGetInputData = GetInputData;
serverAuthHandler.FProcessInput = GetStateData;
serverAuthHandler.FCalculateError = CalculateError;
serverAuthHandler.FLoadState = LoadStateData;
serverAuthHandler.isServer = MultiplayerManager.isServer;
*/
}
new void Update()
{
base.Update();
if (!IsOwner) return;
MoveCamera();
}
void MoveCamera()
{
Vector2 deltaMouse;
deltaMouse = new Vector2(Input.GetAxis("Mouse X"), -Input.GetAxis("Mouse Y"));
float treatedCameraX;
if (cameraTransform.eulerAngles.x < 180)
{
treatedCameraX = cameraTransform.eulerAngles.x;
}
else
{
treatedCameraX = -180 + (cameraTransform.eulerAngles.x -180);
}
transform.Rotate(deltaMouse.x * sensitivity * Time.deltaTime * Vector3.up);
cameraTransform.eulerAngles = new Vector3(Mathf.Clamp(treatedCameraX + deltaMouse.y * sensitivity * Time.deltaTime, -90, 90), cameraTransform.eulerAngles.y, cameraTransform.eulerAngles.z);
}
public override InputData GetInputData()
{
Vector2 movement = Vector2.zero;
if (Input.GetKey(KeyCode.W))
{
movement += new Vector2(transform.forward.x, transform.forward.z);
}
if (Input.GetKey(KeyCode.A))
{
movement -= new Vector2(transform.right.x, transform.right.z);
}
if (Input.GetKey(KeyCode.S))
{
movement -= new Vector2(transform.forward.x, transform.forward.z);
}
if (Input.GetKey(KeyCode.D))
{
movement += new Vector2(transform.right.x, transform.right.z);
}
InputData pMovement = new()
{
jump = Input.GetKeyDown(KeyCode.Space),
sprint = Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.W),
movement = movement
};
return pMovement;
}
public override StateData ProcessInput(InputData inputData)
{
if (inputData.jump && Physics.Raycast(transform.position, Vector3.down, 1f)) GetComponent<Rigidbody>().AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
transform.Translate((inputData.sprint ? 1f : 1.5f) * base.timePerTick * speed * new Vector3(inputData.movement.x, 0, inputData.movement.y).normalized, Space.World);
StateData pPos = new()
{
position = transform.position,
velocity = GetComponent<Rigidbody>().linearVelocity
};
return pPos;
}
public override bool CalculateError(StateData serverState, StateData clientState)
{
return Vector3.Distance(serverState.position, clientState.position) > maxPosError || Vector3.Distance(serverState.velocity, clientState.velocity) > maxVelError;
}
public override void LoadState(StateData stateData)
{
transform.position = stateData.position;
GetComponent<Rigidbody>().linearVelocity = stateData.velocity;
}
}
public struct InputData : INetworkSerializable
{
public Vector2 movement;
public bool jump;
public bool sprint;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref movement);
serializer.SerializeValue(ref jump);
serializer.SerializeValue(ref sprint);
}
}
public struct StateData : INetworkSerializable
{
public Vector3 position;
public Vector3 velocity;
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref position);
serializer.SerializeValue(ref velocity);
}
}
InputData and StateData are structs that inherit from INetworkSerializable
Edit:
I tried replacing TInputData and TStateData with InputData and StateData and it worked perfectly, so the issue is only with generic types.
TInputData and TStateData are also used inside 2 structs, InputPayload and StatePayload, and those are sent back and forth between server and client through RPCs.
There is also another error, but i thought it was a result of the first one:
Assembly 'Library/ScriptAssemblies/Assembly-CSharp-Editor.dll' will not be loaded due to errors:
Unable to resolve reference 'Assembly-CSharp'. Is the assembly missing or incompatible with the current platform?
Reference validation can be disabled in the Plugin Inspector.