[내일배움캠프 5주차] 조별과제 WIL - 스킬 콜라이더

하얀요니콘·2025년 8월 5일
0

이번주는 팀 프로젝트로 궁수의 전설을 개발하는 시점이 있어서 몇일간 TIL을 작성하지 못하였지만, 이번 기회를 계기로 WIL처럼 작성하여 일주일간 한 내용들을 정리해보며, 트러블 슈팅을 진행하겠다.

1. 시작하기에 앞서

우선 기본적으로 할 내용에 대해 정리하고 가자. 간단하게 이번주에 무엇을 하였는지 결과를 정리하는 곳이다. 이번주의 목표는 다음과 같다.

적이 사용할 패턴을 여러가지 스킬들로 모듈화 시켜서 만들기

간단하게 요약하면 이정도이고, 개념부터 정의하자.

  • 패턴 : 여러가지 스킬들을 사용하는 구조이다. 여러 공격이 조합되서 하나의 패턴이 구상될 수도 있고, 스킬 하나만 존재 하는 패턴 또한 있을 수 있다. 예를들면 한번에 원형을 여러게 까는 스킬들의 조합이 패턴이다.
  • 스킬 : 각각의 공격의 구성이다. 예를들면 원형으로 공격하는 스킬, 투사체를 날리는 스킬, 이런것들이 스킬로 분류된다.

1-1. 스킬 내용

스킬 추가 구현 내용은 다음과 같다.

  • 다양한 공격 스킬 구성 만들기 (여러 종료의 콜라이더, 데미지 등)
  • 미리 경고를 띄워주고 플레이어가 이 위치에 공격이 떨어진다는것을 확인 하게 해줌 (경고창이 생략 될 수 도 있음)
  • 지속시간 있는 장판도 포함되어야 하며, 각 공격에 맞게 회전이 되어야 한다.

1-2. 패턴 내용

패턴 추가 구현 내용은 다음과 같다.

  • 여러개의 스킬을 순차적으로 정해진 딜레이에 사용 할 수 있어야 한다.
  • 여러개의 스킬을 원하는 위치에 떨어트려야한다. (위치는 다양하게 존재)

1-3. 기본적인 구성

우선 이 정리만 해보고 구성을 해보면 다음과 같다.

아직은 별거 없지만, 점차 확장 해 나갈것이다.

2. 스킬 세분화

우선 스킬을 세분화 할 필요가 있다. 우리는 범위공격도 있고, 탄환공격도 있을것이다. 이를 skill을 받아서 두가지로 나누어 보도록 하자.

abstract class Skill : Scriptable Object

스킬은 Scriptable Object를 활용하여 만들것이다. 이제 이 추상 클래스를 활용하여 범위공격과, 탄환공격을 확장 해 나갈것이다.
기본적으로 Skill내부에는 공통으로 필요한 내용을 정리하면 다음과 같다.

  • 스킬 공격력
  • 스킬 공격력 강화 배율
  • 스킬의 시전 방향 및 떨어질 위치
  • 애니메이션 등 꾸밀 요소

2-1. 범위 공격

범위공격은 Ranged Skill이라는 이름으로 사용 할 것이다.

RangedSkill : Skill

범위공격에 추가적으로 필요한 내용을 정리 해 보자.

  • 적이 공격 할 위치에 필요한 경고 및 콜라이더
    • 공격 할 콜라이더가 얼마나 유지될 지
    • 경고를 보여 줄 시간
    • 해당 공격의 각도, 공격 콜라이더가 회전을 할 것인지
    • 콜라이더 크기

가장 중요한 내용은 이 Scriptable Object에 콜라이더가 필요하다는 점이다. 이를 따로 만들 필요가 생기게 된다.

class HitCollider : Monobehaviour

범위 공격을 표시하기 위해 실제로 생성하는 HitCollider이 필요해 졌다. 이 HitCollider은 Monobehaviour을 상속받아, 실제로 화면에 표출이 되야한다. (위에 있는 데이터 덩어리와는 다르다.)
HitCollider의 구성은 다음과 같아야 한다

  • 스킬의 데미지를 받기 위해 필요한 콜라이더
  • 스킬의 공격범위를 경고 해 줄 경고 스프라이트
    • 스킬의 딜레이를 표현해 주기위해 점차 증가하는 범위
    • 스킬이 어디까지 공격이 될 지 표현해 주기 위한 총 범위

