미로스 - 개인 진행 결과물(디테일 등) - 1.독자적 보스, 패턴 구현맵의 변화
- 100 powerun
- 2024년 11월 27일
- 7분 분량
최종 수정일: 2월 13일
우선 보스는 총 3개의 패턴을 가지고 있고 보스의 공격은 각각 다른 동작을 띄기에 그만큼 콜라이더 또한 다양하게 적용되어 있습니다.

1번 패턴 타격점
1번 패턴의 특징은 가장 단순한 형태의 콜라이더를 가지고 있습니다.

2번 패턴 첫 번째 타격점

2번 패턴 두 번째 타격점
2번 패턴의 경우는 애니메이션이 공격을 우에서 좌로 크게 휘둘러 보스 바로 아래까지 끄시는 동작을 취합니다. 하나의 콜라이더로 관리하기에는 이미 휘두르지 않은 부분에서 피격 판정이 나는 부자연스러움이 발생하기에 2단계로 발동되도록 콜라이더를 이중으로 적용했습니다.

3번 패턴 타격점
3번 패턴은 여러 개의 콜라이더가 있는데, 애니메이션에서도 보이듯 내리치는 동작이 1번에 비해 매우 큰 동작으로 이걸 하나의 콜라이더로 관리하기에는 이상한 부분에서 피격 점이 생기거나, 맞아야 할 부분에서 피격이 되지 않는 부자연스러움이 발생하게 됩니다.
2번 패턴과는 다르게 빠르게 내려치기 때문에 차례대로 콜라이더가 나오는 것이 아닌 모든 콜라이더가 한번에 켜졌다가 꺼지게 됩니다.
"만약 한 오브젝트 내에 여러 개의 콜라이더가 존재한다면 이들은 배열화 되어 관리할 수 있게된다."
인터넷에서 찾은 설명은 위와 같은데, 어째서인지 그냥 하나의 박스 콜라이더를 활성화 시키거나 비활성화 시켜도 큰 문제가 나진 않았습니다.

