diff --git a/Assets/Plugins/LiteEntitySystem/ClientEntityManager.cs b/Assets/Plugins/LiteEntitySystem/ClientEntityManager.cs index 86af30b..b68a1ca 100644 --- a/Assets/Plugins/LiteEntitySystem/ClientEntityManager.cs +++ b/Assets/Plugins/LiteEntitySystem/ClientEntityManager.cs @@ -17,21 +17,11 @@ public sealed class ClientEntityManager : EntityManager /// Current interpolated server tick /// public ushort ServerTick { get; private set; } - - /// - /// Current rollback tick (valid only in Rollback state) - /// - public ushort RollBackTick { get; private set; } - - /// - /// Tick of currently executing rpc (check only in client RPC methods) - /// - public ushort CurrentRPCTick { get; internal set; } /// /// Is server->client rpc currently executing /// - public bool IsExecutingRPC { get; private set; } + public bool IsExecutingRPC { get; internal set; } /// /// Current state server tick @@ -107,7 +97,7 @@ public sealed class ClientEntityManager : EntityManager /// Preferred input and incoming states buffer length in seconds lowest bound /// Buffer automatically increases to Jitter time + PreferredBufferTimeLowest /// - public float PreferredBufferTimeLowest = 0.01f; + public float PreferredBufferTimeLowest = 0.025f; /// /// Preferred input and incoming states buffer length in seconds lowest bound @@ -122,6 +112,7 @@ public sealed class ClientEntityManager : EntityManager private const float TimeSpeedChangeFadeTime = 0.1f; private const float MaxJitter = 0.2f; + private const float MinJitter = 0.001f; /// /// Maximum stored inputs count @@ -129,40 +120,62 @@ public sealed class ClientEntityManager : EntityManager public const int InputBufferSize = 64; //predicted entities that should use rollback - private readonly AVLTree _predictedEntities = new(); + private readonly AVLTree _modifiedEntitiesToRollback = new(); private readonly AbstractNetPeer _netPeer; private readonly Queue _statesPool = new(MaxSavedStateDiff); private readonly Dictionary _receivedStates = new(); private readonly SequenceBinaryHeap _readyStates = new(MaxSavedStateDiff); - private readonly Queue<(ushort tick, EntityLogic entity)> _spawnPredictedEntities = new (); + private readonly Queue _tempLocalEntities = new (); private readonly byte[] _sendBuffer = new byte[NetConstants.MaxPacketSize]; + private readonly byte[] _zeroFieldBuffer = new byte[StateSerializer.MaxStateSize]; private readonly HashSet _changedEntities = new(); private readonly CircularBuffer _storedInputHeaders = new(InputBufferSize); private InternalEntity[] _entitiesToRemove = new InternalEntity[64]; - private readonly AVLTree _tempEntityTree = new(); + private readonly Queue _entitiesToRollback = new(); private int _entitiesToRemoveCount; private ServerSendRate _serverSendRate; private ServerStateData _stateA; private ServerStateData _stateB; - private float _lerpTime; - private double _timer; - private int _syncCallsCount; + private float _remoteInterpolationTotalTime; + private float _remoteInterpolationTimer; + private float _remoteLerpFactor; + private ushort _prevTick; private readonly IdGeneratorUShort _localIdQueue = new(MaxSyncedEntityCount, MaxEntityCount); + + private SyncCallInfo[] _syncCalls; + private int _syncCallsCount; + + private ushort _lastReceivedInputTick; + private ushort _lastReadyTick; + + //used for debug info + private ushort _rollbackStep; + private ushort _rollbackTotalSteps; + private ushort _tickBeforeRollback; + + //time manipulation + private readonly float[] _jitterSamples = new float[50]; + private int _jitterSampleIdx; + private readonly Stopwatch _jitterTimer = new(); + private float _jitterPrevTime; + private float _jitterMiddle; + private float _jitterSum; + + //local player + private NetPlayer _localPlayer; private readonly struct SyncCallInfo { private readonly InternalEntity _entity; - private readonly MethodCallDelegate _onSync; private readonly int _prevDataPos; - private readonly int _dataSize; + private readonly int _fieldId; - public SyncCallInfo(MethodCallDelegate onSync, InternalEntity entity, int prevDataPos, int dataSize) + public SyncCallInfo(InternalEntity entity, int prevDataPos, int fieldId) { - _dataSize = dataSize; - _onSync = onSync; + _fieldId = fieldId; _entity = entity; _prevDataPos = prevDataPos; } @@ -171,7 +184,9 @@ public void Execute(ServerStateData state) { try { - _onSync(_entity, new ReadOnlySpan(state.Data, _prevDataPos, _dataSize)); + ref var classData = ref _entity.ClassData; + ref var fieldInfo = ref classData.Fields[_fieldId]; + fieldInfo.OnSync(fieldInfo.GetTargetObject(_entity), new ReadOnlySpan(state.Data, _prevDataPos, fieldInfo.IntSize)); } catch (Exception e) { @@ -179,22 +194,13 @@ public void Execute(ServerStateData state) } } } - private SyncCallInfo[] _syncCalls; - - private ushort _lastReceivedInputTick; - private float _logicLerpMsec; - private ushort _lastReadyTick; - //time manipulation - private readonly float[] _jitterSamples = new float[50]; - private int _jitterSampleIdx; - private readonly Stopwatch _jitterTimer = new(); - private float _jitterPrevTime; - private float _jitterMiddle; - private float _jitterSum; - - //local player - private NetPlayer _localPlayer; + public override string GetCurrentFrameDebugInfo(DebugFrameModes modes) => + UpdateMode == UpdateMode.PredictionRollback && modes.HasFlagFast(DebugFrameModes.Rollback) + ? $"[ClientRollback({_rollbackStep}/{_rollbackTotalSteps})] Tick {_tickBeforeRollback}. RollbackTick: {_tick}. ServerTick: {ServerTick}" + : modes.HasFlagFast(DebugFrameModes.Client) && UpdateMode == UpdateMode.Normal + ? $"[Client] Tick {_tick}. ServerTick: {ServerTick}" + : string.Empty; /// /// Return client controller if exist @@ -229,13 +235,16 @@ public ClientEntityManager( _sendBuffer[1] = InternalPackets.ClientInput; for (int i = 0; i < MaxSavedStateDiff; i++) - _statesPool.Enqueue(new ServerStateData()); + _statesPool.Enqueue(new ServerStateData(this)); } public override void Reset() { base.Reset(); + _modifiedEntitiesToRollback.Clear(); _localIdQueue.Reset(); + _readyStates.Clear(); + _syncCallsCount = 0; _entitiesToRemoveCount = 0; } @@ -244,14 +253,15 @@ public override void Reset() /// /// Entity type /// Created entity or null if entities limit is reached () - internal T AddLocalEntity(EntityLogic parent, Action initMethod) where T : EntityLogic + internal unsafe T AddLocalEntity(EntityLogic parent, Action initMethod) where T : PredictableEntityLogic { if (_localIdQueue.AvailableIds == 0) { Logger.LogError("Max local entities count reached"); return null; } - + + ref var classData = ref ClassDataDict[EntityClassInfo.ClassId]; var entity = AddEntity(new EntityParams( _localIdQueue.GetNewId(), new EntityDataHeader( @@ -259,7 +269,7 @@ internal T AddLocalEntity(EntityLogic parent, Action initMethod) where T : 0, 0), this, - ClassDataDict[EntityClassInfo.ClassId].AllocateDataCache())); + classData.AllocateDataCache())); //Logger.Log($"AddPredicted, tick: {_tick}, rb: {InRollBackState}, id: {entity.Id}"); @@ -267,41 +277,39 @@ internal T AddLocalEntity(EntityLogic parent, Action initMethod) where T : entity.SetParentInternal(parent); initMethod(entity); ConstructEntity(entity); - _spawnPredictedEntities.Enqueue((_tick, entity)); - - return entity; - } + _tempLocalEntities.Enqueue(entity); - internal EntityLogic FindEntityByPredictedId(ushort tick, ushort parentId, ushort predictedId) - { - foreach (var predictedEntity in _spawnPredictedEntities) + fixed (byte* predictedData = classData.GetLastServerData(entity)) + { + for (int i = 0; i < classData.FieldsCount; i++) + { + ref var field = ref classData.Fields[i]; + var targetObject = field.GetTargetObjectAndOffset(entity, out int offset); + + if(field.Flags.HasFlagFast(SyncFlags.Interpolated)) + field.TypeProcessor.SetInterpValueFromCurrentValue(targetObject, offset); + if(field.IsPredicted) + field.TypeProcessor.WriteTo(targetObject, offset, predictedData + field.PredictedOffset); + } + } + + //init syncable rollback + for (int i = 0; i < classData.SyncableFieldsCustomRollback.Length; i++) { - if (predictedEntity.tick == tick && - predictedEntity.entity.ParentId.Id == parentId && - predictedEntity.entity.PredictedId == predictedId) - return predictedEntity.entity; + var syncableField = RefMagic.GetFieldValue(entity, classData.SyncableFieldsCustomRollback[i].Offset); + syncableField.BeforeReadRPC(); + syncableField.AfterReadRPC(); } - return null; - } - internal override void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue) - { - //currently nothing + return entity; } - protected override unsafe void OnAliveEntityAdded(InternalEntity entity) + internal PredictableEntityLogic FindEntityByPredictedId(ushort tick, ushort parentId, ushort predictedId) { - ref var classData = ref ClassDataDict[entity.ClassId]; - fixed (byte* interpDataPtr = classData.ClientInterpolatedNextData(entity), - prevDataPtr = classData.ClientInterpolatedPrevData(entity)) - { - for (int i = 0; i < classData.InterpolatedCount; i++) - { - var field = classData.Fields[i]; - field.TypeProcessor.WriteTo(entity, field.Offset, interpDataPtr + field.FixedOffset); - field.TypeProcessor.WriteTo(entity, field.Offset, prevDataPtr + field.FixedOffset); - } - } + foreach (var predictedEntity in _tempLocalEntities) + if (predictedEntity.IsEntityMatch(predictedId, parentId, tick)) + return predictedEntity; + return null; } /// Read incoming data @@ -349,17 +357,18 @@ public unsafe DeserializeResult Deserialize(ReadOnlySpan inData) _localPlayer = new NetPlayer(_netPeer, InternalPlayerId); ServerTick = _stateA.Tick; _lastReadyTick = ServerTick; + _remoteInterpolationTimer = 0f; + _remoteLerpFactor = 0f; foreach (var controller in GetEntities()) controller.ClearClientStoredInputs(); _storedInputHeaders.Clear(); _jitterTimer.Reset(); - IsExecutingRPC = true; - _stateA.ExecuteRpcs(this, 0, true); - IsExecutingRPC = false; - int readerPosition = _stateA.DataOffset; - ReadDiff(ref readerPosition); - ExecuteSyncCallsAndWriteHistory(_stateA); + _stateA.ExecuteRpcs((ushort)(_stateA.Tick - 1),RPCExecuteMode.FirstSync); + ReadDiff(); + ExecuteSyncCalls(_stateA); + foreach (var lagCompensatedEntity in LagCompensatedEntities) + ClassDataDict[lagCompensatedEntity.ClassId].WriteHistory(lagCompensatedEntity, ServerTick); Logger.Log($"[CEM] Got baseline sync. Assigned player id: {header.PlayerId}, Original: {_stateA.Size}, Tick: {header.Tick}, SendRate: {_serverSendRate}"); } @@ -391,7 +400,7 @@ public unsafe DeserializeResult Deserialize(ReadOnlySpan inData) { if (_statesPool.Count == 0) { - serverState = new ServerStateData { Tick = diffHeader.Tick }; + serverState = new ServerStateData(this) { Tick = diffHeader.Tick }; } else { @@ -415,9 +424,11 @@ public unsafe DeserializeResult Deserialize(ReadOnlySpan inData) if (_readyStates.Count == MaxSavedStateDiff) { //one state should be already preloaded - _timer = _lerpTime; + _remoteInterpolationTimer = _remoteInterpolationTotalTime; //fast-forward GoToNextState(); + + //to add space to _readyStates PreloadNextState(); } @@ -449,12 +460,12 @@ private bool PreloadNextState() //limit jitter for pause scenarios if (NetworkJitter > MaxJitter) NetworkJitter = MaxJitter; - float lowestBound = NetworkJitter + PreferredBufferTimeLowest; - float upperBound = NetworkJitter + PreferredBufferTimeHighest; + float lowestBound = NetworkJitter * 1.5f + PreferredBufferTimeLowest; + float upperBound = NetworkJitter * 1.5f + PreferredBufferTimeHighest; //tune buffer playing speed - _lerpTime = Utils.SequenceDiff(_stateB.Tick, _stateA.Tick) * DeltaTimeF; - _lerpTime *= 1 - GetSpeedMultiplier(LerpBufferTimeLength)*TimeSpeedChangeCoef; + _remoteInterpolationTotalTime = Utils.SequenceDiff(_stateB.Tick, _stateA.Tick) * DeltaTimeF; + _remoteInterpolationTotalTime *= 1 - GetSpeedMultiplier(LerpBufferTimeLength)*TimeSpeedChangeCoef; //tune game prediction and input generation speed SpeedMultiplier = GetSpeedMultiplier(_stateB.BufferedInputsCount * DeltaTimeF); @@ -468,45 +479,122 @@ float GetSpeedMultiplier(float bufferTime) => private unsafe void GoToNextState() { - //remove processed inputs - foreach (var controller in GetEntities()) - controller.RemoveClientProcessedInputs(_stateB.ProcessedTick); - while (_storedInputHeaders.Count > 0 && Utils.SequenceDiff(_stateB.ProcessedTick, _storedInputHeaders.Front().Tick) >= 0) - _storedInputHeaders.PopFront(); + _remoteInterpolationTimer -= _remoteInterpolationTotalTime; + + _tickBeforeRollback = _tick; + _rollbackStep = 0; + _rollbackTotalSteps = (ushort)_storedInputHeaders.Count; - //delete predicted - while (_spawnPredictedEntities.TryPeek(out var info)) + ushort targetTick = _tick; + var humanControllerFilter = GetEntities(); + + //================== Rollback part =========================== + _entitiesToRollback.Clear(); + foreach (var entity in _modifiedEntitiesToRollback) + { + if(!entity.IsRemoved) + _entitiesToRollback.Enqueue(entity); + } + + UpdateMode = UpdateMode.PredictionRollback; + //reset predicted entities + foreach (var entity in _entitiesToRollback) { - if (Utils.SequenceDiff(_stateB.ProcessedTick, info.tick) >= 0) + ref var classData = ref ClassDataDict[entity.ClassId]; + var rollbackFields = classData.GetRollbackFields(entity.IsLocalControlled); + entity.OnBeforeRollback(); + + fixed (byte* lastServerData = classData.GetLastServerData(entity)) { - //Logger.Log($"Delete predicted. Tick: {info.tick}, Entity: {info.entity}"); - _spawnPredictedEntities.Dequeue(); - info.entity.DestroyInternal(); - RemoveEntity(info.entity); - _localIdQueue.ReuseId(info.entity.Id); + for (int i = 0; i < rollbackFields.Length; i++) + { + ref var field = ref classData.Fields[rollbackFields[i]]; + var target = field.GetTargetObjectAndOffset(entity, out int offset); + + if (field.OnSync != null && (field.OnSyncFlags & BindOnChangeFlags.ExecuteOnRollbackReset) != 0) + { + //this is currently only place which doesn't call classData.CallOnSync + field.TypeProcessor.SetFromAndSync(target, offset, lastServerData + field.PredictedOffset, field.OnSync); + } + else + { + field.TypeProcessor.SetFrom(target, offset, lastServerData + field.PredictedOffset); + } + } } - else + for (int i = 0; i < classData.SyncableFieldsCustomRollback.Length; i++) + RefMagic.GetFieldValue(entity, classData.SyncableFieldsCustomRollback[i].Offset).OnRollback(); + entity.OnRollback(); + } + + //clear modified here to readd changes after RollbackUpdate + _modifiedEntitiesToRollback.Clear(); + + //Step a little to match "predicted" state at server processed tick + //for correct BindOnSync execution + int cmdNum = 0; + while(_storedInputHeaders.Count > 0 && Utils.SequenceDiff(_stateB.ProcessedTick, _storedInputHeaders.Front().Tick) >= 0) + { + var storedInput = _storedInputHeaders.Front(); + _storedInputHeaders.PopFront(); + _localPlayer.LoadInputInfo(storedInput.Header); + _tick = storedInput.Tick; + foreach (var controller in humanControllerFilter) + controller.ReadStoredInput(cmdNum); + cmdNum++; + _rollbackStep++; + + //simple update + foreach (var entity in _entitiesToRollback) { - break; + if (!entity.IsLocalControlled || !AliveEntities.Contains(entity)) + continue; + + //skip local entities that spawned later + if (entity.IsLocal && entity is PredictableEntityLogic el && Utils.SequenceDiff(el.CreatedAtTick, _tick) >= 0) + continue; + + entity.Update(); } } + UpdateMode = UpdateMode.Normal; + + //remove processed inputs + foreach (var controller in humanControllerFilter) + controller.RemoveClientProcessedInputs(_stateB.ProcessedTick); ushort minimalTick = _stateA.Tick; _statesPool.Enqueue(_stateA); _stateA = _stateB; _stateB = null; + ServerTick = _stateA.Tick; - //Logger.Log($"GotoState: IST: {ServerTick}, TST:{_stateA.Tick}}"); - - //================== ReadEntityStates BEGIN ================== + //Logger.Log($"GotoState: IST: {ServerTick}, TST:{_stateA.Tick}"); _changedEntities.Clear(); - ServerTick = _stateA.Tick; - IsExecutingRPC = true; - _stateA.ExecuteRpcs(this, minimalTick, false); - IsExecutingRPC = false; - int readerPosition = _stateA.DataOffset; - ReadDiff(ref readerPosition); - ExecuteSyncCallsAndWriteHistory(_stateA); + _stateA.ExecuteRpcs(minimalTick, RPCExecuteMode.OnNextState); + ReadDiff(); + ExecuteSyncCalls(_stateA); + + //delete predicted + while (_tempLocalEntities.TryPeek(out var entity)) + { + if (Utils.SequenceDiff(_stateA.ProcessedTick, entity.CreatedAtTick) >= 0) + { + //Logger.Log($"Delete predicted. Tick: {info.tick}, Entity: {info.entity}"); + _tempLocalEntities.Dequeue(); + entity.DestroyInternal(); + RemoveEntity(entity); + _localIdQueue.ReuseId(entity.Id); + } + else + { + break; + } + } + + //write lag comp history for remote rollbackable lagcomp fields + foreach (var lagCompensatedEntity in LagCompensatedEntities) + ClassDataDict[lagCompensatedEntity.ClassId].WriteHistory(lagCompensatedEntity, ServerTick); for(int i = 0; i < _entitiesToRemoveCount; i++) { @@ -514,8 +602,6 @@ private unsafe void GoToNextState() var entityToRemove = _entitiesToRemove[i]; if (_changedEntities.Contains(entityToRemove)) continue; - - _predictedEntities.Remove(entityToRemove); //Logger.Log($"[CLI] RemovingEntity: {_entitiesToRemove[i].Id}"); RemoveEntity(entityToRemove); @@ -525,105 +611,91 @@ private unsafe void GoToNextState() _entitiesToRemove[_entitiesToRemoveCount] = null; i--; } - - //================== ReadEntityStates END ==================== - - _timer -= _lerpTime; - - //================== Rollback part =========================== - //reset owned entities - foreach (var entity in _predictedEntities) - { - ref var classData = ref ClassDataDict[entity.ClassId]; - var rollbackFields = classData.GetRollbackFields(entity.IsLocalControlled); - if(rollbackFields == null || rollbackFields.Length == 0) - continue; - entity.OnBeforeRollback(); - fixed (byte* predictedData = classData.ClientPredictedData(entity)) - { - for (int i = 0; i < rollbackFields.Length; i++) - { - ref var field = ref rollbackFields[i]; - if (field.FieldType == FieldType.SyncableSyncVar) - { - var syncableField = RefMagic.RefFieldValue(entity, field.Offset); - field.TypeProcessor.SetFrom(syncableField, field.SyncableSyncVarOffset, predictedData + field.PredictedOffset); - } - else - { - field.TypeProcessor.SetFrom(entity, field.Offset, predictedData + field.PredictedOffset); - } - } - } - for (int i = 0; i < classData.SyncableFields.Length; i++) - RefMagic.RefFieldValue(entity, classData.SyncableFields[i].Offset).OnRollback(); - entity.OnRollback(); - } - //reapply input - UpdateMode = UpdateMode.PredictionRollback; - for(int cmdNum = 0; cmdNum < _storedInputHeaders.Count; cmdNum++) + UpdateMode = UpdateMode.PredictionRollback; + for(cmdNum = 0; cmdNum < _storedInputHeaders.Count; cmdNum++) { //reapply input data var storedInput = _storedInputHeaders[cmdNum]; _localPlayer.LoadInputInfo(storedInput.Header); - RollBackTick = storedInput.Tick; - foreach (var controller in GetEntities()) + _tick = storedInput.Tick; + foreach (var controller in humanControllerFilter) controller.ReadStoredInput(cmdNum); + _rollbackStep++; + //simple update - foreach (var entity in AliveEntities) + foreach (var entity in _entitiesToRollback) { - if (entity.IsLocal || !entity.IsLocalControlled) + if (!entity.IsLocalControlled || !AliveEntities.Contains(entity)) continue; + + //skip local entities that spawned later + if (entity.IsLocal && entity is PredictableEntityLogic el && Utils.SequenceDiff(el.CreatedAtTick, _tick) >= 0) + continue; + + //update interpolation data + if (cmdNum == _storedInputHeaders.Count - 1) + { + ref var classData = ref ClassDataDict[entity.ClassId]; + for(int i = 0; i < classData.InterpolatedCount; i++) + { + ref var field = ref classData.Fields[i]; + var target = field.GetTargetObjectAndOffset(entity, out int offset); + field.TypeProcessor.SetInterpValueFromCurrentValue(target, offset); + } + } + entity.Update(); } } UpdateMode = UpdateMode.Normal; + + _tick = targetTick; + _entitiesToRollback.Clear(); + } + + internal void MarkEntityChanged(InternalEntity entity) + { + if (entity.IsRemoved || IsExecutingRPC) + return; + _modifiedEntitiesToRollback.Add(entity); + } + + internal override unsafe void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue, ref T oldValue, bool skipOnSync) + { + if (entity.IsRemoved) + return; + + ref var classData = ref ClassDataDict[entity.ClassId]; + ref var fieldInfo = ref classData.Fields[fieldId]; + + if (!skipOnSync && (fieldInfo.OnSyncFlags & BindOnChangeFlags.ExecuteOnPrediction) != 0) + { + T oldValueCopy = oldValue; + fieldInfo.OnSync(fieldInfo.GetTargetObject(entity), new ReadOnlySpan(&oldValueCopy, fieldInfo.IntSize)); + } + + var rollbackFields = classData.GetRollbackFields(entity.IsLocalControlled); + if (rollbackFields != null && rollbackFields.Length > 0 && fieldInfo.IsPredicted) + _modifiedEntitiesToRollback.Add(entity); } internal override void OnEntityDestroyed(InternalEntity e) { if (!e.IsLocal) { + //because remote and server alive entities managed by EntityManager if(e.IsLocalControlled && e is EntityLogic eLogic) RemoveOwned(eLogic); Utils.AddToArrayDynamic(ref _entitiesToRemove, ref _entitiesToRemoveCount, e); } - + _modifiedEntitiesToRollback.Remove(e); base.OnEntityDestroyed(e); } - protected override unsafe void OnLogicTick() + protected override void OnLogicTick() { - if (_stateB != null) - { - ServerTick = Utils.LerpSequence(_stateA.Tick, _stateB.Tick, (float)(_timer/_lerpTime)); - - //restore actual field values - _tempEntityTree.Clear(); - foreach (var entity in AliveEntities) - { - ref var classData = ref ClassDataDict[entity.ClassId]; - if(classData.InterpolatedCount == 0) - continue; - if (entity.IsLocal || entity.IsLocalControlled) - { - //save data for interpolation before update - fixed (byte* currentDataPtr = classData.ClientInterpolatedNextData(entity)) - { - //restore previous - for (int i = 0; i < classData.InterpolatedCount; i++) - { - var field = classData.Fields[i]; - field.TypeProcessor.SetFrom(entity, field.Offset, currentDataPtr + field.FixedOffset); - } - } - _tempEntityTree.Add(entity); - } - } - } - //apply input var humanControllers = GetEntities(); if (humanControllers.Count > 0) @@ -632,7 +704,7 @@ protected override unsafe void OnLogicTick() { StateA = _stateA.Tick, StateB = RawTargetServerTick, - LerpMsec = _logicLerpMsec + LerpMsec = _remoteLerpFactor })); foreach (var controller in humanControllers) { @@ -642,93 +714,68 @@ protected override unsafe void OnLogicTick() //local only and UpdateOnClient foreach (var entity in AliveEntities) - entity.Update(); - - if (_stateB != null) { - //save interpolation data - foreach (var entity in _tempEntityTree) + //first logic tick in Update + if (_tick == _prevTick && (entity.IsLocal || entity.IsLocalControlled)) { ref var classData = ref ClassDataDict[entity.ClassId]; - fixed (byte* currentDataPtr = classData.ClientInterpolatedNextData(entity), - prevDataPtr = classData.ClientInterpolatedPrevData(entity)) + //save data for interpolation before update + for (int i = 0; i < classData.InterpolatedCount; i++) { - RefMagic.CopyBlock(prevDataPtr, currentDataPtr, (uint)classData.InterpolatedFieldsSize); - for(int i = 0; i < classData.InterpolatedCount; i++) - { - var field = classData.Fields[i]; - field.TypeProcessor.WriteTo(entity, field.Offset, currentDataPtr + field.FixedOffset); - } + ref var field = ref classData.Fields[i]; + var target = field.GetTargetObjectAndOffset(entity, out int offset); + field.TypeProcessor.SetInterpValueFromCurrentValue(target, offset); } } - //execute rpcs and spawn entities - IsExecutingRPC = true; - _stateB.ExecuteRpcs(this, _stateA.Tick, false); - IsExecutingRPC = false; - ExecuteSyncCallsAndWriteHistory(_stateB); + entity.Update(); } - - if (NetworkJitter > _jitterMiddle) - NetworkJitter -= DeltaTimeF * 0.1f; + ExecuteLocalSingletonsLateUpdate(); } /// /// Update method, call this every frame /// - public override unsafe void Update() + public override void Update() { //skip update until receive first sync and tickrate if (Tickrate == 0) return; //logic update - ushort prevTick = _tick; - + _prevTick = _tick; + ServerTick = Utils.LerpSequence(_stateA.Tick, _stateB?.Tick ?? _stateA.Tick, _remoteLerpFactor); + base.Update(); + //reduce max jitter to middle + float dt = (float)VisualDeltaTime; + if (NetworkJitter > _jitterMiddle) + { + NetworkJitter -= dt * 0.1f; + if (NetworkJitter < MinJitter) + NetworkJitter = MinJitter; + } + //send buffered input - if (_tick != prevTick) + if (_tick != _prevTick) SendBufferedInput(); - if (PreloadNextState()) + if (_stateB != null) { - _timer += VisualDeltaTime; - while(_timer >= _lerpTime) + //execute rpcs and spawn entities + _stateB.ExecuteRpcs(_stateA.Tick, RPCExecuteMode.BetweenStates); + ExecuteSyncCalls(_stateB); + + while(_remoteInterpolationTimer >= _remoteInterpolationTotalTime) { GoToNextState(); if (!PreloadNextState()) break; } - - if (_stateB != null) - { - _logicLerpMsec = (float)(_timer/_lerpTime); - _stateB.RemoteInterpolation(EntitiesDict, _logicLerpMsec); - } - } - - //local interpolation - float localLerpT = LerpFactor; - foreach (var entity in AliveEntities) - { - if (!entity.IsLocalControlled && !entity.IsLocal) - continue; - - ref var classData = ref ClassDataDict[entity.ClassId]; - fixed (byte* currentDataPtr = classData.ClientInterpolatedNextData(entity), prevDataPtr = classData.ClientInterpolatedPrevData(entity)) - { - for(int i = 0; i < classData.InterpolatedCount; i++) - { - ref var field = ref classData.Fields[i]; - field.TypeProcessor.SetInterpolation( - entity, - field.Offset, - prevDataPtr + field.FixedOffset, - currentDataPtr + field.FixedOffset, - localLerpT); - } - } + _stateB?.PreloadInterpolationForNewEntities(); + _remoteLerpFactor = _remoteInterpolationTimer / _remoteInterpolationTotalTime; + _remoteInterpolationTimer += dt; } //local only and UpdateOnClient @@ -738,6 +785,17 @@ public override unsafe void Update() } } + internal T GetInterpolatedValue(ref SyncVar syncVar, T interpValue) where T : unmanaged + { + if (IsLagCompensationEnabled && IsEntityLagCompensated(syncVar.Container)) + return syncVar.Value; + + var typeProcessor = (ValueTypeProcessor)ClassDataDict[syncVar.Container.ClassId].Fields[syncVar.FieldId].TypeProcessor; + return syncVar.Container.IsLocalControlled + ? typeProcessor.GetInterpolatedValue(interpValue, syncVar.Value, LerpFactor) + : typeProcessor.GetInterpolatedValue(syncVar.Value, interpValue, _remoteLerpFactor); + } + private unsafe void SendBufferedInput() { if (_storedInputHeaders.Count == 0) @@ -747,7 +805,7 @@ private unsafe void SendBufferedInput() *(ushort*)(sendBuffer + 2) = Tick; *(InputPacketHeader*)(sendBuffer + 4) = new InputPacketHeader { - LerpMsec = _logicLerpMsec, + LerpMsec = _remoteLerpFactor, StateA = _stateA.Tick, StateB = RawTargetServerTick }; @@ -833,7 +891,7 @@ internal void RemoveOwned(EntityLogic entity) AliveEntities.Remove(entity); } - internal unsafe void ReadNewRPC(ushort entityId, byte* rawData) + internal unsafe void ReadNewRPC(ushort entityId, byte* rawData, int byteCount) { //Logger.Log("NewRPC"); var entityDataHeader = *(EntityDataHeader*)rawData; @@ -844,38 +902,35 @@ internal unsafe void ReadNewRPC(ushort entityId, byte* rawData) } var entity = EntitiesDict[entityId]; - //Logger.Log($"[CEM] ReadBaseline Entity: {entityId} pos: {bytesRead}"); + //Logger.Log($"[CEM] New Entity: {entityId}"); //remove old entity if (entity != null && entity.Version != entityDataHeader.Version) { + //can happen when entities in remove list but new arrived //this can be only on logics (not on singletons) Logger.Log($"[CEM] Replace entity by new: {entityDataHeader.Version}. Class: {entityDataHeader.ClassId}. Id: {entityId}"); entity.DestroyInternal(); + RemoveEntity(entity); - _predictedEntities.Remove(entity); entity = null; } if (entity == null) //create new { ref var classData = ref ClassDataDict[entityDataHeader.ClassId]; entity = AddEntity(new EntityParams(entityId, entityDataHeader, this, classData.AllocateDataCache())); - - if (classData.PredictedSize > 0 || classData.SyncableFields.Length > 0) - { - _predictedEntities.Add(entity); - //Logger.Log($"Add predicted: {entity.GetType()}"); - } } + + ApplyEntityDelta(entity, rawData, StateSerializer.HeaderSize, true); + + //Logger.Log($"NewRPC bytes (client) eid:{entityId} cls:{entity.ClassId} data:{Utils.BytesToHexString(new ReadOnlySpan(rawData, byteCount))}"); } - private void ExecuteSyncCallsAndWriteHistory(ServerStateData stateData) + private void ExecuteSyncCalls(ServerStateData stateData) { ExecuteLateConstruct(); for (int i = 0; i < _syncCallsCount; i++) _syncCalls[i].Execute(stateData); _syncCallsCount = 0; - foreach (var lagCompensatedEntity in LagCompensatedEntities) - ClassDataDict[lagCompensatedEntity.ClassId].WriteHistory(lagCompensatedEntity, ServerTick); } internal unsafe void ReadConstructRPC(ushort entityId, byte* rawData, int readerPosition) @@ -888,83 +943,79 @@ internal unsafe void ReadConstructRPC(ushort entityId, byte* rawData, int reader } var entity = EntitiesDict[entityId]; + bool writeInterpolationData = !entity.IsConstructed || entity.IsRemoteControlled; ref var classData = ref entity.ClassData; Utils.ResizeOrCreate(ref _syncCalls, _syncCallsCount + classData.FieldsCount); - - fixed (byte* interpDataPtr = classData.ClientInterpolatedNextData(entity), - prevDataPtr = classData.ClientInterpolatedPrevData(entity), - predictedData = classData.ClientPredictedData(entity)) + + //set values to same as predicted entity for correct OnSync calls + if (!entity.IsConstructed && entity is PredictableEntityLogic pel) { - for (int i = 0; i < classData.FieldsCount; i++) + foreach (var localEntity in _tempLocalEntities) { - ref var field = ref classData.Fields[i]; - if (field.ReadField( - entity, - rawData + readerPosition, - predictedData, - writeInterpolationData ? interpDataPtr : null, - writeInterpolationData ? prevDataPtr : null)) + if (!pel.IsSameAsLocal(localEntity)) + continue; + + pel.IsRecreated = true; + + //Logger.Log($"Found predictable entity. LocalId was: {localEntity.Id}, server id: {entityId}"); + + for (int i = 0; i < classData.FieldsCount; i++) { - _syncCalls[_syncCallsCount++] = new SyncCallInfo(field.OnSync, entity, readerPosition, field.IntSize); + ref var field = ref classData.Fields[i]; + //skip some fields because they need to trigger onChange + if (field.OnSync == null || i == pel.InternalOwnerId.FieldId || i == pel.IsSyncEnabledFieldId) + continue; + + var target = field.GetTargetObjectAndOffset(entity, out int offset); + field.TypeProcessor.CopyFrom(target, localEntity, offset); } - - //Logger.Log($"E {entity.Id} Field updated: {field.Name}"); - readerPosition += field.IntSize; + + break; } } - //Construct and fast forward predicted entities - if (ConstructEntity(entity) == false) - return; - - if (entity is not EntityLogic entityLogic) - return; - - //Logger.Log($"ConstructedEntity: {entityId}, pid: {entityLogic.PredictedId}"); - - entityLogic.RefreshOwnerInfo(null); - - if (entityLogic.IsLocalControlled && entityLogic.IsPredicted && AliveEntities.Contains(entity)) + fixed (byte* predictedData = classData.GetLastServerData(entity)) { - UpdateMode = UpdateMode.PredictionRollback; - for(int cmdNum = Utils.SequenceDiff(ServerTick, _stateA.Tick); cmdNum < _storedInputHeaders.Count; cmdNum++) + for (int i = 0; i < classData.FieldsCount; i++) { - //reapply input data - var storedInput = _storedInputHeaders[cmdNum]; - _localPlayer.LoadInputInfo(storedInput.Header); - RollBackTick = storedInput.Tick; - foreach (var controller in GetEntities()) - controller.ReadStoredInput(cmdNum); + ref var field = ref classData.Fields[i]; + var target = field.GetTargetObjectAndOffset(entity, out int offset); + + if (writeInterpolationData && field.Flags.HasFlagFast(SyncFlags.Interpolated)) + field.TypeProcessor.SetInterpValue(target, offset, rawData + readerPosition); - if (cmdNum == _storedInputHeaders.Count - 1) + if (field.IsPredicted) + RefMagic.CopyBlock(predictedData + field.PredictedOffset, rawData + readerPosition, field.Size); + + if (field.OnSync != null && (field.OnSyncFlags & BindOnChangeFlags.ExecuteOnSync) != 0) { - for(int i = 0; i < classData.InterpolatedCount; i++) - { - fixed (byte* prevDataPtr = classData.ClientInterpolatedPrevData(entity)) - { - ref var field = ref classData.Fields[i]; - field.TypeProcessor.WriteTo(entity, field.Offset, prevDataPtr + field.FixedOffset); - } - } + if (field.TypeProcessor.SetFromAndSync(target, offset, rawData + readerPosition)) + _syncCalls[_syncCallsCount++] = new SyncCallInfo(entity, readerPosition, i); } - entity.Update(); - } - for (int i = 0; i < classData.InterpolatedCount; i++) - { - fixed (byte* currentDataPtr = classData.ClientInterpolatedNextData(entity)) + else { - ref var field = ref classData.Fields[i]; - field.TypeProcessor.WriteTo(entity, field.Offset, currentDataPtr + field.FixedOffset); + field.TypeProcessor.SetFrom(target, offset, rawData + readerPosition); } + + //Logger.Log($"E {entity.Id} Field updated: {field.Name} = {field.TypeProcessor.ToString(entity, field.Offset)}"); + readerPosition += field.IntSize; } - - UpdateMode = UpdateMode.Normal; } + + //add owned entities to rollback queue + if(!entity.IsConstructed && entity.InternalOwnerId.Value == InternalPlayerId) + _entitiesToRollback.Enqueue(entity); + + //Construct + ConstructEntity(entity); + + //Logger.Log($"ConstructedEntity: {entityId}, pid: {entityLogic.PredictedId}"); } - private unsafe void ReadDiff(ref int readerPosition) + private unsafe void ReadDiff() { + int readerPosition = _stateA.DataOffset; fixed (byte* rawData = _stateA.Data) { while (readerPosition < _stateA.DataOffset + _stateA.DataSize) @@ -982,39 +1033,76 @@ private unsafe void ReadDiff(ref int readerPosition) readerPosition = endPos; continue; } + + ApplyEntityDelta(entity, rawData, readerPosition, false); + readerPosition = endPos; + } + } + } - ref var classData = ref entity.ClassData; - bool writeInterpolationData = entity.IsRemoteControlled; - readerPosition += classData.FieldsFlagsSize; - _changedEntities.Add(entity); - Utils.ResizeOrCreate(ref _syncCalls, _syncCallsCount + classData.FieldsCount); + private unsafe void ApplyEntityDelta(InternalEntity entity, byte* rawData, int fieldsFlagsOffset, bool insideNewRPC) + { + ref var classData = ref entity.ClassData; + _changedEntities.Add(entity); + if (!insideNewRPC) + Utils.ResizeOrCreate(ref _syncCalls, _syncCallsCount + classData.FieldsCount); + int readerPosition = fieldsFlagsOffset + classData.FieldsFlagsSize; + + fixed (byte* predictedData = classData.GetLastServerData(entity), zeroFieldData = _zeroFieldBuffer) + { + for (int i = 0; i < classData.FieldsCount; i++) + { + ref var field = ref classData.Fields[i]; + bool hasData = Utils.IsBitSet(rawData + fieldsFlagsOffset, i); + + byte* fieldData; + int fieldSize = field.IntSize; - int fieldsFlagsOffset = readerPosition - classData.FieldsFlagsSize; - fixed (byte* interpDataPtr = classData.ClientInterpolatedNextData(entity), - predictedData = classData.ClientPredictedData(entity)) + //inside new rpc StateSerializer compares data with zeroes. So use same logic to decode + if (insideNewRPC && !hasData) { - for (int i = 0; i < classData.FieldsCount; i++) + if (fieldSize > StateSerializer.MaxStateSize) { - if (!Utils.IsBitSet(rawData + fieldsFlagsOffset, i)) - continue; - ref var field = ref classData.Fields[i]; - if (field.ReadField( - entity, - rawData + readerPosition, - predictedData, - writeInterpolationData ? interpDataPtr : null, - null)) - { - _syncCalls[_syncCallsCount++] = new SyncCallInfo(field.OnSync, entity, readerPosition, - field.IntSize); - } - - //Logger.Log($"E {entity.Id} Field updated: {field.Name}"); - readerPosition += field.IntSize; + Logger.LogError($"Field too large for zero buffer. Entity: {entity}. Field: {field.Name}. Size: {fieldSize}"); + continue; } + fieldData = zeroFieldData; + } + else if (!hasData) + { + continue; + } + else + { + fieldData = rawData + readerPosition; + } + + var target = field.GetTargetObjectAndOffset(entity, out int offset); + + if (field.IsPredicted) + RefMagic.CopyBlock(predictedData + field.PredictedOffset, fieldData, field.Size); + + + if (field.OnSync != null && insideNewRPC) + { + if ((field.OnSyncFlags & BindOnChangeFlags.ExecuteOnNew) != 0) + { + //execute immediately using temp data buffer inside SetFromAndSync + field.TypeProcessor.SetFromAndSync(target, offset, fieldData, field.OnSync); + } + //else skip set? because will be triggered in OnConstructed + } + else if (field.OnSync != null && (field.OnSyncFlags & BindOnChangeFlags.ExecuteOnSync) != 0 && field.TypeProcessor.SetFromAndSync(target, offset, fieldData)) + { + _syncCalls[_syncCallsCount++] = new SyncCallInfo(entity, readerPosition, i); + } + else + { + field.TypeProcessor.SetFrom(target, offset, fieldData); } - readerPosition = endPos; + if (hasData) + readerPosition += field.IntSize; } } } @@ -1032,7 +1120,7 @@ private static bool IsEntityIdValid(ushort id) public void GetDiagnosticData(Dictionary diagnosticDataDict) { diagnosticDataDict.Clear(); - _stateA?.GetDiagnosticData(EntitiesDict, ClassDataDict, diagnosticDataDict); + _stateA?.GetDiagnosticData(diagnosticDataDict); } } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Collections/AVLTree.cs b/Assets/Plugins/LiteEntitySystem/Collections/AVLTree.cs index b89c25f..2f1104b 100644 --- a/Assets/Plugins/LiteEntitySystem/Collections/AVLTree.cs +++ b/Assets/Plugins/LiteEntitySystem/Collections/AVLTree.cs @@ -147,7 +147,7 @@ private static int RightLeftRotate(TreeNode[] nodes, int a) return c; } - internal virtual void Add(T data) + internal void Add(T data) { if (_nodesCount + 1 == int.MaxValue) throw new Exception("collection overflow"); diff --git a/Assets/Plugins/LiteEntitySystem/Collections/BitSpan.cs b/Assets/Plugins/LiteEntitySystem/Collections/BitSpan.cs index d3da66b..bbb4a46 100644 --- a/Assets/Plugins/LiteEntitySystem/Collections/BitSpan.cs +++ b/Assets/Plugins/LiteEntitySystem/Collections/BitSpan.cs @@ -20,21 +20,21 @@ public BitSpan(Span bitRegion) public BitSpan(Span bitRegion, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = bitRegion; } public unsafe BitSpan(byte* bitRegion, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = new Span(bitRegion, ByteCount); } public BitSpan(byte[] bitRegion, int offset, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = new Span(bitRegion, offset, ByteCount); } @@ -82,28 +82,28 @@ public BitReadOnlySpan(Span bitRegion) public BitReadOnlySpan(Span bitRegion, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = bitRegion; } public BitReadOnlySpan(ReadOnlySpan bitRegion, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = bitRegion; } public unsafe BitReadOnlySpan(byte* bitRegion, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = new Span(bitRegion, ByteCount); } public BitReadOnlySpan(byte[] bitRegion, int offset, int bitCount) { BitCount = bitCount; - ByteCount = bitCount / BitsInByte + (bitCount % BitsInByte == 0 ? 0 : 1); + ByteCount = (bitCount + 7) / BitsInByte; _bitRegion = new Span(bitRegion, offset, ByteCount); } diff --git a/Assets/Plugins/LiteEntitySystem/Collections/SequenceBinaryHeap.cs b/Assets/Plugins/LiteEntitySystem/Collections/SequenceBinaryHeap.cs index 7d77466..6e8d334 100644 --- a/Assets/Plugins/LiteEntitySystem/Collections/SequenceBinaryHeap.cs +++ b/Assets/Plugins/LiteEntitySystem/Collections/SequenceBinaryHeap.cs @@ -21,6 +21,11 @@ public SequenceBinaryHeap(int capacity) _data = new SequenceHeapNode[capacity]; _count = 0; } + + public void Clear() + { + _count = 0; + } public void Add(T item, ushort sequence) { diff --git a/Assets/Plugins/LiteEntitySystem/EntityFilter.cs b/Assets/Plugins/LiteEntitySystem/EntityFilter.cs index 1e4b9f4..b3d7e37 100644 --- a/Assets/Plugins/LiteEntitySystem/EntityFilter.cs +++ b/Assets/Plugins/LiteEntitySystem/EntityFilter.cs @@ -8,6 +8,7 @@ public interface IEntityFilter { internal void Add(InternalEntity entity); internal void Remove(InternalEntity entity); + internal void TriggerConstructedEvent(InternalEntity entity); } //SortedSet like collection based on AVLTree @@ -29,7 +30,8 @@ public void SubscribeToConstructed(Action onConstructed, bool callOnExisting) { if (callOnExisting) foreach (var entity in this) - onConstructed(entity); + if(entity.CreationState == EntityState.LateConstructed) + onConstructed(entity); OnConstructed += onConstructed; } @@ -46,14 +48,9 @@ internal override bool Remove(T entity) return base.Remove(entity); } - internal override void Add(T data) - { - base.Add(data); - OnConstructed?.Invoke(data); - } - void IEntityFilter.Add(InternalEntity entity) => Add((T) entity); void IEntityFilter.Remove(InternalEntity entity) => Remove((T) entity); + void IEntityFilter.TriggerConstructedEvent(InternalEntity entity) => OnConstructed?.Invoke((T)entity); internal override void Clear() { diff --git a/Assets/Plugins/LiteEntitySystem/EntityLogic.cs b/Assets/Plugins/LiteEntitySystem/EntityLogic.cs index fe726f8..5307644 100644 --- a/Assets/Plugins/LiteEntitySystem/EntityLogic.cs +++ b/Assets/Plugins/LiteEntitySystem/EntityLogic.cs @@ -85,19 +85,6 @@ public ChangeParentData(ChangeType type, EntitySharedReference reference) [SyncVarFlags(SyncFlags.OnlyForOwner)] private SyncVar _localPredictedIdCounter; - - [SyncVarFlags(SyncFlags.OnlyForOwner)] - private SyncVar _predictedId; - - [SyncVarFlags(SyncFlags.OnlyForOwner)] - private SyncVar _isPredicted; - - internal ulong PredictedId => _predictedId.Value; - - /// - /// Is entity spawned using AddPredictedEntity - /// - public bool IsPredicted => _isPredicted.Value; //specific per player private SyncVar _isSyncEnabled; @@ -169,28 +156,56 @@ public void DisableLagCompensationForOwner() => EntityManager.DisableLagCompensation(); /// - /// Get synchronized seed for random generators based on current tick. Can be used for rollback or inside RPCs + /// Gets owner tick on client if entity is owned, and on server it returns owner tick. + /// Useful as seed for random generators based on current tick. /// - /// current tick depending on entity manager state (IsExecutingRPC and InRollBackState) - public int GetFrameSeed() => - EntityManager.IsClient - ? (EntityManager.InRollBackState ? ClientManager.RollBackTick : (ClientManager.IsExecutingRPC ? ClientManager.CurrentRPCTick : EntityManager.Tick)) - : (InternalOwnerId.Value == EntityManager.ServerPlayerId ? EntityManager.Tick : ServerManager.GetPlayer(InternalOwnerId).LastProcessedTick); + /// true if owner tick accessible, false if you trying get remote player tick + public bool TryGetOwnerTick(out ushort tick) + { + if (EntityManager.IsClient) + { + if (ClientManager.InternalPlayerId == InternalOwnerId.Value) + { + tick = EntityManager.Tick; + return true; + } + + if (InternalOwnerId.Value == EntityManager.ServerPlayerId) + { + tick = ClientManager.ServerTick; + return true; + } + + //else + tick = EntityManager.Tick; + return false; + } + + //Server + if(InternalOwnerId.Value == EntityManager.ServerPlayerId) + { + tick = EntityManager.Tick; + return true; + } + + tick = ServerManager.GetPlayer(InternalOwnerId).LastProcessedTick; + return true; + } /// /// Create predicted entity (like projectile) that will be replaced by server entity if prediction is successful /// Should be called also in rollback mode + /// Don't call this method inside Server->Client RPC! This will break many things. /// /// Entity type - /// Method that will be called after entity constructed + /// Method that will be called before entity OnConstructed /// Created predicted local entity - public T AddPredictedEntity(Action initMethod = null) where T : EntityLogic + public T AddPredictedEntity(Action initMethod = null) where T : PredictableEntityLogic { if (EntityManager.IsServer) return ServerManager.AddEntity(this, e => { - e._predictedId.Value = _localPredictedIdCounter.Value++; - e._isPredicted.Value = true; + e.InitEntity(_localPredictedIdCounter.Value++, this); initMethod?.Invoke(e); }); @@ -212,12 +227,12 @@ public T AddPredictedEntity(Action initMethod = null) where T : EntityLogi //local counter here should be reset ushort potentialId = _localPredictedIdCounter.Value++; - var origEnt = ClientManager.FindEntityByPredictedId(ClientManager.RollBackTick, Id, potentialId); + var origEnt = ClientManager.FindEntityByPredictedId(ClientManager.Tick, Id, potentialId); entity = origEnt as T; if (entity == null) { - Logger.LogWarning($"Requested RbTick{ClientManager.RollBackTick}, ParentId: {Id}, potentialId: {potentialId}, requestedType: {typeof(T)}, foundType: {origEnt?.GetType()}"); - Logger.LogWarning($"Misspredicted entity add? RbTick: {ClientManager.RollBackTick}, potentialId: {potentialId}"); + Logger.LogWarning($"Requested RbTick{ClientManager.Tick}, ParentId: {Id}, potentialId: {potentialId}, requestedType: {typeof(T)}, foundType: {origEnt?.GetType()}"); + Logger.LogWarning($"Misspredicted entity add? RbTick: {ClientManager.Tick}, potentialId: {potentialId}"); } else { @@ -229,8 +244,7 @@ public T AddPredictedEntity(Action initMethod = null) where T : EntityLogi entity = ClientManager.AddLocalEntity(this, e => { - e._predictedId.Value = _localPredictedIdCounter.Value++; - e._isPredicted.Value = true; + e.InitEntity(_localPredictedIdCounter.Value++, this); //Logger.Log($"AddPredictedEntity. Id: {e.Id}, ClassId: {e.ClassId} PredId: {e._predictedId.Value}, Tick: {e.ClientManager.Tick}"); initMethod?.Invoke(e); }); @@ -243,9 +257,9 @@ public T AddPredictedEntity(Action initMethod = null) where T : EntityLogi /// /// Entity type /// SyncVar of class that will be set to predicted entity and synchronized once confirmation will be received - /// Method that will be called after entity constructed + /// Method that will be called before entity OnConstructed /// Created predicted local entity - public void AddPredictedEntity(ref SyncVar targetReference, Action initMethod = null) where T : EntityLogic + public void AddPredictedEntity(ref SyncVar targetReference, Action initMethod = null) where T : PredictableEntityLogic { T entity; if (EntityManager.InRollBackState) @@ -260,7 +274,11 @@ public void AddPredictedEntity(ref SyncVar targetRefer if (EntityManager.IsServer) { - entity = ServerManager.AddEntity(this, initMethod); + entity = ServerManager.AddEntity(this, e => + { + e.InitEntity(_localPredictedIdCounter.Value++, this); + initMethod?.Invoke(e); + }); targetReference.Value = entity; return; } @@ -273,7 +291,7 @@ public void AddPredictedEntity(ref SyncVar targetRefer entity = ClientManager.AddLocalEntity(this, e => { - e._predictedId.Value = _localPredictedIdCounter.Value++; + e.InitEntity(_localPredictedIdCounter.Value++, this); initMethod?.Invoke(e); }); targetReference.Value = entity; @@ -361,8 +379,7 @@ internal override void DestroyInternal() { ClientManager.RemoveOwned(this); } - var parent = EntityManager.GetEntityById(_parentRef); - if (parent != null && !parent.IsDestroyed) + if (EntityManager.TryGetEntityById(_parentRef, out var parent) && !parent.IsDestroyed) { parent.Childs.Remove(this); } @@ -406,11 +423,9 @@ protected virtual void OnChildRemoved(EntityLogic child) } - internal void RefreshOwnerInfo(EntityLogic oldOwner) + private void OnOwnerChanged(byte oldPlayerId) { - if (EntityManager.IsServer || IsLocal) - return; - if(oldOwner != null && oldOwner.InternalOwnerId.Value == EntityManager.InternalPlayerId) + if(oldPlayerId == EntityManager.InternalPlayerId) ClientManager.RemoveOwned(this); if(InternalOwnerId.Value == EntityManager.InternalPlayerId) ClientManager.AddOwned(this); @@ -420,7 +435,8 @@ protected override void RegisterRPC(ref RPCRegistrator r) { base.RegisterRPC(ref r); - r.BindOnChange(ref _isSyncEnabled, (e, _) => e.OnSyncGroupsChanged(e._isSyncEnabled.Value)); + r.BindOnChange(ref _isSyncEnabled, (e, _) => e.OnSyncGroupsChanged(e._isSyncEnabled.Value)); + r.BindOnChange(this, ref InternalOwnerId, OnOwnerChanged); r.CreateRPCAction( (e, data) => @@ -429,7 +445,6 @@ protected override void RegisterRPC(ref RPCRegistrator r) { case ChangeType.Parent: var oldOwner = e.EntityManager.GetEntityById(data.Ref); - e.RefreshOwnerInfo(oldOwner); e.OnParentChanged(oldOwner); break; case ChangeType.ChildAdd: diff --git a/Assets/Plugins/LiteEntitySystem/EntityManager.cs b/Assets/Plugins/LiteEntitySystem/EntityManager.cs index e57911b..eaa9ded 100644 --- a/Assets/Plugins/LiteEntitySystem/EntityManager.cs +++ b/Assets/Plugins/LiteEntitySystem/EntityManager.cs @@ -66,6 +66,18 @@ public enum DeserializeResult Error, HeaderCheckFailed } + + [Flags] + public enum DebugFrameModes + { + Client = 1 << 0, + Server = 1 << 1, + Rollback = 1 << 2, + + All = Client | Server | Rollback, + ClientAndRollback = Client | Rollback, + ClientAndServer = Server | Client + } public enum MaxHistorySize : byte { @@ -91,10 +103,19 @@ public abstract class EntityManager /// /// Maximum synchronized (without LocalOnly) entities /// - public const int MaxSyncedEntityCount = 32767; + public const int MaxSyncedEntityCount = 64000; - public const int MaxEntityCount = MaxSyncedEntityCount * 2; + //because 0 - invalid id + public const int MaxEntityCount = ushort.MaxValue - 1; + + /// + /// Maximum count of predicted local entities + /// + public const int MaxLocalEntityCount = MaxEntityCount - MaxSyncedEntityCount; + /// + /// Server player Id - always 0 and reserved + /// public const byte ServerPlayerId = 0; /// @@ -162,6 +183,9 @@ public abstract class EntityManager /// public byte PlayerId => InternalPlayerId; + /// + /// EntityManager packets header byte (can be used to distinguish LES packets from custom made) + /// public readonly byte HeaderByte; public bool InRollBackState => UpdateMode == UpdateMode.PredictionRollback; @@ -171,7 +195,16 @@ public abstract class EntityManager internal const int MaxParts = 256; private const int MaxTicksPerUpdate = 5; + /// + /// Delta time between visual Updates + /// public double VisualDeltaTime { get; private set; } + + /// + /// Delta time between visual Updates (float) + /// + public float VisualDeltaTimeF => (float)VisualDeltaTime; + public const int MaxPlayers = byte.MaxValue-1; protected ushort _tick; @@ -210,6 +243,11 @@ public abstract class EntityManager /// IsRunning - sets to false after Reset() call /// public bool IsRunning => _stopwatch.IsRunning; + + /// + /// Is lag compensation currently enabled + /// + public bool IsLagCompensationEnabled => _lagCompensationEnabled; /// /// Register custom field type with interpolation @@ -217,6 +255,12 @@ public abstract class EntityManager /// interpolation function public static void RegisterFieldType(InterpolatorDelegateWithReturn interpolationDelegate) where T : unmanaged => ValueTypeProcessor.Registered[typeof(T)] = new UserTypeProcessor(interpolationDelegate); + + /// + /// Get information about current update frame + /// + /// debug formatted string + public abstract string GetCurrentFrameDebugInfo(DebugFrameModes modes); /// /// Register custom field type @@ -316,8 +360,12 @@ protected EntityManager( public void GetEntitySyncVarInfo(InternalEntity entity, IEntitySyncVarInfoPrinter resultPrinter) { ref var classData = ref ClassDataDict[entity.ClassId]; - foreach (EntityFieldInfo fi in classData.Fields) - resultPrinter.PrintFieldInfo(fi.Name, fi.TypeProcessor.ToString(entity, fi.Offset)); + for(int i = 0; i < classData.FieldsCount; i++) + { + ref var fi = ref classData.Fields[i]; + var target = fi.GetTargetObjectAndOffset(entity, out int offset); + resultPrinter.PrintFieldInfo(fi.Name, fi.TypeProcessor.ToString(target, offset)); + } } /// @@ -325,18 +373,18 @@ public void GetEntitySyncVarInfo(InternalEntity entity, IEntitySyncVarInfoPrinte /// public virtual void Reset() { - EntitiesCount = 0; - - _tick = 0; - VisualDeltaTime = 0.0; - _accumulator = 0; - _lastTime = 0; - InternalPlayerId = 0; - _stopwatch.Stop(); - _stopwatch.Reset(); + var entityFilter = GetEntities(); + var entitiesToDestroy = new InternalEntity[entityFilter.Count]; + int idx = 0; foreach (var entity in GetEntities()) - entity.DestroyInternal(); + entitiesToDestroy[idx++] = entity; + + for (int i = 0; i < entitiesToDestroy.Length; i++) + entitiesToDestroy[i].DestroyInternal(); + + for (int i = 0; i < entitiesToDestroy.Length; i++) + RemoveEntity(entitiesToDestroy[i]); foreach (var localSingleton in _localSingletons) localSingleton.Value?.Destroy(); @@ -345,8 +393,18 @@ public virtual void Reset() AliveEntities.Clear(); _localSingletons.Clear(); _localSingletonBaseTypes.Clear(); - Array.Clear(EntitiesDict, 0, EntitiesDict.Length); Array.Clear(_entityFilters, 0, _entityFilters.Length); + + if(EntitiesCount != 0) + Logger.LogWarning($"Entities not fully removed: {EntitiesCount}"); + EntitiesCount = 0; + + _tick = 0; + VisualDeltaTime = 0.0; + _accumulator = 0; + _lastTime = 0; + _stopwatch.Stop(); + _stopwatch.Reset(); } /// @@ -391,8 +449,12 @@ public EntityFilter GetEntities() where T : InternalEntity { foreach (var entity in GetEntities()) { - if(entity is T castedEnt) + if (entity is T castedEnt) + { typedFilter.Add(castedEnt); + if(entity.CreationState == EntityState.LateConstructed) + entityFilter.TriggerConstructedEvent(entity); + } } } @@ -518,11 +580,11 @@ protected T AddEntity(EntityParams entityParams) where T : InternalEntity return entity; } - protected bool ConstructEntity(InternalEntity e) + protected void ConstructEntity(InternalEntity e) { if (e.IsConstructed) - return false; - e.IsConstructed = true; + return; + e.ConstructInternal(); ref var classData = ref ClassDataDict[e.ClassId]; if (classData.IsSingleton) @@ -547,16 +609,8 @@ protected bool ConstructEntity(InternalEntity e) if (IsEntityAlive(classData.Flags, e)) { AliveEntities.Add(e); - OnAliveEntityAdded(e); } _entitiesToLateConstruct.Enqueue(e); - - return true; - } - - protected virtual void OnAliveEntityAdded(InternalEntity e) - { - } protected static bool IsEntityLagCompensated(InternalEntity e) @@ -593,6 +647,10 @@ internal virtual void OnEntityDestroyed(InternalEntity e) protected void RemoveEntity(InternalEntity e) { + //this can happen on entity replace. + if (e.IsRemoved) + return; + if(!e.IsDestroyed) Logger.LogError($"Remove not destroyed entity!: {e}"); @@ -600,7 +658,7 @@ protected void RemoveEntity(InternalEntity e) EntitiesDict[e.Id] = null; EntitiesCount--; ClassDataDict[e.ClassId].ReleaseDataCache(e); - e.IsRemoved = true; + e.Remove(); //Logger.Log($"{Mode} - RemoveEntity: {e.Id}"); } @@ -633,16 +691,41 @@ public void DisableLagCompensation() protected void ExecuteLateConstruct() { - foreach (var internalEntity in _entitiesToLateConstruct) - internalEntity.OnLateConstructed(); + foreach (var e in _entitiesToLateConstruct) + { + if (e.IsDestroyed) + continue; + e.LateConstructInternal(); + + ref var classData = ref ClassDataDict[e.ClassId]; + if (classData.IsSingleton) + { + foreach (var baseTypeInfo in classData.BaseTypes) + if(!baseTypeInfo.IsSingleton) + _entityFilters[baseTypeInfo.Id]?.TriggerConstructedEvent(e); + } + else + { + _entityFilters[classData.FilterId]?.TriggerConstructedEvent(e); + foreach (var baseTypeInfo in classData.BaseTypes) + _entityFilters[baseTypeInfo.Id]?.TriggerConstructedEvent(e); + } + } _entitiesToLateConstruct.Clear(); } protected abstract void OnLogicTick(); - - internal abstract void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue) + + internal abstract void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue, ref T oldValue, bool skipOnSync) where T : unmanaged; + protected void ExecuteLocalSingletonsLateUpdate() + { + foreach (var localSingleton in _localSingletons) + if(localSingleton.Value is ILocalSingletonWithUpdate updSingleton) + updSingleton.LateUpdate(DeltaTimeF); + } + /// /// Main update method, updates internal fixed timer and do all other stuff /// diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/LocalMessageBus.cs b/Assets/Plugins/LiteEntitySystem/Extensions/LocalMessageBus.cs new file mode 100644 index 0000000..129db85 --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/Extensions/LocalMessageBus.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using LiteEntitySystem; +using LiteEntitySystem.Internal; + +namespace LiteEntitySystem.Extensions +{ + /// + /// Represents a subscription to a message bus channel that can be disposed to unsubscribe. + /// + public readonly struct BusSubscription : IDisposable + { + private readonly Action _dispose; + public BusSubscription(Action dispose) => _dispose = dispose; + + /// + /// Disposes the subscription, unsubscribing the handler from the message bus. + /// + public void Dispose() => _dispose?.Invoke(); + } + + /// + /// Marker interface for message bus messages. TSender declares the expected sender type for the message. + /// + /// The type of the entity that sends this message. + public interface IBusMessage + { + } + + /// + /// A local message bus for publishing and subscribing to typed messages within a local singleton context. + /// + public sealed class LocalMessageBus : ILocalSingleton + { + /// + /// If true, all published messages will be logged for debugging purposes. + /// + public bool DebugToLog; + + private interface IChannel : IDisposable { } + + private interface IChannel : IChannel + where TMessage : struct, IBusMessage + { + BusSubscription Subscribe(Action handler); + void Publish(TSender sender, in TMessage msg, bool debugToLog = false); + } + + // Null-object channel used after the bus is disposed to avoid null checks and NREs + private sealed class NullChannel : IChannel + where TMessage : struct, IBusMessage + { + public static readonly NullChannel Instance = new(); + public BusSubscription Subscribe(Action handler) => default; + public void Publish(TSender sender, in TMessage msg, bool debugToLog = false) { } + public void Dispose() { } + } + + private sealed class Channel : IChannel + where TMessage : struct, IBusMessage + { + private const int PreallocatedSize = 8; + private Action[] _buffer; + + private readonly List> _handlers = new(); + private bool _disposed; + + public BusSubscription Subscribe(Action handler) + { + if (_disposed) throw new ObjectDisposedException(nameof(Channel)); + _handlers.Add(handler); + return new BusSubscription(() => Unsubscribe(handler)); + } + + private void Unsubscribe(Action handler) + { + _handlers.Remove(handler); + } + + public void Publish(TSender sender, in TMessage msg, bool debugToLog = false) + { + if (_disposed || _handlers == null || _handlers.Count == 0) + return; + + var count = _handlers.Count; + if (_buffer == null || _buffer.Length < count) + _buffer = new Action[Math.Max(count, PreallocatedSize)]; + + // Fast copy handlers into buffer + _handlers.CopyTo(0, _buffer, 0, count); + + for (int i = 0; i < count; i++) + { + if (debugToLog) + { + Logger.Log($"MsgBus: {typeof(TSender)} -> {_buffer[i].Target}, msg: {msg}"); + } + _buffer[i](sender, msg); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _handlers.Clear(); + } + } + + private readonly Dictionary _channels = new(); + private bool _disposed; + + private IChannel Get() + where TMessage : struct, IBusMessage + { + if (_disposed) + return NullChannel.Instance; + + var t = typeof(TMessage); + if (_channels.TryGetValue(t, out var ch)) + return (IChannel)ch; + + var created = new Channel(); + _channels.Add(t, created); + return created; + } + + /// + /// Subscribes a handler to receive messages of a specific type. + /// + /// The type of the sender of the message. + /// The type of message to subscribe to. + /// The handler delegate to invoke when messages are published. + /// A subscription token that can be disposed to unsubscribe. + public BusSubscription Subscribe(Action handler) + where TMessage : struct, IBusMessage + => Get().Subscribe(handler); + + /// + /// Publishes a message to all subscribers. + /// + /// The type of the sender of the message. + /// The type of message to publish. + /// The entity sending the message. + /// The message to publish. + public void Send(TSender sender, in TMessage msg) + where TMessage : struct, IBusMessage + => Get().Publish(sender, in msg, DebugToLog); + + /// + /// Destroys the message bus, disposing all channels and unsubscribing all handlers. + /// + public void Destroy() + { + if (_disposed) return; + _disposed = true; + foreach (var ch in _channels.Values) + ch.Dispose(); + _channels.Clear(); + } + } + + /// + /// Extension methods for accessing the local message bus from entities. + /// + public static class LocalMessageBusExtensions + { + /// + /// Gets or creates a local message bus singleton for the entity manager. + /// + /// The entity to get the message bus from. + /// The local message bus instance, creating it if it doesn't exist. + public static LocalMessageBus GetOrCreateLocalMessageBus(this InternalEntity ent) + { + var msgBus = ent.EntityManager.GetLocalSingleton(); + if(msgBus == null) + { + msgBus = new LocalMessageBus(); + ent.EntityManager.AddLocalSingleton(msgBus); + } + return msgBus; + } + + /// + /// Gets the local message bus singleton from the entity manager. + /// + /// The entity to get the message bus from. + /// The local message bus instance, or null if not yet created. + public static LocalMessageBus GetLocalMessageBus(this InternalEntity ent) => + ent.EntityManager.GetLocalSingleton(); + } +} \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/LocalMessageBus.cs.meta b/Assets/Plugins/LiteEntitySystem/Extensions/LocalMessageBus.cs.meta new file mode 100644 index 0000000..60b61bf --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/Extensions/LocalMessageBus.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a935e910dd44ccf1696a1b7fd0028dc5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncDict.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncDict.cs index 232112d..5e45273 100644 --- a/Assets/Plugins/LiteEntitySystem/Extensions/SyncDict.cs +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncDict.cs @@ -4,7 +4,7 @@ namespace LiteEntitySystem.Extensions { - public class SyncDict : SyncableField, IEnumerable> where TKey : unmanaged where TValue : unmanaged + public class SyncDict : SyncableFieldCustomRollback, IEnumerable> where TKey : unmanaged where TValue : unmanaged { private struct KeyValue { @@ -31,8 +31,6 @@ public KeyValue(TKey key, TValue value) public Dictionary.KeyCollection Keys => _data.Keys; public Dictionary.ValueCollection Values => _data.Values; - - public override bool IsRollbackSupported => true; protected internal override void RegisterRPC(ref SyncableRPCRegistrator r) { @@ -85,18 +83,21 @@ private void InitAction(ReadOnlySpan data) private void AddAction(KeyValue kv) { _data[kv.Key] = kv.Value; + MarkAsChanged(); } public void Add(TKey key, TValue value) { _data.Add(key, value); ExecuteRPC(_addAction, new KeyValue(key,value)); + MarkAsChanged(); } public void Clear() { _data.Clear(); ExecuteRPC(_clearAction); + MarkAsChanged(); } public bool TryGetValue(TKey key, out TValue value) @@ -124,6 +125,7 @@ public bool Remove(TKey key) if (!_data.Remove(key)) return false; ExecuteRPC(_removeAction, key); + MarkAsChanged(); return true; } diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncHashSet.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncHashSet.cs index 3ed1c83..be9d8e4 100644 --- a/Assets/Plugins/LiteEntitySystem/Extensions/SyncHashSet.cs +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncHashSet.cs @@ -4,7 +4,7 @@ namespace LiteEntitySystem.Extensions { - public class SyncHashSet : SyncableField, IEnumerable where T : unmanaged + public class SyncHashSet : SyncableFieldCustomRollback, IEnumerable where T : unmanaged { public int Count => _data.Count; @@ -16,8 +16,6 @@ public class SyncHashSet : SyncableField, IEnumerable where T : unmanaged private static RemoteCall _clearAction; private static RemoteCall _removeAction; private static RemoteCallSpan _initAction; - - public override bool IsRollbackSupported => true; protected internal override void RegisterRPC(ref SyncableRPCRegistrator r) { @@ -86,12 +84,14 @@ public void Add(T x) { _data.Add(x); ExecuteRPC(_addAction, x); + MarkAsChanged(); } public void Clear() { _data.Clear(); ExecuteRPC(_clearAction); + MarkAsChanged(); } public bool Contains(T x) => _data.Contains(x); @@ -103,6 +103,7 @@ public bool Remove(T key) if (!_data.Remove(key)) return false; ExecuteRPC(_removeAction, key); + MarkAsChanged(); return true; } diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncList.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncList.cs index 2263eb4..ef56c9b 100644 --- a/Assets/Plugins/LiteEntitySystem/Extensions/SyncList.cs +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncList.cs @@ -5,8 +5,28 @@ namespace LiteEntitySystem.Extensions { - public class SyncList : SyncableField, ICollection, IReadOnlyList where T : unmanaged + public class SyncList : SyncableFieldCustomRollback, ICollection, IReadOnlyList where T : unmanaged { + public struct SyncListEnumerator : IEnumerator + { + private readonly T[] _data; + private readonly int _count; + private int _index; + + public SyncListEnumerator(T[] data, int count) + { + _data = data; + _count = count; + _index = -1; + } + + public bool MoveNext() => ++_index < _count; + public void Reset() => _index = -1; + public T Current => _index >= 0 && _index < _data.Length ? _data[_index] : default; + object IEnumerator.Current => Current; + public void Dispose() {} + } + struct SetValueData { public int Index; @@ -28,8 +48,6 @@ struct SetValueData private static RemoteCallSpan _initAction; private static RemoteCall _setAction; - public override bool IsRollbackSupported => true; - protected internal override void BeforeReadRPC() { _serverData ??= new T[_data.Length]; @@ -106,12 +124,14 @@ public void Add(T item) _data[_count] = item; _count++; ExecuteRPC(_addAction, item); + MarkAsChanged(); } public void Clear() { _count = 0; ExecuteRPC(_clearAction); + MarkAsChanged(); } public bool Contains(T item) @@ -158,6 +178,7 @@ public void RemoveAt(int index) _data[_count - 1] = default; _count--; ExecuteRPC(_removeAtAction, index); + MarkAsChanged(); } public T this[int index] @@ -170,19 +191,10 @@ public T this[int index] } } - public IEnumerator GetEnumerator() - { - int index = 0; - while (index < _count) - { - yield return _data[index]; - index++; - } - } + public SyncListEnumerator GetEnumerator() => new(_data, _count); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncPongTimer.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncPongTimer.cs new file mode 100644 index 0000000..8630812 --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncPongTimer.cs @@ -0,0 +1,50 @@ +namespace LiteEntitySystem.Extensions +{ + public class SyncPongTimer : SyncableField + { + public float MaxTime => _maxTime; + public float ElapsedTime => _time; + public bool IsTimeElapsed => _time >= _maxTime; + public float CountdownTime => _maxTime - _time; + public bool HasStarted => _time > 0; + + private SyncVar _time; + private const float _maxTime = 1f; + + public SyncPongTimer() { } + + public float Progress => _time; + + public void Reset() + { + _time.Value = 0f; + } + + public void Finish() + { + _time.Value = _maxTime; + } + + public bool UpdateForward(float delta) + { + if (delta > 0f) + { + float newTime = _time.Value + delta; + if (newTime > 1f) newTime = 1f; + _time.Value = newTime; + } + return IsTimeElapsed; + } + + public bool UpdateBackward(float delta) + { + if (delta > 0f) + { + float newTime = _time.Value - delta; + if (newTime < 0f) newTime = 0f; + _time.Value = newTime; + } + return !HasStarted; + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncPongTimer.cs.meta b/Assets/Plugins/LiteEntitySystem/Extensions/SyncPongTimer.cs.meta new file mode 100644 index 0000000..9b49d65 --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncPongTimer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 074123fd4653005b096387a88a742e15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncQueue.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncQueue.cs index 56a7187..bc695ae 100644 --- a/Assets/Plugins/LiteEntitySystem/Extensions/SyncQueue.cs +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncQueue.cs @@ -4,7 +4,7 @@ namespace LiteEntitySystem.Extensions { - public class SyncQueue : SyncableField, IReadOnlyCollection, ICollection where T : unmanaged + public class SyncQueue : SyncableFieldCustomRollback, IReadOnlyCollection, ICollection where T : unmanaged { // TODO: implement ring buffer instead of using .net's Queue. @@ -20,9 +20,7 @@ public class SyncQueue : SyncableField, IReadOnlyCollection, ICollection w public int Count => _data.Count; public bool IsSynchronized => false; public object SyncRoot => throw new NotImplementedException("The SyncQueue Collection isn't thread-safe."); - - public override bool IsRollbackSupported => true; - + protected internal override void RegisterRPC(ref SyncableRPCRegistrator r) { base.RegisterRPC(ref r); @@ -74,6 +72,7 @@ public void Enqueue(T item) { _data.Enqueue(item); ExecuteRPC(_enqueueAction, item); + MarkAsChanged(); } private void EnqueueClientAction(T item) => _data.Enqueue(item); @@ -82,6 +81,7 @@ public T Dequeue() { var value = _data.Dequeue(); ExecuteRPC(_dequeueAction); + MarkAsChanged(); return value; } @@ -89,7 +89,10 @@ public bool TryDequeue(out T item) { bool hasValue = _data.TryDequeue(out item); if (hasValue) + { ExecuteRPC(_dequeueAction); + MarkAsChanged(); + } return hasValue; } @@ -105,6 +108,7 @@ public void Clear() { _data.Clear(); ExecuteRPC(_clearAction); + MarkAsChanged(); } private void ClearClientAction() => _data.Clear(); diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncStateMachine.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncStateMachine.cs index 7ac74c6..9ce3b4f 100644 --- a/Assets/Plugins/LiteEntitySystem/Extensions/SyncStateMachine.cs +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncStateMachine.cs @@ -15,8 +15,6 @@ public class SyncStateMachine : SyncableField where T : unmanaged, Enum public T CurrentState => _state; - public override bool IsRollbackSupported => true; - private readonly StateCalls[] _data; public SyncStateMachine() diff --git a/Assets/Plugins/LiteEntitySystem/Extensions/SyncTimer.cs b/Assets/Plugins/LiteEntitySystem/Extensions/SyncTimer.cs index 1d62cb9..487c1d9 100644 --- a/Assets/Plugins/LiteEntitySystem/Extensions/SyncTimer.cs +++ b/Assets/Plugins/LiteEntitySystem/Extensions/SyncTimer.cs @@ -1,6 +1,4 @@ -using LiteEntitySystem.Internal; - -namespace LiteEntitySystem.Extensions +namespace LiteEntitySystem.Extensions { public class SyncTimer : SyncableField { @@ -13,8 +11,6 @@ public class SyncTimer : SyncableField private SyncVar _time; private SyncVar _maxTime; - public override bool IsRollbackSupported => true; - public SyncTimer(float maxTime) { _maxTime.Value = maxTime; @@ -30,6 +26,9 @@ public float Progress { get { + if(_maxTime == 0f) + return 1f; + float p = _time/_maxTime; return p > 1f ? 1f : p; } diff --git a/Assets/Plugins/LiteEntitySystem/HumanControllerLogic.cs b/Assets/Plugins/LiteEntitySystem/HumanControllerLogic.cs index bb386fe..68336ea 100644 --- a/Assets/Plugins/LiteEntitySystem/HumanControllerLogic.cs +++ b/Assets/Plugins/LiteEntitySystem/HumanControllerLogic.cs @@ -240,6 +240,12 @@ public InputCommand(ushort tick, TInput data) //server part internal readonly SequenceBinaryHeap AvailableInput; + /// + /// Returns default new input for custom cases where not all values should be 0 + /// + /// New clean input + protected virtual TInput GetDefaultInput() => default; + /// /// Get pending input reference for modifications /// Pending input resets to default after it was assigned to CurrentInput inside logic tick @@ -249,7 +255,7 @@ protected ref TInput ModifyPendingInput() { if (_shouldResetInput) { - _pendingInput = default; + _pendingInput = GetDefaultInput(); _shouldResetInput = false; } return ref _pendingInput; @@ -276,6 +282,10 @@ protected HumanControllerLogic(EntityParams entityParams) : base(entityParams, U { AvailableInput = new(ServerEntityManager.MaxStoredInputs); } + + // ReSharper disable once VirtualMemberCallInConstructor + _currentInput = GetDefaultInput(); + _pendingInput = _currentInput; } internal override void RemoveClientProcessedInputs(ushort processedTick) @@ -320,7 +330,7 @@ internal override void ApplyIncomingInput(ushort tick) else if (seqDiff == 0) { //Set input if tick equals - CurrentInput = AvailableInput.ExtractMin(); + _currentInput = AvailableInput.ExtractMin(); break; } } diff --git a/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.dll b/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.dll index 5c12d9f..8d3b14b 100644 Binary files a/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.dll and b/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.dll differ diff --git a/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.il b/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.il new file mode 100644 index 0000000..896ad8c --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.il @@ -0,0 +1,163 @@ +.assembly extern netstandard +{ + .publickeytoken = (CC 7B 13 FF CD 2D DD 51 ) + .ver 2:1:0:0 +} +.assembly RefMagic +{ + .custom instance void [netstandard]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) + .custom instance void [netstandard]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 // ....T..WrapNonEx + 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) // ceptionThrows. + .custom instance void [netstandard]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = ( 01 00 19 2E 4E 45 54 53 74 61 6E 64 61 72 64 2C // ....NETStandard, + 56 65 72 73 69 6F 6E 3D 76 32 2E 31 01 00 54 0E // Version=v2.1..T. + 14 46 72 61 6D 65 77 6F 72 6B 44 69 73 70 6C 61 // .FrameworkDispla + 79 4E 61 6D 65 11 2E 4E 45 54 20 53 74 61 6E 64 // yName..NET Stand + 61 72 64 20 32 2E 31 ) // ard 2.1 // rdLibrary.. + .permissionset reqmin + = {[netstandard]System.Security.Permissions.SecurityPermissionAttribute = {property bool 'SkipVerification' = bool(true)}} + .hash algorithm 0x00008004 + .ver 1:0:0:0 +} +.custom instance void [netstandard]System.Security.UnverifiableCodeAttribute::.ctor() = ( 01 00 00 00 ) +.imagebase 0x10000000 +.file alignment 0x00000200 +.stackreserve 0x00100000 +.subsystem 0x0003 // WINDOWS_CUI +.corflags 0x00000001 // ILONLY +.module RefMagic.dll + +.class interface public abstract auto ansi beforefieldinit LiteEntitySystem.Internal.ISyncVar`1< + valuetype .ctor (class [netstandard]System.ValueType modreq([netstandard]System.Runtime.InteropServices.UnmanagedType)) T> +{ + .param type T + .custom instance void [netstandard]System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = (01 00 00 00 ) + .method public hidebysig virtual newslot abstract instance void SvSetDirect(!T 'value') cil managed {} + .method public hidebysig virtual newslot abstract instance void SvSetDirectAndStorePrev(!T 'value', [out] !T& prevValue) cil managed {} + .method public hidebysig virtual newslot abstract instance bool SvSetFromAndSync(!T& 'value') cil managed {} + .method public hidebysig virtual newslot abstract instance void SvSetInterpValue(!T 'value') cil managed {} + .method public hidebysig virtual newslot abstract instance void SvSetInterpValueFromCurrent() cil managed {} +} + +.class public abstract auto ansi sealed beforefieldinit LiteEntitySystem.Internal.RefMagic extends [netstandard]System.Object +{ + .method public hidebysig static !!T GetFieldValue(object obj, int32 offs) cil managed aggressiveinlining + { + ldarg obj + conv.i + ldarg offs + add + ldobj !!T + ret + } + + .method public hidebysig static void SetFieldValue(object obj, int32 offs, !!T 'value') cil managed aggressiveinlining + { + ldarg obj + conv.i + ldarg offs + add + ldarg 'value' + stobj !!T + ret + } + + .method public hidebysig static void SyncVarSetDirect< + valuetype .ctor (class [netstandard]System.ValueType modreq ([netstandard]System.Runtime.InteropServices.UnmanagedType)) T, + valuetype .ctor (class LiteEntitySystem.Internal.ISyncVar`1, class [netstandard]System.ValueType) TSyncVar> + (object obj, int32 offs, !!T 'value') cil managed aggressiveinlining + { + .param type T + .custom instance void [netstandard]System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = (01 00 00 00 ) + + ldarg obj + conv.i + ldarg offs + add + ldarg 'value' + constrained. !!TSyncVar + callvirt instance void class LiteEntitySystem.Internal.ISyncVar`1::SvSetDirect(!0) + ret + } + + .method public hidebysig static void SyncVarSetInterp< + valuetype .ctor (class [netstandard]System.ValueType modreq ([netstandard]System.Runtime.InteropServices.UnmanagedType)) T, + valuetype .ctor (class LiteEntitySystem.Internal.ISyncVar`1, class [netstandard]System.ValueType) TSyncVar> + (object obj, int32 offs, !!T 'value') cil managed aggressiveinlining + { + .param type T + .custom instance void [netstandard]System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = (01 00 00 00 ) + + ldarg obj + conv.i + ldarg offs + add + ldarg 'value' + constrained. !!TSyncVar + callvirt instance void class LiteEntitySystem.Internal.ISyncVar`1::SvSetInterpValue(!0) + ret + } + + .method public hidebysig static void SyncVarSetInterpFromCurrent< + valuetype .ctor (class [netstandard]System.ValueType modreq ([netstandard]System.Runtime.InteropServices.UnmanagedType)) T, + valuetype .ctor (class LiteEntitySystem.Internal.ISyncVar`1, class [netstandard]System.ValueType) TSyncVar> + (object obj, int32 offs) cil managed aggressiveinlining + { + .param type T + .custom instance void [netstandard]System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = (01 00 00 00 ) + + ldarg obj + conv.i + ldarg offs + add + constrained. !!TSyncVar + callvirt instance void class LiteEntitySystem.Internal.ISyncVar`1::SvSetInterpValueFromCurrent() + ret + } + + .method public hidebysig static void SyncVarSetDirectAndStorePrev< + valuetype .ctor (class [netstandard]System.ValueType modreq([netstandard]System.Runtime.InteropServices.UnmanagedType)) T, + valuetype .ctor (class LiteEntitySystem.Internal.ISyncVar`1, [netstandard]System.ValueType) TSyncVar> + (object obj, int32 offs, !!T 'value', [out] !!T& prevValue) cil managed aggressiveinlining + { + .param type T + .custom instance void [netstandard]System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = ( 01 00 00 00 ) + + ldarg obj + conv.i + ldarg offs + add + ldarg 'value' + ldarg prevValue + constrained. !!TSyncVar + callvirt instance void class LiteEntitySystem.Internal.ISyncVar`1::SvSetDirectAndStorePrev(!0, !0&) + ret + } + + .method public hidebysig static bool SyncVarSetFromAndSync< + valuetype .ctor (class [netstandard]System.ValueType modreq([netstandard]System.Runtime.InteropServices.UnmanagedType)) T, + valuetype .ctor (class LiteEntitySystem.Internal.ISyncVar`1, [netstandard]System.ValueType) TSyncVar> + (object obj, int32 offs, !!T& 'value') cil managed aggressiveinlining + { + .param type T + .custom instance void [netstandard]System.Runtime.CompilerServices.IsUnmanagedAttribute::.ctor() = (01 00 00 00 ) + + ldarg obj + conv.i + ldarg offs + add + ldarg 'value' + constrained. !!TSyncVar + callvirt instance bool class LiteEntitySystem.Internal.ISyncVar`1::SvSetFromAndSync(!0&) + ret + } + + .method public hidebysig static void CopyBlock(void* destination, void* source, unsigned int32 byteCount) cil managed aggressiveinlining + { + .maxstack 3 + ldarg.0 + ldarg.1 + ldarg.2 + cpblk + ret + } +} \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.il.meta b/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.il.meta new file mode 100644 index 0000000..e597eb6 --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/ILPart/RefMagic.il.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ed546e403437eb249b60a62a394b4da0 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/LiteEntitySystem/ILocalSingleton.cs b/Assets/Plugins/LiteEntitySystem/ILocalSingleton.cs index abb7971..0f5d310 100644 --- a/Assets/Plugins/LiteEntitySystem/ILocalSingleton.cs +++ b/Assets/Plugins/LiteEntitySystem/ILocalSingleton.cs @@ -9,5 +9,6 @@ public interface ILocalSingletonWithUpdate : ILocalSingleton { void Update(float dt); void VisualUpdate(float dt); + void LateUpdate(float dt); } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Internal/DeltaCompressor.cs b/Assets/Plugins/LiteEntitySystem/Internal/DeltaCompressor.cs index 6957c6d..9d4f583 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/DeltaCompressor.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/DeltaCompressor.cs @@ -18,8 +18,8 @@ internal struct DeltaCompressor public DeltaCompressor(int size) { Size = size; - DeltaBits = Size / FieldsDivision + (Size % FieldsDivision == 0 ? 0 : 1); - MinDeltaSize = DeltaBits / 8 + (DeltaBits % 8 == 0 ? 0 : 1); + DeltaBits = (Size + FieldsDivision - 1) / FieldsDivision; + MinDeltaSize = (DeltaBits + 7) / 8; MaxDeltaSize = MinDeltaSize + Size; _firstFullData = null; } diff --git a/Assets/Plugins/LiteEntitySystem/Internal/EntityClassData.cs b/Assets/Plugins/LiteEntitySystem/Internal/EntityClassData.cs index a5d32b5..f004d5a 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/EntityClassData.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/EntityClassData.cs @@ -63,7 +63,7 @@ internal struct EntityClassData public readonly int PredictedSize; public readonly EntityFieldInfo[] Fields; public readonly SyncableFieldInfo[] SyncableFields; - public readonly int InterpolatedFieldsSize; + public readonly SyncableFieldInfo[] SyncableFieldsCustomRollback; public readonly int InterpolatedCount; public readonly EntityFieldInfo[] LagCompensatedFields; public readonly int LagCompensatedSize; @@ -74,8 +74,8 @@ internal struct EntityClassData public RpcFieldInfo[] RemoteCallsClient; public readonly Type Type; - private readonly EntityFieldInfo[] _ownedRollbackFields; - private readonly EntityFieldInfo[] _remoteRollbackFields; + private readonly int[] _ownedRollbackFields; + private readonly int[] _remoteRollbackFields; private static readonly Type InternalEntityType = typeof(InternalEntity); internal static readonly Type SingletonEntityType = typeof(SingletonEntityLogic); @@ -86,14 +86,13 @@ internal struct EntityClassData private readonly int _dataCacheSize; private readonly int _maxHistoryCount; private readonly int _historyStart; + + public Span GetLastServerData(InternalEntity e) => new (e.IOBuffer, 0, PredictedSize); - public Span ClientInterpolatedPrevData(InternalEntity e) => new (e.IOBuffer, 0, InterpolatedFieldsSize); - public Span ClientInterpolatedNextData(InternalEntity e) => new (e.IOBuffer, InterpolatedFieldsSize, InterpolatedFieldsSize); - public Span ClientPredictedData(InternalEntity e) => new (e.IOBuffer, InterpolatedFieldsSize*2, PredictedSize); - - public EntityFieldInfo[] GetRollbackFields(bool isOwned) => + public int[] GetRollbackFields(bool isOwned) => isOwned ? _ownedRollbackFields : _remoteRollbackFields; + //here Offset used because SyncableFields doesn't support LagCompensated fields public unsafe void WriteHistory(EntityLogic e, ushort tick) { int historyOffset = ((tick % _maxHistoryCount)+1)*LagCompensatedSize; @@ -174,7 +173,6 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp FixedFieldsSize = 0; LagCompensatedSize = 0; InterpolatedCount = 0; - InterpolatedFieldsSize = 0; RemoteCallsClient = null; ClassId = typeInfo.ClassId; ClassEnumName = typeInfo.ClassName; @@ -190,9 +188,10 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp var fields = new List(); var syncableFields = new List(); + var syncableFieldsWithCustomRollback = new List(); var lagCompensatedFields = new List(); - var ownedRollbackFields = new List(); - var remoteRollbackFields = new List(); + var ownedRollbackFields = new List(); + var remoteRollbackFields = new List(); var allTypesStack = Utils.GetBaseTypes(entType, InternalEntityType, true, true); while(allTypesStack.Count > 0) @@ -231,10 +230,9 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp int fieldSize = valueTypeProcessor.Size; if (syncFlags.HasFlagFast(SyncFlags.Interpolated) && !ft.IsEnum) { - InterpolatedFieldsSize += fieldSize; InterpolatedCount++; } - var fieldInfo = new EntityFieldInfo($"{baseType.Name}-{field.Name}", valueTypeProcessor, offset, syncVarFlags); + var fieldInfo = new EntityFieldInfo($"{baseType.Name}-{field.Name}", valueTypeProcessor, offset, syncVarFlags?.Flags ?? SyncFlags.None); if (syncFlags.HasFlagFast(SyncFlags.LagCompensated)) { lagCompensatedFields.Add(fieldInfo); @@ -253,6 +251,11 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp throw new Exception($"Syncable fields should be readonly! (Class: {entType} Field: {field.Name})"); syncableFields.Add(new SyncableFieldInfo(offset, syncFlags)); + + //add custom rollbacked separately + if (ft.IsSubclassOf(typeof(SyncableFieldCustomRollback))) + syncableFieldsWithCustomRollback.Add(new SyncableFieldInfo(offset, syncFlags)); + var syncableFieldTypesWithBase = Utils.GetBaseTypes(ft, SyncableFieldType, true, true); while(syncableFieldTypesWithBase.Count > 0) { @@ -279,8 +282,21 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp Logger.LogError($"Unregistered field type: {syncableFieldType}"); continue; } + + var mergedSyncFlags = (syncableField.GetCustomAttribute()?.Flags ?? SyncFlags.None) | (syncVarFlags?.Flags ?? SyncFlags.None); + if (mergedSyncFlags.HasFlagFast(SyncFlags.OnlyForOwner) && + mergedSyncFlags.HasFlagFast(SyncFlags.OnlyForOtherPlayers)) + { + Logger.LogWarning($"{SyncFlags.OnlyForOwner} and {SyncFlags.OnlyForOtherPlayers} flags can't be used together! Field: {syncableType} - {syncableField.Name}"); + } + if (mergedSyncFlags.HasFlagFast(SyncFlags.AlwaysRollback) && + mergedSyncFlags.HasFlagFast(SyncFlags.NeverRollBack)) + { + Logger.LogWarning($"{SyncFlags.AlwaysRollback} and {SyncFlags.NeverRollBack} flags can't be used together! Field: {syncableType} - {syncableField.Name}"); + } + int syncvarOffset = Utils.GetFieldOffset(syncableField); - var fieldInfo = new EntityFieldInfo($"{baseType.Name}-{field.Name}:{syncableField.Name}", valueTypeProcessor, offset, syncvarOffset, syncVarFlags); + var fieldInfo = new EntityFieldInfo($"{baseType.Name}-{field.Name}:{syncableField.Name}", valueTypeProcessor, offset, syncvarOffset, mergedSyncFlags); fields.Add(fieldInfo); FixedFieldsSize += fieldInfo.IntSize; if (fieldInfo.IsPredicted) @@ -300,6 +316,7 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp }); Fields = fields.ToArray(); SyncableFields = syncableFields.ToArray(); + SyncableFieldsCustomRollback = syncableFieldsWithCustomRollback.ToArray(); FieldsCount = Fields.Length; FieldsFlagsSize = (FieldsCount-1) / 8 + 1; LagCompensatedFields = lagCompensatedFields.ToArray(); @@ -316,9 +333,13 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp { field.PredictedOffset = predictedOffset; predictedOffset += field.IntSize; - ownedRollbackFields.Add(field); + + //singletons can't be owned + if (!IsSingleton) + ownedRollbackFields.Add(i); + if(field.Flags.HasFlagFast(SyncFlags.AlwaysRollback)) - remoteRollbackFields.Add(field); + remoteRollbackFields.Add(i); } else { @@ -339,8 +360,8 @@ public EntityClassData(EntityManager entityManager, ushort filterId, Type entTyp } else { - _dataCacheSize = InterpolatedFieldsSize * 2 + PredictedSize + historySize; - _historyStart = InterpolatedFieldsSize * 2 + PredictedSize; + _dataCacheSize = PredictedSize + historySize; + _historyStart = PredictedSize; } Type = entType; diff --git a/Assets/Plugins/LiteEntitySystem/Internal/EntityFieldInfo.cs b/Assets/Plugins/LiteEntitySystem/Internal/EntityFieldInfo.cs index 549b7ea..93819ad 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/EntityFieldInfo.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/EntityFieldInfo.cs @@ -1,4 +1,7 @@ -namespace LiteEntitySystem.Internal +using System; +using System.Runtime.CompilerServices; + +namespace LiteEntitySystem.Internal { internal enum FieldType { @@ -10,7 +13,6 @@ internal struct EntityFieldInfo { public readonly string Name; //used for debug public readonly ValueTypeProcessor TypeProcessor; - public readonly int Offset; public readonly int SyncableSyncVarOffset; public readonly uint Size; public readonly int IntSize; @@ -19,18 +21,24 @@ internal struct EntityFieldInfo public readonly bool IsPredicted; public MethodCallDelegate OnSync; + public BindOnChangeFlags OnSyncFlags; public int FixedOffset; public int PredictedOffset; + + /// + /// Direct field offset which for Entities - is SyncVar, and for SyncableField - SyncableField + /// + public readonly int Offset; //for value type - public EntityFieldInfo(string name, ValueTypeProcessor valueTypeProcessor, int offset, SyncVarFlags flags) : + public EntityFieldInfo(string name, ValueTypeProcessor valueTypeProcessor, int offset, SyncFlags flags) : this(name, valueTypeProcessor, offset, -1, flags, FieldType.SyncVar) { } //For syncable syncvar - public EntityFieldInfo(string name, ValueTypeProcessor valueTypeProcessor, int offset, int syncableSyncVarOffset, SyncVarFlags flags) : + public EntityFieldInfo(string name, ValueTypeProcessor valueTypeProcessor, int offset, int syncableSyncVarOffset, SyncFlags flags) : this(name, valueTypeProcessor, offset, syncableSyncVarOffset, flags, FieldType.SyncableSyncVar) { @@ -41,9 +49,10 @@ private EntityFieldInfo( ValueTypeProcessor valueTypeProcessor, int offset, int syncableSyncVarOffset, - SyncVarFlags flags, + SyncFlags flags, FieldType fieldType) { + OnSyncFlags = 0; Name = name; TypeProcessor = valueTypeProcessor; SyncableSyncVarOffset = syncableSyncVarOffset; @@ -54,49 +63,28 @@ private EntityFieldInfo( FixedOffset = 0; PredictedOffset = 0; OnSync = null; - Flags = flags?.Flags ?? SyncFlags.None; + Flags = flags; IsPredicted = Flags.HasFlagFast(SyncFlags.AlwaysRollback) || (!Flags.HasFlagFast(SyncFlags.OnlyForOtherPlayers) && !Flags.HasFlagFast(SyncFlags.NeverRollBack)); } - - public unsafe bool ReadField( - InternalEntity entity, - byte* rawData, - byte* predictedData, - byte* nextInterpDataPtr, - byte* prevInterpDataPtr) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public InternalBaseClass GetTargetObjectAndOffset(InternalEntity entity, out int offset) { - if (IsPredicted) - RefMagic.CopyBlock(predictedData + PredictedOffset, rawData, Size); if (FieldType == FieldType.SyncableSyncVar) { - var syncableField = RefMagic.RefFieldValue(entity, Offset); - TypeProcessor.SetFrom(syncableField, SyncableSyncVarOffset, rawData); + offset = SyncableSyncVarOffset; + return RefMagic.GetFieldValue(entity, Offset); } - else - { - if (Flags.HasFlagFast(SyncFlags.Interpolated)) - { - if(nextInterpDataPtr != null) - RefMagic.CopyBlock(nextInterpDataPtr + FixedOffset, rawData, Size); - if(prevInterpDataPtr != null) - RefMagic.CopyBlock(prevInterpDataPtr + FixedOffset, rawData, Size); - } - - if (OnSync != null) - { - if (TypeProcessor.SetFromAndSync(entity, Offset, rawData)) - return true; //create sync call - } - else - { - TypeProcessor.SetFrom(entity, Offset, rawData); - } - } - - return false; + offset = Offset; + return entity; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public InternalBaseClass GetTargetObject(InternalEntity entity) => + FieldType == FieldType.SyncableSyncVar + ? RefMagic.GetFieldValue(entity, Offset) + : entity; } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Internal/InternalBaseClass.cs b/Assets/Plugins/LiteEntitySystem/Internal/InternalBaseClass.cs index 3b0d9b0..e9f820b 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/InternalBaseClass.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/InternalBaseClass.cs @@ -1,7 +1,14 @@ namespace LiteEntitySystem.Internal { + /// + /// Base class for SyncableFields and Entities + /// public abstract class InternalBaseClass { + /// + /// Method for executing RPCs containing initial sync data that need to be sent after entity creation + /// to existing players or when new player connected + /// protected internal virtual void OnSyncRequested() { diff --git a/Assets/Plugins/LiteEntitySystem/Internal/InternalEntity.cs b/Assets/Plugins/LiteEntitySystem/Internal/InternalEntity.cs index 4f22cef..a5fadc8 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/InternalEntity.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/InternalEntity.cs @@ -4,11 +4,22 @@ namespace LiteEntitySystem.Internal { + public enum EntityState + { + New = 0, + Constructed = 1, + LateConstructed = 2, + Destroyed = 3, + Removed = 4 + } + public abstract class InternalEntity : InternalBaseClass, IComparable { [SyncVarFlags(SyncFlags.NeverRollBack)] internal SyncVar InternalOwnerId; + private EntityState _entityState; + internal byte[] IOBuffer; internal readonly int UpdateOrderNum; @@ -22,7 +33,6 @@ public abstract class InternalEntity : InternalBaseClass, IComparable public readonly ushort Id; - /// /// Entity manager @@ -39,12 +49,17 @@ public abstract class InternalEntity : InternalBaseClass, IComparable public bool IsClient => EntityManager.IsClient; + /// + /// Entity ClassId name + /// + public string ClassIdName => ClassData.ClassEnumName; + /// /// Entity version (for id reuse) /// public readonly byte Version; - internal EntityDataHeader DataHeader => new EntityDataHeader + internal EntityDataHeader DataHeader => new ( ClassId, Version, @@ -54,7 +69,7 @@ public abstract class InternalEntity : InternalBaseClass, IComparable /// Is entity is destroyed /// - public bool IsDestroyed { get; private set; } + public bool IsDestroyed => _entityState >= EntityState.Destroyed; /// /// Is entity local controlled @@ -103,12 +118,23 @@ public abstract class InternalEntity : InternalBaseClass, IComparable /// Is entity constructed (OnConstruct called) /// - public bool IsConstructed { get; internal set; } + public bool IsConstructed => _entityState >= EntityState.Constructed; /// /// Is entity released and not used after destroy. /// - public bool IsRemoved { get; internal set; } + public bool IsRemoved => _entityState == EntityState.Removed; + + /// + /// Entity state. New, Constructed, Destroyed, Removed + /// + public EntityState CreationState => _entityState; + + /// + /// Used to detect situation when base.RegisterRPC isn't called + /// + [ThreadStatic] + private static bool IsBaseRegisterRPCCalled; /// /// Destroy entity @@ -132,11 +158,42 @@ internal virtual void DestroyInternal() { if (IsDestroyed) return; - IsDestroyed = true; + if (_entityState == EntityState.Constructed) + { + //id destroyed before late construct - execute late construct first + LateConstructInternal(); + } + + _entityState = EntityState.Destroyed; EntityManager.OnEntityDestroyed(this); OnDestroy(); } + internal void ConstructInternal() + { + if (_entityState != EntityState.New) + Logger.LogError($"Error! Calling construct on not new entity: {this}. State: {_entityState}"); + + _entityState = EntityState.Constructed; + } + + internal void LateConstructInternal() + { + if (_entityState != EntityState.Constructed) + Logger.LogError($"Error! Calling late construct on not constructed entity: {this}. State: {_entityState}"); + + _entityState = EntityState.LateConstructed; + OnLateConstructed(); + } + + internal void Remove() + { + if (_entityState != EntityState.Destroyed) + Logger.LogError($"Error! Calling remove on not destroyed entity: {this}. State: {_entityState}"); + + _entityState = EntityState.Removed; + } + internal void SafeUpdate() { try @@ -191,7 +248,7 @@ protected internal virtual void OnConstructed() /// /// Called when entity constructed but at end of frame /// - protected internal virtual void OnLateConstructed() + protected virtual void OnLateConstructed() { } @@ -201,19 +258,15 @@ internal void RegisterRpcInternal() ref var classData = ref EntityManager.ClassDataDict[ClassId]; //setup field ids for BindOnChange and pass on server this for OnChangedEvent to StateSerializer - var onChangeTarget = EntityManager.IsServer && !IsLocal ? this : null; for (int i = 0; i < classData.FieldsCount; i++) { ref var field = ref classData.Fields[i]; - if (field.FieldType == FieldType.SyncVar) - { - field.TypeProcessor.InitSyncVar(this, field.Offset, onChangeTarget, (ushort)i); - } - else - { - var syncableField = RefMagic.RefFieldValue(this, field.Offset); - field.TypeProcessor.InitSyncVar(syncableField, field.SyncableSyncVarOffset, onChangeTarget, (ushort)i); - } + var target = field.GetTargetObjectAndOffset(this, out int offset); + + //init before SyncVar init because SyncVar can call OnChange + if (field.FieldType == FieldType.SyncableSyncVar) + RefMagic.GetFieldValue(this, field.Offset).Init(this, field.Flags); + field.TypeProcessor.InitSyncVar(target, offset, this, (ushort)i); } List rpcCache = null; @@ -224,14 +277,19 @@ internal void RegisterRpcInternal() RemoteCallPacket.InitReservedRPCs(rpcCache); var rpcRegistrator = new RPCRegistrator(rpcCache, classData.Fields); + IsBaseRegisterRPCCalled = false; RegisterRPC(ref rpcRegistrator); + if (!IsBaseRegisterRPCCalled) + { + Logger.LogError($"Error! You need always call to base.RegisterRPC in your overrides. Failed class: {ClassIdName}"); + } //Logger.Log($"RegisterRPCs for class: {classData.ClassId}"); } //setup id for later sync calls for (int i = 0; i < classData.SyncableFields.Length; i++) { ref var syncFieldInfo = ref classData.SyncableFields[i]; - var syncField = RefMagic.RefFieldValue(this, syncFieldInfo.Offset); + var syncField = RefMagic.GetFieldValue(this, syncFieldInfo.Offset); syncField.Init(this, syncFieldInfo.Flags); if (rpcCache == null) //classData.RemoteCallsClient != null { @@ -241,7 +299,7 @@ internal void RegisterRpcInternal() { syncField.RPCOffset = (ushort)rpcCache.Count; syncFieldInfo.RPCOffset = syncField.RPCOffset; - var syncablesRegistrator = new SyncableRPCRegistrator(syncFieldInfo.Offset, rpcCache); + var syncablesRegistrator = new SyncableRPCRegistrator(syncFieldInfo.Offset, rpcCache, classData.Fields); syncField.RegisterRPC(ref syncablesRegistrator); } } @@ -256,7 +314,7 @@ internal void RegisterRpcInternal() /// protected virtual void RegisterRPC(ref RPCRegistrator r) { - + IsBaseRegisterRPCCalled = true; } protected void ExecuteRPC(in RemoteCall rpc) diff --git a/Assets/Plugins/LiteEntitySystem/Internal/RemoteCallPacket.cs b/Assets/Plugins/LiteEntitySystem/Internal/RemoteCallPacket.cs index e5cbcad..03c3211 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/RemoteCallPacket.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/RemoteCallPacket.cs @@ -10,6 +10,16 @@ internal struct RPCHeader public ushort Tick; public ushort ByteCount; } + + internal enum InternalRPCType : ushort + { + New, + NewOwned, + Construct, + Destroy, + + Total + } internal sealed class RemoteCallPacket { @@ -18,17 +28,15 @@ internal sealed class RemoteCallPacket public NetPlayer OnlyForPlayer; public ExecuteFlags ExecuteFlags; - public unsafe int TotalSize => sizeof(RPCHeader) + Header.ByteCount; - - public const int ReserverdRPCsCount = 3; - public const ushort NewRPCId = 0; - public const ushort ConstructRPCId = 1; - public const ushort DestroyRPCId = 2; + public int TotalSize => RpcDeltaCompressor.MaxDeltaSize + Header.ByteCount; + + //can be static because doesnt use any buffers + private static DeltaCompressor RpcDeltaCompressor = new(Utils.SizeOfStruct()); public static void InitReservedRPCs(List rpcCache) { - for(int i = 0; i < ReserverdRPCsCount; i++) - rpcCache.Add(new RpcFieldInfo(-1, null)); + for(var i = InternalRPCType.New; i < InternalRPCType.Total; i++) + rpcCache.Add(new RpcFieldInfo(null)); } public bool AllowToSendForPlayer(byte forPlayerId, byte entityOwnerId) @@ -42,21 +50,15 @@ public bool AllowToSendForPlayer(byte forPlayerId, byte entityOwnerId) return false; } - - public unsafe void WriteTo(byte* resultData, ref int position) - { - *(RPCHeader*)(resultData + position) = Header; - fixed (byte* rpcData = Data) - RefMagic.CopyBlock(resultData + sizeof(RPCHeader) + position, rpcData, Header.ByteCount); - position += TotalSize; - } - public unsafe void WriteToDeltaCompressed(ref DeltaCompressor deltaCompressor, byte* resultData, ref int position, RPCHeader prevHeader) + public unsafe int WriteTo(byte* resultData, ref int position, ref RPCHeader prevHeader) { - int headerEncodedSize = deltaCompressor.Encode(ref prevHeader, ref Header, new Span(resultData + position, sizeof(RPCHeader))); + int headerEncodedSize = RpcDeltaCompressor.Encode(ref prevHeader, ref Header, new Span(resultData + position, RpcDeltaCompressor.MaxDeltaSize)); fixed (byte* rpcData = Data) RefMagic.CopyBlock(resultData + headerEncodedSize + position, rpcData, Header.ByteCount); position += headerEncodedSize + Header.ByteCount; + prevHeader = Header; + return headerEncodedSize + Header.ByteCount; } public void Init(NetPlayer targetPlayer, InternalEntity entity, ushort tick, ushort byteCount, ushort rpcId, ExecuteFlags executeFlags) diff --git a/Assets/Plugins/LiteEntitySystem/Internal/ServerStateData.cs b/Assets/Plugins/LiteEntitySystem/Internal/ServerStateData.cs index 8a9db16..55b581e 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/ServerStateData.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/ServerStateData.cs @@ -5,24 +5,13 @@ namespace LiteEntitySystem.Internal { - internal readonly struct InterpolatedCache + internal enum RPCExecuteMode { - public readonly InternalEntity Entity; - public readonly int FieldOffset; - public readonly int FieldFixedOffset; - public readonly ValueTypeProcessor TypeProcessor; - public readonly int StateReaderOffset; - - public InterpolatedCache(InternalEntity entity, ref EntityFieldInfo field, int offset) - { - Entity = entity; - FieldOffset = field.Offset; - FieldFixedOffset = field.FixedOffset; - TypeProcessor = field.TypeProcessor; - StateReaderOffset = offset; - } + FirstSync, + BetweenStates, + OnNextState } - + internal readonly struct EntityDataCache { public readonly ushort EntityId; @@ -35,6 +24,20 @@ public EntityDataCache(ushort entityId, int offset) } } + internal readonly struct RemoteCallInfo + { + public readonly RPCHeader Header; + public readonly int DataOffset; + public readonly bool ExecuteOnNextState; + + public RemoteCallInfo(RPCHeader header, int dataOffset, bool executeOnNextState) + { + Header = header; + DataOffset = dataOffset; + ExecuteOnNextState = executeOnNextState; + } + } + internal class ServerStateData { private const int DefaultCacheSize = 32; @@ -46,9 +49,6 @@ internal class ServerStateData public ushort LastReceivedTick; public byte BufferedInputsCount; - private int _interpolatedCachesCount; - private InterpolatedCache[] _interpolatedCaches = new InterpolatedCache[DefaultCacheSize]; - private int _totalPartsCount; private int _receivedPartsCount; private byte _maxReceivedPart; @@ -57,48 +57,57 @@ internal class ServerStateData private int _dataOffset; private int _dataSize; - private int _rpcReadPos; - private int _rpcEndPos; private EntityDataCache[] _nullEntitiesData = new EntityDataCache[DefaultCacheSize]; + private RemoteCallInfo[] _remoteCallInfos = new RemoteCallInfo[DefaultCacheSize]; + private int _nullEntitiesCount; + private int _remoteCallsCount; + private int _rpcIndex; public int DataOffset => _dataOffset; public int DataSize => _dataSize; - private readonly HashSet _syncablesSet = new(); + [ThreadStatic] + private static HashSet SyncablesBufferSet; + + [ThreadStatic] + private static HashSet LocalEntitiesBuffer; - public unsafe void GetDiagnosticData( - InternalEntity[] entityDict, - EntityClassData[] classDatas, - Dictionary diagnosticDataDict) + private DeltaCompressor _rpcDeltaCompressor = new (Utils.SizeOfStruct()); + + private readonly ClientEntityManager _entityManager; + + public ServerStateData(ClientEntityManager entityManager) { - fixed (byte* rawData = Data) + _entityManager = entityManager; + } + + public unsafe void GetDiagnosticData(Dictionary diagnosticDataDict) + { + for (int i = 0; i < _remoteCallsCount; i++) { - int readPos = 0; - while (readPos < _rpcEndPos) - { - if (_rpcEndPos - readPos < sizeof(RPCHeader)) - break; - var header = *(RPCHeader*)(rawData + readPos); - int rpcSize = header.ByteCount + sizeof(RPCHeader); - readPos += rpcSize; + var header = _remoteCallInfos[i].Header; + int rpcSize = header.ByteCount + sizeof(RPCHeader); + int dictId = ushort.MaxValue + header.Id; - int dictId = ushort.MaxValue + header.Id; - - if(!diagnosticDataDict.TryGetValue(dictId, out LESDiagnosticDataEntry entry)) - { - entry = new LESDiagnosticDataEntry { IsRPC = true, Count = 1, Name = $"{header.Id}", Size = rpcSize }; - } - else - { - entry.Count++; - entry.Size += rpcSize; - } - diagnosticDataDict[dictId] = entry; + if (!diagnosticDataDict.TryGetValue(dictId, out LESDiagnosticDataEntry entry)) + { + entry = new LESDiagnosticDataEntry + { IsRPC = true, Count = 1, Name = $"{header.Id}", Size = rpcSize }; } + else + { + entry.Count++; + entry.Size += rpcSize; + } + + diagnosticDataDict[dictId] = entry; } + var entityDict = _entityManager.EntitiesDict; + var classDatas = _entityManager.ClassDataDict; + for (int bytesRead = _dataOffset; bytesRead < _dataOffset + _dataSize;) { int initialReaderPosition = bytesRead; @@ -112,22 +121,6 @@ public unsafe void GetDiagnosticData( ? classDatas[classId].ClassEnumName : "Unknown"; - /* - if (classId >= 0) - { - ref var classData = ref classDatas[classId]; - int entityFieldsOffset = initialReaderPosition + StateSerializer.DiffHeaderSize; - //interpolated fields goes first so can skip some checks - for (int i = 0; i < classData.FieldsCount; i++) - { - if (!Utils.IsBitSet(Data, entityFieldsOffset, i)) - continue; - ref var field = ref classData.Fields[i]; - Logger.Log($"{field.Name}({i}) - {field.Size}"); - } - } - */ - if(!diagnosticDataDict.TryGetValue(classId, out LESDiagnosticDataEntry entry)) { entry = new LESDiagnosticDataEntry { Count = 1, Name = name, Size = totalSize }; @@ -169,7 +162,7 @@ public void Preload(InternalEntity[] entityDict) } } - private void PreloadInterpolation(InternalEntity entity, int offset) + private unsafe void PreloadInterpolation(InternalEntity entity, int offset) { if (!entity.IsRemoteControlled) return; @@ -180,26 +173,27 @@ private void PreloadInterpolation(InternalEntity entity, int offset) int entityFieldsOffset = offset + StateSerializer.DiffHeaderSize; int stateReaderOffset = entityFieldsOffset + classData.FieldsFlagsSize; - - //preload interpolation info - Utils.ResizeIfFull(ref _interpolatedCaches, _interpolatedCachesCount + classData.InterpolatedCount); //interpolated fields goes first so can skip some checks - for (int i = 0; i < classData.InterpolatedCount; i++) + fixed (byte* rawData = Data) { - if (!Utils.IsBitSet(Data, entityFieldsOffset, i)) - continue; - ref var field = ref classData.Fields[i]; - _interpolatedCaches[_interpolatedCachesCount++] = new InterpolatedCache(entity, ref field, stateReaderOffset); - stateReaderOffset += field.IntSize; + for (int i = 0; i < classData.InterpolatedCount; i++) + { + if (!Utils.IsBitSet(Data, entityFieldsOffset, i)) + continue; + ref var field = ref classData.Fields[i]; + var target = field.GetTargetObjectAndOffset(entity, out int fieldOffset); + field.TypeProcessor.SetInterpValue(target, fieldOffset, rawData + stateReaderOffset); + stateReaderOffset += field.IntSize; + } } } - public unsafe void RemoteInterpolation(InternalEntity[] entityDict, float logicLerpMsec) + public void PreloadInterpolationForNewEntities() { for (int i = 0; i < _nullEntitiesCount; i++) { - var entity = entityDict[_nullEntitiesData[i].EntityId]; + var entity = _entityManager.EntitiesDict[_nullEntitiesData[i].EntityId]; if (entity == null) continue; @@ -212,92 +206,95 @@ public unsafe void RemoteInterpolation(InternalEntity[] entityDict, float logicL _nullEntitiesData[i] = _nullEntitiesData[_nullEntitiesCount]; i--; } - - for(int i = 0; i < _interpolatedCachesCount; i++) - { - ref var interpolatedCache = ref _interpolatedCaches[i]; - fixed (byte* initialDataPtr = interpolatedCache.Entity.ClassData.ClientInterpolatedNextData(interpolatedCache.Entity), nextDataPtr = Data) - interpolatedCache.TypeProcessor.SetInterpolation( - interpolatedCache.Entity, - interpolatedCache.FieldOffset, - initialDataPtr + interpolatedCache.FieldFixedOffset, - nextDataPtr + interpolatedCache.StateReaderOffset, - logicLerpMsec); - } } - public unsafe void ExecuteRpcs(ClientEntityManager entityManager, ushort minimalTick, bool firstSync) + public unsafe void ExecuteRpcs(ushort minimalTick, RPCExecuteMode executeMode) { - _syncablesSet.Clear(); + if (SyncablesBufferSet == null) + SyncablesBufferSet = new HashSet(); + else + SyncablesBufferSet.Clear(); + //if(_remoteCallsCount > 0) // Logger.Log($"Executing rpcs (ST: {Tick}) for tick: {entityManager.ServerTick}, Min: {minimalTick}, Count: {_remoteCallsCount}"); + + int initialRpcIndex = _rpcIndex; + if (executeMode == RPCExecuteMode.OnNextState) + _rpcIndex = 0; + + _entityManager.IsExecutingRPC = true; + fixed (byte* rawData = Data) { - while (_rpcReadPos < _rpcEndPos) + for(;_rpcIndex < _remoteCallsCount; _rpcIndex++) { - if (_rpcEndPos - _rpcReadPos < sizeof(RPCHeader)) - { - Logger.LogError("Broken rpcs sizes?"); - break; - } + var remoteCallInfo = _remoteCallInfos[_rpcIndex]; + var header = remoteCallInfo.Header; - var header = *(RPCHeader*)(rawData + _rpcReadPos); - if (!firstSync) + if (executeMode != RPCExecuteMode.FirstSync) { - if (Utils.SequenceDiff(header.Tick, entityManager.ServerTick) > 0) + if (executeMode == RPCExecuteMode.BetweenStates) { - //Logger.Log($"Skip rpc. Entity: {header.EntityId}. Tick {header.Tick} > ServerTick: {entityManager.ServerTick}. Id: {header.Id}."); - break; + if (remoteCallInfo.ExecuteOnNextState) + { + continue; + } + if (Utils.SequenceDiff(header.Tick, _entityManager.ServerTick) > 0) + { + //Logger.Log($"Skip rpc. Entity: {header.EntityId}. Tick {header.Tick} > ServerTick: {entityManager.ServerTick}. Id: {header.Id}."); + break; + } + } + //skip executed inside interpolation + else if (executeMode == RPCExecuteMode.OnNextState && remoteCallInfo.ExecuteOnNextState == false && _rpcIndex < initialRpcIndex) + { + //Logger.Log($"Skip rpc. Entity: {header.EntityId}. _rpcIndex {_rpcIndex} < initialRpcIndex: {initialRpcIndex}. Id: {header.Id}."); + continue; } if (Utils.SequenceDiff(header.Tick, minimalTick) <= 0) { - _rpcReadPos += header.ByteCount + sizeof(RPCHeader); //Logger.Log($"Skip rpc. Entity: {header.EntityId}. Tick {header.Tick} <= MinimalTick: {minimalTick}. Id: {header.Id}. StateATick: {entityManager.RawServerTick}. StateBTick: {entityManager.RawTargetServerTick}"); continue; } } - - int rpcDataStart = _rpcReadPos + sizeof(RPCHeader); - _rpcReadPos += header.ByteCount + sizeof(RPCHeader); + if (header.Id == (ushort)InternalRPCType.New || + header.Id == (ushort)InternalRPCType.NewOwned) + { + _entityManager.ReadNewRPC(header.EntityId, rawData + remoteCallInfo.DataOffset, remoteCallInfo.Header.ByteCount); + continue; + } + //Logger.Log($"Executing rpc. Entity: {header.EntityId}. Tick {header.Tick}. Id: {header.Id}"); - var entity = entityManager.EntitiesDict[header.EntityId]; + var entity = _entityManager.EntitiesDict[header.EntityId]; if (entity == null) { - if (header.Id == RemoteCallPacket.NewRPCId) - { - entityManager.ReadNewRPC(header.EntityId, rawData + rpcDataStart); - continue; - } - Logger.LogError($"Entity is null: {header.EntityId}. RPCId: {header.Id}"); continue; } - entityManager.CurrentRPCTick = header.Tick; - - var rpcFieldInfo = entityManager.ClassDataDict[entity.ClassId].RemoteCallsClient[header.Id]; + var rpcFieldInfo = _entityManager.ClassDataDict[entity.ClassId].RemoteCallsClient[header.Id]; if (rpcFieldInfo.SyncableOffset == -1) { try { - if (header.Id == RemoteCallPacket.NewRPCId) - { - entityManager.ReadNewRPC(header.EntityId, rawData + rpcDataStart); - } - else if (header.Id == RemoteCallPacket.ConstructRPCId) - { - //Logger.Log($"ConstructRPC for entity: {header.EntityId}, RpcReadPos: {_rpcReadPos}, Tick: {header.Tick}"); - entityManager.ReadConstructRPC(header.EntityId, rawData, rpcDataStart); - } - else if (header.Id == RemoteCallPacket.DestroyRPCId) - { - entity.DestroyInternal(); - } - else + switch ((InternalRPCType)header.Id) { - rpcFieldInfo.Method(entity, new ReadOnlySpan(rawData + rpcDataStart, header.ByteCount)); + case InternalRPCType.Construct: + //Logger.Log($"ConstructRPC for entity: {header.EntityId}, Size: {header.ByteCount}, RpcReadPos: {remoteCallInfo.DataOffset}, Tick: {header.Tick}"); + //Logger.Log($"CRPCData: {Utils.BytesToHexString(new ReadOnlySpan(rawData + remoteCallInfo.DataOffset, header.ByteCount))}"); + _entityManager.ReadConstructRPC(header.EntityId, rawData, remoteCallInfo.DataOffset); + break; + + case InternalRPCType.Destroy: + //Logger.Log($"DestroyRPC for {header.EntityId}"); + entity.DestroyInternal(); + break; + + default: + rpcFieldInfo.Method(entity, new ReadOnlySpan(rawData + remoteCallInfo.DataOffset, header.ByteCount)); + break; } } catch (Exception e) @@ -307,14 +304,13 @@ public unsafe void ExecuteRpcs(ClientEntityManager entityManager, ushort minimal } else { - var syncableField = RefMagic.RefFieldValue(entity, rpcFieldInfo.SyncableOffset); - if (_syncablesSet.Add(syncableField)) - { - syncableField.BeforeReadRPC(); - } + var syncableField = RefMagic.GetFieldValue(entity, rpcFieldInfo.SyncableOffset); + if (syncableField is SyncableFieldCustomRollback sf && SyncablesBufferSet.Add(sf)) + sf.BeforeReadRPC(); + try { - rpcFieldInfo.Method(syncableField, new ReadOnlySpan(rawData + rpcDataStart, header.ByteCount)); + rpcFieldInfo.Method(syncableField, new ReadOnlySpan(rawData + remoteCallInfo.DataOffset, header.ByteCount)); } catch (Exception e) { @@ -323,22 +319,75 @@ public unsafe void ExecuteRpcs(ClientEntityManager entityManager, ushort minimal } } } - foreach (var syncableField in _syncablesSet) + + foreach (var syncableField in SyncablesBufferSet) syncableField.AfterReadRPC(); + + _entityManager.IsExecutingRPC = false; } public void Reset(ushort tick) { Tick = tick; _receivedParts.SetAll(false); - _interpolatedCachesCount = 0; _maxReceivedPart = 0; _receivedPartsCount = 0; _totalPartsCount = 0; Size = 0; _partMtu = 0; _nullEntitiesCount = 0; - _rpcReadPos = 0; + } + + private unsafe void PreloadRPCs(int rpcsSize) + { + if (LocalEntitiesBuffer == null) + LocalEntitiesBuffer = new HashSet(); + else + LocalEntitiesBuffer.Clear(); + + //parse and preload rpc infos + int rpcReadPos = 0; + _rpcIndex = 0; + _remoteCallsCount = 0; + _rpcDeltaCompressor.Init(); + while (rpcReadPos < rpcsSize) + { + if (rpcsSize - rpcReadPos < _rpcDeltaCompressor.MinDeltaSize) + { + Logger.LogError("Broken rpcs sizes?"); + break; + } + + RPCHeader header = new(); + int encodedHeaderSize = _rpcDeltaCompressor.Decode( + new ReadOnlySpan(Data, rpcReadPos, rpcsSize - rpcReadPos), + new Span(&header, sizeof(RPCHeader))); + + //Logger.Log($"ReadRPC: EID: {header.EntityId}, RPCID: {header.Id}, BC: {header.ByteCount}"); + + rpcReadPos += encodedHeaderSize; + + bool executeOnNextState; + if (LocalEntitiesBuffer.Contains(header.EntityId)) + { + executeOnNextState = true; + } + else if (header.Id == (ushort)InternalRPCType.NewOwned || + _entityManager.EntitiesDict[header.EntityId]?.InternalOwnerId.Value == _entityManager.InternalPlayerId) + { + LocalEntitiesBuffer.Add(header.EntityId); + executeOnNextState = true; + } + else + { + executeOnNextState = false; + } + + Utils.ResizeIfFull(ref _remoteCallInfos, _remoteCallsCount); + _remoteCallInfos[_remoteCallsCount++] = new RemoteCallInfo(header, rpcReadPos, executeOnNextState); + + rpcReadPos += header.ByteCount; + } } public unsafe bool ReadBaseline(BaselineDataHeader header, byte* rawData, int fullSize) @@ -348,8 +397,6 @@ public unsafe bool ReadBaseline(BaselineDataHeader header, byte* rawData, int fu Data = new byte[header.OriginalLength]; _dataOffset = header.EventsSize; _dataSize = header.OriginalLength - header.EventsSize; - _rpcEndPos = header.EventsSize; - _rpcReadPos = 0; fixed (byte* stateData = Data) { int decodedBytes = LZ4Codec.Decode( @@ -363,6 +410,7 @@ public unsafe bool ReadBaseline(BaselineDataHeader header, byte* rawData, int fu return false; } } + PreloadRPCs(header.EventsSize); return true; } @@ -383,7 +431,6 @@ public unsafe bool ReadPart(DiffPartHeader partHeader, byte* rawData, int partSi ProcessedTick = lastPartData.LastProcessedTick; BufferedInputsCount = lastPartData.BufferedInputsCount; _dataOffset = lastPartData.EventsSize; - _rpcEndPos = lastPartData.EventsSize; //Logger.Log($"TPC: {partHeader.Part} {_partMtu}, LastReceivedTick: {LastReceivedTick}, LastProcessedTick: {ProcessedTick}"); } partSize -= sizeof(DiffPartHeader); @@ -402,6 +449,8 @@ public unsafe bool ReadPart(DiffPartHeader partHeader, byte* rawData, int partSi if (_receivedPartsCount == _totalPartsCount) { _dataSize = Size - _dataOffset; + //rpc before data - so data offset equals size + PreloadRPCs(_dataOffset); return true; } return false; diff --git a/Assets/Plugins/LiteEntitySystem/Internal/StateSerializer.cs b/Assets/Plugins/LiteEntitySystem/Internal/StateSerializer.cs index 4ce8e74..9d54726 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/StateSerializer.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/StateSerializer.cs @@ -4,7 +4,7 @@ namespace LiteEntitySystem.Internal { internal struct StateSerializer { - public static readonly int HeaderSize = Utils.SizeOfStruct(); + public static readonly ushort HeaderSize = (ushort)Utils.SizeOfStruct(); public const int DiffHeaderSize = 4; public const int MaxStateSize = 32767; //half of ushort @@ -19,12 +19,18 @@ internal struct StateSerializer private ushort[] _fieldChangeTicks; private ushort _versionChangedTick; private uint _fullDataSize; + private DateTime _lastRefreshedTime; + private int _secondsBetweenRefresh; public byte NextVersion; public ushort LastChangedTick; - private DateTime _lastRefreshedTime; - private int _secondsBetweenRefresh; + /// + /// Get maximum delta size + /// + /// FullDataSize + ushort(size of data) + ushort(entityId) + _fieldFlagsSize + public int MaximumSize => + _entity == null ? 0 : (int)_fullDataSize + sizeof(ushort) + sizeof(ushort) + _fieldsFlagsSize; public void AllocateMemory(ref EntityClassData classData, byte[] ioBuffer) { @@ -78,7 +84,7 @@ public void MarkFieldsChanged(ushort minimalTick, ushort tick, SyncFlags onlyWit public void MarkChanged(ushort minimalTick, ushort tick) { LastChangedTick = tick; - //refresh every X seconds to prevent wrap-arounded bugs + //refresh every X seconds to prevent wrap-around bugs DateTime currentTime = DateTime.UtcNow; if ((currentTime - _lastRefreshedTime).TotalSeconds > _secondsBetweenRefresh) { @@ -90,29 +96,99 @@ public void MarkChanged(ushort minimalTick, ushort tick) } } - public int GetMaximumSize() => - _entity == null ? 0 : (int)_fullDataSize + sizeof(ushort) + sizeof(ushort) + _fieldsFlagsSize; - - public void MakeNewRPC() + public RemoteCallPacket MakeNewRPC() { - _entity.ServerManager.AddRemoteCall( + //add rpc + return _entity.ServerManager.AddRemoteCall( _entity, - new ReadOnlySpan(_latestEntityData, 0, HeaderSize), - RemoteCallPacket.NewRPCId, + (ushort)InternalRPCType.New, ExecuteFlags.SendToAll); } + + public unsafe void RefreshNewRPC(RemoteCallPacket packet) + { + //skip cases when no packet generated + if (packet == null) + return; + + Utils.ResizeOrCreate(ref packet.Data, MaximumSize); + + fixed (byte* lastEntityData = _latestEntityData, resultData = packet.Data) + { + //copy header + RefMagic.CopyBlock(resultData, lastEntityData, HeaderSize); + //make diff between default data + byte* entityDataAfterHeader = lastEntityData + HeaderSize; + + // -1 for cycle + int writePosition = HeaderSize + _fieldsFlagsSize; + byte* fields = resultData + HeaderSize - 1; + int readPosition = 0; + + //write fields + for (int i = 0; i < _fieldsCount; i++) + { + if (i % 8 == 0) //reset next byte each 8 bits + *++fields = 0; + + ref var field = ref _fields[i]; + + /* + if (((field.Flags & SyncFlags.OnlyForOwner) != 0 && !isOwned) || + ((field.Flags & SyncFlags.OnlyForOtherPlayers) != 0 && isOwned)) + { + //Logger.Log($"SkipSync: {field.Name}, isOwned: {isOwned}"); + continue; + } + + + if(!isOwned && SyncGroupUtils.IsSyncVarDisabled(enabledSyncGroups, field.Flags)) + { + //IgnoreDiffSyncSettings + continue; + } + */ + //compare with 0 + if (Utils.IsZero(lastEntityData + HeaderSize + readPosition, field.IntSize)) + { + readPosition += field.IntSize; + continue; + } + readPosition += field.IntSize; + + *fields |= (byte)(1 << i % 8); + //Logger.Log($"WriteNewChanges. Entity: {_entity}, {field.Name}"); + + RefMagic.CopyBlock(resultData + writePosition, entityDataAfterHeader + field.FixedOffset, field.Size); + writePosition += field.IntSize; + } + + if (writePosition > MaxStateSize) + { + Logger.LogError($"Entity {_entity.Id}, Class: {_entity.ClassId} state size is more than: {MaxStateSize}"); + writePosition = HeaderSize; + } + + //update info after resize for correct buffer allocation + int prevTotalSize = packet.TotalSize; + packet.Header.ByteCount = (ushort)writePosition; + _entity.ServerManager.NotifyRPCResized(prevTotalSize, packet.TotalSize); + + //if (writePosition > HeaderSize + _fieldsFlagsSize) + // Logger.Log($"NewRPC bytes (server) eid:{_entity.Id} cls:{_entity.ClassId} len:{writePosition} data:{Utils.BytesToHexString(new ReadOnlySpan(resultData, writePosition))}"); + } + } //refresh construct rpc with latest values (old behaviour) - public unsafe void RefreshConstructedRPC(RemoteCallPacket packet, NetPlayer player) + public unsafe void RefreshConstructedRPC(RemoteCallPacket packet) { - RefreshSyncGroupsVariable(player); fixed (byte* sourceData = _latestEntityData, rawData = packet.Data) { - RefMagic.CopyBlock(rawData, sourceData + HeaderSize, (uint)(_fullDataSize - HeaderSize)); + RefMagic.CopyBlock(rawData, sourceData + HeaderSize, _fullDataSize - HeaderSize); } } - - private SyncGroup RefreshSyncGroupsVariable(NetPlayer player) + + public SyncGroup RefreshSyncGroupsVariable(NetPlayer player, Span target) { SyncGroup enabledGroups = SyncGroup.All; if (_entity is EntityLogic el) @@ -127,7 +203,7 @@ private SyncGroup RefreshSyncGroupsVariable(NetPlayer player) //if no data it "never" changed _fieldChangeTicks[el.IsSyncEnabledFieldId] = _versionChangedTick; } - _latestEntityData[HeaderSize + _fields[el.IsSyncEnabledFieldId].FixedOffset] = (byte)enabledGroups; + target[_fields[el.IsSyncEnabledFieldId].FixedOffset] = (byte)enabledGroups; } return enabledGroups; @@ -140,8 +216,7 @@ public void MakeConstructedRPC(NetPlayer player) { var syncableFields = _entity.ClassData.SyncableFields; for (int i = 0; i < syncableFields.Length; i++) - RefMagic.RefFieldValue(_entity, syncableFields[i].Offset) - .OnSyncRequested(); + RefMagic.GetFieldValue(_entity, syncableFields[i].Offset).OnSyncRequested(); _entity.OnSyncRequested(); } catch (Exception e) @@ -150,14 +225,16 @@ public void MakeConstructedRPC(NetPlayer player) } //it can be null on entity creation + var entityDataSpan = new Span(_latestEntityData, HeaderSize, (int)(_fullDataSize - HeaderSize)); + if(player != null) - RefreshSyncGroupsVariable(player); + RefreshSyncGroupsVariable(player, entityDataSpan); //actual on constructed rpc _entity.ServerManager.AddRemoteCall( _entity, - new ReadOnlySpan(_latestEntityData, HeaderSize, (int)(_fullDataSize - HeaderSize)), - RemoteCallPacket.ConstructRPCId, + (ReadOnlySpan)entityDataSpan, + (ushort)InternalRPCType.Construct, ExecuteFlags.SendToAll); //Logger.Log($"Added constructed RPC: {_entity}"); } @@ -168,7 +245,7 @@ public void MakeDestroyedRPC(ushort tick) LastChangedTick = tick; _entity.ServerManager.AddRemoteCall( _entity, - RemoteCallPacket.DestroyRPCId, + (ushort)InternalRPCType.Destroy, ExecuteFlags.SendToAll); } @@ -224,7 +301,7 @@ public unsafe bool MakeDiff(NetPlayer player, ushort minimalTick, byte* resultDa : player.CurrentServerTick; //overwrite IsSyncEnabled for each player - SyncGroup enabledSyncGroups = RefreshSyncGroupsVariable(player); + SyncGroup enabledSyncGroups = RefreshSyncGroupsVariable(player, new Span(_latestEntityData, HeaderSize, (int)(_fullDataSize - HeaderSize))); fixed (byte* lastEntityData = _latestEntityData) //make diff { @@ -240,17 +317,8 @@ public unsafe bool MakeDiff(NetPlayer player, ushort minimalTick, byte* resultDa //write fields for (int i = 0; i < _fieldsCount; i++) { - if (i % 8 == 0) - { - fields++; - *fields = 0; - } - - //skip very old - if (Utils.SequenceDiff(_fieldChangeTicks[i], minimalTick) <= 0) - { - continue; - } + if (i % 8 == 0) //reset next byte each 8 bits + *++fields = 0; //not actual if (Utils.SequenceDiff(_fieldChangeTicks[i], compareToTick) <= 0) diff --git a/Assets/Plugins/LiteEntitySystem/Internal/ValueTypeProcessor.cs b/Assets/Plugins/LiteEntitySystem/Internal/ValueTypeProcessor.cs index a15a9a9..899fac2 100644 --- a/Assets/Plugins/LiteEntitySystem/Internal/ValueTypeProcessor.cs +++ b/Assets/Plugins/LiteEntitySystem/Internal/ValueTypeProcessor.cs @@ -16,8 +16,11 @@ internal abstract unsafe class ValueTypeProcessor internal abstract void InitSyncVar(InternalBaseClass obj, int offset, InternalEntity entity, ushort fieldId); internal abstract void SetFrom(InternalBaseClass obj, int offset, byte* data); internal abstract bool SetFromAndSync(InternalBaseClass obj, int offset, byte* data); + internal abstract void SetFromAndSync(InternalBaseClass obj, int offset, byte* data, MethodCallDelegate onSyncDelegate); + internal abstract void SetInterpValue(InternalBaseClass obj, int offset, byte* data); + internal abstract void SetInterpValueFromCurrentValue(InternalBaseClass obj, int offset); internal abstract void WriteTo(InternalBaseClass obj, int offset, byte* data); - internal abstract void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer); + internal abstract void CopyFrom(InternalBaseClass toObj, InternalBaseClass fromObj, int offset); internal abstract void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime); internal abstract int GetHashCode(InternalBaseClass obj, int offset); internal abstract string ToString(InternalBaseClass obj, int offset); @@ -27,100 +30,95 @@ internal unsafe class ValueTypeProcessor : ValueTypeProcessor where T : unman { public ValueTypeProcessor() : base(sizeof(T)) { } - internal override void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - throw new Exception($"This type: {typeof(T)} can't be interpolated"); + internal virtual T GetInterpolatedValue(T prev, T current, float t) => current; - internal override void InitSyncVar(InternalBaseClass obj, int offset, InternalEntity entity, ushort fieldId) => - RefMagic.RefFieldValue>(obj, offset).Init(entity, fieldId); - - internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) + internal sealed override void InitSyncVar(InternalBaseClass obj, int offset, InternalEntity entity, ushort fieldId) { - ref var a = ref RefMagic.RefFieldValue>(obj, offset); - *(T*)tempHistory = a; - a.SetDirect(*(T*)historyA); + var sv = RefMagic.GetFieldValue>(obj, offset); + sv.Init(entity, fieldId); + RefMagic.SetFieldValue(obj, offset, sv); } + + internal override void CopyFrom(InternalBaseClass toObj, InternalBaseClass fromObj, int offset) => + RefMagic.SyncVarSetDirect>(toObj, offset, RefMagic.GetFieldValue>(fromObj, offset).Value); + + internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) => + RefMagic.SyncVarSetDirectAndStorePrev>(obj, offset, *(T*)historyA, out *(T*)tempHistory); - internal override void SetFrom(InternalBaseClass obj, int offset, byte* data) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(*(T*)data); + internal sealed override void SetFrom(InternalBaseClass obj, int offset, byte* data) => + RefMagic.SyncVarSetDirect>(obj, offset, *(T*)data); - internal override bool SetFromAndSync(InternalBaseClass obj, int offset, byte* data) => - RefMagic.RefFieldValue>(obj, offset).SetFromAndSync(data); + internal sealed override bool SetFromAndSync(InternalBaseClass obj, int offset, byte* data) => + RefMagic.SyncVarSetFromAndSync>(obj, offset, ref *(T*)data); - internal override void WriteTo(InternalBaseClass obj, int offset, byte* data) => - *(T*)data = RefMagic.RefFieldValue>(obj, offset); + internal sealed override void SetFromAndSync(InternalBaseClass obj, int offset, byte* data, MethodCallDelegate onSyncDelegate) + { + var tempData = *(T*)data; + if(RefMagic.SyncVarSetFromAndSync>(obj, offset, ref tempData)) + onSyncDelegate(obj, new ReadOnlySpan(&tempData, Size)); + } - internal override int GetHashCode(InternalBaseClass obj, int offset) => - RefMagic.RefFieldValue>(obj, offset).GetHashCode(); + internal sealed override void SetInterpValue(InternalBaseClass obj, int offset, byte* data) => + RefMagic.SyncVarSetInterp>(obj, offset, *(T*)data); - internal override string ToString(InternalBaseClass obj, int offset) => - RefMagic.RefFieldValue>(obj, offset).ToString(); + internal sealed override void SetInterpValueFromCurrentValue(InternalBaseClass obj, int offset) => + RefMagic.SyncVarSetInterpFromCurrent>(obj, offset); + + internal sealed override void WriteTo(InternalBaseClass obj, int offset, byte* data) => + *(T*)data = RefMagic.GetFieldValue>(obj, offset); + + internal sealed override int GetHashCode(InternalBaseClass obj, int offset) => + RefMagic.GetFieldValue>(obj, offset).GetHashCode(); + + internal sealed override string ToString(InternalBaseClass obj, int offset) => + RefMagic.GetFieldValue>(obj, offset).ToString(); } internal class ValueTypeProcessorInt : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(int*)prev, *(int*)current, fTimer)); + internal override int GetInterpolatedValue(int prev, int current, float t) => Utils.Lerp(prev, current, t); - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) - { - ref var a = ref RefMagic.RefFieldValue>(obj, offset); - *(int*)tempHistory = a; - a.SetDirect(Utils.Lerp(*(int*)historyA, *(int*)historyB, lerpTime)); - } + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) => + RefMagic.SyncVarSetDirectAndStorePrev>(obj, offset, + Utils.Lerp(*(int*)historyA, *(int*)historyB, lerpTime), out *(int*)tempHistory); } internal class ValueTypeProcessorLong : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(long*)prev, *(long*)current, fTimer)); + internal override long GetInterpolatedValue(long prev, long current, float t) => Utils.Lerp(prev, current, t); - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) - { - ref var a = ref RefMagic.RefFieldValue>(obj, offset); - *(long*)tempHistory = a; - a.SetDirect(Utils.Lerp(*(long*)historyA, *(long*)historyB, lerpTime)); - } + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) => + RefMagic.SyncVarSetDirectAndStorePrev>(obj, offset, + Utils.Lerp(*(long*)historyA, *(long*)historyB, lerpTime), out *(long*)tempHistory); } internal class ValueTypeProcessorFloat : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(float*)prev, *(float*)current, fTimer)); + internal override float GetInterpolatedValue(float prev, float current, float t) => Utils.Lerp(prev, current, t); - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) - { - ref var a = ref RefMagic.RefFieldValue>(obj, offset); - *(float*)tempHistory = a; - a.SetDirect(Utils.Lerp(*(float*)historyA, *(float*)historyB, lerpTime)); - } + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) => + RefMagic.SyncVarSetDirectAndStorePrev>(obj, offset, + Utils.Lerp(*(float*)historyA, *(float*)historyB, lerpTime), out *(float*)tempHistory); } internal class ValueTypeProcessorDouble : ValueTypeProcessor { - internal override unsafe void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(Utils.Lerp(*(double*)prev, *(double*)current, fTimer)); + internal override double GetInterpolatedValue(double prev, double current, float t) => Utils.Lerp(prev, current, t); - internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) - { - ref var a = ref RefMagic.RefFieldValue>(obj, offset); - *(double*)tempHistory = a; - a.SetDirect(Utils.Lerp(*(double*)historyA, *(double*)historyB, lerpTime)); - } + internal override unsafe void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) => + RefMagic.SyncVarSetDirectAndStorePrev>(obj, offset, + Utils.Lerp(*(double*)historyA, *(double*)historyB, lerpTime), out *(double*)tempHistory); } internal unsafe class UserTypeProcessor : ValueTypeProcessor where T : unmanaged { private readonly InterpolatorDelegateWithReturn _interpDelegate; - internal override void SetInterpolation(InternalBaseClass obj, int offset, byte* prev, byte* current, float fTimer) => - RefMagic.RefFieldValue>(obj, offset).SetDirect(_interpDelegate?.Invoke(*(T*)prev, *(T*)current, fTimer) ?? *(T*)prev); + internal override T GetInterpolatedValue(T prev, T current, float t) => _interpDelegate?.Invoke(prev, current, t) ?? current; - internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) - { - ref var a = ref RefMagic.RefFieldValue>(obj, offset); - *(T*)tempHistory = a; - a.SetDirect(_interpDelegate?.Invoke(*(T*)historyA, *(T*)historyB, lerpTime) ?? *(T*)historyA); - } + internal override void LoadHistory(InternalBaseClass obj, int offset, byte* tempHistory, byte* historyA, byte* historyB, float lerpTime) => + RefMagic.SyncVarSetDirectAndStorePrev>(obj, offset, + _interpDelegate?.Invoke(*(T*)historyA, *(T*)historyB, lerpTime) ?? *(T*)historyA, out *(T*)tempHistory); public UserTypeProcessor(InterpolatorDelegateWithReturn interpolationDelegate) => _interpDelegate = interpolationDelegate; diff --git a/Assets/Plugins/LiteEntitySystem/NetPlayer.cs b/Assets/Plugins/LiteEntitySystem/NetPlayer.cs index c429905..9a73532 100644 --- a/Assets/Plugins/LiteEntitySystem/NetPlayer.cs +++ b/Assets/Plugins/LiteEntitySystem/NetPlayer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using LiteEntitySystem.Collections; using LiteEntitySystem.Internal; using LiteEntitySystem.Transport; @@ -43,7 +44,8 @@ public class NetPlayer internal NetPlayerState State; internal readonly SequenceBinaryHeap AvailableInput; internal readonly Dictionary EntitySyncInfo; - + internal DateTime ServerTickChangedTime; + internal NetPlayer(AbstractNetPeer peer, byte id) { Id = id; diff --git a/Assets/Plugins/LiteEntitySystem/PawnLogic.cs b/Assets/Plugins/LiteEntitySystem/PawnLogic.cs index 8e0a781..dd492ea 100644 --- a/Assets/Plugins/LiteEntitySystem/PawnLogic.cs +++ b/Assets/Plugins/LiteEntitySystem/PawnLogic.cs @@ -14,6 +14,9 @@ public ControllerLogic Controller get => EntityManager.GetEntityById(_controller); internal set { + if(IsDestroyed) + return; + byte ownerId = EntityManager.ServerPlayerId; if (value != null) { @@ -27,7 +30,6 @@ internal set protected internal override void Update() { - base.Update(); Controller?.BeforeControlledUpdate(); } diff --git a/Assets/Plugins/LiteEntitySystem/PredictableEntityLogic.cs b/Assets/Plugins/LiteEntitySystem/PredictableEntityLogic.cs new file mode 100644 index 0000000..6e4c0e7 --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/PredictableEntityLogic.cs @@ -0,0 +1,65 @@ +namespace LiteEntitySystem +{ + /// + /// Entity that can be spawned on client prediction + /// + public abstract class PredictableEntityLogic : EntityLogic + { + private struct InitialData + { + public ushort PredictedId; + public EntitySharedReference Parent; + } + + private ushort _predictedId; + private EntitySharedReference _initialParent; + private static RemoteCall SyncRPC; + + //used for spawn prediction + internal readonly ushort CreatedAtTick; + + /// + /// Is entity is recrated from server when created using AddPredictedEntity + /// Can be true only on client + /// + public bool IsRecreated { get; internal set; } + + internal void InitEntity(ushort predictedId, EntitySharedReference initialParent) + { + //Logger.Log($"InitEntity. PredId: {predictedId}. Id: {Id}, Class: {ClassData.ClassEnumName}. Mode: {EntityManager.Mode}. InitalParrent: {initialParent}"); + _predictedId = predictedId; + _initialParent = initialParent; + } + + protected PredictableEntityLogic(EntityParams entityParams) : base(entityParams) + { + CreatedAtTick = entityParams.EntityManager.Tick; + } + + internal bool IsSameAsLocal(PredictableEntityLogic other) => + _predictedId == other._predictedId && _initialParent == other._initialParent && ClassId == other.ClassId; + + //used for finding entity in rollback + internal bool IsEntityMatch(ushort predictedId, ushort parentId, ushort createdAtTick) => + _predictedId == predictedId && _initialParent.Id == parentId && CreatedAtTick == createdAtTick; + + protected override void RegisterRPC(ref RPCRegistrator r) + { + base.RegisterRPC(ref r); + r.CreateRPCAction(this, OnSyncRPC, ref SyncRPC, ExecuteFlags.SendToOwner); + } + + private void OnSyncRPC(InitialData initialData) + { + _predictedId = initialData.PredictedId; + _initialParent = initialData.Parent; + //Logger.Log($"OnSyncRPC called. pred id: {_predictedId}, initial parent: {_initialParent}"); + } + + protected internal override void OnSyncRequested() + { + base.OnSyncRequested(); + ExecuteRPC(SyncRPC, new InitialData { PredictedId = _predictedId, Parent = ParentId }); + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/PredictableEntityLogic.cs.meta b/Assets/Plugins/LiteEntitySystem/PredictableEntityLogic.cs.meta new file mode 100644 index 0000000..5509d33 --- /dev/null +++ b/Assets/Plugins/LiteEntitySystem/PredictableEntityLogic.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25deb888ed71d176b8e1726b2999fbed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/LiteEntitySystem/RPCRegistrator.cs b/Assets/Plugins/LiteEntitySystem/RPCRegistrator.cs index e42f400..40cc174 100644 --- a/Assets/Plugins/LiteEntitySystem/RPCRegistrator.cs +++ b/Assets/Plugins/LiteEntitySystem/RPCRegistrator.cs @@ -5,13 +5,48 @@ namespace LiteEntitySystem { + [Flags] + public enum BindOnChangeFlags + { + /// + /// Execute in OnSync stage. Mostly useful for RemoteControlled entities to trigger change notification + /// + ExecuteOnSync = 1 << 0, + + /// + /// Execute on local prediction on Client and on rollback + /// + ExecuteOnPrediction = 1 << 1, + + /// + /// Execute when value changed on server + /// + ExecuteOnServer = 1 << 2, + + /// + /// Execute when SyncVar values reset in rollback (before OnRollback() called) + /// + ExecuteOnRollbackReset = 1 << 3, + + /// + /// Execute after entity new() called and initial state read before OnConstructed + /// + ExecuteOnNew = 1 << 4, + + /// + /// Combines ExecuteOnSync, ExecuteOnPrediction and ExecuteOnServer flags + /// + ExecuteAlways = ExecuteOnSync | ExecuteOnPrediction | ExecuteOnServer | ExecuteOnRollbackReset + } + public delegate void SpanAction(ReadOnlySpan data); public delegate void SpanAction(TCaller caller, ReadOnlySpan data); - internal delegate void MethodCallDelegate(object classPtr, ReadOnlySpan buffer); + internal delegate void MethodCallDelegate(InternalBaseClass classPtr, ReadOnlySpan buffer); public readonly struct RemoteCall { - internal static MethodCallDelegate CreateMCD(Action methodToCall) => (classPtr, _) => methodToCall((TClass)classPtr); + internal static MethodCallDelegate CreateMCD(Action methodToCall) where TClass : InternalBaseClass => + (classPtr, _) => methodToCall((TClass)classPtr); internal readonly Action CachedAction; internal readonly ushort Id; @@ -29,7 +64,7 @@ internal RemoteCall(Action action, ushort rpcId, ExecuteFlags fl public readonly struct RemoteCall where T : unmanaged { - internal static unsafe MethodCallDelegate CreateMCD(Action methodToCall) => + internal static unsafe MethodCallDelegate CreateMCD(Action methodToCall) where TClass : InternalBaseClass => (classPtr, buffer) => { fixed (byte* data = buffer) @@ -52,7 +87,7 @@ internal RemoteCall(Action action, ushort rpcId, ExecuteFlags public readonly struct RemoteCallSpan where T : unmanaged { - internal static MethodCallDelegate CreateMCD(SpanAction methodToCall) => + internal static MethodCallDelegate CreateMCD(SpanAction methodToCall) where TClass : InternalBaseClass => (classPtr, buffer) => methodToCall((TClass)classPtr, MemoryMarshal.Cast(buffer)); internal readonly SpanAction CachedAction; @@ -71,7 +106,7 @@ internal RemoteCallSpan(SpanAction action, ushort rpcId, Exec public readonly struct RemoteCallSerializable where T : struct, ISpanSerializable { - internal static MethodCallDelegate CreateMCD(Action methodToCall) => + internal static MethodCallDelegate CreateMCD(Action methodToCall) where TClass : InternalBaseClass => (classPtr, buffer) => { var t = default(T); @@ -116,10 +151,10 @@ internal static void CheckTarget(object ent, object target) /// /// Variable to bind /// Action that will be called when variable changes by sync - /// order of execution - public void BindOnChange(ref SyncVar syncVar, Action onChangedAction) where T : unmanaged where TEntity : InternalEntity + public void BindOnChange(ref SyncVar syncVar, Action onChangedAction, BindOnChangeFlags flags = BindOnChangeFlags.ExecuteOnSync) where T : unmanaged where TEntity : InternalEntity { _fields[syncVar.FieldId].OnSync = RemoteCall.CreateMCD(onChangedAction); + _fields[syncVar.FieldId].OnSyncFlags = flags; } /// @@ -128,11 +163,10 @@ public void BindOnChange(ref SyncVar syncVar, Action /// Target entity for binding /// Variable to bind /// Action that will be called when variable changes by sync - /// order of execution - public void BindOnChange(TEntity self, ref SyncVar syncVar, Action onChangedAction) where T : unmanaged where TEntity : InternalEntity + public void BindOnChange(TEntity self, ref SyncVar syncVar, Action onChangedAction, BindOnChangeFlags flags = BindOnChangeFlags.ExecuteOnSync) where T : unmanaged where TEntity : InternalEntity { CheckTarget(self, onChangedAction.Target); - BindOnChange(ref syncVar, onChangedAction.Method.CreateDelegateHelper>()); + BindOnChange(ref syncVar, onChangedAction.Method.CreateDelegateHelper>(), flags); } /// @@ -191,6 +225,7 @@ public void CreateRPCAction(TEntity self, Action methodToCall, re /// /// Creates cached rpc action + /// This method can be used for virtual methods /// /// RPC method to call /// output handle that should be used to call rpc @@ -204,6 +239,7 @@ public void CreateRPCAction(Action methodToCall, ref RemoteCal /// /// Creates cached rpc action with valueType argument + /// This method can be used for virtual methods /// /// RPC method to call /// output handle that should be used to call rpc @@ -217,6 +253,7 @@ public void CreateRPCAction(Action methodToCall, ref Rem /// /// Creates cached rpc action with Span argument + /// This method can be used for virtual methods /// /// RPC method to call /// output handle that should be used to call rpc @@ -230,6 +267,7 @@ public void CreateRPCAction(SpanAction methodToCall, ref /// /// Creates cached rpc action with ISpanSerializable argument + /// This method can be used for virtual methods /// /// RPC method to call /// output handle that should be used to call rpc @@ -246,12 +284,14 @@ public void CreateRPCAction(Action methodToCall, ref Rem public readonly ref struct SyncableRPCRegistrator { + private readonly EntityFieldInfo[] _fields; private readonly List _calls; private readonly int _syncableOffset; private readonly ushort _initialCallsSize; - internal SyncableRPCRegistrator(int syncableOffset, List remoteCallsList) + internal SyncableRPCRegistrator(int syncableOffset, List remoteCallsList, EntityFieldInfo[] fields) { + _fields = fields; _calls = remoteCallsList; _initialCallsSize = (ushort)_calls.Count; _syncableOffset = syncableOffset; @@ -308,5 +348,28 @@ public void CreateClientAction(Action methodToCall remoteCallHandle = new RemoteCallSerializable(null, (ushort)(_calls.Count - _initialCallsSize), 0); _calls.Add(new RpcFieldInfo(_syncableOffset, RemoteCallSerializable.CreateMCD(methodToCall))); } + + /// + /// Bind notification of SyncVar changes to action + /// + /// Variable to bind + /// Action that will be called when variable changes by sync + public void BindOnChange(ref SyncVar syncVar, Action onChangedAction, BindOnChangeFlags flags = BindOnChangeFlags.ExecuteOnSync) where T : unmanaged where TSyncField : SyncableField + { + _fields[syncVar.FieldId].OnSync = RemoteCall.CreateMCD(onChangedAction); + _fields[syncVar.FieldId].OnSyncFlags = flags; + } + + /// + /// Bind notification of SyncVar changes to action + /// + /// Target entity for binding + /// Variable to bind + /// Action that will be called when variable changes by sync + public void BindOnChange(TSyncField self, ref SyncVar syncVar, Action onChangedAction, BindOnChangeFlags flags = BindOnChangeFlags.ExecuteOnSync) where T : unmanaged where TSyncField : SyncableField + { + RPCRegistrator.CheckTarget(self, onChangedAction.Target); + BindOnChange(ref syncVar, onChangedAction.Method.CreateDelegateHelper>(), flags); + } } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/ServerEntityManager.cs b/Assets/Plugins/LiteEntitySystem/ServerEntityManager.cs index d65ec5f..85dbdfd 100644 --- a/Assets/Plugins/LiteEntitySystem/ServerEntityManager.cs +++ b/Assets/Plugins/LiteEntitySystem/ServerEntityManager.cs @@ -9,10 +9,24 @@ namespace LiteEntitySystem { + /// + /// Rate at which server sends state packets + /// public enum ServerSendRate : byte { + /// + /// Send rate is equal to Tickrate + /// EqualToFPS = 1, + + /// + /// Send rate is Tickrate / 2 + /// HalfOfFPS = 2, + + /// + /// Send rate is Tickrate / 3 + /// ThirdOfFPS = 3 } @@ -34,7 +48,7 @@ public sealed class ServerEntityManager : EntityManager private readonly byte[] _inputDecodeBuffer = new byte[NetConstants.MaxUnreliableDataSize]; private readonly NetDataReader _requestsReader = new(); private readonly Queue _pendingRPCs = new(); - + private NetPlayer _syncForPlayer; private int _maxDataSize; @@ -47,6 +61,11 @@ public sealed class ServerEntityManager : EntityManager /// Network players count /// public int PlayersCount => _netPlayers.Count; + + /// + /// Timeout after which player will receive baseline state if player cannot receive big partial state + /// + public float PlayerResyncTimeout = 4.0f; /// /// Rate at which server will make and send packets @@ -84,6 +103,9 @@ public ServerEntityManager( SetTickrate(framesPerSecond); } + public override string GetCurrentFrameDebugInfo(DebugFrameModes modes) => + modes.HasFlagFast(DebugFrameModes.Server) ? $"[Server] Tick {_tick}" : string.Empty; + public override void Reset() { base.Reset(); @@ -271,7 +293,7 @@ public T AddController(NetPlayer owner, Action initMethod = null) where T /// /// Player that owns this controller /// pawn that will be controlled - /// Method that will be called after entity construction + /// Method that will be called before entity OnConstructed /// Entity type /// Created entity or null in case of limit public T AddController(NetPlayer owner, PawnLogic entityToControl, Action initMethod = null) where T : HumanControllerLogic => @@ -285,7 +307,7 @@ public T AddController(NetPlayer owner, PawnLogic entityToControl, Action /// /// Add new AI controller entity /// - /// Method that will be called after entity construction + /// Method that will be called before entity OnConstructed /// Entity type /// Created entity or null in case of limit public T AddAIController(Action initMethod = null) where T : AiControllerLogic => @@ -294,7 +316,7 @@ public T AddAIController(Action initMethod = null) where T : AiControllerL /// /// Add new entity /// - /// Method that will be called after entity construction + /// Method that will be called before entity OnConstructed /// Entity type /// Created entity or null in case of limit public T AddSingleton(Action initMethod = null) where T : SingletonEntityLogic => @@ -303,7 +325,7 @@ public T AddSingleton(Action initMethod = null) where T : SingletonEntityL /// /// Add new entity /// - /// Method that will be called after entity construction + /// Method that will be called before entity OnConstructed /// Entity type /// Created entity or null in case of limit public T AddEntity(Action initMethod = null) where T : EntityLogic => @@ -313,7 +335,7 @@ public T AddEntity(Action initMethod = null) where T : EntityLogic => /// Add new entity and set parent entity /// /// Parent entity - /// Method that will be called after entity construction + /// Method that will be called before entity OnConstructed /// Entity type /// Created entity or null in case of limit public T AddEntity(EntityLogic parent, Action initMethod = null) where T : EntityLogic => @@ -339,7 +361,7 @@ public DeserializeResult Deserialize(AbstractNetPeer peer, ReadOnlySpan in /// incoming data with header public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan inData) { - if (inData[0] != HeaderByte) + if (inData.Length == 0 || inData[0] != HeaderByte) return DeserializeResult.HeaderCheckFailed; inData = inData.Slice(1); @@ -378,8 +400,11 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan humanControllerLogic.DeltaDecodeInit(); } + if (player.State == NetPlayerState.WaitingForFirstInput) + player.AvailableInput.Clear(); + ushort clientTick = BitConverter.ToUInt16(inData); - inData = inData.Slice(2); + inData = inData.Slice(sizeof(ushort)); while (inData.Length >= InputPacketHeader.Size) { var inputInfo = new InputInfo{ Tick = clientTick }; @@ -412,7 +437,10 @@ public unsafe DeserializeResult Deserialize(NetPlayer player, ReadOnlySpan } inputInfo.Header.LerpMsec = Math.Clamp(inputInfo.Header.LerpMsec, 0f, 1f); if (Utils.SequenceDiff(inputInfo.Header.StateB, player.CurrentServerTick) > 0) + { player.CurrentServerTick = inputInfo.Header.StateB; + player.ServerTickChangedTime = DateTime.UtcNow; + } //Logger.Log($"ReadInput: {clientTick} stateA: {inputInfo.Header.StateA}"); clientTick++; @@ -468,17 +496,21 @@ private T Add(Action initMethod) where T : InternalEntity stateSerializer.Init(entity, _tick); //create new rpc - stateSerializer.MakeNewRPC(); + var newRPCPacket = stateSerializer.MakeNewRPC(); //init and construct initMethod?.Invoke(entity); + + //refresh with data set after init method + stateSerializer.RefreshNewRPC(newRPCPacket); + ConstructEntity(entity); //create OnConstructed rpc stateSerializer.MakeConstructedRPC(null); _changedEntities.Add(entity); - _maxDataSize += stateSerializer.GetMaximumSize(); + _maxDataSize += stateSerializer.MaximumSize; //Debug.Log($"[SEM] Entity create. clsId: {classData.ClassId}, id: {entityId}, v: {version}"); return entity; @@ -487,7 +519,8 @@ private T Add(Action initMethod) where T : InternalEntity internal override void OnEntityDestroyed(InternalEntity e) { //sync all disabled data before destroy - if (e is EntityLogic el) + /* + if (_netPlayers.Count > 0 && e is EntityLogic el) { for(int i = 0; i < _netPlayers.Count; i++) { @@ -495,9 +528,14 @@ internal override void OnEntityDestroyed(InternalEntity e) MarkFieldsChanged(e, SyncGroupUtils.ToSyncFlags(~syncGroupData.EnabledGroups)); } } + */ - _stateSerializers[e.Id].MakeDestroyedRPC(_tick); base.OnEntityDestroyed(e); + + if(_netPlayers.Count == 0) + RemoveEntity(e); + else + _stateSerializers[e.Id].MakeDestroyedRPC(_tick); } protected override unsafe void OnLogicTick() @@ -516,6 +554,8 @@ protected override unsafe void OnLogicTick() _minimalTick = _tick; bool resizeCompressionBuffer = false; int playersCount = _netPlayers.Count; + + //apply pending incoming inputs for (int pidx = 0; pidx < playersCount; pidx++) { var player = _netPlayers.GetByIndex(pidx); @@ -548,6 +588,7 @@ protected override unsafe void OnLogicTick() } } + //update entities if (SafeEntityUpdate) { foreach (var aliveEntity in AliveEntities) @@ -560,8 +601,16 @@ protected override unsafe void OnLogicTick() if(!aliveEntity.IsDestroyed) aliveEntity.Update(); } - + ExecuteLateConstruct(); + ExecuteLocalSingletonsLateUpdate(); + + //refresh construct rpc with latest updates to entity at creation tick + foreach (var rpcNode in _pendingRPCs) + { + if (rpcNode.Header.Id == (ushort)InternalRPCType.Construct && rpcNode.Header.Tick == _tick) + _stateSerializers[rpcNode.Header.EntityId].RefreshConstructedRPC(rpcNode); + } foreach (var lagCompensatedEntity in LagCompensatedEntities) ClassDataDict[lagCompensatedEntity.ClassId].WriteHistory(lagCompensatedEntity, _tick); @@ -571,6 +620,8 @@ protected override unsafe void OnLogicTick() //================================================================== if (playersCount == 0 || _tick % (int) SendRate != 0) return; + + var currentTimeUtc = DateTime.UtcNow; //remove old rpcs while (_pendingRPCs.TryPeek(out var rpcNode) && Utils.SequenceDiff(rpcNode.Header.Tick, _minimalTick) < 0) @@ -598,6 +649,9 @@ protected override unsafe void OnLogicTick() var player = _netPlayers.GetByIndex(pidx); _syncForPlayer = null; int rpcSize = 0; + RPCHeader prevRpcHeader = new(); + + //if player need baseline state - send it reliably if (player.State == NetPlayerState.RequestBaseline) { int originalLength = 0; @@ -610,7 +664,8 @@ protected override unsafe void OnLogicTick() { if (_stateSerializers[e.Id].ShouldSync(player.Id, false)) { - _stateSerializers[e.Id].MakeNewRPC(); + var newRPC = _stateSerializers[e.Id].MakeNewRPC(); + _stateSerializers[e.Id].RefreshNewRPC(newRPC); _stateSerializers[e.Id].MakeConstructedRPC(player); } } @@ -622,15 +677,15 @@ protected override unsafe void OnLogicTick() continue; var entity = EntitiesDict[rpcNode.Header.EntityId]; if(rpcNode.AllowToSendForPlayer(player.Id, entity.OwnerId)) - rpcNode.WriteTo(packetBuffer, ref originalLength); + rpcNode.WriteTo(packetBuffer, ref originalLength, ref prevRpcHeader); } rpcSize = originalLength; } else //like baseline but only queued rpcs and diff data { foreach (var rpcNode in _pendingRPCs) - if(ShouldSendRPC(rpcNode, player)) - rpcNode.WriteTo(packetBuffer, ref originalLength); + if(ShouldSendRPC(rpcNode, player, true)) + rpcNode.WriteTo(packetBuffer, ref originalLength, ref prevRpcHeader); rpcSize = originalLength; foreach (var e in GetEntities()) @@ -663,15 +718,25 @@ protected override unsafe void OnLogicTick() player.StateBTick = _tick; player.CurrentServerTick = _tick; player.State = NetPlayerState.WaitingForFirstInput; + player.ServerTickChangedTime = DateTime.UtcNow; Logger.Log($"[SEM] SendWorld to player {player.Id}. orig: {originalLength} b, compressed: {encodedLength} b, ExecutedTick: {_tick}"); continue; } + if (player.State != NetPlayerState.Active) { //waiting to load initial state continue; } + //check if player cannot receive partial state due to high packet loss and large state size + if ((currentTimeUtc - player.ServerTickChangedTime).TotalSeconds > PlayerResyncTimeout) + { + Logger.Log($"P:{player.Id} Request baseline {_tick} because timeout"); + player.State = NetPlayerState.RequestBaseline; + continue; + } + //Partial diff sync var header = (DiffPartHeader*)packetBuffer; header->UserHeader = HeaderByte; @@ -683,11 +748,12 @@ protected override unsafe void OnLogicTick() foreach (var rpcNode in _pendingRPCs) { - if(!ShouldSendRPC(rpcNode, player)) + if(!ShouldSendRPC(rpcNode, player, false)) continue; - rpcSize += rpcNode.TotalSize; - rpcNode.WriteTo(packetBuffer, ref writePosition); + //use another approach to sum size because writePosition can be edited by CheckOverflow + //!!! + rpcSize += rpcNode.WriteTo(packetBuffer, ref writePosition, ref prevRpcHeader); CheckOverflowAndSend(player, header, packetBuffer, ref writePosition, maxPartSize); if(player.State == NetPlayerState.RequestBaseline) break; @@ -720,7 +786,7 @@ protected override unsafe void OnLogicTick() //Logger.Log($"Removed highest order entity: {e.UpdateOrderNum}, new highest: {_nextOrderNum}"); } _entityIdQueue.ReuseId(entity.Id); - _maxDataSize -= stateSerializer.GetMaximumSize(); + _maxDataSize -= stateSerializer.MaximumSize; stateSerializer.Free(); //Logger.Log($"[SRV] RemoveEntity: {e.Id}"); RemoveEntity(entity); @@ -761,6 +827,7 @@ protected override unsafe void OnLogicTick() _netPlayers.GetByIndex(0).Peer.TriggerSend(); //Logger.Log($"ServerSendTick: {_tick}"); + //Logger.Log($"SendTotal: {RemoteCallPacket.TotalWriteSizeNormal}, SendDelta: {RemoteCallPacket.TotalWriteSizeDelta}"); return; void CheckOverflowAndSend(NetPlayer player, DiffPartHeader *header, byte* packetBuffer, ref int writePosition, int maxPartSize) { @@ -769,7 +836,7 @@ void CheckOverflowAndSend(NetPlayer player, DiffPartHeader *header, byte* packet { if (header->Part == MaxParts-1) { - Logger.Log($"P:{player.Id} Request baseline {_tick}"); + Logger.Log($"P:{player.Id} Request baseline {_tick} because state size"); player.State = NetPlayerState.RequestBaseline; break; } @@ -785,7 +852,7 @@ void CheckOverflowAndSend(NetPlayer player, DiffPartHeader *header, byte* packet } } - bool ShouldSendRPC(RemoteCallPacket rpcNode, NetPlayer player) + bool ShouldSendRPC(RemoteCallPacket rpcNode, NetPlayer player, bool isBaseline) { //SyncForPlayer calls? if (rpcNode.OnlyForPlayer != null && rpcNode.OnlyForPlayer != player) @@ -808,11 +875,11 @@ bool ShouldSendRPC(RemoteCallPacket rpcNode, NetPlayer player) return false; var entity = EntitiesDict[rpcNode.Header.EntityId]; - if (!rpcNode.AllowToSendForPlayer(player.Id, entity.OwnerId)) + if (!rpcNode.AllowToSendForPlayer(player.Id, entity.InternalOwnerId.Value)) return false; //check sync groups - if (entity.OwnerId != player.Id) + if (entity.InternalOwnerId.Value != player.Id) { if (entity is EntityLogic el && player.EntitySyncInfo.TryGetValue(el, out var syncGroups) && @@ -823,23 +890,46 @@ bool ShouldSendRPC(RemoteCallPacket rpcNode, NetPlayer player) return false; } } - - if (rpcNode.Header.Id == RemoteCallPacket.ConstructRPCId) - stateSerializer.RefreshConstructedRPC(rpcNode, player); + + switch ((InternalRPCType)rpcNode.Header.Id) + { + case InternalRPCType.NewOwned when entity.InternalOwnerId.Value != player.Id || isBaseline: + rpcNode.Header.Id = (ushort)InternalRPCType.New; + break; + + //do this change only for diff states for best local->remote entity swap + case InternalRPCType.New when entity.InternalOwnerId.Value == player.Id && !isBaseline: + rpcNode.Header.Id = (ushort)InternalRPCType.NewOwned; + break; + + case InternalRPCType.Construct: + stateSerializer.RefreshSyncGroupsVariable(player, new Span(rpcNode.Data)); + break; + } return true; } } - internal override void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue) + internal override unsafe void EntityFieldChanged(InternalEntity entity, ushort fieldId, ref T newValue, ref T oldValue, bool skipOnSync) { if (entity.IsRemoved) { //old freed entity return; } + if(entity is AiControllerLogic) + return; + _changedEntities.Add(entity); _stateSerializers[entity.Id].UpdateFieldValue(fieldId, _minimalTick, _tick, ref newValue); + + ref var fieldInfo = ref entity.ClassData.Fields[fieldId]; + if (!skipOnSync && (fieldInfo.OnSyncFlags & BindOnChangeFlags.ExecuteOnServer) != 0) + { + T value = oldValue; + fieldInfo.OnSync(fieldInfo.GetTargetObject(entity), new ReadOnlySpan(&value, fieldInfo.IntSize)); + } } internal void MarkFieldsChanged(InternalEntity entity, SyncFlags onlyWithFlags) @@ -848,34 +938,41 @@ internal void MarkFieldsChanged(InternalEntity entity, SyncFlags onlyWithFlags) _stateSerializers[entity.Id].MarkFieldsChanged(_minimalTick, _tick, onlyWithFlags); } - internal void AddRemoteCall(InternalEntity entity, ushort rpcId, ExecuteFlags flags) + internal RemoteCallPacket AddRemoteCall(InternalEntity entity, ushort rpcId, ExecuteFlags flags) { if (PlayersCount == 0 || entity.IsRemoved || entity is AiControllerLogic || (flags & ExecuteFlags.SendToAll) == 0) - return; + return null; var rpc = _rpcPool.Count > 0 ? _rpcPool.Dequeue() : new RemoteCallPacket(); rpc.Init(_syncForPlayer, entity, _tick, 0, rpcId, flags); _pendingRPCs.Enqueue(rpc); _maxDataSize += rpc.TotalSize; + return rpc; + } + + internal void NotifyRPCResized(int prevTotalSize, int newTotalSize) + { + _maxDataSize -= prevTotalSize; + _maxDataSize += newTotalSize; } - internal unsafe void AddRemoteCall(InternalEntity entity, ReadOnlySpan value, ushort rpcId, ExecuteFlags flags) where T : unmanaged + internal unsafe RemoteCallPacket AddRemoteCall(InternalEntity entity, ReadOnlySpan value, ushort rpcId, ExecuteFlags flags) where T : unmanaged { if (PlayersCount == 0 || entity.IsRemoved || entity is AiControllerLogic || (flags & ExecuteFlags.SendToAll) == 0) - return; + return null; var rpc = _rpcPool.Count > 0 ? _rpcPool.Dequeue() : new RemoteCallPacket(); int dataSize = sizeof(T) * value.Length; if (dataSize > ushort.MaxValue) { Logger.LogError($"DataSize on rpc: {rpcId}, entity: {entity} is more than {ushort.MaxValue}"); - return; + return null; } rpc.Init(_syncForPlayer, entity, _tick, (ushort)dataSize, rpcId, flags); @@ -884,6 +981,7 @@ entity is AiControllerLogic || RefMagic.CopyBlock(rawData, rawValue, (uint)dataSize); _pendingRPCs.Enqueue(rpc); _maxDataSize += rpc.TotalSize; + return rpc; } } } diff --git a/Assets/Plugins/LiteEntitySystem/SpanReader.cs b/Assets/Plugins/LiteEntitySystem/SpanReader.cs index a67fc01..74fe0c5 100644 --- a/Assets/Plugins/LiteEntitySystem/SpanReader.cs +++ b/Assets/Plugins/LiteEntitySystem/SpanReader.cs @@ -27,6 +27,17 @@ public void Get(out T result, Func constructor) where T : class, ISpanSeri result = constructor(); result.Deserialize(ref this); } + + public void GetStruct(out T result) where T : unmanaged => result = GetStruct(); + + public unsafe T GetStruct() where T : unmanaged + { + T value; + fixed (byte* rawData = RawData) + value = *(T*)(rawData+Position); + Position += sizeof(T); + return value; + } public void Get(out byte result) => result = GetByte(); public void Get(out sbyte result) => result = (sbyte)GetByte(); diff --git a/Assets/Plugins/LiteEntitySystem/SpanWriter.cs b/Assets/Plugins/LiteEntitySystem/SpanWriter.cs index b5dbb47..319a793 100644 --- a/Assets/Plugins/LiteEntitySystem/SpanWriter.cs +++ b/Assets/Plugins/LiteEntitySystem/SpanWriter.cs @@ -99,6 +99,12 @@ public void Put(byte[] data) new ReadOnlySpan(data).CopyTo(RawData.Slice(Position, data.Length)); Position += data.Length; } + + public void Put(ReadOnlySpan data) + { + data.CopyTo(RawData.Slice(Position, data.Length)); + Position += data.Length; + } public void PutSBytesWithLength(sbyte[] data, int offset, ushort length) { @@ -113,6 +119,21 @@ public void PutBytesWithLength(byte[] data, int offset, ushort length) new ReadOnlySpan(data, offset, length).CopyTo(RawData.Slice(Position + 2, length)); Position += 2 + length; } + + public unsafe void PutStruct(T data) where T : unmanaged + { + Put(sizeof(T)); + fixed (byte* rawData = RawData) + *(T*) (rawData + Position) = data; + Position += sizeof(T); + } + + public void PutBytesWithLength(ReadOnlySpan data) + { + Put(data.Length); + data.CopyTo(RawData.Slice(Position + 2, data.Length)); + Position += 2 + data.Length; + } public void PutArray(T[] arr, int sz) where T : unmanaged { diff --git a/Assets/Plugins/LiteEntitySystem/SyncChilds.cs b/Assets/Plugins/LiteEntitySystem/SyncChilds.cs index f84b71b..5345f94 100644 --- a/Assets/Plugins/LiteEntitySystem/SyncChilds.cs +++ b/Assets/Plugins/LiteEntitySystem/SyncChilds.cs @@ -4,7 +4,7 @@ namespace LiteEntitySystem { - public sealed class SyncChilds : SyncableField, IEnumerable + public sealed class SyncChilds : SyncableFieldCustomRollback, IEnumerable { class EqualityComparer : IEqualityComparer { @@ -27,8 +27,6 @@ class EqualityComparer : IEqualityComparer private static RemoteCall _removeAction; private static RemoteCallSpan _initAction; - public override bool IsRollbackSupported => true; - protected internal override void RegisterRPC(ref SyncableRPCRegistrator r) { r.CreateClientAction(this, Add, ref _addAction); @@ -39,17 +37,18 @@ protected internal override void RegisterRPC(ref SyncableRPCRegistrator r) protected internal override void OnRollback() { - if (_data == null || _serverData == null) + if (_data == null) return; _data.Clear(); + //Logger.Log($"ClearChilds: {ParentEntity.EntityManager.UpdateMode}"); + if (_serverData == null) + return; foreach (var x in _serverData) _data.Add(x); } protected internal override void BeforeReadRPC() { - if (_data == null) - return; _serverData ??= new HashSet(SharedReferenceComparer); _tempData = _data; _data = _serverData; @@ -57,9 +56,16 @@ protected internal override void BeforeReadRPC() protected internal override void AfterReadRPC() { - if (_data == null || _tempData == null) - return; _data = _tempData; + _tempData = null; + if (_serverData == null) + return; + if (_data == null) + { + if (_serverData.Count == 0) + return; + _data = new HashSet(SharedReferenceComparer); + } _data.Clear(); foreach (var kv in _serverData) _data.Add(kv); @@ -112,6 +118,9 @@ internal void Add(EntitySharedReference x) _data ??= new HashSet(SharedReferenceComparer); _data.Add(x); ExecuteRPC(_addAction, x); + MarkAsChanged(); + //if(IsClient) + // Logger.Log($"AddChild: {x} {ParentEntity.EntityManager.UpdateMode} at tick: {ParentEntity.ClientManager.Tick}"); } internal void Clear() @@ -120,6 +129,7 @@ internal void Clear() return; _data.Clear(); ExecuteRPC(_clearAction); + MarkAsChanged(); } public bool Contains(EntitySharedReference x) => _data != null && _data.Contains(x); @@ -131,6 +141,7 @@ internal bool Remove(EntitySharedReference key) if (_data == null || !_data.Remove(key)) return false; ExecuteRPC(_removeAction, key); + MarkAsChanged(); return true; } diff --git a/Assets/Plugins/LiteEntitySystem/SyncVar.cs b/Assets/Plugins/LiteEntitySystem/SyncVar.cs index 114883e..8fd8673 100644 --- a/Assets/Plugins/LiteEntitySystem/SyncVar.cs +++ b/Assets/Plugins/LiteEntitySystem/SyncVar.cs @@ -68,44 +68,85 @@ public class SyncVarFlags : Attribute public SyncVarFlags(SyncFlags flags) => Flags = flags; } + /// + /// Synchronized variable + /// + /// Variable type [StructLayout(LayoutKind.Sequential)] - public struct SyncVar : IEquatable, IEquatable> where T : unmanaged + public struct SyncVar : ISyncVar, IEquatable, IEquatable> where T : unmanaged { private T _value; + private T _interpValue; + internal ushort FieldId; internal InternalEntity Container; + + /// + /// Interpolated value on client (on server equals to Value) + /// + public T InterpolatedValue => Container == null || Container.IsServer + ? _value + : Container.ClientManager.GetInterpolatedValue(ref this, _interpValue); + + //for interpolation + void ISyncVar.SvSetInterpValue(T value) => _interpValue = value; + void ISyncVar.SvSetInterpValueFromCurrent() => _interpValue = _value; + + void ISyncVar.SvSetDirect(T value) => _value = value; - internal void SetDirect(T value) => _value = value; + void ISyncVar.SvSetDirectAndStorePrev(T value, out T prevValue) + { + prevValue = _value; + _value = value; + } + + bool ISyncVar.SvSetFromAndSync(ref T value) + { + if (!Utils.FastEquals(ref _value, ref value)) + { + // ReSharper disable once SwapViaDeconstruction + var oldValue = _value; + _value = value; + value = oldValue; + return true; + } + return false; + } + /// + /// Actual logical value + /// public T Value { get => _value; set { - if (Container != null && !Utils.FastEquals(ref value, ref _value)) - Container.EntityManager.EntityFieldChanged(Container, FieldId, ref value); + var oldValue = _value; _value = value; + if (Container != null && !Utils.FastEquals(ref value, ref oldValue)) + Container.EntityManager.EntityFieldChanged(Container, FieldId, ref value, ref oldValue, false); } } + /// + /// Set value without triggering local OnSync notifications if there is any + /// + /// + public void SetValueWithoutOnSyncNotification(T value) + { + var oldValue = _value; + _value = value; + if (Container != null && !Utils.FastEquals(ref value, ref oldValue)) + Container.EntityManager.EntityFieldChanged(Container, FieldId, ref value, ref oldValue, true); + } + internal void Init(InternalEntity container, ushort fieldId) { Container = container; FieldId = fieldId; - Container?.EntityManager.EntityFieldChanged(Container, FieldId, ref _value); - } - - internal unsafe bool SetFromAndSync(byte* data) - { - if (!Utils.FastEquals(ref _value, data)) - { - var temp = _value; - _value = *(T*)data; - *(T*)data = temp; - return true; - } - _value = *(T*)data; - return false; + T defaultValue = default; + if(!Utils.FastEquals(ref _value, ref defaultValue)) + Container.EntityManager.EntityFieldChanged(Container, FieldId, ref _value, ref defaultValue, false); } public static implicit operator T(SyncVar sv) => sv._value; diff --git a/Assets/Plugins/LiteEntitySystem/SyncableField.cs b/Assets/Plugins/LiteEntitySystem/SyncableField.cs index fbbaef5..93944b2 100644 --- a/Assets/Plugins/LiteEntitySystem/SyncableField.cs +++ b/Assets/Plugins/LiteEntitySystem/SyncableField.cs @@ -30,7 +30,6 @@ public abstract class SyncableField : InternalBaseClass private InternalEntity _parentEntity; private ExecuteFlags _executeFlags; - internal int FieldId; internal ushort RPCOffset; /// @@ -43,11 +42,6 @@ public abstract class SyncableField : InternalBaseClass /// protected internal bool IsServer => _parentEntity != null && _parentEntity.IsServer; - /// - /// Is supported rollback by this syncable field - /// - public virtual bool IsRollbackSupported => false; - /// /// Owner of this syncable field /// @@ -55,6 +49,10 @@ public abstract class SyncableField : InternalBaseClass internal void Init(InternalEntity parentEntity, SyncFlags fieldFlags) { + //already initialized + if (_parentEntity != null) + return; + _parentEntity = parentEntity; if (fieldFlags.HasFlagFast(SyncFlags.OnlyForOwner)) _executeFlags = ExecuteFlags.SendToOwner; @@ -68,21 +66,6 @@ internal void Init(InternalEntity parentEntity, SyncFlags fieldFlags) /// Owner of this syncable field casted to EntityLogic /// protected EntityLogic ParentEntityLogic => _parentEntity as EntityLogic; - - protected internal virtual void BeforeReadRPC() - { - - } - - protected internal virtual void AfterReadRPC() - { - - } - - protected internal virtual void OnRollback() - { - - } protected internal virtual void RegisterRPC(ref SyncableRPCRegistrator r) { @@ -120,4 +103,25 @@ protected void ExecuteRPC(in RemoteCallSerializable rpc, T value) where T } } } + + /// + /// Syncable fields with custom rollback notifications and implementation + /// + public abstract class SyncableFieldCustomRollback : SyncableField + { + /// + /// Marks that SyncableField was modified on client and add parent entity to Rollback list + /// + protected void MarkAsChanged() + { + if(ParentEntity != null && ParentEntity.IsClient) + ParentEntity.ClientManager.MarkEntityChanged(ParentEntity); + } + + protected internal abstract void BeforeReadRPC(); + + protected internal abstract void AfterReadRPC(); + + protected internal abstract void OnRollback(); + } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Transport/LiteNetLibNetPeer.cs b/Assets/Plugins/LiteEntitySystem/Transport/LiteNetLibNetPeer.cs index 60217cb..8569165 100644 --- a/Assets/Plugins/LiteEntitySystem/Transport/LiteNetLibNetPeer.cs +++ b/Assets/Plugins/LiteEntitySystem/Transport/LiteNetLibNetPeer.cs @@ -5,9 +5,9 @@ namespace LiteEntitySystem.Transport { public class LiteNetLibNetPeer : AbstractNetPeer { - public readonly NetPeer NetPeer; + public readonly LiteNetPeer NetPeer; - public LiteNetLibNetPeer(NetPeer netPeer, bool assignToTag) + public LiteNetLibNetPeer(LiteNetPeer netPeer, bool assignToTag) { NetPeer = netPeer; if(assignToTag) @@ -15,15 +15,15 @@ public LiteNetLibNetPeer(NetPeer netPeer, bool assignToTag) } public override void TriggerSend() => NetPeer.NetManager.TriggerUpdate(); - public override void SendReliableOrdered(ReadOnlySpan data) => NetPeer.Send(data, 0, DeliveryMethod.ReliableOrdered); - public override void SendUnreliable(ReadOnlySpan data) => NetPeer.Send(data, 0, DeliveryMethod.Unreliable); + public override void SendReliableOrdered(ReadOnlySpan data) => NetPeer.Send(data, DeliveryMethod.ReliableOrdered); + public override void SendUnreliable(ReadOnlySpan data) => NetPeer.Send(data, DeliveryMethod.Unreliable); public override int GetMaxUnreliablePacketSize() => NetPeer.GetMaxSinglePacketSize(DeliveryMethod.Unreliable); public override string ToString() => NetPeer.ToString(); } public static class LiteNetLibExtensions { - public static LiteNetLibNetPeer GetLiteNetLibNetPeerFromTag(this NetPeer peer) => (LiteNetLibNetPeer)peer.Tag; + public static LiteNetLibNetPeer GetLiteNetLibNetPeerFromTag(this LiteNetPeer peer) => (LiteNetLibNetPeer)peer.Tag; public static LiteNetLibNetPeer GetLiteNetLibNetPeer(this NetPlayer player) => (LiteNetLibNetPeer)player.Peer; } } \ No newline at end of file diff --git a/Assets/Plugins/LiteEntitySystem/Utils.cs b/Assets/Plugins/LiteEntitySystem/Utils.cs index 73ca22c..e436e39 100644 --- a/Assets/Plugins/LiteEntitySystem/Utils.cs +++ b/Assets/Plugins/LiteEntitySystem/Utils.cs @@ -153,6 +153,26 @@ public static unsafe bool FastEquals(ref T a, byte *x2) where T : unmanaged } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool FastEquals(byte *x1, byte *x2, int l) + { + for (int i=0; i < l/8; i++, x1+=8, x2+=8) + if (*(long*)x1 != *(long*)x2) return false; + if ((l & 4)!=0) { if (*(int*)x1!=*(int*)x2) return false; x1+=4; x2+=4; } + if ((l & 2)!=0) { if (*(short*)x1!=*(short*)x2) return false; x1+=2; x2+=2; } + return (l & 1) == 0 || *x1 == *x2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool IsZero(byte *x1, int l) + { + for (int i=0; i < l/8; i++, x1+=8) + if (*(long*)x1 != 0) return false; + if ((l & 4)!=0) { if (*(int*)x1!=0) return false; x1+=4; } + if ((l & 2)!=0) { if (*(short*)x1!=0) return false; x1+=2; } + return (l & 1) == 0 || *x1 == 0; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ResizeIfFull(ref T[] arr, int count) { @@ -216,6 +236,18 @@ public static ushort LerpSequence(ushort seq1, ushort seq2, float t) => [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static T CreateDelegateHelper(this MethodInfo method) where T : Delegate => (T)method.CreateDelegate(typeof(T)); + + public static string BytesToHexString(ReadOnlySpan bytes) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.Length; i++) + { + sb.Append(bytes[i].ToString("X2")); + if(i != bytes.Length - 1) + sb.Append('_'); + } + return sb.ToString(); + } internal static Stack GetBaseTypes(Type ofType, Type until, bool includeSelf, bool includeLast) { @@ -305,7 +337,7 @@ static Utils() var field = typeof(TestOffset).GetField("TestValue"); int offset = GetFieldOffset(field); var to = new TestOffset(); - if (RefMagic.RefFieldValue(to, offset) != to.TestValue) + if (RefMagic.GetFieldValue(to, offset) != to.TestValue) Logger.LogError("Unknown native field offset"); } } diff --git a/Assets/Scripts/Client/ClientLogic.cs b/Assets/Scripts/Client/ClientLogic.cs index b247d21..7268822 100644 --- a/Assets/Scripts/Client/ClientLogic.cs +++ b/Assets/Scripts/Client/ClientLogic.cs @@ -12,7 +12,7 @@ namespace Code.Client { - public class ClientLogic : MonoBehaviour, INetEventListener + public class ClientLogic : MonoBehaviour, ILiteNetEventListener { [SerializeField] private ClientPlayerView _clientPlayerViewPrefab; [SerializeField] private RemotePlayerView _remotePlayerViewPrefab; @@ -24,12 +24,12 @@ public class ClientLogic : MonoBehaviour, INetEventListener private GamePool _shootsPool; private GamePool _hitsPool; - private NetManager _netManager; + private LiteNetManager _netManager; private NetDataWriter _writer; private NetPacketProcessor _packetProcessor; private string _userName; - private NetPeer _server; + private LiteNetPeer _server; private ClientEntityManager _entityManager; private int _ping; @@ -81,7 +81,7 @@ private void Awake() _shootsPool = new GamePool(ShootEffectConstructor, 200); _hitsPool = new GamePool(HitEffectConstructor, 200); _packetProcessor = new NetPacketProcessor(); - _netManager = new NetManager(this) + _netManager = new LiteNetManager(this) { AutoRecycle = true, EnableStatistics = true, @@ -158,7 +158,7 @@ public void SpawnHit(Vector2 from) _server.Send(_writer, deliveryMethod); } - void INetEventListener.OnPeerConnected(NetPeer peer) + void ILiteNetEventListener.OnPeerConnected(LiteNetPeer peer) { Debug.Log("[C] Connected to server: " + peer); _server = peer; @@ -192,7 +192,7 @@ void INetEventListener.OnPeerConnected(NetPeer peer) }, true); } - void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + void ILiteNetEventListener.OnPeerDisconnected(LiteNetPeer peer, DisconnectInfo disconnectInfo) { _server = null; _entityManager = null; @@ -204,12 +204,12 @@ void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnec } } - void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) + void ILiteNetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) { Debug.Log("[C] NetworkError: " + socketError); } - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + void ILiteNetEventListener.OnNetworkReceive(LiteNetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) { byte packetType = reader.PeekByte(); var pt = (PacketType) packetType; @@ -230,18 +230,18 @@ void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, by } } - void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, + void ILiteNetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { } - void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) + void ILiteNetEventListener.OnNetworkLatencyUpdate(LiteNetPeer peer, int latency) { _ping = latency; } - void INetEventListener.OnConnectionRequest(ConnectionRequest request) + void ILiteNetEventListener.OnConnectionRequest(ConnectionRequest request) { request.Reject(); } diff --git a/Assets/Scripts/Client/RemotePlayerView.cs b/Assets/Scripts/Client/RemotePlayerView.cs index 7197510..96d756c 100644 --- a/Assets/Scripts/Client/RemotePlayerView.cs +++ b/Assets/Scripts/Client/RemotePlayerView.cs @@ -16,6 +16,7 @@ public static RemotePlayerView Create(RemotePlayerView prefab, BasePlayer player obj._player = player; var textObj = new GameObject($"text_{player.Name}_{player.Id}"); textObj.transform.SetParent(obj.transform); + textObj.transform.localPosition = Vector3.zero; obj._health = textObj.AddComponent(); obj._health.characterSize = 0.3f; obj._health.anchor = TextAnchor.MiddleCenter; diff --git a/Assets/Scripts/Server/ServerLogic.cs b/Assets/Scripts/Server/ServerLogic.cs index e393898..ae8216c 100644 --- a/Assets/Scripts/Server/ServerLogic.cs +++ b/Assets/Scripts/Server/ServerLogic.cs @@ -11,9 +11,9 @@ namespace Code.Server { - public class ServerLogic : MonoBehaviour, INetEventListener + public class ServerLogic : MonoBehaviour, ILiteNetEventListener { - private NetManager _netManager; + private LiteNetManager _netManager; private NetPacketProcessor _packetProcessor; public ushort Tick => _serverEntityManager.Tick; private ServerEntityManager _serverEntityManager; @@ -27,7 +27,7 @@ static ServerLogic() private void Awake() { EntityManager.RegisterFieldType(Vector2.Lerp); - _netManager = new NetManager(this) + _netManager = new LiteNetManager(this) { AutoRecycle = true, PacketPoolSize = 1000, @@ -43,7 +43,7 @@ private void Awake() #endif _packetProcessor = new NetPacketProcessor(); - _packetProcessor.SubscribeReusable(OnJoinReceived); + _packetProcessor.SubscribeReusable(OnJoinReceived); var typesMap = new EntityTypesMap() .Register(GameEntities.Player, e => new BasePlayer(e)) @@ -89,7 +89,7 @@ private void Update() _serverEntityManager?.Update(); } - private void OnJoinReceived(JoinPacket joinPacket, NetPeer peer) + private void OnJoinReceived(JoinPacket joinPacket, LiteNetPeer peer) { Debug.Log("[S] Join packet received: " + joinPacket.UserName); @@ -109,12 +109,12 @@ private void OnJoinReceived(JoinPacket joinPacket, NetPeer peer) _serverEntityManager.AddController(serverPlayer, player); } - void INetEventListener.OnPeerConnected(NetPeer peer) + void ILiteNetEventListener.OnPeerConnected(LiteNetPeer peer) { Debug.Log("[S] Player connected: " + peer); } - void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + void ILiteNetEventListener.OnPeerDisconnected(LiteNetPeer peer, DisconnectInfo disconnectInfo) { Debug.Log("[S] Player disconnected: " + disconnectInfo.Reason); @@ -124,12 +124,12 @@ void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnec } } - void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) + void ILiteNetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) { Debug.Log("[S] NetworkError: " + socketError); } - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) + void ILiteNetEventListener.OnNetworkReceive(LiteNetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) { byte packetType = reader.PeekByte(); switch ((PacketType)packetType) @@ -149,18 +149,18 @@ void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, by } } - void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, + void ILiteNetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { } - void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) + void ILiteNetEventListener.OnNetworkLatencyUpdate(LiteNetPeer peer, int latency) { } - void INetEventListener.OnConnectionRequest(ConnectionRequest request) + void ILiteNetEventListener.OnConnectionRequest(ConnectionRequest request) { request.AcceptIfKey("ExampleGame"); } diff --git a/Assets/Scripts/Shared/BasePlayer.cs b/Assets/Scripts/Shared/BasePlayer.cs index cb5e247..a5d8fb7 100644 --- a/Assets/Scripts/Shared/BasePlayer.cs +++ b/Assets/Scripts/Shared/BasePlayer.cs @@ -35,8 +35,8 @@ public class BasePlayer : PawnLogic private bool _projectileFire; public byte Health => _health; - public Vector2 Position => _position; - public float Rotation => _rotation.Value; + public Vector2 Position => _position.InterpolatedValue; + public float Rotation => _rotation.InterpolatedValue; public GameObject UnityObject; public GameObject View; @@ -63,7 +63,7 @@ protected override void OnConstructed() _rigidbody.isKinematic = true; _collider.isTrigger = true; _collider.size = Vector2.one * 0.66f; - UnityObject.AddComponent().AttachedPlayer = this; + UnityObject.AddComponent().AttachedPlayer = this; Debug.Log($"Player joined: {Name.Value}"); } @@ -177,7 +177,7 @@ protected override void Update() for (int i = 0; i < hitsCount; i++) { ref var hit = ref RaycastHits[i]; - if (hit.transform.TryGetComponent(out var playerProxy) && playerProxy.AttachedPlayer != this ) + if (hit.transform.TryGetComponent(out var playerProxy) && playerProxy.AttachedPlayer != this ) { if (EntityManager.InNormalState) ExecuteRPC(_hitRemoteCall, new HitPacket { Position = hit.point }); diff --git a/Assets/Scripts/Shared/BasePlayerView.cs b/Assets/Scripts/Shared/PlayerProxy.cs similarity index 85% rename from Assets/Scripts/Shared/BasePlayerView.cs rename to Assets/Scripts/Shared/PlayerProxy.cs index 1a2281b..f23a3ee 100644 --- a/Assets/Scripts/Shared/BasePlayerView.cs +++ b/Assets/Scripts/Shared/PlayerProxy.cs @@ -2,7 +2,7 @@ namespace Code.Shared { - public class BasePlayerView : MonoBehaviour + public class PlayerProxy : MonoBehaviour { public BasePlayer AttachedPlayer; diff --git a/Assets/Scripts/Shared/BasePlayerView.cs.meta b/Assets/Scripts/Shared/PlayerProxy.cs.meta similarity index 100% rename from Assets/Scripts/Shared/BasePlayerView.cs.meta rename to Assets/Scripts/Shared/PlayerProxy.cs.meta diff --git a/Assets/Scripts/Shared/SimpleProjectile.cs b/Assets/Scripts/Shared/SimpleProjectile.cs index 31912bb..2c0e377 100644 --- a/Assets/Scripts/Shared/SimpleProjectile.cs +++ b/Assets/Scripts/Shared/SimpleProjectile.cs @@ -13,7 +13,7 @@ public struct ProjectileInitParams } [EntityFlags(EntityFlags.UpdateOnClient)] - public class SimpleProjectile : EntityLogic + public class SimpleProjectile : PredictableEntityLogic { private static readonly RaycastHit2D[] RaycastHits = new RaycastHit2D[10]; @@ -87,7 +87,7 @@ protected override void Update() for (int i = 0; i < hitsCount; i++) { ref var hit = ref RaycastHits[i]; - if (hit.transform.TryGetComponent(out var playerProxy) && playerProxy.AttachedPlayer.SharedReference != ShooterPlayer ) + if (hit.transform.TryGetComponent(out var playerProxy) && playerProxy.AttachedPlayer.SharedReference != ShooterPlayer ) { playerProxy.AttachedPlayer.Damage(25); if (EntityManager.IsClient && EntityManager.InNormalState) @@ -107,7 +107,7 @@ protected override void Update() protected override void VisualUpdate() { - UnityObject.transform.position = Position.Value; + UnityObject.transform.position = Position.InterpolatedValue; } } } \ No newline at end of file diff --git a/Packages/manifest.json b/Packages/manifest.json index 30ec5d6..0cbb338 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,12 +1,11 @@ { "dependencies": { - "com.revenantx.litenetlib": "1.3.3", + "com.revenantx.litenetlib": "2.0.0", "com.unity.2d.sprite": "1.0.0", "com.unity.2d.tilemap": "1.0.0", - "com.unity.ai.navigation": "1.1.6", "com.unity.ext.nunit": "1.0.6", - "com.unity.ide.rider": "3.0.36", - "com.unity.ide.visualstudio": "2.0.23", + "com.unity.ide.rider": "3.0.38", + "com.unity.ide.visualstudio": "2.0.25", "com.unity.ide.vscode": "1.2.5", "com.unity.test-framework": "1.1.33", "com.unity.textmeshpro": "3.0.9", @@ -15,7 +14,6 @@ "com.unity.toolchain.macos-x86_64-linux-x86_64": "2.0.10", "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.10", "com.unity.ugui": "1.0.0", - "com.unity.xr.legacyinputhelpers": "2.1.12", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index a1fb5dc..dba2226 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,7 +1,7 @@ { "dependencies": { "com.revenantx.litenetlib": { - "version": "1.3.3", + "version": "2.0.0", "depth": 0, "source": "registry", "dependencies": {}, @@ -22,15 +22,6 @@ "com.unity.modules.uielements": "1.0.0" } }, - "com.unity.ai.navigation": { - "version": "1.1.6", - "depth": 0, - "source": "registry", - "dependencies": { - "com.unity.modules.ai": "1.0.0" - }, - "url": "https://packages.unity.com" - }, "com.unity.ext.nunit": { "version": "1.0.6", "depth": 0, @@ -39,7 +30,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "3.0.36", + "version": "3.0.38", "depth": 0, "source": "registry", "dependencies": { @@ -48,11 +39,11 @@ "url": "https://packages.unity.com" }, "com.unity.ide.visualstudio": { - "version": "2.0.23", + "version": "2.0.25", "depth": 0, "source": "registry", "dependencies": { - "com.unity.test-framework": "1.1.9" + "com.unity.test-framework": "1.1.31" }, "url": "https://packages.unity.com" }, @@ -150,16 +141,6 @@ "com.unity.modules.imgui": "1.0.0" } }, - "com.unity.xr.legacyinputhelpers": { - "version": "2.1.12", - "depth": 0, - "source": "registry", - "dependencies": { - "com.unity.modules.vr": "1.0.0", - "com.unity.modules.xr": "1.0.0" - }, - "url": "https://packages.unity.com" - }, "com.unity.modules.ai": { "version": "1.0.0", "depth": 0, diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 7a6f40b..587f809 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2022.3.62f1 -m_EditorVersionWithRevision: 2022.3.62f1 (4af31df58517) +m_EditorVersion: 2022.3.62f3 +m_EditorVersionWithRevision: 2022.3.62f3 (96770f904ca7)