이를 이용하여 스프라이트를 제작 해 주면 된다. 이제 이 부분에서 첫번째 트러블 슈팅이 발생한다. 이는 후에 다루고, 우선 탄환들을 보자.

2-2. 탄환 공격

탄환 공격은 Projectile Skill이라는 이름을 사용 할 것이다.

class ProjectileSkill : Skill

탄환공격에 추가적으로 필요한 내용을 정리해보자.

  • 스킬을 사용 시 발사될 투사체
    • 해당 투사체의 크기
    • 해당 투사체의 속도
    • 해당 투사체의 개수
    • 투사체를 발사할 각도
    • 해당 투사체가 적을 추적 할 것인지?

탄환 공격은 반대로 탄환이라는 것을 따로 만들어 줄 필요가 있다.

class Projectile : Monobehaviour

탄환에 필요한 내용은 다음과 같다.

  • 투사체의 rigidbody
    • 투사체의 공격 방향, 속도를 정해줌
    • 투사체의 충돌 콜라이더 모양을 포함

보통 탄환은 많이 생성이 되다보니, 기본적으로는 원형으로 처리를 할 것이다. 일단 원형으로 넣어두고 생각하자.

2-3. 기본적인 구성

이제 스킬 부분을 정리해 보도록 하자.

3. 트러블 슈팅 1 - HitCollider

이제 HitCollider을 여러가지 제작하며, 발생한 문제들에 대해 기술한다.

3-1. 존재하지 않는 Sprite

기획된 공격 패턴을 보면 사분원과, 사각형 판정들이 몇가지 존재한다, 이제 이를 만들때 문제점이 생긴다.

Unity의 사분원

사분원은 기본적으로 지원되지 않는다. 부채꼴을 만들 수 있다고 생각하고 작업을 하려고 하였지만, 실제로는 이를 생성하려면 직접 mesh를 그려줘야 할 것이다.

Sprite 의 Pivot

사각형의 경우에는, 해당 공격의 중점이 시작지점, 즉 사각형의 끝 지점에서 시작해야 한다. 하지만 기본적으로 Unity에서 제공되는 사각형 Sprite는 전부 중앙에서 시작되는 스프라이트이며, 수정이 불가능하다.

해결 방안

우선 이 문제를 접근하기 전에, 우리가 결국 충돌 판정을 하기 위해서는 Physics2D를 활용할 수 있을것이다. 하지만 경고판정을 띄우기 위해서라면, 그리고 플레이어에게 범위를 표시 해 주기 위해서라면, 스프라이트를 만드는 형식이 더 알맞다고 생각 하였다. 사분원과 사각형은 금방 만들 수 있을 것이고, 이제 만든 스프라이트를 pivot을 설정하여 hitcollider프리팹들을 제작 해 주어 해결 해 주었다.

Collider은 Polygon 콜라이더를 사용하였다. 다른 콜라이더보다 무겁긴 하지만, 그래도 사분원과 사각형은 무거운 콜라이더는 아니다 보니 괜찮을 것이다.

3-2. 지속 데미지

이제 구현을 하다보니, Entity가 데미지를 받는 현상이 발생 할 경우, Entity가 해당 장판에 그대로 정지 해 있을 경우, 콜라이더 인식이 되지 않는 경우가 발생하였다.

처음에는 플레이어 콜라이더가 장판을 인식을 못하는 것인줄 알았지만, 디버그 로그를 파악해보니, 몇번 장판과 충돌 한 후 플레이어 이동이 되지 않을 시 더이상 collision을 처리하지 않는다.

해결 방안

이는 결국 entity의 Rigidbody가 비활성화 되는 부분에서 예상과 다른 결과가 나온 것이다.

Rigidbody의 SleepMode

Rigidbody에는 해당 Rigidbody에 대한 최적한 연산을 하기 위한, SleepMode라는것이 존재한다. 즉 Rigidbody에서 더이상 이동같은 연산이 필요하지 않을 시, Rigidbody에 대한 연산을 더이상 하지 않는다고 보면된다.

