You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
12 KiB
C#

3 weeks ago
using System;
using Unity.BossRoom.CameraUtils;
using Unity.BossRoom.Gameplay.UserInput;
using Unity.BossRoom.Gameplay.Configuration;
using Unity.BossRoom.Gameplay.Actions;
using Unity.BossRoom.Utils;
using Unity.Netcode;
using UnityEngine;
namespace Unity.BossRoom.Gameplay.GameplayObjects.Character
{
/// <summary>
/// <see cref="ClientCharacter"/> is responsible for displaying a character on the client's screen based on state information sent by the server.
/// </summary>
public class ClientCharacter : NetworkBehaviour
{
[SerializeField]
Animator m_ClientVisualsAnimator;
[SerializeField]
VisualizationConfiguration m_VisualizationConfiguration;
/// <summary>
/// Returns a reference to the active Animator for this visualization
/// </summary>
public Animator OurAnimator => m_ClientVisualsAnimator;
/// <summary>
/// Returns the targeting-reticule prefab for this character visualization
/// </summary>
public GameObject TargetReticulePrefab => m_VisualizationConfiguration.TargetReticule;
/// <summary>
/// Returns the Material to plug into the reticule when the selected entity is hostile
/// </summary>
public Material ReticuleHostileMat => m_VisualizationConfiguration.ReticuleHostileMat;
/// <summary>
/// Returns the Material to plug into the reticule when the selected entity is friendly
/// </summary>
public Material ReticuleFriendlyMat => m_VisualizationConfiguration.ReticuleFriendlyMat;
CharacterSwap m_CharacterSwapper;
public CharacterSwap CharacterSwap => m_CharacterSwapper;
public bool CanPerformActions => m_ServerCharacter.CanPerformActions;
ServerCharacter m_ServerCharacter;
public ServerCharacter serverCharacter => m_ServerCharacter;
ClientActionPlayer m_ClientActionViz;
PositionLerper m_PositionLerper;
RotationLerper m_RotationLerper;
// this value suffices for both positional and rotational interpolations; one may have a constant value for each
const float k_LerpTime = 0.08f;
Vector3 m_LerpedPosition;
Quaternion m_LerpedRotation;
float m_CurrentSpeed;
/// <summary>
/// /// Server to Client RPC that broadcasts this action play to all clients.
/// </summary>
/// <param name="data"> Data about which action to play and its associated details. </param>
[Rpc(SendTo.ClientsAndHost)]
public void ClientPlayActionRpc(ActionRequestData data)
{
ActionRequestData data1 = data;
m_ClientActionViz.PlayAction(ref data1);
}
/// <summary>
/// This RPC is invoked on the client when the active action FXs need to be cancelled (e.g. when the character has been stunned)
/// </summary>
[Rpc(SendTo.ClientsAndHost)]
public void ClientCancelAllActionsRpc()
{
m_ClientActionViz.CancelAllActions();
}
/// <summary>
/// This RPC is invoked on the client when active action FXs of a certain type need to be cancelled (e.g. when the Stealth action ends)
/// </summary>
[Rpc(SendTo.ClientsAndHost)]
public void ClientCancelActionsByPrototypeIDRpc(ActionID actionPrototypeID)
{
m_ClientActionViz.CancelAllActionsWithSamePrototypeID(actionPrototypeID);
}
/// <summary>
/// Called on all clients when this character has stopped "charging up" an attack.
/// Provides a value between 0 and 1 inclusive which indicates how "charged up" the attack ended up being.
/// </summary>
[Rpc(SendTo.ClientsAndHost)]
public void ClientStopChargingUpRpc(float percentCharged)
{
m_ClientActionViz.OnStoppedChargingUp(percentCharged);
}
void Awake()
{
enabled = false;
}
public override void OnNetworkSpawn()
{
if (!IsClient || transform.parent == null)
{
return;
}
enabled = true;
m_ClientActionViz = new ClientActionPlayer(this);
m_ServerCharacter = GetComponentInParent<ServerCharacter>();
m_ServerCharacter.IsStealthy.OnValueChanged += OnStealthyChanged;
m_ServerCharacter.MovementStatus.OnValueChanged += OnMovementStatusChanged;
OnMovementStatusChanged(MovementStatus.Normal, m_ServerCharacter.MovementStatus.Value);
// sync our visualization position & rotation to the most up to date version received from server
transform.SetPositionAndRotation(serverCharacter.physicsWrapper.Transform.position,
serverCharacter.physicsWrapper.Transform.rotation);
m_LerpedPosition = transform.position;
m_LerpedRotation = transform.rotation;
// similarly, initialize start position and rotation for smooth lerping purposes
m_PositionLerper = new PositionLerper(serverCharacter.physicsWrapper.Transform.position, k_LerpTime);
m_RotationLerper = new RotationLerper(serverCharacter.physicsWrapper.Transform.rotation, k_LerpTime);
if (!m_ServerCharacter.IsNpc)
{
name = "AvatarGraphics" + m_ServerCharacter.OwnerClientId;
if (m_ServerCharacter.TryGetComponent(out ClientPlayerAvatarNetworkAnimator characterNetworkAnimator))
{
m_ClientVisualsAnimator = characterNetworkAnimator.Animator;
}
m_CharacterSwapper = GetComponentInChildren<CharacterSwap>();
// ...and visualize the current char-select value that we know about
SetAppearanceSwap();
if (m_ServerCharacter.IsOwner)
{
ActionRequestData data = new ActionRequestData { ActionID = GameDataSource.Instance.GeneralTargetActionPrototype.ActionID };
m_ClientActionViz.PlayAction(ref data);
gameObject.AddComponent<CameraController>();
if (m_ServerCharacter.TryGetComponent(out ClientInputSender inputSender))
{
// anticipated actions will only be played on non-host, owning clients
if (!IsServer)
{
inputSender.ActionInputEvent += OnActionInput;
}
inputSender.ClientMoveEvent += OnMoveInput;
}
}
}
}
public override void OnNetworkDespawn()
{
if (m_ServerCharacter)
{
m_ServerCharacter.IsStealthy.OnValueChanged -= OnStealthyChanged;
if (m_ServerCharacter.TryGetComponent(out ClientInputSender sender))
{
sender.ActionInputEvent -= OnActionInput;
sender.ClientMoveEvent -= OnMoveInput;
}
}
enabled = false;
}
void OnActionInput(ActionRequestData data)
{
m_ClientActionViz.AnticipateAction(ref data);
}
void OnMoveInput(Vector3 position)
{
if (!IsAnimating())
{
OurAnimator.SetTrigger(m_VisualizationConfiguration.AnticipateMoveTriggerID);
}
}
void OnStealthyChanged(bool oldValue, bool newValue)
{
SetAppearanceSwap();
}
void SetAppearanceSwap()
{
if (m_CharacterSwapper)
{
var specialMaterialMode = CharacterSwap.SpecialMaterialMode.None;
if (m_ServerCharacter.IsStealthy.Value)
{
if (m_ServerCharacter.IsOwner)
{
specialMaterialMode = CharacterSwap.SpecialMaterialMode.StealthySelf;
}
else
{
specialMaterialMode = CharacterSwap.SpecialMaterialMode.StealthyOther;
}
}
m_CharacterSwapper.SwapToModel(specialMaterialMode);
}
}
/// <summary>
/// Returns the value we should set the Animator's "Speed" variable, given current gameplay conditions.
/// </summary>
float GetVisualMovementSpeed(MovementStatus movementStatus)
{
if (m_ServerCharacter.NetLifeState.LifeState.Value != LifeState.Alive)
{
return m_VisualizationConfiguration.SpeedDead;
}
switch (movementStatus)
{
case MovementStatus.Idle:
return m_VisualizationConfiguration.SpeedIdle;
case MovementStatus.Normal:
return m_VisualizationConfiguration.SpeedNormal;
case MovementStatus.Uncontrolled:
return m_VisualizationConfiguration.SpeedUncontrolled;
case MovementStatus.Slowed:
return m_VisualizationConfiguration.SpeedSlowed;
case MovementStatus.Hasted:
return m_VisualizationConfiguration.SpeedHasted;
case MovementStatus.Walking:
return m_VisualizationConfiguration.SpeedWalking;
default:
throw new Exception($"Unknown MovementStatus {movementStatus}");
}
}
void OnMovementStatusChanged(MovementStatus previousValue, MovementStatus newValue)
{
m_CurrentSpeed = GetVisualMovementSpeed(newValue);
}
void Update()
{
// On the host, Characters are translated via ServerCharacterMovement's FixedUpdate method. To ensure that
// the game camera tracks a GameObject moving in the Update loop and therefore eliminate any camera jitter,
// this graphics GameObject's position is smoothed over time on the host. Clients do not need to perform any
// positional smoothing since NetworkTransform will interpolate position updates on the root GameObject.
if (IsHost)
{
// Note: a cached position (m_LerpedPosition) and rotation (m_LerpedRotation) are created and used as
// the starting point for each interpolation since the root's position and rotation are modified in
// FixedUpdate, thus altering this transform (being a child) in the process.
m_LerpedPosition = m_PositionLerper.LerpPosition(m_LerpedPosition,
serverCharacter.physicsWrapper.Transform.position);
m_LerpedRotation = m_RotationLerper.LerpRotation(m_LerpedRotation,
serverCharacter.physicsWrapper.Transform.rotation);
transform.SetPositionAndRotation(m_LerpedPosition, m_LerpedRotation);
}
if (m_ClientVisualsAnimator)
{
// set Animator variables here
OurAnimator.SetFloat(m_VisualizationConfiguration.SpeedVariableID, m_CurrentSpeed);
}
m_ClientActionViz.OnUpdate();
}
void OnAnimEvent(string id)
{
//if you are trying to figure out who calls this method, it's "magic". The Unity Animation Event system takes method names as strings,
//and calls a method of the same name on a component on the same GameObject as the Animator. See the "attack1" Animation Clip as one
//example of where this is configured.
m_ClientActionViz.OnAnimEvent(id);
}
public bool IsAnimating()
{
if (OurAnimator.GetFloat(m_VisualizationConfiguration.SpeedVariableID) > 0.0) { return true; }
for (int i = 0; i < OurAnimator.layerCount; i++)
{
if (OurAnimator.GetCurrentAnimatorStateInfo(i).tagHash != m_VisualizationConfiguration.BaseNodeTagID)
{
//we are in an active node, not the default "nothing" node.
return true;
}
}
return false;
}
}
}