위의 설명대로 콜라이더가 하나의 오브잭트 내에 있다면 배열화되어 나온다는 것으로 알고 있었었지만, 원활한 관리를 위해 일부로 여러 개의 오브잭트로 쪼개어 만든 뒤 보스가 패턴 재생 때 콜라이더가 적용되게 하였습니다.
public void PtOn()
{
int pattern = UnityEngine.Random.Range(0, 3);
if (Vector2.Distance(GameObject.Find("Player").gameObject.transform.position, transform.position) > 10)
{
MainPositionOnPlayersRocation();
Vector2 moveDirection = (GameObject.Find("Player").transform.position - transform.position).normalized;
rBody.velocity = moveDirection * moveSpeed;
_Anim.SetBool("isMove", true);
_Anim.SetBool("isBackMove", false);
}
else if (Vector2.Distance(GameObject.Find("Player").gameObject.transform.position, transform.position) < 10)
{
_Anim.SetBool("isMove", true);
_Anim.SetBool("isBackMove", false);
if (pattern == 0 && !pt1 && pt1cd >= 15)
{
if (samePatternPrevention[0] == 3)
{
pt1 = true;
onCDTrigger = true;
_Anim.SetTrigger("Pattern01");
Debug.Log("패턴1");
samePatternPrevention[0] = 0;
}
samePatternPrevention[0]++;
}
else if (pattern == 1 && !pt2 && pt2cd >= 15)
{
if (samePatternPrevention[1] == 3)
{
pt2 = true;
onCDTrigger = true;
_Anim.SetTrigger("Pattern02");
Debug.Log("패턴2");
samePatternPrevention[1] = 0;
}
samePatternPrevention[1]++;
}
else if (pattern == 2 && !pt3 && pt3cd >= 15)
{
if (samePatternPrevention[2] == 3)
{
pt3 = true;
onCDTrigger = true;
_Anim.SetTrigger("Pattern03");
Debug.Log("패턴3");
samePatternPrevention[2] = 0;
}
samePatternPrevention[2]++;
}
}
}
이곳이 패턴이 시작되는 부분 입니다.
if (Vector2.Distance(GameObject.Find("Player").gameObject.transform.position, transform.position) > 10)
{
MainPositionOnPlayersRocation();
Vector2 moveDirection = (GameObject.Find("Player").transform.position - transform.position).normalized;
rBody.velocity = moveDirection * moveSpeed;
_Anim.SetBool("isMove", true);
_Anim.SetBool("isBackMove", false);
}
우선 플레이어와의 거리가 10보다 멀다면 플레이어의 위치정보를 받고 보스의 방향을 돌릴지 결정하는 함수가 작동합니다. 플레이어의 위치를 움직이는 방향으로 잡고, 보스에게 이동력을 주어 보스를 움직이는 동시에 애니메이터의 SetBool을 통해 보스가 어떤 애니메이션을 재생해야 하는지를 지정해 줍니다.
void MainPositionOnPlayersRocation()
{
Vector2 direction = (GameObject.Find("Player").gameObject.transform.position - transform.position).normalized;
if (direction.x > 0)
{
gameObject.transform.localScale = new Vector3(-3, 3, 1);
}
else if (direction.x < 0)
{
gameObject.transform.localScale = new Vector3(3, 3, 1);
}
}
MainPositionOnPlayersRocation();은 만약 플레이어의 위치에 따라 보스를 뒤집는 함수입니다. 스케일을 음수로 지정하였는데, 2D유니티에서 스케일이 음수로 가게 된다면 y축을 기준으로, 역으로 출력되기 때문입니다. 이 방법이 편한 이유는 위에서 보듯 보스 개체는 수많은 콜라이더를 가진 오브잭트들을 여러 개 가지고 있는데, 이를 일일이 돌리는 작업은 많은 수고를 해야 하므로 단순하게 스케일을 뒤집음으로써 이 모든 작업을 수행하였습니다.
else if (Vector2.Distance(GameObject.Find("Player").gameObject.transform.position, transform.position) < 10)
{
_Anim.SetBool("isMove", true);
_Anim.SetBool("isBackMove", false);
if (pattern == 0 && !pt1 && pt1cd >= 15)
{
if (samePatternPrevention[0] == 3)
{
pt1 = true;
onCDTrigger = true;
_Anim.SetTrigger("Pattern01");
Debug.Log("패턴1");
samePatternPrevention[0] = 0;
}
samePatternPrevention[0]++;
}
만약 플레이어와의 거리가 10 이하라면 특정 패턴을 구사하기 시작합니다. 원래 계획으로는 거리에 따른 패턴이 나오게 하려 했으나, 이동 공격이 취소되어 3개의 패턴 중 랜덤한 패턴이 재생되도록 만들었습니다.
우선 이동값을 주는 이유는, 보스가 패턴을 끝내고 모든 패턴이 쿨타임에 들어가면 움직이지 않는 부자연스러운 상황이 연출되어 넣었습니다.
int pattern = UnityEngine.Random.Range(0, 3);
패턴값은 매번 랜덤한 값을 가지고 이 중 하나가 지정되면 그 변수에 맞는 패턴이 튀어나오게 됩니다. 위의 1번 패턴을 예시로 0번이 되면 1번 패턴이 동작하게 되는 방식입니다.
if (samePatternPrevention[0] == 3)
{
pt1 = true;
onCDTrigger = true;
_Anim.SetTrigger("Pattern01");
Debug.Log("패턴1");
samePatternPrevention[0] = 0;
}
samePatternPrevention[0]++;
}
pt1은 쿨타임을 시작하기 위한 트리거이고, onCDTrigger의 경우는 보스가 공격 후 딜레이 없이 계속해서 공격을 이어 나가는 것이 아닌 약 5초간의 대기시간을 가지는 대기시간의 트리거입니다.
애니메이터로 보스의 1번 패턴 애니메이션이 재생하게 한 뒤 아래에 samePatternPrevention[0] = 0;가 0이 되는데. 이 변수는 특정 패턴 후 반복적인 패턴이 나오지 않기 위한 트리거 입니다. 총 3개의 패턴이 존재하기에 3개의 모든 패턴을 모두 봐야 반복되는 패턴이 다시 나오게 됩니다.