이제 우리는 Rigidbody가 더이상 잠들지 않게 설정을 해 주고, 해당 장판이 더이상 없어졌을 시 Rigidbody의 SleepMode를 다시 원래대로 되돌려 주는 방식을 사용 하였다.

  private void OnTriggerStay2D(Collider2D collision)
  {
      //if Shooter Collider, skip
      if (collision.gameObject.CompareTag(shooter.tag))
          return;
      //if Entity, Attack
      if (collision.gameObject.GetComponent<Entity>() != null)
      {
          if (collision.gameObject.GetComponent<Rigidbody2D>().sleepMode != RigidbodySleepMode2D.NeverSleep)
          {
              rigidbody2Ds.Add(collision.gameObject.GetComponent<Rigidbody2D>());
              collision.gameObject.GetComponent<Rigidbody2D>().sleepMode = RigidbodySleepMode2D.NeverSleep;
          }
          Entity entity = collision.gameObject.GetComponent<Entity>();
          shooter.ApplyDamage(entity, damage,isJumpAvoidable);
      }
  }

  private void OnDestroy()
  {
      foreach (Rigidbody2D rigidbody in rigidbody2Ds)
      {
          rigidbody.sleepMode = RigidbodySleepMode2D.StartAwake;
      }
  }

우선 일부 코드를 보도록 하자. 이제 collision의 rigidbody2D를 가져와서, 이의 sleepMode를 NeverSleep 으로 변경 해 준다. 즉 해당 rigidbody는 가만히 있어도 연산을 한다는 뜻이다. 이후 이 rigidbody를 내부적으로 저장 해 두었다가, Destroy할 시, 최적호를 위해 다시 sleep모드를 켜주는 방식으로 구현 해 주었다.

4. 패턴 세분화

이제 패턴과 생성해줄 프리팹들이 얼추 정리가 되었으므로, 다음 작업인 패턴을 다시 구현해 보도록 하자. 패턴 또한 데이터 덩어리로 만들어 줄 것이다.

class Pattern : Scriptable Object

우선 패턴에 필요한 기본적인 내용들을 다시 한번 되집어보자.

  • 스킬 종류
    • 이건 스킬의 Scriptable Object를 그대로 가져오면 될 것이다.
  • 스킬이 떨어지는 위치 -> ?
  • 스킬이 떨어지는 시간 -> ?

4-1. 떨어지는 위치?

우선 패턴이 떨어질 수 있는 위치는 너무나도 다양하다

중심 좌표

  • 전체 world position 기반
  • 패턴을 시전하는 Entity 기반
  • 패턴의 대상인 Target 기반

추가 요소

  • 해당 위치의 0,0
  • 해당 위치에서 특정 offset만큼 이동
  • 해당 위치에서 랜덤 위치

기타 좌표

  • 플레이어와 패턴을 시전하는 Entity 사이 중간 어딘가
  • 이전 스킬에서 사용했던 좌표
    • 고정된 N번째 좌표
    • 해당 인덱스의 N번째 전 좌표
  • 플레이어의 x좌표나 y좌표를 고정하고 offset을 설정
  • 설정 없음

일단 중심좌표 X 추가요소만 해도 9가지, 그리고 기타좌표도 4가지라서 들어가야 할 변수가 엄청나게 많아졌다.
추후 트러블슈팅에서도 이를 언급할것이다.

4-2. 스킬이 떨어지는 시간

물론 스킬 내부에도 시전이 완료되는 시점의 쿨타임이 존재한다. 하지만 우리가 동시에 여러 스킬을 조합해서 사용 할 수 있을 것이고, 이를 이용하여 다양한 스킬처럼 보이게 구성 할 수도 있을것이다. 이를 위해 bool값으로 다른 쿨타임을 무시 할지, 하지 않을지, 해당 공격을 하는 시점이 정해지게 될 것이다.
다만 이를 사용했을 때 다른 문제가 발생하게 된다. 이는 트러블 슈팅에서 다룰것이다.

4-3. 구성

우선 Pattern에 여러가지 변수를 추가 해 주겠다. 우선 여러 종류로 확장을 하는 것 보다는, 하나 내부에서 전부 처리하고 싶어서, 모든곳에 몰아주었다. 다만 아쉬운점은 이를 interface로 활용해서 작업했으면 좋았을 수도 있었을 것 같다.

  • float[] Time

    • 해당 스킬의 발동 시점
  • enum[] PosState

    • 해당 pos가 어떤 옵션을 사용 할 것인가
  • Vector2[] Pos

    • 만약 fixed 옵션을 사용한다면, 해당 중점으로부터 offset이 얼마큼 되는가
  • bool IgnoreSkillCooldown

    • 각 스킬의 쿨다운을 무시 할 것인가?
  • Vector2 RandomMin, RandomMax

    • 만약 Random 옵션을 사용한다면, 얼마큼의 Random offset을 줄것인지에 대한 범위
  • float EntityToPlayerScale

    • 플레이어와 Entity사이의 거리 offset이다. 0.5면 중앙, 1이라면 Player, 0이라면 Entity의 위치를 따르는 위치를 판별한다.
  • int PreviousPosIndex

    • Index 참조 옵션을 사용할 때 사용한다.
    • IndexFixed를 사용하면, 해당 인덱스의 위치를 가져온다.
    • IndexOffset을 사용하면, 현재 인덱스 - Index 의 위치를 가져온다.

