1222 lines
42 KiB
C#
Executable File
1222 lines
42 KiB
C#
Executable File
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;
|
|
|
|
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("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<IInteractable> 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<int> 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 {
|
|
m_TargetInteractable = value;
|
|
OnTargetInteractableChanged(value);
|
|
}
|
|
}
|
|
|
|
public Action<IInteractable> OnTargetInteractableChanged = delegate {};
|
|
|
|
readonly List<Actor> m_AdditionalMountedActor = new List<Actor>();
|
|
|
|
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<byte> 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<byte> 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 != TargetInteractable) {
|
|
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<ushort>(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 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 * dt;
|
|
|
|
var newForward = Vector3.RotateTowards(currentCharacterForward,
|
|
targetCharacterForward,
|
|
math.radians(m_CharacterTurnVelocity),
|
|
0.0f);
|
|
m_CharacterForwardTransform.forward = newForward;
|
|
} else {
|
|
m_CharacterTurnVelocity = 0.0f;
|
|
}
|
|
|
|
// Aim Target adjustment
|
|
// @BUG: when humbie interacts with a player then a camera is shaking
|
|
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_CharacterTurnVelocity = 0.0f;
|
|
}
|
|
|
|
var locomotionParams = new PlayerLocomotionAnimatorParams {
|
|
IsGrounded = m_Locomotion.IsGrounded,
|
|
VelocityForwardNormalized = forwardNormalized,
|
|
VelocityRightNormalized = rightNormalized,
|
|
TurnVelocity = math.clamp(m_CharacterTurnVelocity, -1, 1)
|
|
};
|
|
m_PlayerAnimator.SetLocomotionParams(locomotionParams);
|
|
}
|
|
}
|
|
|
|
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<ushort> 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<ushort>(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);
|
|
}
|
|
}
|
|
} |