카타나 공격 구현


코드 (깃허브)


카타나 (칼)

일단 이것은 프로젝트 이쩜오 기획에 맞춰 구현한 것입니다. 같은 칼 공격이더라도 기획에 따라 구현이 많이 바뀔 것입니다.

그래도 구현하며 공부한 내용과 생각한 내용들을 기록해, 다른 기획에 맞춰 구현할 때도 도움이 될 것입니다.

구현

katana

우선, 애님 몽타주와 애니메이션 노티파이를 사용해서 구현했습니다.

판정은 휘두르는 타이밍동안 NotifyState를 사용했고, 콤보는 Notify를 사용해 특정 타이밍에 마우스가 눌려있는가를 검사했습니다.


타격 판정 구현

제가 생각하는 칼 판정 구현 방법 세 가지입니다.

  1. 무기의 콜리전을 공격 애니메이션 시작할 때 켜고, 끝날 때 끄는 방식. 적과 부딪히면 hit 또는 overlap 이벤트 발생. (무기 자체를 or 무기에 소켓을 붙여서)
  2. 칼 경로 따라서 Shape Sweep (틈이 적어서 정밀. 쉽게 두께 반영 가능)
  3. 칼 경로 따라서 Line Trace 연결 (Shape Sweep보다 비용이 쌈)

기획에 따라 고려해 볼 내용입니다.

  1. 판정 기대 수준 (정확한 궤적, 프레임 드랍 시 정밀도 등)
  2. 공격 속도
  3. 칼 두께

콜리전 방식은 프레임 단위로 현재 프레임 위치에서 검사하는 거라, 초당 프레임 수가 낮아지거나 공격 속도가 빨라지면 프레임 사이 칼 위치 차이가 커져서 정밀도가 떨어질 수 있습니다.


Shape Sweep과 Line Trace

  • 한 번의 Shape Sweep이 몇 번의 Line Trace와 성능이 비슷한지는 잘 모르겠습니다.
  • Shape마다 판정식이 다르기에 성능이 다릅니다.
  • 칼의 두께를 반영하는 경우에는 Line Trace를 많이 쏴야합니다.
  • Sweep 방식은 모양 그대로 Sweep 하기 때문에 칼의 회전을 고려하지 못 할 수 있습니다.

저 같은 경우엔, 기획상으로, 공격 속도가 많이 빨라질 수 있고, 카타나는 두께가 거의 없어서 두께를 반영하지 않아도 되고, 더 정확한 궤적으로 판정하기 위해 Line Trace 방식으로 구현했습니다.


칼에 소켓을 세 개 붙여서, 그 소켓 위치를 사용해 Line Trace 했습니다.

처음에는 가장 간단하게, 이전 소켓 위치에서 다음 소켓 위치로 쐈습니다.

그 후, 판정을 촘촘하게 하기 위해서 지그재그 모양으로 쐈습니다.

katana

(이후에, 지그재그 모양을 왼쪽에서 오른쪽으로 바꿨습니다. 라인 하나 추가하는 것으로 정확도가 크게 늘어나는 것 같습니다.)

공격 속도가 빠르거나, 초당 프레임 수가 적어졌을 때 프레임 사이 칼 간격이 멀어져 판정이 부정확해지는 문제가 있었습니다.

옵션 플래그(bStepTrace)와 최소 간격 프로퍼티를 넣어, 원한다면 칼 판정 최소 간격을 설정할 수 있게 구현하였습니다.

StepTrace로 판정을 촘촘하게 만들었지만, 그저 칼 간격이 넓을 시 일정 간격으로 나눠 판정을 진행하는 방식이라 궤적이 둥글지 않고 각지는 문제가 발생했습니다.

아직 해결하지 못했습니다. 해결했습니다. 일명 MetaTrace


MetaTrace (메타데이터 사용)

StepTrace처럼 LineTrace의 시작점과 끝점을 추가하는 것은 같은데, StepTrace와 다르게 칼의 궤적 위에 있는 점으로 추가를 해야 궤적을 살리면서 판정을 촘촘하게 만들 수 있습니다.

