504 lines
14 KiB
C#
504 lines
14 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Profiling;
|
|
using Fusion;
|
|
using Fusion.Addons.KCC;
|
|
|
|
namespace TPSBR
|
|
{
|
|
[DefaultExecutionOrder(-5)]
|
|
public sealed class Agent : ContextBehaviour, ISortedUpdate
|
|
{
|
|
// PUBLIC METHODS
|
|
|
|
public bool IsObserved => Context != null && Context.ObservedAgent == this;
|
|
|
|
public AgentInput AgentInput => _agentInput;
|
|
public Interactions Interactions => _interactions;
|
|
public Character Character => _character;
|
|
public Weapons Weapons => _weapons;
|
|
public Health Health => _health;
|
|
public AgentSenses Senses => _senses;
|
|
public Jetpack Jetpack => _jetpack;
|
|
public AgentVFX Effects => _agentVFX;
|
|
public AgentInterestView InterestView => _interestView;
|
|
|
|
[Networked]
|
|
public NetworkBool LeftSide { get; private set; }
|
|
|
|
// PRIVATE MEMBERS
|
|
|
|
[SerializeField]
|
|
private float _jumpPower;
|
|
[SerializeField]
|
|
private float _topCameraAngleLimit;
|
|
[SerializeField]
|
|
private float _bottomCameraAngleLimit;
|
|
[SerializeField]
|
|
private GameObject _visualRoot;
|
|
|
|
[Header("Fall Damage")]
|
|
[SerializeField]
|
|
private float _minFallDamage = 5f;
|
|
[SerializeField]
|
|
private float _maxFallDamage = 200f;
|
|
[SerializeField]
|
|
private float _maxFallDamageVelocity = 20f;
|
|
[SerializeField]
|
|
private float _minFallDamageVelocity = 5f;
|
|
|
|
private AgentInput _agentInput;
|
|
private Interactions _interactions;
|
|
private AgentFootsteps _footsteps;
|
|
private Character _character;
|
|
private Weapons _weapons;
|
|
private Jetpack _jetpack;
|
|
private AgentSenses _senses;
|
|
private Health _health;
|
|
private AgentVFX _agentVFX;
|
|
private AgentInterestView _interestView;
|
|
private SortedUpdateInvoker _sortedUpdateInvoker;
|
|
private Quaternion _cachedLookRotation;
|
|
private Quaternion _cachedPitchRotation;
|
|
|
|
// NetworkBehaviour INTERFACE
|
|
|
|
public override void Spawned()
|
|
{
|
|
name = Object.InputAuthority.ToString();
|
|
|
|
_sortedUpdateInvoker = Runner.GetSingleton<SortedUpdateInvoker>();
|
|
|
|
_visualRoot.SetActive(true);
|
|
|
|
_character.OnSpawned(this);
|
|
_jetpack.OnSpawned(this);
|
|
_health.OnSpawned(this);
|
|
_agentVFX.OnSpawned(this);
|
|
|
|
if (ApplicationSettings.IsStrippedBatch == true)
|
|
{
|
|
gameObject.SetActive(false);
|
|
|
|
if (ApplicationSettings.GenerateInput == true)
|
|
{
|
|
NetworkEvents networkEvents = Runner.GetComponent<NetworkEvents>();
|
|
networkEvents.OnInput.RemoveListener(GenerateRandomInput);
|
|
networkEvents.OnInput.AddListener(GenerateRandomInput);
|
|
}
|
|
}
|
|
|
|
return;
|
|
|
|
void GenerateRandomInput(NetworkRunner runner, NetworkInput networkInput)
|
|
{
|
|
// Used for batch testing
|
|
|
|
GameplayInput gameplayInput = new GameplayInput();
|
|
gameplayInput.MoveDirection = new Vector2(UnityEngine.Random.value * 2.0f - 1.0f, UnityEngine.Random.value > 0.25f ? 1.0f : -1.0f).normalized;
|
|
gameplayInput.LookRotationDelta = new Vector2(UnityEngine.Random.value * 2.0f - 1.0f, UnityEngine.Random.value * 2.0f - 1.0f);
|
|
gameplayInput.Jump = UnityEngine.Random.value > 0.99f;
|
|
gameplayInput.Attack = UnityEngine.Random.value > 0.99f;
|
|
gameplayInput.Reload = UnityEngine.Random.value > 0.99f;
|
|
gameplayInput.Interact = UnityEngine.Random.value > 0.99f;
|
|
gameplayInput.Weapon = (byte)(UnityEngine.Random.value > 0.99f ? (UnityEngine.Random.value > 0.25f ? 2 : 1) : 0);
|
|
|
|
networkInput.Set(gameplayInput);
|
|
}
|
|
}
|
|
|
|
public override void Despawned(NetworkRunner runner, bool hasState)
|
|
{
|
|
if (_weapons != null) { _weapons.OnDespawned(); }
|
|
if (_jetpack != null) { _jetpack.OnDespawned(); }
|
|
if (_health != null) { _health.OnDespawned(); }
|
|
if (_agentVFX != null) { _agentVFX.OnDespawned(); }
|
|
}
|
|
|
|
public void EarlyFixedUpdateNetwork()
|
|
{
|
|
Profiler.BeginSample($"{nameof(Agent)}(Early)");
|
|
|
|
ProcessFixedInput();
|
|
|
|
_weapons.OnFixedUpdate();
|
|
_jetpack.OnFixedUpdate();
|
|
_character.OnFixedUpdate();
|
|
|
|
Profiler.EndSample();
|
|
}
|
|
|
|
public override void FixedUpdateNetwork()
|
|
{
|
|
Profiler.BeginSample($"{nameof(Agent)}(Regular)");
|
|
|
|
// Performance optimization, unnecessary euler call
|
|
Quaternion currentLookRotation = _character.CharacterController.FixedData.LookRotation;
|
|
if (_cachedLookRotation.ComponentEquals(currentLookRotation) == false)
|
|
{
|
|
_cachedLookRotation = currentLookRotation;
|
|
_cachedPitchRotation = Quaternion.Euler(_character.CharacterController.FixedData.LookPitch, 0.0f, 0.0f);
|
|
}
|
|
|
|
_character.GetCameraHandle().transform.localRotation = _cachedPitchRotation;
|
|
|
|
CheckFallDamage();
|
|
|
|
if (_health.IsAlive == true)
|
|
{
|
|
float sortOrder = _agentInput.FixedInput.LocalAlpha;
|
|
if (sortOrder <= 0.0f)
|
|
{
|
|
// Default LocalAlpha value results in update callback being executed last.
|
|
sortOrder = 1.0f;
|
|
}
|
|
|
|
// Schedule update to process render-accurate shooting.
|
|
_sortedUpdateInvoker.ScheduleSortedUpdate(this, sortOrder);
|
|
|
|
if (Runner.IsServer == true)
|
|
{
|
|
_interestView.SetPlayerInfo(_character.CharacterController.Transform, _character.GetCameraHandle());
|
|
}
|
|
}
|
|
|
|
_health.OnFixedUpdate();
|
|
|
|
Profiler.EndSample();
|
|
}
|
|
|
|
public void EarlyRender()
|
|
{
|
|
if (HasInputAuthority == true)
|
|
{
|
|
ProcessRenderInput();
|
|
}
|
|
|
|
_character.OnRender();
|
|
}
|
|
|
|
public override void Render()
|
|
{
|
|
if (HasInputAuthority == true || IsObserved == true)
|
|
{
|
|
// Performance optimization, unnecessary euler call
|
|
Quaternion currentLookRotation = _character.CharacterController.RenderData.LookRotation;
|
|
if (_cachedLookRotation.ComponentEquals(currentLookRotation) == false)
|
|
{
|
|
_cachedLookRotation = currentLookRotation;
|
|
_cachedPitchRotation = Quaternion.Euler(_character.CharacterController.RenderData.LookPitch, 0.0f, 0.0f);
|
|
}
|
|
|
|
_character.GetCameraHandle().transform.localRotation = _cachedPitchRotation;
|
|
}
|
|
|
|
_character.OnAgentRender();
|
|
_footsteps.OnAgentRender();
|
|
}
|
|
|
|
// ISortedUpdate INTERFACE
|
|
|
|
void ISortedUpdate.SortedUpdate()
|
|
{
|
|
// This method execution is sorted by LocalAlpha property passed in input and preserves realtime order of input actions.
|
|
|
|
bool attackWasActivated = _agentInput.WasActivated(EGameplayInputAction.Attack);
|
|
bool reloadWasActivated = _agentInput.WasActivated(EGameplayInputAction.Reload);
|
|
bool interactWasActivated = _agentInput.WasActivated(EGameplayInputAction.Interact);
|
|
|
|
TryFire(attackWasActivated, _agentInput.FixedInput.Attack);
|
|
TryReload(reloadWasActivated == false);
|
|
|
|
_interactions.TryInteract(interactWasActivated, _agentInput.FixedInput.Interact);
|
|
}
|
|
|
|
// MonoBehaviour INTERFACE
|
|
|
|
private void Awake()
|
|
{
|
|
_agentInput = GetComponent<AgentInput>();
|
|
_interactions = GetComponent<Interactions>();
|
|
_footsteps = GetComponent<AgentFootsteps>();
|
|
_character = GetComponent<Character>();
|
|
_weapons = GetComponent<Weapons>();
|
|
_health = GetComponent<Health>();
|
|
_agentVFX = GetComponent<AgentVFX>();
|
|
_senses = GetComponent<AgentSenses>();
|
|
_jetpack = GetComponent<Jetpack>();
|
|
_interestView = GetComponent<AgentInterestView>();
|
|
}
|
|
|
|
// PRIVATE METHODS
|
|
|
|
private void ProcessFixedInput()
|
|
{
|
|
KCC kcc = _character.CharacterController;
|
|
KCCData kccFixedData = kcc.FixedData;
|
|
|
|
GameplayInput input = default;
|
|
|
|
if (_health.IsAlive == true)
|
|
{
|
|
input = _agentInput.FixedInput;
|
|
}
|
|
|
|
if (input.Aim == true)
|
|
{
|
|
input.Aim &= CanAim(kccFixedData);
|
|
}
|
|
|
|
if (input.Aim == true)
|
|
{
|
|
if (_weapons.CurrentWeapon != null && _weapons.CurrentWeapon.HitType == EHitType.Sniper)
|
|
{
|
|
input.LookRotationDelta *= 0.3f;
|
|
}
|
|
}
|
|
|
|
kcc.SetAim(input.Aim);
|
|
|
|
if (_agentInput.WasActivated(EGameplayInputAction.Jump, input) == true && _character.AnimationController.CanJump() == true)
|
|
{
|
|
kcc.Jump(Vector3.up * _jumpPower);
|
|
}
|
|
|
|
SetLookRotation(kccFixedData, input.LookRotationDelta, _weapons.GetRecoil(), out Vector2 newRecoil);
|
|
_weapons.SetRecoil(newRecoil);
|
|
|
|
kcc.SetInputDirection(input.MoveDirection.IsZero() == true ? Vector3.zero : kcc.FixedData.TransformRotation * input.MoveDirection.X0Y());
|
|
|
|
if (_agentInput.WasActivated(EGameplayInputAction.ToggleSide, input) == true)
|
|
{
|
|
LeftSide = !LeftSide;
|
|
}
|
|
|
|
if (input.Weapon > 0 && _character.AnimationController.CanSwitchWeapons(true) == true && _weapons.SwitchWeapon(input.Weapon - 1) == true)
|
|
{
|
|
_character.AnimationController.SwitchWeapons();
|
|
}
|
|
else if (input.Weapon <= 0 && _weapons.PendingWeaponSlot != _weapons.CurrentWeaponSlot && _character.AnimationController.CanSwitchWeapons(false) == true)
|
|
{
|
|
_character.AnimationController.SwitchWeapons();
|
|
}
|
|
|
|
// if (_agentInput.WasActivated(EGameplayInputAction.ToggleJetpack, input) == true)
|
|
// {
|
|
// if (_jetpack.IsActive == true)
|
|
// {
|
|
// _jetpack.Deactivate();
|
|
// }
|
|
// else if (_character.AnimationController.CanSwitchWeapons(true) == true)
|
|
// {
|
|
// _jetpack.Activate();
|
|
// }
|
|
// }
|
|
|
|
// if (_jetpack.IsActive == true)
|
|
// {
|
|
// _jetpack.FullThrust = input.Thrust;
|
|
// }
|
|
|
|
_agentInput.SetFixedInput(input, false);
|
|
}
|
|
|
|
private void ProcessRenderInput()
|
|
{
|
|
KCC kcc = _character.CharacterController;
|
|
KCCData kccFixedData = kcc.FixedData;
|
|
|
|
GameplayInput input = default;
|
|
|
|
if (_health.IsAlive == true)
|
|
{
|
|
input = _agentInput.RenderInput;
|
|
|
|
var accumulatedInput = _agentInput.AccumulatedInput;
|
|
|
|
input.LookRotationDelta = accumulatedInput.LookRotationDelta;
|
|
input.Aim = accumulatedInput.Aim;
|
|
input.Thrust = accumulatedInput.Thrust;
|
|
}
|
|
|
|
if (input.Aim == true)
|
|
{
|
|
input.Aim &= CanAim(kccFixedData);
|
|
}
|
|
|
|
if (input.Aim == true)
|
|
{
|
|
if (_weapons.CurrentWeapon != null && _weapons.CurrentWeapon.HitType == EHitType.Sniper)
|
|
{
|
|
input.LookRotationDelta *= 0.3f;
|
|
}
|
|
}
|
|
|
|
SetLookRotation(kccFixedData, input.LookRotationDelta, _weapons.GetRecoil(), out Vector2 newRecoil);
|
|
|
|
kcc.SetInputDirection(input.MoveDirection.IsZero() == true ? Vector3.zero : kcc.RenderData.TransformRotation * input.MoveDirection.X0Y());
|
|
|
|
kcc.SetAim(input.Aim);
|
|
|
|
if (_agentInput.WasActivated(EGameplayInputAction.Jump, input) == true && _character.AnimationController.CanJump() == true)
|
|
{
|
|
kcc.Jump(Vector3.up * _jumpPower);
|
|
}
|
|
}
|
|
|
|
private void TryFire(bool attack, bool hold)
|
|
{
|
|
var currentWeapon = _weapons.CurrentWeapon;
|
|
if (currentWeapon is ThrowableWeapon && currentWeapon.WeaponSlot == _weapons.PendingWeaponSlot)
|
|
{
|
|
// Fire is handled form the grenade animation state itself
|
|
_character.AnimationController.ProcessThrow(attack, hold);
|
|
return;
|
|
}
|
|
|
|
if (hold == false)
|
|
return;
|
|
if (_weapons.CanFireWeapon(attack) == false)
|
|
return;
|
|
|
|
if (_character.AnimationController.StartFire() == true)
|
|
{
|
|
if (_weapons.Fire() == true)
|
|
{
|
|
_health.ResetRegenDelay();
|
|
|
|
if (Runner.IsServer == true)
|
|
{
|
|
PlayerRef inputAuthority = Object.InputAuthority;
|
|
if (inputAuthority.IsRealPlayer == true)
|
|
{
|
|
_interestView.UpdateShootInterestTargets();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TryReload(bool autoReload)
|
|
{
|
|
if (_weapons.CanReloadWeapon(autoReload) == false)
|
|
return;
|
|
|
|
if (_character.AnimationController.StartReload() == true)
|
|
{
|
|
_weapons.Reload();
|
|
}
|
|
}
|
|
|
|
private bool CanAim(KCCData kccData)
|
|
{
|
|
if (kccData.IsGrounded == false)
|
|
return false;
|
|
|
|
return _weapons.CanAim();
|
|
}
|
|
|
|
private void SetLookRotation(KCCData kccData, Vector2 lookRotationDelta, Vector2 recoil, out Vector2 newRecoil)
|
|
{
|
|
if (lookRotationDelta.IsZero() == true && recoil.IsZero() == true && _character.CharacterController.Data.Recoil == Vector2.zero)
|
|
{
|
|
newRecoil = recoil;
|
|
return;
|
|
}
|
|
|
|
Vector2 baseLookRotation = kccData.GetLookRotation(true, true) - kccData.Recoil;
|
|
Vector2 recoilReduction = Vector2.zero;
|
|
|
|
if (recoil.x > 0f && lookRotationDelta.x < 0)
|
|
{
|
|
recoilReduction.x = Mathf.Clamp(lookRotationDelta.x, -recoil.x, 0f);
|
|
}
|
|
|
|
if (recoil.x < 0f && lookRotationDelta.x > 0f)
|
|
{
|
|
recoilReduction.x = Mathf.Clamp(lookRotationDelta.x, 0, -recoil.x);
|
|
}
|
|
|
|
if (recoil.y > 0f && lookRotationDelta.y < 0)
|
|
{
|
|
recoilReduction.y = Mathf.Clamp(lookRotationDelta.y, -recoil.y, 0f);
|
|
}
|
|
|
|
if (recoil.y < 0f && lookRotationDelta.y > 0f)
|
|
{
|
|
recoilReduction.y = Mathf.Clamp(lookRotationDelta.y, 0, -recoil.y);
|
|
}
|
|
|
|
lookRotationDelta -= recoilReduction;
|
|
recoil += recoilReduction;
|
|
|
|
lookRotationDelta.x = Mathf.Clamp(baseLookRotation.x + lookRotationDelta.x, -_topCameraAngleLimit, _bottomCameraAngleLimit) - baseLookRotation.x;
|
|
|
|
_character.CharacterController.SetLookRotation(baseLookRotation + recoil + lookRotationDelta);
|
|
_character.CharacterController.SetRecoil(recoil);
|
|
|
|
_character.AnimationController.Turn(lookRotationDelta.y);
|
|
|
|
newRecoil = recoil;
|
|
}
|
|
|
|
private void CheckFallDamage()
|
|
{
|
|
if (IsProxy == true)
|
|
return;
|
|
|
|
if (_health.IsAlive == false)
|
|
return;
|
|
|
|
var kccData = _character.CharacterController.Data;
|
|
|
|
if (kccData.IsGrounded == false || kccData.WasGrounded == true)
|
|
return;
|
|
|
|
float fallVelocity = -kccData.DesiredVelocity.y;
|
|
for (int i = 1; i < 3; ++i)
|
|
{
|
|
var historyData = _character.CharacterController.GetHistoryData(kccData.Tick - i);
|
|
if (historyData != null)
|
|
{
|
|
fallVelocity = Mathf.Max(fallVelocity, -historyData.DesiredVelocity.y);
|
|
}
|
|
}
|
|
|
|
if (fallVelocity < 0f)
|
|
return;
|
|
|
|
float damage = MathUtility.Map(_minFallDamageVelocity, _maxFallDamageVelocity, 0f, _maxFallDamage, fallVelocity);
|
|
|
|
if (damage <= _minFallDamage)
|
|
return;
|
|
|
|
var hitData = new HitData
|
|
{
|
|
Action = EHitAction.Damage,
|
|
Amount = damage,
|
|
Position = transform.position,
|
|
Normal = Vector3.up,
|
|
Direction = -Vector3.up,
|
|
InstigatorRef = Object.InputAuthority,
|
|
Instigator = _health,
|
|
Target = _health,
|
|
HitType = EHitType.Suicide,
|
|
};
|
|
|
|
(_health as IHitTarget).ProcessHit(ref hitData);
|
|
}
|
|
|
|
private void OnCullingUpdated(bool isCulled)
|
|
{
|
|
bool isActive = isCulled == false;
|
|
|
|
// Show/hide the game object based on AoI (Area of Interest)
|
|
|
|
_visualRoot.SetActive(isActive);
|
|
|
|
if (_character.CharacterController.Collider != null)
|
|
{
|
|
_character.CharacterController.Collider.enabled = isActive;
|
|
}
|
|
}
|
|
}
|
|
}
|