This commit is contained in:
윤기주_playm 2026-01-30 17:53:18 +09:00
parent 7967bfb405
commit 2e401f1042
7 changed files with 93 additions and 80 deletions

View File

@ -146660,6 +146660,9 @@ PrefabInstance:
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 2123637860} addedObject: {fileID: 2123637860}
m_AddedComponents: m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1
addedObject: {fileID: 1432447527}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 1432447519} addedObject: {fileID: 1432447519}
@ -146672,9 +146675,6 @@ PrefabInstance:
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 1432447533} addedObject: {fileID: 1432447533}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1
addedObject: {fileID: 1432447527}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3} - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 76cee5628aa6b874c96342f004fa138b, type: 3}
insertIndex: -1 insertIndex: -1
addedObject: {fileID: 1432447531} addedObject: {fileID: 1432447531}
@ -146787,8 +146787,9 @@ MonoBehaviour:
pAnim: {fileID: 1432447533} pAnim: {fileID: 1432447533}
playerHealth: {fileID: 1432447526} playerHealth: {fileID: 1432447526}
weaponHitBox: {fileID: 1938848879} weaponHitBox: {fileID: 1938848879}
attackCooldown: 0.4 attackCooldown: 0.5
fullChargeTime: 3 fullChargeTime: 3
postComboDelay: 1.2
--- !u!114 &1432447528 --- !u!114 &1432447528
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@ -85,7 +85,14 @@ ModelImporter:
mirror: 0 mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000 bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: [] curves: []
events: [] events:
- time: 0.26926506
functionName: OnAttackEnd
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: [] transformMask: []
maskType: 3 maskType: 3
maskSource: {instanceID: 0} maskSource: {instanceID: 0}

View File

@ -65218,14 +65218,14 @@ AnimationClip:
m_HasMotionFloatCurves: 0 m_HasMotionFloatCurves: 0
m_Events: m_Events:
- time: 0.6333333 - time: 0.6333333
functionName: OnAttackShake functionName: StartWeaponCollision
data: data:
objectReferenceParameter: {fileID: 0} objectReferenceParameter: {fileID: 0}
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.6333333 - time: 0.65
functionName: StartWeaponCollision functionName: OnAttackShake
data: data:
objectReferenceParameter: {fileID: 0} objectReferenceParameter: {fileID: 0}
floatParameter: 0 floatParameter: 0

View File

@ -47825,7 +47825,7 @@ AnimationClip:
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.43333334 - time: 0.45
functionName: OnAttackShake functionName: OnAttackShake
data: data:
objectReferenceParameter: {fileID: 0} objectReferenceParameter: {fileID: 0}

View File

@ -67324,14 +67324,14 @@ AnimationClip:
m_HasMotionFloatCurves: 0 m_HasMotionFloatCurves: 0
m_Events: m_Events:
- time: 0.2 - time: 0.2
functionName: OnAttackShake functionName: StartWeaponCollision
data: data:
objectReferenceParameter: {fileID: 0} objectReferenceParameter: {fileID: 0}
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.20410536 - time: 0.21666667
functionName: StartWeaponCollision functionName: OnAttackShake
data: data:
objectReferenceParameter: {fileID: 0} objectReferenceParameter: {fileID: 0}
floatParameter: 0 floatParameter: 0

View File

