서론
기획하는 내용에 관해 하나하나 적고 진행하기에는 뭔가 그때 그때 나아보이는 방향성이 다를 때가 있어서 진행이 막힐 때
먼저 적고 → 개발을 하고, 지금처럼 영감이 넘칠 때는 개발 → 정리를 하도록 하자. 아무튼, ISKM은 5.6? 5.7? 부터 Experimental에 있었다고 듣긴 했는데, 그나마 안정적인 실험 버전이 5.8인거 같아서 5.8로 버전을 업데이트 했다. ISM과 VAT는 내가 프로젝트 버전에 종속된것도 아닌데 너무 억지로 구형 기능을 이용하는 것 같아서, 최신기능을 이용해야지. 그건 이제 개발을 시작한 내용이고, 오늘은 이전 아티클에서 작성했던 히트 시스템 이야기.히트 시스템

글씨체가 살짝 난감하긴 한데, 전체적인 흐름은 이렇다. ① 플레이어의 스킬 사용 ② 스킬 시스템에서 CombatQuerySubsystem에 핸들을 열고 히트박스들을 등록한다. └ 히트박스들은
처리되지 않은 히트박스 배열에 추가된다. ③ HitQueryProcessor 에서 처리되지 않은 히트박스 를 받아간 뒤, 히트박스에 명중한 Entity들을 대상으로 피격된 히트박스의 핸들값을 기록한다. ④ HitResolveProcessor 에서 피격된 히트박스가 있는 엔티티들을 대상으로, 피격된 히트박스의 핸들에 대한 HitSpec(데미지 등) 정보를 CombatQuerySubsystem에서 받아온다. ⑤ HitSpec에 엔티티 정보를 넣어 피해를 처리한다. (HitSpec은 피해 처리를 위한 기반클래스로 여러 파생클래스가 있다.) ⑥ 피해 처리가 종료되면, 처리한 핸들에 대한 반환을 시도한다.※ 핸들은 최대 64개 까지 등록될 수 있다. uint64 비트플래그 방식을 사용하고 있기 때문인데... 나중에 어느정도 구현되고 스트레스테스트에서 최대 필요 핸들 개수 견적이 나오면 수정이 필요하다.※ 핸들 종료는 "스킬 시스템의 핸들 종료 요청" 과 HitQuery의 종료요청(명중된 적이 0인 경우) 또는 HitResolve의 종료 요청(명중된 적에 대한 피해 처리가 종료된 경우) 가 모두 만족하는 경우에 점유된 핸들이 반환된다.작업 스레드 이관
기본적으로 Subsystem은 게임시스템에서 돌아가는데, 피해처리 작업은 사실 스킬시스템에서 쿼리를 등록하는 것만 게임 스레드에서 동작하면 되고, 나머지 피해처리 등은 EntityQuery 순회나 HashGrid를 이용한 거리체크, Fragment의 값 수정 등 UObject와 연관이 없는 동작들을 수행한다. 따라서 작업 주체를 워커로 이동시키면 좋겠다는 생각을 했다.
template <>struct TMassExternalSubsystemTraits<UMassCombatQuerySubsystem>{ enum { GameThreadOnly = false, ThreadSafeWrite = true };};서브시스템의 경우 기본적으로 게임스레드에서 실행되므로, 트레잇을 이용하여 GameThreadOnly를 꺼줄 수 있다. 이후
FThreadManager::GetThreadName(FPlatformTLS::GetCurrentThreadId())를 사용해서 스레드 이름을 확인해보면 Foreground Worker랑 Background Worker를 확인할 수 있다. 
매크로를 이용한 작업 단순화
프로세서에서는 EntityQuery에 대해서 AddRequirement 등을 통해서 엔티티쿼리가 탐색할 프래그먼트 조건을 설정하게 되어있다. 문제는 이게 보기에 좀 정렬이 안맞아서 지저분하고 읽기가 좀 힘들다.

끼리끼리 묶고 정렬을 맞춘다고 쳐도, ReadOnly, ReadWrite, All.. 같은 열거형 값도 있고, 구조체들 이름도 비슷비슷하고 서로 붙어있으니 읽기도 뭐하고 해서 매크로로 묶었다.


단점은 이제
DEFINE_FRAGMENT_R/RW같은 매크로는 F##VarName으로 템플릿 클래스 이름을 작성하기 때문에, 실제 매크로 내에는 아래처럼 F를 떼고 작성을 해야하는 것이다.
UMassCombatQuerySubsystem처럼 자신 + HitQueryProcessor, HitResolveProcessor, 스킬시스템 등 여러 시스템에서 접근하면서 워커스레드에서도 처리하는 곳은 FScopeLock 같은걸로 RAII 규칙에 따른 락을 걸어서 데이터를 보호한다. 근데, ScopeLock의 범위를 한정하려고 { }를 이용해서 블럭을 짜고, ScopeLock 여러개가 겹치면 변수 이름도 바꾸고 하다보니 아.. 이것도 뭔가 좀 코드가 못생겨진다.

