프로젝트/프로젝트 A

# 021 Golem (2) (with. PlayerHpBar)

효따 2025. 8. 26. 23:52

안녕하세요.🙂

 

오늘은 어제에 이어서 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;
    }
}

 


 

 

 

 

음.. 일단 원하는 결과물들이 만들어지고 있긴 한데요

 

분위기나 연출 효과 같은 부분들이 매우 미흡한 것 같습니다.

 

내일은

 

각 오브젝트 배치 및 맵 디자인을 더불어

 

디테일을 조금 더 살려보도록 하겠습니다.

 


 

오늘도

 

감사합니다.😌

 

좋은 하루 보내세요.

'프로젝트 > 프로젝트 A' 카테고리의 다른 글

# 023 Object Icon (with. Minimap Drag & Click)  (2) 2025.08.29
# 022 Map Design  (4) 2025.08.28
# 020 Golem (1) (with. State)  (2) 2025.08.25
#019 Enemy (3) (whit. Combat)  (6) 2025.08.24
# 018 Login (with. Firebase)  (2) 2025.08.23