유니티 RPG - 45. 보스몬스터 구현
Intro
이번포스팅에서는 던전의 최종 보스몬스터를 구현해보았습니다.
보스몬스터 에셋은
를 사용하였습니다.
보스몬스터 구조
처음에는 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
가 필요합니다.
Grunt 프리팹을 만들고, 이것을 Dungeon 씬에서 사용하려합니다.
DungeonManager
던전매니저 클래스에서
모든 몬스터를 처치했을 때 보스몬스터가 등장하고, 컷씬을 플레이하는 부분에
보스몬스터를 미리 지정해둔 스폰위치에 생성하는 부분을 추가하였습니다.
// 보스몬스터 스폰
private void SpawnBoss()
{
bossSpawned = true;
Instantiate(bossMonsterPrefab, bossSpawnPoint);
// 보스몬스터 정보 가져오기
boss = GetComponentInChildren<BossMonster>();
// 컷씬 출력
cutSceneObj.gameObject.SetActive(false);
pd.Play(timelineAsset[0]);
Debug.Log("모든 몬스터 처리 완료");
}
댓글남기기