3 분 소요

Intro

이번포스팅에서는 던전의 최종 보스몬스터를 구현해보았습니다.

보스몬스터 에셋은

image

를 사용하였습니다.

보스몬스터 구조

처음에는 Monster 클래스를 재사용할까 생각해봤지만

보스몬스터는 상태패턴을 사용하지 않을것이고, 일반 몬스터에게만 필요한 데이터들이 많이있는것같아

BossMonster 클래스를 따로 빼주기로 했습니다.

  • BossMonster
    • Grunt : 보스몬스터 (Grunt) 클래스

BossMonster 클래스

BossMonster 클래스는 보스몬스터가 공통적으로 가지는 데이터 및 함수를 정의합니다.

using UnityEngine;
using UnityEngine.AI;

/*
                    BossMonster - 보스몬스터의 공통 데이터를 관리

        - Boss Monster 컴포넌트 정보 가져오기

        - 공통함수
            - GetDamaged : 받은 데미지만큼 Hp 감소
 */

public class BossMonster : MonoBehaviour
{
    [SerializeField] private Transform damageTextPos; // 데미지 텍스트 표시 위치

    #region ** Monster Status **
    [Header("#Boss Monster Stats")]
    public float maxHp;
    public float curHp;
    public float speed;
    public float damage;                            // 공격력
    public float attackRange;                       // 공격가능한 범위
    #endregion

    #region ** Private Fields **
    public PlayerController targetPlayer;             // 타깃 플레이어
    protected BoxCollider hitBoxCol;                  // 몬스터 히트박스
    protected Animator anim;                          // 몬스터 애니메이터
    protected NavMeshAgent nav;                       // 몬스터 네비게이션
    #endregion

    #region ** Flags **
    [HideInInspector]
    protected bool isAttackReady;                      // 공격 가능 여부
    [HideInInspector]
    protected bool isDead;                             // 죽었는지 여부
    [HideInInspector]
    protected bool isAttacking = false;                // 공격중인지 여부
    #endregion

    #region ** Properties **
    public NavMeshAgent Nav => nav;
    #endregion

    protected virtual void Awake()
    {
        Init();
    }

    private void Init()
    {
        anim = GetComponent<Animator>();
        nav = GetComponent<NavMeshAgent>();
        hitBoxCol = GetComponent<BoxCollider>();
    }

    // 공격받음
    public void GetDamaged(float damage)
    {
        float minDamage = damage * 0.8f;
        float maxDamage = damage * 1.2f;
        int randomDamage = (int)Random.Range(minDamage, maxDamage);
        DamageTextManager.Instance.ShowDamage(damageTextPos.position, randomDamage);
        curHp -= randomDamage;
    }
}
  • 보스몬스터가 가지는 공통데이터 및 함수 정의


Grunt

Grunt 는 BossMonster 클래스를 상속받아 Grunt 의 데이터를 초기화하고, Grunt 만 가지는 특징들을 구현해주어야합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Grunt : BossMonster
{
    readonly private int hashAttackTrigger = Animator.StringToHash("Attack");
    readonly private int hashAttackType = Animator.StringToHash("AttackType");
    readonly private int hashDeadTrigger = Animator.StringToHash("Dead");
    readonly private int hashSpeed = Animator.StringToHash("Speed");

    protected override void Awake()
    {
        // 부모(Monster)의 초기화
        base.Awake();
        InitData();
    }

    private void Update()
    {
        if (targetPlayer == null || isDead)
            return;

        Move();
        Attack();
        Die();
    }

    // Grunt 데이터 초기화
    private void InitData()
    {
        maxHp = 15;
        curHp = maxHp;
        speed = 1.5f;
        damage = 15f;
        attackRange = 3f;

        nav.speed = speed;
    }

    // 이동
    private void Move()
    {
        if (isAttacking)
            return;

        nav.SetDestination(targetPlayer.transform.position);
        anim.SetFloat(hashSpeed, speed);
    }

    // 공격
    private void Attack()
    {
        float distanceToPlayer = Vector3.Distance(transform.position, targetPlayer.transform.position);
        Vector3 direction = (targetPlayer.transform.position - transform.position).normalized;

        // 공격범위안에 들어왔을 때
        if (!isAttacking && distanceToPlayer <= attackRange)
        {
            transform.LookAt(targetPlayer.transform);
            StartCoroutine(DecideNextAttack());
        }
    }

    // 죽음
    private void Die()
    {
        if(!isDead && curHp <= 0)
        {
            isDead = true;
            anim.SetTrigger(hashDeadTrigger);
            hitBoxCol.enabled = false;
            nav.isStopped = true;
        }
    }

    // 다음 공격 정하기
    private IEnumerator DecideNextAttack()
    {
        nav.isStopped = true;
        isAttacking = true;
        anim.SetFloat(hashSpeed, 0);

        int randomAct = Random.Range(0, 2);

        // 다음 공격을 랜덤하게 결정
        switch(randomAct)
        {
            case 0:
                anim.SetInteger(hashAttackType, randomAct);
                anim.SetTrigger(hashAttackTrigger);
                break;
            case 1:
                anim.SetInteger(hashAttackType, randomAct);
                anim.SetTrigger(hashAttackTrigger);
                break;
        }

        yield return null;
    }

    // 근접 공격 판정(애니메이션 이벤트)
    private void MeleeAttack()
    {
        // Raycast할 위치, 방향
        Vector3 origin = transform.position + new Vector3(0, 1f, 0);
        Vector3 direction = transform.forward;

        RaycastHit hit;

        if(Physics.SphereCast(origin, 1f, direction, out hit, 2f, LayerMask.GetMask("Player")))
        {
            if(hit.collider.CompareTag("Player"))
            {
                // 플레이어 데미지
                PlayerData playerData = DataManager.Instance.GetPlayerData();
                playerData.GetDamaged(Random.Range(damage * 0.8f, damage * 1.2f));
            }
        }
    }

    // 공격 끝(애니메이션 이벤트)
    private void EndAttack()
    {
        isAttacking = !isAttacking;
        nav.isStopped = false;
    }
}
  • 보스몬스터는 플레이어와의 거리와 상관없이 무조건 쫓아가도록 하였습니다.

  • 플레이어가 공격범위에 들어서면 공격을 시도하며 2가지 기본 공격 모션이 있습니다.
    • 보스 몬스터의 스킬공격은 아직 미구현
  • 애니메이션 이벤트를 통해 공격판정을 구현하였습니다.


Grunt 오브젝트에는 일반 몬스터와같이

  • 플레이어 탐지범위를 위한 스크립트 : GruntRange
  • 데미지 텍스트를 표시할 위치 : Damage Text Pos

가 필요합니다.

image

Grunt 프리팹을 만들고, 이것을 Dungeon 씬에서 사용하려합니다.

DungeonManager

던전매니저 클래스에서

모든 몬스터를 처치했을 때 보스몬스터가 등장하고, 컷씬을 플레이하는 부분에

보스몬스터를 미리 지정해둔 스폰위치에 생성하는 부분을 추가하였습니다.

// 보스몬스터 스폰
    private void SpawnBoss()
    {
        bossSpawned = true;

        Instantiate(bossMonsterPrefab, bossSpawnPoint);

        // 보스몬스터 정보 가져오기
        boss = GetComponentInChildren<BossMonster>();
        
        // 컷씬 출력
        cutSceneObj.gameObject.SetActive(false);
        pd.Play(timelineAsset[0]);

        Debug.Log("모든 몬스터 처리 완료");
    }

플레이 영상

image

댓글남기기