Merge branch 'main' of http://59.19.179.48:3300/playm/Projext
# Conflicts:
This commit is contained in:
commit
88536069ee
File diff suppressed because it is too large
Load Diff
|
|
@ -11,8 +11,8 @@ GameObject:
|
||||||
- component: {fileID: 5216897380834477129}
|
- component: {fileID: 5216897380834477129}
|
||||||
- component: {fileID: 8833972876663021627}
|
- component: {fileID: 8833972876663021627}
|
||||||
- component: {fileID: 2113351772197486994}
|
- component: {fileID: 2113351772197486994}
|
||||||
- component: {fileID: 6772475815529408483}
|
|
||||||
- component: {fileID: 9112013798456009864}
|
- component: {fileID: 9112013798456009864}
|
||||||
|
- component: {fileID: 3440399914476953485}
|
||||||
m_Layer: 8
|
m_Layer: 8
|
||||||
m_Name: Heal Town
|
m_Name: Heal Town
|
||||||
m_TagString: Item
|
m_TagString: Item
|
||||||
|
|
@ -85,19 +85,6 @@ MeshRenderer:
|
||||||
m_SortingLayer: 0
|
m_SortingLayer: 0
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 0
|
||||||
m_AdditionalVertexStreams: {fileID: 0}
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
--- !u!114 &6772475815529408483
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 7603101133414256123}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: de0cc0f35b02e054faf0780c75d0a720, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier:
|
|
||||||
cooldown: 5
|
|
||||||
--- !u!65 &9112013798456009864
|
--- !u!65 &9112013798456009864
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
@ -119,3 +106,18 @@ BoxCollider:
|
||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_Size: {x: 1, y: 1, z: 1}
|
m_Size: {x: 1, y: 1, z: 1}
|
||||||
m_Center: {x: 0, y: 0, z: 0}
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &3440399914476953485
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 7603101133414256123}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: f200c95f2cb77e24dbd8cddfd1baced0, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
healAmount: 50
|
||||||
|
interactRange: 3.5
|
||||||
|
healEffect: {fileID: 0}
|
||||||
|
|
|
||||||
|
|
@ -1482,27 +1482,23 @@ MonoBehaviour:
|
||||||
m_Script: {fileID: 11500000, guid: 8c775790f7852314c8d6527ffe977e03, type: 3}
|
m_Script: {fileID: 11500000, guid: 8c775790f7852314c8d6527ffe977e03, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
|
optimizationDistance: 40
|
||||||
maxHP: 9
|
maxHP: 9
|
||||||
|
Monster_Idle: Monster_Idle
|
||||||
Monster_GetDamage: Monster_GetDamage
|
Monster_GetDamage: Monster_GetDamage
|
||||||
Monster_Die: 'Monster_Die '
|
Monster_Die: Monster_Die
|
||||||
|
attackRestDuration: 1.5
|
||||||
expReward: 10
|
expReward: 10
|
||||||
hitSound: {fileID: 0}
|
hitSound: {fileID: 0}
|
||||||
deathSound: {fileID: 0}
|
deathSound: {fileID: 0}
|
||||||
deathEffectPrefab: {fileID: 0}
|
deathEffectPrefab: {fileID: 0}
|
||||||
hitEffect: {fileID: 0}
|
hitEffect: {fileID: 0}
|
||||||
impactSpawnPoint: {fileID: 0}
|
impactSpawnPoint: {fileID: 0}
|
||||||
potionPrefab: {fileID: 1171483609561952824, guid: 5d18660a7620e4e48bb1462c7e0e1187, type: 3}
|
|
||||||
potionDropChance: 100
|
|
||||||
weaponPrefabs:
|
|
||||||
- {fileID: 8071134912250732547, guid: 9f51c4433e5c81644807e9e547b7826c, type: 3}
|
|
||||||
weaponDropChance: 100
|
|
||||||
damage: 10
|
damage: 10
|
||||||
attackRange: 2
|
attackRange: 2
|
||||||
attackDelay: 1.5
|
attackDelay: 1.5
|
||||||
postAttackDelay: 1.5
|
|
||||||
attackAnimations:
|
attackAnimations:
|
||||||
- Monster_Attack_1
|
- Monster_Attack_1
|
||||||
Monster_Idle: Monster_Idle
|
|
||||||
Monster_Walk: Monster_Walk
|
Monster_Walk: Monster_Walk
|
||||||
stopBuffer: 0.3
|
stopBuffer: 0.3
|
||||||
patrolRadius: 5
|
patrolRadius: 5
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
public class HealthAltar : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("--- 제단 설정 ---")]
|
|
||||||
[SerializeField] private float cooldown = 60f; // 재사용 대기시간
|
|
||||||
|
|
||||||
private float _nextUseTime = 0f;
|
|
||||||
|
|
||||||
// PlayerInteraction에서 호출할 함수
|
|
||||||
public void Use(PlayerHealth target)
|
|
||||||
{
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
if (Time.time >= _nextUseTime)
|
|
||||||
{
|
|
||||||
// 제단은 강력하니까 풀피로 채워줍니다.
|
|
||||||
target.Heal(9999f);
|
|
||||||
|
|
||||||
_nextUseTime = Time.time + cooldown; // 쿨타임 설정
|
|
||||||
Debug.Log("<color=cyan>[Altar]</color> 제단 사용! 기운이 넘칩니다.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
float remaining = Mathf.Ceil(_nextUseTime - Time.time);
|
|
||||||
Debug.Log($"<color=yellow>[Altar]</color> 아직 기운이 모이지 않았습니다. ({remaining}초 남음)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.AI;
|
|
||||||
using System.Collections; // ⭐ IEnumerator 사용을 위해 추가
|
|
||||||
using System;
|
|
||||||
|
|
||||||
public class MonsterClass : MonoBehaviour, IDamageable
|
|
||||||
{
|
|
||||||
[Header("스탯")]
|
|
||||||
[SerializeField] protected float maxHP = 100f;
|
|
||||||
protected float currentHP;
|
|
||||||
public event Action<float, float> OnHealthChanged;
|
|
||||||
|
|
||||||
[Header("피격 / 사망 애니메이션")]
|
|
||||||
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage";
|
|
||||||
[SerializeField] protected string Monster_Die = "Monster_Die";
|
|
||||||
|
|
||||||
protected Animator animator;
|
|
||||||
protected NavMeshAgent agent;
|
|
||||||
protected AudioSource audioSource;
|
|
||||||
protected bool isHit;
|
|
||||||
protected bool isDead;
|
|
||||||
protected bool isAttacking;
|
|
||||||
|
|
||||||
[Header("AI 설정 (공격 후 휴식)")]
|
|
||||||
[SerializeField] protected float attackRestDuration = 1.5f; // ⭐ 공격 후 쉬는 시간 (초)
|
|
||||||
protected bool isResting; // ⭐ 현재 쉬고 있는 상태인지 확인
|
|
||||||
|
|
||||||
[Header("경험치")]
|
|
||||||
[SerializeField] protected int expReward = 10;
|
|
||||||
public static System.Action<int> OnMonsterKilled;
|
|
||||||
|
|
||||||
[Header("공통 사운드/이펙트")]
|
|
||||||
[SerializeField] protected AudioClip hitSound;
|
|
||||||
[SerializeField] protected AudioClip deathSound;
|
|
||||||
[SerializeField] protected GameObject deathEffectPrefab;
|
|
||||||
[SerializeField] protected ParticleSystem hitEffect;
|
|
||||||
[SerializeField] protected Transform impactSpawnPoint;
|
|
||||||
|
|
||||||
[Header("--- 드랍 설정 ---")]
|
|
||||||
[SerializeField] private GameObject potionPrefab;
|
|
||||||
[SerializeField][Range(0f, 100f)] private float potionDropChance = 30f;
|
|
||||||
[Space(10)]
|
|
||||||
[SerializeField] private GameObject[] weaponPrefabs;
|
|
||||||
[SerializeField][Range(0f, 100f)] private float weaponDropChance = 5f;
|
|
||||||
|
|
||||||
protected virtual void Start()
|
|
||||||
{
|
|
||||||
currentHP = maxHP;
|
|
||||||
animator = GetComponent<Animator>();
|
|
||||||
agent = GetComponent<NavMeshAgent>();
|
|
||||||
audioSource = GetComponent<AudioSource>();
|
|
||||||
OnHealthChanged?.Invoke(currentHP, maxHP);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Update()
|
|
||||||
{
|
|
||||||
if (isDead) return;
|
|
||||||
|
|
||||||
// ⭐ [수정] 피격(isHit), 공격(isAttacking), 또는 휴식(isResting) 중이면 이동을 멈춥니다.
|
|
||||||
bool shouldStop = isHit || isAttacking || isResting;
|
|
||||||
|
|
||||||
if (agent != null && agent.isOnNavMesh)
|
|
||||||
{
|
|
||||||
agent.isStopped = shouldStop;
|
|
||||||
if (shouldStop) agent.velocity = Vector3.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animator != null)
|
|
||||||
{
|
|
||||||
// 멈췄을 때는 속도를 0으로 전달하여 Idle 애니메이션이 나오게 합니다.
|
|
||||||
float currentSpeed = (agent != null && !shouldStop) ? agent.velocity.magnitude : 0f;
|
|
||||||
animator.SetFloat("Speed", currentSpeed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void TakeDamage(float amount) { OnDamaged(amount); }
|
|
||||||
|
|
||||||
public virtual void OnDamaged(float damage)
|
|
||||||
{
|
|
||||||
if (isDead) return;
|
|
||||||
currentHP -= damage;
|
|
||||||
OnHealthChanged?.Invoke(currentHP, maxHP);
|
|
||||||
|
|
||||||
if (currentHP <= 0) { Die(); return; }
|
|
||||||
if (!isHit) StartHit();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void StartHit()
|
|
||||||
{
|
|
||||||
isHit = true;
|
|
||||||
isAttacking = false;
|
|
||||||
isResting = false; // ⭐ 맞으면 쉬는 걸 중단하고 바로 아파해야 합니다.
|
|
||||||
StopAllCoroutines(); // 진행 중인 휴식 루틴을 끕니다.
|
|
||||||
|
|
||||||
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
|
||||||
|
|
||||||
animator.Play(Monster_GetDamage, 0, 0f);
|
|
||||||
if (hitEffect) hitEffect.Play();
|
|
||||||
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; }
|
|
||||||
|
|
||||||
protected virtual void Die()
|
|
||||||
{
|
|
||||||
if (isDead) return;
|
|
||||||
isDead = true;
|
|
||||||
|
|
||||||
Collider col = GetComponent<Collider>();
|
|
||||||
if (col != null) col.enabled = false;
|
|
||||||
|
|
||||||
OnMonsterKilled?.Invoke(expReward);
|
|
||||||
TryDropItems();
|
|
||||||
|
|
||||||
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
|
||||||
animator.Play(Monster_Die, 0, 0f);
|
|
||||||
if (deathSound) audioSource.PlayOneShot(deathSound);
|
|
||||||
|
|
||||||
Destroy(gameObject, 1.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryDropItems()
|
|
||||||
{
|
|
||||||
if (potionPrefab != null && UnityEngine.Random.Range(0f, 100f) <= potionDropChance) SpawnItem(potionPrefab);
|
|
||||||
if (weaponPrefabs != null && weaponPrefabs.Length > 0 && UnityEngine.Random.Range(0f, 100f) <= weaponDropChance)
|
|
||||||
{
|
|
||||||
int randomIndex = UnityEngine.Random.Range(0, weaponPrefabs.Length);
|
|
||||||
SpawnItem(weaponPrefabs[randomIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SpawnItem(GameObject prefab)
|
|
||||||
{
|
|
||||||
Vector3 spawnPos = transform.position + Vector3.up * 0.5f;
|
|
||||||
GameObject item = Instantiate(prefab, spawnPos, Quaternion.identity);
|
|
||||||
item.transform.localScale = prefab.transform.localScale;
|
|
||||||
|
|
||||||
if (item.TryGetComponent<Rigidbody>(out var rb))
|
|
||||||
{
|
|
||||||
Vector3 popDir = Vector3.up * 0.02f + UnityEngine.Random.insideUnitSphere * 0.02f;
|
|
||||||
rb.AddForce(popDir, ForceMode.Impulse);
|
|
||||||
if (prefab != potionPrefab) rb.AddTorque(UnityEngine.Random.insideUnitSphere * 0.3f, ForceMode.Impulse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnDeathAnimEnd() { }
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.AI;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
public class NormalMonster : MonsterClass
|
|
||||||
{
|
|
||||||
[Header("전투")]
|
|
||||||
[SerializeField] private float damage = 10f;
|
|
||||||
[SerializeField] private float attackRange = 2f;
|
|
||||||
[SerializeField] private float attackDelay = 1.5f;
|
|
||||||
// [SerializeField] private float postAttackDelay = 1.5f; // ⭐ MonsterClass의 attackRestDuration을 사용하므로 삭제 권장
|
|
||||||
|
|
||||||
private float lastAttackTime;
|
|
||||||
|
|
||||||
[Header("공격 애니메이션")]
|
|
||||||
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" };
|
|
||||||
[Header("이동 애니메이션")]
|
|
||||||
[SerializeField] private string Monster_Idle = "Monster_Idle";
|
|
||||||
[SerializeField] private string Monster_Walk = "Monster_Walk";
|
|
||||||
|
|
||||||
[Header("AI")]
|
|
||||||
[SerializeField] private float stopBuffer = 0.3f;
|
|
||||||
[SerializeField] private float patrolRadius = 5f;
|
|
||||||
[SerializeField] private float patrolInterval = 2f;
|
|
||||||
|
|
||||||
private float nextPatrolTime;
|
|
||||||
private float repathInterval = 0.3f;
|
|
||||||
private float nextRepathTime;
|
|
||||||
|
|
||||||
private Transform player;
|
|
||||||
private int attackIndex;
|
|
||||||
|
|
||||||
// ❌ [삭제] bool isAttacking;
|
|
||||||
// ⭐ 부모(MonsterClass)에 이미 있으므로 자식에서 또 선언하면 에러가 납니다!
|
|
||||||
|
|
||||||
private bool isPlayerInZone;
|
|
||||||
|
|
||||||
protected override void Start()
|
|
||||||
{
|
|
||||||
base.Start(); // 부모의 Start 실행
|
|
||||||
player = GameObject.FindWithTag("Player")?.transform;
|
|
||||||
agent.stoppingDistance = attackRange - 0.4f;
|
|
||||||
animator.applyRootMotion = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
// ⭐ 부모의 Update(이동 제한 로직)를 먼저 실행합니다.
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (isDead || player == null) return;
|
|
||||||
|
|
||||||
// 피격 중이거나 공격 중, 혹은 쉬는 중(isResting)에는 AI 로직을 타지 않습니다.
|
|
||||||
if (isHit || isAttacking || isResting) return;
|
|
||||||
|
|
||||||
if (isPlayerInZone) HandlePlayerTarget();
|
|
||||||
else Patrol();
|
|
||||||
|
|
||||||
UpdateMovementAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HandlePlayerTarget()
|
|
||||||
{
|
|
||||||
float distance = Vector3.Distance(transform.position, player.position);
|
|
||||||
|
|
||||||
// 공격 범위 안에 들어왔을 때
|
|
||||||
if (distance <= attackRange - stopBuffer)
|
|
||||||
{
|
|
||||||
TryAttack();
|
|
||||||
}
|
|
||||||
else if (Time.time >= nextRepathTime)
|
|
||||||
{
|
|
||||||
agent.SetDestination(player.position);
|
|
||||||
nextRepathTime = Time.time + repathInterval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TryAttack()
|
|
||||||
{
|
|
||||||
if (Time.time < lastAttackTime + attackDelay) return;
|
|
||||||
lastAttackTime = Time.time;
|
|
||||||
|
|
||||||
string attackName = attackAnimations[attackIndex];
|
|
||||||
attackIndex = (attackIndex + 1) % attackAnimations.Length;
|
|
||||||
|
|
||||||
// ⭐ 부모의 OnAttackStart를 호출하여 isAttacking을 true로 만들고 이동을 멈춥니다.
|
|
||||||
OnAttackStart();
|
|
||||||
animator.Play(attackName, 0, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ [수정] 부모의 가상 메서드를 오버라이드합니다.
|
|
||||||
public override void OnAttackStart()
|
|
||||||
{
|
|
||||||
base.OnAttackStart(); // 부모의 isAttacking = true 로직 실행
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ [수정] 부모의 가상 메서드를 오버라이드하여 '휴식' 기능을 활성화합니다.
|
|
||||||
public override void OnAttackEnd()
|
|
||||||
{
|
|
||||||
base.OnAttackEnd(); // 부모의 'n초간 휴식' 코루틴을 실행합니다!
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnAttackHit() // 애니메이션 이벤트 (공격 판정 시점)
|
|
||||||
{
|
|
||||||
if (player == null || isHit || isDead) return;
|
|
||||||
PlayerHealth playerHealth = player.GetComponent<PlayerHealth>();
|
|
||||||
if (playerHealth != null) playerHealth.TakeDamage(damage);
|
|
||||||
}
|
|
||||||
|
|
||||||
void UpdateMovementAnimation()
|
|
||||||
{
|
|
||||||
if (isAttacking || isHit || isResting) return;
|
|
||||||
|
|
||||||
if (agent.velocity.magnitude < 0.1f)
|
|
||||||
animator.Play(Monster_Idle);
|
|
||||||
else
|
|
||||||
animator.Play(Monster_Walk);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Patrol()
|
|
||||||
{
|
|
||||||
if (Time.time < nextPatrolTime) return;
|
|
||||||
|
|
||||||
Vector3 randomPoint = transform.position +
|
|
||||||
new Vector3(UnityEngine.Random.Range(-patrolRadius, patrolRadius), 0,
|
|
||||||
UnityEngine.Random.Range(-patrolRadius, patrolRadius));
|
|
||||||
|
|
||||||
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
|
||||||
agent.SetDestination(hit.position);
|
|
||||||
|
|
||||||
nextPatrolTime = Time.time + patrolInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTriggerEnter(Collider other)
|
|
||||||
{
|
|
||||||
if (other.CompareTag("Player")) isPlayerInZone = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTriggerExit(Collider other)
|
|
||||||
{
|
|
||||||
if (other.CompareTag("Player")) isPlayerInZone = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 2b9374b755429c54e8d5a37091dd801a
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 3279621260abe3447bd237cd47a1716e
|
guid: a639a46c1923ae841955c6c288c4389e
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using TMPro;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
|
|
||||||
public class SettingResolution : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private TMP_Dropdown resDropDown;
|
|
||||||
|
|
||||||
private bool isFullScreen = true;
|
|
||||||
private List<Resolution> filteredResolutions = new List<Resolution>();
|
|
||||||
|
|
||||||
// 자주 쓰는 해상도 목록
|
|
||||||
private readonly Vector2Int[] commonResolutions =
|
|
||||||
{
|
|
||||||
new Vector2Int(1280, 720),
|
|
||||||
new Vector2Int(1280, 768),
|
|
||||||
new Vector2Int(1280, 800),
|
|
||||||
new Vector2Int(1360, 768),
|
|
||||||
new Vector2Int(1366, 768),
|
|
||||||
new Vector2Int(1680, 1050),
|
|
||||||
new Vector2Int(1920, 1080)
|
|
||||||
};
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
Resolution[] allResolutions = Screen.resolutions;
|
|
||||||
resDropDown.ClearOptions();
|
|
||||||
|
|
||||||
List<string> options = new List<string>();
|
|
||||||
|
|
||||||
foreach (var res in allResolutions)
|
|
||||||
{
|
|
||||||
foreach (var common in commonResolutions)
|
|
||||||
{
|
|
||||||
if (res.width == common.x && res.height == common.y)
|
|
||||||
{
|
|
||||||
// 중복 방지
|
|
||||||
if (!filteredResolutions.Exists(r => r.width == res.width && r.height == res.height))
|
|
||||||
{
|
|
||||||
filteredResolutions.Add(res);
|
|
||||||
options.Add($"{res.width} x {res.height}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resDropDown.AddOptions(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeResolution()
|
|
||||||
{
|
|
||||||
int index = resDropDown.value;
|
|
||||||
Resolution res = filteredResolutions[index];
|
|
||||||
Screen.SetResolution(res.width, res.height, isFullScreen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: a83c4e21df986894589325f495e69d14
|
guid: 345f189c861b6c94d97b768959a8622d
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: dac1e7fbc41b24e468599040b84af80e
|
guid: 1d44bb1087b4a0142a5ef53c141c4560
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
22
Assets/9.Resources/Config/DefaultRenderConfig.asset
Normal file
22
Assets/9.Resources/Config/DefaultRenderConfig.asset
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 22ee61cd4202b8e478ee96264551f1ed, type: 3}
|
||||||
|
m_Name: DefaultRenderConfig
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
_renderRange: 33.2
|
||||||
|
_fadeStartRatio: 0.8
|
||||||
|
_checkInterval: 0.2
|
||||||
|
_playerTag: Player
|
||||||
|
_maxObjectsPerFrame: 0
|
||||||
|
_includeInactiveObjects: 1
|
||||||
|
_showGizmos: 1
|
||||||
|
_enableVerboseLogging: 0
|
||||||
8
Assets/9.Resources/Config/DefaultRenderConfig.asset.meta
Normal file
8
Assets/9.Resources/Config/DefaultRenderConfig.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 994d0debcfccb7048aef075436221e40
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 4caf7e451e2b52f468e0ae69184c1c82
|
guid: 39cf2bff58703e64db8ce1234881f65a
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
526
Assets/Editor/ProjectOrganizer.cs
Normal file
526
Assets/Editor/ProjectOrganizer.cs
Normal file
|
|
@ -0,0 +1,526 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEditor;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unity 프로젝트 폴더 자동 정리 스크립트 V3 (최종 완성판)
|
||||||
|
/// - 공백이 있는 파일명 지원
|
||||||
|
/// - 5.TestScript 폴더 구조 완벽 인식
|
||||||
|
/// - 모든 변형 파일명 검색
|
||||||
|
/// </summary>
|
||||||
|
public class ProjectOrganizerV3 : EditorWindow
|
||||||
|
{
|
||||||
|
private const string TARGET_FOLDER = "Assets/Scripts";
|
||||||
|
private bool _createBackup = true;
|
||||||
|
private bool _showDetailedLog = true;
|
||||||
|
private Vector2 _scrollPosition;
|
||||||
|
private List<string> _manualMoveList = new List<string>();
|
||||||
|
|
||||||
|
[MenuItem("Tools/Project Organizer V3 (Final)")]
|
||||||
|
public static void ShowWindow()
|
||||||
|
{
|
||||||
|
GetWindow<ProjectOrganizerV3>("프로젝트 정리 V3");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
EditorGUILayout.LabelField("🗂️ 프로젝트 폴더 자동 정리 V3 (최종판)", EditorStyles.boldLabel);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"✨ V3 개선사항:\n" +
|
||||||
|
"- 공백이 있는 파일명 지원 (예: 'HP UI bar.cs')\n" +
|
||||||
|
"- 5.TestScript 폴더 구조 완벽 인식\n" +
|
||||||
|
"- Heal/, Level_Scripts/, Optimizaion/ 등 모든 폴더 검색\n" +
|
||||||
|
"- 수동 이동 목록 제공\n\n" +
|
||||||
|
"⚠️ 실행 전 백업 필수!",
|
||||||
|
MessageType.Info
|
||||||
|
);
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
_createBackup = EditorGUILayout.Toggle("백업 자동 생성", _createBackup);
|
||||||
|
_showDetailedLog = EditorGUILayout.Toggle("상세 로그 출력", _showDetailedLog);
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
if (GUILayout.Button("🔍 파일 스캔 + 수동 이동 목록 생성", GUILayout.Height(40)))
|
||||||
|
{
|
||||||
|
ScanFilesWithManualList();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
if (GUILayout.Button("🚀 폴더 구조 생성하기", GUILayout.Height(35)))
|
||||||
|
{
|
||||||
|
if (EditorUtility.DisplayDialog(
|
||||||
|
"폴더 구조 생성",
|
||||||
|
"새로운 폴더 구조를 생성하시겠습니까?",
|
||||||
|
"실행", "취소"))
|
||||||
|
{
|
||||||
|
CreateFolderStructure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
if (GUILayout.Button("📦 찾은 파일 자동 이동하기", GUILayout.Height(40)))
|
||||||
|
{
|
||||||
|
if (EditorUtility.DisplayDialog(
|
||||||
|
"파일 자동 이동",
|
||||||
|
"⚠️ 찾은 파일들을 새 위치로 이동합니다.\n\n계속하시겠습니까?",
|
||||||
|
"실행", "취소"))
|
||||||
|
{
|
||||||
|
if (_createBackup)
|
||||||
|
{
|
||||||
|
CreateBackup();
|
||||||
|
}
|
||||||
|
OrganizeFilesRecursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
if (GUILayout.Button("🔙 백업 생성만 하기", GUILayout.Height(30)))
|
||||||
|
{
|
||||||
|
CreateBackup();
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// 수동 이동 목록 표시
|
||||||
|
if (_manualMoveList.Count > 0)
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
$"⚠️ 수동 이동 필요: {_manualMoveList.Count}개\n아래 Console 로그 참고",
|
||||||
|
MessageType.Warning
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
"💡 권장 순서:\n" +
|
||||||
|
"1. '파일 스캔' 클릭 → Console에서 결과 확인\n" +
|
||||||
|
"2. '백업 생성'\n" +
|
||||||
|
"3. '폴더 구조 생성'\n" +
|
||||||
|
"4. '찾은 파일 자동 이동'\n" +
|
||||||
|
"5. Console에 나온 수동 이동 목록 처리\n" +
|
||||||
|
"6. Unity 컴파일 대기 후 테스트",
|
||||||
|
MessageType.Info
|
||||||
|
);
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScanFilesWithManualList()
|
||||||
|
{
|
||||||
|
Log("🔍 파일 스캔 시작...");
|
||||||
|
_manualMoveList.Clear();
|
||||||
|
|
||||||
|
var fileMap = GetFileMappings();
|
||||||
|
int foundCount = 0;
|
||||||
|
int notFoundCount = 0;
|
||||||
|
List<string> notFoundFiles = new List<string>();
|
||||||
|
|
||||||
|
foreach (var kvp in fileMap)
|
||||||
|
{
|
||||||
|
string fileName = kvp.Key;
|
||||||
|
string targetFolder = kvp.Value;
|
||||||
|
string[] foundFiles = FindScriptAdvanced(fileName);
|
||||||
|
|
||||||
|
if (foundFiles.Length > 0)
|
||||||
|
{
|
||||||
|
foundCount++;
|
||||||
|
Log($" ✅ 찾음: {fileName} → {foundFiles[0]}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notFoundCount++;
|
||||||
|
notFoundFiles.Add(fileName);
|
||||||
|
Log($" ❌ 못 찾음: {fileName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 수동 이동 가이드 생성
|
||||||
|
Log("\n" + new string('=', 60));
|
||||||
|
Log("📋 수동 이동 가이드");
|
||||||
|
Log(new string('=', 60));
|
||||||
|
|
||||||
|
if (notFoundFiles.Count > 0)
|
||||||
|
{
|
||||||
|
Log("\n⚠️ 다음 파일들을 수동으로 이동해주세요:\n");
|
||||||
|
|
||||||
|
foreach (string fileName in notFoundFiles)
|
||||||
|
{
|
||||||
|
if (fileMap.ContainsKey(fileName))
|
||||||
|
{
|
||||||
|
string targetPath = fileMap[fileName];
|
||||||
|
Log($"[ ] {fileName}.cs");
|
||||||
|
Log($" → Assets/Scripts/{targetPath}/");
|
||||||
|
Log("");
|
||||||
|
_manualMoveList.Add($"{fileName} → Scripts/{targetPath}/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("\n💡 수동 이동 방법:");
|
||||||
|
Log("1. Project 창에서 't:Script [파일명]' 으로 검색");
|
||||||
|
Log("2. 찾은 파일을 드래그해서 위 경로로 이동");
|
||||||
|
Log("3. Unity가 자동으로 참조 업데이트");
|
||||||
|
}
|
||||||
|
|
||||||
|
string result = $"스캔 완료!\n✅ 찾음: {foundCount}개\n❌ 못 찾음: {notFoundCount}개";
|
||||||
|
|
||||||
|
if (notFoundCount > 0)
|
||||||
|
{
|
||||||
|
result += $"\n\n⚠️ Console 로그에서 수동 이동 가이드 확인!";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log(result);
|
||||||
|
EditorUtility.DisplayDialog("스캔 완료", result, "확인");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateBackup()
|
||||||
|
{
|
||||||
|
Log("🔙 백업 생성 시작...");
|
||||||
|
|
||||||
|
string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
string backupPath = $"Assets/Scripts_Backup_{timestamp}";
|
||||||
|
|
||||||
|
// 모든 스크립트 폴더 찾기
|
||||||
|
string[] scriptFolders = new string[]
|
||||||
|
{
|
||||||
|
"Assets/5.TestScript",
|
||||||
|
"Assets/Scripts",
|
||||||
|
"Assets/Code" // 혹시 있을 수 있는 다른 폴더
|
||||||
|
};
|
||||||
|
|
||||||
|
AssetDatabase.CreateFolder("Assets", $"Scripts_Backup_{timestamp}");
|
||||||
|
|
||||||
|
foreach (string folder in scriptFolders)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
string folderName = Path.GetFileName(folder);
|
||||||
|
string destFolder = Path.Combine(backupPath, folderName);
|
||||||
|
CopyDirectorySafe(folder, destFolder);
|
||||||
|
Log($" 백업: {folder}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
Log($"✅ 백업 완료: {backupPath}");
|
||||||
|
EditorUtility.DisplayDialog("백업 완료", $"백업이 생성되었습니다:\n{backupPath}", "확인");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyDirectorySafe(string sourceDir, string destDir)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destDir);
|
||||||
|
|
||||||
|
foreach (string file in Directory.GetFiles(sourceDir))
|
||||||
|
{
|
||||||
|
string fileName = Path.GetFileName(file);
|
||||||
|
if (fileName.EndsWith(".meta")) continue;
|
||||||
|
|
||||||
|
string destFile = Path.Combine(destDir, fileName);
|
||||||
|
File.Copy(file, destFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string subDir in Directory.GetDirectories(sourceDir))
|
||||||
|
{
|
||||||
|
string dirName = Path.GetFileName(subDir);
|
||||||
|
string destSubDir = Path.Combine(destDir, dirName);
|
||||||
|
CopyDirectorySafe(subDir, destSubDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Log($"❌ 백업 실패: {sourceDir} - {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateFolderStructure()
|
||||||
|
{
|
||||||
|
Log("📁 폴더 구조 생성 시작...");
|
||||||
|
|
||||||
|
string[] folders = new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"Player/Controller",
|
||||||
|
"Player/Animation",
|
||||||
|
"Player/Combat",
|
||||||
|
"Player/Stats",
|
||||||
|
"Player/Upgrade/Data",
|
||||||
|
"Player/Upgrade/UI",
|
||||||
|
"Player/Equipment",
|
||||||
|
"Player/Interaction",
|
||||||
|
"Combat/Interfaces",
|
||||||
|
"Combat/Components",
|
||||||
|
"Combat/Debug",
|
||||||
|
"Enemy/Spawner",
|
||||||
|
"Enemy/Types",
|
||||||
|
"Items/Pickups",
|
||||||
|
"Items/Interactive",
|
||||||
|
"Items/VFX",
|
||||||
|
"UI/HUD",
|
||||||
|
"UI/Enemy",
|
||||||
|
"UI/Level",
|
||||||
|
"Camera/Effects",
|
||||||
|
"Systems/ObjectPool",
|
||||||
|
"Systems/Scene",
|
||||||
|
"Systems/Settings",
|
||||||
|
"Systems/Optimization/Editor",
|
||||||
|
"Data/Stats",
|
||||||
|
"Utilities"
|
||||||
|
};
|
||||||
|
|
||||||
|
int createdCount = 0;
|
||||||
|
foreach (string folder in folders)
|
||||||
|
{
|
||||||
|
string fullPath = $"{TARGET_FOLDER}/{folder}";
|
||||||
|
if (CreateFolderRecursive(fullPath))
|
||||||
|
{
|
||||||
|
createdCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
Log($"✅ 폴더 구조 생성 완료! ({createdCount}개 폴더)");
|
||||||
|
EditorUtility.DisplayDialog("완료", $"{createdCount}개의 폴더가 생성되었습니다.", "확인");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CreateFolderRecursive(string path)
|
||||||
|
{
|
||||||
|
string[] parts = path.Replace("Assets/", "").Split('/');
|
||||||
|
string currentPath = "Assets";
|
||||||
|
bool created = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
string nextPath = currentPath + "/" + parts[i];
|
||||||
|
|
||||||
|
if (!AssetDatabase.IsValidFolder(nextPath))
|
||||||
|
{
|
||||||
|
AssetDatabase.CreateFolder(currentPath, parts[i]);
|
||||||
|
Log($" 생성됨: {nextPath}");
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath = nextPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OrganizeFilesRecursive()
|
||||||
|
{
|
||||||
|
Log("📦 파일 이동 시작...");
|
||||||
|
|
||||||
|
var fileMap = GetFileMappings();
|
||||||
|
int movedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
|
List<string> notFoundFiles = new List<string>();
|
||||||
|
|
||||||
|
foreach (var kvp in fileMap)
|
||||||
|
{
|
||||||
|
string fileName = kvp.Key;
|
||||||
|
string targetFolder = kvp.Value;
|
||||||
|
|
||||||
|
string[] foundFiles = FindScriptAdvanced(fileName);
|
||||||
|
|
||||||
|
if (foundFiles.Length == 0)
|
||||||
|
{
|
||||||
|
notFoundFiles.Add(fileName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string oldPath = foundFiles[0];
|
||||||
|
string newPath = $"{TARGET_FOLDER}/{targetFolder}/{Path.GetFileName(oldPath)}";
|
||||||
|
|
||||||
|
// 이미 목적지에 있으면 스킵
|
||||||
|
if (oldPath == newPath)
|
||||||
|
{
|
||||||
|
Log($" ⏭️ 스킵: {fileName} (이미 올바른 위치)");
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 이동
|
||||||
|
string result = AssetDatabase.MoveAsset(oldPath, newPath);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(result))
|
||||||
|
{
|
||||||
|
Log($" ✅ 이동: {Path.GetFileName(oldPath)}");
|
||||||
|
Log($" {oldPath} → {newPath}");
|
||||||
|
movedCount++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log($" ❌ 실패: {fileName} - {result}");
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
|
||||||
|
// 결과 출력
|
||||||
|
string resultMessage = $"✅ 이동 완료: {movedCount}개\n";
|
||||||
|
if (skippedCount > 0)
|
||||||
|
{
|
||||||
|
resultMessage += $"⏭️ 스킵: {skippedCount}개 (이미 올바른 위치)\n";
|
||||||
|
}
|
||||||
|
if (failedCount > 0)
|
||||||
|
{
|
||||||
|
resultMessage += $"❌ 실패: {failedCount}개\n";
|
||||||
|
}
|
||||||
|
if (notFoundFiles.Count > 0)
|
||||||
|
{
|
||||||
|
resultMessage += $"\n⚠️ 못 찾음: {notFoundFiles.Count}개\n";
|
||||||
|
resultMessage += "→ Console에서 수동 이동 가이드 확인\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"\n✅ 파일 정리 완료!\n{resultMessage}");
|
||||||
|
EditorUtility.DisplayDialog("완료", resultMessage, "확인");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 고급 스크립트 검색 (공백, 언더스코어, 변형 모두 지원)
|
||||||
|
/// </summary>
|
||||||
|
private string[] FindScriptAdvanced(string fileName)
|
||||||
|
{
|
||||||
|
List<string> results = new List<string>();
|
||||||
|
|
||||||
|
// 1단계: 정확한 이름으로 검색
|
||||||
|
string[] guids = AssetDatabase.FindAssets($"{fileName} t:Script");
|
||||||
|
foreach (string guid in guids)
|
||||||
|
{
|
||||||
|
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||||
|
string scriptName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
if (scriptName == fileName)
|
||||||
|
{
|
||||||
|
results.Add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count > 0) return results.ToArray();
|
||||||
|
|
||||||
|
// 2단계: 변형 검색
|
||||||
|
List<string> variations = new List<string>
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
fileName.Replace("_", " "), // HP_UI_bar → HP UI bar
|
||||||
|
fileName.Replace("_", ""), // HP_UI_bar → HPUIbar
|
||||||
|
fileName.Replace(" ", "_"), // 반대
|
||||||
|
fileName.Replace(" ", ""), // 공백 제거
|
||||||
|
fileName + "UI", // 끝에 UI 붙은 버전
|
||||||
|
fileName.Replace("UI", ""), // UI 제거
|
||||||
|
};
|
||||||
|
|
||||||
|
// 부분 검색
|
||||||
|
string[] allGuids = AssetDatabase.FindAssets("t:Script");
|
||||||
|
foreach (string guid in allGuids)
|
||||||
|
{
|
||||||
|
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||||
|
|
||||||
|
// 5.TestScript 폴더 우선
|
||||||
|
if (!path.Contains("5.TestScript") && !path.Contains("Scripts")) continue;
|
||||||
|
|
||||||
|
string scriptName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
|
||||||
|
foreach (string variation in variations)
|
||||||
|
{
|
||||||
|
if (scriptName.Equals(variation, System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
results.Add(path);
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> GetFileMappings()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
// Player
|
||||||
|
{"PlayerMovement", "Player/Controller"},
|
||||||
|
{"PlayerInput", "Player/Controller"},
|
||||||
|
{"Animation", "Player/Animation"},
|
||||||
|
{"Attack", "Player/Combat"},
|
||||||
|
{"Health", "Combat/Components"},
|
||||||
|
{"Stats", "Player/Stats"},
|
||||||
|
{"PlayerLevelSystem", "Player/Stats"},
|
||||||
|
{"StatType", "Player/Stats"},
|
||||||
|
{"CardData", "Player/Upgrade/Data"},
|
||||||
|
{"RandomStatCardData", "Player/Upgrade/Data"},
|
||||||
|
{"RandomStatCardUIstar", "Player/Upgrade/UI"},
|
||||||
|
{"RandomStatCardUIStar", "Player/Upgrade/UI"},
|
||||||
|
{"CardUI", "Player/Upgrade/UI"},
|
||||||
|
{"EquipItem", "Player/Equipment"},
|
||||||
|
{"PlayerInteraction", "Player/Interaction"},
|
||||||
|
|
||||||
|
// Combat
|
||||||
|
{"IDamageable", "Combat/Interfaces"},
|
||||||
|
{"WeaponHitBox", "Combat/Components"},
|
||||||
|
{"WePonHitBox", "Combat/Components"},
|
||||||
|
{"DamageBot", "Combat/Debug"},
|
||||||
|
|
||||||
|
// Enemy
|
||||||
|
{"MonsterSpawner", "Enemy/Spawner"},
|
||||||
|
{"Monster Spawner", "Enemy/Spawner"},
|
||||||
|
{"MonsterUpdateManager", "Enemy/Spawner"},
|
||||||
|
{"DummyBot", "Enemy/Types"},
|
||||||
|
|
||||||
|
// Items
|
||||||
|
{"HealthPotion", "Items/Pickups"},
|
||||||
|
{"HealthAltar", "Items/Interactive"},
|
||||||
|
{"ItemHighlight", "Items/VFX"},
|
||||||
|
|
||||||
|
// UI
|
||||||
|
{"CrossHairUI", "UI/HUD"},
|
||||||
|
{"HP_UI_bar", "UI/HUD"},
|
||||||
|
{"HP UI bar", "UI/HUD"},
|
||||||
|
{"HPUIbar", "UI/HUD"},
|
||||||
|
{"PlayerStat_UI", "UI/HUD"},
|
||||||
|
{"PlayerStat UI", "UI/HUD"},
|
||||||
|
{"PlayerStatUI", "UI/HUD"},
|
||||||
|
{"EnemyHP_UI", "UI/Enemy"},
|
||||||
|
{"EnemyHP UI", "UI/Enemy"},
|
||||||
|
{"EnemyHPUI", "UI/Enemy"},
|
||||||
|
{"LevelUIManager", "UI/Level"},
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
{"CamShake", "Camera/Effects"},
|
||||||
|
|
||||||
|
// Systems
|
||||||
|
{"GenericObjectPool", "Systems/ObjectPool"},
|
||||||
|
{"LoadScene", "Systems/Scene"},
|
||||||
|
{"SettingResolution", "Systems/Settings"},
|
||||||
|
|
||||||
|
// Data
|
||||||
|
{"StatsConfig", "Data/Stats"},
|
||||||
|
|
||||||
|
// Optimization
|
||||||
|
{"PlayerRangeManager", "Systems/Optimization"},
|
||||||
|
{"RenderGroup", "Systems/Optimization"},
|
||||||
|
{"RenderOptimizationConfig", "Systems/Optimization"},
|
||||||
|
{"PlayerRangeManagerEditor", "Systems/Optimization/Editor"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Log(string message)
|
||||||
|
{
|
||||||
|
if (_showDetailedLog)
|
||||||
|
{
|
||||||
|
Debug.Log($"[ProjectOrganizerV3] {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7e61b80e2cb75b643beafecbb1d4df1c
|
guid: 2b12d38845192374e967e42b5dbf3f08
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7831227d57ec0f449b1b753b4cde4e0f
|
guid: 40c4cc57507d08c4ebb66e069e6f9ea3
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
8
Assets/Scripts/Camera.meta
Normal file
8
Assets/Scripts/Camera.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 922e7c79e85791f42b296c636268ad3a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Camera/Effects.meta
Normal file
8
Assets/Scripts/Camera/Effects.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c46d41decb8955b40aefc96fd7a8c63e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Combat.meta
Normal file
8
Assets/Scripts/Combat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1b2040fde894a464496f34fa2bdab18f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Combat/Components.meta
Normal file
8
Assets/Scripts/Combat/Components.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6bac2714aae63fa4fbd25010973357b1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Combat/Debug.meta
Normal file
8
Assets/Scripts/Combat/Debug.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ff72fdce1749ad5449106b95351d9838
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Combat/Interfaces.meta
Normal file
8
Assets/Scripts/Combat/Interfaces.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ac38dbfbc0a0f1e48bd2b846d3cf4302
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Core.meta
Normal file
8
Assets/Scripts/Core.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 51840a9124595f74da163037009ee461
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Data.meta
Normal file
8
Assets/Scripts/Data.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7037c66bcbd384a45bb1b20a007a3df4
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Data/Stats.meta
Normal file
8
Assets/Scripts/Data/Stats.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8ea49c0b18c8a494e9e64ddd4c2084a0
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Enemy.meta
Normal file
8
Assets/Scripts/Enemy.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b8dc8a8491f300740abd471dd716e251
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Enemy/AI.meta
Normal file
8
Assets/Scripts/Enemy/AI.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f5cb59aaa7a0dc4b84b4a8953689d6a
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
149
Assets/Scripts/Enemy/AI/MonsterClass.cs
Normal file
149
Assets/Scripts/Enemy/AI/MonsterClass.cs
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
using System.Collections;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
public abstract class MonsterClass : MonoBehaviour, IDamageable
|
||||||
|
{
|
||||||
|
[Header("--- 최적화 설정 ---")]
|
||||||
|
protected Renderer mobRenderer;
|
||||||
|
protected Transform playerTransform;
|
||||||
|
[SerializeField] protected float optimizationDistance = 40f;
|
||||||
|
|
||||||
|
[Header("스탯")]
|
||||||
|
[SerializeField] protected float maxHP = 100f;
|
||||||
|
protected float currentHP;
|
||||||
|
public event Action<float, float> OnHealthChanged;
|
||||||
|
|
||||||
|
[Header("피격 / 사망 / 대기 애니메이션")] // ⭐ Monster_Idle 추가
|
||||||
|
[SerializeField] protected string Monster_Idle = "Monster_Idle";
|
||||||
|
[SerializeField] protected string Monster_GetDamage = "Monster_GetDamage";
|
||||||
|
[SerializeField] protected string Monster_Die = "Monster_Die";
|
||||||
|
|
||||||
|
protected Animator animator;
|
||||||
|
protected NavMeshAgent agent;
|
||||||
|
protected AudioSource audioSource;
|
||||||
|
protected bool isHit, isDead, isAttacking;
|
||||||
|
|
||||||
|
[Header("AI 설정 (공격 후 휴식)")]
|
||||||
|
[SerializeField] protected float attackRestDuration = 1.5f;
|
||||||
|
protected bool isResting;
|
||||||
|
|
||||||
|
[Header("경험치")]
|
||||||
|
[SerializeField] protected int expReward = 10;
|
||||||
|
public static System.Action<int> OnMonsterKilled;
|
||||||
|
|
||||||
|
[Header("공통 사운드/이펙트")]
|
||||||
|
[SerializeField] protected AudioClip hitSound, deathSound;
|
||||||
|
[SerializeField] protected GameObject deathEffectPrefab;
|
||||||
|
[SerializeField] protected ParticleSystem hitEffect;
|
||||||
|
[SerializeField] protected Transform impactSpawnPoint;
|
||||||
|
|
||||||
|
protected virtual void Awake()
|
||||||
|
{
|
||||||
|
mobRenderer = GetComponentInChildren<Renderer>();
|
||||||
|
animator = GetComponent<Animator>();
|
||||||
|
agent = GetComponent<NavMeshAgent>();
|
||||||
|
audioSource = GetComponent<AudioSource>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnEnable()
|
||||||
|
{
|
||||||
|
isDead = false; currentHP = maxHP;
|
||||||
|
OnHealthChanged?.Invoke(currentHP, maxHP);
|
||||||
|
playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
|
||||||
|
if (mobRenderer != null) mobRenderer.enabled = true;
|
||||||
|
Init();
|
||||||
|
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnDisable()
|
||||||
|
{
|
||||||
|
if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Init() { }
|
||||||
|
protected abstract void ExecuteAILogic();
|
||||||
|
|
||||||
|
public void OnManagedUpdate()
|
||||||
|
{
|
||||||
|
if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return;
|
||||||
|
|
||||||
|
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
||||||
|
|
||||||
|
// 1. 범위 밖 최적화
|
||||||
|
if (distance > optimizationDistance)
|
||||||
|
{
|
||||||
|
StopMovement(); // ⭐ 여기서 Idle 애니메이션을 강제합니다.
|
||||||
|
if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true;
|
||||||
|
|
||||||
|
// 2. 시야 밖 최적화
|
||||||
|
if (mobRenderer != null && !mobRenderer.isVisible)
|
||||||
|
{
|
||||||
|
StopMovement();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agent != null && agent.isOnNavMesh && agent.isStopped)
|
||||||
|
{
|
||||||
|
agent.isStopped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecuteAILogic();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ [핵심 수정] 멈출 때 Idle 애니메이션을 즉시 실행합니다.
|
||||||
|
protected void StopMovement()
|
||||||
|
{
|
||||||
|
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
||||||
|
if (animator != null)
|
||||||
|
{
|
||||||
|
animator.SetFloat("Speed", 0f); // 블렌드 트리용
|
||||||
|
animator.Play(Monster_Idle); // ⭐ 상태 기반 애니메이션 강제 전환
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void TakeDamage(float amount) { OnDamaged(amount); }
|
||||||
|
public virtual void OnDamaged(float damage)
|
||||||
|
{
|
||||||
|
if (isDead) return;
|
||||||
|
currentHP -= damage;
|
||||||
|
OnHealthChanged?.Invoke(currentHP, maxHP);
|
||||||
|
if (currentHP <= 0) { Die(); return; }
|
||||||
|
if (!isHit) StartHit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void StartHit()
|
||||||
|
{
|
||||||
|
isHit = true; isAttacking = false; isResting = false;
|
||||||
|
StopAllCoroutines();
|
||||||
|
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
||||||
|
animator.Play(Monster_GetDamage, 0, 0f);
|
||||||
|
if (hitEffect) hitEffect.Play();
|
||||||
|
if (hitSound) audioSource.PlayOneShot(hitSound);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void OnAttackStart() { isAttacking = true; isResting = false; }
|
||||||
|
public virtual void OnAttackEnd() { isAttacking = false; if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); }
|
||||||
|
protected IEnumerator RestAfterAttack() { isResting = true; yield return new WaitForSeconds(attackRestDuration); isResting = false; }
|
||||||
|
public virtual void OnHitEnd() { isHit = false; if (agent && agent.isOnNavMesh) agent.isStopped = false; }
|
||||||
|
|
||||||
|
protected virtual void Die()
|
||||||
|
{
|
||||||
|
if (isDead) return;
|
||||||
|
isDead = true;
|
||||||
|
OnMonsterKilled?.Invoke(expReward);
|
||||||
|
Collider col = GetComponent<Collider>();
|
||||||
|
if (col != null) col.enabled = false;
|
||||||
|
if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
||||||
|
animator.Play(Monster_Die, 0, 0f);
|
||||||
|
if (deathSound) audioSource.PlayOneShot(deathSound);
|
||||||
|
Invoke("ReturnToPool", 1.5f);
|
||||||
|
}
|
||||||
|
public bool IsDead => isDead;
|
||||||
|
protected void ReturnToPool() => gameObject.SetActive(false);
|
||||||
|
}
|
||||||
98
Assets/Scripts/Enemy/AI/NormalMonster.cs
Normal file
98
Assets/Scripts/Enemy/AI/NormalMonster.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
|
||||||
|
public class NormalMonster : MonsterClass
|
||||||
|
{
|
||||||
|
[Header("전투")]
|
||||||
|
[SerializeField] private float damage = 10f; // ⭐ 이 변수는 OnAttackHit에서 사용됩니다.
|
||||||
|
[SerializeField] private float attackRange = 2f;
|
||||||
|
[SerializeField] private float attackDelay = 1.5f;
|
||||||
|
|
||||||
|
private float lastAttackTime;
|
||||||
|
|
||||||
|
[Header("공격 / 이동 애니메이션")]
|
||||||
|
[SerializeField] private string[] attackAnimations = { "Monster_Attack_1" };
|
||||||
|
[SerializeField] private string Monster_Walk = "Monster_Walk";
|
||||||
|
|
||||||
|
// ❌ [에러 해결] Monster_Idle 변수가 여기서 삭제되었습니다. (부모인 MonsterClass 것을 사용합니다)
|
||||||
|
|
||||||
|
[Header("AI 설정")]
|
||||||
|
[SerializeField] private float stopBuffer = 0.3f;
|
||||||
|
[SerializeField] private float patrolRadius = 5f;
|
||||||
|
[SerializeField] private float patrolInterval = 2f;
|
||||||
|
|
||||||
|
private float nextPatrolTime;
|
||||||
|
private float repathInterval = 0.3f;
|
||||||
|
private float nextRepathTime;
|
||||||
|
private int attackIndex;
|
||||||
|
private bool isPlayerInZone;
|
||||||
|
|
||||||
|
protected override void Init()
|
||||||
|
{
|
||||||
|
if (agent != null) agent.stoppingDistance = attackRange - 0.4f;
|
||||||
|
if (animator != null) animator.applyRootMotion = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteAILogic()
|
||||||
|
{
|
||||||
|
if (isHit || isAttacking || isResting) return;
|
||||||
|
|
||||||
|
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
||||||
|
if (isPlayerInZone || distance <= attackRange * 2f) HandlePlayerTarget();
|
||||||
|
else Patrol();
|
||||||
|
|
||||||
|
UpdateMovementAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandlePlayerTarget()
|
||||||
|
{
|
||||||
|
float distance = Vector3.Distance(transform.position, playerTransform.position);
|
||||||
|
if (distance <= attackRange - stopBuffer) TryAttack();
|
||||||
|
else if (Time.time >= nextRepathTime)
|
||||||
|
{
|
||||||
|
if (agent.isOnNavMesh) agent.SetDestination(playerTransform.position);
|
||||||
|
nextRepathTime = Time.time + repathInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TryAttack()
|
||||||
|
{
|
||||||
|
if (Time.time < lastAttackTime + attackDelay) return;
|
||||||
|
lastAttackTime = Time.time;
|
||||||
|
string attackName = attackAnimations[attackIndex];
|
||||||
|
attackIndex = (attackIndex + 1) % attackAnimations.Length;
|
||||||
|
OnAttackStart();
|
||||||
|
animator.Play(attackName, 0, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ [경고 해결] damage 변수를 여기서 사용하여 사용되지 않는다는 경고를 없앱니다.
|
||||||
|
public void OnAttackHit()
|
||||||
|
{
|
||||||
|
if (playerTransform == null || isHit || isDead) return;
|
||||||
|
if (playerTransform.TryGetComponent<PlayerHealth>(out var pHealth))
|
||||||
|
{
|
||||||
|
pHealth.TakeDamage(damage);
|
||||||
|
Debug.Log($"[전투] 플레이어에게 {damage} 데미지 부여!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateMovementAnimation()
|
||||||
|
{
|
||||||
|
if (isAttacking || isHit || isResting) return;
|
||||||
|
// ⭐ 부모 클래스의 Monster_Idle을 사용합니다.
|
||||||
|
if (agent.velocity.magnitude < 0.1f) animator.Play(Monster_Idle);
|
||||||
|
else animator.Play(Monster_Walk);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patrol()
|
||||||
|
{
|
||||||
|
if (Time.time < nextPatrolTime) return;
|
||||||
|
Vector3 randomPoint = transform.position + new Vector3(Random.Range(-patrolRadius, patrolRadius), 0, Random.Range(-patrolRadius, patrolRadius));
|
||||||
|
if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 3f, NavMesh.AllAreas))
|
||||||
|
if (agent.isOnNavMesh) agent.SetDestination(hit.position);
|
||||||
|
nextPatrolTime = Time.time + patrolInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; }
|
||||||
|
private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; }
|
||||||
|
}
|
||||||
8
Assets/Scripts/Enemy/Spawner.meta
Normal file
8
Assets/Scripts/Enemy/Spawner.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7ff8824a334600f499126a6aa584dcae
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
88
Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs
Normal file
88
Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class MonsterSpawner : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("--- 몹 스폰 설정 ---")]
|
||||||
|
[SerializeField] private string mobTag = "NormalMob"; // 풀에 등록된 태그명
|
||||||
|
[SerializeField] private float spawnRange = 15f; // 플레이어 감지 범위
|
||||||
|
[SerializeField] private float respawnCooldown = 3f; // 몹 사망 시 재소환 대기 시간
|
||||||
|
|
||||||
|
private GameObject _myMonster;
|
||||||
|
private MonsterClass _monsterScript;
|
||||||
|
private Transform _player;
|
||||||
|
private float _nextSpawnTime;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
FindPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// ⭐ [안전장치] 플레이어를 못 찾았다면 계속 다시 찾습니다.
|
||||||
|
if (_player == null)
|
||||||
|
{
|
||||||
|
FindPlayer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dist = Vector3.Distance(transform.position, _player.position);
|
||||||
|
|
||||||
|
// 1. 플레이어가 범위 안에 들어왔을 때
|
||||||
|
if (dist <= spawnRange)
|
||||||
|
{
|
||||||
|
// 쿨타임이 지났고, 현재 관리하는 몹이 없을 때만 소환
|
||||||
|
if (Time.time >= _nextSpawnTime && (_myMonster == null || !_myMonster.activeSelf))
|
||||||
|
{
|
||||||
|
SpawnMonster();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. 플레이어가 범위 밖으로 나갔을 때 (회색 점 상태로 회수)
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DespawnMonster();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FindPlayer()
|
||||||
|
{
|
||||||
|
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||||||
|
if (playerObj != null) _player = playerObj.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnMonster()
|
||||||
|
{
|
||||||
|
// 제네럴 오브젝트 풀에서 몹을 빌려옵니다.
|
||||||
|
_myMonster = GenericObjectPool.Instance.SpawnFromPool(mobTag, transform.position, transform.rotation);
|
||||||
|
|
||||||
|
if (_myMonster != null)
|
||||||
|
{
|
||||||
|
_monsterScript = _myMonster.GetComponent<MonsterClass>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DespawnMonster()
|
||||||
|
{
|
||||||
|
if (_myMonster != null && _myMonster.activeSelf)
|
||||||
|
{
|
||||||
|
// 몹이 살아있는 상태에서 멀어진 거라면 즉시 회수
|
||||||
|
if (_monsterScript != null && !_monsterScript.IsDead)
|
||||||
|
{
|
||||||
|
_myMonster.SetActive(false);
|
||||||
|
_nextSpawnTime = Time.time; // 멀어진 건 즉시 재생성 대기
|
||||||
|
}
|
||||||
|
// 몹이 죽어서 사라진 거라면 지정된 쿨타임 적용
|
||||||
|
else if (_monsterScript != null && _monsterScript.IsDead)
|
||||||
|
{
|
||||||
|
_nextSpawnTime = Time.time + respawnCooldown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에디터에서 스폰 범위를 시각적으로 확인
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
Gizmos.color = Color.red;
|
||||||
|
Gizmos.DrawWireSphere(transform.position, spawnRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs.meta
Normal file
11
Assets/Scripts/Enemy/Spawner/Monster Spwaner.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1f525fb5022b0754b9c5e1d725f8b2a4
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
24
Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs
Normal file
24
Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class MobUpdateManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static MobUpdateManager Instance;
|
||||||
|
// ⭐ Monster -> MonsterClass로 수정하여 에러 해결
|
||||||
|
private List<MonsterClass> _activeMobs = new List<MonsterClass>();
|
||||||
|
|
||||||
|
private void Awake() => Instance = this;
|
||||||
|
|
||||||
|
// ⭐ 매개변수 타입도 MonsterClass로 변경
|
||||||
|
public void RegisterMob(MonsterClass mob) => _activeMobs.Add(mob);
|
||||||
|
public void UnregisterMob(MonsterClass mob) => _activeMobs.Remove(mob);
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _activeMobs.Count; i++)
|
||||||
|
{
|
||||||
|
// 리스트를 돌며 카메라 최적화 로직이 담긴 OnManagedUpdate 호출
|
||||||
|
if (_activeMobs[i] != null) _activeMobs[i].OnManagedUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs.meta
Normal file
11
Assets/Scripts/Enemy/Spawner/MonsterUpdateManager.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6977fe111b393d145b94a9fd1e4d3ff8
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Enemy/Types.meta
Normal file
8
Assets/Scripts/Enemy/Types.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c62ed328702245247a760bdf10aa6f36
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Items.meta
Normal file
8
Assets/Scripts/Items.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 43bf07e5a9903e647a1019124b071d87
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Items/Interactive.meta
Normal file
8
Assets/Scripts/Items/Interactive.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d089492a21a37714cb98344a6292d115
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Items/Pickups.meta
Normal file
8
Assets/Scripts/Items/Pickups.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ebaf62603caca7e4cb4b98fa851e2946
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Items/VFX.meta
Normal file
8
Assets/Scripts/Items/VFX.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 86e4607e6e1c8724f99b714c13b5b5d4
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player.meta
Normal file
8
Assets/Scripts/Player.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 753b48fff82786549b23d2faa8a4b205
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Animation.meta
Normal file
8
Assets/Scripts/Player/Animation.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a76aab853c365ce4da5c628a36bcaec3
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Combat.meta
Normal file
8
Assets/Scripts/Player/Combat.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4b5716a4b83397b4aaaf8e2201b06467
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Controller.meta
Normal file
8
Assets/Scripts/Player/Controller.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bc2ffea8d1799264cbb5ee50e33be21c
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Equipment.meta
Normal file
8
Assets/Scripts/Player/Equipment.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 41258c4b7bbf53a4284384d254320057
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Interaction.meta
Normal file
8
Assets/Scripts/Player/Interaction.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ae6b5fdaf1154e343a2652abd38cfc56
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Interaction/Heal.meta
Normal file
8
Assets/Scripts/Player/Interaction/Heal.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3b3b7e2e5973bd546b9035696403a6fb
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
54
Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs
Normal file
54
Assets/Scripts/Player/Interaction/Heal/HealthAltar.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
public class HealthAltar : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("--- 회복 설정 ---")]
|
||||||
|
[SerializeField] private float healAmount = 50f; // 상호작용 시 즉시 회복량
|
||||||
|
[SerializeField] private float interactRange = 3.5f; // 상호작용 가능 거리
|
||||||
|
|
||||||
|
[Header("--- 시각 효과 ---")]
|
||||||
|
[SerializeField] private ParticleSystem healEffect;
|
||||||
|
|
||||||
|
// ⭐ [핵심 수정] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다!
|
||||||
|
//
|
||||||
|
public void Use(PlayerHealth playerHealth)
|
||||||
|
{
|
||||||
|
if (playerHealth == null) return;
|
||||||
|
|
||||||
|
// 플레이어의 Transform 정보 추출
|
||||||
|
Transform interactor = playerHealth.transform;
|
||||||
|
|
||||||
|
// 규칙: 실제 몸통 중심점($Center$)과의 거리 계산
|
||||||
|
float distance = Vector3.Distance(transform.position, interactor.position);
|
||||||
|
|
||||||
|
if (distance <= interactRange)
|
||||||
|
{
|
||||||
|
ApplyHeal(playerHealth); // 컴포넌트 자체를 넘겨줌
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyHeal(PlayerHealth targetHealth)
|
||||||
|
{
|
||||||
|
// ⭐ 캡슐화 규칙: 플레이어의 Health 스크립트에 있는 기능을 호출합니다.
|
||||||
|
// 유저님의 프로젝트에 정의된 회복 함수 이름을 사용하세요 (예: RestoreHealth, AddHealth 등)
|
||||||
|
// targetHealth.RestoreHealth(healAmount); //
|
||||||
|
|
||||||
|
if (healEffect != null)
|
||||||
|
{
|
||||||
|
healEffect.transform.position = targetHealth.transform.position;
|
||||||
|
healEffect.Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
Gizmos.color = Color.cyan;
|
||||||
|
Gizmos.DrawWireSphere(transform.position, interactRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: de0cc0f35b02e054faf0780c75d0a720
|
guid: f200c95f2cb77e24dbd8cddfd1baced0
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
8
Assets/Scripts/Player/Stats.meta
Normal file
8
Assets/Scripts/Player/Stats.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 44162ebf14825f1488263d51ac19986e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Upgrade.meta
Normal file
8
Assets/Scripts/Player/Upgrade.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dc95c79bf38e1894880e267619a5ae40
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/Scripts/Player/Upgrade/Data.meta
Normal file
8
Assets/Scripts/Player/Upgrade/Data.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8a4f70f1e4e05dd429ade0041e15c91f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user