feat: Player
This commit is contained in:
276
Assets/_Content/Scripts/Characters/Player.cs
Normal file
276
Assets/_Content/Scripts/Characters/Player.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
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;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct References
|
||||
{
|
||||
public CharacterController Controller;
|
||||
public ProjectileLauncher Launcher;
|
||||
public InputActionReference MoveAction;
|
||||
public InputActionReference LookAction;
|
||||
public InputActionReference AttackAction;
|
||||
public InputActionReference InteractAction;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class StateContainer
|
||||
{
|
||||
public bool IsMoving;
|
||||
}
|
||||
|
||||
#region Fields
|
||||
|
||||
private const float KMH_TO_MS = 1f / 3.6f;
|
||||
private const float AIM_STICK_DEADZONE = 0.2f;
|
||||
|
||||
[SerializeField] private Settings _settings;
|
||||
[SerializeField] private References _references;
|
||||
[SerializeField] private StateContainer _state;
|
||||
[SerializeField] private Health _health;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the interact action is performed.
|
||||
/// </summary>
|
||||
public event Action OnInteract;
|
||||
|
||||
/// <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;
|
||||
|
||||
private Camera _camera;
|
||||
private Vector2 _moveInput;
|
||||
private Vector2 _aimStickInput;
|
||||
private bool _isAimingWithStick;
|
||||
private bool _isAttacking;
|
||||
private bool _isInteracting;
|
||||
private Vector3 _aimDirection = Vector3.forward;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
// Camera.main is the camera tagged MainCamera
|
||||
_camera = Camera.main;
|
||||
_health.Initialize();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
SetInputsEnabled(true);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
SetInputsEnabled(false);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// A dead player no longer moves nor shoots
|
||||
if (_health.IsDead)
|
||||
return;
|
||||
|
||||
float deltaTime = Time.deltaTime;
|
||||
|
||||
GetInputs();
|
||||
Move(deltaTime);
|
||||
Aim();
|
||||
Attack();
|
||||
Interact();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
_health.TakeDamage(amount);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
Vector3 position = new Vector3(pose.x, pose.y, pose.z);
|
||||
Quaternion rotation = Quaternion.Euler(0f, pose.w, 0f);
|
||||
transform.SetPositionAndRotation(position, rotation);
|
||||
|
||||
_references.Controller.enabled = true;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void SetInputsEnabled(bool isEnabled)
|
||||
{
|
||||
InputActionReference[] actions =
|
||||
{
|
||||
_references.MoveAction,
|
||||
_references.LookAction,
|
||||
_references.AttackAction,
|
||||
_references.InteractAction
|
||||
};
|
||||
|
||||
foreach (InputActionReference action in actions)
|
||||
{
|
||||
if (isEnabled)
|
||||
action.action.Enable();
|
||||
else
|
||||
action.action.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetInputs()
|
||||
{
|
||||
_moveInput = _references.MoveAction.action.ReadValue<Vector2>();
|
||||
_isAttacking = _references.AttackAction.action.IsPressed();
|
||||
_isInteracting = _references.InteractAction.action.WasPerformedThisFrame();
|
||||
|
||||
InputAction look = _references.LookAction.action;
|
||||
_aimStickInput = look.ReadValue<Vector2>();
|
||||
|
||||
// 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 deltaTime)
|
||||
{
|
||||
Vector3 direction = ToWorldDirection(_moveInput);
|
||||
_state.IsMoving = direction.sqrMagnitude > 0f;
|
||||
|
||||
Vector3 velocity = direction * _settings.Speed * KMH_TO_MS;
|
||||
_references.Controller.Move(velocity * deltaTime);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
transform.rotation = Quaternion.LookRotation(_aimDirection);
|
||||
}
|
||||
|
||||
private void Attack()
|
||||
{
|
||||
if (_isAttacking)
|
||||
_references.Launcher.TryFire();
|
||||
}
|
||||
|
||||
private void Interact()
|
||||
{
|
||||
if (_isInteracting)
|
||||
OnInteract?.Invoke();
|
||||
}
|
||||
|
||||
private bool IsStickPushed()
|
||||
{
|
||||
return _aimStickInput.sqrMagnitude > AIM_STICK_DEADZONE * AIM_STICK_DEADZONE;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
[ContextMenu("Debug/Take 25 Damage")]
|
||||
private void DebugTakeDamage()
|
||||
{
|
||||
TakeDamage(25);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user