using System; using System.Collections.Generic; using RebootKit.Engine.Extensions; using RebootKit.Engine.Main; using RebootKit.Engine.Network; using RebootKit.Engine.Simulation; using RebootReality.jelycho.Enemies; using RebootReality.jelycho.Items; using Unity.Collections; using Unity.Mathematics; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootReality.jelycho.Player { public class PlayerActor : Actor, IKillable { static readonly Logger s_Logger = new Logger(nameof(PlayerActor)); [SerializeField] PlayerAnimator m_PlayerAnimator; [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; [SerializeField] float m_TurnTransitionSpeed = 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; float m_CharacterTurnVelocitySmooth = 0.0f; [Header("Animations")] [SerializeField] int m_HandsLayerIndex; [SerializeField] string m_HandsIdleStateName = "Hands Locomotion"; [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; } int m_SelectedInventorySlotIndex = 0; public int SelectedInventorySlot { get { return m_SelectedInventorySlotIndex; } set { m_SelectedInventorySlotIndex = value; OnSelectedInventorySlotChanged?.Invoke(value); } } public Action OnSelectedInventorySlotChanged = delegate {}; ItemActor m_EquippedItem; [SerializeField] float m_StartChargeDelay = 0.15f; bool m_IsCharging; float m_ChargeTimer; [SerializeField] float m_QuickAttackComboMaxDelay = 0.5f; enum QuickAttackState { None, PlayingAnimation, WaitingForNextAttack } QuickAttackState m_QuickAttackState; int m_QuickAttackComboCounter; float m_QuickAttackComboTimer; 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)); } } IInteractable m_TargetInteractable = null; public IInteractable TargetInteractable { get { return m_TargetInteractable; } private set { s_Logger.Info("NEW"); m_TargetInteractable = value; OnTargetInteractableChanged(value); } } public Action OnTargetInteractableChanged = delegate {}; readonly List m_AdditionalMountedActor = new List(); 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; m_PlayerAnimator.onQuickAttackFinished.AddListener(OnQuickAttackFinishedAnimation); m_PlayerAnimator.onChargeReady.AddListener(OnChargeReadyAnimation); } void OnDisable() { Inventory.OnItemPickedUp -= OnItemPickedUp; Inventory.OnItemDropped -= OnItemDropped; m_PlayerAnimator.onQuickAttackFinished.RemoveListener(OnQuickAttackFinishedAnimation); m_PlayerAnimator.onChargeReady.RemoveListener(OnChargeReadyAnimation); } // // @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 DropItem() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot drop item when not set up as owner."); return; } if (m_EquippedItem != null) { var command = new PlayerActorDropItemCommand { InventorySlotIndex = (byte) SelectedInventorySlot }; SendActorCommand((byte) PlayerActorCommands.DropItem, ref command); } } public void Kick() { if (!m_IsSetupAsOwner) { return; } m_PlayerAnimator.PlayKickAnimation(); } public void BeginPrimaryAction() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot begin primary action when not set up as owner."); return; } if (m_EquippedItem == null) { return; } m_IsCharging = false; m_ChargeTimer = 0.0f; } public void HoldingPrimaryAction() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot begin primary action when not set up as owner."); return; } if (m_EquippedItem == null) { return; } ItemConfig itemConfig = m_EquippedItem.Config; if (!m_IsCharging && itemConfig.isChargeable && m_EquippedItem.Config.chargeAction != null) { m_ChargeTimer += Time.deltaTime; if (m_ChargeTimer >= m_StartChargeDelay) { if (itemConfig.chargeAction.OnChargeStart(this, m_EquippedItem)) { SetChargingAnimation(); m_IsCharging = true; m_ChargeTimer = 0.0f; } } } if (m_IsCharging) { m_ChargeTimer += Time.deltaTime; itemConfig.chargeAction.OnChargeUpdate(this, m_EquippedItem, GetChargeProgress()); } } float GetChargeProgress() { if (m_EquippedItem == null || !m_EquippedItem.Config.isChargeable) { return 0.0f; } float chargeProgress = Mathf.InverseLerp(m_EquippedItem.Config.minChargeDuration, m_EquippedItem.Config.maxChargeDuration, m_ChargeTimer); return Mathf.Clamp01(chargeProgress); } public void EndPrimaryAction() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot end primary action when not set up as owner."); return; } if (m_EquippedItem == null) { return; } if (m_IsCharging) { ItemConfig itemConfig = m_EquippedItem.Config; if (m_ChargeTimer >= itemConfig.minChargeDuration) { float chargeProgress = GetChargeProgress(); itemConfig.chargeAction.OnChargeEnd(this, m_EquippedItem, chargeProgress); SetChargedUseAnimation(); } else { itemConfig.chargeAction.OnChargeCancel(this, m_EquippedItem); SetHandsIdleAnimation(); } if (RR.World.Context is WorldContext context) { context.FeedbacksManager.HideChargeReadyIndicator(); } m_IsCharging = false; } else if (m_EquippedItem.Config.canQuickAttack) { if (m_QuickAttackState == QuickAttackState.None) { m_QuickAttackComboCounter = 0; PlayQuickAttackAnimation(m_QuickAttackComboCounter); m_QuickAttackState = QuickAttackState.PlayingAnimation; if (m_EquippedItem.Config.quickAttackAction != null) { m_EquippedItem.Config.quickAttackAction.Attack(this, m_EquippedItem); } } else if (m_QuickAttackState == QuickAttackState.PlayingAnimation) { m_QuickAttackComboCounter = 0; } else if (m_QuickAttackState == QuickAttackState.WaitingForNextAttack) { m_QuickAttackComboCounter += 1; PlayQuickAttackAnimation(m_QuickAttackComboCounter); m_QuickAttackState = QuickAttackState.PlayingAnimation; if (m_EquippedItem.Config.quickAttackAction != null) { m_EquippedItem.Config.quickAttackAction.Attack(this, m_EquippedItem); } } } } void OnQuickAttackFinishedAnimation() { if (m_QuickAttackState != QuickAttackState.PlayingAnimation) { return; } m_QuickAttackComboTimer = m_QuickAttackComboMaxDelay; m_QuickAttackState = QuickAttackState.WaitingForNextAttack; if (m_IsSetupAsOwner) { if (RR.World.Context is WorldContext context) { context.FeedbacksManager.ShowQuickAttackIndicator(); } } } void OnChargeReadyAnimation() { if (!m_IsCharging) { return; } if (m_IsSetupAsOwner) { if (RR.World.Context is WorldContext context) { context.FeedbacksManager.ShowChargeReadyIndicator(); } } } public void SecondaryAction() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot perform secondary action when not set up as owner."); return; } if (m_IsCharging) { m_EquippedItem.Config.chargeAction.OnChargeCancel(this, m_EquippedItem); m_IsCharging = false; m_ChargeTimer = 0.0f; SetHandsIdleAnimation(); } } public void Interact() { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot perform interaction when not set up as owner."); return; } if (TargetInteractable is ItemActor itemActor) { Pickup(itemActor); } else if (TargetInteractable is not null) { TargetInteractable.Interact(); } } // // @MARK: Hands animations // void SetHandsIdleAnimation() { m_PlayerAnimator.PlayHandsIdle(); } void SetChargingAnimation() { m_PlayerAnimator.PlayCharging(); } void SetChargedUseAnimation() { m_PlayerAnimator.PlayChargedUse(); } void PlayQuickAttackAnimation(int combo) { m_PlayerAnimator.PlayQuickAttack(combo); } // // @MARK: Actor // public override void OnServerTick(float deltaTime) { base.OnServerTick(deltaTime); NativeArray remoteStateData = DataSerializationUtils.Serialize(m_RemoteState); SendActorEvent((byte) PlayerActorEvents.UpdatedRemoteState, remoteStateData); } public override void OnClientTick(float deltaTime) { base.OnClientTick(deltaTime); if (m_IsSetupAsOwner) { TickCamera(deltaTime); UpdateAnimator(m_Locomotion.Velocity); SenseInteractable(); if (m_QuickAttackComboTimer > 0.0f) { m_QuickAttackComboTimer -= deltaTime; if (m_QuickAttackComboTimer <= 0.0f && m_QuickAttackState == QuickAttackState.WaitingForNextAttack) { m_QuickAttackState = QuickAttackState.None; SetHandsIdleAnimation(); } } m_SyncRemoteStateTimer -= deltaTime; if (m_SyncRemoteStateTimer <= 0.0f) { m_SyncRemoteStateTimer = 1.0f / NetworkSystem.TickRate.IndexValue; var 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((byte) PlayerActorCommands.UpdateRemoteState, data); } } else { InterpolateActorState(deltaTime); } TickCharacterRotation(deltaTime); } protected override void OnActorCommandServer(ulong senderID, ActorCommand actorCommand) { switch ((PlayerActorCommands) actorCommand.CommandID) { case PlayerActorCommands.UpdateRemoteState: { RemotePlayerActorState remoteState = new RemotePlayerActorState(); DataSerializationUtils.Deserialize(actorCommand.Data, ref remoteState); m_RemoteState = remoteState; break; } case 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."); } break; } case 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 = command.SlotIndex; UpdateEquippedItem(); break; } case PlayerActorCommands.DropItem: { PlayerActorDropItemCommand command = new PlayerActorDropItemCommand(); DataSerializationUtils.Deserialize(actorCommand.Data, ref command); Inventory.TryDrop(command.InventorySlotIndex, out _); break; } case PlayerActorCommands.RequestHandsAnimation: { PlayerActorRequestHandsAnimationCommand command = new PlayerActorRequestHandsAnimationCommand(); DataSerializationUtils.Deserialize(actorCommand.Data, ref command); // if (m_Animator.HasState(m_HandsLayerIndex, command.AnimationHash)) { // PlayerPlayHandsAnimationEvent handsAnimationEvent = new PlayerPlayHandsAnimationEvent { // AnimationHash = command.AnimationHash // }; // SendActorEvent((byte)PlayerActorEvents.PlayHandsAnimation, ref handsAnimationEvent); // } else { // s_Logger.Error($"Animator does not have state with hash {command.AnimationHash}"); // } break; } case PlayerActorCommands.DealDamage: { PlayerActorDealDamageCommand dealDamageCommand = new PlayerActorDealDamageCommand(); DataSerializationUtils.Deserialize(actorCommand.Data, ref dealDamageCommand); Actor targetActor = RR.FindSpawnedActor(dealDamageCommand.TargetActorID); if (targetActor == null) { s_Logger.Error($"Target actor with ID {dealDamageCommand.TargetActorID} not found."); break; } if (targetActor is IKillable killable) { killable.OnHit(this, 100); } break; } } } protected override void OnActorEventClient(ActorEvent actorEvent) { base.OnActorEventClient(actorEvent); switch ((PlayerActorEvents) actorEvent.EventID) { case PlayerActorEvents.UpdatedRemoteState: { var remoteState = new RemotePlayerActorState(); DataSerializationUtils.Deserialize(actorEvent.Data, ref remoteState); m_RemoteState = remoteState; break; } case PlayerActorEvents.PrimaryEquippedItemChanged: { var 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."); } } SetHandsIdleAnimation(); break; } case PlayerActorEvents.UpdateInventory: { if (RR.IsServer()) { break; } var updateInventoryEvent = new PlayerUpdateInventoryEvent(); DataSerializationUtils.Deserialize(actorEvent.Data, ref updateInventoryEvent); for (int i = 0; i < Inventory.SlotsCount; i++) { ushort actorID = updateInventoryEvent.SlotsActorIDs[i]; if (actorID == 0) { Inventory.SetItem(i, null); } else { Actor itemActor = RR.FindSpawnedActor(actorID); if (itemActor is ItemActor item) { Inventory.SetItem(i, item); } else { s_Logger.Error($"Item actor with ID {actorID} not found for inventory slot {i}."); } } } break; } case PlayerActorEvents.PlayHandsAnimation: { if (RR.IsServer()) { break; } var handsAnimationEvent = new PlayerPlayHandsAnimationEvent(); DataSerializationUtils.Deserialize(actorEvent.Data, ref handsAnimationEvent); // if (m_Animator.HasState(m_HandsLayerIndex, handsAnimationEvent.AnimationHash)) { // m_Animator.CrossFade(handsAnimationEvent.AnimationHash, 0.0f, m_HandsLayerIndex); // } else { // s_Logger.Error($"Animator does not have state with hash {handsAnimationEvent.AnimationHash}"); // } break; } default: s_Logger.Error("Invalid actor event received: " + actorEvent.EventID); break; } } // // @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(float deltaTime) { // Camera Stuff m_Camera.Tick(deltaTime); 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 * deltaTime); // m_Camera.SetBobbing(m_CurrentCameraBobbing); m_Camera.SetBobbing(0.0f); // m_CameraSpring.UpdateSpring(deltaTime, // m_CharacterForwardTransform.up, // m_CharacterForwardTransform.right, // m_CharacterForwardTransform.forward); } void SenseInteractable() { IInteractable interactable = m_InteractablesSensor.Sense(); if (interactable != null) { s_Logger.Info("NOT NULL"); } if (interactable != TargetInteractable) { s_Logger.Info("sensed different interactable"); TargetInteractable = interactable; } } public void DealDamage(IKillable target) { if (target is Actor actor) { var dealDamageCommand = new PlayerActorDealDamageCommand { TargetActorID = actor.ActorID }; SendActorCommand((byte) PlayerActorCommands.DealDamage, ref dealDamageCommand); } else { s_Logger.Error($"Player can only deal damage to other actors!"); } } // // @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 SendInventoryState() { if (!RR.IsServer()) { s_Logger.Error("Only the server can send inventory state."); return; } var updateInventoryEvent = new PlayerUpdateInventoryEvent { SlotsActorIDs = new NativeArray(Inventory.SlotsCount, Allocator.Temp) }; for (int i = 0; i < Inventory.SlotsCount; i++) { updateInventoryEvent.SlotsActorIDs[i] = Inventory.GetItem(i)?.ActorID ?? (ushort) 0; } SendActorEvent((byte) PlayerActorEvents.UpdateInventory, ref updateInventoryEvent); } void OnItemDropped(ItemActor item) { if (!RR.IsServer()) { return; } 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); SendInventoryState(); } void OnItemPickedUp(ItemActor item) { if (!RR.IsServer()) { return; } item.SetHidden(true); SendInventoryState(); UpdateEquippedItem(); } void UpdateEquippedItem() { if (!RR.IsServer()) { s_Logger.Error("Only the server can update selected item."); return; } ItemActor itemActor = Inventory.GetItem(SelectedInventorySlot); 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, m_EquippedItem.Config.characterEquippedMountSlotName); } var itemChangedEvent = new PlayerActorPrimaryEquippedItemChangedEvent { ItemActorID = m_EquippedItem != null ? m_EquippedItem.ActorID : (ushort) 0 }; SendActorEvent((byte) PlayerActorEvents.PrimaryEquippedItemChanged, ref itemChangedEvent); if (m_EquippedItem != null) { m_PlayerAnimator.SetHandsAnimationSet(m_EquippedItem.Config.handsAnimationClipsSets); SpawnAdditionalEquippedItemActors(); } else { m_PlayerAnimator.SetHandsAnimationSet(null); DestroyAdditionalEquippedItemActors(); } SetHandsIdleAnimation(); } void SpawnAdditionalEquippedItemActors() { DestroyAdditionalEquippedItemActors(); foreach (ItemActorMountingConfig localMountInfo in m_EquippedItem.Config.additionalActorsToMount) { Actor actor = RR.SpawnLocalOnlyActor(localMountInfo.actor, Vector3.zero, Quaternion.identity); actor.MountTo(this, localMountInfo.slotName); m_AdditionalMountedActor.Add(actor); } } void DestroyAdditionalEquippedItemActors() { foreach (Actor actor in m_AdditionalMountedActor) { RR.DestroyActor(actor); } m_AdditionalMountedActor.Clear(); } public void WarpTo(Vector3 position) { if (!RR.IsServer()) { s_Logger.Error("Only the server can warp players."); return; } m_Locomotion.WarpTo(position); } // // @MARK: IKillable // public bool IsAlive() { return true; } public ulong OnHit(Actor attacker, ulong damage) { return 0; } // // @MARK: Common // void TickCharacterRotation(float dt) { float3 targetCharacterForward = math.normalize(LookDirection.With(y: 0.0f)); float3 currentCharacterForward = math.normalize(m_CharacterForwardTransform.forward.With(y: 0.0f)); float angleDeg = Mathf.DeltaAngle(m_Camera.Yaw, m_Locomotion.YawRotation); 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 * dt; m_CharacterTurnVelocitySmooth = m_CharacterTurnVelocity; var 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 // public bool TryGetBeaconPosition(out Vector3 position) { var 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; } public Vector3 GetAttackPosition() { float3 origin = m_Camera.Camera.transform.position; float3 dir = m_Camera.Camera.transform.forward; float3 pos = origin + dir * 1.5f; return pos; } // // @MARK: Inventory // void Pickup(ItemActor actor) { if (!m_IsSetupAsOwner) { s_Logger.Error("Cannot pick up items when not set up as owner."); return; } var command = new PlayerActorPickupItemCommand { ItemActorID = actor.ActorID }; SendActorCommand((byte) PlayerActorCommands.PickupItem, ref command); } public void SelectPreviousItemSlot() { if (!m_IsSetupAsOwner) { s_Logger.Error("Only the owner can change inventory selection."); return; } if (SelectedInventorySlot > 0) { SelectedInventorySlot--; } else { SelectedInventorySlot = Inventory.SlotsCount - 1; } var command = new PlayerActorSelectItemSlotCommand { SlotIndex = SelectedInventorySlot }; SendActorCommand((byte) PlayerActorCommands.SelectItemSlot, ref command); } public void SelectNextItemSlot() { if (!m_IsSetupAsOwner) { s_Logger.Error("Only the owner can change inventory selection."); return; } if (SelectedInventorySlot < Inventory.SlotsCount - 1) { SelectedInventorySlot++; } else { SelectedInventorySlot = 0; } var command = new PlayerActorSelectItemSlotCommand { SlotIndex = SelectedInventorySlot }; SendActorCommand((byte) 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 = slotIndex; var command = new PlayerActorSelectItemSlotCommand { SlotIndex = SelectedInventorySlot }; SendActorCommand((byte) PlayerActorCommands.SelectItemSlot, ref command); } // // @MARK: Animations // void UpdateAnimator(Vector3 velocity) { Vector3 localVelocity = m_CharacterForwardTransform.InverseTransformDirection(velocity); float forwardNormalized = localVelocity.z / m_Locomotion.runSpeed; float rightNormalized = localVelocity.x / m_Locomotion.runSpeed; forwardNormalized = math.clamp(forwardNormalized, -1.0f, 1.0f); rightNormalized = math.clamp(rightNormalized, -1.0f, 1.0f); if (math.abs(forwardNormalized) > 0.01f || math.abs(rightNormalized) > 0.01f || !m_Locomotion.IsGrounded) { m_CharacterTurnVelocitySmooth = 0.0f; } var locomotionParams = new PlayerLocomotionAnimatorParams { IsGrounded = m_Locomotion.IsGrounded, VelocityForwardNormalized = forwardNormalized, VelocityRightNormalized = rightNormalized, TurnVelocity = m_CharacterTurnVelocitySmooth }; m_PlayerAnimator.SetLocomotionParams(locomotionParams); m_CharacterTurnVelocitySmooth = Mathf.MoveTowards(m_CharacterTurnVelocitySmooth, 0.0f, m_TurnTransitionSpeed * Time.deltaTime); } } 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 : byte { None = 0x00, UpdateRemoteState = 0x01, PickupItem = 0x02, DropItem = 0x03, EquipItem = 0x04, SelectItemSlot = 0x05, RequestHandsAnimation = 0x06, DealDamage = 0x07 } struct PlayerActorPickupItemCommand : IActorData { public ushort ItemActorID; public void Serialize(NetworkBufferWriter writer) { writer.Write(ItemActorID); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ItemActorID); } public int GetMaxBytes() { return sizeof(ushort); // ItemActorID } } struct PlayerActorDropItemCommand : IActorData { public byte InventorySlotIndex; public void Serialize(NetworkBufferWriter writer) { writer.Write(InventorySlotIndex); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out InventorySlotIndex); } public int GetMaxBytes() { return sizeof(byte); } } 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 } } struct PlayerActorRequestHandsAnimationCommand : IActorData { public int AnimationHash; public void Serialize(NetworkBufferWriter writer) { writer.Write(AnimationHash); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out AnimationHash); } public int GetMaxBytes() { return sizeof(int); } } struct PlayerActorDealDamageCommand : IActorData { public ushort TargetActorID; public int GetMaxBytes() { return sizeof(ushort); } public void Serialize(NetworkBufferWriter writer) { writer.Write(TargetActorID); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out TargetActorID); } } // @MARK: Player Actor Events enum PlayerActorEvents : byte { None = 0x00, PrimaryEquippedItemChanged = 0x01, UpdatedRemoteState = 0x02, UpdateInventory = 0x03, PlayHandsAnimation = 0x04, } struct PlayerActorPrimaryEquippedItemChangedEvent : IActorData { public ushort ItemActorID; public void Serialize(NetworkBufferWriter writer) { writer.Write(ItemActorID); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out ItemActorID); } public int GetMaxBytes() { return sizeof(ushort); // ItemActorID } } struct PlayerUpdateInventoryEvent : IActorData { public NativeArray SlotsActorIDs; public void Serialize(NetworkBufferWriter writer) { writer.Write((byte) SlotsActorIDs.Length); for (int i = 0; i < SlotsActorIDs.Length; i++) { writer.Write(SlotsActorIDs[i]); } } public void Deserialize(NetworkBufferReader reader) { reader.Read(out byte slotsCount); SlotsActorIDs = new NativeArray(slotsCount, Allocator.Temp); for (int i = 0; i < slotsCount; i++) { reader.Read(out ushort actorID); SlotsActorIDs[i] = actorID; } } public int GetMaxBytes() { return sizeof(byte) + SlotsActorIDs.Length * sizeof(ushort); } } struct PlayerPlayHandsAnimationEvent : IActorData { public int AnimationHash; public void Serialize(NetworkBufferWriter writer) { writer.Write(AnimationHash); } public void Deserialize(NetworkBufferReader reader) { reader.Read(out AnimationHash); } public int GetMaxBytes() { return sizeof(int); } } }