안녕하세요.🙂
오늘은 어제에 이어서 Golem에 관련된 마무리 작업을 하려고 합니다.
바로 이어서 코드를 보면서 일지를 작성해 보겠습니다.
기능이 추가된 "GolemAI"입니다.
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
public class GolemAI : MonoBehaviour
{
[SerializeField] private PlayerMovement playerMovement;
[SerializeField] private PlayerAttack playerAttack;
[SerializeField] private GolemStats golemStats;
[SerializeField] private GolemHpBar golemHpBar;
[Header("전투 관련")]
[SerializeField] private float attackCD = 3f; // 공격 쿨다운
[SerializeField] private float attackRange = 1f; // 공격 범위
[SerializeField] private float aggroRange = 4f; // 어그로 범위
[SerializeField] private float maxRange = 10f; // 최대 활동 반경
[Header("참조")]
[SerializeField] private GameObject player;
[SerializeField] private NavMeshAgent agent;
[SerializeField] private Animator animator;
[SerializeField] private Collider goleCollider;
[Header("스폰 정보")]
[SerializeField] private Vector3 spawnPosition;
[SerializeField] private Quaternion spawnRotation;
float timePassed;
float newDestinationCD = 0.5f;
enum GolemState { Idle, Chase, Attack, Return, Die }
[SerializeField] private GolemState currentState = GolemState.Idle;
void Start()
{
spawnPosition = transform.position;
spawnRotation = transform.rotation;
}
void Update()
{
if (currentState == GolemState.Die) return;
float distanceToPlayer = Vector3.Distance(player.transform.position, transform.position);
float distanceToSpawn = Vector3.Distance(transform.position, spawnPosition);
if (distanceToSpawn > maxRange)
{
currentState = GolemState.Return;
aggroRange = 0f;
}
else if (distanceToPlayer <= attackRange)
{
currentState = GolemState.Attack;
}
else if (distanceToPlayer <= aggroRange)
{
currentState = GolemState.Chase;
}
else if (distanceToSpawn > 0.1f)
{
currentState = GolemState.Return;
}
else
{
currentState = GolemState.Idle;
aggroRange = 5.5f;
}
animator.SetFloat("Speed", agent.velocity.magnitude / agent.speed);
// --- 상태별 행동 ---
switch (currentState)
{
case GolemState.Attack:
if (timePassed >= attackCD)
{
animator.SetTrigger("IsAttack");
timePassed = 0f;
}
agent.SetDestination(transform.position);
transform.LookAt(player.transform);
break;
case GolemState.Chase:
if (newDestinationCD <= 0)
{
newDestinationCD = 0.5f;
agent.SetDestination(player.transform.position);
}
transform.LookAt(player.transform);
break;
case GolemState.Return:
if (newDestinationCD <= 0)
{
newDestinationCD = 0.5f;
agent.SetDestination(spawnPosition);
}
transform.LookAt(spawnPosition);
break;
case GolemState.Idle:
agent.SetDestination(transform.position);
transform.rotation = Quaternion.Slerp(
transform.rotation,
spawnRotation,
Time.deltaTime * 2f
);
if (golemStats.golemHp < golemStats.golemMaxHp)
{
golemStats.golemHp = Mathf.Min(
golemStats.golemHp + 1f * Time.deltaTime,
golemStats.golemMaxHp
);
golemHpBar.HpVarUpdate(golemStats.golemHp, golemStats.golemMaxHp);
}
break;
}
timePassed += Time.deltaTime;
newDestinationCD -= Time.deltaTime;
}
public void TakeDamage(float damage)
{
golemStats.golemHp -= damage;
golemHpBar.HpVarUpdate(golemStats.golemHp, golemStats.golemMaxHp);
if (golemStats.golemHp <= 0)
{
goleCollider.enabled = false;
agent.enabled = false;
playerAttack.enumyGameobject = null;
playerMovement.enumyGameobject = null;
animator.SetTrigger("IsDie");
currentState = GolemState.Die;
StartCoroutine(GolemDieProcess());
}
}
private IEnumerator GolemDieProcess()
{
yield return new WaitForSeconds(5f);
Destroy(gameObject);
}
private void OnDrawGizmos()
{
// 공격 범위 (빨강)
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);
// 어그로 범위 (노랑)
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, aggroRange);
// 최대 활동 범위 (초록)
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(spawnPosition, maxRange);
}
}
변수 선언부입니다.
[SerializeField] private PlayerMovement playerMovement;
[SerializeField] private PlayerAttack playerAttack;
[SerializeField] private GolemStats golemStats;
[SerializeField] private GolemHpBar golemHpBar;
[Header("전투 관련")]
[SerializeField] private float attackCD = 3f; // 공격 쿨다운
[SerializeField] private float attackRange = 1f; // 공격 범위
[SerializeField] private float aggroRange = 4f; // 어그로 범위
[SerializeField] private float maxRange = 10f; // 최대 활동 반경
[Header("참조")]
[SerializeField] private GameObject player;
[SerializeField] private NavMeshAgent agent;
[SerializeField] private Animator animator;
[SerializeField] private Collider goleCollider;
[Header("스폰 정보")]
[SerializeField] private Vector3 spawnPosition;
[SerializeField] private Quaternion spawnRotation;
float timePassed;
float newDestinationCD = 0.5f;
enum GolemState { Idle, Chase, Attack, Return, Die }
[SerializeField] private GolemState currentState = GolemState.Idle;
1. 컴포넌트 의존성
[SerializeField] private PlayerMovement playerMovement;
[SerializeField] private PlayerAttack playerAttack;
[SerializeField] private GolemStats golemStats;
[SerializeField] private GolemHpBar golemHpBar;
- 의존성 주입(Inspector 세팅): 다른 스크립트/컴포넌트를 직렬화하여 인스펙터에서 연결합니다.
- PlayerMovement, PlayerAttack: 골렘이 죽을 때 플레이어 쪽 타깃 참조를 끊어 게임오브젝트 정리를 깔끔히 하려는 의도
- GolemStats: 체력/최대체력 등 수치 관리 전용
- GolemHpBar: 체력 변화 시 UI를 즉시 갱신
2. 전투 관련 파라미터
[SerializeField] private float attackCD = 3f; // 공격 쿨다운
[SerializeField] private float attackRange = 1f; // 공격 범위
[SerializeField] private float aggroRange = 4f; // 어그로 범위
[SerializeField] private float maxRange = 10f; // 최대 활동 반경
- 디자인 파라미터를 코드 수정 없이 밸런싱 가능하도록 직렬화
- maxRange: 골렘이 스폰 지점에서 너무 멀어지지 않도록 리쉬(Leash) 보정 (골렘의 최대 이동 범위를 제한하기 위함)
3. 런타임 참조
[SerializeField] private GameObject player;
[SerializeField] private NavMeshAgent agent;
[SerializeField] private Animator animator;
[SerializeField] private Collider goleCollider;
- NavMeshAgent: 길 찾기/추적/귀환을 담당
- Animator: 이동 속도 파라미터, 트리거로 공격/히트/사망 애니메이션 제어
- Collider: 사망 시 즉시 비활성화해 추가 히트/피격 판정 차단
- golemCollider: 골렘 몬스터의 Box Collider;
4. 골렘 초기 위치 정보
[SerializeField] private Vector3 spawnPosition;
[SerializeField] private Quaternion spawnRotation;
- Start()에서 캐싱: 귀환 위치/대기 자세의 기준
- 귀환(Return) 완료 후 Idle에서 부드럽게 원래 방향으로 복귀(Slerp)
5. 내부 타이머
float timePassed;
float newDestinationCD = 0.5f;
- timePassed: 공격 애니를 일정 간격으로만 트리거
- newDestinationCD: 매 프레임 SetDestination 호출을 피해서 성능과 떨림 완화
6. 상태 머신 정의
enum GolemState { Idle, Chase, Attack, Return, Die }
[SerializeField] private GolemState currentState = GolemState.Idle;
- 상태 기반 AI (대기, 추적, 공격, 리턴, 죽음)
7. Start() 초기화
void Start()
{
spawnPosition = transform.position;
spawnRotation = transform.rotation;
}
- 현재 위치/회전을 스폰 기준으로 저장. 게임 시작 시점(Start())에 정확한 참조를 얻습니다.
8. Update() 상태 전환
void Update()
{
if (currentState == GolemState.Die) return;
float distanceToPlayer = Vector3.Distance(player.transform.position, transform.position);
float distanceToSpawn = Vector3.Distance(transform.position, spawnPosition);
if (distanceToSpawn > maxRange)
{
currentState = GolemState.Return;
aggroRange = 0f;
}
else if (distanceToPlayer <= attackRange)
{
currentState = GolemState.Attack;
}
else if (distanceToPlayer <= aggroRange)
{
currentState = GolemState.Chase;
}
else if (distanceToSpawn > 0.1f)
{
currentState = GolemState.Return;
}
else
{
currentState = GolemState.Idle;
aggroRange = 5.5f;
}
- 우선순위: 리쉬 초과 → 공격 → 추적 → 귀환 → 대기.
- aggroRange를 Return 중에는 0으로 잠깐 낮춤: 귀환 도중에 다시 어그로를 새로 태우지 않게 하려는 장치 (몬스터의 aggroRange범위 안에 플레이어가 있으면서 최대 범위 밖으로 벗어나려 하면 반복 왔다 갔다 하는 현상을 없애기 위함)
- Idle 진입 시 aggroRange 복구(5.5f): 정상 탐지 범위로 되돌림 (즉 retrun 중엔 대미지는 입지만, 플레이어를 향해 다시 추적하는 로직은 하지 않음)
9. 애니메이터 동기화
animator.SetFloat("Speed", agent.velocity.magnitude / agent.speed);
- 실제 이동 속도를 정규화해 이동/대기 애니 블렌딩을 자연스럽게
10. 상태별 행동 Attack
case GolemState.Attack:
if (timePassed >= attackCD)
{
animator.SetTrigger("IsAttack");
timePassed = 0f;
}
agent.SetDestination(transform.position);
transform.LookAt(player.transform);
break;
- 쿨다운 기반 공격. 에이전트를 멈춰 공격 애니메이션이 안정적으로 재생됩니다.
11. 상태별 행동 Chase
case GolemState.Chase:
if (newDestinationCD <= 0)
{
newDestinationCD = 0.5f;
agent.SetDestination(player.transform.position);
}
transform.LookAt(player.transform);
break;
- 목적지 갱신을 0.5초마다 제한해 경로 재계산 비용 감소 및 회전 떨림 완화.
- 타깃은 Player
12. 상태별 행동 Return
case GolemState.Return:
if (newDestinationCD <= 0)
{
newDestinationCD = 0.5f;
agent.SetDestination(spawnPosition);
}
transform.LookAt(spawnPosition);
break;
- 스폰 지점으로 복귀. 동일하게 스로틀링
- 타깃은 spawnPosition
13. 상태별 행동 Idle
case GolemState.Idle:
agent.SetDestination(transform.position);
transform.rotation = Quaternion.Slerp(
transform.rotation,
spawnRotation,
Time.deltaTime * 2f
);
if (golemStats.golemHp < golemStats.golemMaxHp)
{
golemStats.golemHp = Mathf.Min(
golemStats.golemHp + 1f * Time.deltaTime,
golemStats.golemMaxHp
);
golemHpBar.HpVarUpdate(golemStats.golemHp, golemStats.golemMaxHp);
}
break;
- 부드러운 원위치 바라보기: Slerp로 초기 회전까지 자연스럽게 복귀
- 자연 회복(Idle 힐): 초당 1씩 회복, UI 즉시 갱신
15. 타이머 갱신
timePassed += Time.deltaTime;
newDestinationCD -= Time.deltaTime;
공격 쿨다운 증가, 목적지 갱신 쿨타임 감소
16. 피격 및 사망 처리
public void TakeDamage(float damage)
{
golemStats.golemHp -= damage;
golemHpBar.HpVarUpdate(golemStats.golemHp, golemStats.golemMaxHp);
if (golemStats.golemHp <= 0)
{
goleCollider.enabled = false;
agent.enabled = false;
playerAttack.enumyGameobject = null;
playerMovement.enumyGameobject = null;
animator.SetTrigger("IsDie");
currentState = GolemState.Die;
StartCoroutine(GolemDieProcess());
}
}
- 사망 직후 상호작용 차단(콜라이더/에이전트 비활), 플레이어 쪽 참조도 해제 (골렘이 쓰러진 후 플레이어의 공격을 멈추기 위함)
- Die 상태로 전환해 Update를 막고, 코루틴으로 지연 제거
17. 사망 코루틴
private IEnumerator GolemDieProcess()
{
yield return new WaitForSeconds(5f);
Destroy(gameObject);
}
18. 디버그 시각화
private void OnDrawGizmos()
{
// 공격 범위 (빨강)
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, attackRange);
// 어그로 범위 (노랑)
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, aggroRange);
// 최대 활동 범위 (초록)
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(spawnPosition, maxRange);
}
- 전투/인식/리쉬 반경을 에디터 Scene에서 즉시 확인 가능
- 참고: 편집 모드에서도 정확히 보이려면 spawnPosition 초기화 타이밍을 OnValidate에서 보정하는 것도 방법
- 단순 디버깅용
다음은 GolemAttack 스크립트입니다.
using UnityEngine;
public class GolemAttack : MonoBehaviour
{
[SerializeField] private PlayerStats playerStats;
[SerializeField] private GolemStats golemStats;
void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
playerStats.TakeDamage(golemStats.golemDamage);
}
}
}
- 골렘의 공격 판정을 담당하는 스크립트
- playerStats: 플레이어의 체력 및 방어 관련 로직이 담긴 스크립트 참조
- golemStats: 골렘의 공격력(golemDamage) 수치 가져오기
- OnTriggerEnter(Collider other): Unity 물리 이벤트. 충돌체가 "Player" 태그를 가진 경우 플레이어 체력 감소
→ 골렘이 여러 대상과 부딪혀도 플레이어만 대미지를 입음
콜라이더를 켜고 끄는 곳은 애니메이션의 Events를 활용
다음은 GolemStats 스크립트입니다.
using UnityEngine;
public class GolemStats : MonoBehaviour
{
public float golemMaxHp = 300f;
public float golemHp = 0f;
public float golemDamage = 10f;
public Collider golemAttackCollider;
void Start()
{
golemHp = golemMaxHp;
}
public void AttackColliderOn()
{
golemAttackCollider.enabled = true;
}
public void AttackColliderOff()
{
golemAttackCollider.enabled = false;
}
}
- golemMaxHp: 최대 체력
- golemHp: 현재 체력. Start()에서 시작 시 최대 체력으로 세팅
- golemDamage: 공격력. 공격 시 이 값을 플레이어에게 전달
- golemAttackCollider: 골렘의 무기 Collider. 평소에는 꺼두고 공격 모션 중 특정 타이밍에만 켜짐
- AttackColliderOn() / AttackColliderOff(): 애니메이션 이벤트(Animation Event)로 호출 → 공격 모션 도중에만 판정 활성화
골렘의 Hp의 UI를 담당하는 GeolemHpBar 스크립트입니다.
using UnityEngine;
using UnityEngine.UI;
public class GolemHpBar : MonoBehaviour
{
[SerializeField] private Slider sliderHpBar;
public void HpVarUpdate(float hp, float maxHp)
{
sliderHpBar.value = hp / maxHp;
}
}
- sliderHpBar: Unity UI의 Slider 컴포넌트 참조.
- HpVarUpdate(float hp, float maxHp): 외부에서 체력 변화를 넘겨주면 비율 계산 후 Slider에 적용.
→ 체력이 깎일 때마다 UI가 자동으로 반영됨.
마지막으로 플레이어의 피격 관련은 지난번 작성한 PlayerStats 스크립트에서
피격 시 호출되게 해 주었습니다.
또한 플레이어의 Hp 관련 UI 스크립트도 새로 작성해서 관리를 해주었습니다.
using UnityEngine;
public class PlayerStats : MonoBehaviour
{
[SerializeField] private PlayerHpBar playerHpBar;
[Header("Level")]
public int playerLevel = 1;
[Header("Hp")]
public float playerMaxHp = 100f;
private float playerHp = 100f;
[Header("Mp")]
public float playerMaxMp = 100f;
public float playerMp = 100f;
public float playerManaRegenRate = 5.0f;
public float playerMpQConsumption = 35f;
public float playerMpWConsumption = 20f;
public float playerMpEConsumption = 20f;
public float playerMpRConsumption = 50f;
[Header("Exp")]
public float playerMaxExp = 100;
public float playerExp = 0;
[Header("Point")]
public int playerStatPoint = 0;
[Header("Attack")]
public float playerDamage = 10f;
public float playerSkillQDamage = 2f;
public float playerSkillWDamage = 0f;
public float playerSkillEDamage = 0f;
public float playerSkillRDamage = 4f;
[Header("Setting Value")]
public float playerAttackSpeed = 3f;
[Header("Gold")]
public float playerGold = 0f;
public void TakeDamage(float damage)
{
playerHp -= damage;
playerHpBar.HpVarUpdate(playerHp, playerMaxHp);
if (playerHp <= 0)
{
Destroy(this.gameObject);
}
}
}
using UnityEngine;
using UnityEngine.UI;
public class PlayerHpBar : MonoBehaviour
{
[SerializeField] private Slider playerHpBar;
[SerializeField] private Slider playerBottomHpBar;
public void HpVarUpdate(float hp, float maxHp)
{
playerHpBar.value = hp / maxHp;
playerBottomHpBar.value = hp / maxHp;
}
}
음.. 일단 원하는 결과물들이 만들어지고 있긴 한데요
분위기나 연출 효과 같은 부분들이 매우 미흡한 것 같습니다.
내일은
각 오브젝트 배치 및 맵 디자인을 더불어
디테일을 조금 더 살려보도록 하겠습니다.
오늘도
감사합니다.😌
좋은 하루 보내세요.