273 lines
8.9 KiB
C#
273 lines
8.9 KiB
C#
using UnityEngine;
|
|
using System.Collections;
|
|
|
|
/// <summary>
|
|
/// CharacterController 기반 플레이어 이동 (최종 버전)
|
|
/// - 벽 뚫림 방지
|
|
/// - 중력 처리
|
|
/// - 대시 충돌 감지
|
|
/// - 무기 충돌 문제 해결
|
|
/// - 물리 상호작용 제거 (몬스터 안 밀림)
|
|
/// </summary>
|
|
[RequireComponent(typeof(CharacterController))]
|
|
public class PlayerMovement : MonoBehaviour
|
|
{
|
|
[Header("=== 참조 ===")]
|
|
[SerializeField] private Stats stats;
|
|
[SerializeField] private PlayerHealth health;
|
|
[SerializeField] private PlayerAnimator pAnim;
|
|
[SerializeField] private PlayerAttack attackScript;
|
|
|
|
[Header("=== CharacterController ===")]
|
|
private CharacterController _controller;
|
|
|
|
[Header("=== 대시 설정 ===")]
|
|
[SerializeField] private float dashDistance = 3f;
|
|
[SerializeField] private float dashDuration = 0.08f;
|
|
[SerializeField] private float dashCooldown = 1.5f;
|
|
|
|
[Header("=== 차징 감속 설정 ===")]
|
|
[Range(0.1f, 1f)]
|
|
[SerializeField] private float minSpeedMultiplier = 0.3f;
|
|
|
|
[Header("=== 중력 설정 ===")]
|
|
[SerializeField] private float gravity = -20f;
|
|
|
|
[Header("=== 충돌 설정 ===")]
|
|
[Tooltip("무기 레이어 (이 레이어와는 충돌하지 않음)")]
|
|
[SerializeField] private LayerMask weaponLayer;
|
|
|
|
[Tooltip("플레이어가 서 있어야 하는 최소 높이 (버그 방지)")]
|
|
[SerializeField] private float minGroundHeight = 0.1f;
|
|
|
|
// 이동 상태
|
|
private Vector3 _moveDir;
|
|
private bool _isSprinting;
|
|
private bool _isDashing;
|
|
private float _lastDashTime;
|
|
private float _verticalVelocity;
|
|
|
|
// 디버그용
|
|
private float _initialYPosition;
|
|
|
|
private void Awake()
|
|
{
|
|
_controller = GetComponent<CharacterController>();
|
|
|
|
if (_controller == null)
|
|
{
|
|
Debug.LogError("[PlayerMovement] CharacterController가 필요합니다!");
|
|
}
|
|
|
|
// 초기 높이 저장
|
|
_initialYPosition = transform.position.y;
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
// ⭐ 무기와 플레이어 레이어 간 충돌 무시 설정
|
|
SetupLayerCollision();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 레이어 충돌 설정 (무기는 플레이어와 충돌하지 않음)
|
|
/// </summary>
|
|
private void SetupLayerCollision()
|
|
{
|
|
// "Player" 레이어와 "Weapon" 레이어 간 충돌 무시
|
|
int playerLayer = gameObject.layer;
|
|
|
|
// weaponLayer가 설정되어 있으면 충돌 무시
|
|
if (weaponLayer != 0)
|
|
{
|
|
// LayerMask에서 실제 레이어 번호 추출
|
|
int weaponLayerIndex = GetLayerFromMask(weaponLayer);
|
|
if (weaponLayerIndex >= 0)
|
|
{
|
|
Physics.IgnoreLayerCollision(playerLayer, weaponLayerIndex, true);
|
|
Debug.Log($"[PlayerMovement] {LayerMask.LayerToName(playerLayer)}와 {LayerMask.LayerToName(weaponLayerIndex)} 간 충돌 무시 설정 완료");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// LayerMask에서 레이어 인덱스 추출
|
|
/// </summary>
|
|
private int GetLayerFromMask(LayerMask mask)
|
|
{
|
|
int layerNumber = 0;
|
|
int layer = mask.value;
|
|
while (layer > 1)
|
|
{
|
|
layer = layer >> 1;
|
|
layerNumber++;
|
|
}
|
|
return layerNumber;
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// 입력 처리
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
public void SetMoveInput(Vector3 dir, bool sprint)
|
|
{
|
|
_moveDir = dir;
|
|
_isSprinting = sprint;
|
|
}
|
|
|
|
public void AttemptDash()
|
|
{
|
|
if (CanDash()) StartCoroutine(DashRoutine());
|
|
}
|
|
|
|
private bool CanDash()
|
|
{
|
|
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown;
|
|
bool isAlive = health != null && !health.IsDead;
|
|
return isCooldownOver && !_isDashing && isAlive;
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// 메인 이동
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
private void Update()
|
|
{
|
|
// 1. 사망/대시/피격 중 이동 차단
|
|
if (health != null && (health.IsDead || health.isHit || _isDashing))
|
|
{
|
|
if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f);
|
|
ApplyGravityOnly();
|
|
return;
|
|
}
|
|
|
|
// 2. 공격 중 이동 차단
|
|
if (attackScript != null && attackScript.IsAttacking)
|
|
{
|
|
if (pAnim != null) pAnim.UpdateMove(0f);
|
|
ApplyGravityOnly();
|
|
return;
|
|
}
|
|
|
|
// 3. 이동 속도 계산
|
|
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed;
|
|
|
|
if (attackScript != null && attackScript.IsCharging)
|
|
{
|
|
float speedReduction = Mathf.Lerp(1.0f, minSpeedMultiplier, attackScript.ChargeProgress);
|
|
speed *= speedReduction;
|
|
}
|
|
|
|
// 4. ✅ CharacterController로 이동 (벽 충돌 처리!)
|
|
Vector3 motion = _moveDir * speed * Time.deltaTime;
|
|
|
|
ApplyGravity();
|
|
motion.y = _verticalVelocity * Time.deltaTime;
|
|
|
|
_controller.Move(motion);
|
|
|
|
// ⭐ 높이 제한 체크 (버그 방지)
|
|
CheckAbnormalHeight();
|
|
|
|
// 5. 애니메이션
|
|
UpdateAnimation();
|
|
}
|
|
|
|
private void ApplyGravity()
|
|
{
|
|
if (_controller.isGrounded)
|
|
{
|
|
_verticalVelocity = -2f;
|
|
}
|
|
else
|
|
{
|
|
_verticalVelocity += gravity * Time.deltaTime;
|
|
}
|
|
}
|
|
|
|
private void ApplyGravityOnly()
|
|
{
|
|
ApplyGravity();
|
|
_controller.Move(new Vector3(0, _verticalVelocity * Time.deltaTime, 0));
|
|
|
|
// ⭐ 높이 제한 체크
|
|
CheckAbnormalHeight();
|
|
}
|
|
|
|
/// <summary>
|
|
/// ⭐ 비정상적으로 높이 올라갔는지 체크 (무기 충돌 버그 방지)
|
|
/// </summary>
|
|
private void CheckAbnormalHeight()
|
|
{
|
|
// 땅에서 너무 높이 떠있으면 강제로 내림
|
|
RaycastHit hit;
|
|
if (Physics.Raycast(transform.position, Vector3.down, out hit, 100f))
|
|
{
|
|
float heightAboveGround = transform.position.y - hit.point.y;
|
|
|
|
// 땅에서 일정 높이 이상 떠있고, 땅에 닿지 않았다면
|
|
if (heightAboveGround > 3f && !_controller.isGrounded)
|
|
{
|
|
// 강제로 땅 근처로 이동
|
|
Vector3 correctedPos = transform.position;
|
|
correctedPos.y = hit.point.y + _controller.height / 2f + minGroundHeight;
|
|
|
|
// CharacterController는 enabled 끄고 위치 변경
|
|
_controller.enabled = false;
|
|
transform.position = correctedPos;
|
|
_controller.enabled = true;
|
|
|
|
_verticalVelocity = 0f;
|
|
|
|
Debug.LogWarning("[PlayerMovement] 비정상적인 높이 감지! 위치 보정함");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateAnimation()
|
|
{
|
|
if (pAnim == null) return;
|
|
|
|
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f;
|
|
if (attackScript != null && attackScript.IsCharging) animVal *= 0.5f;
|
|
|
|
pAnim.UpdateMove(animVal);
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// 대시
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
private IEnumerator DashRoutine()
|
|
{
|
|
_isDashing = true;
|
|
_lastDashTime = Time.time;
|
|
|
|
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward;
|
|
|
|
if (health != null) health.isInvincible = true;
|
|
|
|
float startTime = Time.time;
|
|
while (Time.time < startTime + dashDuration)
|
|
{
|
|
float speed = dashDistance / dashDuration;
|
|
Vector3 dashMotion = dashDir * speed * Time.deltaTime;
|
|
|
|
// ✅ 벽 충돌 감지
|
|
CollisionFlags flags = _controller.Move(dashMotion);
|
|
|
|
yield return null;
|
|
}
|
|
|
|
if (health != null) health.isInvincible = false;
|
|
_isDashing = false;
|
|
}
|
|
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
// 유틸리티
|
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
public bool IsGrounded() => _controller.isGrounded;
|
|
public bool IsDashing() => _isDashing;
|
|
public float GetCurrentSpeed() => _controller.velocity.magnitude;
|
|
} |