420 lines
27 KiB
C#
420 lines
27 KiB
C#
|
|
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
|||
|
|
using System; // 시스템 기능을 사용할거에요 -> Array.IndexOf용
|
|||
|
|
|
|||
|
|
// ============================================================
|
|||
|
|
// PlayerFootstep v3
|
|||
|
|
//
|
|||
|
|
// 방식: 발 본(foot.l / foot.r)의 월드 Y 좌표를 매 프레임 추적
|
|||
|
|
// 발이 아래로 내려오다가 멈추는 순간 = 실제 접지 순간
|
|||
|
|
// → 그 순간에 발소리 + 파티클 재생
|
|||
|
|
//
|
|||
|
|
// 장점:
|
|||
|
|
// - 콜라이더 / 물리 설정 불필요
|
|||
|
|
// - 애니메이션 이벤트 등록 불필요
|
|||
|
|
// - 어떤 애니메이션에도 자동 대응
|
|||
|
|
// - 발바닥이 실제로 멈추는 순간을 정확히 감지
|
|||
|
|
//
|
|||
|
|
// 필요 설정:
|
|||
|
|
// 1. Player 오브젝트에 Add Component → PlayerFootstep
|
|||
|
|
// 2. 발소리 클립 슬롯 채우기
|
|||
|
|
// 3. (선택) 발자국 파티클 프리팹 연결
|
|||
|
|
// ============================================================
|
|||
|
|
|
|||
|
|
[RequireComponent(typeof(AudioSource))] // 컴포넌트를 강제 추가할거에요 -> AudioSource를
|
|||
|
|
public class PlayerFootstep : MonoBehaviour // 클래스를 선언할거에요 -> 플레이어 발소리를
|
|||
|
|
{
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발 본 설정
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 발 본 설정 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("왼발 본 이름 (Hierarchy에서 확인)")]
|
|||
|
|
[SerializeField] private string leftFootBoneName = "foot.l"; // 변수를 선언할거에요 -> 왼발 본 이름을
|
|||
|
|
|
|||
|
|
[Tooltip("오른발 본 이름 (Hierarchy에서 확인)")]
|
|||
|
|
[SerializeField] private string rightFootBoneName = "foot.r"; // 변수를 선언할거에요 -> 오른발 본 이름을
|
|||
|
|
|
|||
|
|
[Tooltip("접지 판정 Y속도 임계값 (음수)\n발이 내려오다 이 속도 이하가 되면 접지로 판정\n기본 -0.002 / 너무 예민하면 -0.005로 낮춰요")]
|
|||
|
|
[SerializeField] private float landingVelocityThreshold = -0.002f; // 변수를 선언할거에요 -> 접지 판정 Y속도 임계값을
|
|||
|
|
|
|||
|
|
[Tooltip("발 높이 최대값 (루트 기준)\n이 높이 이상에서는 발소리 무시 → 점프 중 오감지 방지\n기본 0.25")]
|
|||
|
|
[SerializeField] private float maxFootHeight = 0.25f; // 변수를 선언할거에요 -> 공중 판정 발 높이를
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 이동 속도 설정
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 이동 속도 설정 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("이 속도 이하면 발소리 재생 안 함 (제자리 애니메이션 방지)")]
|
|||
|
|
[SerializeField] private float minSpeedThreshold = 0.1f; // 변수를 선언할거에요 -> 최소 속도를
|
|||
|
|
|
|||
|
|
[Tooltip("이 속도 이상이면 달리기 발소리 세트 사용")]
|
|||
|
|
[SerializeField] private float runSpeedThreshold = 4.0f; // 변수를 선언할거에요 -> 달리기 판정 속도를
|
|||
|
|
|
|||
|
|
[Tooltip("발소리 재생 쿨타임 (초)\n양발 동시 감지 중복 방지 / 기본 0.1")]
|
|||
|
|
[SerializeField] private float footstepCooldown = 0.1f; // 변수를 선언할거에요 -> 발소리 쿨타임을
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발소리 — 걷기
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 발소리 — 걷기 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("걷기 기본 발소리 클립 배열 (재질 매칭 실패 시 사용)")]
|
|||
|
|
[SerializeField] private AudioClip[] walkFootsteps; // 배열을 선언할거에요 -> 걷기 발소리들을
|
|||
|
|
|
|||
|
|
[Tooltip("걷기 발소리 볼륨 (기본 0.35)")]
|
|||
|
|
[SerializeField][Range(0f, 1f)] private float walkVolume = 0.35f; // 변수를 선언할거에요 -> 걷기 볼륨을
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발소리 — 달리기
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 발소리 — 달리기 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("달리기 발소리 클립 배열 (없으면 걷기 발소리 사용)")]
|
|||
|
|
[SerializeField] private AudioClip[] runFootsteps; // 배열을 선언할거에요 -> 달리기 발소리들을
|
|||
|
|
|
|||
|
|
[Tooltip("달리기 발소리 볼륨 (기본 0.5)")]
|
|||
|
|
[SerializeField][Range(0f, 1f)] private float runVolume = 0.5f; // 변수를 선언할거에요 -> 달리기 볼륨을
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발소리 — 바닥 재질별
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 발소리 — 바닥 재질별 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("바닥 감지 Raycast 거리 (발 아래로 쏘는 레이, 기본 0.3)")]
|
|||
|
|
[SerializeField] private float groundRayDistance = 0.3f; // 변수를 선언할거에요 -> 바닥 감지 거리를
|
|||
|
|
|
|||
|
|
[Tooltip("바닥 감지 레이어 (Ground 레이어 선택)")]
|
|||
|
|
[SerializeField] private LayerMask groundLayerMask = ~0; // 변수를 선언할거에요 -> 바닥 레이어를
|
|||
|
|
|
|||
|
|
[Tooltip("재질별 발소리 배열 (SurfaceFootstep.cs 공유)\nTag 또는 PhysicMaterial 이름으로 자동 매칭")]
|
|||
|
|
[SerializeField] private SurfaceFootstep[] surfaceFootsteps; // 배열을 선언할거에요 -> 재질별 발소리 묶음들을
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// Pitch / Volume 랜덤
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== Pitch / Volume 랜덤 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("Pitch 최솟값 (기본 0.93 → ±7% 범위)")]
|
|||
|
|
[SerializeField][Range(0.5f, 1.5f)] private float pitchMin = 0.93f; // 변수를 선언할거에요 -> 피치 최솟값을
|
|||
|
|
|
|||
|
|
[Tooltip("Pitch 최댓값 (기본 1.07)")]
|
|||
|
|
[SerializeField][Range(0.5f, 1.5f)] private float pitchMax = 1.07f; // 변수를 선언할거에요 -> 피치 최댓값을
|
|||
|
|
|
|||
|
|
[Tooltip("Volume 랜덤 범위 ± (기본 0.05 → ±5%)")]
|
|||
|
|
[SerializeField][Range(0f, 0.2f)] private float volumeVariation = 0.05f; // 변수를 선언할거에요 -> 볼륨 변동 범위를
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발자국 파티클
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 발자국 파티클 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("발자국 파티클 프리팹 (없으면 파티클 생략)")]
|
|||
|
|
[SerializeField] private GameObject footstepParticlePrefab; // 변수를 선언할거에요 -> 발자국 파티클 프리팹을
|
|||
|
|
|
|||
|
|
[Tooltip("파티클 자동 제거 시간 (초, 기본 2.0)")]
|
|||
|
|
[SerializeField] private float particleDestroyTime = 2.0f; // 변수를 선언할거에요 -> 파티클 제거 시간을
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 참조
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
[Header("=== 참조 ===")]
|
|||
|
|
|
|||
|
|
[Tooltip("이동 속도 체크용 (비워두면 자동 캐싱)")]
|
|||
|
|
[SerializeField] private PlayerMovement playerMovement; // 변수를 선언할거에요 -> 이동 속도 체크용을
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 내부 변수
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private AudioSource _audioSource; // 변수를 선언할거에요 -> AudioSource를
|
|||
|
|
private float _originalPitch; // 변수를 선언할거에요 -> 원래 Pitch를 (복원용)
|
|||
|
|
private AudioClip _lastFootstepClip; // 변수를 선언할거에요 -> 마지막 재생 클립을 (반복 방지)
|
|||
|
|
private float _lastFootstepTime; // 변수를 선언할거에요 -> 마지막 발소리 시간을 (쿨타임용)
|
|||
|
|
|
|||
|
|
private Transform _leftFootBone; // 변수를 선언할거에요 -> 왼발 본 Transform을
|
|||
|
|
private Transform _rightFootBone; // 변수를 선언할거에요 -> 오른발 본 Transform을
|
|||
|
|
|
|||
|
|
private float _leftFootPrevY; // 변수를 선언할거에요 -> 이전 프레임 왼발 Y를 (Y속도 계산용)
|
|||
|
|
private float _rightFootPrevY; // 변수를 선언할거에요 -> 이전 프레임 오른발 Y를 (Y속도 계산용)
|
|||
|
|
|
|||
|
|
private bool _leftFootGrounded; // 변수를 선언할거에요 -> 왼발 현재 접지 상태를 (연속 감지 방지)
|
|||
|
|
private bool _rightFootGrounded; // 변수를 선언할거에요 -> 오른발 현재 접지 상태를 (연속 감지 방지)
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 초기화
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void Awake() // 함수를 실행할거에요 -> 초기화를
|
|||
|
|
{
|
|||
|
|
_audioSource = GetComponent<AudioSource>(); // 가져올거에요 -> AudioSource를
|
|||
|
|
_audioSource.playOnAwake = false; // 끌거에요 -> 자동 재생을
|
|||
|
|
_originalPitch = _audioSource.pitch; // 저장할거에요 -> 원래 Pitch를
|
|||
|
|
|
|||
|
|
if (playerMovement == null) // 조건이 맞으면 실행할거에요 -> 연결 안 됐으면
|
|||
|
|
playerMovement = GetComponent<PlayerMovement>(); // 가져올거에요 -> 자동 캐싱
|
|||
|
|
|
|||
|
|
_leftFootBone = FindBoneRecursive(transform, leftFootBoneName); // 찾을거에요 -> 왼발 본을
|
|||
|
|
_rightFootBone = FindBoneRecursive(transform, rightFootBoneName); // 찾을거에요 -> 오른발 본을
|
|||
|
|
|
|||
|
|
if (_leftFootBone == null) // 조건이 맞으면 실행할거에요 -> 못 찾으면
|
|||
|
|
Debug.LogWarning($"[PlayerFootstep] 왼발 본 '{leftFootBoneName}' 을 찾지 못했어요!"); // 경고를 출력할거에요
|
|||
|
|
if (_rightFootBone == null) // 조건이 맞으면 실행할거에요 -> 못 찾으면
|
|||
|
|
Debug.LogWarning($"[PlayerFootstep] 오른발 본 '{rightFootBoneName}' 을 찾지 못했어요!"); // 경고를 출력할거에요
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void Start() // 함수를 실행할거에요 -> 시작 시 초기 Y 저장
|
|||
|
|
{
|
|||
|
|
// 첫 프레임 오감지 방지용 초기 Y 저장
|
|||
|
|
if (_leftFootBone != null) _leftFootPrevY = _leftFootBone.position.y; // 저장할거에요 -> 왼발 초기 Y를
|
|||
|
|
if (_rightFootBone != null) _rightFootPrevY = _rightFootBone.position.y; // 저장할거에요 -> 오른발 초기 Y를
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 매 프레임 발 Y속도 감지
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void Update() // 함수를 실행할거에요 -> 매 프레임 발 접지 감지를
|
|||
|
|
{
|
|||
|
|
// 이동 속도가 너무 낮으면 감지 스킵 (성능 최적화 + 제자리 방지)
|
|||
|
|
if (playerMovement != null && playerMovement.GetCurrentSpeed() < minSpeedThreshold) // 조건이 맞으면 실행할거에요 -> 거의 안 움직이면
|
|||
|
|
{
|
|||
|
|
_leftFootGrounded = false; // 초기화할거에요 -> 다음 걸음 감지 준비로
|
|||
|
|
_rightFootGrounded = false; // 초기화할거에요 -> 다음 걸음 감지 준비로
|
|||
|
|
return; // 중단할거에요
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (_leftFootBone != null) // 조건이 맞으면 실행할거에요 -> 왼발 본 있으면
|
|||
|
|
CheckFootLanding(_leftFootBone, ref _leftFootPrevY, ref _leftFootGrounded); // 확인할거에요 -> 왼발 접지를
|
|||
|
|
|
|||
|
|
if (_rightFootBone != null) // 조건이 맞으면 실행할거에요 -> 오른발 본 있으면
|
|||
|
|
CheckFootLanding(_rightFootBone, ref _rightFootPrevY, ref _rightFootGrounded); // 확인할거에요 -> 오른발 접지를
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발 접지 감지 핵심 로직
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void CheckFootLanding(Transform footBone, ref float prevY, ref bool isGrounded) // 함수를 선언할거에요 -> 발 접지를 감지하는 CheckFootLanding을
|
|||
|
|
{
|
|||
|
|
float currentY = footBone.position.y; // 가져올거에요 -> 현재 발의 월드 Y 좌표를
|
|||
|
|
|
|||
|
|
// 루트 기준 발 높이 계산 → 너무 높으면 공중(점프 중) → 무시
|
|||
|
|
float footHeightAboveRoot = currentY - transform.position.y; // 계산할거에요 -> 루트 기준 발 높이를
|
|||
|
|
if (footHeightAboveRoot > maxFootHeight) // 조건이 맞으면 실행할거에요 -> 발이 너무 높으면 (공중)
|
|||
|
|
{
|
|||
|
|
prevY = currentY; // 갱신할거에요 -> 이전 Y를
|
|||
|
|
isGrounded = false; // 초기화할거에요 -> 접지 상태를
|
|||
|
|
return; // 중단할거에요 -> 공중이므로 무시
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Y속도 계산: (현재Y - 이전Y) / deltaTime
|
|||
|
|
// → 양수 = 올라가는 중 / 음수 = 내려오는 중 / 0에 가까움 = 멈춤
|
|||
|
|
float yVelocity = (currentY - prevY) / Time.deltaTime; // 계산할거에요 -> 발의 Y축 속도를
|
|||
|
|
|
|||
|
|
// ⭐ 핵심 감지:
|
|||
|
|
// Y속도가 임계값 이하 (내려오다 멈추는 순간) + 아직 접지 아닐 때
|
|||
|
|
// = 발바닥이 실제로 바닥에 닿는 바로 그 순간
|
|||
|
|
if (yVelocity <= landingVelocityThreshold && !isGrounded) // 조건이 맞으면 실행할거에요 -> 발이 내려오다 멈추는 순간
|
|||
|
|
{
|
|||
|
|
isGrounded = true; // 설정할거에요 -> 접지 상태로
|
|||
|
|
TryPlayFootstep(footBone.position); // 재생할거에요 -> 발소리를
|
|||
|
|
}
|
|||
|
|
// 발이 다시 올라가기 시작하면 접지 해제 → 다음 스텝 감지 준비
|
|||
|
|
else if (yVelocity > 0.001f && isGrounded) // 조건이 맞으면 실행할거에요 -> 발이 다시 올라가면
|
|||
|
|
{
|
|||
|
|
isGrounded = false; // 해제할거에요 -> 접지 상태를
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
prevY = currentY; // 갱신할거에요 -> 이전 Y를 현재 Y로 (다음 프레임 계산용)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발소리 재생
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void TryPlayFootstep(Vector3 footPosition) // 함수를 선언할거에요 -> 발소리 재생을 시도하는 TryPlayFootstep을
|
|||
|
|
{
|
|||
|
|
// 쿨타임 체크 (양발 동시 접지 시 중복 방지)
|
|||
|
|
if (Time.time - _lastFootstepTime < footstepCooldown) return; // 중단할거에요 -> 쿨타임 중이면
|
|||
|
|
|
|||
|
|
// 현재 상태에 맞는 클립 세트 선택
|
|||
|
|
AudioClip[] clips = GetCurrentClipSet(footPosition); // 가져올거에요 -> 클립 배열을
|
|||
|
|
if (clips == null || clips.Length == 0) return; // 중단할거에요 -> 클립 없으면
|
|||
|
|
|
|||
|
|
// 직전 클립과 다른 클립 선택 (반복 방지)
|
|||
|
|
AudioClip clip = PickNonRepeat(clips); // 뽑을거에요 -> 반복 없는 클립을
|
|||
|
|
if (clip == null) return; // 중단할거에요 -> null이면
|
|||
|
|
|
|||
|
|
// 걷기/달리기에 따른 기본 볼륨 결정
|
|||
|
|
float baseVolume = IsRunning() ? runVolume : walkVolume; // 결정할거에요 -> 볼륨을
|
|||
|
|
|
|||
|
|
// Pitch 랜덤 적용 (매번 다른 느낌)
|
|||
|
|
_audioSource.pitch = UnityEngine.Random.Range(pitchMin, pitchMax); // 설정할거에요 -> 랜덤 Pitch를
|
|||
|
|
|
|||
|
|
// Volume 랜덤 적용
|
|||
|
|
float vol = Mathf.Clamp( // 계산할거에요 -> 0~1 범위 랜덤 볼륨을
|
|||
|
|
baseVolume + UnityEngine.Random.Range(-volumeVariation, volumeVariation),
|
|||
|
|
0f, 1f
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
_audioSource.PlayOneShot(clip, vol); // 재생할거에요 -> 발소리를
|
|||
|
|
_audioSource.pitch = _originalPitch; // 복원할거에요 -> 원래 Pitch로
|
|||
|
|
|
|||
|
|
_lastFootstepClip = clip; // 저장할거에요 -> 마지막 클립을
|
|||
|
|
_lastFootstepTime = Time.time; // 저장할거에요 -> 마지막 재생 시간을
|
|||
|
|
|
|||
|
|
SpawnFootstepParticle(footPosition); // 생성할거에요 -> 발자국 파티클을
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 클립 세트 선택 (바닥 재질 + 이동 상태)
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private AudioClip[] GetCurrentClipSet(Vector3 footPosition) // 함수를 선언할거에요 -> 현재 상태에 맞는 클립 배열을 반환하는 GetCurrentClipSet을
|
|||
|
|
{
|
|||
|
|
SurfaceType surface = DetectSurface(footPosition); // 감지할거에요 -> 바닥 재질을
|
|||
|
|
|
|||
|
|
// 재질별 클립 우선 반환
|
|||
|
|
if (surfaceFootsteps != null) // 조건이 맞으면 실행할거에요 -> 재질별 설정 있으면
|
|||
|
|
{
|
|||
|
|
foreach (SurfaceFootstep sf in surfaceFootsteps) // 반복할거에요 -> 순회를
|
|||
|
|
{
|
|||
|
|
if (sf.surfaceType == surface && sf.clips != null && sf.clips.Length > 0) // 조건이 맞으면 실행할거에요 -> 일치하면
|
|||
|
|
return sf.clips; // 반환할거에요 -> 재질 클립을
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 달리기 / 걷기 기본 세트
|
|||
|
|
if (IsRunning() && runFootsteps != null && runFootsteps.Length > 0) // 조건이 맞으면 실행할거에요 -> 달리기 중이고 클립 있으면
|
|||
|
|
return runFootsteps; // 반환할거에요 -> 달리기 클립을
|
|||
|
|
|
|||
|
|
return walkFootsteps; // 반환할거에요 -> 걷기 클립을
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private SurfaceType DetectSurface(Vector3 position) // 함수를 선언할거에요 -> 바닥 재질을 감지하는 DetectSurface를
|
|||
|
|
{
|
|||
|
|
if (!Physics.Raycast(position + Vector3.up * 0.1f, Vector3.down, out RaycastHit hit, groundRayDistance, groundLayerMask)) // 조건이 맞으면 실행할거에요 -> 바닥 못 찾으면
|
|||
|
|
return SurfaceType.Default; // 반환할거에요 -> 기본 타입을
|
|||
|
|
|
|||
|
|
SurfaceType surface = TagToSurface(hit.collider.tag); // 변환할거에요 -> Tag로 재질을
|
|||
|
|
|
|||
|
|
if (surface == SurfaceType.Default && hit.collider.sharedMaterial != null) // 조건이 맞으면 실행할거에요 -> Tag 실패 시 PhysicMaterial로
|
|||
|
|
surface = NameToSurface(hit.collider.sharedMaterial.name); // 변환할거에요 -> 이름으로 재질을
|
|||
|
|
|
|||
|
|
return surface; // 반환할거에요 -> 감지된 재질을
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 발자국 파티클
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private void SpawnFootstepParticle(Vector3 position) // 함수를 선언할거에요 -> 발자국 파티클을 생성하는 SpawnFootstepParticle을
|
|||
|
|
{
|
|||
|
|
if (footstepParticlePrefab == null) return; // 중단할거에요 -> 프리팹 없으면
|
|||
|
|
|
|||
|
|
Vector3 spawnPos = position; // 초기화할거에요 -> 생성 위치를
|
|||
|
|
|
|||
|
|
// Raycast로 정확한 바닥 표면 위치 찾기
|
|||
|
|
if (Physics.Raycast(position + Vector3.up * 0.1f, Vector3.down, out RaycastHit hit, groundRayDistance, groundLayerMask)) // 조건이 맞으면 실행할거에요 -> 바닥 찾으면
|
|||
|
|
spawnPos = hit.point; // 설정할거에요 -> 정확한 바닥 표면으로
|
|||
|
|
|
|||
|
|
GameObject particle = Instantiate(footstepParticlePrefab, spawnPos, Quaternion.identity); // 생성할거에요 -> 파티클 오브젝트를
|
|||
|
|
Destroy(particle, particleDestroyTime); // 제거할거에요 -> 지정 시간 후 자동으로
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 유틸리티
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
private bool IsRunning() // 함수를 선언할거에요 -> 달리기 중인지 판별하는 IsRunning을
|
|||
|
|
{
|
|||
|
|
if (playerMovement == null) return false; // 반환할거에요 -> 참조 없으면 false를
|
|||
|
|
return playerMovement.GetCurrentSpeed() >= runSpeedThreshold; // 반환할거에요 -> 속도 기준 이상이면 true를
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private AudioClip PickNonRepeat(AudioClip[] clips) // 함수를 선언할거에요 -> 직전 클립과 다른 클립을 뽑는 PickNonRepeat를
|
|||
|
|
{
|
|||
|
|
if (clips.Length == 1) return clips[0]; // 반환할거에요 -> 1개뿐이면 그냥
|
|||
|
|
|
|||
|
|
AudioClip picked = clips[UnityEngine.Random.Range(0, clips.Length)]; // 뽑을거에요 -> 랜덤으로
|
|||
|
|
|
|||
|
|
if (picked == _lastFootstepClip) // 조건이 맞으면 실행할거에요 -> 직전과 같으면
|
|||
|
|
{
|
|||
|
|
int idx = Array.IndexOf(clips, picked); // 찾을거에요 -> 현재 인덱스를
|
|||
|
|
picked = clips[(idx + 1) % clips.Length]; // 뽑을거에요 -> 다음 인덱스로
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return picked; // 반환할거에요 -> 선택된 클립을
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private Transform FindBoneRecursive(Transform parent, string boneName) // 함수를 선언할거에요 -> 본 이름으로 재귀 탐색하는 FindBoneRecursive를
|
|||
|
|
{
|
|||
|
|
if (parent.name == boneName) return parent; // 반환할거에요 -> 찾았으면 바로
|
|||
|
|
|
|||
|
|
foreach (Transform child in parent) // 반복할거에요 -> 모든 자식을
|
|||
|
|
{
|
|||
|
|
Transform found = FindBoneRecursive(child, boneName); // 탐색할거에요 -> 재귀로
|
|||
|
|
if (found != null) return found; // 반환할거에요 -> 찾았으면
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null; // 반환할거에요 -> 없으면 null을
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private SurfaceType TagToSurface(string tag) // 함수를 선언할거에요 -> Tag를 SurfaceType으로 변환하는 TagToSurface를
|
|||
|
|
{
|
|||
|
|
switch (tag)
|
|||
|
|
{
|
|||
|
|
case "Stone": return SurfaceType.Stone;
|
|||
|
|
case "Dirt": return SurfaceType.Dirt;
|
|||
|
|
case "Wood": return SurfaceType.Wood;
|
|||
|
|
case "Metal": return SurfaceType.Metal;
|
|||
|
|
case "Grass": return SurfaceType.Grass;
|
|||
|
|
case "Water": return SurfaceType.Water;
|
|||
|
|
default: return SurfaceType.Default;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private SurfaceType NameToSurface(string materialName) // 함수를 선언할거에요 -> PhysicMaterial 이름을 SurfaceType으로 변환하는 NameToSurface를
|
|||
|
|
{
|
|||
|
|
string lower = materialName.ToLower(); // 변환할거에요 -> 소문자로
|
|||
|
|
if (lower.Contains("stone") || lower.Contains("concrete") || lower.Contains("rock")) return SurfaceType.Stone;
|
|||
|
|
if (lower.Contains("dirt") || lower.Contains("sand") || lower.Contains("soil")) return SurfaceType.Dirt;
|
|||
|
|
if (lower.Contains("wood") || lower.Contains("plank")) return SurfaceType.Wood;
|
|||
|
|
if (lower.Contains("metal") || lower.Contains("iron") || lower.Contains("steel")) return SurfaceType.Metal;
|
|||
|
|
if (lower.Contains("grass") || lower.Contains("lawn")) return SurfaceType.Grass;
|
|||
|
|
if (lower.Contains("water") || lower.Contains("puddle")) return SurfaceType.Water;
|
|||
|
|
return SurfaceType.Default;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
// 에디터 디버그 (씬 뷰 시각화)
|
|||
|
|
// ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
#if UNITY_EDITOR
|
|||
|
|
private void OnDrawGizmos() // 함수를 실행할거에요 -> 씬 뷰 디버그를
|
|||
|
|
{
|
|||
|
|
if (!Application.isPlaying) return; // 중단할거에요 -> 실행 중 아니면
|
|||
|
|
|
|||
|
|
// 접지 상태에 따라 색상 변경 (초록 = 접지, 빨강 = 공중)
|
|||
|
|
if (_leftFootBone != null) // 조건이 맞으면 실행할거에요 -> 왼발 본 있으면
|
|||
|
|
{
|
|||
|
|
Gizmos.color = _leftFootGrounded ? Color.green : Color.red; // 설정할거에요 -> 접지 여부 색상을
|
|||
|
|
Gizmos.DrawWireSphere(_leftFootBone.position, 0.04f); // 그릴거에요 -> 왼발 위치 구를
|
|||
|
|
}
|
|||
|
|
if (_rightFootBone != null) // 조건이 맞으면 실행할거에요 -> 오른발 본 있으면
|
|||
|
|
{
|
|||
|
|
Gizmos.color = _rightFootGrounded ? Color.green : Color.red; // 설정할거에요 -> 접지 여부 색상을
|
|||
|
|
Gizmos.DrawWireSphere(_rightFootBone.position, 0.04f); // 그릴거에요 -> 오른발 위치 구를
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
#endif
|
|||
|
|
}
|