자 여기까지만 내용을 봐도 더럽다. 하지만 이때는 구조화가 덜 된체로 작업을 해서 이렇게 더럽게 보이는 점이 많이 아쉽긴 하다.

그래도 Scriptable Object를 통해 작업을 하다보니 패턴들 생산은 간단해진다.

5. 트러블 슈팅 2 - Pattern

패턴을 제작하면서, 여러 수정사항을 거치게 되었다.

5-1. Position State 수정... 또?

우선 작업을 하면서 기획서를 보다보니, 예외적인 패턴이 너무나도 많았다. 지금 위에서는 완료된 결과물만 보여줬지만, 원래는 Pattern을 상속하는 Floor패턴 등 다양하게 확장을 시키려고 했지만, 그럴수록 생성하는데 햇갈리게 되었다.

해결 방안

결국 Pattern을 위에 설명한 것과 같이 패턴 내부에서 전부 설정이 가능하도록 바꾸었다. 결과적으로는 확장을 일부러 안한것이다. (매번 확장을 위해 추가로 생성 작업을 하는 것보다는, Pattern하나에서 모두 처리하는것이 낮다고 판단하였다.)

5-2. 딜레이

우리가 스킬을 사용 할 수는 있다. 다만 이를 적 행동양식에 넣다보니, 적이 쉴세없이 공격을 하는 상황이 발생하였다.

우리의 구현 방식을 다시 돌아보면, 패턴이 끝나는 시점은 곧 마지막 스킬을 사용한 시점이 되게 되는데, 이를 추가적으로 딜레이를 넣어줘야, 플레이어가 공격 할 시간이 생기는 것이다. 지금은 이를 설정 해 줄 방식이 따로 존재하진 않았다.

해결 방안

우선 접근 방식을 다르게 잡아보자.

패턴이 끝나는 시점은 결국 마지막 패턴이 끝나는 시점

그럼 반대로 마지막 패턴이 끝나는 시점을 조절 해 주면 되는것이다. 나는 이를 Array의 Nil과 같이 제일 마지막에 Place Holder로 두는 EndSkill 이라는 것을 만들어 작업을 해 주었다.

EndSkill : Skill

실제로는 아무 동작도 하지 않는 스킬이지만, UseSkill에서 패턴을 사용하는 주체인 Enemy가 스킬을 사용 완료했다는 정보를 전달 해 주기 위하여, Flag를 바꾸어 주는 작업만을 한다.

이제 모든 패턴 제일 마지막에 EndSkill을 넣어주고, 이 ActiveTime을 넣어주면, 결국 이 정보가 스킬의 딜레이가 되게 된다.

5-3. Scriptable Object의 한계

우선 우리는 Pattern이라는 것을 Scriptable Object에서 만들어 주었고, 이 구간에서 Skill을 딜레이 넣어 실행 시켜주기 위해서는, 코루틴을 돌리는것이 맞는데, 문제는 Scriptable Object는 코루틴을 못돌린다! 이 사안은 다음 내용에서 자세히 다룬다.

6. PatternActor

결국 우리는 패턴을 여러개 가지고, 이를 유동적으로 사용 하긴 하겠지만, 위에 5-3에 언급한 내용에 따르면, 패턴 자체적으로는 실행이 불가능하고, 이제 이를 실행시켜줄 주체가 필요하다. 이 시점에서 Enemy가 개발이 되지 않아, PatternActor이라는 클래스를 만들어 주었다.

PatternActor : Monobehaviour

결국엔 패턴들 리스트와, 어느 패턴을 실행시킬 지 정한다면, 코루틴을 돌려서 패턴에 있는 스킬들을 차례대로 activeTime에 맞춰 실행 해 주는 역활만 한다. 결국 이 PatternActor은 Enemy에 상속 되겠지만, 패턴을 굴리기 위해서는 Monobehaviour을 사용해야한다.

