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; /// /// Raised when the interact action is performed. /// public event Action OnInteract; /// /// The only player of the scene. /// 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); } /// /// Teleports the player. /// /// x, y, z: position. w: angle around the vertical axis, in degrees. 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; } /// /// Teleports the player to the position and heading of a target, such as a spawn point. /// 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(); _isAttacking = _references.AttackAction.action.IsPressed(); _isInteracting = _references.InteractAction.action.WasPerformedThisFrame(); InputAction look = _references.LookAction.action; _aimStickInput = look.ReadValue(); // 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; } /// /// Converts a 2D input into a world direction on the ground, relative to the camera heading. /// 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); } /// /// Direction from the player to the point under the pointer, on a horizontal plane at the player's height. /// 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 } }