- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP) - ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트 - 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom) - 씬, 프리팹, 애니메이션 등 전체 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
329 lines
21 KiB
C#
329 lines
21 KiB
C#
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
|
using System; // 시스템 기능을 사용할거에요 -> Array.IndexOf용
|
|
|
|
// ============================================================
|
|
// PlayerFootstep v4
|
|
//
|
|
// 방식: 속도 기반 타이머
|
|
// 이동 중일 때 일정 간격으로 발소리 재생
|
|
// 속도가 빠를수록 간격이 짧아짐
|
|
//
|
|
// 장점:
|
|
// - 본 추적 불필요 → 어떤 모델에도 즉시 작동
|
|
// - 변수가 적어 안정적
|
|
// - 설정이 간단 (클립만 넣으면 끝)
|
|
//
|
|
// v3 → v4 변경:
|
|
// - 발 본 Y좌표 추적 방식 제거
|
|
// - 속도 기반 타이머 방식으로 교체
|
|
// - 기존 기능 유지: 재질 감지, 피치/볼륨 랜덤, 파티클
|
|
// ============================================================
|
|
|
|
[RequireComponent(typeof(AudioSource))] // 컴포넌트를 강제 추가할거에요 -> AudioSource를
|
|
public class PlayerFootstep : MonoBehaviour // 클래스를 선언할거에요 -> 플레이어 발소리를
|
|
{
|
|
// ─────────────────────────────────────────────────────────
|
|
// 이동 속도 설정
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
[Header("=== 이동 속도 설정 ===")]
|
|
|
|
[Tooltip("이 속도 이하면 발소리 재생 안 함 (제자리 방지)")]
|
|
[SerializeField] private float minSpeedThreshold = 0.1f; // 변수를 선언할거에요 -> 최소 속도를
|
|
|
|
[Tooltip("이 속도 이상이면 달리기 발소리 세트 사용")]
|
|
[SerializeField] private float runSpeedThreshold = 4.0f; // 변수를 선언할거에요 -> 달리기 판정 속도를
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 발소리 간격 설정 (속도 기반 타이머 핵심)
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
[Header("=== 발소리 간격 설정 ===")]
|
|
|
|
[Tooltip("걷기 시 발소리 간격 (초)\n낮을수록 발소리가 자주 남\n추천: 0.4~0.6")]
|
|
[SerializeField] private float walkStepInterval = 0.5f; // 변수를 선언할거에요 -> 걷기 발소리 간격을
|
|
|
|
[Tooltip("달리기 시 발소리 간격 (초)\n걷기보다 짧아야 자연스러움\n추천: 0.25~0.35")]
|
|
[SerializeField] private float runStepInterval = 0.3f; // 변수를 선언할거에요 -> 달리기 발소리 간격을
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 발소리 — 걷기
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
[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.5)")]
|
|
[SerializeField] private float groundRayDistance = 0.5f; // 변수를 선언할거에요 -> 바닥 감지 거리를
|
|
|
|
[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 _stepTimer; // 변수를 선언할거에요 -> 다음 발소리까지 남은 시간 타이머를
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 초기화
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
private void Awake() // 함수를 실행할거에요 -> 초기화를
|
|
{
|
|
_audioSource = GetComponent<AudioSource>(); // 가져올거에요 -> AudioSource를
|
|
_audioSource.playOnAwake = false; // 끌거에요 -> 자동 재생을
|
|
_originalPitch = _audioSource.pitch; // 저장할거에요 -> 원래 Pitch를
|
|
|
|
if (playerMovement == null) // 조건이 맞으면 실행할거에요 -> 연결 안 됐으면
|
|
playerMovement = GetComponent<PlayerMovement>(); // 가져올거에요 -> 같은 오브젝트에서 자동 캐싱
|
|
|
|
if (playerMovement == null) // 조건이 맞으면 실행할거에요 -> 그래도 없으면
|
|
playerMovement = GetComponentInParent<PlayerMovement>(); // 가져올거에요 -> 부모에서 자동 캐싱
|
|
|
|
if (playerMovement == null) // 조건이 맞으면 실행할거에요 -> 그래도 없으면
|
|
Debug.LogError("[PlayerFootstep] PlayerMovement를 찾을 수 없습니다! 인스펙터에서 연결해주세요."); // 에러를 출력할거에요
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 매 프레임 — 속도 기반 타이머
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
private void Update() // 함수를 실행할거에요 -> 매 프레임 발소리 타이머 체크를
|
|
{
|
|
// --- 1. 이동 속도 가져오기 ---
|
|
if (playerMovement == null) return; // 중단할거에요 -> 참조 없으면
|
|
|
|
float currentSpeed = playerMovement.GetCurrentSpeed(); // 가져올거에요 -> 현재 이동 속도를
|
|
|
|
// --- 2. 멈춰있으면 타이머 리셋 ---
|
|
if (currentSpeed < minSpeedThreshold) // 조건이 맞으면 실행할거에요 -> 거의 안 움직이면
|
|
{
|
|
_stepTimer = 0f; // 리셋할거에요 -> 타이머를 0으로 (다시 움직이면 바로 소리 나게)
|
|
return; // 중단할거에요 -> 멈춰있으니 발소리 안 냄
|
|
}
|
|
|
|
// --- 3. 현재 속도에 맞는 발소리 간격 계산 ---
|
|
bool isRunning = currentSpeed >= runSpeedThreshold; // 판별할거에요 -> 달리기 중인지
|
|
float currentInterval = isRunning ? runStepInterval : walkStepInterval; // 결정할거에요 -> 현재 발소리 간격을
|
|
|
|
// --- 4. 타이머 누적 ---
|
|
_stepTimer += Time.deltaTime; // 더할거에요 -> 경과 시간을
|
|
|
|
// --- 5. 간격 도달 시 발소리 재생 ---
|
|
if (_stepTimer >= currentInterval) // 조건이 맞으면 실행할거에요 -> 간격에 도달했으면
|
|
{
|
|
_stepTimer = 0f; // 리셋할거에요 -> 타이머를 0으로 (다음 발소리 대기)
|
|
PlayFootstep(); // 재생할거에요 -> 발소리를
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 발소리 재생
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
private void PlayFootstep() // 함수를 선언할거에요 -> 발소리를 재생하는 PlayFootstep을
|
|
{
|
|
// --- 1. 플레이어 발 아래 위치 계산 ---
|
|
Vector3 footPosition = transform.position; // 가져올거에요 -> 플레이어 위치를 (발 위치 근사)
|
|
|
|
// --- 2. 현재 상태에 맞는 클립 세트 선택 ---
|
|
AudioClip[] clips = GetCurrentClipSet(footPosition); // 가져올거에요 -> 클립 배열을
|
|
if (clips == null || clips.Length == 0) return; // 중단할거에요 -> 클립 없으면
|
|
|
|
// --- 3. 직전 클립과 다른 클립 선택 (반복 방지) ---
|
|
AudioClip clip = PickNonRepeat(clips); // 뽑을거에요 -> 반복 없는 클립을
|
|
if (clip == null) return; // 중단할거에요 -> null이면
|
|
|
|
// --- 4. 걷기/달리기에 따른 기본 볼륨 결정 ---
|
|
bool isRunning = playerMovement.GetCurrentSpeed() >= runSpeedThreshold; // 판별할거에요 -> 달리기 여부를
|
|
float baseVolume = isRunning ? runVolume : walkVolume; // 결정할거에요 -> 볼륨을
|
|
|
|
// --- 5. Pitch 랜덤 적용 (매번 다른 느낌) ---
|
|
_audioSource.pitch = UnityEngine.Random.Range(pitchMin, pitchMax); // 설정할거에요 -> 랜덤 Pitch를
|
|
|
|
// --- 6. Volume 랜덤 적용 ---
|
|
float vol = Mathf.Clamp( // 계산할거에요 -> 0~1 범위 랜덤 볼륨을
|
|
baseVolume + UnityEngine.Random.Range(-volumeVariation, volumeVariation),
|
|
0f, 1f
|
|
);
|
|
|
|
// --- 7. 재생 ---
|
|
_audioSource.PlayOneShot(clip, vol); // 재생할거에요 -> 발소리를
|
|
|
|
// --- 8. 복원 및 기록 ---
|
|
_audioSource.pitch = _originalPitch; // 복원할거에요 -> 원래 Pitch로
|
|
_lastFootstepClip = clip; // 저장할거에요 -> 마지막 클립을
|
|
|
|
// --- 9. 파티클 ---
|
|
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 (playerMovement.GetCurrentSpeed() >= runSpeedThreshold && 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 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 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;
|
|
}
|
|
}
|