This commit is contained in:
2026-11-04 15:51:04 +01:00
parent d21f0c9b6c
commit cd65f702ae
111 changed files with 246848 additions and 1021 deletions

View File

@@ -0,0 +1,205 @@
using UnityEngine;
namespace Arena.Characters
{
/// <summary>
/// Animates a character from the movement of its transform and from the calls of its owner (Player).
/// Placed on the character root. Only sets parameters: the states, transitions and their durations
/// are set in the Animator Controller.
/// Floats Forward and Right: speed in the character space, divided by the walk speed (locomotion blend tree).
/// Bools IsGrounded, IsRunning, IsStunned. Triggers Jump, Dash, Die, Win, Lose.
/// </summary>
public class CharacterAnimator : MonoBehaviour
{
[System.Serializable]
private class Settings
{
[Tooltip("Speed that plays the walk animations at full blend, in km/h.")]
[Min(0.1f)] public float WalkSpeed = 10f;
[Tooltip("Smoothing of the Forward and Right parameters, in seconds.")]
[Min(0f)] public float Damping = 0.1f;
[Tooltip("Speed below which a running character is not shown running, in km/h.")]
[Min(0f)] public float RunMinSpeed = 2f;
}
[System.Serializable]
private struct References
{
public Animator Animator;
}
[System.Serializable]
public class StateContainer
{
public bool IsGrounded = true;
public bool IsRunning;
public bool IsStunned;
public bool IsFinal;
[Tooltip("Speed in the character space, divided by the walk speed. x: right, y: forward.")]
public Vector2 Locomotion;
}
#region FIELDS
const float KMH_TO_MS = 1 / 3.6f;
// Parameters, named as in the Animator Controller
private static readonly int ForwardParameter = Animator.StringToHash("Forward");
private static readonly int RightParameter = Animator.StringToHash("Right");
private static readonly int IsGroundedParameter = Animator.StringToHash("IsGrounded");
private static readonly int IsRunningParameter = Animator.StringToHash("IsRunning");
private static readonly int IsStunnedParameter = Animator.StringToHash("IsStunned");
private static readonly int JumpTrigger = Animator.StringToHash("Jump");
private static readonly int DashTrigger = Animator.StringToHash("Dash");
private static readonly int DieTrigger = Animator.StringToHash("Die");
private static readonly int WinTrigger = Animator.StringToHash("Win");
private static readonly int LoseTrigger = Animator.StringToHash("Lose");
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
[SerializeField] private StateContainer _state;
public StateContainer State => _state;
private Vector3 _lastPosition;
private float _speed;
private float _stunEndTime;
#endregion
#region UNITY LIFECYCLES
void Awake()
{
_lastPosition = transform.position;
_references.Animator.SetBool(IsGroundedParameter, _state.IsGrounded);
}
void Update()
{
float t = Time.deltaTime;
// A paused game (time scale at 0) gives no speed to measure
if (t > 0f)
UpdateLocomotion(t);
if (_state.IsStunned && Time.time >= _stunEndTime)
SetStunned(false);
_references.Animator.SetBool(IsRunningParameter, _state.IsRunning && _speed >= _settings.RunMinSpeed * KMH_TO_MS);
}
#endregion
#region PUBLIC METHODS
/// <summary>
/// Tells whether the character stands on the ground. Called by the owner when it changes.
/// </summary>
public void SetGrounded(bool isGrounded)
{
_state.IsGrounded = isGrounded;
_references.Animator.SetBool(IsGroundedParameter, isGrounded);
}
/// <summary>
/// Tells whether the character is running. Called by the owner when it changes.
/// IsRunning is true in the Animator only while the character also moves.
/// </summary>
public void SetRunning(bool isRunning)
{
_state.IsRunning = isRunning;
}
public void Jump()
{
SetTrigger(JumpTrigger);
}
public void Dash()
{
SetTrigger(DashTrigger);
}
/// <summary>
/// Sets IsStunned for a duration, in seconds.
/// </summary>
public void Stun(float duration)
{
if (_state.IsFinal)
return;
_stunEndTime = Time.time + duration;
SetStunned(true);
}
public void Die()
{
SetFinalTrigger(DieTrigger);
}
public void Win()
{
SetFinalTrigger(WinTrigger);
}
public void Lose()
{
SetFinalTrigger(LoseTrigger);
}
#endregion
#region PRIVATE METHODS
private void UpdateLocomotion(float t)
{
Vector3 position = transform.position;
Vector3 velocity = (position - _lastPosition) / t;
_lastPosition = position;
velocity.y = 0f;
_speed = velocity.magnitude;
// Speed in the character space, so that a sideways move plays walkRight
Vector3 localVelocity = transform.InverseTransformDirection(velocity);
Vector2 locomotion = new Vector2(localVelocity.x, localVelocity.z) / (_settings.WalkSpeed * KMH_TO_MS);
_state.Locomotion = Vector2.ClampMagnitude(locomotion, 1f);
_references.Animator.SetFloat(ForwardParameter, _state.Locomotion.y, _settings.Damping, t);
_references.Animator.SetFloat(RightParameter, _state.Locomotion.x, _settings.Damping, t);
}
private void SetStunned(bool isStunned)
{
_state.IsStunned = isStunned;
_references.Animator.SetBool(IsStunnedParameter, isStunned);
}
private void SetTrigger(int trigger)
{
if (_state.IsFinal || _state.IsStunned)
return;
_references.Animator.SetTrigger(trigger);
}
private void SetFinalTrigger(int trigger)
{
if (_state.IsFinal)
return;
_state.IsFinal = true;
SetStunned(false);
// Win and lose are played while the game is paused (time scale at 0)
_references.Animator.updateMode = AnimatorUpdateMode.UnscaledTime;
_references.Animator.SetTrigger(trigger);
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bab6a0f29562e1147b9630b053b4017e

View File

@@ -1,276 +1,454 @@
using Arena.Combat;
using System;
using UnityEngine;
using UnityEngine.InputSystem;
using Arena.Combat;
namespace Arena.Characters
{
public class Player : MonoBehaviour, IDamageable
{
[System.Serializable]
public class Settings
{
[Tooltip("Movement speed, in km/h.")]
[Min(0f)] public float Speed = 5f;
}
public class Player : MonoBehaviour, IDamageable
{
[System.Serializable]
private class Settings
{
[Tooltip("Movement speed, in km/h.")]
public float Speed = 10;
[System.Serializable]
public struct References
{
public CharacterController Controller;
public ProjectileLauncher Launcher;
public InputActionReference MoveAction;
public InputActionReference LookAction;
public InputActionReference AttackAction;
public InputActionReference InteractAction;
}
[Tooltip("Smoothing of the orientation, in seconds. 0: the player turns instantly.")]
[Min(0f)] public float RotationDamping = 0.08f;
[System.Serializable]
public class StateContainer
{
public bool IsMoving;
}
[Tooltip("Speed while the sprint action is held, in km/h. The player then faces where it runs and cannot shoot.")]
[Min(0f)] public float RunSpeed = 18f;
#region Fields
[Tooltip("Distance covered by a dash, in meters.")]
[Min(0f)] public float DashDistance = 4f;
private const float KMH_TO_MS = 1f / 3.6f;
private const float AIM_STICK_DEADZONE = 0.2f;
[Tooltip("Duration of a dash, in seconds.")]
[Min(0.05f)] public float DashDuration = 0.2f;
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
[SerializeField] private StateContainer _state;
[SerializeField] private Health _health;
[Tooltip("Time between the start of two dashes, in seconds.")]
[Min(0f)] public float DashCooldown = 1f;
/// <summary>
/// Raised when the interact action is performed.
/// </summary>
public event Action OnInteract;
[Tooltip("Jump height, in meters.")]
[Min(0f)] public float JumpHeight = 1.2f;
/// <summary>
/// The only player of the scene.
/// </summary>
public static Player Instance { get; private set; }
public StateContainer State => _state;
public Health Health => _health;
public ProjectileLauncher Launcher => _references.Launcher;
[Tooltip("Gravity, in meters per second squared.")]
[Min(0f)] public float Gravity = 20f;
}
private Camera _camera;
private Vector2 _moveInput;
private Vector2 _aimStickInput;
private bool _isAimingWithStick;
private bool _isAttacking;
private bool _isInteracting;
private Vector3 _aimDirection = Vector3.forward;
[System.Serializable]
private struct References
{
public CharacterController Controller;
public InputActionReference MoveAction;
public InputActionReference InteractAction;
public InputActionReference LookAction;
public InputActionReference AttackAction;
public InputActionReference JumpAction;
public InputActionReference SprintAction;
#endregion
[Tooltip("Crouch action of the template, used for the dash.")]
public InputActionReference DashAction;
public ProjectileLauncher Launcher;
public CharacterAnimator CharacterAnimator;
}
#region Unity Lifecycle
[System.Serializable]
public class StateContainer
{
public bool IsMoving = false;
public bool IsGrounded = true;
public bool IsRunning = false;
public bool IsDashing = false;
public bool IsStunned = false;
}
void Awake()
{
// Only one player can exist: a second one is removed
if (Instance && Instance != this)
{
Debug.LogWarning($"A second Player was found on {name}, it is destroyed.");
Destroy(gameObject);
return;
}
#region FIELDS
Instance = this;
public static Player Instance { get; private set; }
// Camera.main is the camera tagged MainCamera
_camera = Camera.main;
_health.Initialize();
}
const float KMH_TO_MS = 1 / 3.6f;
const float AIM_STICK_DEADZONE = 0.2f;
void OnEnable()
{
SetInputsEnabled(true);
}
// Small downward speed kept on the ground, so that the CharacterController stays grounded
const float GROUNDED_SPEED = -2f;
void OnDisable()
{
SetInputsEnabled(false);
}
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
[SerializeField] private StateContainer _state;
[SerializeField] private Health _health;
void OnDestroy()
{
if (Instance == this)
Instance = null;
}
public event Action OnInteract;
void Update()
{
// A dead player no longer moves nor shoots
if (_health.IsDead)
return;
public StateContainer State => _state;
public Health Health => _health;
public ProjectileLauncher Launcher => _references.Launcher;
float deltaTime = Time.deltaTime;
private bool _isInteracting;
private Vector2 _moveInput;
private Camera _camera;
private Vector2 _aimStickInput;
private bool _isAimingWithStick;
private bool _isAttacking;
private Vector3 _aimDirection = Vector3.forward;
private bool _isJumping;
private bool _isSprinting;
private bool _isDashPressed;
private Vector3 _moveDirection;
private Vector3 _dashDirection;
private float _dashEndTime;
private float _nextDashTime;
private float _verticalSpeed;
private float _stunEndTime;
GetInputs();
Move(deltaTime);
Aim();
Attack();
Interact();
}
#endregion
#endregion
#region UNITY LIFECYCLES
#region Public Methods
void Awake()
{
if (Instance && Instance != this)
{
Debug.LogWarning($"A second Player was found on {name}, it is destroyed.");
Destroy(gameObject);
return;
}
public void TakeDamage(int amount)
{
_health.TakeDamage(amount);
}
Instance = this;
/// <summary>
/// Teleports the player.
/// </summary>
/// <param name="pose">x, y, z: position. w: angle around the vertical axis, in degrees.</param>
public void Teleport(Vector4 pose)
{
// An enabled CharacterController would overwrite the new position
_references.Controller.enabled = false;
_health.Initialize();
_health.OnDied += HandleDied;
Vector3 position = new Vector3(pose.x, pose.y, pose.z);
Quaternion rotation = Quaternion.Euler(0f, pose.w, 0f);
transform.SetPositionAndRotation(position, rotation);
_camera = Camera.main;
}
_references.Controller.enabled = true;
}
void OnEnable()
{
_references.MoveAction.action.Enable();
_references.InteractAction.action.Enable();
_references.LookAction.action.Enable();
_references.AttackAction.action.Enable();
_references.JumpAction.action.Enable();
_references.SprintAction.action.Enable();
_references.DashAction.action.Enable();
}
/// <summary>
/// Teleports the player to the position and heading of a target, such as a spawn point.
/// </summary>
public void Teleport(Transform target)
{
Vector3 position = target.position;
Vector4 pose = new Vector4(position.x, position.y, position.z, target.eulerAngles.y);
Teleport(pose);
}
void OnDisable()
{
if (Instance == this)
{
_references.MoveAction.action.Disable();
_references.InteractAction.action.Disable();
_references.LookAction.action.Disable();
_references.AttackAction.action.Disable();
_references.JumpAction.action.Disable();
_references.SprintAction.action.Disable();
_references.DashAction.action.Disable();
}
}
#endregion
void OnDestroy()
{
_health.OnDied -= HandleDied;
#region Private Methods
if (Instance == this)
Instance = null;
}
private void SetInputsEnabled(bool isEnabled)
{
InputActionReference[] actions =
{
_references.MoveAction,
_references.LookAction,
_references.AttackAction,
_references.InteractAction
};
void Update()
{
if (_health.IsDead)
return;
foreach (InputActionReference action in actions)
{
if (isEnabled)
action.action.Enable();
else
action.action.Disable();
}
}
float t = Time.deltaTime;
private void GetInputs()
{
_moveInput = _references.MoveAction.action.ReadValue<Vector2>();
_isAttacking = _references.AttackAction.action.IsPressed();
_isInteracting = _references.InteractAction.action.WasPerformedThisFrame();
GetInputs();
UpdateStun();
Move(t);
InputAction look = _references.LookAction.action;
_aimStickInput = look.ReadValue<Vector2>();
// A stunned player only falls: no aiming, shooting nor interacting
if (_state.IsStunned)
return;
// Look is also bound to the mouse delta: only the gamepad counts as a stick
bool isStick = look.activeControl != null && look.activeControl.device is Gamepad;
Aim(t);
Attack();
Interact();
}
// The last aiming device used wins: the stick when pushed, the mouse when moved
if (isStick && IsStickPushed())
_isAimingWithStick = true;
else if (Pointer.current != null && Pointer.current.delta.ReadValue().sqrMagnitude > 0f)
_isAimingWithStick = false;
}
#endregion
private void Move(float deltaTime)
{
Vector3 direction = ToWorldDirection(_moveInput);
_state.IsMoving = direction.sqrMagnitude > 0f;
#region PUBLIC METHODS
Vector3 velocity = direction * _settings.Speed * KMH_TO_MS;
_references.Controller.Move(velocity * deltaTime);
}
public void TakeDamage(DamageInfo damage)
{
_health.TakeDamage(damage);
}
private void Aim()
{
if (_isAimingWithStick)
{
// Released stick: the player keeps its last direction
if (IsStickPushed())
_aimDirection = ToWorldDirection(_aimStickInput);
}
else if (TryGetPointerDirection(out Vector3 direction))
{
_aimDirection = direction;
}
/// <summary>
/// Takes the control away from the player for a duration, in seconds, and plays the stunned animation.
/// </summary>
public void Stun(float duration)
{
if (_health.IsDead)
return;
transform.rotation = Quaternion.LookRotation(_aimDirection);
}
_stunEndTime = Time.time + duration;
_state.IsStunned = true;
_state.IsDashing = false;
SetRunning(false);
_references.CharacterAnimator.Stun(duration);
}
private void Attack()
{
if (_isAttacking)
_references.Launcher.TryFire();
}
/// <summary>
/// Teleports the player.
/// </summary>
/// <param name="pose">x, y, z: position. w: angle around the vertical axis, in degrees.</param>
public void Teleport(Vector4 pose)
{
// An enabled CharacterController would overwrite the new position
_references.Controller.enabled = false;
private void Interact()
{
if (_isInteracting)
OnInteract?.Invoke();
}
Vector3 position = new Vector3(pose.x, pose.y, pose.z);
Quaternion rotation = Quaternion.Euler(0f, pose.w, 0f);
transform.SetPositionAndRotation(position, rotation);
private bool IsStickPushed()
{
return _aimStickInput.sqrMagnitude > AIM_STICK_DEADZONE * AIM_STICK_DEADZONE;
}
_references.Controller.enabled = true;
}
/// <summary>
/// Converts a 2D input into a world direction on the ground, relative to the camera heading.
/// </summary>
private Vector3 ToWorldDirection(Vector2 input)
{
// Only the camera rotation around the vertical axis is kept
Quaternion cameraHeading = Quaternion.Euler(0f, _camera.transform.eulerAngles.y, 0f);
return cameraHeading * new Vector3(input.x, 0f, input.y);
}
/// <summary>
/// Teleports the player to the position and heading of a target, such as a spawn point.
/// </summary>
public void Teleport(Transform target)
{
Vector3 position = target.position;
Vector4 pose = new Vector4(position.x, position.y, position.z, target.eulerAngles.y);
Teleport(pose);
}
/// <summary>
/// Direction from the player to the point under the pointer, on a horizontal plane at the player's height.
/// </summary>
private bool TryGetPointerDirection(out Vector3 direction)
{
direction = Vector3.zero;
#endregion
if (Pointer.current == null)
return false;
#region PRIVATE METHODS
// Mouse position read directly: the Look action only gives its movement
Ray ray = _camera.ScreenPointToRay(Pointer.current.position.ReadValue());
Plane ground = new Plane(Vector3.up, transform.position);
#if UNITY_EDITOR
[ContextMenu("Debug/Take 25 Damage")]
private void DebugTakeDamage()
{
TakeDamage(new DamageInfo(25, DamageType.Physical, gameObject));
}
if (!ground.Raycast(ray, out float distance))
return false;
[ContextMenu("Debug/Stun 2 s")]
private void DebugStun()
{
Stun(2f);
}
#endif
direction = ray.GetPoint(distance) - transform.position;
direction.y = 0f;
return direction.sqrMagnitude > 0.01f;
}
private void GetInputs()
{
_moveInput = _references.MoveAction.action.ReadValue<Vector2>();
_isInteracting = _references.InteractAction.action.WasPressedThisFrame();
_isAttacking = _references.AttackAction.action.IsPressed();
_isJumping = _references.JumpAction.action.WasPressedThisFrame();
_isSprinting = _references.SprintAction.action.IsPressed();
_isDashPressed = _references.DashAction.action.WasPressedThisFrame();
[ContextMenu("Debug/Take 25 Damage")]
private void DebugTakeDamage()
{
TakeDamage(25);
}
InputAction look = _references.LookAction.action;
_aimStickInput = look.ReadValue<Vector2>();
#endregion
}
// Look is also bound to the mouse delta: only the gamepad counts as a stick
bool isStick = look.activeControl != null && look.activeControl.device is Gamepad;
// The last aiming device used wins: the stick when pushed, the mouse when moved
if (isStick && IsStickPushed())
_isAimingWithStick = true;
else if (Pointer.current != null && Pointer.current.delta.ReadValue().sqrMagnitude > 0f)
_isAimingWithStick = false;
}
private void Move(float t)
{
Vector2 input = _state.IsStunned ? Vector2.zero : _moveInput;
_moveDirection = ToWorldDirection(input);
bool hasInput = _moveDirection.sqrMagnitude > .01f;
if (_state.IsDashing && Time.time >= _dashEndTime)
_state.IsDashing = false;
if (_isDashPressed && !_state.IsStunned && !_state.IsDashing && Time.time >= _nextDashTime)
Dash(hasInput ? _moveDirection.normalized : transform.forward);
SetRunning(_isSprinting && hasInput && !_state.IsStunned);
Vector3 velocity;
if (_state.IsDashing)
{
velocity = _dashDirection * (_settings.DashDistance / _settings.DashDuration);
}
else
{
float speed = _state.IsRunning ? _settings.RunSpeed : _settings.Speed;
velocity = _moveDirection * KMH_TO_MS * speed;
}
_state.IsMoving = velocity.sqrMagnitude > .01f;
if (_state.IsGrounded && _isJumping && !_state.IsStunned)
Jump();
// Gravity: the CharacterController does not apply it
_verticalSpeed -= _settings.Gravity * t;
velocity.y = _verticalSpeed;
_references.Controller.Move(velocity * t);
SetGrounded(_references.Controller.isGrounded);
if (_state.IsGrounded && _verticalSpeed < 0f)
_verticalSpeed = GROUNDED_SPEED;
}
private void Jump()
{
// Speed that reaches the jump height against gravity: v = sqrt(2 g h)
_verticalSpeed = Mathf.Sqrt(2f * _settings.Gravity * _settings.JumpHeight);
_references.CharacterAnimator.Jump();
}
private void Dash(Vector3 direction)
{
_dashDirection = direction;
_dashEndTime = Time.time + _settings.DashDuration;
_nextDashTime = Time.time + _settings.DashCooldown;
_state.IsDashing = true;
_references.CharacterAnimator.Dash();
}
private void SetRunning(bool isRunning)
{
if (_state.IsRunning == isRunning)
return;
_state.IsRunning = isRunning;
_references.CharacterAnimator.SetRunning(isRunning);
}
private void SetGrounded(bool isGrounded)
{
if (_state.IsGrounded == isGrounded)
return;
_state.IsGrounded = isGrounded;
_references.CharacterAnimator.SetGrounded(isGrounded);
}
private void UpdateStun()
{
if (_state.IsStunned && Time.time >= _stunEndTime)
_state.IsStunned = false;
}
private void HandleDied()
{
_references.CharacterAnimator.Die();
}
private void Aim(float t)
{
// Running and dashing face the movement: aiming is suspended
if (_state.IsDashing)
{
TurnTowards(_dashDirection, t);
return;
}
// In the air, the player keeps the orientation it had when it left the ground
if (!_state.IsGrounded)
return;
if (_state.IsRunning)
{
TurnTowards(_moveDirection, t);
return;
}
if (_isAimingWithStick)
{
// Released stick: the player keeps its last direction
if (IsStickPushed())
_aimDirection = ToWorldDirection(_aimStickInput);
}
else if (TryGetPointerDirection(out Vector3 direction))
{
_aimDirection = direction;
}
TurnTowards(_aimDirection, t);
}
private void TurnTowards(Vector3 direction, float t)
{
Quaternion target = Quaternion.LookRotation(direction);
if (_settings.RotationDamping <= 0f)
{
transform.rotation = target;
}
else
{
// Exponential smoothing: the same feel whatever the frame rate
float blend = 1f - Mathf.Exp(-t / _settings.RotationDamping);
transform.rotation = Quaternion.Slerp(transform.rotation, target, blend);
}
}
private void Attack()
{
// No shooting while running
if (_isAttacking && !_state.IsRunning)
_references.Launcher.TryFire();
}
private void Interact()
{
if (_isInteracting)
OnInteract?.Invoke();
}
private bool IsStickPushed()
{
return _aimStickInput.sqrMagnitude > AIM_STICK_DEADZONE * AIM_STICK_DEADZONE;
}
private Vector3 ToWorldDirection(Vector2 input)
{
// Only the camera rotation around the vertical axis is kept
Quaternion cameraHeading = Quaternion.Euler(0f, _camera.transform.eulerAngles.y, 0f);
return cameraHeading * new Vector3(input.x, 0f, input.y);
}
/// <summary>
/// Direction from the player to the point under the pointer, on a horizontal plane at the player's height.
/// </summary>
private bool TryGetPointerDirection(out Vector3 direction)
{
direction = Vector3.zero;
if (Pointer.current == null)
return false;
// Mouse position read directly: the Look action only gives its movement
Ray ray = _camera.ScreenPointToRay(Pointer.current.position.ReadValue());
Plane ground = new Plane(Vector3.up, transform.position);
if (!ground.Raycast(ray, out float distance))
return false;
direction = ray.GetPoint(distance) - transform.position;
direction.y = 0f;
return direction.sqrMagnitude > 0.01f;
}
#endregion
}
}

View File

@@ -2,88 +2,74 @@ using UnityEngine;
namespace Arena.Characters
{
[ExecuteAlways]
public class PlayerCamera : MonoBehaviour
{
[System.Serializable]
public class Settings
{
[Tooltip("x: horizontal distance from the player, y: height above the player, in meters.")]
public Vector2 Offset = new Vector2(8f, 12f);
[ExecuteAlways]
public class PlayerCamera : MonoBehaviour
{
[System.Serializable]
public class Settings
{
[Tooltip("x: horizontal distance from the player, y: height above the player, in meters.")]
public Vector2 Offset = new Vector2(7f, 8f);
[Tooltip("Rotation of the offset around the player, in degrees.")]
public float Angle = 0f;
[Tooltip("Rotation of the offset around the player, in degrees.")]
public float Angle = 0f;
[Tooltip("Time to catch up with the player, in seconds. 0: no smoothing.")]
[Range(0f, 1f)] public float Smooth = 0.2f;
}
[Tooltip("Time to catch up with the player, in seconds. 0: no smoothing.")]
[Range(0f, 1f)] public float Smooth = 0.2f;
}
[System.Serializable]
public struct References
{
[Tooltip("Followed player. Found automatically when empty.")]
public Player Player;
}
[System.Serializable]
private struct References
{
public Player Player;
}
#region Fields
#region FIELDS
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
[SerializeField] private Settings _settings;
[SerializeField] private References _references;
private Vector3 _velocity;
private Vector3 _velocity;
#endregion
#endregion
#region Unity Lifecycle
// LateUpdate: the player has already moved this frame
void LateUpdate()
{
if (!GetPlayer())
return;
// LateUpdate: the player has already moved this frame
void LateUpdate()
{
if (!GetPlayer())
return;
Vector3 targetPos = GetTargetPosition();
Vector3 targetPos = GetTargetPosition();
if (!Application.isPlaying || _settings.Smooth == 0)
{
transform.position = targetPos;
}
else
{
transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref _velocity, _settings.Smooth);
}
}
// No smoothing in edit mode: the camera snaps while the scene is edited
if (!Application.isPlaying || _settings.Smooth <= 0f)
{
transform.position = targetPos;
_velocity = Vector3.zero;
}
else
{
transform.position = Vector3.SmoothDamp(transform.position, targetPos, ref _velocity, _settings.Smooth);
}
}
private bool GetPlayer()
{
if (_references.Player)
return true;
#endregion
// In edit mode, Player.Awake has not run, so the singleton is empty
else if (Player.Instance)
_references.Player = Player.Instance;
else
_references.Player = FindFirstObjectByType<Player>();
#region Private Methods
return _references.Player != null;
}
/// <summary>
/// Looks for the player: singleton first, then a search in the scene. Returns false if none is found.
/// </summary>
private bool GetPlayer()
{
if (_references.Player)
return true;
// In edit mode, Player.Awake has not run, so the singleton is empty
else if (Player.Instance)
_references.Player = Player.Instance;
else
_references.Player = FindFirstObjectByType<Player>();
return _references.Player != null;
}
private Vector3 GetTargetPosition()
{
Quaternion rotation = Quaternion.Euler(0f, _settings.Angle, 0f);
Vector3 offset = rotation * new Vector3(0f, _settings.Offset.y, -_settings.Offset.x);
return _references.Player.transform.position + offset;
}
#endregion
}
private Vector3 GetTargetPosition()
{
Quaternion rotation = Quaternion.Euler(0, _settings.Angle, 0);
Vector3 offset = rotation * new Vector3(0, _settings.Offset.y, -_settings.Offset.x);
return _references.Player.transform.position + offset;
}
}
}

View File

@@ -1,96 +0,0 @@
using UnityEngine;
using UnityEngine.Events;
using Arena.Characters;
namespace Arena.Interactions
{
/// <summary>
/// Zone that raises OnPlayer when the player enters it, or when the player interacts inside it.
/// It never acts on the player itself: the actions are set up in the Inspector.
/// </summary>
[RequireComponent(typeof(Collider))]
public class PlayerEvent : MonoBehaviour
{
public enum Trigger
{
OnPlayerEnter,
OnPlayerInteract
}
[System.Serializable]
public class Settings
{
[Tooltip("OnPlayerEnter: as soon as the player enters. OnPlayerInteract: when the player interacts inside the zone.")]
public Trigger Trigger;
}
#region Fields
[SerializeField] private Settings _settings;
[Tooltip("Actions raised for the player: heal, ammo, sound, hide this object...")]
[SerializeField] private UnityEvent _onPlayer;
private bool _isListening;
#endregion
#region Unity Lifecycle
private void OnDisable()
{
// The object can be hidden while the player is inside
StopListening();
}
private void OnTriggerEnter(Collider other)
{
if (!IsPlayer(other))
return;
if (_settings.Trigger == Trigger.OnPlayerEnter)
_onPlayer.Invoke();
else
StartListening();
}
private void OnTriggerExit(Collider other)
{
if (IsPlayer(other))
StopListening();
}
#endregion
#region Private Methods
private bool IsPlayer(Collider other)
{
return Player.Instance != null && other.gameObject == Player.Instance.gameObject;
}
private void StartListening()
{
Player.Instance.OnInteract += HandleInteract;
_isListening = true;
}
private void StopListening()
{
if (!_isListening)
return;
if (Player.Instance != null)
Player.Instance.OnInteract -= HandleInteract;
_isListening = false;
}
private void HandleInteract()
{
_onPlayer.Invoke();
}
#endregion
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: d3c874cb9bc4c364d87e100aada2b434