그래서 칼의 궤적, 즉 타격 판정을 위해 칼에 붙인 Socket의 위치를 원하는 어떤 시점이든 알아내야 했습니다. (StepTrace는 틱마다 엔진이 계산해서 주는 값만 사용)

칼 위 Socket의 위치를 구하는 과정을 나열해 봤습니다.

katana

이 중에 1번, 3번을 제외한 값들은 우리 게임에서 게임 시작 전 결정되고 게임동안 바뀌지 않는 값입니다.

1번은 게임 중에 계속 바뀌는 값이고, 3번은 한 애니메이션이 진행됨에 따라 포즈가 바뀌어 값이 바뀌지만, 한 애니메이션을 통째로 봤을 때는 바뀌지 않습니다.

그래서 이전 틱과 현재 틱 사이의 시간에서 소켓 위치를 구할 때,

1번은 이전 틱과 현재 틱에서의 값을 보간해서 쓰는 것이 최선이고, (참고로, 애니메이션에서 캐릭터가 도는 건 뼈가 돌아가는 것이라 그런지 World Rotation이 바뀌지 않았습니다. 그저 움직이는 만큼 Position만 조금씩 바뀝니다.)

3번은 애니메이션 단위로는 변하지 않는 값이니깐, 미리 추출해서 애님 메타데이터로 가지고 있는 것이 가능합니다. 이것이 런타임에 직접 얻는 것보다 속도 측면에서 효율적일 것입니다.

그래서 칼이 부착되는 Bone Space에서 Skeletal Component으로의 변환을 메타데이터로 추출하기로 했습니다.
그리고 그때의 시간을 알기 위해 메타데이터에 시간 배열도 넣기로 했습니다.

그리고 추출하기 쉽게 에디터 툴을 만들기로 했습니다.


메타데이터 추출

생각한 추출 방법은

  1. 에디터 환경에서 레벨에 Skeletal Mesh Component를 스폰하고,
  2. Skeletal Mesh를 입히고,
  3. Animation을 설정하고,
  4. 설정한 주기씩 애니메이션을 움직이면서,
  5. 메타데이터를 추출하는 것입니다.

( 쉽게 생각한 것만큼 쉽게 구현되진 않았습니다:) )


처음에 Anim Sequence와 Anim Montage 중 어떤 단위로 메타데이터를 추출하는 것이 좋을지 고민했습니다.

Anim Sequence별로 메타데이터를 두는 것이 더 분할되어 좋다고 생각했습니다.


성능 측면에서는,
시퀀스 단위의 경우 : 몽타주에서 현재 재생 시간을 사용해서 현재 재생중인 시퀀스를 찾는 것은 고작 몽타주 속 시퀀스 개수만큼만 for문을 돌리면 됩니다.

몽타주 단위의 경우 : 메타데이터 인덱스가 배로 늘어나지만, 시간 데이터가 정렬되어 있으니 이진탐색을 사용하면 최악의 경우에도 시퀀스 단위보다 계산이 횟수가 적습니다.

시퀀스 개수가 많지 않기 때문에 둘 사이에 성능 차이는 크게 없다고 판단했습니다.

그래서 Anim Sequence 단위로 메타데이터를 추출하는 것을 구현했고, 성공했습니다.

(이후 몽타주 단위로도 추출하고 추가로 깨달은 것은, 몽타주 단위로 하면 메타데이터 크기가 커져서 에디터에서 다룰 때 렉이 걸려 불편합니다.)


몽타주 단위로 메타데이터를 추출하는 것도 해 보고 싶어서 해 봤습니다.

몽타주는 애님 블루프린트의 슬롯이 없으면 재생이 안 된다고 알고 있었기에 시퀀스랑 다르게 캐릭터도 스폰하고 애님 블루프린트도 적용해 줬습니다.

그럼에도 Anim Sequence로 추출했을 때의 로직을 사용했는데, 모든 변환 값이 똑같이 나오는 문제가 발생했습니다.


TickAnimation()을 호출했는데도 불구하고, Transform 계산 로직이 진행이 안 된 것 같아서