패턴 진행은 애니메이션 내의 애니메이션 이벤트를 통해 호출됩니다.
크게 3가지의 종류로 존재하는데
콜라이더 활성화(푸른색)
콜라이더 비활성화(붉은색)
쿨타임 시작(노란색)
이렇게 3가지의 대분류를 가집니다.
콜라이더 활성화 -
피격판정을 활성화하는 이벤트로 보스가 가지고 있던 콜라이더를 일정 시간 활성화해 보스가 가지고 있던 애니메이션과 알맞게 피격판정이 나오게 만듭니다.
콜라이더 비활성화 -
보스의 애니메이션 중 보스가 휘두르는 무기가 콜라이더 지역을 벗어나거나, 더 이상 힘을 가할 운동에너지가 안 나온다고 판단되는 부분에서 콜라이더를 끄는 이벤트입니다. 그 외에도 콜라이더 잔류로 인해 공격 애니메이션이 나가지 않은 데에도 피격이 되는 것을 방지해줍니다.
쿨타임 -
시작 쿨타임은 모든 동작을 종료한 후에 적용될 수 있게 하고자 애니메이션 맨 끝에 쿨타임이 시작하는 이벤트를 두었습니다.
콜라이더는 위에서 설명한 대로 보스의 동작에 맞춰서 작동하게 맞춰놨습니다. 이는 맞은 것 같지 않은 동작에도 피격을 당하는 상황을 없애기 위함이고. 쿨타임은 발동 시작부터가 아닌, 발동 종료 후라 생각하여 애니메이션의 끝자락에 쿨타임이 시작되게 만들었습니다.