이제 여기도 문제점이 있다면, 구조적으로 락을 거는 FScopeLock 바깥에 대괄호가 있어야하므로 탭보간을 직접해줘야하고, IDE에서 정렬해버리면 매크로랑 코드의 탭 구간이 같아지는 것이다. 근데 사실 그래도 뭐 상관은 없어서 그냥 쓰고 있다. (위 이미지에서도
LOCK_SUBSYSTEM 아래의 if문만 탭 한 칸 띄워져있고 아래는 당겨진걸 볼 수 있다.※ 굳이 하려면 그냥 LOCK_? { /** Codes */ } UNLOCK_ 방식으로 하면 되긴 할 것 같은데. 가독성에 그렇게 큰 차이는 없을 것 같다. 어차피 매크로에 대한 하이라이트 글자색도 달라서 충분히 구분되는 느낌이라...미처리 쿼리 요청


MoveTemp로
TArray&&로 반환해서 HitQueryProcessor에서 대입연산자로 받으면, MoveOrCopy가 실행되면서 소유권이 이전된다. 그래서 따로 서브시스템에서 배열 초기화를 하거나 배열 내 HitQuery에 대한 정보를 복사할 필요 없이 그냥 배열채로 넘겨버릴 수 있다. 피격 처리는 UMassNavigationSubsystem의 FNavigationObstacleHashGrid2D를 받아온 다음 여기서 쿼리한다. 
함수의 주석에도 써져있듯이, 후보군을 추측하는 것이므로 이걸 그대로 피격 판정으로는 사용할 수 없다. 어차피 대규모 상황에서 쓰는건데 false positive 좀 있어도 되지... 하고 따로 처리 안하고 써봤는데 상당히 많이 발생한다. 피격 히트박스가 걸치는 모든 그리드의 적을 가져오는 듯? 확인해보지는 않아서 모르겠다.
const FVector EntityLocalPosition = Query.Rotation.UnrotateVector( Transform->GetTransform().GetLocation() - Query.Box.GetCenter());const FVector BoxExtent = Query.Box.GetExtent();const double DistX = FMath::Max(FMath::Abs(EntityLocalPosition.X) - BoxExtent.X, 0.0f);const double DistY = FMath::Max(FMath::Abs(EntityLocalPosition.Y) - BoxExtent.Y, 0.0f); bool bIsHit = (DistX * DistX) + (DistY * DistY) <= AgentRadius->Radius * AgentRadius->Radius;피격 판정은 대충 이런데 (2D)① 엔티티의 위치 정보를 월드좌표 → 히트박스에 대한 상대좌표 로 변환② 엔티티가 박스 경계와 얼마나 가까이 있는지 X,Y축 따로 체크 └ 각 축마다 박스 내부에 있는 경우(음수 = 0으로 clamp), 외부에 있는 경우 (양수)③ 각 축의 거리 값으로 대각선 길이를 구해서 엔티티의 반지름과 비교함 └ 거리가 반지름보다 멀면 히트박스 밖, 가까우면 히트박스에 걸치거나 안에 있음.

히트된 경우, 엔티티가 피격된 히트박스의 핸들 번호와 일치하는 플래그 켜기
피해 처리
FMassEntityAilment는 피해처리를 총괄하는 구조체다. 아직 완전히 구현은 하지 않은 상태인데, 나중에 피해 종류에 따라 파생클래스(파생구조체?)를 만들어서 구현해야한다. 왜 구조체냐하면... DataTable로 관리하고 싶어서 이다. TInstancedStruct<T>를 알게 된 지 얼마지나지 않아서 너무 쓰고 싶었다. 그리고 너무 말이 안되는 구조도 아닌 것 같고? 간단하게 이 구조체에 대해 소개하자면...
이렇게 기본적인 데미지값, 지속시간, 일시데미지 여부 를 설정가능하고,

스탯이나 상태이상 등으로 기본 설정값을 조정하려면? Multiplier를 전달해서 값을 추가 수정할 수 있다. 또한 파생 클래스에서 이를 상속해서 추가적인 프로퍼티와 키값(열거형)을 이용해 다양하게 수치 커스텀을 할 수 있는 배경을 준비했다.

Ailment를 실행할때는 이 Activate 함수를 이용해서 실행시킨다.

파생 구조체들은 이 가상함수를 오버라이딩해서 실제 피해를 구현한다.

간단하게 그냥 피해를 입히는 Ailment의 예시이다.

서브시스템에 핸들 열기를 요청할 때, 이렇게 피해 타입에 대한 컨테이너(Ailment와 파라미터배율이 작성된 구조체)를 전달한다.

HitResolveProcessor에서 서브시스템의 Ailment 컨테이너를 복사해서 가져온 뒤, 계산하여 캐싱한다. 그리고 그 값을 이번 틱의 처리에서 쭉 사용한다. AilmentPtr->Activate로 가면 위의 구조체 실행과 같다.
결론
Ailment의 상세구현이 아직 미완료 상태이지만, 기본적인
피해 요청 → 쿼리 → 피해 처리 의 구조는 어느정도 작성이 완료되었다. Ailment에 대한 상세 구현은 뒤쪽으로 미뤄둔 뒤 아마 ISKM, AnimBank를 이용한 엔티티 애니메이션 구현을 먼저 한 뒤, 그 뒤에 Ailment 구현을 진행할 것이다.