using R3; using RebootKit.Engine.Extensions; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; using RebootKit.Engine.Simulation.Sensors; using RebootReality.jelycho.Items; using Unity.Collections; using Unity.Mathematics; using Unity.Netcode; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootReality.jelycho.Player { public class PlayerActor : Actor { static readonly Logger s_Logger = new Logger(nameof(PlayerActor)); [SerializeField] Animator m_Animator; [Header("Movement")] [SerializeField] PlayerFPPLocomotion m_Locomotion; [Header("Camera")] [SerializeField] FPPCamera m_Camera; [SerializeField] CameraSpring m_CameraSpring; [SerializeField, Range(0.0f, 1.0f), Tooltip("Percentage of run speed")] float m_EnableCameraBobbingPercentThreshold = 0.5f; [SerializeField] float m_SprintCameraBobbing = 1.0f; [SerializeField] float m_RunCameraBobbing = 0.5f; [SerializeField] float m_IdleCameraBobbing = 0.0f; [SerializeField] float m_CameraBobbingTransitionSpeed = 5.0f; float m_TargetCameraBobbing = 0.0f; float m_CurrentCameraBobbing = 0.0f; [Header("Character")] [SerializeField] Transform m_CharacterRootTransform; [SerializeField] Transform m_HeadBoneTransform; [SerializeField] Transform m_HeadAimTargetTransform; [SerializeField] Transform m_CharacterForwardTransform; [SerializeField, Range(0.0f, 90.0f)] float m_CharacterRotateDeadAngle = 5.0f; [SerializeField, Range(0.0f, 90.0f)] float m_CharacterRotateSoftAngle = 90.0f; [SerializeField] float m_CharacterRotateSpeed = 180.0f; [SerializeField] float m_CharacterRotateFastSpeed = 720.0f; float m_CharacterTurnVelocity = 0.0f; [Header("Dragging")] [SerializeField] Transform m_DragGutStartPosition; [SerializeField] PhysicsObjectDragger m_PhysicsDragger; [SerializeField] FloatRange m_DragDistanceRange = new FloatRange(1.0f, 5.0f); [Header("Beacon location picking")] [SerializeField] LayerMask m_BeaconPlacementLayerMask = 0; [SerializeField] float m_BeaconPlacementMaxDistance = 15.0f; [SerializeField] float m_NormalDotUpThreshold = 0.5f; [Header("Sensors")] [SerializeField] SingleRaySensor m_InteractablesSensor; [Header("Network")] [SerializeField] float m_MinTeleportDistance = 0.5f; [SerializeField] float m_ItemPickupDistance = 2.0f; bool m_IsSetupAsOwner = false; float m_SyncRemoteStateTimer = 0.0f; RemotePlayerActorState m_RemoteState; [Header("Inventory")] [SerializeField] int m_InventorySize = 10; public Inventory Inventory { get; private set; } ItemActor m_EquippedItem; public ReactiveProperty SelectedInventorySlot { get; private set; } = new ReactiveProperty(0); public float3 LookDirection { get { float pitchRad = math.radians(-m_Camera.Pitch); float yawRad = math.radians(m_Camera.Yaw); return new float3(math.sin(yawRad) * math.cos(pitchRad), math.sin(pitchRad), math.cos(yawRad) * math.cos(pitchRad)); } } readonly ReactiveProperty m_TargetInteractable = new ReactiveProperty(null); public ReadOnlyReactiveProperty TargetInteractable { get { return m_TargetInteractable; } } protected override IActorData CreateActorData() { return new PlayerActorData { }; } void Awake() { Inventory = new Inventory(m_InventorySize); // @NOTE: By default player actor should be set up as remote SetupAsRemote(); } void Start() { m_CameraSpring.Initialize(); } void OnEnable() { Inventory.OnItemPickedUp += OnItemPickedUp; Inventory.OnItemDropped += OnItemDropped; } void OnDisable() { Inventory.OnItemPickedUp -= OnItemPickedUp; Inventory.OnItemDropped -= OnItemDropped; } // // @MARK: Actor // public override void OnServerTick(float deltaTime) { base.OnServerTick(deltaTime); // Update actor data // PlayerActorData data = DataAs(); // IsDataDirty = true; NativeArray remoteStateData = DataSerializationUtils.Serialize(m_RemoteState); SendActorEvent((ushort) PlayerActorEvents.UpdatedRemoteState, remoteStateData); } public override void OnClientTick(float deltaTime) { base.OnClientTick(deltaTime); if (m_IsSetupAsOwner) { TickCamera(); UpdateAnimator(m_Locomotion.Velocity); SenseInteractable(); m_SyncRemoteStateTimer -= deltaTime; if (m_SyncRemoteStateTimer <= 0.0f) { m_SyncRemoteStateTimer = 1.0f / NetworkSystem.TickRate.IndexValue; RemotePlayerActorState remoteState = new RemotePlayerActorState { Position = transform.position, Velocity = m_Locomotion.Velocity, LookPitch = m_Camera.Pitch, LookYaw = m_Camera.Yaw, IsGrounded = m_Locomotion.IsGrounded }; NativeArray data = DataSerializationUtils.Serialize(remoteState); SendActorCommand((ushort) PlayerActorCommands.UpdateRemoteState, data); } } else { InterpolateActorState(deltaTime); } TickCharacterRotation(); } protected override void OnActorCommandServer(ActorCommand actorCommand) { base.OnActorCommandServer(actorCommand); if (actorCommand.CommandID == (ushort) PlayerActorCommands.UpdateRemoteState) { RemotePlayerActorState remoteState = new RemotePlayerActorState(); DataSerializationUtils.Deserialize(actorCommand.Data, ref remoteState); m_RemoteState = remoteState; } else if (actorCommand.CommandID == (ushort) PlayerActorCommands.PickupItem) { PlayerActorPickupItemCommand command = new PlayerActorPickupItemCommand(); DataSerializationUtils.Deserialize(actorCommand.Data, ref command); Actor itemActor = RR.FindSpawnedActor(command.ItemActorID); if (itemActor is ItemActor item) { if (math.distancesq(itemActor.transform.position, m_HeadBoneTransform.position) <= m_ItemPickupDistance * m_ItemPickupDistance) { if (Inventory.TryPickup(item)) { s_Logger.Info($"Item {item.name} picked up successfully by player {name}."); UpdateEquippedItem(); } else { s_Logger.Info($"Failed to pick up item {item.name}. Inventory is full."); } } else { s_Logger.Info($"Item actor {item.name} is too far away to pick up."); } } else { s_Logger.Error($"Item actor with ID {command.ItemActorID} not found."); } } else if (actorCommand.CommandID == (ushort) PlayerActorCommands.SelectItemSlot) { PlayerActorSelectItemSlotCommand command = new PlayerActorSelectItemSlotCommand(); DataSerializationUtils.Deserialize(actorCommand.Data, ref command); if (command.SlotIndex < 0 || command.SlotIndex >= Inventory.SlotsCount) { s_Logger.Error($"Invalid slot index {command.SlotIndex}. Must be between 0 and {Inventory.SlotsCount - 1}."); return; } SelectedInventorySlot.Value = command.SlotIndex; UpdateEquippedItem(); } else if (actorCommand.CommandID == (ushort) PlayerActorCommands.DropItem) { PlayerActorDropItemCommand command = new PlayerActorDropItemCommand(); DataSerializationUtils.Deserialize(actorCommand.Data, ref command); if (command.Count == 1) { Inventory.TryDropOne(command.InventorySlotIndex); } else { s_Logger.Error("DropItem command with count != 1 is not supported yet."); } } } protected override void OnActorEventClient(ActorEvent actorEvent) { base.OnActorEventClient(actorEvent); if (actorEvent.EventID == (ushort) PlayerActorEvents.UpdatedRemoteState) { RemotePlayerActorState remoteState = new RemotePlayerActorState(); DataSerializationUtils.Deserialize(actorEvent.Data, ref remoteState); m_RemoteState = remoteState; } else if (actorEvent.EventID == (ushort) PlayerActorEvents.PrimaryEquippedItemChanged) { PlayerActorPrimaryEquippedItemChangedEvent itemChangedEvent = new PlayerActorPrimaryEquippedItemChangedEvent(); DataSerializationUtils.Deserialize(actorEvent.Data, ref itemChangedEvent); if (itemChangedEvent.ItemActorID == 0) { m_EquippedItem = null; } else { Actor itemActor = RR.FindSpawnedActor(itemChangedEvent.ItemActorID); if (itemActor is ItemActor item) { s_Logger.Info($"Primary equipped item changed to {item.name} for player {name}."); m_EquippedItem = item; } else { s_Logger.Error($"Primary equipped item with ID {itemChangedEvent.ItemActorID} not found."); } } } } // // @MARK: Controller API // public void SetSprint(bool isSprinting) { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot set sprint state when not set up as owner."); return; } m_Locomotion.SetSprint(isSprinting); } public void Jump() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot jump when not set up as owner."); return; } m_Locomotion.Jump(); } public void Look(Vector2 input) { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot look when not set up as owner."); return; } m_Camera.Rotate(input.x, input.y); } public void SetMoveInput(Vector2 input) { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot set move input when not set up as owner."); return; } float3 direction = Quaternion.AngleAxis(m_Camera.Yaw, Vector3.up) * new float3(input.x, 0.0f, input.y); m_Locomotion.SetWishDirection(direction); } public void StartDrag() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot start dragging when not set up as owner."); return; } GameObject pickedGameObject = m_Camera.Sensor.Sense(); if (pickedGameObject != null && pickedGameObject.TryGetComponent(out Rigidbody rigidbody)) { m_PhysicsDragger.Grab(rigidbody); } } public void StopDrag() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot stop dragging when not set up as owner."); return; } m_PhysicsDragger.Drop(); } public void DropItem() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot drop item when not set up as owner."); return; } if (m_EquippedItem != null) { PlayerActorDropItemCommand command = new PlayerActorDropItemCommand { InventorySlotIndex = SelectedInventorySlot.Value, Count = 1 }; SendActorCommand((ushort) PlayerActorCommands.DropItem, ref command); } } public void PrimaryAction() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot perform primary action when not set up as owner."); return; } // if (!IsOwner()) { // s_Logger.Error("Only the owner can perform primary actions."); // return; // } // if (TryGetBeaconPosition(out Vector3 beaconPosition)) { // SetAnimatorTriggerRpc(AnimatorParamHashes.Throw); // // if (RR.World.Context is WorldContext worldContext) { // worldContext.BaseManager.TrySpawnBeacon(beaconPosition); // } // } } public void SecondaryAction() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot perform secondary action when not set up as owner."); return; } m_Animator.SetTrigger(AnimatorParamHashes.Block); } public void Interact() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot perform interaction when not set up as owner."); return; } if (m_TargetInteractable.Value is ItemActor itemActor) { Pickup(itemActor); } else if (m_TargetInteractable.Value is not null) { m_TargetInteractable.Value.Interact(); // SetAnimatorTriggerRpc(AnimatorParamHashes.Throw); } } // // @MARK: Owner // public void SetupAsOwner() { m_Camera.enabled = true; m_Camera.Camera.enabled = true; m_Locomotion.enabled = true; if (TryGetComponent(out Rigidbody rbody)) { rbody.isKinematic = false; } m_IsSetupAsOwner = true; } void TickCamera() { // Camera Stuff m_Camera.Tick(); if (m_Locomotion.IsGrounded && m_Locomotion.SpeedXZ >= m_Locomotion.runSpeed * m_EnableCameraBobbingPercentThreshold) { if (m_Locomotion.IsSprinting) { m_TargetCameraBobbing = m_SprintCameraBobbing; } else { m_TargetCameraBobbing = m_RunCameraBobbing; } } else { m_TargetCameraBobbing = m_IdleCameraBobbing; } m_CurrentCameraBobbing = Mathf.MoveTowards(m_CurrentCameraBobbing, m_TargetCameraBobbing, m_CameraBobbingTransitionSpeed * Time.deltaTime); m_Camera.SetBobbing(m_CurrentCameraBobbing); m_CameraSpring.UpdateSpring(Time.deltaTime, m_CharacterForwardTransform.up, m_CharacterForwardTransform.right, m_CharacterForwardTransform.forward); } void SenseInteractable() { IInteractable interactable = m_InteractablesSensor.Sense(); if (interactable != m_TargetInteractable.Value) { m_TargetInteractable.Value = interactable; } } // // @MARK: Remote // void SetupAsRemote() { m_Camera.enabled = false; m_Camera.Camera.enabled = false; m_Locomotion.enabled = false; if (TryGetComponent(out Rigidbody rbody)) { rbody.isKinematic = true; } m_IsSetupAsOwner = false; } void InterpolateActorState(float deltaTime) { Vector3 targetPosition = m_RemoteState.Position; if ((transform.position - m_RemoteState.Position).sqrMagnitude < m_MinTeleportDistance * m_MinTeleportDistance) { targetPosition = Vector3.MoveTowards(transform.position, m_RemoteState.Position, m_Locomotion.runSpeed * deltaTime); } m_Locomotion.WarpTo(targetPosition); m_Camera.Pitch = m_RemoteState.LookPitch; m_Camera.Yaw = m_RemoteState.LookYaw; UpdateAnimator(m_RemoteState.Velocity); } // // @MARK: Server // void OnItemDropped(ItemActor item) { if (!RR.IsServer()) { return; } // @BUG: Sometimes the item will not update it's physics state and will keep floating in the air. It's rare? UpdateEquippedItem(); item.SetHidden(false); item.UnMount(); item.transform.position = m_HeadBoneTransform.position + m_HeadBoneTransform.forward * 1.0f; item.transform.rotation = Quaternion.LookRotation(m_HeadBoneTransform.forward, Vector3.up); } void OnItemPickedUp(ItemActor item) { if (!RR.IsServer()) { return; } item.SetHidden(true); } void UpdateEquippedItem() { if (!RR.IsServer()) { s_Logger.Error("Only the server can update selected item."); return; } ItemActor itemActor = Inventory.GetFirstItem(SelectedInventorySlot.Value); if (itemActor == m_EquippedItem) { return; } if (m_EquippedItem != null) { m_EquippedItem.SetHidden(true); } m_EquippedItem = itemActor; if (m_EquippedItem != null) { m_EquippedItem.SetHidden(false); m_EquippedItem.MountTo(this, "hand_right"); } PlayerActorPrimaryEquippedItemChangedEvent itemChangedEvent = new PlayerActorPrimaryEquippedItemChangedEvent { ItemActorID = m_EquippedItem != null ? m_EquippedItem.ActorID : 0 }; SendActorEvent((ushort) PlayerActorEvents.PrimaryEquippedItemChanged, ref itemChangedEvent); } public void WarpTo(Vector3 position) { if (!RR.IsServer()) { s_Logger.Error("Only the server can warp players."); return; } m_Locomotion.WarpTo(position); } // // @MARK: Common // void TickCharacterRotation() { float3 targetCharacterForward = math.normalize(LookDirection.With(y: 0.0f)); float3 currentCharacterForward = math.normalize(m_CharacterForwardTransform.forward.With(y: 0.0f)); float angleRad = math.acos(math.clamp(math.dot(targetCharacterForward, currentCharacterForward) / (math.length(targetCharacterForward) * math.length(currentCharacterForward)), -1f, 1f)); float angleDeg = math.degrees(angleRad); bool rotateCharacter = false; float rotateCharacterSpeed = m_CharacterRotateSpeed; m_CharacterTurnVelocity = 0.0f; if (math.abs(angleDeg) > m_CharacterRotateDeadAngle) { if (math.abs(angleDeg) < m_CharacterRotateSoftAngle) { rotateCharacter = true; } else { rotateCharacter = true; rotateCharacterSpeed = m_CharacterRotateFastSpeed; } } float velocityForward = m_Locomotion.Velocity.z; if (!rotateCharacter && math.abs(velocityForward) > 0.01f) { rotateCharacter = true; } if (rotateCharacter) { m_CharacterTurnVelocity = rotateCharacterSpeed * Time.deltaTime; Vector3 newForward = Vector3.RotateTowards(currentCharacterForward, targetCharacterForward, math.radians(m_CharacterTurnVelocity), 0.0f); m_CharacterForwardTransform.forward = newForward; } // Aim Target adjustment m_HeadAimTargetTransform.position = (float3) m_HeadBoneTransform.position + LookDirection * 5.0f; } // // @MARK: Sensors // bool TryGetBeaconPosition(out Vector3 position) { Ray ray = new Ray(m_Camera.Camera.transform.position, m_Camera.Camera.transform.forward); if (Physics.Raycast(ray, out RaycastHit hit, m_BeaconPlacementMaxDistance, m_BeaconPlacementLayerMask) && Vector3.Dot(hit.normal, Vector3.up) >= m_NormalDotUpThreshold) { position = hit.point; return true; } position = Vector3.zero; return false; } // // @MARK: Inventory // void Pickup(ItemActor actor) { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot pick up items when not set up as owner."); return; } PlayerActorPickupItemCommand command = new PlayerActorPickupItemCommand { ItemActorID = actor.ActorID }; SendActorCommand((ushort) PlayerActorCommands.PickupItem, ref command); } public void SelectPreviousItemSlot() { if (!m_IsSetupAsOwner) { s_Logger.Error("Only the owner can change inventory selection."); return; } if (SelectedInventorySlot.Value > 0) { SelectedInventorySlot.Value--; } else { SelectedInventorySlot.Value = Inventory.SlotsCount - 1; } PlayerActorSelectItemSlotCommand command = new PlayerActorSelectItemSlotCommand(); command.SlotIndex = SelectedInventorySlot.Value; SendActorCommand((ushort) PlayerActorCommands.SelectItemSlot, ref command); } public void SelectNextItemSlot() { if (!m_IsSetupAsOwner) { s_Logger.Error("Only the owner can change inventory selection."); return; } if (SelectedInventorySlot.Value < Inventory.SlotsCount - 1) { SelectedInventorySlot.Value++; } else { SelectedInventorySlot.Value = 0; } PlayerActorSelectItemSlotCommand command = new PlayerActorSelectItemSlotCommand(); command.SlotIndex = SelectedInventorySlot.Value; SendActorCommand((ushort) PlayerActorCommands.SelectItemSlot, ref command); } public void SelectItemSlot(int slotIndex) { if (!m_IsSetupAsOwner) { s_Logger.Error("Only the owner can change inventory selection."); return; } if (slotIndex < 0 || slotIndex >= Inventory.SlotsCount) { s_Logger.Error($"Invalid slot index {slotIndex}. Must be between 0 and {Inventory.SlotsCount - 1}."); return; } SelectedInventorySlot.Value = slotIndex; PlayerActorSelectItemSlotCommand command = new PlayerActorSelectItemSlotCommand(); command.SlotIndex = SelectedInventorySlot.Value; SendActorCommand((ushort) PlayerActorCommands.SelectItemSlot, ref command); } // // @MARK: Animations // struct AnimatorParamHashes { public static readonly int VelocityForwardNormalized = Animator.StringToHash("VelocityForwardNormalized"); public static readonly int VelocityRightNormalized = Animator.StringToHash("VelocityRightNormalized"); public static readonly int TurnVelocity = Animator.StringToHash("TurnVelocity"); public static readonly int IsGrounded = Animator.StringToHash("IsGrounded"); public static readonly int Attack = Animator.StringToHash("Attack"); public static readonly int Block = Animator.StringToHash("Block"); public static readonly int Throw = Animator.StringToHash("Throw"); public static readonly int Holding = Animator.StringToHash("Holding"); } void UpdateAnimator(Vector3 velocity) { Vector3 localVelocity = m_CharacterForwardTransform.InverseTransformDirection(velocity); float forwardNormalized = localVelocity.z / m_Locomotion.runSpeed; float rightNormalized = localVelocity.x / m_Locomotion.runSpeed; float turnVelocity = m_CharacterTurnVelocity; if (math.abs(forwardNormalized) > 0.01f || math.abs(rightNormalized) > 0.01f || !m_Locomotion.IsGrounded) { turnVelocity = 0.0f; } m_Animator.SetFloat(AnimatorParamHashes.VelocityForwardNormalized, forwardNormalized); m_Animator.SetFloat(AnimatorParamHashes.VelocityRightNormalized, rightNormalized); m_Animator.SetFloat(AnimatorParamHashes.TurnVelocity, turnVelocity); m_Animator.SetBool(AnimatorParamHashes.IsGrounded, m_Locomotion.IsGrounded); m_Animator.SetInteger(AnimatorParamHashes.Holding, 1); } } public class PlayerActorData : IActorData { public void Serialize(NetworkBufferWriter writer) { } public void Deserialize(NetworkBufferReader reader) { } public int GetMaxBytes() { return 0; } } public struct RemotePlayerActorState : IActorData { public Vector3 Position; public Vector3 Velocity; public float LookPitch; public float LookYaw; public bool IsGrounded; public void Serialize(NetworkBufferWriter writer) { writer.Write(Position); writer.Write(Velocity); writer.Write(LookPitch); writer.Write(LookYaw); writer.Write(IsGrounded); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out Position); reader.Read(out Velocity); reader.Read(out LookPitch); reader.Read(out LookYaw); reader.Read(out IsGrounded); } public int GetMaxBytes() { return sizeof(float) * 3 + // Position; sizeof(float) * 3 + // Velocity sizeof(float) * 2 + // LookPitch, LookYaw sizeof(bool); // IsGrounded } } // @MARK: Player Actor Commands enum PlayerActorCommands : ushort { None = 0x0000, UpdateRemoteState = 0x0001, PickupItem = 0x0002, DropItem = 0x0003, EquipItem = 0x0004, SelectItemSlot = 0x0005, } struct PlayerActorPickupItemCommand : IActorData { public ulong ItemActorID; public void Serialize(NetworkBufferWriter writer) { writer.Write(ItemActorID); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ItemActorID); } public int GetMaxBytes() { return sizeof(ulong); // ItemActorID } } struct PlayerActorDropItemCommand : IActorData { public int InventorySlotIndex; public int Count; public void Serialize(NetworkBufferWriter writer) { writer.Write(InventorySlotIndex); writer.Write(Count); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out InventorySlotIndex); reader.Read(out Count); } public int GetMaxBytes() { return sizeof(int) * 2; // InventorySlotIndex, Count } } struct PlayerActorEquipItemCommand : IActorData { public int InventorySlotIndex; public void Serialize(NetworkBufferWriter writer) { writer.Write(InventorySlotIndex); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out InventorySlotIndex); } public int GetMaxBytes() { return sizeof(int); // InventorySlotIndex } } struct PlayerActorSelectItemSlotCommand : IActorData { public int SlotIndex; public void Serialize(NetworkBufferWriter writer) { writer.Write(SlotIndex); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out SlotIndex); } public int GetMaxBytes() { return sizeof(int); // SlotIndex } } // @MARK: Player Actor Events enum PlayerActorEvents : ushort { None = 0x0000, PrimaryEquippedItemChanged = 0x0001, UpdatedRemoteState = 0x0002, } struct PlayerActorPrimaryEquippedItemChangedEvent : IActorData { public ulong ItemActorID; public void Serialize(NetworkBufferWriter writer) { writer.Write(ItemActorID); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ItemActorID); } public int GetMaxBytes() { return sizeof(ulong); // ItemActorID } } }