이를 이제 보스에 정해진 패턴들의 ScriptableObject를 합친 Prefab을 합쳐 만들어준 후, Enemy에 붙이기만 하면 적의 패턴 공격 방식은 완료가 된다.

7. 트러블 슈팅 - ScriptableObjects

이제 테스팅을 하면서, ScriptableObjects에 특수한 이상이 생겼다.

7-1. Player은 Pattern이 아닌 Skill만을 가져다 쓴다

Pattern은 스킬들의 시퀀스지만, 인풋을 받는 플레이어 입장에서는 Skill만을 받아도 된다. 이제 각 Input에 맞게 Skill을 사용 해 줄것이다.

해결 방안

Skill말고 패링, 점프도 구현을 해 뒀었는데 (애니메이팅이 안되서 보이지는 않는다), 이를 포함한 ActionDelay를 지정 해두고, 현제 시간과 발동 시간을 비교하여 UseSkill이 나가게 작업 해 주었다.

물론 Player을 위한 Skill들은 ScriptableObject로 생성하여 따로 제작 해 두었다.

7-2. Scriptable Object의 데이터 오버라이딩

기본적으로 Pattern, Skill들을 가져다 쓰다보면, 해당 값들이 계속해서 바뀌고, 바뀐값이 해당된 Scriptable Object에 게임이 종료되도 저장이 된다. 하지만 우리가 Scriptable Object를 만든 이유는, 고정된 값들을 사용하기 위해서였다.

해결 방안

우선, PatternActor에서 해당 Pattern, Skill들을 모두 복제를 해서 사용한다. 그래서인지 부하가 조금 걸리는 듯 하지만, 이는 구현 시간의 문제로 object polling같은 기법을 사용하지 못한점이 아쉽다.

7-3. 팀원이 구현한 적 Behaviour Tree와 연동관계

우선 스킬마다의 딜레이를 만들긴 하였지만, 적 Behaviour Tree 내부에서 해당 state machine같은 구조를 돌리려다 보니, 충돌이 좀 있었다. 이는 내가 약간 설명을 잘 못해서 endskill부분에 혼동이 있었던 부분인데, 결국 endskill의 딜레이와 Behaviour Tree의 Await같이 노드에서 기다리는 방식을 사용하여 딜레이를 주어 맞추어 나갔다.

8. 소감

얼추 작업을 완료하고, 이제 다른 팀원이 못한 작업들도 손을 많이 보았지만, 스스로 한 스킬부분을 정리 하였다. 아무래도 빠진 분의 작업을 추가로 하다보니, 정리도 제대로 되지 않고 클래스 다이어그램도 잘 못따라간 부분이 있고, 버그 체크 및 수정 할 시간이 부족했던 것도 아쉬웠다. 다른 작업들도 소개 하고 싶었지만, 이번에는 이것을 위주로 설명한다.

우선 해당 Git과 시연 영상을 가볍게 남긴다.
[Git] https://github.com/gyro1515/TeamProject_SilverTown_Utopia
[영상] https://drive.google.com/file/d/1ZlKxKa5pyQVcBOz3SyGD0MA1IDGE7lzZ/

9. 추후 알게 된 것

우선 개발 한 부분이 PatternActor가 모든것을 받고, 이제 내부적으로 모듈적 구조를 완성 하였다. 다른 조원분이 말하는 내용들을 들으며 알게 된 부분 중, 이는 데코레이터 패턴과 유사하다고 판단된다.

데코레이터 패턴 : 하나의 컴포넌트에 장식을 붙이듯이 여러 파츠파츠를 붙여서 완성하는 방식

어찌 정리해보면

HitCollider과 Projectile을 붙인 Skill
Skill여러개를 붙인 Pattern
그리고 이 Pattern여러개를 붙인 PatternActor
이 PatternActor을 붙인 Enemy

이렇게 진행이 되다 보니 유사하다고 느꼈던 것 같다.
다만 나는 디자인 패턴을 모르고 구현을 한거였는데, 정작 이름같은거를 모르다 보니 (전에 포함된 Event Bus Pattern 또한 마찬가지), 아직 배워야 할 부분이 많은것 같으면서도, 직접 이런 상황을 만들어 나갔다는게 놀랍기도 하다.

profile
코딩공부용

0개의 댓글