@ -1,5 +1,6 @@
using UnityEngine; using UnityEngine;
using UnityEngine.AI; using UnityEngine.AI;
using System.Collections; // ⭐ IEnumerator 사용을 위해 추가
using System; using System;
public class MonsterClass : MonoBehaviour, IDamageable public class MonsterClass : MonoBehaviour, IDamageable
@ -18,6 +19,11 @@ public class MonsterClass : MonoBehaviour, IDamageable
protected AudioSource audioSource; protected AudioSource audioSource;
protected bool isHit; protected bool isHit;
protected bool isDead; protected bool isDead;
protected bool isAttacking;
[Header("AI 설정 (공격 후 휴식)")]
[SerializeField] protected float attackRestDuration = 1.5f; // ⭐ 공격 후 쉬는 시간 (초)
protected bool isResting; // ⭐ 현재 쉬고 있는 상태인지 확인
[Header("경험치")] [Header("경험치")]
[SerializeField] protected int expReward = 10; [SerializeField] protected int expReward = 10;
@ -33,7 +39,6 @@ public class MonsterClass : MonoBehaviour, IDamageable
[Header("--- 드랍 설정 ---")] [Header("--- 드랍 설정 ---")]
[SerializeField] private GameObject potionPrefab; [SerializeField] private GameObject potionPrefab;
[SerializeField][Range(0f, 100f)] private float potionDropChance = 30f; [SerializeField][Range(0f, 100f)] private float potionDropChance = 30f;
[Space(10)] [Space(10)]
[SerializeField] private GameObject[] weaponPrefabs; [SerializeField] private GameObject[] weaponPrefabs;
[SerializeField][Range(0f, 100f)] private float weaponDropChance = 5f; [SerializeField][Range(0f, 100f)] private float weaponDropChance = 5f;
@ -47,15 +52,23 @@ public class MonsterClass : MonoBehaviour, IDamageable
OnHealthChanged?.Invoke(currentHP, maxHP); OnHealthChanged?.Invoke(currentHP, maxHP);
} }
// ⭐ [애니메이션 버그 해결] 매 프레임 속도를 체크해서 애니메이터에 전달합니다.
protected virtual void Update() protected virtual void Update()
{ {
if (isDead) return; if (isDead) return;
if (agent != null && animator != null) // ⭐ [수정] 피격(isHit), 공격(isAttacking), 또는 휴식(isResting) 중이면 이동을 멈춥니다.
bool shouldStop = isHit || isAttacking || isResting;
if (agent != null && agent.isOnNavMesh)
{ {
// 에이전트의 현재 이동 속도를 가져와서 "Speed" 파라미터에 넣어줍니다. agent.isStopped = shouldStop;
float currentSpeed = agent.velocity.magnitude; if (shouldStop) agent.velocity = Vector3.zero;
}
if (animator != null)
{
// 멈췄을 때는 속도를 0으로 전달하여 Idle 애니메이션이 나오게 합니다.
float currentSpeed = (agent != null && !shouldStop) ? agent.velocity.magnitude : 0f;
animator.SetFloat("Speed", currentSpeed); animator.SetFloat("Speed", currentSpeed);
} }
} }
@ -75,12 +88,37 @@ public class MonsterClass : MonoBehaviour, IDamageable
protected virtual void StartHit() protected virtual void StartHit()
{ {
isHit = true; isHit = true;
isAttacking = false;
isResting = false; // ⭐ 맞으면 쉬는 걸 중단하고 바로 아파해야 합니다.
StopAllCoroutines(); // 진행 중인 휴식 루틴을 끕니다.
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
animator.Play(Monster_GetDamage, 0, 0f); animator.Play(Monster_GetDamage, 0, 0f);
if (hitEffect) hitEffect.Play(); if (hitEffect) hitEffect.Play();
if (hitSound) audioSource.PlayOneShot(hitSound); if (hitSound) audioSource.PlayOneShot(hitSound);
} }
public virtual void OnAttackStart() { isAttacking = true; isResting = false; }
// ⭐ [수정] 공격이 끝나면 바로 움직이는 게 아니라 '휴식' 코루틴을 실행합니다.
public virtual void OnAttackEnd()
{
isAttacking = false;
if (!isDead && !isHit)
{
StartCoroutine(RestAfterAttack());
}
}
// ⭐ 공격 후 대기 코루틴
private IEnumerator RestAfterAttack()
{
isResting = true;
yield return new WaitForSeconds(attackRestDuration); // 설정한 시간만큼 대기
isResting = false;
}
public virtual void OnHitEnd() { isHit = false; if (agent && agent.isOnNavMesh) agent.isStopped = false; } public virtual void OnHitEnd() { isHit = false; if (agent && agent.isOnNavMesh) agent.isStopped = false; }
protected virtual void Die() protected virtual void Die()
@ -88,7 +126,6 @@ public class MonsterClass : MonoBehaviour, IDamageable
if (isDead) return; if (isDead) return;
isDead = true; isDead = true;
// 아이템 날아감 방지: 몬스터 시체의 충돌을 끕니다.
Collider col = GetComponent<Collider>(); Collider col = GetComponent<Collider>();
if (col != null) col.enabled = false; if (col != null) col.enabled = false;
@ -97,7 +134,6 @@ public class MonsterClass : MonoBehaviour, IDamageable
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
animator.Play(Monster_Die, 0, 0f); animator.Play(Monster_Die, 0, 0f);
Debug.Log("애니메이션테스트");
if (deathSound) audioSource.PlayOneShot(deathSound); if (deathSound) audioSource.PlayOneShot(deathSound);
Destroy(gameObject, 1.5f); Destroy(gameObject, 1.5f);
@ -105,11 +141,7 @@ public class MonsterClass : MonoBehaviour, IDamageable
private void TryDropItems() private void TryDropItems()
{ {
if (potionPrefab != null && UnityEngine.Random.Range(0f, 100f) <= potionDropChance) if (potionPrefab != null && UnityEngine.Random.Range(0f, 100f) <= potionDropChance) SpawnItem(potionPrefab);
{
SpawnItem(potionPrefab);
}
if (weaponPrefabs != null && weaponPrefabs.Length > 0 && UnityEngine.Random.Range(0f, 100f) <= weaponDropChance) if (weaponPrefabs != null && weaponPrefabs.Length > 0 && UnityEngine.Random.Range(0f, 100f) <= weaponDropChance)
{ {
int randomIndex = UnityEngine.Random.Range(0, weaponPrefabs.Length); int randomIndex = UnityEngine.Random.Range(0, weaponPrefabs.Length);
@ -121,20 +153,13 @@ public class MonsterClass : MonoBehaviour, IDamageable
{ {
Vector3 spawnPos = transform.position + Vector3.up * 0.5f; Vector3 spawnPos = transform.position + Vector3.up * 0.5f;
GameObject item = Instantiate(prefab, spawnPos, Quaternion.identity); GameObject item = Instantiate(prefab, spawnPos, Quaternion.identity);
// ✅ [유저 요청] 배율 없이 프리팹의 원래 크기를 100% 그대로 적용
item.transform.localScale = prefab.transform.localScale; item.transform.localScale = prefab.transform.localScale;
if (item.TryGetComponent<Rigidbody>(out var rb)) if (item.TryGetComponent<Rigidbody>(out var rb))
{ {
// 튀어오르는 힘은 유저님이 설정하신 0.02f 유지
Vector3 popDir = Vector3.up * 0.02f + UnityEngine.Random.insideUnitSphere * 0.02f; Vector3 popDir = Vector3.up * 0.02f + UnityEngine.Random.insideUnitSphere * 0.02f;
rb.AddForce(popDir, ForceMode.Impulse); rb.AddForce(popDir, ForceMode.Impulse);
if (prefab != potionPrefab) rb.AddTorque(UnityEngine.Random.insideUnitSphere * 0.3f, ForceMode.Impulse);
if (prefab != potionPrefab)
{
rb.AddTorque(UnityEngine.Random.insideUnitSphere * 0.3f, ForceMode.Impulse);
}
} }
} }

View File

@ -10,12 +10,10 @@ public class NormalMonster : MonsterClass
[SerializeField] private float damage = 10f; [SerializeField] private float damage = 10f;
[SerializeField] private float attackRange = 2f; [SerializeField] private float attackRange = 2f;
[SerializeField] private float attackDelay = 1.5f; [SerializeField] private float attackDelay = 1.5f;
[SerializeField] private float postAttackDelay = 1.5f; // [SerializeField] private float postAttackDelay = 1.5f; // ⭐ MonsterClass의 attackRestDuration을 사용하므로 삭제 권장
private float lastAttackTime; private float lastAttackTime;
// ⭐ MonsterClass에 OnHealthChanged가 있으므로 여기서는 삭제합니다.
[Header("공격 애니메이션")] [Header("공격 애니메이션")]
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" }; [SerializeField] private string[] attackAnimations = { "Monster_Attack_1" };
[Header("이동 애니메이션")] [Header("이동 애니메이션")]
@ -33,12 +31,15 @@ public class NormalMonster : MonsterClass
private Transform player; private Transform player;
private int attackIndex; private int attackIndex;
private bool isAttacking;
// ❌ [삭제] bool isAttacking;
// ⭐ 부모(MonsterClass)에 이미 있으므로 자식에서 또 선언하면 에러가 납니다!
private bool isPlayerInZone; private bool isPlayerInZone;
protected override void Start() protected override void Start()
{ {
base.Start(); base.Start(); // 부모의 Start 실행
player = GameObject.FindWithTag("Player")?.transform; player = GameObject.FindWithTag("Player")?.transform;
agent.stoppingDistance = attackRange - 0.4f; agent.stoppingDistance = attackRange - 0.4f;
animator.applyRootMotion = false; animator.applyRootMotion = false;
@ -46,13 +47,17 @@ public class NormalMonster : MonsterClass
protected override void Update() protected override void Update()
{ {
// ⭐ 부모의 Update(이동 제한 로직)를 먼저 실행합니다.
base.Update();
if (isDead || player == null) return; if (isDead || player == null) return;
// ⭐ CS0103 에러 해결: 아래 정의된 Patrol()을 호출 // 피격 중이거나 공격 중, 혹은 쉬는 중(isResting)에는 AI 로직을 타지 않습니다.
if (isHit || isAttacking || isResting) return;
if (isPlayerInZone) HandlePlayerTarget(); if (isPlayerInZone) HandlePlayerTarget();
else Patrol(); else Patrol();
// ⭐ CS0103 에러 해결: 아래 정의된 UpdateMovementAnimation()을 호출
UpdateMovementAnimation(); UpdateMovementAnimation();
} }
@ -60,21 +65,12 @@ public class NormalMonster : MonsterClass
{ {
float distance = Vector3.Distance(transform.position, player.position); float distance = Vector3.Distance(transform.position, player.position);
if (distance <= agent.stoppingDistance + 0.05f) // 공격 범위 안에 들어왔을 때
{
agent.isStopped = true;
agent.velocity = Vector3.zero;
}
else if (!isAttacking)
{
agent.isStopped = false;
}
if (distance <= attackRange - stopBuffer) if (distance <= attackRange - stopBuffer)
{ {
if (!isAttacking) TryAttack(); TryAttack();
} }
else if (Time.time >= nextRepathTime && !isAttacking) else if (Time.time >= nextRepathTime)
{ {
agent.SetDestination(player.position); agent.SetDestination(player.position);
nextRepathTime = Time.time + repathInterval; nextRepathTime = Time.time + repathInterval;
@ -89,41 +85,33 @@ public class NormalMonster : MonsterClass
string attackName = attackAnimations[attackIndex]; string attackName = attackAnimations[attackIndex];
attackIndex = (attackIndex + 1) % attackAnimations.Length; attackIndex = (attackIndex + 1) % attackAnimations.Length;
isAttacking = true; // ⭐ 부모의 OnAttackStart를 호출하여 isAttacking을 true로 만들고 이동을 멈춥니다.
agent.isStopped = true; OnAttackStart();
agent.velocity = Vector3.zero;
animator.Play(attackName, 0, 0f); animator.Play(attackName, 0, 0f);
} }
// ⭐ CS0506 해결: 부모의 virtual 메서드를 정상적으로 재정의 // ⭐ [수정] 부모의 가상 메서드를 오버라이드합니다.
public override void TakeDamage(float amount) public override void OnAttackStart()
{ {
base.TakeDamage(amount); // 부모의 OnDamaged(UI 신호 포함)를 실행 base.OnAttackStart(); // 부모의 isAttacking = true 로직 실행
} }
public void OnAttackEnd() // ⭐ [수정] 부모의 가상 메서드를 오버라이드하여 '휴식' 기능을 활성화합니다.
public override void OnAttackEnd()
{ {
StartCoroutine(PostAttackWait()); base.OnAttackEnd(); // 부모의 'n초간 휴식' 코루틴을 실행합니다!
} }
private IEnumerator PostAttackWait() public void OnAttackHit() // 애니메이션 이벤트 (공격 판정 시점)
{ {
yield return new WaitForSeconds(postAttackDelay); if (player == null || isHit || isDead) return;
isAttacking = false;
if (agent && agent.isOnNavMesh) agent.isStopped = false;
}
public void OnAttackHit()
{
if (player == null) return;
PlayerHealth playerHealth = player.GetComponent<PlayerHealth>(); PlayerHealth playerHealth = player.GetComponent<PlayerHealth>();
if (playerHealth != null) playerHealth.TakeDamage(damage); if (playerHealth != null) playerHealth.TakeDamage(damage);
} }
// ⭐ 복구된 이동 애니메이션 함수
void UpdateMovementAnimation() void UpdateMovementAnimation()
{ {
if (isAttacking || isHit) return; if (isAttacking || isHit || isResting) return;
if (agent.velocity.magnitude < 0.1f) if (agent.velocity.magnitude < 0.1f)
animator.Play(Monster_Idle); animator.Play(Monster_Idle);
@ -131,7 +119,6 @@ public class NormalMonster : MonsterClass
animator.Play(Monster_Walk); animator.Play(Monster_Walk);
} }
// ⭐ 복구된 순찰 함수
void Patrol() void Patrol()
{ {
if (Time.time < nextPatrolTime) return; if (Time.time < nextPatrolTime) return;
@ -155,11 +142,4 @@ public class NormalMonster : MonsterClass
{ {
if (other.CompareTag("Player")) isPlayerInZone = false; if (other.CompareTag("Player")) isPlayerInZone = false;
} }
protected override void StartHit()
{
isAttacking = false;
if (agent && agent.isOnNavMesh) agent.isStopped = false;
base.StartHit();
}
} }