Starter
This commit is contained in:
205
Assets/_Content/Scripts/Characters/CharacterAnimator.cs
Normal file
205
Assets/_Content/Scripts/Characters/CharacterAnimator.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bab6a0f29562e1147b9630b053b4017e
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3c874cb9bc4c364d87e100aada2b434
|
||||
@@ -1,34 +0,0 @@
|
||||
using UnityEngine;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gives ammo to the player. Apply is meant to be linked to a PlayerEvent.
|
||||
/// </summary>
|
||||
public class AmmoEffect : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Ammo given to the player.")]
|
||||
[Min(1)] public int Amount = 10;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
if (Player.Instance != null)
|
||||
Player.Instance.Launcher.AddAmmo(_settings.Amount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3831749972a75dc4dbe47f8a551785fc
|
||||
26
Assets/_Content/Scripts/Combat/DamageInfo.cs
Normal file
26
Assets/_Content/Scripts/Combat/DamageInfo.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Everything a hit carries: amount, type and source. Passed to IDamageable.TakeDamage.
|
||||
/// </summary>
|
||||
public readonly struct DamageInfo
|
||||
{
|
||||
public readonly int Amount;
|
||||
|
||||
public readonly DamageType Type;
|
||||
|
||||
/// <summary>
|
||||
/// Object that dealt the damage: shooter, damage zone... Can be null.
|
||||
/// </summary>
|
||||
public readonly GameObject Source;
|
||||
|
||||
public DamageInfo(int amount, DamageType type, GameObject source = null)
|
||||
{
|
||||
Amount = amount;
|
||||
Type = type;
|
||||
Source = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Combat/DamageInfo.cs.meta
Normal file
2
Assets/_Content/Scripts/Combat/DamageInfo.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2876a0af3a1b47e4dbd254a5fdb33f9f
|
||||
12
Assets/_Content/Scripts/Combat/DamageType.cs
Normal file
12
Assets/_Content/Scripts/Combat/DamageType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Nature of a damage, carried by a DamageInfo.
|
||||
/// </summary>
|
||||
public enum DamageType
|
||||
{
|
||||
Physical,
|
||||
Energy,
|
||||
Digital
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Combat/DamageType.cs.meta
Normal file
2
Assets/_Content/Scripts/Combat/DamageType.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f2a3b56adabf304a994febc81bd5489
|
||||
171
Assets/_Content/Scripts/Combat/DamageZone.cs
Normal file
171
Assets/_Content/Scripts/Combat/DamageZone.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Sphere that damages every IDamageable inside it, less and less towards its edge.
|
||||
/// Continuous: hits every tick while a target stays inside (contact damage, toxic area).
|
||||
/// Instant: hits everything in the radius once, when Explode is called (explosion).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(SphereCollider), typeof(Rigidbody))]
|
||||
public class DamageZone : MonoBehaviour
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
Continuous,
|
||||
Instant
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
private class Settings
|
||||
{
|
||||
[Tooltip("Continuous: damage every tick while a target stays inside. Instant: one hit on everything in the radius, when Explode is called.")]
|
||||
public Mode Mode;
|
||||
|
||||
[Tooltip("Damage at the center of the sphere.")]
|
||||
[Min(0)] public int Damage = 10;
|
||||
|
||||
public DamageType DamageType;
|
||||
|
||||
[Tooltip("Damage multiplier by distance to the center. 0: center, 1: edge of the sphere.")]
|
||||
public AnimationCurve Falloff = AnimationCurve.Linear(0f, 1f, 1f, 0f);
|
||||
|
||||
[Tooltip("Continuous mode: time between two hits on the same target, in seconds.")]
|
||||
[Min(0.05f)] public float TickInterval = 0.5f;
|
||||
|
||||
[Tooltip("Instant mode: explode as soon as the object is enabled, for an explosion prefab.")]
|
||||
public bool ExplodeOnEnable = true;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
private struct References
|
||||
{
|
||||
public SphereCollider Zone;
|
||||
|
||||
[Tooltip("Never hurt by the zone and given as damage source, usually the object that carries it.")]
|
||||
public GameObject Owner;
|
||||
}
|
||||
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private References _references;
|
||||
|
||||
private readonly Dictionary<IDamageable, float> _nextHitTimes = new Dictionary<IDamageable, float>();
|
||||
private readonly HashSet<IDamageable> _explosionTargets = new HashSet<IDamageable>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
void Reset()
|
||||
{
|
||||
// Called when the component is added: a trigger sphere that physics can detect
|
||||
_references.Zone = GetComponent<SphereCollider>();
|
||||
_references.Zone.isTrigger = true;
|
||||
GetComponent<Rigidbody>().isKinematic = true;
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (_settings.Mode == Mode.Instant && _settings.ExplodeOnEnable)
|
||||
Explode();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
_nextHitTimes.Clear();
|
||||
}
|
||||
|
||||
void OnTriggerStay(Collider other)
|
||||
{
|
||||
if (_settings.Mode != Mode.Continuous || !TryGetTarget(other, out IDamageable target))
|
||||
return;
|
||||
|
||||
if (_nextHitTimes.TryGetValue(target, out float nextHitTime) && Time.time < nextHitTime)
|
||||
return;
|
||||
|
||||
_nextHitTimes[target] = Time.time + _settings.TickInterval;
|
||||
Hit(target, other.transform.position);
|
||||
}
|
||||
|
||||
void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (TryGetTarget(other, out IDamageable target))
|
||||
_nextHitTimes.Remove(target);
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
if (!_references.Zone)
|
||||
return;
|
||||
|
||||
Gizmos.color = new Color(1f, 0.45f, 0.35f, 0.8f);
|
||||
Gizmos.DrawWireSphere(GetCenter(), GetRadius());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
/// <summary>
|
||||
/// Hits once every IDamageable in the radius. Can be linked to a UnityEvent.
|
||||
/// </summary>
|
||||
public void Explode()
|
||||
{
|
||||
_explosionTargets.Clear();
|
||||
Collider[] colliders = Physics.OverlapSphere(GetCenter(), GetRadius(), ~0, QueryTriggerInteraction.Ignore);
|
||||
|
||||
foreach (Collider other in colliders)
|
||||
{
|
||||
// A target with several colliders is hit only once
|
||||
if (TryGetTarget(other, out IDamageable target) && _explosionTargets.Add(target))
|
||||
Hit(target, other.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private bool TryGetTarget(Collider other, out IDamageable target)
|
||||
{
|
||||
target = other.GetComponentInParent<IDamageable>();
|
||||
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
// The owner never hurts itself
|
||||
return !_references.Owner || ((Component)target).gameObject != _references.Owner;
|
||||
}
|
||||
|
||||
private void Hit(IDamageable target, Vector3 targetPosition)
|
||||
{
|
||||
float distance = Vector3.Distance(GetCenter(), targetPosition);
|
||||
float falloff = _settings.Falloff.Evaluate(Mathf.Clamp01(distance / GetRadius()));
|
||||
int amount = Mathf.RoundToInt(_settings.Damage * falloff);
|
||||
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
GameObject source = _references.Owner ? _references.Owner : gameObject;
|
||||
target.TakeDamage(new DamageInfo(amount, _settings.DamageType, source));
|
||||
}
|
||||
|
||||
private Vector3 GetCenter()
|
||||
{
|
||||
return transform.TransformPoint(_references.Zone.center);
|
||||
}
|
||||
|
||||
private float GetRadius()
|
||||
{
|
||||
// The sphere radius follows the largest scale axis
|
||||
Vector3 scale = transform.lossyScale;
|
||||
float maxScale = Mathf.Max(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z));
|
||||
return Mathf.Max(_references.Zone.radius * maxScale, 0.01f);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Combat/DamageZone.cs.meta
Normal file
2
Assets/_Content/Scripts/Combat/DamageZone.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86310e0cb69dcf54cb7343a4e704cb12
|
||||
79
Assets/_Content/Scripts/Combat/Destructible.cs
Normal file
79
Assets/_Content/Scripts/Combat/Destructible.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Object destroyed when its health points reach zero: crate, barrel, turret...
|
||||
/// Raises OnDestroyed, spawns an optional prefab (debris, explosion with an instant DamageZone), then disappears.
|
||||
/// </summary>
|
||||
public class Destructible : MonoBehaviour, IDamageable
|
||||
{
|
||||
[System.Serializable]
|
||||
private class Settings
|
||||
{
|
||||
[Tooltip("Optional prefab spawned in place of the object: debris, explosion...")]
|
||||
public GameObject SpawnOnDestroyed;
|
||||
}
|
||||
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private Health _health;
|
||||
|
||||
/// <summary>
|
||||
/// Raised once, when the object is destroyed by damage.
|
||||
/// </summary>
|
||||
public event Action<Destructible> OnDestroyed;
|
||||
|
||||
public Health Health => _health;
|
||||
|
||||
#endregion
|
||||
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_health.Initialize();
|
||||
_health.OnDied += HandleDied;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
_health.OnDied -= HandleDied;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public void TakeDamage(DamageInfo damage)
|
||||
{
|
||||
_health.TakeDamage(damage);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private void HandleDied()
|
||||
{
|
||||
OnDestroyed?.Invoke(this);
|
||||
|
||||
if (_settings.SpawnOnDestroyed)
|
||||
Instantiate(_settings.SpawnOnDestroyed, transform.position, transform.rotation);
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Debug/Take 25 Physical Damage")]
|
||||
private void DebugTakeDamage()
|
||||
{
|
||||
TakeDamage(new DamageInfo(25, DamageType.Physical, gameObject));
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Combat/Destructible.cs.meta
Normal file
2
Assets/_Content/Scripts/Combat/Destructible.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a59b2dd5620afef449293e11a903ae07
|
||||
@@ -1,34 +0,0 @@
|
||||
using UnityEngine;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Heals the player. Apply is meant to be linked to a PlayerEvent.
|
||||
/// </summary>
|
||||
public class HealEffect : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Hit points given to the player.")]
|
||||
[Min(1)] public int Amount = 25;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
if (Player.Instance != null)
|
||||
Player.Instance.Health.Heal(_settings.Amount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b9353836241da04e9bb64cc87f89745
|
||||
@@ -3,81 +3,85 @@ using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Hit points of an entity. Serialized inside the component that owns it (Player, Enemy),
|
||||
/// which calls Initialize in its Awake.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class Health
|
||||
{
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// Health points of an entity. Serialized inside the component that owns it (Player, Destructible),
|
||||
/// which calls Initialize in its Awake.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class Health
|
||||
{
|
||||
#region FIELDS
|
||||
|
||||
[Tooltip("Hit points when full.")]
|
||||
[Min(1)] public int Max = 100;
|
||||
[Tooltip("Health points when full.")]
|
||||
[Min(1)] public int Max = 100;
|
||||
|
||||
[Tooltip("Current hit points, visible for debugging.")]
|
||||
[SerializeField] private int _current;
|
||||
[Tooltip("Current health points, visible for debugging.")]
|
||||
[SerializeField] private int _current;
|
||||
|
||||
/// <summary>
|
||||
/// Raised with the current and max hit points each time they change.
|
||||
/// </summary>
|
||||
public event Action<int, int> OnChange;
|
||||
/// <summary>
|
||||
/// Raised with the current and max health points each time they change.
|
||||
/// </summary>
|
||||
public event Action<int, int> OnChange;
|
||||
|
||||
/// <summary>
|
||||
/// Raised once, when the hit points reach zero.
|
||||
/// </summary>
|
||||
public event Action OnDied;
|
||||
/// <summary>
|
||||
/// Raised once, when the health points reach zero.
|
||||
/// </summary>
|
||||
public event Action OnDied;
|
||||
|
||||
public int Current => _current;
|
||||
public bool IsDead => _current <= 0;
|
||||
public int Current => _current;
|
||||
public bool IsDead => _current <= 0;
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region PUBLIC METHODS
|
||||
|
||||
/// <summary>
|
||||
/// Fills the hit points. Called by the owner in its Awake.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
SetCurrent(Max);
|
||||
}
|
||||
/// <summary>
|
||||
/// Fills the health points. Called by the owner in its Awake.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
SetCurrent(Max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes hit points and raises OnDied the first time they reach zero.
|
||||
/// </summary>
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
if (IsDead || amount <= 0)
|
||||
return;
|
||||
/// <summary>
|
||||
/// Removes health points and raises OnDied the first time they reach zero.
|
||||
/// Returns the health points actually removed.
|
||||
/// </summary>
|
||||
public int TakeDamage(DamageInfo damage)
|
||||
{
|
||||
if (IsDead || damage.Amount <= 0)
|
||||
return 0;
|
||||
|
||||
SetCurrent(_current - amount);
|
||||
int previous = _current;
|
||||
SetCurrent(_current - damage.Amount);
|
||||
|
||||
if (IsDead)
|
||||
OnDied?.Invoke();
|
||||
}
|
||||
if (IsDead)
|
||||
OnDied?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Gives hit points back without exceeding Max. Has no effect once dead.
|
||||
/// </summary>
|
||||
public void Heal(int amount)
|
||||
{
|
||||
if (IsDead || amount <= 0)
|
||||
return;
|
||||
return previous - _current;
|
||||
}
|
||||
|
||||
SetCurrent(_current + amount);
|
||||
}
|
||||
/// <summary>
|
||||
/// Gives health points back without exceeding Max. Has no effect once dead.
|
||||
/// </summary>
|
||||
public void Heal(int amount)
|
||||
{
|
||||
if (IsDead || amount <= 0)
|
||||
return;
|
||||
|
||||
#endregion
|
||||
SetCurrent(_current + amount);
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
#endregion
|
||||
|
||||
private void SetCurrent(int value)
|
||||
{
|
||||
_current = Mathf.Clamp(value, 0, Max);
|
||||
OnChange?.Invoke(_current, Max);
|
||||
}
|
||||
#region PRIVATE METHODS
|
||||
|
||||
#endregion
|
||||
}
|
||||
private void SetCurrent(int value)
|
||||
{
|
||||
_current = Mathf.Clamp(value, 0, Max);
|
||||
OnChange?.Invoke(_current, Max);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Anything that can receive damage: player, enemy, destructible object.
|
||||
/// </summary>
|
||||
public interface IDamageable
|
||||
{
|
||||
void TakeDamage(int amount);
|
||||
}
|
||||
/// <summary>
|
||||
/// Anything that can receive damage: player, destructible object.
|
||||
/// </summary>
|
||||
public interface IDamageable
|
||||
{
|
||||
void TakeDamage(DamageInfo damage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,67 +2,64 @@ using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves straight ahead and damages the first IDamageable it touches.
|
||||
/// Needs a trigger collider and a kinematic Rigidbody.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class Projectile : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Speed, in meters per second.")]
|
||||
[Min(0f)] public float Speed = 20f;
|
||||
/// <summary>
|
||||
/// Moves straight ahead and damages the first IDamageable it touches.
|
||||
/// Needs a trigger collider and a kinematic Rigidbody.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class Projectile : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Speed, in meters per second.")]
|
||||
[Min(0f)] public float Speed = 20f;
|
||||
|
||||
[Tooltip("Time before an unused projectile is destroyed, in seconds.")]
|
||||
[Min(0.1f)] public float Lifetime = 2f;
|
||||
}
|
||||
[Tooltip("Time before an unused projectile is destroyed, in seconds.")]
|
||||
[Min(0.1f)] public float Lifetime = 2f;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private Settings _settings;
|
||||
|
||||
private int _damage;
|
||||
private GameObject _owner;
|
||||
private DamageInfo _damage;
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
private void Update()
|
||||
{
|
||||
transform.position += transform.forward * _settings.Speed * Time.deltaTime;
|
||||
}
|
||||
private void Update()
|
||||
{
|
||||
transform.position += transform.forward * _settings.Speed * Time.deltaTime;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// Ignore the shooter and the other trigger zones
|
||||
if (other.isTrigger || other.gameObject == _owner)
|
||||
return;
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// Ignore the shooter and the other trigger zones
|
||||
if (other.isTrigger || other.gameObject == _damage.Source)
|
||||
return;
|
||||
|
||||
if (other.TryGetComponent(out IDamageable target))
|
||||
target.TakeDamage(_damage);
|
||||
if (other.TryGetComponent(out IDamageable target))
|
||||
target.TakeDamage(_damage);
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region PUBLIC METHODS
|
||||
|
||||
/// <summary>
|
||||
/// Starts the projectile. Called right after Instantiate by whoever fires it.
|
||||
/// </summary>
|
||||
/// <param name="damage">Damage dealt to the IDamageable it touches.</param>
|
||||
/// <param name="owner">Shooter, ignored by the projectile.</param>
|
||||
public void Launch(int damage, GameObject owner)
|
||||
{
|
||||
_damage = damage;
|
||||
_owner = owner;
|
||||
Destroy(gameObject, _settings.Lifetime);
|
||||
}
|
||||
/// <summary>
|
||||
/// Starts the projectile. Called right after Instantiate by whoever fires it.
|
||||
/// </summary>
|
||||
/// <param name="damage">Damage dealt to the IDamageable it touches. Its source is the shooter, ignored by the projectile.</param>
|
||||
public void Launch(DamageInfo damage)
|
||||
{
|
||||
_damage = damage;
|
||||
Destroy(gameObject, _settings.Lifetime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,105 +3,106 @@ using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Fires projectiles from a muzzle, limited by the fire interval and the ammo count.
|
||||
/// Placed on the root of the shooter, which the projectiles ignore.
|
||||
/// </summary>
|
||||
public class ProjectileLauncher : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Weapon data: damage, fire interval, ammo, projectile.")]
|
||||
public WeaponData Weapon;
|
||||
}
|
||||
/// <summary>
|
||||
/// Fires projectiles from a muzzle, limited by the fire interval and the ammo count.
|
||||
/// Placed on the root of the shooter, which the projectiles ignore.
|
||||
/// </summary>
|
||||
public class ProjectileLauncher : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Weapon data: damage, fire interval, ammo, projectile.")]
|
||||
public WeaponData Weapon;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct References
|
||||
{
|
||||
[Tooltip("Spawn point of the projectiles. Its forward axis gives the direction.")]
|
||||
public Transform Muzzle;
|
||||
}
|
||||
[System.Serializable]
|
||||
public struct References
|
||||
{
|
||||
[Tooltip("Spawn point of the projectiles. Its forward axis gives the direction.")]
|
||||
public Transform Muzzle;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class StateContainer
|
||||
{
|
||||
public int Ammo;
|
||||
}
|
||||
[System.Serializable]
|
||||
public class StateContainer
|
||||
{
|
||||
public int Ammo;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private References _references;
|
||||
[SerializeField] private StateContainer _state;
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private References _references;
|
||||
[SerializeField] private StateContainer _state;
|
||||
|
||||
/// <summary>
|
||||
/// Raised with the current and max ammo each time the ammo changes.
|
||||
/// </summary>
|
||||
public event Action<int, int> OnAmmoChange;
|
||||
/// <summary>
|
||||
/// Raised with the current and max ammo each time the ammo changes.
|
||||
/// </summary>
|
||||
public event Action<int, int> OnAmmoChange;
|
||||
|
||||
public StateContainer State => _state;
|
||||
public int MaxAmmo => _settings.Weapon.MaxAmmo;
|
||||
public StateContainer State => _state;
|
||||
public int MaxAmmo => _settings.Weapon.MaxAmmo;
|
||||
|
||||
private float _nextFireTime;
|
||||
private float _nextFireTime;
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_state.Ammo = MaxAmmo;
|
||||
}
|
||||
private void Awake()
|
||||
{
|
||||
_state.Ammo = MaxAmmo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region PUBLIC METHODS
|
||||
|
||||
/// <summary>
|
||||
/// Fires one projectile if the weapon is ready and has ammo. Returns true if a shot was fired.
|
||||
/// </summary>
|
||||
public bool TryFire()
|
||||
{
|
||||
if (Time.time < _nextFireTime || _state.Ammo <= 0)
|
||||
return false;
|
||||
/// <summary>
|
||||
/// Fires one projectile if the weapon is ready and has ammo. Returns true if a shot was fired.
|
||||
/// </summary>
|
||||
public bool TryFire()
|
||||
{
|
||||
if (Time.time < _nextFireTime || _state.Ammo <= 0)
|
||||
return false;
|
||||
|
||||
_nextFireTime = Time.time + _settings.Weapon.FireInterval;
|
||||
SetAmmo(_state.Ammo - 1);
|
||||
_nextFireTime = Time.time + _settings.Weapon.FireInterval;
|
||||
|
||||
Transform muzzle = _references.Muzzle;
|
||||
Projectile projectile = Instantiate(_settings.Weapon.ProjectilePrefab, muzzle.position, muzzle.rotation);
|
||||
projectile.Launch(_settings.Weapon.Damage, gameObject);
|
||||
return true;
|
||||
}
|
||||
SetAmmo(_state.Ammo - 1);
|
||||
|
||||
/// <summary>
|
||||
/// Adds ammo without exceeding the weapon maximum.
|
||||
/// </summary>
|
||||
public void AddAmmo(int amount)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
Transform muzzle = _references.Muzzle;
|
||||
Projectile projectile = Instantiate(_settings.Weapon.ProjectilePrefab, muzzle.position, muzzle.rotation);
|
||||
projectile.Launch(new DamageInfo(_settings.Weapon.Damage, _settings.Weapon.DamageType, gameObject));
|
||||
return true;
|
||||
}
|
||||
|
||||
SetAmmo(Mathf.Min(_state.Ammo + amount, MaxAmmo));
|
||||
}
|
||||
/// <summary>
|
||||
/// Adds ammo without exceeding the weapon maximum.
|
||||
/// </summary>
|
||||
public void AddAmmo(int amount)
|
||||
{
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
#endregion
|
||||
SetAmmo(Mathf.Min(_state.Ammo + amount, MaxAmmo));
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
#endregion
|
||||
|
||||
private void SetAmmo(int value)
|
||||
{
|
||||
_state.Ammo = value;
|
||||
OnAmmoChange?.Invoke(_state.Ammo, MaxAmmo);
|
||||
}
|
||||
#region PRIVATE METHODS
|
||||
|
||||
[ContextMenu("Debug/Try Fire")]
|
||||
private void DebugTryFire()
|
||||
{
|
||||
TryFire();
|
||||
}
|
||||
private void SetAmmo(int value)
|
||||
{
|
||||
_state.Ammo = value;
|
||||
OnAmmoChange?.Invoke(_state.Ammo, MaxAmmo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
[ContextMenu("Debug/Try Fire")]
|
||||
private void DebugTryFire()
|
||||
{
|
||||
TryFire();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
using UnityEngine;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Single entry point of the UI: connects the views to the player and switches screens.
|
||||
/// </summary>
|
||||
public class UiManager : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct References
|
||||
{
|
||||
[Tooltip("In-game screen: health, ammo.")]
|
||||
public GameObject Hud;
|
||||
public HealthView HealthView;
|
||||
public AmmoView AmmoView;
|
||||
public GameOverView GameOverView;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
|
||||
[SerializeField] private References _references;
|
||||
|
||||
private Player _player;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Start: every Awake has run, so Player.Instance is set
|
||||
_player = Player.Instance;
|
||||
|
||||
if (_player == null)
|
||||
{
|
||||
Debug.LogError("UiManager: no Player in the scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
_references.HealthView.Bind(_player.Health);
|
||||
_references.AmmoView.Bind(_player.Launcher);
|
||||
_player.Health.OnDied += HandlePlayerDied;
|
||||
|
||||
_references.Hud.SetActive(true);
|
||||
_references.GameOverView.Hide();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_player != null)
|
||||
_player.Health.OnDied -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void HandlePlayerDied()
|
||||
{
|
||||
_references.Hud.SetActive(false);
|
||||
_references.GameOverView.Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c6cb5ac8e4f8d0479afda241378ff0b
|
||||
@@ -2,31 +2,35 @@ using UnityEngine;
|
||||
|
||||
namespace Arena.Combat
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings of a weapon, shared by every launcher that uses it.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "WeaponData_new", menuName = "Arena/Combat/Weapon Data")]
|
||||
public class WeaponData : ScriptableObject
|
||||
{
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// Settings of a weapon, shared by every launcher that uses it.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "WeaponData_new", menuName = "Arena/Combat/Weapon Data")]
|
||||
public class WeaponData : ScriptableObject
|
||||
{
|
||||
#region FIELDS
|
||||
|
||||
[Tooltip("Damage dealt by one projectile.")]
|
||||
[SerializeField, Min(0)] private int _damage = 10;
|
||||
[Tooltip("Damage dealt by one projectile.")]
|
||||
[SerializeField, Min(0)] private int _damage = 10;
|
||||
|
||||
[Tooltip("Time between two shots, in seconds.")]
|
||||
[SerializeField, Min(0.05f)] private float _fireInterval = 0.2f;
|
||||
[Tooltip("Type of the damage dealt by the projectiles.")]
|
||||
[SerializeField] private DamageType _damageType;
|
||||
|
||||
[Tooltip("Ammo when the weapon is full.")]
|
||||
[SerializeField, Min(1)] private int _maxAmmo = 30;
|
||||
[Tooltip("Time between two shots, in seconds.")]
|
||||
[SerializeField, Min(0.05f)] private float _fireInterval = 0.2f;
|
||||
|
||||
[Tooltip("Prefab fired by the weapon.")]
|
||||
[SerializeField] private Projectile _projectilePrefab;
|
||||
[Tooltip("Ammo when the weapon is full.")]
|
||||
[SerializeField, Min(1)] private int _maxAmmo = 30;
|
||||
|
||||
public int Damage => _damage;
|
||||
public float FireInterval => _fireInterval;
|
||||
public int MaxAmmo => _maxAmmo;
|
||||
public Projectile ProjectilePrefab => _projectilePrefab;
|
||||
[Tooltip("Prefab fired by the weapon.")]
|
||||
[SerializeField] private Projectile _projectilePrefab;
|
||||
|
||||
#endregion
|
||||
}
|
||||
public int Damage => _damage;
|
||||
public DamageType DamageType => _damageType;
|
||||
public float FireInterval => _fireInterval;
|
||||
public int MaxAmmo => _maxAmmo;
|
||||
public Projectile ProjectilePrefab => _projectilePrefab;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/_Content/Scripts/Interactions.meta
Normal file
8
Assets/_Content/Scripts/Interactions.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f92dd718d963b04f81884b191f8d1ef
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/_Content/Scripts/Interactions/AmmoEffect.cs
Normal file
34
Assets/_Content/Scripts/Interactions/AmmoEffect.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gives ammo to the player. Apply is meant to be linked to a PlayerEvent.
|
||||
/// </summary>
|
||||
public class AmmoEffect : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Ammo given to the player.")]
|
||||
[Min(1)] public int Amount = 10;
|
||||
}
|
||||
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
if (Player.Instance != null)
|
||||
Player.Instance.Launcher.AddAmmo(_settings.Amount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Interactions/AmmoEffect.cs.meta
Normal file
2
Assets/_Content/Scripts/Interactions/AmmoEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6739c594c812f4a4a84f51156853fc45
|
||||
34
Assets/_Content/Scripts/Interactions/HealEffect.cs
Normal file
34
Assets/_Content/Scripts/Interactions/HealEffect.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.Interactions
|
||||
{
|
||||
/// <summary>
|
||||
/// Heals the player. Apply is meant to be linked to a PlayerEvent.
|
||||
/// </summary>
|
||||
public class HealEffect : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Settings
|
||||
{
|
||||
[Tooltip("Health points given to the player.")]
|
||||
[Min(1)] public int Amount = 25;
|
||||
}
|
||||
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public void Apply()
|
||||
{
|
||||
if (Player.Instance != null)
|
||||
Player.Instance.Health.Heal(_settings.Amount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Interactions/HealEffect.cs.meta
Normal file
2
Assets/_Content/Scripts/Interactions/HealEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f7b07697ca242a4cbfd52a7158769ef
|
||||
90
Assets/_Content/Scripts/Interactions/PlayerEvent.cs
Normal file
90
Assets/_Content/Scripts/Interactions/PlayerEvent.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.Interactions
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private UnityEvent _onPlayer;
|
||||
|
||||
private bool _isListening;
|
||||
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// The object can be hidden while the player is inside
|
||||
StopListening();
|
||||
}
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!IsPlayer(other))
|
||||
return;
|
||||
|
||||
if (_settings.Trigger == Trigger.OnPlayerEnter)
|
||||
_onPlayer.Invoke();
|
||||
else
|
||||
StartListening();
|
||||
}
|
||||
|
||||
void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (IsPlayer(other))
|
||||
StopListening();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void Teleport(Transform target)
|
||||
{
|
||||
if (!Player.Instance)
|
||||
return;
|
||||
|
||||
StopListening();
|
||||
Player.Instance.Teleport(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/Interactions/PlayerEvent.cs.meta
Normal file
2
Assets/_Content/Scripts/Interactions/PlayerEvent.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bed1a9cab72cda46bb8e8dd2d8c60a3
|
||||
@@ -4,56 +4,72 @@ using Arena.Combat;
|
||||
|
||||
namespace Arena.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the ammo of a launcher as "current / max".
|
||||
/// </summary>
|
||||
public class AmmoView : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct References
|
||||
{
|
||||
public TMP_Text Label;
|
||||
}
|
||||
/// <summary>
|
||||
/// Displays the ammo of a launcher as "current / max".
|
||||
/// </summary>
|
||||
public class AmmoView : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
private struct References
|
||||
{
|
||||
public TMP_Text Label;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private References _references;
|
||||
[SerializeField] private References _references;
|
||||
|
||||
private ProjectileLauncher _launcher;
|
||||
private ProjectileLauncher _launcher;
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_launcher != null)
|
||||
_launcher.OnAmmoChange -= HandleAmmoChange;
|
||||
}
|
||||
void OnDestroy()
|
||||
{
|
||||
Unbind();
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region PUBLIC METHODS
|
||||
|
||||
/// <summary>
|
||||
/// Starts displaying the ammo of a launcher. Called by the UiManager.
|
||||
/// </summary>
|
||||
public void Bind(ProjectileLauncher launcher)
|
||||
{
|
||||
_launcher = launcher;
|
||||
_launcher.OnAmmoChange += HandleAmmoChange;
|
||||
HandleAmmoChange(_launcher.State.Ammo, _launcher.MaxAmmo);
|
||||
}
|
||||
/// <summary>
|
||||
/// Starts displaying the ammo of a launcher, instead of the previous one. Called by the UiManager.
|
||||
/// </summary>
|
||||
public void Bind(ProjectileLauncher launcher)
|
||||
{
|
||||
Unbind();
|
||||
|
||||
#endregion
|
||||
_launcher = launcher;
|
||||
|
||||
#region Private Methods
|
||||
if (!_launcher)
|
||||
{
|
||||
_references.Label.SetText("-");
|
||||
return;
|
||||
}
|
||||
|
||||
private void HandleAmmoChange(int current, int max)
|
||||
{
|
||||
_references.Label.SetText("{0} / {1}", current, max);
|
||||
}
|
||||
_launcher.OnAmmoChange += HandleAmmoChange;
|
||||
HandleAmmoChange(_launcher.State.Ammo, _launcher.MaxAmmo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private void Unbind()
|
||||
{
|
||||
if (_launcher)
|
||||
_launcher.OnAmmoChange -= HandleAmmoChange;
|
||||
|
||||
_launcher = null;
|
||||
}
|
||||
|
||||
private void HandleAmmoChange(int current, int max)
|
||||
{
|
||||
_references.Label.SetText("{0} / {1}", current, max);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,31 @@ using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Arena.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Game over screen. Its Restart button reloads the scene.
|
||||
/// </summary>
|
||||
public class GameOverView : MonoBehaviour
|
||||
{
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Game over screen. Its Restart button reloads the scene.
|
||||
/// </summary>
|
||||
public class GameOverView : MonoBehaviour
|
||||
{
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public void Show()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
public void Show()
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
public void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the Restart button (OnClick in the Inspector).
|
||||
/// </summary>
|
||||
public void Restart()
|
||||
{
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
||||
}
|
||||
/// <summary>
|
||||
/// Called by the Restart button (OnClick in the Inspector).
|
||||
/// </summary>
|
||||
public void Restart()
|
||||
{
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,57 +4,66 @@ using Arena.Combat;
|
||||
|
||||
namespace Arena.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays a Health as "current / max".
|
||||
/// </summary>
|
||||
public class HealthView : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct References
|
||||
{
|
||||
public TMP_Text Label;
|
||||
}
|
||||
/// <summary>
|
||||
/// Displays a Health as "current / max".
|
||||
/// </summary>
|
||||
public class HealthView : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
private struct References
|
||||
{
|
||||
public TMP_Text Label;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private References _references;
|
||||
[SerializeField] private References _references;
|
||||
|
||||
private Health _health;
|
||||
private Health _health;
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Every subscription has its unsubscription
|
||||
if (_health != null)
|
||||
_health.OnChange -= HandleHealthChange;
|
||||
}
|
||||
void OnDestroy()
|
||||
{
|
||||
Unbind();
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region PUBLIC METHODS
|
||||
|
||||
/// <summary>
|
||||
/// Starts displaying a Health. Called by the UiManager.
|
||||
/// </summary>
|
||||
public void Bind(Health health)
|
||||
{
|
||||
_health = health;
|
||||
_health.OnChange += HandleHealthChange;
|
||||
HandleHealthChange(_health.Current, _health.Max);
|
||||
}
|
||||
/// <summary>
|
||||
/// Starts displaying a Health, instead of the previous one. Called by the UiManager.
|
||||
/// </summary>
|
||||
public void Bind(Health health)
|
||||
{
|
||||
Unbind();
|
||||
|
||||
#endregion
|
||||
_health = health;
|
||||
_health.OnChange += HandleHealthChange;
|
||||
HandleHealthChange(_health.Current, _health.Max);
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
#endregion
|
||||
|
||||
private void HandleHealthChange(int current, int max)
|
||||
{
|
||||
_references.Label.SetText("{0} / {1}", current, max);
|
||||
}
|
||||
#region PRIVATE METHODS
|
||||
|
||||
#endregion
|
||||
}
|
||||
private void Unbind()
|
||||
{
|
||||
// Every subscription has its unsubscription
|
||||
if (_health != null)
|
||||
_health.OnChange -= HandleHealthChange;
|
||||
|
||||
_health = null;
|
||||
}
|
||||
|
||||
private void HandleHealthChange(int current, int max)
|
||||
{
|
||||
_references.Label.SetText("{0} / {1}", current, max);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
68
Assets/_Content/Scripts/UI/UiManager.cs
Normal file
68
Assets/_Content/Scripts/UI/UiManager.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using UnityEngine;
|
||||
using Arena.Characters;
|
||||
|
||||
namespace Arena.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Single entry point of the UI: connects the views to the player and switches screens.
|
||||
/// </summary>
|
||||
public class UiManager : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
private struct References
|
||||
{
|
||||
[Tooltip("In-game screen: health, ammo.")]
|
||||
public GameObject Hud;
|
||||
public HealthView HealthView;
|
||||
public AmmoView AmmoView;
|
||||
public GameOverView GameOverView;
|
||||
}
|
||||
|
||||
#region FIELDS
|
||||
|
||||
[SerializeField] private References _references;
|
||||
|
||||
private Player _player;
|
||||
|
||||
#endregion
|
||||
|
||||
#region UNITY LIFECYCLES
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Start: every Awake has run, so Player.Instance is set
|
||||
_player = Player.Instance;
|
||||
|
||||
if (!_player)
|
||||
{
|
||||
Debug.LogError("UiManager: no Player in the scene.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
_references.HealthView.Bind(_player.Health);
|
||||
_references.AmmoView.Bind(_player.Launcher);
|
||||
_player.Health.OnDied += HandlePlayerDied;
|
||||
|
||||
_references.Hud.SetActive(true);
|
||||
_references.GameOverView.Hide();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_player)
|
||||
_player.Health.OnDied -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private void HandlePlayerDied()
|
||||
{
|
||||
_references.Hud.SetActive(false);
|
||||
_references.GameOverView.Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
2
Assets/_Content/Scripts/UI/UiManager.cs.meta
Normal file
2
Assets/_Content/Scripts/UI/UiManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c86da6486be14fc40bd26d838f2e8762
|
||||
Reference in New Issue
Block a user