290 lines
10 KiB
C#
290 lines
10 KiB
C#
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using UnityEngine.InputSystem;
|
|
|
|
[RequireComponent(typeof(CharacterController))]
|
|
[RequireComponent(typeof(CombatController))]
|
|
public class PlayerController : MonoBehaviour
|
|
{
|
|
[Header("Movement Settings")]
|
|
[SerializeField] private float moveSpeed = 6.0f;
|
|
[SerializeField] private float rotationSpeed = 12.0f;
|
|
[SerializeField] private float stoppingDistance = 0.2f;
|
|
[SerializeField] private LayerMask groundLayer = ~0; // Default to all layers
|
|
|
|
[Header("Interaction & Combat Settings")]
|
|
[SerializeField] private LayerMask interactableLayer = ~0;
|
|
[SerializeField] private float defaultInteractionRadius = 2.0f;
|
|
[Tooltip("If true, the player automatically turns to attack any enemy that damages them (RuneScape style).")]
|
|
[SerializeField] private bool autoRetaliate = true;
|
|
|
|
[Header("Visual Feedback Prefabs (Optional)")]
|
|
[SerializeField] private GameObject moveClickEffectPrefab;
|
|
[SerializeField] private GameObject interactClickEffectPrefab;
|
|
|
|
private Camera mainCamera;
|
|
private CharacterController characterController;
|
|
private NavMeshAgent navAgent;
|
|
private CombatController combatController;
|
|
|
|
private Vector3 targetPosition;
|
|
private IInteractable targetInteractable;
|
|
private bool isMovingToInteractable = false;
|
|
private bool isMoving = false;
|
|
|
|
private void Awake()
|
|
{
|
|
mainCamera = Camera.main;
|
|
characterController = GetComponent<CharacterController>();
|
|
navAgent = GetComponent<NavMeshAgent>();
|
|
combatController = GetComponent<CombatController>();
|
|
targetPosition = transform.position;
|
|
|
|
if (combatController != null && combatController.Stats != null)
|
|
{
|
|
combatController.Stats.onHealthChanged.AddListener(OnHealthChanged);
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
HandleInput();
|
|
ProcessMovement();
|
|
ProcessInteractionCheck();
|
|
ProcessCombatCheck();
|
|
}
|
|
|
|
private void HandleInput()
|
|
{
|
|
// Using New Unity Input System (UnityEngine.InputSystem)
|
|
Mouse currentMouse = Mouse.current;
|
|
if (currentMouse == null) return;
|
|
|
|
Vector2 screenMousePos = currentMouse.position.ReadValue();
|
|
|
|
// Left Click / Hold: Click or drag to move continuously
|
|
if (currentMouse.leftButton.isPressed)
|
|
{
|
|
bool isFirstPress = currentMouse.leftButton.wasPressedThisFrame;
|
|
HandleLeftClickOrDrag(screenMousePos, isFirstPress);
|
|
}
|
|
|
|
// Right Click: Interact with object or attack enemy
|
|
if (currentMouse.rightButton.wasPressedThisFrame)
|
|
{
|
|
HandleRightClick(screenMousePos);
|
|
}
|
|
}
|
|
|
|
private void HandleLeftClickOrDrag(Vector2 screenPosition, bool isFirstPress)
|
|
{
|
|
Ray ray = mainCamera.ScreenPointToRay(screenPosition);
|
|
if (Physics.Raycast(ray, out RaycastHit hit, 100f, groundLayer))
|
|
{
|
|
SetDestination(hit.point);
|
|
ClearTargetInteractable();
|
|
combatController.ClearTarget();
|
|
|
|
// Only spawn click effect on the initial click frame (not every frame while dragging)
|
|
if (isFirstPress)
|
|
{
|
|
SpawnEffect(moveClickEffectPrefab, hit.point);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleRightClick(Vector2 screenPosition)
|
|
{
|
|
Ray ray = mainCamera.ScreenPointToRay(screenPosition);
|
|
|
|
// Check for Raycast hit on Interactable or Enemy
|
|
if (Physics.Raycast(ray, out RaycastHit hit, 100f, interactableLayer))
|
|
{
|
|
// Check if object is an Enemy / CharacterStats for Combat
|
|
CharacterStats enemyStats = hit.collider.GetComponent<CharacterStats>() ?? hit.collider.GetComponentInParent<CharacterStats>() ?? hit.collider.GetComponentInChildren<CharacterStats>();
|
|
if (enemyStats != null && enemyStats != combatController.Stats && !enemyStats.IsDead)
|
|
{
|
|
ClearTargetInteractable();
|
|
combatController.SetTarget(enemyStats);
|
|
SetDestination(enemyStats.transform.position);
|
|
SpawnEffect(interactClickEffectPrefab, hit.point);
|
|
return;
|
|
}
|
|
|
|
// Check if object is an IInteractable
|
|
IInteractable interactable = hit.collider.GetComponent<IInteractable>() ?? hit.collider.GetComponentInParent<IInteractable>() ?? hit.collider.GetComponentInChildren<IInteractable>();
|
|
if (interactable != null)
|
|
{
|
|
combatController.ClearTarget();
|
|
targetInteractable = interactable;
|
|
isMovingToInteractable = true;
|
|
SetDestination(interactable.GetTransform().position);
|
|
SpawnEffect(interactClickEffectPrefab, hit.point);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Right-clicked ground with no interactable/enemy
|
|
if (Physics.Raycast(ray, out RaycastHit groundHit, 100f, groundLayer))
|
|
{
|
|
ClearTargetInteractable();
|
|
combatController.ClearTarget();
|
|
}
|
|
}
|
|
|
|
private void ProcessCombatCheck()
|
|
{
|
|
if (combatController.CurrentTarget == null) return;
|
|
|
|
if (combatController.CurrentTarget.IsDead)
|
|
{
|
|
combatController.ClearTarget();
|
|
return;
|
|
}
|
|
|
|
// Update destination to track moving target
|
|
targetPosition = combatController.CurrentTarget.transform.position;
|
|
if (navAgent != null && navAgent.enabled)
|
|
{
|
|
navAgent.stoppingDistance = combatController.Stats.AttackRange * 0.9f;
|
|
navAgent.SetDestination(targetPosition);
|
|
}
|
|
|
|
// Check if inside attack range
|
|
if (combatController.IsInAttackRange(combatController.CurrentTarget))
|
|
{
|
|
isMoving = false;
|
|
if (navAgent != null && navAgent.enabled && navAgent.hasPath)
|
|
{
|
|
navAgent.ResetPath();
|
|
}
|
|
combatController.TryExecuteAttack();
|
|
}
|
|
}
|
|
|
|
private void OnHealthChanged(float currentHealth, float maxHealth)
|
|
{
|
|
if (!autoRetaliate || combatController.CurrentTarget != null) return;
|
|
|
|
// Scan nearby hostile objects / enemies that are attacking the player
|
|
Collider[] hits = Physics.OverlapSphere(transform.position, 10.0f, interactableLayer);
|
|
foreach (var hit in hits)
|
|
{
|
|
CharacterStats attackerStats = hit.GetComponent<CharacterStats>() ?? hit.GetComponentInParent<CharacterStats>() ?? hit.GetComponentInChildren<CharacterStats>();
|
|
if (attackerStats != null && attackerStats != combatController.Stats && !attackerStats.IsDead)
|
|
{
|
|
combatController.SetTarget(attackerStats);
|
|
SetDestination(attackerStats.transform.position);
|
|
Debug.Log($"[PlayerController] Auto-retaliating against '{attackerStats.CharacterName}'!");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SetDestination(Vector3 destination)
|
|
{
|
|
targetPosition = destination;
|
|
isMoving = true;
|
|
|
|
if (navAgent != null && navAgent.enabled)
|
|
{
|
|
navAgent.SetDestination(destination);
|
|
}
|
|
}
|
|
|
|
private void ProcessMovement()
|
|
{
|
|
if (!isMoving) return;
|
|
|
|
// If using NavMeshAgent
|
|
if (navAgent != null && navAgent.enabled)
|
|
{
|
|
float targetStoppingDistance = isMovingToInteractable && targetInteractable != null
|
|
? (targetInteractable.GetInteractionRadius() > 0 ? targetInteractable.GetInteractionRadius() : defaultInteractionRadius) * 0.9f
|
|
: stoppingDistance;
|
|
|
|
navAgent.stoppingDistance = targetStoppingDistance;
|
|
|
|
if (!navAgent.pathPending && navAgent.remainingDistance <= navAgent.stoppingDistance)
|
|
{
|
|
if (!navAgent.hasPath || navAgent.velocity.sqrMagnitude == 0f)
|
|
{
|
|
isMoving = false;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fallback: Manual Transform/CharacterController movement
|
|
Vector3 direction = (targetPosition - transform.position);
|
|
direction.y = 0; // Keep movement top-down flat
|
|
|
|
float distance = direction.magnitude;
|
|
float currentStoppingDistance = isMovingToInteractable && targetInteractable != null
|
|
? (targetInteractable.GetInteractionRadius() > 0 ? targetInteractable.GetInteractionRadius() : defaultInteractionRadius) * 0.9f
|
|
: stoppingDistance;
|
|
|
|
if (distance > currentStoppingDistance)
|
|
{
|
|
// Smooth Rotation
|
|
Quaternion targetRotation = Quaternion.LookRotation(direction);
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
|
|
|
// Move
|
|
Vector3 moveVelocity = direction.normalized * moveSpeed * Time.deltaTime;
|
|
characterController.Move(moveVelocity);
|
|
}
|
|
else
|
|
{
|
|
isMoving = false;
|
|
}
|
|
}
|
|
|
|
private void ProcessInteractionCheck()
|
|
{
|
|
if (!isMovingToInteractable || targetInteractable == null) return;
|
|
|
|
Vector3 flatPlayerPos = new Vector3(transform.position.x, 0, transform.position.z);
|
|
Vector3 flatTargetPos = new Vector3(targetInteractable.GetTransform().position.x, 0, targetInteractable.GetTransform().position.z);
|
|
|
|
float distance = Vector3.Distance(flatPlayerPos, flatTargetPos);
|
|
float radius = targetInteractable.GetInteractionRadius() > 0 ? targetInteractable.GetInteractionRadius() : defaultInteractionRadius;
|
|
|
|
if (distance <= radius + 0.1f)
|
|
{
|
|
// Face target interactable
|
|
Vector3 dir = (targetInteractable.GetTransform().position - transform.position);
|
|
dir.y = 0;
|
|
if (dir != Vector3.zero)
|
|
{
|
|
transform.rotation = Quaternion.LookRotation(dir);
|
|
}
|
|
|
|
// Trigger interaction
|
|
targetInteractable.Interact(gameObject);
|
|
ClearTargetInteractable();
|
|
}
|
|
}
|
|
|
|
private void ClearTargetInteractable()
|
|
{
|
|
targetInteractable = null;
|
|
isMovingToInteractable = false;
|
|
}
|
|
|
|
private void SpawnEffect(GameObject effectPrefab, Vector3 position)
|
|
{
|
|
if (effectPrefab != null)
|
|
{
|
|
Instantiate(effectPrefab, position + Vector3.up * 0.05f, Quaternion.identity);
|
|
}
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawWireSphere(targetPosition, 0.3f);
|
|
Gizmos.DrawLine(transform.position, targetPosition);
|
|
}
|
|
}
|