namespace TPSBR { using System; using System.Collections.Generic; using Unity.Profiling; using UnityEngine; using Fusion; using Fusion.Addons.KCC; // !!! WARNING !!! // This processor is a 1:1 copy of default PlatformProcessor except it supports only single platform to reduce network state size to a minimum. // !!! WARNING !!! /// /// This processor tracks overlapping platforms (KCC processors implementing IPlatform) and propagates their position and rotation changes to KCC. /// Make sure the script that moves with the IPlatform object has lower execution order => it must be executed before PlatformProcessor and PlatformProcessorUpdater. /// When PlatformProcessor propagates all platform changes, it notifies IPlatformListener processors with absolute transform deltas. /// [DefaultExecutionOrder(BRPlatformProcessor.EXECUTION_ORDER)] [RequireComponent(typeof(NetworkObject))] public unsafe sealed class BRPlatformProcessor : NetworkKCCProcessor, IKCCProcessor, IBeginMove, IEndMove { // CONSTANTS public const int EXECUTION_ORDER = -400; public const int MAX_PLATFORMS = 1; // PRIVATE MEMBERS [SerializeField][Tooltip("How long it takes to move the KCC from world space to platform space.")] private float _platformSpaceTransitionDuration = 0.75f; [SerializeField][Tooltip("How long it takes to move the KCC from platform space to world space.")] private float _worldSpaceTransitionDuration = 0.5f; [Networked] private ref ProcessorState _state => ref MakeRef(); private KCC _kcc; private Platform[] _renderPlatforms = new Platform[MAX_PLATFORMS]; private static List _cachedPlatforms = new List(); private static List _cachedNetworkIds1 = new List(); private static List _cachedNetworkIds2 = new List(); private static ProfilerMarker _fixedUpdateMarker = new ProfilerMarker(nameof(BRPlatformProcessor)); // PUBLIC METHODS /// /// Returns true if there is at least one platorm tracked. /// public bool IsActive() { return _state.IsActive; } /// /// Called by PlatformProcessorUpdater. Do not use from user code. /// public void ProcessFixedUpdate() { if (ReferenceEquals(_kcc, null) == true) return; _fixedUpdateMarker.Begin(); if (Object.IsInSimulation != _kcc.Object.IsInSimulation) { // Synchronize simulation state of the processor with KCC. Runner.SetIsSimulated(Object, _kcc.Object.IsInSimulation); } if (Object.IsInSimulation == true) { // Update state of platforms, track new, cleanup old. UpdatePlatforms(_kcc); if (_state.IsActive == true) { // For predicted KCC, propagate position and rotation deltas of all platforms since last fixed update. PropagateMovement(_kcc, _kcc.FixedData, true); // Copy fixed state to render state as a base. SynchronizeRenderPlatforms(); } } else { // Otherwise snap the KCC to tracked platforms based on interpolated offsets. // Notice we modify only position, this is essential to get correct results from KCC physics queries. Rotation keeps unchanged. if (TrySetInterpolatedPosition(_kcc, _kcc.FixedData) == true) { _kcc.SynchronizeTransform(true, false, false); } } _fixedUpdateMarker.End(); } /// /// Called by PlatformProcessorUpdater. Do not use from user code. /// public void ProcessRender() { if (ReferenceEquals(_kcc, null) == true) return; if (_kcc.IsPredictingInRenderUpdate == true) { if (_state.IsActive == true) { // For render-predicted KCC, propagate position and rotation deltas of all platforms since last fixed or render update. PropagateMovement(_kcc, _kcc.RenderData, false); } } else { // Otherwise snap the KCC to tracked platforms based on interpolated offsets. // Notice we modify only position, this is essential to get correct results from KCC physics queries. Rotation keeps unchanged. if (TrySetInterpolatedPosition(_kcc, _kcc.RenderData) == true) { _kcc.SynchronizeTransform(true, false, false); } } } // NetworkBehaviour INTERFACE public override sealed void Spawned() { Runner.GetSingleton().Register(this); } public override sealed void Despawned(NetworkRunner runner, bool hasState) { runner.GetSingleton().Unregister(this); _kcc = null; } // NetworkKCCProcessor INTERFACE public override float GetPriority(KCC kcc) => float.MinValue; public override void OnEnter(KCC kcc, KCCData data) { _kcc = kcc; } public override void OnExit(KCC kcc, KCCData data) { _kcc = null; } public override void OnInterpolate(KCC kcc, KCCData data) { // This code path can be executed for: // 1. Proxy interpolated in fixed update. // 2. Proxy interpolated in render update. // 3. Input/State authority interpolated in render update. // For KCC proxy, KCCData.TargetPosition equals to snapshot interpolated position at this point. // However platforms are predicted everywhere - on all server and clients. // If a platform is predicted and KCC proxy interpolated, it results in KCC visual being delayed behind the platform visual. // Following code recalculates KCC position by snapping it to predicted platform space, matching position of the platform visual. // [KCC position] = [local IPlatform position] + [interpolated IPlatform => KCC offset]. TrySetInterpolatedPosition(kcc, data); } // IKCCProcessor INTERFACE bool IKCCProcessor.IsActive(KCC kcc) => _state.IsActive; // IBeginMove INTERFACE float IKCCStage.GetPriority(KCC kcc) => float.MaxValue; void IKCCStage.Execute(BeginMove stage, KCC kcc, KCCData data) { // Disable prediction correction and anti-jitter if there is at least one platform tracked. // This must be called in both fixed and render update. kcc.SuppressFeature(EKCCFeature.PredictionCorrection); kcc.SuppressFeature(EKCCFeature.AntiJitter); } // IEndMove INTERFACE float IKCCStage.GetPriority(KCC kcc) => float.MinValue; void IKCCStage.Execute(EndMove stage, KCC kcc, KCCData data) { bool isInFixedUpdate = kcc.IsInFixedUpdate; // Update Platform => KCC offset after KCC moves. for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { Platform platform = GetPlatform(i, isInFixedUpdate); if (platform.State != EPlatformState.None) { platform.KCCOffset = Quaternion.Inverse(platform.Rotation) * (data.TargetPosition - platform.Position); SetPlatform(i, platform, isInFixedUpdate); } } } // PRIVATE METHODS private void UpdatePlatforms(KCC kcc) { // 1. Get all platform objects tracked by KCC. kcc.GetProcessors(_cachedPlatforms); // Early exit - performance optimziation. if (_cachedPlatforms.Count <= 0 && _state.IsActive == false) return; _cachedNetworkIds1.Clear(); // Used to store platforms tracked by KCC. _cachedNetworkIds2.Clear(); // Used to store platforms tracked by PlatformProcessor. foreach (IPlatform platform in _cachedPlatforms) { _cachedNetworkIds1.Add(platform.Object.Id); } // 2. Mark all platforms in PlatformProcessor state as inactive if they are not tracked by KCC. for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { Platform platform = _state.Platforms.Get(i); if (platform.State == EPlatformState.Active && _cachedNetworkIds1.Contains(platform.Id) == false) { platform.State = EPlatformState.Inactive; _state.Platforms.Set(i, platform); } if (platform.Id.IsValid == true) { _cachedNetworkIds2.Add(platform.Id); } } // 3. Register all platforms tracked by KCC that are not tracked by PlatformProcessor. foreach (IPlatform trackedPlatform in _cachedPlatforms) { NetworkObject platformObject = trackedPlatform.Object; if (_cachedNetworkIds2.Contains(platformObject.Id) == false) { // The platform is not yet tracked by PlatformProcessor. Let's try adding it. for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { if (_state.Platforms.Get(i).State == EPlatformState.None) { _cachedNetworkIds2.Add(platformObject.Id); platformObject.transform.GetPositionAndRotation(out Vector3 platformPosition, out Quaternion platformRotation); Platform platform = new Platform(); platform.Id = platformObject.Id; platform.State = EPlatformState.Active; platform.Alpha = default; platform.Position = platformPosition; platform.Rotation = platformRotation; platform.KCCOffset = Quaternion.Inverse(platformRotation) * (kcc.Transform.position - platformPosition); _state.Platforms.Set(i, platform); break; } } } } bool isActive = false; // 4. Update platforms alpha values. // The platform alpha defines how much is the KCC position affected by the platform and is used for smooth transition from from world space to platform space. for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { Platform platform = _state.Platforms.Get(i); if (platform.State == EPlatformState.Active) { isActive = true; if (platform.Alpha < 1.0f) { // The KCC stands within the platform, increasing alpha to 1.0f. platform.Alpha = _platformSpaceTransitionDuration > 0.001f ? Mathf.Min(platform.Alpha + Runner.DeltaTime / _platformSpaceTransitionDuration, 1.0f) : 1.0f; _state.Platforms.Set(i, platform); } } else if (platform.State == EPlatformState.Inactive) { // The KCC left the the platform, decreasing alpha to 0.0f. platform.Alpha -= _worldSpaceTransitionDuration > 0.001f ? (Runner.DeltaTime / _worldSpaceTransitionDuration) : 1.0f; if (platform.Alpha <= 0.0f) { // Once the alpha is 0.0f, we can remove the platform entirely. platform = default; } else { isActive = true; } _state.Platforms.Set(i, platform); } } _state.IsActive = isActive; } private void PropagateMovement(KCC kcc, KCCData data, bool isInFixedUpdate) { bool synchronize = false; Vector3 basePosition = data.TargetPosition; Quaternion baseRotation = data.TransformRotation; // 1. Iterate over all tracked platforms, calculate their position and rotation deltas and propagate them to the KCC. for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { Platform platform = GetPlatform(i, isInFixedUpdate); if (platform.State != EPlatformState.Active || platform.Id.IsValid == false) continue; NetworkObject platformObject = Runner.FindObject(platform.Id); if (platformObject == null || platformObject.TryGetComponent(out IPlatform synchronizePlatform) == false) continue; platformObject.transform.GetPositionAndRotation(out Vector3 currentPlatformPosition, out Quaternion currentPlatformRotation); // Calculate platform position and rotation delta since last update. Vector3 platformPositionDelta = currentPlatformPosition - platform.Position; Quaternion platformRotationDelta = Quaternion.Inverse(platform.Rotation) * currentPlatformRotation; if (platform.State == EPlatformState.Inactive) { // With decreasing alpha we are also lowering the impact of platform transform changes. platformRotationDelta = Quaternion.Slerp(Quaternion.identity, platformRotationDelta, platform.Alpha); } // The platform rotated, we have to rotate stored KCC position offset. Vector3 recalculatedKCCOffset = platformRotationDelta * platform.KCCOffset; // Calculate delta between old and new KCC position offset. This needs to be added to KCC to stay on a platform spot. Vector3 kccOffsetDelta = recalculatedKCCOffset - platform.KCCOffset; // Final KCC position delta is calculated as sum of platform delta and KCC offset delta. // Notice the KCC offset is in platform local space so it needs to be rotated. Vector3 kccPositionDelta = platformPositionDelta + currentPlatformRotation * kccOffsetDelta; if (platform.State == EPlatformState.Inactive) { // With decreasing alpha we are also lowering the impact of platform transform changes. kccPositionDelta = Vector3.Lerp(Vector3.zero, kccPositionDelta, platform.Alpha); } // Propagate calculated position delta to the KCC. data.BasePosition += kccPositionDelta; data.DesiredPosition += kccPositionDelta; data.TargetPosition += kccPositionDelta; // Propagate rotation delta to the KCC. data.AddLookRotation(0.0f, platformRotationDelta.eulerAngles.y); // Update platform properties with new values. platform.Position = currentPlatformPosition; platform.Rotation = currentPlatformRotation; platform.KCCOffset = recalculatedKCCOffset; // Update PlatformProcessor state. SetPlatform(i, platform, isInFixedUpdate); // Set flag to synchronize Transform and Ridigbody components. synchronize = true; } // 2. Deltas from all platforms are propagated, now we have to recalculate Platform => KCC offsets. for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { Platform platform = GetPlatform(i, isInFixedUpdate); if (platform.State != EPlatformState.None) { // Offset needs to be calculated for both Active and Inactive platforms. platform.KCCOffset = Quaternion.Inverse(platform.Rotation) * (data.TargetPosition - platform.Position); // Update PlatformProcessor state. SetPlatform(i, platform, isInFixedUpdate); } } if (synchronize == true) { // There is at least one platform tracked, Transform and Rigidbody should be refreshed before any KCC begins predicted move. kcc.SynchronizeTransform(true, true, false); Vector3 positionDelta = data.TargetPosition - basePosition; Quaternion rotationDelta = Quaternion.Inverse(baseRotation) * data.TransformRotation; // Notify all listeners. foreach (IPlatformListener listener in kcc.GetProcessors(true)) { try { listener.OnTransform(kcc, data, positionDelta, rotationDelta); } catch (Exception exception) { Debug.LogException(exception); } } } } private bool TrySetInterpolatedPosition(KCC kcc, KCCData data) { // At this point all platforms (IPlatform) should have updated their transforms. // This method calculates interpolated position of the KCC by taking local platform positions + interpolated Position => KCC offsets. // Calculations below result in smooth transition between world and multiple platform spaces. bool buffersValid = TryGetSnapshotsBuffers(out NetworkBehaviourBuffer fromBuffer, out NetworkBehaviourBuffer toBuffer, out float alpha); if (buffersValid == false) return false; bool isInSimulation = Object.IsInSimulation; Vector3 averagePosition = default; float averageAlpha = default; ProcessorState fromState = fromBuffer.ReinterpretState(); ProcessorState toState = toBuffer.ReinterpretState(); for (int i = 0; i < toState.Platforms.Length; ++i) { Platform fromPlatform = fromState.Platforms.Get(i); Platform toPlatform = toState.Platforms.Get(i); if (fromPlatform.State == EPlatformState.None) { if (toPlatform.State == EPlatformState.None) continue; // Only To is valid => the KCC just jumped on the platform. // Render interpolated KCC with predicted fixed simulation - for perfect snapping we want to interpolate only if the state is Active (KCC stands within the platform trigger). // Otherwise the KCC could penetrate geometry while keeping Inactive state during platform-space => world-space transition, which is undesired. if (isInSimulation == true && toPlatform.State != EPlatformState.Active) continue; NetworkObject toPlatformObject = Runner.FindObject(toPlatform.Id); if (toPlatformObject == null) continue; // In following calculations we're interpolating between [world-space interpolated KCC position] and [platform-space interpolated KCC position]. toPlatformObject.transform.GetPositionAndRotation(out Vector3 platformPosition, out Quaternion platformRotation); Vector3 kccPosition = data.TargetPosition; Vector3 toPosition = platformPosition + platformRotation * toPlatform.KCCOffset; if (kcc.GetInterpolatedNetworkBufferPosition(out Vector3 interpolatedKCCPosition) == true) { kccPosition = interpolatedKCCPosition; } averagePosition += Vector3.Lerp(kccPosition, toPosition, alpha) * toPlatform.Alpha; averageAlpha += toPlatform.Alpha; } else if (toPlatform.State == EPlatformState.None) { if (fromPlatform.State == EPlatformState.None) continue; // Only From is valid => the KCC just left the platform. // Render interpolated KCC with predicted fixed simulation - for perfect snapping we want to interpolate only if the state is Active (KCC stands within the platform trigger). // Otherwise the KCC could penetrate geometry while keeping Inactive state during platform-space => world-space transition, which is undesired. if (isInSimulation == true && fromPlatform.State != EPlatformState.Active) continue; NetworkObject fromPlatformObject = Runner.FindObject(fromPlatform.Id); if (fromPlatformObject == null) continue; // In following calculations we're interpolating between [world-space interpolated KCC position] and [platform-space interpolated KCC position]. fromPlatformObject.transform.GetPositionAndRotation(out Vector3 platformPosition, out Quaternion platformRotation); Vector3 fromPosition = platformPosition + platformRotation * fromPlatform.KCCOffset; Vector3 kccPosition = data.TargetPosition; if (kcc.GetInterpolatedNetworkBufferPosition(out Vector3 interpolatedKCCPosition) == true) { kccPosition = interpolatedKCCPosition; } averagePosition += Vector3.Lerp(fromPosition, kccPosition, alpha) * fromPlatform.Alpha; averageAlpha += fromPlatform.Alpha; } else { if (toPlatform.Id != fromPlatform.Id) continue; // From and To are same platform objects. // Render interpolated KCC with predicted fixed simulation - for perfect snapping we want to interpolate only if the state is Active (KCC stands within the platform trigger). // Otherwise the KCC could penetrate geometry while keeping Inactive state during platform-space => world-space transition, which is undesired. if (isInSimulation == true && (fromPlatform.State != EPlatformState.Active || toPlatform.State != EPlatformState.Active)) continue; NetworkObject platformObject = Runner.FindObject(toPlatform.Id); if (platformObject == null) continue; // In following calculations we're interpolating between two platform-space interpolated KCC positions. float platformAlpha = Mathf.Lerp(fromPlatform.Alpha, toPlatform.Alpha, alpha); Vector3 platformRelativeOffset = Vector3.Lerp(fromPlatform.KCCOffset, toPlatform.KCCOffset, alpha); platformObject.transform.GetPositionAndRotation(out Vector3 platformPosition, out Quaternion platformRotation); averagePosition += (platformPosition + platformRotation * platformRelativeOffset) * platformAlpha; averageAlpha += platformAlpha; } } if (averageAlpha < 0.001f) return false; // Final position equals to weighted average of snap-interpolated KCC positions. data.TargetPosition = averagePosition / averageAlpha; return true; } // HELPER METHODS private Platform GetPlatform(int index, bool isInFixedUpdate) { return isInFixedUpdate == true ? _state.Platforms.Get(index) : _renderPlatforms[index]; } private void SetPlatform(int index, Platform platform, bool isInFixedUpdate) { if (isInFixedUpdate == true) { _state.Platforms.Set(index, platform); } _renderPlatforms[index] = platform; } private void SynchronizeRenderPlatforms() { for (int i = 0, count = _state.Platforms.Length; i < count; ++i) { _renderPlatforms[i] = _state.Platforms.Get(i); } } // DATA STRUCTURES public enum EPlatformState { None = 0, Active = 1, Inactive = 2, } public struct Platform : INetworkStruct { public NetworkId Id; public EPlatformState State; public float Alpha; public Vector3 Position; public Quaternion Rotation; public Vector3 KCCOffset; } public struct ProcessorState : INetworkStruct { public int Flags; [Networked][Capacity(MAX_PLATFORMS)] public NetworkArray Platforms => default; public bool IsActive { get => (Flags & 1) == 1; set { if (value) { Flags |= 1; } else { Flags &= ~1; } } } } } }