TickAnimation() 내부 함수들 외에, 애님 인스턴스나 몽타주만의 Bone Transform 값을 업데이트하는 다른 로직이 있는지 찾아봤지만 찾지 못 했습니다.


이 문제의 원인과 최종 해결법은…
TickAnimation()에 DeltaTime으로 0.f를 넣어준 것이 문제였습니다. 어쩌다 혹시몰라 다른 값을 넣어줬더니 해결됐습니다.

이미 애니메이션 시간은 직접 설정했으니, 내부 로직만 돌리면 된다고 생각해서 0.f를 넣었는데… 엔진 코드를 더 뜯어보지는 않았지만 dirtyFlag 등 뭔가 이유가 있는 것 같습니다.

Tick에서 관련 로직들이 다 실행이 될 것이기 때문에, 그냥 몽타주를 Pause하거나 재생 속도를 0으로 하고 Tick을 돌리면 되는 것이었습니다.
TickAnimation() 함수에 DeltaTime을 넣어도, 몽타주는 Pause 혹은 재생 속도가 0이라서 문제 없기 때문에 Tick 로직들을 호출하는 용도로 사용할 수 있는 것입니다.


TickAnimation()을 호출하면서 생긴 추가적인 문제가 있었는데, 노티파이가 호출되면서 크래시가 나는 것입니다.

이를 해결하기 위해,
애니메이션 사이에 메타데이터를 Copy 할 수 있는 기능을 만들어서
노티파이가 있는 경우에는, 노티파이 없는 버전으로 추출해서 노티파이 있는 버전으로 복사할 수 있게 만들었습니다.


에디터 툴

katana

코드를 모르는 팀원도 쉽게 추출하고, 빠르게 추출해서 실험할 수 있게 구현한 추출 함수를 호출하는 에디터 툴을 만들기로 했습니다.

에디터 유틸리티 위젯을 사용해서 쉽게 만들 수 있었습니다.

시퀀스나 몽타주를 선택하고, Skeletal Mesh를 선택하고, Bone 이름을 입력하고 추출하면 됩니다.

추가로, 위에 적어둔 메타데이터 복사 기능도 넣어뒀습니다.


밑에는 설명 없는 시연 영상입니다.


최종 결과

추출한 메타데이터를 사용해서 간단하게 궤적 보정을 해 보니,
(파란 디버그 점이 메타데이터를 사용해서 얻은 점입니다.)

katana

일반적인 공격 속도에서는 아무런 문제가 없는데,

katana

빠른 공격에서는 첫 부분이 궤적이 뭔가 이상해 졌습니다.

katana

더 편하게 문제를 찾기 위해서 Tick 때 계산되는 소켓 위치들도 찍어보니, 메타데이터 문제가 아님을 쉽게 알 수 있었습니다.

이건 Blend 때문이었습니다. 실제 게임에서는 애니메이션이 Blend 되면서 칼의 궤적이 변하는 것이었습니다.

katana

블렌드를 아예 없애거나, 혹은 이건 부자연스러울 수 있으니 블렌드 시간을 좀 줄이거나 Exponential Out과 같이 초반에 빠르게 Blend하는 Curve를 사용하는 것으로 해결이 가능했습니다.


최최종 결과

katana

일반 속도

katana

재생 속도 2배


죄종적으로, Tick 사이에 칼이 얼마나 이동하든지 상관없이 설정한 값보다 많이 이동하면 그 사이에 칼의 궤적 위의 점을 추가해서

궤적을 유지하면서 판정을 촘촘하게 할 수 있게 됐습니다.


콤보 구현

특정 타이밍에 키를 누르고 있어야 콤보가 진행되어야 한다는 기획에 맞춰서, 키를 누른 상태를 bool 값으로 저장하고 노티파이 시 이를 사용해 콤보 진행 유무를 결정했습니다.


멀티 플레이어 구현 (리슨 서버)

RPC를 사용해서 모든 플레이어에서 공격 애니메이션이 재생되고, 콤보 체크 및 판정 구현은 서버에서만 진행되게 구현했습니다.

Leave a comment