namespace Fusion.Addons.KCC { using System; using System.Collections.Generic; using Unity.Profiling; using UnityEngine; using ReadOnlyProcessors = System.Collections.ObjectModel.ReadOnlyCollection; /// /// Fusion kinematic character controller component. /// [DisallowMultipleComponent] [RequireComponent(typeof(Rigidbody))] public sealed partial class KCC : NetworkTRSP, IAfterSpawned, IAfterClientPredictionReset, IBeforeTick, IAfterTick { // CONSTANTS public const int CACHE_SIZE = 64; public const int HISTORY_SIZE = 60; public const string TRACING_SCRIPT_DEFINE = "KCC_TRACE"; // PRIVATE MEMBERS [SerializeField] private KCCSettings _settings = new KCCSettings(); private Transform _transform; private Rigidbody _rigidbody; private bool _isSpawned; private bool _isInFixedUpdate; private bool _hasManualUpdate; private KCCDebug _debug = new KCCDebug(); private KCCCollider _collider = new KCCCollider(); private KCCData _fixedData = new KCCData(); private KCCData _renderData = new KCCData(); private KCCData[] _historyData = new KCCData[HISTORY_SIZE]; private KCCSettings _defaultSettings = new KCCSettings(); private KCCOverlapInfo _extendedOverlapInfo = new KCCOverlapInfo(CACHE_SIZE); private KCCOverlapInfo _trackOverlapInfo = new KCCOverlapInfo(CACHE_SIZE); private List _childColliders = new List(); private RaycastHit[] _raycastHits = new RaycastHit[CACHE_SIZE]; private Collider[] _hitColliders = new Collider[CACHE_SIZE]; private Collider[] _addColliders = new Collider[CACHE_SIZE]; private Collider[] _removeColliders = new Collider[CACHE_SIZE]; private KCCCollision[] _removeCollisions = new KCCCollision[CACHE_SIZE]; private KCCResolver _resolver = new KCCResolver(CACHE_SIZE); private EKCCFeatures _activeFeatures = EKCCFeatures.None; private List _activeStages = new List(); private IKCCProcessor[] _cachedProcessors = new IKCCProcessor[CACHE_SIZE * 2]; private int _cachedProcessorCount = 0; private IKCCProcessor[] _stageProcessors = new IKCCProcessor[CACHE_SIZE * 2]; private int _stageProcessorCount = 0; private BeginMove _beginMove = new BeginMove(); private PrepareData _prepareData = new PrepareData(); private AfterMoveStep _afterMoveStep = new AfterMoveStep(); private EndMove _endMove = new EndMove(); private List _localProcessors = new List(); private ReadOnlyProcessors _localROProcessors; private int _lastPredictedFixedTick; private int _lastPredictedRenderFrame; private int _lastPredictedLookRotationFrame; private float _lastRenderTime; private Vector3 _lastRenderPosition; private Vector3 _lastAntiJitterPosition; private Collider _lastNonNetworkedCollider; private Vector3 _predictionError; private event Action _onSpawn; private static ProfilerMarker _fixedUpdateMarker = new ProfilerMarker("KCC.FixedUpdate"); private static ProfilerMarker _renderUpdateMarker = new ProfilerMarker("KCC.RenderUpdate"); private static ProfilerMarker _restoreStateMarker = new ProfilerMarker("KCC.RestoreState"); private static ProfilerMarker _afterTickMarker = new ProfilerMarker("KCC.AfterTick"); private static ProfilerMarker _simulatedMoveMarker = new ProfilerMarker("Simulated Move"); private static ProfilerMarker _interpolationMarker = new ProfilerMarker("Interpolation"); // PUBLIC METHODS /// /// Immediately synchronize Transform and Rigidbody based on current state. /// /// Propagates KCCData.TargetPosition to Transform and Rigidbody components. /// Propagates KCCData.TransformRotation to Transform component. /// Allows anti-jitter feature. This has effect only in render. /// Use Rigidbody.MovePosition() instead of setting Rigidbody.position directly. public void SynchronizeTransform(bool synchronizePosition, bool synchronizeRotation, bool allowAntiJitter = true, bool moveRigidbody = false) { if (IsInFixedUpdate == true) { allowAntiJitter = false; } SynchronizeTransform(Data, synchronizePosition, synchronizeRotation, allowAntiJitter, moveRigidbody); } /// /// Refresh child colliders list, used for collision filtering. /// Child colliders are ignored completely, triggers are treated as valid collision. /// public void RefreshChildColliders() { _childColliders.Clear(); GetComponentsInChildren(true, _childColliders); int currentIndex = 0; int lastIndex = _childColliders.Count - 1; while (currentIndex <= lastIndex) { Collider childCollider = _childColliders[currentIndex]; if (childCollider.isTrigger == true || childCollider == _collider.Collider) { _childColliders[currentIndex] = _childColliders[lastIndex]; _childColliders.RemoveAt(lastIndex); --lastIndex; } else { ++currentIndex; } } } /// /// Returns fixed data for specific tick in history. Default history size is 60 ticks. /// public KCCData GetHistoryData(int tick) { if (tick < 0) return null; KCCData data = _historyData[tick % HISTORY_SIZE]; if (data != null && data.Tick == tick) return data; return null; } /// /// Controls whether update methods are driven by default Fusion methods or called manually using ManualFixedUpdate() and ManualRenderUpdate(). /// public void SetManualUpdate(bool hasManualUpdate) { _hasManualUpdate = hasManualUpdate; } /// /// Invokes callback when the KCC is spawned. /// If the KCC is already spawned, the callback is invoked immediately. /// public void InvokeOnSpawn(Action callback) { if (_isSpawned == true) { callback(this); } else { _onSpawn -= callback; _onSpawn += callback; } } /// /// Manual fixed update execution, SetManualUpdate(true) must be called prior usage. /// public void ManualFixedUpdate() { if (_isSpawned == false) return; if (_hasManualUpdate == false) throw new InvalidOperationException($"[{name}] Manual update is not set!"); _fixedUpdateMarker.Begin(); OnFixedUpdateInternal(); _fixedUpdateMarker.End(); } /// /// Manual render update execution, SetManualUpdate(true) must be called prior usage. /// public void ManualRenderUpdate() { if (_isSpawned == false) return; if (_hasManualUpdate == false) throw new InvalidOperationException($"[{name}] Manual update is not set!"); _renderUpdateMarker.Begin(); OnRenderUpdateInternal(); _renderUpdateMarker.End(); } // MonoBehaviour INTERFACE private void Awake() { _transform = transform; _rigidbody = GetComponent(); _rigidbody.isKinematic = true; _localROProcessors = new ReadOnlyProcessors(_localProcessors); RefreshCollider(true); } private void OnDestroy() { SetDefaults(true); } private void OnDrawGizmosSelected() { if (_settings == null) return; float radius = Mathf.Max(0.01f, _settings.Radius); float height = Mathf.Max(radius * 2.0f, _settings.Height); Vector3 basePosition = transform.position; Color gizmosColor = Gizmos.color; Vector3 baseLow = basePosition + Vector3.up * radius; Vector3 baseHigh = basePosition + Vector3.up * (height - radius); Vector3 offsetFront = Vector3.forward * radius; Vector3 offsetBack = Vector3.back * radius; Vector3 offsetLeft = Vector3.left * radius; Vector3 offsetRight = Vector3.right * radius; Gizmos.color = Color.green; Gizmos.DrawWireSphere(baseLow, radius); Gizmos.DrawWireSphere(baseHigh, radius); Gizmos.DrawLine(baseLow + offsetFront, baseHigh + offsetFront); Gizmos.DrawLine(baseLow + offsetBack, baseHigh + offsetBack); Gizmos.DrawLine(baseLow + offsetLeft, baseHigh + offsetLeft); Gizmos.DrawLine(baseLow + offsetRight, baseHigh + offsetRight); if (_settings.Extent > 0.0f) { float extendedRadius = radius + _settings.Extent; Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(baseLow, extendedRadius); Gizmos.DrawWireSphere(baseHigh, extendedRadius); } Gizmos.color = gizmosColor; } // NetworkBehaviour INTERFACE public override int? DynamicWordCount => GetNetworkDataWordCount(); public override void Spawned() { Trace(nameof(Spawned)); if (_isSpawned == true) throw new InvalidOperationException($"[{name}] KCC is already spawned!"); _defaultSettings.CopyFromOther(_settings); SetDefaults(false); _isSpawned = true; _isInFixedUpdate = true; KCCUtility.GetClampedLookRotationAngles(_transform.rotation, out float lookPitch, out float lookYaw); _fixedData = new KCCData(); _fixedData.Frame = Time.frameCount; _fixedData.Tick = Runner.Tick.Raw; _fixedData.Time = Runner.SimulationTime; _fixedData.DeltaTime = Runner.DeltaTime; _fixedData.UpdateDeltaTime = _fixedData.DeltaTime; _fixedData.Gravity = Physics.gravity; _fixedData.MaxGroundAngle = 60.0f; _fixedData.MaxWallAngle = 5.0f; _fixedData.MaxHangAngle = 30.0f; _fixedData.BasePosition = _transform.position; _fixedData.DesiredPosition = _transform.position; _fixedData.TargetPosition = _transform.position; _fixedData.LookPitch = lookPitch; _fixedData.LookYaw = lookYaw; _lastPredictedFixedTick = _fixedData.Tick; _lastPredictedRenderFrame = _fixedData.Frame; _lastPredictedLookRotationFrame = _fixedData.Frame; if (Object.HasStateAuthority == false) { ReadNetworkData(); SynchronizeTransform(_fixedData, true, true, false, false); } _renderData = new KCCData(); _renderData.CopyFromOther(_fixedData); _lastRenderPosition = _renderData.TargetPosition; _lastAntiJitterPosition = _renderData.TargetPosition; RefreshCollider(_fixedData.IsActive); RefreshChildColliders(); UnityEngine.Object[] processors = _settings.Processors; if (processors != null) { for (int i = 0, count = processors.Length; i < count; ++i) { UnityEngine.Object processorObject = processors[i]; if (processorObject == null) { KCCUtility.Log(this, this, default, EKCCLogType.Warning, $"Missing processor object - {nameof(KCCSettings)}.{nameof(KCCSettings.Processors)} at index {i}"); continue; } if (KCCUtility.ResolveProcessor(processorObject, out IKCCProcessor processor, out GameObject gameObject, out Component component, out ScriptableObject scriptableObject) == false) { KCCUtility.Log(this, processorObject, default, EKCCLogType.Error, $"Failed to resolve {nameof(IKCCProcessor)} in {processorObject.name} ({processorObject.GetType().FullName}) - {nameof(KCCSettings)}.{nameof(KCCSettings.Processors)} at index {i}"); continue; } AddLocalProcessor(processor); } } if (_onSpawn != null) { try { _onSpawn(this); } catch (Exception exception) { UnityEngine.Debug.LogException(exception); } _onSpawn = null; } } public override void Despawned(NetworkRunner runner, bool hasState) { if (_isSpawned == false) return; Trace(nameof(Despawned)); ForceRemoveAllCollisions(_fixedData); ForceRemoveAllModifiers(_fixedData); while (_localProcessors.Count > 0) { RemoveLocalProcessor(_localProcessors[_localProcessors.Count - 1]); } SetDefaults(true); } public override void FixedUpdateNetwork() { if (_hasManualUpdate == true) return; _fixedUpdateMarker.Begin(); OnFixedUpdateInternal(); _fixedUpdateMarker.End(); } public override void Render() { if (_hasManualUpdate == true) return; _renderUpdateMarker.Begin(); OnRenderUpdateInternal(); _renderUpdateMarker.End(); } // IAfterSpawned INTERFACE void IAfterSpawned.AfterSpawned() { _renderData.CopyFromOther(_fixedData); if (Object.IsInSimulation == true) { WriteNetworkData(); } _isInFixedUpdate = false; } // IAfterClientPredictionReset INTERFACE void IAfterClientPredictionReset.AfterClientPredictionReset() { int latestServerTick = Runner.LatestServerTick; Trace(nameof(IAfterClientPredictionReset.AfterClientPredictionReset), $"Tick:{latestServerTick}"); _restoreStateMarker.Begin(); KCCData historyData = _historyData[latestServerTick % HISTORY_SIZE]; if (historyData != null && historyData.Tick == latestServerTick) { _fixedData.CopyFromOther(historyData); _fixedData.Frame = Time.frameCount; } ReadNetworkData(); if (historyData != default) { RestoreHistoryData(historyData); } RefreshCollider(_fixedData.IsActive); if (_fixedData.IsActive == true) { SynchronizeTransform(_fixedData, true, true, false, false); } _lastPredictedFixedTick = latestServerTick; _restoreStateMarker.End(); } // IBeforeTick INTERFACE void IBeforeTick.BeforeTick() { _isInFixedUpdate = true; _fixedData.Frame = Time.frameCount; _fixedData.Tick = Runner.Tick.Raw; _fixedData.Alpha = 0.0f; _fixedData.Time = Runner.SimulationTime; _fixedData.DeltaTime = Runner.DeltaTime; _fixedData.UpdateDeltaTime = _fixedData.DeltaTime; Trace(nameof(IBeforeTick.BeforeTick), "[Fixed Update Initialization]", $"Time:{_fixedData.Time:F6}", $"DeltaTime:{_fixedData.DeltaTime:F6}", $"Alpha:{_fixedData.Alpha:F4}", $"HasInputAuthority:{Object.HasInputAuthority}", $"HasStateAuthority:{Object.HasStateAuthority}"); } // IAfterTick INTERFACE void IAfterTick.AfterTick() { Trace(nameof(IAfterTick.AfterTick)); _afterTickMarker.Begin(); PublishFixedData(true, true); WriteNetworkData(); _isInFixedUpdate = false; _afterTickMarker.End(); } // PRIVATE METHODS private void OnFixedUpdateInternal() { if (IsInFixedUpdate == false) throw new InvalidOperationException($"[{name}] KCC fixed update called from render update! This is not allowed."); Trace(nameof(OnFixedUpdateInternal)); _debug.BeforePredictedFixedMove(this); RefreshCollider(_fixedData.IsActive); _simulatedMoveMarker.Begin(); MovePredicted(_fixedData); if (_fixedData.IsActive == true) { SynchronizeTransform(_fixedData, true, true, false, true); } PublishFixedData(false, true); _lastPredictedFixedTick = _fixedData.Tick; _simulatedMoveMarker.End(); _debug.AfterPredictedFixedMove(this); } private void OnRenderUpdateInternal() { if (IsInFixedUpdate == true) throw new InvalidOperationException($"[{name}] KCC render update called from fixed update! This is not allowed."); Trace(nameof(OnRenderUpdateInternal)); int frame = Time.frameCount; float deltaTime = Runner.DeltaTime; bool isInSimulation = Object.IsInSimulation; _renderData.Frame = frame; if (isInSimulation == true && Object.RenderTimeframe != RenderTimeframe.Remote) { _simulatedMoveMarker.Begin(); _renderData.Tick = Runner.Tick; _renderData.Alpha = Runner.LocalAlpha; float previousTime = _renderData.Time; _renderData.Time = Runner.SimulationTime + _renderData.Alpha * deltaTime; if (IsInterpolatingInRenderUpdate == true) { _renderData.Tick -= 1; _renderData.Time -= deltaTime; if (_renderData.Frame == _fixedData.Frame) { previousTime -= deltaTime; } } _renderData.DeltaTime = _renderData.Time - previousTime; _renderData.UpdateDeltaTime = _renderData.DeltaTime; UpdatePredictionError(); #if UNITY_EDITOR if (_debug.ShowPath == true) { if (_renderData.Frame == _fixedData.Frame) { UnityEngine.Debug.DrawLine(_fixedData.TargetPosition, _renderData.TargetPosition, KCCDebug.FixedToRenderPathColor, _debug.DisplayTime); } else { UnityEngine.Debug.DrawLine(_lastRenderPosition, _renderData.TargetPosition, KCCDebug.PredictionCorrectionColor, _debug.DisplayTime); } } #endif if (IsPredictingInRenderUpdate == true) { MovePredicted(_renderData); _lastPredictedRenderFrame = frame; _lastPredictedLookRotationFrame = frame; } else { MoveInterpolated(_renderData); if (IsPredictingLookRotation == true) { _lastPredictedLookRotationFrame = frame; } } if (_renderData.IsActive == true) { SynchronizeTransform(_renderData, true, true, true, true); } _simulatedMoveMarker.End(); } else { _interpolationMarker.Begin(); if (_settings.ProxyInterpolationMode == EKCCInterpolationMode.Transform) { InterpolateNetworkTransform(); } else { if (isInSimulation == false) { _fixedData.Frame = frame; _fixedData.Tick = Object.LastReceiveTick; _fixedData.Alpha = 1.0f; _fixedData.Time = _fixedData.Tick * deltaTime; _fixedData.DeltaTime = deltaTime; _fixedData.UpdateDeltaTime = deltaTime; ReadNetworkData(); } InterpolateNetworkData(); RefreshCollider(_renderData.IsActive); if (_renderData.IsActive == true) { CacheProcessors(_renderData); InvokeOnInterpolate(_renderData); SynchronizeTransform(_renderData, true, true, false, true); } } _interpolationMarker.End(); } _lastRenderPosition = _renderData.TargetPosition; _lastRenderTime = _renderData.Time; _debug.AfterRenderUpdate(this); } private void UpdatePredictionError() { if (_activeFeatures.Has(EKCCFeature.PredictionCorrection) == true && _renderData.Frame == _fixedData.Frame) { KCCData current = GetHistoryData(_renderData.Tick); if (current != null && _lastRenderTime <= current.Time) { for (int i = 0; i < 5; ++i) { KCCData previous = GetHistoryData(current.Tick - 1); if (previous == null) break; if (_lastRenderTime >= previous.Time) { if (current.HasTeleported == true || previous.HasTeleported == true) { _predictionError = default; return; } float deltaTime = current.Time - previous.Time; if (deltaTime <= 0.000001f) { _predictionError = default; return; } float lastRenderAlpha = (_lastRenderTime - previous.Time) / deltaTime; Vector3 expectedRenderPosition = Vector3.Lerp(previous.TargetPosition, current.TargetPosition, lastRenderAlpha); #if UNITY_EDITOR if (_debug.ShowPath == true) { UnityEngine.Debug.DrawLine(expectedRenderPosition, _lastRenderPosition, KCCDebug.PredictionErrorColor, _debug.DisplayTime); } #endif _predictionError = _lastRenderPosition - expectedRenderPosition; if (_predictionError.sqrMagnitude >= 1.0f) { _predictionError = default; return; } _predictionError = Vector3.Lerp(_predictionError, Vector3.zero, _settings.PredictionCorrectionSpeed * Time.deltaTime); _renderData.BasePosition += _predictionError; _renderData.DesiredPosition += _predictionError; _renderData.TargetPosition += _predictionError; return; } current = previous; } } } if (_predictionError.IsAlmostZero(0.000001f) == true) { _predictionError = default; } else { _renderData.BasePosition -= _predictionError; _renderData.DesiredPosition -= _predictionError; _renderData.TargetPosition -= _predictionError; _predictionError = Vector3.Lerp(_predictionError, Vector3.zero, _settings.PredictionCorrectionSpeed * Time.deltaTime); _renderData.BasePosition += _predictionError; _renderData.DesiredPosition += _predictionError; _renderData.TargetPosition += _predictionError; } } private void MovePredicted(KCCData data) { _activeFeatures = _settings.Features; float baseTime = data.Time; float baseDeltaTime = data.DeltaTime; Vector3 basePosition = data.TargetPosition; Vector3 desiredPosition = data.TargetPosition; bool wasGrounded = data.IsGrounded; bool wasSteppingUp = data.IsSteppingUp; bool wasSnappingToGround = data.IsSnappingToGround; data.DeltaTime = baseDeltaTime; data.BasePosition = basePosition; data.DesiredPosition = desiredPosition; SetBaseProperties(data); if (data.IsActive == false) { data.ClearTransientProperties(); ForceRemoveAllCollisions(data); ForceRemoveAllHits(data); data.RealVelocity = default; data.RealSpeed = default; return; } CacheProcessors(data); ExecuteStageInternal(_beginMove, data); if (data.IsActive == false) { data.ClearTransientProperties(); ForceRemoveAllCollisions(data); ForceRemoveAllHits(data); data.RealVelocity = default; data.RealSpeed = default; ExecuteStageInternal(_endMove, data); return; } baseDeltaTime = data.DeltaTime; basePosition = data.BasePosition; if (baseDeltaTime < KCCSettings.ExtrapolationDeltaTimeThreshold) { Vector3 extrapolationVelocity = data.DesiredVelocity; if (data.RealVelocity.sqrMagnitude <= extrapolationVelocity.sqrMagnitude) { extrapolationVelocity = data.RealVelocity; } desiredPosition = basePosition + extrapolationVelocity * baseDeltaTime; data.BasePosition = basePosition; data.DesiredPosition = desiredPosition; data.TargetPosition = desiredPosition; data.RealVelocity = extrapolationVelocity; data.RealSpeed = data.RealVelocity.magnitude; ExecuteStageInternal(_endMove, data); InvokeOnStay(data); return; } ExecuteStageInternal(_prepareData, data); ForceRemoveAllHits(data); float pendingDeltaTime = Mathf.Clamp01(baseDeltaTime); Vector3 pendingDeltaPosition = data.DesiredVelocity * pendingDeltaTime + data.ExternalDelta; desiredPosition = data.BasePosition + pendingDeltaPosition; data.DesiredPosition = desiredPosition; data.TargetPosition = data.BasePosition; data.ExternalDelta = default; bool hasFinished = false; float radiusMultiplier = Mathf.Clamp(_settings.CCDRadiusMultiplier, 0.1f, 0.9f); float maxDeltaMagnitude = _settings.Radius * (radiusMultiplier + 0.1f); float optimalDeltaMagnitude = _settings.Radius * radiusMultiplier; Vector3 nonTeleportedPosition = data.TargetPosition; while (hasFinished == false && data.HasTeleported == false) { data.BasePosition = data.TargetPosition; float consumeDeltaTime = pendingDeltaTime; Vector3 consumeDeltaPosition = pendingDeltaPosition; if (_activeFeatures.Has(EKCCFeature.CCD) == true) { float consumeDeltaPositionMagnitude = consumeDeltaPosition.magnitude; if (consumeDeltaPositionMagnitude > maxDeltaMagnitude) { float deltaRatio = optimalDeltaMagnitude / consumeDeltaPositionMagnitude; consumeDeltaTime *= deltaRatio; consumeDeltaPosition *= deltaRatio; } else { hasFinished = true; } } else { hasFinished = true; } pendingDeltaTime -= consumeDeltaTime; pendingDeltaPosition -= consumeDeltaPosition; if (pendingDeltaTime <= 0.0f) { pendingDeltaTime = 0.0f; } data.Time = baseTime - pendingDeltaTime; data.DeltaTime = consumeDeltaTime; data.DesiredPosition = data.BasePosition + consumeDeltaPosition; data.TargetPosition = data.DesiredPosition; data.WasGrounded = data.IsGrounded; data.WasSteppingUp = data.IsSteppingUp; data.WasSnappingToGround = data.IsSnappingToGround; ProcessMoveStep(data); if (data.HasTeleported == false) { nonTeleportedPosition = data.TargetPosition; } UpdateCollisions(data, _trackOverlapInfo); if (data.HasTeleported == true) { UpdateHits(data, null, EKCCHitsOverlapQuery.New); UpdateCollisions(data, _trackOverlapInfo); } if (hasFinished == true && data.ExternalDelta.IsZero() == false) { pendingDeltaPosition += data.ExternalDelta; data.ExternalDelta = default; hasFinished = false; } } data.Time = baseTime; data.DeltaTime = baseDeltaTime; data.BasePosition = basePosition; data.DesiredPosition = desiredPosition; data.WasGrounded = wasGrounded; data.WasSteppingUp = wasSteppingUp; data.WasSnappingToGround = wasSnappingToGround; data.RealVelocity = (nonTeleportedPosition - data.BasePosition) / data.DeltaTime; data.RealSpeed = data.RealVelocity.magnitude; Vector3 targetPosition = data.TargetPosition; ExecuteStageInternal(_endMove, data); if (data.TargetPosition.IsEqual(targetPosition) == false) { UpdateHits(data, null, EKCCHitsOverlapQuery.New); UpdateCollisions(data, _trackOverlapInfo); } targetPosition = data.TargetPosition; InvokeOnStay(data); if (data.TargetPosition.IsEqual(targetPosition) == false) { UpdateHits(data, null, EKCCHitsOverlapQuery.New); UpdateCollisions(data, _trackOverlapInfo); } } private void MoveInterpolated(KCCData data) { if (data.IsActive == false) return; KCCData currentFixedData = _fixedData; if (currentFixedData.HasTeleported == true) { data.BasePosition = currentFixedData.BasePosition; data.DesiredPosition = currentFixedData.DesiredPosition; data.TargetPosition = currentFixedData.TargetPosition; data.LookPitch = currentFixedData.LookPitch; data.LookYaw = currentFixedData.LookYaw; } else { KCCData previousFixedData = GetHistoryData(currentFixedData.Tick - 1); if (previousFixedData != null) { float alpha = data.Alpha; data.BasePosition = previousFixedData.TargetPosition + _predictionError; data.DesiredPosition = currentFixedData.TargetPosition + _predictionError; data.TargetPosition = Vector3.Lerp(previousFixedData.TargetPosition, currentFixedData.TargetPosition, alpha) + _predictionError; data.RealVelocity = Vector3.Lerp(previousFixedData.RealVelocity, currentFixedData.RealVelocity, alpha); data.RealSpeed = Mathf.Lerp(previousFixedData.RealSpeed, currentFixedData.RealSpeed, alpha); if (IsPredictingLookRotation == false) { data.LookPitch = Mathf.Lerp(previousFixedData.LookPitch, currentFixedData.LookPitch, alpha); data.LookYaw = KCCUtility.InterpolateRange(previousFixedData.LookYaw, currentFixedData.LookYaw, -180.0f, 180.0f, alpha); } } } data.HasTeleported = false; int historyTeleportTick = currentFixedData.Tick; while (historyTeleportTick > _networkContext.LastInterpolationTeleportCounter) { KCCData historyData = GetHistoryData(historyTeleportTick); if (historyData == null) break; if (historyData.HasTeleported == true) { data.HasTeleported = true; break; } --historyTeleportTick; } _networkContext.LastInterpolationTeleportCounter = currentFixedData.Tick; data.JumpFrames = 0; int historyJumpTick = currentFixedData.Tick; while (historyJumpTick > _networkContext.LastInterpolationJumpCounter) { KCCData historyData = GetHistoryData(historyJumpTick); if (historyData == null) break; if (historyData.HasJumped == true) { data.JumpFrames = 1; break; } --historyJumpTick; } _networkContext.LastInterpolationJumpCounter = currentFixedData.Tick; CacheProcessors(data); InvokeOnInterpolate(data); } private void SetBaseProperties(KCCData data) { data.HasTeleported = default; data.MaxPenetrationSteps = _settings.MaxPenetrationSteps; if (data.Frame == _fixedData.Frame) { data.JumpFrames = default; } } private void ProcessMoveStep(KCCData data) { data.IsGrounded = default; data.IsSteppingUp = default; data.IsSnappingToGround = default; data.GroundNormal = default; data.GroundTangent = default; data.GroundPosition = default; data.GroundDistance = default; data.GroundAngle = default; ForceRemoveAllHits(data); bool hasJumped = data.JumpFrames > 0; if (_settings.CollisionLayerMask != 0 && _collider.IsSpawned == true) { float baseOverlapQueryExtent = _settings.Radius; EKCCHitsOverlapQuery baseHitsOverlapQuery = EKCCHitsOverlapQuery.Default; if (_settings.ForceSingleOverlapQuery == true) { baseOverlapQueryExtent = _settings.Extent; baseHitsOverlapQuery = EKCCHitsOverlapQuery.Reuse; } CapsuleOverlap(_extendedOverlapInfo, data, data.TargetPosition, _settings.Radius, _settings.Height, baseOverlapQueryExtent, _settings.CollisionLayerMask, QueryTriggerInteraction.Collide); ResolvePenetration(_extendedOverlapInfo, data, data.MaxPenetrationSteps, hasJumped == false, true); UpdateHits(data, _extendedOverlapInfo, baseHitsOverlapQuery); } if (hasJumped == true) { data.IsGrounded = false; } _afterMoveStep.OverlapInfo.CopyFromOther(_extendedOverlapInfo); ExecuteStageInternal(_afterMoveStep, data); } [Obsolete("Use SynchronizeTransform(KCCData data, bool synchronizePosition, bool synchronizeRotation, bool allowAntiJitter, bool moveRigidBody)")] private void SynchronizeTransform(KCCData data, bool synchronizePosition, bool synchronizeRotation, bool allowAntiJitter) { SynchronizeTransform(data, synchronizePosition, synchronizeRotation, allowAntiJitter, false); } private void SynchronizeTransform(KCCData data, bool synchronizePosition, bool synchronizeRotation, bool allowAntiJitter, bool moveRigidBody) { if (synchronizePosition == true) { Vector3 targetPosition = data.TargetPosition; if (moveRigidBody == true) { _rigidbody.MovePosition(targetPosition); } else { _rigidbody.position = targetPosition; } if (allowAntiJitter == true && _activeFeatures.Has(EKCCFeature.AntiJitter) == true && _settings.AntiJitterDistance.IsZero() == false) { Vector3 targetDelta = targetPosition - _lastAntiJitterPosition; if (targetDelta.sqrMagnitude < 1.0f) { targetPosition = _lastAntiJitterPosition; float distanceY = Mathf.Abs(targetDelta.y); if (distanceY > 0.000001f && distanceY > _settings.AntiJitterDistance.y) { targetPosition.y += targetDelta.y * Mathf.Clamp01((distanceY - _settings.AntiJitterDistance.y) / distanceY); } Vector3 targetDeltaXZ = targetDelta.OnlyXZ(); float distanceXZ = Vector3.Magnitude(targetDeltaXZ); if (distanceXZ > 0.000001f && distanceXZ > _settings.AntiJitterDistance.x) { targetPosition += targetDeltaXZ * Mathf.Clamp01((distanceXZ - _settings.AntiJitterDistance.x) / distanceXZ); } } _lastAntiJitterPosition = targetPosition; } if (synchronizeRotation == true) { _transform.SetPositionAndRotation(targetPosition, data.TransformRotation); } else { _transform.position = targetPosition; } } else { if (synchronizeRotation == true) { _transform.rotation = data.TransformRotation; } } } private void RefreshCollider(bool isActive) { if (isActive == false || _settings.Shape == EKCCShape.None) { _collider.Destroy(); return; } _settings.Radius = Mathf.Max(0.01f, _settings.Radius); _settings.Height = Mathf.Max(_settings.Radius * 2.0f, _settings.Height); _settings.Extent = Mathf.Max(0.0f, _settings.Extent); _collider.Update(this); } private void SetDefaults(bool cleanup) { _debug.SetDefaults(); _fixedData.Clear(); _renderData.Clear(); _historyData.Clear(); _afterMoveStep.OverlapInfo.Reset(true); _extendedOverlapInfo.Reset(true); _trackOverlapInfo.Reset(true); _childColliders.Clear(); _raycastHits.Clear(); _hitColliders.Clear(); _addColliders.Clear(); _removeColliders.Clear(); _removeCollisions.Clear(); _activeStages.Clear(); _cachedProcessors.Clear(); _stageProcessors.Clear(); _localProcessors.Clear(); _cachedProcessorCount = default; _stageProcessorCount = default; _rigidbody.isKinematic = true; _activeFeatures = default; _lastPredictedFixedTick = default; _lastPredictedRenderFrame = default; _lastPredictedLookRotationFrame = default; _lastRenderTime = default; _lastRenderPosition = default; _lastAntiJitterPosition = default; _lastNonNetworkedCollider = default; _predictionError = default; if (cleanup == true) { _isSpawned = default; _isInFixedUpdate = default; _hasManualUpdate = default; _settings.CopyFromOther(_defaultSettings); _collider.Destroy(); _onSpawn = default; ResolveCollision = default; OnCollisionEnter = default; OnCollisionExit = default; GetExternalProcessors = default; } } private void PublishFixedData(bool render, bool history) { if (render == true) { _renderData.CopyFromOther(_fixedData); } if (history == true) { KCCData historyData = _historyData[_fixedData.Tick % HISTORY_SIZE]; if (historyData == null) { historyData = new KCCData(); _historyData[_fixedData.Tick % HISTORY_SIZE] = historyData; } historyData.CopyFromOther(_fixedData); } } } }