각 패턴은 고유한 스크립트에서 관리되고, 이 스크립트 내에 애니메이션 이벤트에서 호출되는 함수를 가지고 있습니다.
public class TestBossPattern01 : MonoBehaviour
{
public GameObject Pt1HitBox01;
public bool isHit = false;
public int damage = 93;
void Start()
{
Pt1HitBox01.SetActive(false);
Pt1HitBox01.gameObject.GetComponent<PatternTrigger>().pattern = 1;
}
public void Pt1CollisionOn()
{
Pt1HitBox01.SetActive(true);
}
public void Pt1CollisionOff()
{
Pt1HitBox01.SetActive(false);
isHit = false;
}
}
public bool isHit = false;
public int damage = 93;
void Start()
{
Pt1HitBox01.SetActive(false);
Pt1HitBox01.gameObject.GetComponent<PatternTrigger>().pattern = 1;
}
우선 isHit은 플레이어가 이미 닿았는지 점검하는 bool함수이고 만약 플레이어가 닿았다면 다시 닿아도 대미지를 입지 않게 만들었습니다.
기본적으로 TriggerEnter를 써서 여러 번 맞을 가능성은 없으나, 테스트 도중 콜라이더 범위를 나갔다가 다시 들어오자, 피격 판정이 나자 넣었습니다.
Start에선 우선 콜라이더를 비활성화하고 보유하고 있는 객체 중 PtHitBox01내부에 있는 히트박스 트리거에 1번 패턴임을 인지시켜 줍니다.
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.layer == LayerMask.NameToLayer("Player")
&& pattern == 1)
{
Managers.Object.Player.OnDamaged(this, Pt.gameObject.GetComponent<TestBossPattern01>().damage);
}
if(collision.gameObject.layer == LayerMask.NameToLayer("Player")
&& pattern == 2)
{
Managers.Object.Player.OnDamaged(this, Pt.gameObject.GetComponent<TestBossPattern02>().damage);
}
if (collision.gameObject.layer == LayerMask.NameToLayer("Player")
&& pattern == 3)
{
Managers.Object.Player.OnDamaged(this, Pt.gameObject.GetComponent<TestBossPattern03>().damage);
}
}
패턴 트리거의 핵심 역할은 바로 보스 몬스터가 공격을 날렸을 때 어떤 패턴인지를 파악하고, 그 패턴에 맞게 플레이어에게 피해를 주는 중요한 역할을 해줍니다.
각각 독자적인 스크립트로 선언해서 관리할 수 있었긴 했으나, 하나의 스크립트에서 관리하고자 이렇게 설계하였습니다.
public void CDControll()
{
if (onCDTrigger == true)
{
if (onCD == true)
{
onCdTime -= Time.deltaTime;
MainPositionOnPlayersRocation();
Vector2 moveDirection = new Vector2(transform.position.x - GameObject.Find("Player").transform.position.x, 0).normalized;
rBody.velocity = moveDirection * 1.5f;
_Anim.SetBool("isMove", false);
_Anim.SetBool("isBackMove", true);
if (onCdTime <= 0)
{
onCdTime = 0;
onCDTrigger = false;
onCD = false;
onCdTime = 5f;
}
}
}
하나의 동작이 끝나게 되면 보스는 쿨타임 상태에 돌입하게 되는데 이 패턴은 보스가 공격 후 주어진 시간 동안 후진을 합니다. 이때에도 위에 설명한 일반적인 움직임처럼 플레이어의 위치를 계속 받아와 후진 방향을 실시간으로 갱신합니다.
이 쿨타임은 보스가 공격 후 플레이어에게 공격할 타이밍을 주는 대기시간으로 이 상태가 되면 보스는 다른 패턴의 쿨타임이 끝난 상태라 하더라도 공격을 수행하지 않습니다.
if (onCdTime <= 0)
{
onCdTime = 0;
onCDTrigger = false;
onCD = false;
onCdTime = 5f;
}
대기시간이 종료되면 위의 코드처럼 쿨타임 상태를 해제, 그리고 쿨타임을 초기화시켜 다음 쿨타임 상태가 되었을 때도 동일한 시간만큼 대기시간을 가지게 만들어 줍니다.
if (CD1Trigger && pt1)
{
pt1cd -= Time.deltaTime;
if (pt1cd <= 0)
{
pt1cd = 0;
pt1 = false;
CD1Trigger = false;
pt1cd = 15f;
}
}
if (CD2Trigger && pt2)
{
pt2cd -= Time.deltaTime;
if (pt2cd <= 0)
{
pt2cd = 0;
pt2 = false;
CD2Trigger = false;
pt2cd = 15f;
}
}
if (CD3Trigger && pt3)
{
pt3cd -= Time.deltaTime;
if (pt3cd <= 0)
{
pt3cd = 0;
pt3 = false;
CD3Trigger = false;
pt3cd = 15f;
}
}
위는 각 패턴의 쿨타임 계산기입니다. 패턴은 패턴마다 고유한 대기시간을 가지게 됩니다. 처음 테스트 때에는 모든 쿨타임이 같이 돌아 구현하고자 하는 방향과 전혀 맞지 않는 방식이 되었었으나, 수정을 통해 공격 종료 후 패턴마다 고유의 대기시간을 가지게 만들었습니다.
public void GetDamage()
{
hp -= GameObject.Find("AtkEffect").GetComponent<AtkEffect>().nomalAtk;
Debug.Log("남은체력:" + hp);
spriteRenderer.color = new Color(1f, 0, 0);
StartCoroutine(ReturnColor(originalColor, 0.4f));
}
public void GetDamage(int dmg)
{
hp -= dmg;
Debug.Log("남은체력:" + hp);
spriteRenderer.color = new Color(1f, 0, 0);
StartCoroutine(ReturnColor(originalColor, 0.4f));
}
보스는 피격시 플레이어가 가지고 있던 공격 콜라이더 오브젝트 내부에 있는 공격력에 따라 피해를 입습니다.
보스가 피격 시 피격을 알리는 이팩트를 실행하는데, 보스가 여타 다른 2d게임처럼 빨간색으로 변했다가 점차 원래 색으로 돌아오는 식의 피격 효과를 내고 싶었기 때문입니다.
IEnumerator ReturnColor(Color targetColor, float duration)
{
Color startColor = spriteRenderer.color;
float timer = 0f;
while (timer < duration)
{
timer += Time.deltaTime;
float t = timer / duration;
spriteRenderer.color = Color.Lerp(startColor, targetColor, t);
yield return null;
}
spriteRenderer.color = targetColor;
}
보스가 피격 시 우선 빨간색으로 만든 뒤 위의 코루틴 함수를 호출합니다. 코루틴 함수는 우선 보스몬스터의 기본 색상과 얼마나 지속될지를 정할 간격을 가져옵니다.
startColor는 현재 빨개진 보스의 색상을 저장하고 아래에 이 코루틴에서 사용할 타이머 변수를 선언합니다.
만약 0부터 시작하는 타이머가 간격 변수보다 작다면 타이머가 작동하게 되고 아래의 t를 선언한 뒤 타이머와 간격 변수를 나눠 1이 최댓값이 되도록 만들어줍니다.
그 후 spriteRenderer 컬러 중에서 색상을 점차 변환시켜 주는 Lerp를 이용해 빨간색을 시작으로 원래의 색으로 돌려놓는데, value값이 0~1 중에서 t만큼 색상을 교차하며 바꿔가게 됩니다.
만약 해당 색상 교차가 끝난다면 혹시라도 미세한 색상 오차가 있는 것을 방지하여 맨 아래에 다시 원래 색상으로 돌려놓습니다.
void Dead()
{
for (int i = 0; i < QuestManager.instance.curNpc.Count; i++)
{
if (QuestManager.instance.curNpc[i] != null)
{
QuestManager.instance.curNpc[i].GetComponent<NPC>().KillMonster(id);
}
}
HitBoxs.SetActive(false);
deadTrigger = true;
onBattle = false;
_Anim.SetTrigger("Dead");
}
보스는 사망 시 우선 퀘스트를 가지고 있다면 퀘스트에 해당 보스가 가지고 있던 id를 보내어 퀘스트에 목표가 되는지 안 되는지를 확인시켜 줍니다.
그 후 박스 콜라이더를 없에 보스의 시체가 피격 효과를 나는 것을 방지하고, deadTrigger를 활성화하고 전투 상태를 해제한 뒤 사망 애니메이션이 나오게 합니다.
deadTrigger는 단순히 보스가 죽었음을 알려주는 bool변수로 이 트리거가 꺼지게 되면 이 이후의 모든 동작을 수행하지 못하게 만들어 줍니다.
void DeadMassage()
{
GameObject.Find("BossCanvas").GetComponent<BossCanvas>().OnDead();
Exit.SetActive(true);
Collider2D[] colliders = GetComponents<Collider2D>();
rBody.gravityScale = 0;
foreach (Collider2D collider in colliders)
{
collider.enabled = false;
}
GetComponent<CapsuleCollider2D>().enabled = false;
StartCoroutine(MassageFade());
}
보스는 사망 후 소울 라이크 게임처럼 시각적 연출을 출력합니다. 위의 함수는 애니메이션 이벤트로 작동합니다.
Collider2D[] colliders = GetComponents<Collider2D>();
rBody.gravityScale = 0;
foreach (Collider2D collider in colliders)
{
collider.enabled = false;
}
GetComponent<CapsuleCollider2D>().enabled = false;
해당 코드가 가장 위에서 설명한 콜라이더가 여러 개일 경우 배열처럼 다룬다는 것을 이용한 곳으로 보스의 콜라이더는 몸체 부위와 머리, 그리고 발 뒤쪽 이렇게 총 3개의 피격판정 콜라이더를 가지고 있습니다. 모든 콜라이더를 배열로 가져온 뒤 foreach를 이용해 해당 피격판정 콜라이더 중 박스 콜라이더를 끄게 만들었습니다.
그리고 아래에는 박스콜라이더가 아닌 캡슐콜라이더인 머리 피격판정 콜라이더를 끄는 부분입니다.
StartCoroutine(MassageFade());
이 부분이 다크소울식 보스 처치시 연출을 시작시키는 부분입니다.
연출의 모습
IEnumerator MassageFade()
{
float timer = 0;
Color massageColor = BossKillMassage.color;
Color ImageColor = BossKillImage.color;
while (timer <= 1)
{
timer += Time.deltaTime;
float t = timer / 0.5f;
BossKillImage.color = Color.Lerp(ImageColor, new Color(0, 0, 0, 1), t); // Alpha 값을 1로 변경
BossKillMassage.color = Color.Lerp(massageColor, new Color(1, 0, 0, 1), t); // Red 값만 1로 변경
yield return null;
}
yield return new WaitForSeconds(3);
timer = 0;
massageColor = BossKillMassage.color;
ImageColor = BossKillImage.color;
while (timer <= 1)
{
timer += Time.deltaTime;
float t = timer / 0.5f;
BossKillImage.color = Color.Lerp(ImageColor, new Color(0, 0, 0, 0), t); // Alpha 값을 0으로 변경
BossKillMassage.color = Color.Lerp(massageColor, new Color(0, 0, 0, 0), t); // 전체 색상을 0으로 변경
yield return null;
}
}
코루틴 함수 내부는 이렇게 생겼습니다.
우선 텍스트와 배경 이미지의 색상을 불러온 뒤 보스가 피격을 연출하는 코루틴 함수처럼 타이머를 작동시킨 뒤 0.5초의 그라데이션 변경 시간을 줍니다. 우선 이 시간동안 보스 처치 텍스트와 배경 이미지가 그라데이션처럼 나오고 약 3초의 대기 시간을 가진 뒤 다시 타이머를 0으로 만들고 텍스처와 배경 이미지의 색상을 가져오고 아랫부분에서 다시 투명하게 바꿔줍니다.
Kommentare