26년 9월
2주차
이즈나 섬광탄을 맞은 상태에서 한번 더 맞을 경우 크래시 발생 수정, Statistic 복구 안되는 현상 수정
문제
- 이즈나가 섬광에 연속 피격되면 이동 속도가 정상으로 복구되지 않고, 섬광 GamplayEffect 타이머 만료 후 다시 피격될 때 크래시가 발생
원인
- 중첩 시 새 GameplayEffect를 적용하고 타이머도 바꿨지만, 활성 목록에는 이전 GameplayEffect가 남았다.
타이머 만료 시 새 GameplayEffect만 해제하고 스택 정보를 삭제하면서 이전 GameplayEffect가 남았고, 다음 피격에서 삭제된 새 GameplayEffect 스택 정보를 조회해 크래시가 발생했다.
수정
- 같은 EffectIdentifier일 때, 새로운 GameplayEffect를 등록하지 않고, 중첩하도록 변경
- EffectIdentifier를 키로 하는 TMap으로 활성 효과를 관리
고민한 내용
- GameplayEffect 객체를 매번 교체하기보다 식별자별 객체 하나에 적용과 해제 책임을 모아 관리 상태가 어긋나지 않도록 했다.
-
같은 EffectIdentifier의 GameplayEffect가 여러 번 적용되면 능력치가 바뀐 내역을 모두 보관하고, GameplayEffect 종료 시누적된 변경을 전부 되돌리도록 변경
호시노 방패가 화면 전환을 따라가지 못하는 현상 수정
문제
- 호시노가 방패를 장착하고 방향을 돌리면, 다른 플레이어 화면에서 방패가 캐릭터의 손과 어긋났다.
원인
- 캐릭터와 장비가 RootYawOffset을 독립적으로 누적해서 계산하는데, 캐릭터와 달리 장비 애니메이션 21개에는 보정값을 해소하는 커브가 없어, 캐릭터와 최대 관측 각도 차이 178.76°가 발생했다.
- 해당 커브는 제자리 회전에서 일정 각도를 화면 전환해야 캐릭터가 회전하는 임계값을 나타내는 커브
수정
- 장비의 독립 계산을 제거하고, 부착된 3인칭 캐릭터의 보정값을 공유하도록 변경했다.
- 1인칭이나 유효한 원본이 없는 경우에는 0으로 초기화한다.
고민한 내용
- 선택 근거: 장비에 회전 커브와 상태를 복제하면 두 계산을 계속 맞춰야 한다. 회전 결정은 캐릭터가 소유하고, 장비는 결과만 받아 포즈에 적용하도록 책임을 정리했다.
- 보정값 불일치와 방패 커브 부재를 확인했다.
1주차
장비 교체시 쿨타임이 초기화되는 현상 수정
문제
- 스킬 사용 후 쿨타임 중 장비를 빠르게 교체하면 즉시 재사용할 수 있었다.
- 장비 교체 중에는 쿨타임을 유지하고, 리스폰 시에만 초기화해야 했다.
원인
- 장비 재활성화 시 캐시된 어빌리티를 재등록하면서 InitAbility()가 진행 중인 쿨타임 타이머를 삭제하고 있었다.
수정
- 재등록 시 타이머를 유지하고 UI에 남은 시간을 전달하도록 변경했다.
- 리스폰 시에는 서버가 비활성 장비를 포함한 캐시 전체의 쿨타임과 충전 상태를 초기화하고 소유 클라이언트에 반영하도록 했다.
고민한 내용
- 리스폰에 따른 초기화 요청은 어빌리티 생명주기를 관리하는 컴포넌트가 맡고, 어빌리티는 초기화 요청을 수행하도록 책임을 분리했다.
LoadMap으로 월드를 전환하거나 엔진을 종료할 때, 남아 있던 오브젝트를 정리하는 과정에서 크래시가 발생
원인
장비 정리를 OnComponentDestroyed()에서 시작해 소유 캐릭터와 월드 상태가 BeginDestroy 플래그가 설정됐거나 정리된 뒤 Deactivate()가 호출될 수 있었다. 이때 무효화된 월드나 소유 캐릭터를 검사하지 않고 참조한 것이 직접적인 크래시 원인
수정
- 장비 정리를 OnComponentDestroyed()에서 EndPlay()로 옮겨 LevelTransition과 정상 종료의 Quit 경로에서 파괴 판정 전에 먼저 실행되도록 했다.
- World, 소유 캐릭터, 총기의 유효성 검사를 추가
고민한 내용
- null 검사만 적용하면 크래시는 피할 수 있지만 장비 상태 정리가 누락될 수 있어, RouteEndPlay() 단계에서 비활성화와 파괴를 시작하도록 수명주기를 변경했다. 다만 EndPlay()는 GC 콜백이 아니며 강제 종료와 최종 UObject 파괴 단계에는 호출이 보장되지 않으므로, 파괴 과정에서는 월드 의존적인 코드를 수행하지 않도록 방어 코드도 함께 추가
26년 8월
4주차
슬라이딩 중 점프 입력시 점프되도록 변경
기획의 요청에 따라 점프되도록 변경 진행
수정
- 슬라이딩을 Walk로 전환한 뒤 점프를 예약하고, UpdateCharacterStateAfterMovement()에서 실제 점프를 요청하도록 변경
- 기립할 공간이 없거나 실제 캡슐이 여전히 웅크린 상태라면 Crouch로 복구
- 크라우치 해제·슬라이딩 종료·점프 처리에 중복되어 있던 CanStand() 분기를 제거하고, ValidateActionState()가 기립할 수 없는 Walk 전이를 Crouch로 보정하도록 통합하여 중복 코드 제거
고민한 내용
- 캐릭터 무브먼트의 DoJump()를 직접 호출하면 점프 횟수와 홀드 처리 같은 엔진 규칙을 우회하므로, 한 번의 이동 갱신 지연을 감수하고 기존 Jump() 경로를 유지
시행착오
처음에는 예약 점프를 UpdateCharacterStateBeforeMovement()에서 소비했지만, 여기서 설정한 입력이 같은 프레임의 ClearJumpInput()에 제거되어 액션 상태만 Jumping으로 남고 VelocityZ=0, IsFalling=false가 유지되어 점프 무브먼트가 되지 않음
AnimBP Main 그래프 모듈화
문제
캐릭터별 1인칭 AnimBP에 Main 상태 머신이 각각 들어 있어 공통 전이 수정이 중복
수정
- Main 스테이트 머신을 AT_Character_1P_MainModule Linked Anim Graph로 분리하고 캐릭터별 AnimBP가 공유하도록 변경
고민한 내용
공유하는 AnimBP 상속 방식도 검토했다. 하지만 상속은 Main만 선택적으로 가져오는 것이 아니라 전체 AnimGraph를 묶어 캐릭터별 모든 그래프와 Skeleton 구성을 제약하므로, 기존 AnimBP는 유지하면서 Main만 Template Linked Anim Graph로 호출하는 방식을 선택
3주차
충전형 어빌리티의 카운트 관련을 컴포넌트로 리팩토링
문제
이즈나 대시 어빌리티가 충전 횟수, 재충전 타이머, 클라이언트 동기화와 UI 알림까지 직접 관리해 다른 충전형 어빌리티에서 재사용하기 어려워 확장성이 떨어짐
원인
진짜 문제는 충전 규칙이 공통 어빌리티 구조가 아니라 MFGAIzunaDashMove에 종속되어 있었다는 점이다.
기본 AMFAbility도 단일 쿨다운만 처리해 충전이 남은 상태를 표현하지 못했다.
수정
MFRechargeComponent를 추가해 서버 권한의 횟수 소비·복구와 클라이언트 상태 전달을 담당하게 했다.
AMFAbility는 컴포넌트가 있으면 활성화 가능 횟수와 재충전 시간을 사용
비충전형 어빌리티의 기존 쿨다운 경로는 유지
받는 피해 배율 통계치 구현
문제
- 기존 UMFSSReduceDamage는 피해 감소만 표현할 수 있어, 받는 피해 증가와 고정 피해 감소를 하나의 통계 체계에서 처리하기 어려웠다.
- 피해 계산 책임도 MFDamageHandlerComponent에 섞여 있었다.
원인
- 기존 통계가 감소율 중심으로 설계됐고, Handler가 비율 및 고정 감소 공식을 직접 계산하고 있었다. 이 구조에서는 피해 증가나 DamageType별 보정 우회 정책을 확장하기 어려웠다.
수정
- UMFSSReduceDamage를 UMFSSIncomingDamageModifier로 교체하고 IncomingDamagePercentModifier와 FlatDamageReduction을 추가했다.
- 비율 보정은 RawDamage × (1 + Percent / 100)으로 계산하며, 하한만 -100으로 제한하고 양수 증가에는 상한을 두지 않았다.
- 계산 책임을 StatisticSet으로 이동하고, Handler는 DamageType 정책 확인과 계산 호출만 담당하도록 정리했다.
- 즉사 DamageType은 보정을 우회하며, 미등록 StatisticSet을 참조하는 GameplayEffect는 경고를 남기고 해당 Modifier만 건너뛰도록 수정했다.
- 모든 캐릭터에 강제 등록하지 않고 Blueprint별 opt-in 구조를 유지했으며, 기존 BP는 새 퍼센트 단위에 맞게 마이그레이션했다.
서버에서 장비 교체 어빌리티 UI가 활성화되지 않는 현상 수정
원인
- 서버에서 장비 교체 어빌리티는 실행되지만, 활성화 알림이 정상 전달되지 않아 UI가 활성화되지 않았다.
- Activate() 도중 장비를 교체하고 장비의 어빌리티를 변경하면서 RegisteredAbilities 배열이 변경됐다. 이때 기존 AbilityContainer 포인터가 무효화됐지만, 실행 후 다시 참조해 어빌리티 태그를 가져오고 있었다.
수정
- Activate() 호출 전에 AMFAbility*와 FGameplayTag를 복사하도록 변경했다. 컨테이너가 무효화되더라도 보존한 태그로 ClientNotifyActivateAbility()와 Server_NotifyActivateAbility()를 호출한다.
고민한 내용
어빌리티 실행 후 컨테이너를 다시 조회하는 방법도 있지만, 활성화 과정에서 배열 상태가 달라질 수 있어 실행 대상과 태그를 미리 보존하는 방식을 선택
호시노 권총 탄창 UI에 업데이트 안되는 버그 수정
원인
- UI는 장비의 AvailableCountType을 검사한 뒤, 유효한 경우에만 탄약 텍스트를 변경한다. 호시노 권총·방패 에셋에는 이 설정이 없어 기본값 Invalid로 처리되면서 갱신 분기를 통과하지 못했다.
수정
- BP_DWHoshino_Pistol_With_Shield의 AvailableCountType을 Available로 설정했다.
- MFGAEquipEquipment에 bShowAvailableCount를 추가하고 MFA_SwapMod에 적용해, 무기 슬롯 UI와 별개인 어빌리티 UI에는 불필요한 장비 수량이 표시되지 않도록 분리했다.
고민한 내용
탄창 갱신 이벤트나 UI 그래프를 새로 만드는 방식은 사용하지 않았다.
2주차
슬라이딩 도중 벽타기를 시도한 뒤 빠르게 종료하면, 평지에서도 1인칭 벽타기 애니메이션이 계속 재생되는 현상 수정
원인
- 벽타기 상태 진입이 거부된 뒤에도 어빌리티가 성공 여부를 확인하지 않고 몽타주를 재생했으며, 실제 Climbing 상태가 아니어서 Climbing 종료 델리게이트도 발생하지 않음
수정
- 기획 의도에 맞춰 지상에서는 Walk/Sprint일 때만, 공중에서는 이동 상태와 관계없이 벽타기에 진입하도록 기존 가드를 수정
- Climbing()이 상태 전환 성공 여부를 반환하고, 진입 실패 시 벽타기 어빌리티에서 몽타주를 취소한 뒤 재생하지 않도록 변경
고민한 내용
단순히 bWantsToCrouch만 확인하면 슬라이딩·앉기 상태로 공중에 진입한 경우까지 벽타기가 차단된다. 따라서 지상은 액션 상태로 제한하고, 공중은 IsFalling()으로 별도 판정해 기획 의도를 명확히 표현
연막이 클라에서 폭발 반응이 안되는 현상 수정
원인
폭발 반응이 서버의 렌더 타깃과 동적 머티리얼에만 적용됐으며, 연막 액터와 시각 효과 변경이 클라이언트로 전파되지 않았다.
수정
- 연막 액터의 리플리케이션을 활성화했다.
- 서버에서 폭발 위치와 구멍 반지름을 계산한 뒤 Reliable NetMulticast RPC를 호출해 모든 클라이언트가 각자의 머티리얼과 렌더 타깃에 동일한 구멍을 그리도록 변경했다.
- 폭발 유효성 및 구멍 크기 계산은 서버 권한으로 제한해 판정이 중복되거나 달라지지 않게 했다.
연막의 복셀 마스크와 차폐 판정 개선
연막은 구 모양의 볼륨을 그리는 것만으로 끝나지 않는다. 벽을 만나면 반대편으로 퍼지지 않아야 하고, 실제 연기가 닿은 플레이어만 차폐 상태로 판정해야 한다.
문제
연막은 BFS로 장애물을 피해 복셀을 채우고 있었지만, 실제 결과에는 세 가지 문제가 있었다.
- 일단 벽이나 바닥처럼 장애물이 있는 곳에서 연기가 지오메트리 안으로 퍼지거나, 반대로 표면과 연막 사이에 빈 틈이 생겼다.
- BFS가 찾아낸 복셀마다 머터리얼 파라미터를 바꾸고 `DrawMaterialToRenderTarget`를 호출했다. 연막 하나의 마스크를 만들기 위해 수천 번의 렌더 타깃 갱신이 여러 프레임에 걸쳐 발생했고, 여러 연막을 동시에 생성하면 렌더 스레드와 GPU 부하가 한꺼번에 증가했다.
- 화면과 게임플레이 판정이 서로 다른 정보를 사용한다는 점이었다. 화면은 BFS 결과에 따라 벽에서 막혔지만 플레이어 차폐 판정은 연막 전체의 SphereCollision만 확인했다. 벽 뒤라서 실제 연기가 도달하지 않은 플레이어도 구형 콜리전 안에 있으면 차폐된 것으로 처리될 수 있었다.
원인
- BFS로 판정 후 복셀 하나마다 렌더 타깃을 다시 그림
BFS 결과를 그대로 볼륨 텍스처로 만들었다
기존 구조는 BFS 결과를 CPU 배열로 유지하지 않고 각 복셀을 2D Vanish 렌더 타깃에 구 형태로 누적했다. 복셀 수가 늘어나면 `DrawMaterialToRenderTarget` 호출 수도 그대로 증가하여 여러 프레임에 걸쳐 렌더 스레드와 GPU 작업을 계속 만들었다.
한 프레임에 처리할 복셀 수를 나눠 순간 부하는 분산했지만 총작업량은 줄지 않았다. 오히려 연막 마스크가 완성될 때까지 같은 렌더 경로가 여러 프레임 동안 유지됐다. - 차폐 판정이 BFS 결과를 확인하지 않았다
기존 `OnBeginOverlap`은 적 플레이어가 구형 콜리전에 들어오고 일정 시간이 지나면 바로 `OverlapCount`를 증가시켰다. 렌더링에서는 비어 있는 벽 뒤 공간과 실제 연기가 채워진 공간을 게임플레이 판정에서는 구분하지 못했다.
수정
- BFS 결과를 볼륨 텍스처로 만들었다
BFS가 채운 셀을 `TBitArray`인 `FilledGrid`에 기록한다. BFS가 끝나면 이 배열로 `PF_G8` 볼륨 텍스처를 한 번 생성한다.
머터리얼은 생성된 볼륨 텍스처를 직접 샘플링한다. 이에 따라 복셀별 Vanish 렌더 타깃, 동적 머터리얼과 드로우 스케줄러를 제거했다.
격자 좌표는 인덱스가 증가할수록 로컬 음의 방향으로 이동하지만 볼륨 UV의 0은 로컬 최소점이다. 마스크가 반대로 기록되지 않도록 세 축을 모두 다음과 같이 뒤집었다. 여기서 좌표축을 그대로 복사하면 마스크가 반대로 나왔다. 격자 좌표는 월드 변환에서 인덱스가 커질수록 로컬 음의 방향으로 이동하지만 볼륨 UV의 0은 로컬 최소점이기 때문이다. 따라서 X·Y·Z를 모두 `Size - 1 - Grid`로 뒤집어 기록했다.
텍스처 주소 모드도 기본 `Wrap` 대신 `Clamp`로 지정했다. 그렇지 않으면 경계 밖 샘플이 반대편 마스크를 끌어와 연막 끝에 잘못된 밀도를 만든다. - 화면과 판정이 달랐다는 점이었다
렌더링만 고치면 벽 뒤로 연기가 보이는 문제는 해결된다. 그런데 플레이어 차폐 판정은 여전히 연막 전체의 `SphereCollision` 진입 여부만 사용하고 있었다. 화면에는 벽 때문에 연기가 도달하지 않았는데도 구 안에 있다는 이유만으로 적을 가린 것으로 계산할 수 있었다. - 실제로 채워진 복셀에서만 차폐로 판정했다
이제 구형 콜리전은 후보를 찾는 브로드페이즈로만 쓴다. 오버랩 유지 시간이 지나면 플레이어의 월드 위치를 복셀 인덱스로 바꾸고 `FilledGrid`를 확인한다. 실제 BFS가 채운 복셀일 때만 `OverlapCount`를 올린다. 아직 연기가 닿지 않는 위치라면 타이머를 다시 걸어 다음 임계 구간에 재검사한다.구형 범위 안에 있더라도 벽 때문에 연기가 도달하지 않은 셀이면 타이머를 다시 걸고 다음 임계 구간에 재검사한다.
Unreal Insights Trace 측정
연막 10개를 5열로 한 번에 생성하고, 같은 코드 경로의 호출 수와 누적 시간을 확인했다.
| 항목 | 개선 전 | 개선 후 | 변화 |
| `SpawnSmokeBatch` GameThread 구간 | 162.263 ms | 57.890 ms | 104.373 ms 감소, **64.3% 감소** |
| `DrawMaterialToRenderTarget` 호출 | 12,140회 | 40회 | 12,100회 감소, **99.67% 감소** |
| `DrawMaterialToRenderTarget` 누적 CPU 시간 | 721.022 ms | 10.378 ms | 710.645 ms 감소, **98.56% 감소** |
| GPU `CanvasDrawTiles` 호출 | 1,390회 | 110회 | 1,280회 감소, **92.1% 감소** |
| GPU `CanvasDrawTiles` 누적 시간 | 153.915 ms | 10.761 ms | 143.154 ms 감소, **93.0% 감소** |
발밑 수류탄 자폭 반동이 적게 Launch되는 현상 수정
원인
- 관통 해소 가드가 런치를 되돌렸다. 발밑 폭발은 캐릭터 캡슐이 발사체와 겹친 상태에서 일어나므로 같은 프레임에 관통 해소가 돈다. 그러면 PenetrationVelocityGuardFrames가 무장되고, 프레임 끝의 OnMovementUpdated가 속도 급증을 관통 역산으로 오인해 Velocity = OldVelocity로 되돌렸다.
- 수류탄 발사체가 캐릭터의 베이스로 잡혔다.
발사체는 폭발과 같은 프레임에 파괴되는데, 그 프레임의 클라 위치 보정이 이미 사라진 컴포넌트를 NewBase로 참조하게 되어 ClientAdjustPosition이 보정을 통째로 폐기했다.
수정
- HandlePendingLaunch 오버라이드로 런치 프레임을 표시하고, OnMovementUpdated에서 그 프레임의 관통 보정을 건너뜀
- 가드 카운터 감소는 런치 여부와 무관하게 수행한다. 조기 반환으로 건너뛰면 가드가 한 프레임 더 살아남아, 런치 다음 프레임의 착지 충격·2차 관통 해소가 되돌림 대상이 된다. 지키려던 넉백이 결국 다시 지워진다.
- AMFSOProjectileMovement::CanBeBaseForCharacter가 false를 반환한다. 발사체는 애초에 캐릭터가 딛고 설 대상이 아니다.
시로코 점프 중 수류탄 자폭 반동 미적용 수정
원인
- 투척물이 던진 사람과 충돌할 때의 처리 경로가 두 갈래(`NotifyHit` / `OnProjectileOverlap`)로 나뉘어 있었고, 자기 자신 제외 로직이 Overlap 쪽에만 있었다.
- IgnoreActorWhenMoving은 자기 액터가 스윕할 때만 소비되는 단방향 설정이라, 다른 액터가 움직여 오버랩되는 경우(점프 낙하 등)를 막지 못한다.
이때 엔진이 bSelfMoved=false로 NotifyHit을 보내고, 투척물이 시전자 몸에 붙어 멈추거나 즉시 터지면서 속도 Z 값이 의도와 다른 반대 방향이었고 의도한 자폭 반동이 되지 않음
해결
- Overlap과 Hit이 동일한 조건을 체크하도록 동일한 함수 사용 : 유효성, 자기 자신, Owner, Instigator를 한 곳에서 걸러낸다. 파생 클래스가 조건을 완화하지 못하도록 비가상으로 둠
- Overlap과 Hit은 가드만 수행하고, 실제 처리는 `HandleBlockingHit` / `HandleOverlap` 훅에 위임하도록 정리. `NotifyHit`은 `final` 처리하여 파생 클래스도 같은 조건을 검사하도록 변경
26년 7월
5주차
아군 캐릭터에 쏠 때 이펙트가 발생하는 현상 수정
문제
아군에게 사격하면 데미지/디버프는 안 들어가는데 히트 이펙트(피격 파티클, 탄흔)는 그대로 재생돼서, 맞은 것처럼 보이는 오인 피드백이 발생했다.
원인
팀 판정이 이펙트 경로에 없었다.
어빌리티들이 각자 IsSameTeam()을 중복 구현해 어빌리티 단계에서만 걸렀고, 실제 이펙트를 스폰하는 곳에서는 팀을 전혀 보지 않았다.
델리게이트도 AActor*만 넘겨서 소비자마다 Cast와 팀 계산을 반복하는 구조였다.
해결
- FMFShotHitResult 구조체 신설 하고 한 발이 무엇을 맞혔는지(HitActor/HitCharacter/ImpactPoint/BodyPart/오브젝트 타입/아군 여부)를 트레이스 직후 한 번만 확정하여 저장
- OnHitActor(AActor*) → OnShotHit(const FMFShotHitResult&)로 델리게이트 교체하여 데미지·이펙트·어빌리티가 같은 결과를 공유
- 팀 판정을 UMFGameplayStatics::IsSameTeam(Instigator, HitActor)로 일원화하고 두 어빌리티의 중복 IsSameTeam() 제거.
- 아군 히트 시 모든 연출을 생략하여 배경 탄흔까지 막는 게 의도 — 탄흔만 찍히면 아군을 관통한 것처럼 보이기 때문
- 레이캐스트 발사체는 머신에서 로컬 생성되므로 FMFShotHitResult는 리플리케이트하지 않음
클라에서 호시노 방패 돌진이 느린 현상 수정
원인
- 돌진 전진 속도를 서버 어빌리티 Tick에서 직접 Movement->Velocity에 꽂고 있었다.
클라는 이 코드를 타지 않으므로 로컬에서는 그냥 걷기 속도로 움직였고, 매 프레임 서버 위치 보정에 끌려다니는 모양이 됐다. 느린 속도 + 끊김은 같은 원인의 두 증상 - 액터 틱에서 속도를 덮어쓰는 방식 자체도 문제였다. CMC의 이동 파이프라인 바깥이라 클라 예측과 보정 후 재생(replay)이 재현할 수 없고, 지상에서는 마찰로 깎이고 공중에서는 안 깎여 점프할 때만 빨라지는 현상도 발생
수정
- 속도 결정을 CMC 안으로 옮겼다.
BeginDash/EndDash와 CalcVelocity 오버라이드를 추가해, 대시 중에는 입력·마찰·감속을 무시하고 전방으로 DashSpeed를 고정한다.
중력이 만든 Z는 보존해 낙하는 PhysFalling이 이어받는다. - 서버와 로컬 클라 모두 CMC Dash를 요청
같은 함수가 CalcVelocity 안에서 같은 값을 내므로 예측과 재생이 서버와 일치한다. - 어빌리티 쪽은 판정만 남겼다. 속도 설정 코드를 제거하고, 틱은 서버의 TraceChargeHit만 호출
틱도 bStartWithTickEnabled = false로 두고 활성/비활성에서 켜고 끈다.
메모
대시가 이동 파이프라인 안에 들어오면서 공중 상태를 CMC가 일관되게 다룬다.
Begin은 클라→서버, End는 서버→클라 순서를 전제로 CMC에 별도 RPC를 두지 않았다. 두 번 불려도 안전하다.
호시노 ADS 중 방패 돌진 시 마우스 감도 오염 수정 / 돌진 시 ADS 해제
문제
호시노 방패 돌진에서 두 가지가 얽혀 있었다.
- 감도 조정이 CachedCurrentSensitivity / SetMFCharacterSensitivity / RestoreCachedSensitivity의 단일 캐시 방식이었다. ADS 중 돌진하면 돌진이 ADS 배율이 곱해진 값을 캐시로 잡고, 해제 순서가 엇갈리면서 감도가 영구히 오염
- 돌진 중에도 ADS 상태가 유지돼 조준/돌진이 동시에 성립
해결
- 감도를 키 기반 조정자 맵으로 전환
- 캐시-복원 대신 기본값에 대해 곱셈 누적이라 해제 순서와 무관해짐
- 오브젝트의 FName을 키로 하여 재등록은 덮어쓰기
- 설정 변경(ApplyLocalUserSettings)은 캐릭터의 기본값만 갱신하고 재계산하므로, 진행 중인 ADS/돌진 배율이 날아가지 않음
- 돌진 시 ADS 해제 : 상태 태그 소유권 설계로 해결
돌진 진입 시 현재 활성 장비에 ChargeFireArmStateTag를 붙여 ADS를 막음
UMFEquipmentManager를 통해 현재 장비에 StateTag를 추가하고, 장비는 파츠에 전파하여 Sight Block 태그가 추가될 때 ADS 해제하도록 구현
호출자는 태그 소유권을 판단해 자기가 붙인 태그만 되돌림
돌진 도중 장비가 바뀌어도 떠난 장비가 스스로 태그를 비우므로 태그가 유지되지 않음
월드 종료 중 게임플레이 이벤트 브로드캐스트 차단
PIE 종료로 월드가 내려가는 도중 게임플레이 이벤트가 계속 흘러 엔진 ensure에 걸렸다. 종료 중 위젯을 새로 만드는 경로가 대표적이었다.
원인
UMFGameplayEventSubsystem::Get()은 월드가 유효하기만 하면 서브시스템을 돌려줬다. 서브시스템 자체는 월드 정리 마지막까지 살아 있으므로, 종료가 시작된 뒤에도 델리게이트가 정상적으로 브로드캐스트됐다. 이때 구독자 중에는 이미 파괴 절차에 들어간 액터·위젯이 섞여 있다.
호출부도 `Get()`의 반환을 검사하지 않고 바로 `->`로 접근하는 곳이 많아, 반환값을 null로 바꾸는 순간 그대로 크래시가 나는 상태였다.
수정
- 종료가 시작되면 null을 반환
종료 중 게임플레이 이벤트는 전부 무의미하다. 구독/해제/브로드캐스트를 한 지점에서 끊는 편이, 구독자마다 종료 여부를 따로 검사하는 것보다 확실하다.
if (!World || World->bIsTearingDown)
{
return nullptr;
}
- 호출부 전수 조사하여 nullptr 검증이 빠진 곳을 검증하도록 변경
호시노 방패 돌진 어빌리티 서버 권한 이관, 방패 돌진 적중 후 어빌리티 종료되지 않는 현상 수정
문제
방패 돌진이 적에게 적중한 뒤에도 어빌리티가 종료되지 않았음
원인
진행/종료 판정이 클라이언트 타임라인(ChargeTimeline)과 서버 RPC(Server_StartCharge, Server_SetLaunchCapsuleTrace)에 흩어져 있어서, 서버가 Deactivate()를 불러도 클라이언트 인스턴스에 전파될 경로가 없었던 것
해결 방향
어빌리티 진행·종료의 판정 주체를 서버로 단일화하고, 클라이언트는 연출만 담당하도록 역할을 갈랐다.
- AMFAbility 서버 활성화 경로 추가
bServerActivate 플래그를 추가하여 켜면 서버 인스턴스도 Activate()/Deactivate()를 거쳐 코어 워크를 실행한다.
서버 CanActivate() 실패 시 ClientDeActivate()로 클라이언트를 롤백
쿨타임 타이머 자체는 양쪽에 유지하여 서버에서 CanActivate() 검증 - AMFGASCharge 서버/클라 책임 분리
ChargeTimeline 제거, 서버 bCharging 플래그 + Tick의 OnChargeTick()으로 대체
서버 담당: BeginChargeOnServer / EndChargeOnServer(돌진 진행/종료), ApplyChargeVelocity(전진), TraceChargeHit(적·벽 판정 및 종료)
클라 담당: SetupCharge / RestoreCharge(FOV 타임라인, 감도, 입력 컨텍스트)
1인칭 메시가 벽에 파묻히는 문제 수정
원인
1인칭 메시가 월드와 같은 깊이(depth)로 그려지기 때문. 캡슐이 지오메트리에 파고드는 관통 버그가 아니라 순수 렌더링 문제다.
수정
UE 5.5의 엔진 네이티브 First Person 렌더링을 써서 1인칭 메시의 정점을 카메라 쪽으로 압축해 깊이 버퍼에서 차지하는 범위를 좁혀, 월드와 교차하지 않게 만든다.
- 카메라 위치를 원점으로 삼아 균일하게 축소하는 변환이다. 카메라에서 거리 `d`인 정점이 같은 방향의 `0.6d` 지점으로 옮겨진다.
여기서 크기와 거리가 같은 비율로 줄기 때문에 화면상 크기는 변하지 않는다 — 보이는 크기는 `크기 ÷ 거리`인데 (0.6s) / (0.6d) = s / d로 상쇄
무기가 카메라에서 30~80cm에 걸쳐 있었다면 압축 후 18~48cm가 된다. 벽이 60cm 앞에 있을 때 압축 전 총구(80cm)는 벽 뒤라 가려지지만, 압축 후(48cm)는 벽 앞이라 깊이 테스트에서 이긴다.
절대 보장은 아니다. 엔진 주석도 "씬과 교차할 가능성을 줄인다"고 쓴다. 벽이 48cm보다 가까우면 여전히 뚫린다. 그래서 `FirstPersonScale`은 튜닝값이고, 낮출수록 방지가 강해지는 대신 근평면 잘림이 심해진다(부작용 1번)
렌더 패스마다 뷰가 달라(메인컬러패스(플레이어카메라), 그림자뎁스패스(광원)) 메인 컬러 패스에만 변환이 적용되고 그림자뎁스패스에는 적용되지 않아 원래 위치로 기록되는데 셰이딩은 압축된 위치로 조회하니 어긋난다(부작용 2번)
Mesh->SetFirstPersonPrimitiveType(EFirstPersonPrimitiveType::FirstPerson);
FollowCamera->SetEnableFirstPersonScale(true);
FollowCamera->SetFirstPersonScale(0.6f);
- 플래그와 카메라 설정은 둘 다 필요하다. 카메라 쪽이 꺼져 있으면 변환 행렬이 항등이라 플래그만 줘도 아무 일도 일어나지 않는다.
- 머티리얼은 건드릴 필요 없다. 셰이더가 프리미티브 플래그만 보고 압축을 적용한다(`First Person Output` 노드는 정점별 블렌드가 필요할 때만 쓴다).
1P 메시가 팔·총·투척물로 흩어져 있어 하나라도 빠지면 그 부분만 벽에 파묻히므로, UMFRenderUtility 한 곳으로 모아 1인칭 전용 = First Person 렌더링 대상 불변식을 강제
사이드 이펙트
깊이 압축은 메시를 카메라 쪽으로 당기는 것이라 사이드 이펙트가 따라옴
- 근평면 잘림: 가장 가까운 부위(팔뿌리·개머리판)가 근평면을 넘어가 잘려 NearClipPlane을 10 → 6로 낮춰 해결
근평면을 낮추면 원거리 깊이 정밀도를 소모한다. UE는 reverse-Z 깊이 버퍼를 쓰므로(FarPlane=0, NearPlane=1) 표준 Z보다는 덜 민감하지만 공짜는 아니다. 그래서 잘림이 사라지는 선에서 가능한 한 높은 값을 택했고(3에서 시작해 6까지 올림), 원거리 오브젝트에서 z-파이팅이 나타나지 않는 것을 확인했다.
- 근평면과 원평면의 값을 뒤집어 근평면은 하나의 메시가 여러 개의 값을 사용할 확률이 높고, 원평면은 여러 개의 메시가 하나의 값을 사용할 확률이 높으므로 부동소수점 때문에 0에 가까울 수록 정밀도가 높기 때문에 Z 파이팅 확률을 상쇄
- 부동소수점 지수가 하나 정해지면, 그 구간 안에서는 가수 24비트로 값을 나눕니다. 그래서 [1, 2)든 [0.5, 1)이든 [0.25, 0.5)든 표현 가능한 값의 개수가 똑같이 2²³개(약 838만) 입니다.
그런데 구간의 폭은 0에 가까워질수록 절반씩 줄어듭니다.
같은 개수를 절반 넓이에 밀어 넣으니 밀도가 2배씩 늘어납니다. 0에 가까워질수록 이게 계속 반복되므로 밀도가 기하급수적으로 촘촘해집니다.
- 무기가 검게 변함: 압축된 1P 메시가 3P 메시의 그림자 볼륨 안으로 끌려 들어가 잘못 그늘졌다. 그림자 맵은 원래 위치로 기록되는데 셰이딩은 압축된 위치로 조회하기 때문
1P 메시를 전용 라이팅 채널(1)로 분리하고, 그 채널을 캐릭터에 내장한 뷰모델 라이트만 비추게 했다.
엔진에 프리미티브 단위 그림자 미수신 스위치가 없어서 채널 격리가 유일한 수단이고, 채널을 끊으면 빛과 그림자가 함께 끊기므로 전용 라이트가 필연으로 따라온다. 라이트를 레벨이 아니라 카메라에 붙여 레벨별 작업을 없앴다.
검토한 대안들
클리핑을 없애는 방법
- FOV/스케일만 줄여 벽에 안 닿게: 완화책이지 "무조건 보임"을 보장하지 못한다.
- 반투명 + Disable Depth Test: 위에 그려지긴 하나 디퍼드 라이팅을 못 받아 툰 셰이딩이 무너진다.
툰 셰이딩은 디퍼드로 처리하고, 반투명은 포워드로 처리하기 때문
그림자 처리
- 3P 메시 bCastHiddenShadow를 끔 : 한 줄로 끝나고 무기가 환경 조명을 그대로 받아 오히려 자연스럽다. 대신 1인칭에서 자기 그림자를 잃고, 건물 안에서 무기가 어두워진다.
- 무기가 검게 변하는 걸 SetCastShadow(false)로 고치려 함 : 건물 등에도 그림자를 받을 것이고, 압축 때문에 그림자가 실제와 맞지 않을 것, 3인칭 메시의 그림자를 잃게 됨
4주차
호스트(리슨서버)가 ADS하면 다른 클라이언트의 캐릭터까지 조준 애니메이션이 재생되는 현상 수정
원인
ADS 애니메이션 전파가 MFGameplayEventSubsystem의 전역 델리게이트로 처리되고 있었고, 모든 MFHandlingObject가 BeginPlay에서 이 이벤트를 구독했기 때문에, 호스트 장비가 조준하면 월드에 존재하는 모든 장비가 이벤트를 받아 각자 소유 캐릭터의 조준 애니메이션을 실행했기 때문
수정
- 전역 브로드캐스트 → 소유 무기 직접 호출 구조로 변경
- NotifyADS를 SetIsAiming으로 개편해 MFHOFireArm이 자기 소유 캐릭터의 AnimManagerComponent에만 직접 조준 상태를 설정
- 전역 ADS 이벤트는 UI 갱신 용도로만 유지하되, IsLocallyControlled()인 경우에만 브로드캐스트하도록 제한
ADS 중 다른 장비로 교체하면 조준이 해제되지 않고 ADS 상태가 그대로 남아 있는 문제 해결
원인
총기 교체 시 AMFHOFireArm::Deactivate()가 호출되지만, 총기에 부착된 파츠(조준경 등)에는 비활성화가 전파되지 않아서 조준경이 ADS 종료 처리를 할 기회가 없었음
수정 내용
- UMFFirearmPart에 가상 함수 Deactivate()를 추가하고, AMFHOFireArm::Deactivate()에서 부착된 모든 파츠에 대해 호출하도록 전파 구조를 만듦
- UMFFPSight::Deactivate() 오버라이드에서 EndADS()를 호출해 총기 비활성화 시 조준을 강제 해제
이즈나 E스킬이 호스트(리슨서버) 한정 3회 이상 연속 사용 가능 이슈 수정
원인
- 대시 카운트 관리 로직이 여러 곳에 흩어져 있었고, 권한(Authority) 체크 없이 실행되는 경로가 존재
- SetDashCount가 호스트 환경에서는 서버·클라이언트 역할이 겹치며 카운트가 중복으로 갱신될 여지가 있었음
- 충전 타이머 시작/정지 로직이 StartDashing, DeActivateCoreWork, OnEndCoolDown, ChargingTheDash 네 곳에 분산되어 있어, 쿨다운 종료 시 SetDashCount(MaxDashCount)로 카운트를 통째로 복구하는 등 카운트가 실제보다 많아지는 경로가 있었음
수정
- 카운트와 충전 타이머 관리를 SetDashCount 한 곳으로 일원화
- SetDashCount 진입 시 HasAuthority() 체크를 추가해 서버에서만 카운트가 변경
- 카운트 변경 시점에 타이머를 함께 관리하도록 변경: 최대치 도달 시 타이머 정지, 미만이면서 타이머가 꺼져 있으면 충전 타이머 시작
- 이에 따라 중복 로직이던 DeActivateCoreWork, OnEndCoolDown 오버라이드와 ChargingTheDash 내부의 타이머 정리 분기를 제거했다 (61줄 → 13줄 순감소)
충전형 어빌리티를 통합 및 리팩토링 진행할 예정
호시노 방패돌진이 계단에서 종료되는 현상 수정
원인
돌진 중 캡슐 트레이스에서 충돌면을 DetermineCollideSurfaceType(ImpactNormal)로만 판정했는데, 법선 각도만 보기 때문에 계단의 수직면이 벽으로 분류되어 돌진이 중단됨
수정 내용
- FMFCollisionUtility에 캐릭터 기준 표면 판정 오버로드 추가 : 법선 기준으로 벽으로 나오더라도 충돌 지점 높이가 발 위치 기준 MaxStepHeight 이하이거나 법선이 GetWalkableFloorZ() 이상(걸을 수 있는 경사)이면 → Floor로 재분류
즉, 걷기로 넘을 수 있는 단차/경사는 벽으로 취급하지 않음 - 트레이스 시작점 개선 : 스칼라 전방 거리를 FVector로 변경해 캐릭터 회전 기준 3축 오프셋 조정 가능하도록 변경
캡슐 트레이스 시작점 및 반지름, 높이 개선
대각 이동시 걷는 사운드 재생 간격이 빨라지는 현상 수정
원인
발소리 재생 타이밍은 입력 방향 벡터의 크기를 CurrentWalkAmount에 누적해 임계값에 도달하면 재생하는 방식인데, 대각 입력 시 X·Y 축이 각각 1이라 벡터 크기가 √2(≈1.41)가 되어 누적 속도가 직선 이동보다 약 41% 빨라진 것이 원인
해결
MoveIntensity를 1.0에 클램프하여 대각 이동도 직선 이동과 동일한 누적 속도를 갖게 되어 발소리 간격이 일정해짐
이즈나 대시 스킬 쿨타임이 UI에 갱신되지 않는 현상 수정
문제
기존에는 CurrentDashCount를 Replicated 프로퍼티로 복제만 하고 있어서, 값이 클라이언트에 도착해도 UI에 알려주는 통로가 없었고 쿨타임 시작/종료 시점도 전달되지 않음
해결
- 리플리케이트 방식을 제거하고, 서버에서 대시 횟수가 바뀔 때마다 명시적으로 클라이언트 RPC로 통지하는 방식으로 변경
- 대시 횟수는 해당 플레이어 어빌리티 및 UI에만 필요하므로 모든 클라이언트에 복제할 필요 없이 소유 클라이언트로의 Client RPC면 충분
- 쿨타임 시작/종료는 상태가 아니라 이벤트라서, 스냅샷을 동기화하는 복제보다 발생 시점에 통지하는 RPC가 의미상 맞는 도구
- 대시 횟수 변경 로직을 SetDashCount() 하나로 통일 : 사용 시 감소, 충전 시 증가, 쿨다운 종료 시 최대치 복구가 전부 이 함수를 거치며, 최대치 도달 시 충전 타이머 해제도 여기서 처리
- Client RPC에서 남은 충전 시간, 이전 값, 현재 값을 전달하여 카운트를 설정하고 UI에 이벤트를 보내어 스킬 횟수 및 쿨타임이 갱신되도록 변경
달리기 관련 버그 수정
문제
- 점프 중 달리기 키를 떼면 자동으로 달리기가 되는 현상
- 착지 후 점프 키를 릴리즈하면 달리기가 안 되는 현상
원인
- 상태 복원 구조
CharacterActionState(현재 상태)와 PrevCharacterActionState(직전 상태) 두 값으로 관리됩니다. 점프하면 상태가 Sprint → Jumping으로 바뀌면서 PrevCharacterActionState에 Sprint가 저장되고, 착지 시 ResetJumpState()가 이 직전 상태를 보고 "점프 전 상태로 되돌리는" 방식
버그 발생 흐름
달리는 중(Sprint) 점프 → PrevCharacterActionState = Sprint 저장
공중에서 달리기 키를 뗌 → Sprint(false) 호출로 bSprinting = false가 되지만 현재 상태는 Jumping이라 상태 전이는 없음
착지 → ResetJumpState()가 PrevCharacterActionState == Sprint만 보고 SetMovementState(Sprint) 호출하여 자동 달리기가 됨 - 점프 입력 처리 순서 문제로, 착지 후 점프 키 릴리즈 경로에서도 점프 상태 설정이 실행되어 점프 중이라 판단하고 달리기로 설정되지 않음
수정 내용
- Sprint로 전이 요청됐지만 bSprinting이 false면 Walk로 보정하며, SetMovementState()에서 상태 적용 전에 검증을 거치도록 변경
- 점프 키 press일 때만 점프 상태로 설정하도록 변경
3주차
시로코 사망 후 자폭 데미지 발생 버그 수정
원인
AMFGAAddStatus가 BeginPlay 시점에 상태 태그를 부여하고 있었고, BeginPlay는 어빌리티 액터가 스폰될 때 한 번만 호출되기 때문에, 캐릭터의 생명주기(사망/리스폰)와 무관하게 상태가 적용되어 사망 이후에도 자폭 상태가 남아 데미지가 들어감
- 어빌리티는 캐싱하여 객체를 재사용하기 때문에 BeginPlay가 캐릭터 생명주기와 무관함
수정 내용
- 상태 부여 시점을 캐릭터 스폰 시점으로 이동 : AMFGAAddStatus::BeginPlay 대신 OnSpawnedCharacter에서 새로 스폰된 캐릭터에 AddStatus를 호출하도록 변경
- 캐시된 어빌리티가 등록되지 않는 현상 수정
- 시로코의 어빌리티 중 어빌리티 태그가 None인 어빌리티가 해당 어빌리티 포함 2개였음
- UMFAbilityComponent::FindAbilityContainer에서 None 어빌리티를 같다고 판단하여 다른 이미 등록된 어빌리티를 반환하여 등록하려는 어빌리티가 등록되지 않음
- 유효하지 않은 GameplayTag가 들어오면 조기 nullptr 반환하도록 검사 추가
추후 None인 어빌리티를 등록하려 하면 checkf로 어썰트 발생 예정
호시노 샷건이 벽을 뚫어 발사되는 현상 수정
원인
발사 원점 불일치
- 조준(레이캐스트)은 카메라 시점 기준인데, 투사체는 총구 소켓(FireVFXSocketName) 위치에서 스폰되고 있었음
- 캐릭터가 벽에 붙으면 총구가 벽 너머에 위치해 탄이 벽을 건너뛰고 발사됨
해결
- 발사 원점 규칙 통일: 투사체는 총구 소켓이 아닌 카메라 위치(ViewLocation)에서 스폰, 총구 소켓은 총구 화염 등 연출 전용으로 역할 분리
- AMFCharacter::GetPawnViewLocation() 오버라이드 : 눈 위치를 BaseEyeHeight가 아닌 실제 카메라 컴포넌트 위치로 반환, BP에서 카메라를 옮겨도 조준과 발사 원점이 일치
AMFHOFireArm::SpawnShootableObject() 공용 함수 신설 : 일반 발사(Fire), 샷건 펠릿, 슬러그탄에 흩어져 있던 중복 스폰 코드를 통합 (회전 오버라이드는 TOptional로 처리)
MFSORaycastMovement: 발사 원점이 눈 위치가 되면서 사수 자신의 부착 장비(방패 등)가 탄도를 막는 문제가 생겨, 인스티게이터에 부착된 액터들을 충돌 무시 목록에 추가
호시노 벽에 가까이 갔을 때 뚫려 보이는 현상 수정
원인
다른 캐릭터와 비교하여 호시노 1인칭 카메라의 X Location이 더 앞에 있어 벽과 가까워져 카메라 근평면 클리핑이 발생
해결
호시노 1인칭 카메라의 X Location를 다른 캐릭터와 통일
섬광 폭발 사운드 적용
ExplosionCue에 섬광 폭발 사운드를 등록한 SoundCue를 등록하여 폭발할 때 사운드가 재생되도록 적용
데미지 0 이하일 때 데미지 처리 안되도록 변경
문제
데미지 경감(퍼센트/고정량 감소) 적용 후 최종 데미지가 0 이하가 되어도 이후 처리(킬 관여 기록, 데미지 이벤트 브로드캐스트 등)가 그대로 수행되는 문제가 있었음
수정
MFDamageHandlerComponent::HandleDamageInternal 데미지 경감 계산 후 최종 데미지가 UE_SMALL_NUMBER 이하이면 조기 리턴하여 데미지 무효 처리 + 로그 출력
부수 수정
데미지 처리 대상은 MFStatisticInterface 구현 필수이므로 StatisticComponent 조회를 먼저하여 없을 경우 checkf로 명확하게 실패하도록 변경
기존의 깊은 중첩 구조를 조기 리턴 방식으로 정리하여 가독성 개선
이즈나 섬광 수리검 즉시 폭발 안 되는 현상 수정
폭발 딜레이를 1.0 > 0.0으로 수정
실명 종료 1초 전 점진적으로 회복하도록 변경
실명 FadeIn을 0.1초 이내로 단축
- FlashbangCurve 커브 에셋을 수정해 섬광 피격 시 화면이 거의 즉시 완전 실명 상태에 도달하도록 변경
회복 연출 추가
- 실명 종료 1초 전부터 시야가 회복되도록 별도 Float Curve로부터 머터리얼 파라미터 값 갱신
Tick에서 회복 커브의 길이(GetTimeRange)를 기준으로 회복 시작 시점(ComputedDuration - 커브 길이)을 계산하고, 그 전에는 기존 FlashCurve, 이후에는 RecoveryFlashCurve를 샘플링하도록 분기
2주차
이즈나 점멸이 단차에 걸리는 이슈 수정
문제
이즈나 점멸(순간이동 대쉬) 사용 시 계단이나 낮은 단차에 캡슐 스윕이 막혀, 실제 이동 거리가 짧아지며 "걸리는" 느낌이 발생
시작~목표를 단일 스윕으로 검사해 낮은 턱조차 벽으로 판정했기 때문
해결
핵심 아이디어는 "순간이동 경로를 걷기와 동일한 기준으로 시뮬레이션한다"는 것
- 구간 분할 전진
시작~목표를 한 번에 스윕하지 않고, 캡슐 반지름 단위 구간으로 나눠 반복 전진
단일 스윕은 경로 중간의 단차 하나만 있어도 전체가 막히지만, 구간별로 처리하면 각 구간마다 스텝업/스텝다운을 개별 시도할 수 있음 - 지상 전진: 띄우고 → 전진하고 → 지면에 스냅
매 구간마다 캡슐을 MaxStepHeight만큼 위로 띄운 상태에서 수평 스윕하여 바닥에 붙은 캡슐이 지면 접촉만으로 스윕이 막히는 것을 방지하고, 스텝 높이 이하의 턱은 자연스럽게 넘어감
띄우는 동작 자체는 스윕하지 않게 하여 바닥에 미세하게 겹친 캡슐은 위로 떠나는 스윕조차 전진이 막히는 것을 방지
전진 후 아래로 스윕해 지면에 다시 붙임 - 스냅 하강 허용량에 경사 낙차 포함
스냅 시 허용 하강량을 스텝 높이만이 아니라 스텝 높이 + (수평 이동량 × tan(걷기 가능 경사각))으로 계산
스텝 높이만 허용하면 계단을 내려갈 때 구간 경계가 단 모서리에 걸리는 경우, 실제로는 걸어 내려갈 수 있는 낙차를 절벽으로 오판해 캐릭터가 허공에 멈추는 문제를 해결 - 스냅 프로브는 반지름을 줄인 캡슐 사용
하향 스냅 스윕은 캡슐 반지름을 5cm 줄여 수행하여 원래 크기로 스윕하면 방금 스친 단면이나 벽에 캡슐 옆면이 닿아 전진이 막히는 것을 방지
그래도 막힌다면 이전 지면 높이(Z)를 유지하는 보수적 처리 - 벽 판정 완화
전진 스윕이 막혀도 즉시 벽으로 보지 않고, 접촉 지점에서 2cm 물러난 위치까지의 실제 전진량만 차감하고 다음 반복에서 더 높아진 지면 기준으로 재시도
스텝업을 해도 전진량이 1cm 미만일 때만 진짜 벽으로 판정하고 정지
물러나는 이유는 다음 스윕이 바로 충돌 판정되는 것을 막기 위함 - 절벽과 공중 처리
허용 하강량을 넘는 낙차(절벽)를 만나면 그 지점부터 Z를 유지한 채 허공에서 종료하고 자연 낙하
점멸 시작 시 IsFalling()으로 공중 여부를 판별해 전달하며, 공중에서는 스텝업 없이 같은 Z로 수평 이동하고 벽에 막히면 직전 위치에서 정지
부수 수정
- 기존 대시 방향에서 컨트롤 회전의 Pitch/Roll을 제거해 점멸 방향을 캐릭터 방향으로 고정
검토한 후보와 결론
- 엔진 걷기 로직 재사용 : 실패시 복원 때문에 탈락
한 프레임 안에서 CMC의 SafeMoveUpdatedComponent + StepUp + FindFloor를 반복 호출해 경로를 압축하는 방식. PhysWalking과 같은 함수로 판정하므로 걷기와 대시가 갈라질 수 없다(정의상 일치). 대가는 실제 컴포넌트를 움직이므로 오버랩/트리거 발화 억제, 리플리케이션 순간값, 실패 시 복원 등 부작용 관리가 필요하고 단위 검증이 어려움 - 높이맵 조회 (X,Y → 지면 Z) : 탈락
랜드스케이프 높이 조회나 사전 베이크 높이 그리드로 경로를 따라 Z만 보정하는 방식
(X,Y)당 높이가 하나뿐이라 다층 구조를 표현할 수 없어 2층에서 대시했는데 1층 바닥 높이가 나오면 바닥을 뚫는 문제 우려
벽 판정도 없어서 수평 스윕이 여전히 필요하고, 동적 지오메트리도 반영 못 함 - 내비메시 투영 : 탈락
ProjectPointToNavigation에 수직 QueryExtent를 좁게 주면 다층 구분(1층/2층/계단 스냅)은 가능
쿼리 수 회 vs 스윕 수십 회로 성능은 조금 유리하지만 대시 보정은 발동 순간 1회 실행 및 쿨타임이 있어 병목이 아닌 곳의 최적화라 의미가 없다고 판단- 벽 근처 손해: 내비메시는 벽에서 에이전트 반지름만큼 침식돼 생성되어 벽에 붙는 대시가 한 발짝 앞에서 멈추고, 좁은 통로는 폴리곤이 아예 없어 비유효로 오판될 수 있음. 경계가 벽 때문인지 절벽 때문인지 내비메시만으로 구분할 수 없어 보정 불가 판단
- 가장자리 오판: 복셀화(CellSize/CellHeight) 때문에 폴리곤 경계가 실제 가장자리와 지점마다 다른 방향·크기로 어긋날 수 있음
가짜 절벽(지면인데 허공 판정), 가짜 지면(허공인데 스냅 성공)이 맵·격자 정렬마다 새 지점에서 재발 - 층 경계 애매 지점: 계단 중간처럼 두 층 사이 높이에서는 QueryExtent를 어떻게 잡아도 실패 지점이 있음
좁으면 폴리곤 틈에서 투영 실패, 넓으면 계단 밑 복도로 투영돼 계단을 뚫고 아래층으로 이동하기 때문에 폴리곤 인접 그래프 순회가 필요한데 그 복잡도가 현재 스윕 루프보다 큼
- 대시 중 중력 적용 : 탈락
단일 프레임 순간이동에는 경과 시간이 없어서 중력은 규칙으로 모델링해야 하는데, 무한 낙하 스냅(절벽에서 지면까지 붙임)은 "절벽 → Z 유지, 허공 종료 후 자연 낙하"라는 확정 규칙 때문에 부적합 - Lyra식 루트 모션 소스 (GA_Hero_Dash) : 탈락
Lyra의 대시는 순간이동이 아니라 AbilityTask_ApplyRootMotionConstantForce로 짧은 시간 CMC에 루트 모션 소스를 주입하는 방식
이동이 걷기 물리 그대로라 보정 코드가 필요 없고, CMC 예측 통합으로 네트워킹도 공짜
"단일 프레임 순간이동"과 "이동 중 피격 판정 없음" 규칙을 둘 다 위반, 질풍 대쉬는 돌진(제트)이 아니라 점멸(트레이서)로 정의돼 있으므로 채택 불가
캐릭터 부스팅 방지
캐릭터 위에는 설 수 없고, 얹히면 미끄러져 떨어지도록 다층 방어를 구성
- AMFCharacter::CanBeBaseForCharacter : 다른 AMFCharacter를 무브먼트 베이스로 삼는 것을 거부
- IsWalkable : 캐릭터 표면을 걸을 수 없는 면으로 판정해 착지 자체를 차단
- JumpOff : 베이스가 캐릭터면 점프 대신 즉시 Falling으로 전환
- HandleImpact : 정중앙에 얹히면 미끄러질 방향이 없어 균형 체류가 가능하므로, 45도 이내 윗면 접촉 + 수평 속도 부족 시 수평 속도를 부여해 떨어뜨림
미끄러질 방향 우선순위: 수평 속도 방향 → 두 캐릭터 중심을 잇는 수평 방향 → 아래 캐릭터의 등 뒤 방향
슬라이딩 충돌 시 상대가 밀려나고 간헐적으로 멀리 날아가는 이슈
원인
- 캡슐이 겹치면 엔진의 관통 해소(ResolvePenetration)가 상대 캐릭터에 무브먼트가 적용됨
- 관통 해소로 밀린 위치 델타가 다음 프레임에 속도로 역산되면서, 위치 교정이 물리적 충격처럼 큰 속도로 전환돼 멀리 날아가는 현상이 발생
- 엔진의 bJustTeleported 가드는 이 경로를 전부 막지 못했고, 해소 당일 프레임뿐 아니라 다음 프레임에도 속도가 튀는 사례가 실측으로 확인
해결
ResolvePenetrationImpl에서 겹침 해소 이동을 슬라이딩한 캐릭터 무브먼트에 적용되도록, 밀림 방향의 반대로 상대를 스윕 없이 직접 이동시켜 이동을 상쇄
- cm 단위 이동이라 벽에 박혀도 다음 틱의 관통 해소로 복구됨
OnMovementUpdated에 관통 해소 후 2프레임 속도 가드 추가
- 가드 기간 중 한 프레임에 속도가 급증하면 역산된 속도로 보고 이전 속도로 되돌림
호시노가 스폰 직후 샷건으로 재교체까지 슬러그탄 사용 불가 버그 수정
원인
어빌리티 등록 주체가 두 곳
- 컨트롤러 — 캐릭터 고유 스킬 등록
- 장비 — 슬러그탄 같은 장비 어빌리티 등록
그런데 해제는 UnregisterAllAbility() 하나로 전부 일괄 삭제였는데, 컨트롤러가 InitAbility에서 캐릭터 스킬을 다시 등록하기 전에 모든 어빌리티를 등록 해제하면서, 장비가 등록해 둔 슬러그탄 어빌리티까지 함께 제거됨
이후 샷건으로 재교체하여 장비의 Activate()가 다시 호출되어야만 슬러그탄이 재등록되므로, 그 전까지는 사용이 불가능
수정
어빌리티 등록에 출처(Source) 개념을 도입해, 등록한 주체가 자기 것만 해제하도록 변경
RegisterAbility(AbilityClass, Source) — 등록 시 출처를 FAbilityContainer::Source에 기록
UnregisterAllAbility() → UnregisterAbilitiesBySource(Source) — 해당 출처가 등록한 어빌리티만 일괄 해제
이제 컨트롤러는 자신이 등록한 캐릭터 스킬만 정리하고, 장비가 등록한 어빌리티는 건드리지 않음
부수 수정
UnregisterAbility()에서 RemoveSingle 후 무효화된 포인터를 참조하던 문제 수정 => 제거 전에 태그 복사
캐시된 어빌리티를 다른 Source가 재등록하는 경우는 없다는 가정을 checkf로 명시
1주차
이즈나 대시가 벽/바닥을 뚫는 버그 수정
원인
기존 로직은 3단계로 도착 위치를 보정
- 도착 지점에서 캡슐 오버랩 검사
- 막혀 있으면 바닥 라인 트레이스로 위쪽 위치를 찾아 보정
- 그래도 막혀 있으면 시작점→도착점 스윕으로 후퇴
문제는 1번이 도착 지점만 검사한다는 것, 도착 지점 자체는 비어 있지만 경로 중간에 벽이 있는 경우(예: 얇은 벽 너머)는 막힌 걸로 판정되지 않아 벽을 그대로 통과, 또 2번 바닥 보정으로 구한 위치가 벽 안쪽인 케이스도 완전히 걸러내지 못함
수정
3단계 보정 로직을 버리고, 시작점에서 도착점까지 캡슐 스윕 한 번으로 단순화, 스윕은 경로 전체를 검사하므로 벽 너머로 넘어갈 수 없고, 막히면 충돌 직전 위치가 곧 가장 가까운 유효 위치
이 로직은 MovementUtility으로 분리해서, 앞으로 다른 순간이동류 스킬에서도 재사용할 수 있게 함
26년 6월
5주차
킬로그 피아식별 불가 이슈 수정
원인
킬로그를 표시할 때 킬러/희생자가 아군인지 적군인지 구분하지 않아 피아를 표현하지 못하는 문제 발생
수정
1. 로컬 플레이어 기준 피아 판정
UpdateKillLogDisplay에서 로컬 PlayerState와 킬러/희생자 팀을 UMFGameplayStatics::IsSameTeam으로 비교해 아군 여부를 판정
2. 킬로그 위젯을 전용 클래스로 교체 및 리팩토링
기존에는 GetWidgetFromName으로 위젯 내부 요소를 일일이 찾아 텍스트/이미지를 직접 세팅
이를 전용 위젯 UMFKillLogEntryWidget으로 바꾸고, 피아 정보를 BlueprintImplementableEvent로 전달하여 UMG에서 스스로 표시를 구성하도록 리팩토링
- 문자열 의존 제거: GetWidgetFromName("Text_PlayerName_Attacker")처럼 위젯 이름을 C++에 하드코딩하던 방식은 UMG에서 요소 이름을 바꾸면 컴파일 에러도 없이 조용히 동작이 깨지는 현상 방지
- 표현 로직 분리: 아군/적군 색상, 레이아웃 같은 시각 표현을 UMG와 위젯 안에서 처리하므로, C++ UI 컨트롤러는 "누가 누구를 죽였고 피아가 무엇인지"라는 데이터만 넘기면 된다. 디자인 변경 시 C++ 재컴파일이 필요 없음
- 표시 규칙이 위젯에 캡슐화되어 있어, 다른 화면에서 킬로그 항목을 재사용하거나 표현을 확장하기 쉬워짐
4주차
호시노 방패 돌진이 즉시 멈추는 버그 수정
원인
돌진 중에는 매 틱마다 캐릭터 전방으로 캡슐 트레이스를 쏴서 벽이나 적과의 충돌을 판정
호시노가 들고 있는 방패의 콜리전이 WorldStatic이라, 벽 판정을 위해 WorldStatic 트레이스가 매번 방패 자신을 맞히고 벽에 부딪혔다고 판단해 돌진을 중단시키고 있었음
수정
1. 부착된 액터를 트레이스 무시 목록에 추가
OwnerCharacter->GetAttachedActors(ignoreActors, false, true);
2. 벽 판정을 공용 유틸리티로 교체
매직넘버로 직접 판정하던 부분을 표면 분류 공용 함수로 바꿔 프로젝트 전역과 기준을 통일
// before
const bool bIsWall = hitResult.Normal.Z < 0.85;
// after
const bool bHitWall =
FMFCollisionUtility::DetermineCollideSurfaceType(hitResult.ImpactNormal) == ESurfaceType::Wall;
2주차
매치 BGM 사운드를 GameState에서 실행하도록 리팩토링
동기
- 매치 흐름 BGM(카운트다운/오버타임/종료 사운드)은 기존에 MFTDMGameMode가 사운드 데이터를 소유하고, 페이즈마다 PlayerController를 순회하며 Multicast_PlaySound RPC를 일일이 호출하는 구조
- 서버 전용 액터인 GameMode가 클라이언트 재생까지 직접 지휘하다 보니 PC 순회 보일러플레이트가 페이즈마다 반복
변경 내용
- 사운드 재생 책임을 이미 전 클라이언트에 복제되는 GameState로 이전
HandlePhaseChange()에서 HandleMatchFlowAudio(NewPhase)를 호출해, 페이즈 전이의 부수효과로 사운드를 재생하도록 정리
MFPlayerController::Multicast_PlaySound RPC와 GameMode의 사운드 프로퍼티·PC 순회 코드를 전부 제거
사운드 에셋 프로퍼티를 GameState로 이동하고, Finished 진입 시 재생 중인 사운드를 끄는 StopSoundsOnFinished(Wwise Stop 이벤트)를 추가
효과
- 순수 삭제 우위(50줄 추가 / 118줄 삭제)로 RPC·순회 보일러플레이트 제거
- 사운드 트리거가 페이즈 상태와 한 곳에 모여 응집도 향상, 트리거 지점이 HandlePhaseChange 단일 경로로 단순화
SoundComponent → GameplayCue 전환 리팩토링
동기
- 액터마다 사운드 전용 컴포넌트(UMFSoundManagerComponent)를 생성·복제하는 구조가 비효율적
무기·캐릭터·투척물 등 사운드를 내는 액터마다 컴포넌트가 붙어 메모리·복제 비용이 늘고, 재생 로직도 컴포넌트마다 중복 - 사운드 재생을 GameplayCue 체계로 일원화하고, 폭발처럼 사운드와 VFX가 함께 나가는 연출을 하나의 Cue로 결합 재생할 수 있도록 정리
변경
- UMFSoundManagerComponent 제거 : MFAbility / MFCharacter / MFHandlingObject / MFSOGrenade 등에서 생성하던 서브오브젝트 및 PlaySound 호출 일괄 삭제
- GameplayCue 클래스 신규 추가
UMFGCSound : Wwise 이벤트 재생
UMFGCVFXSound : 사운드 + Niagara VFX를 함께 재생
UMFGameplayCueComponent에 일회성 재생 API 추가
발소리 데이터화 : 기존 if/else 분기를 CharacterActionSoundCues 맵(ActionState → Cue)으로 전환해 디자이너가 데이터로 관리
네트워크 처리
- 발신 예측: 로컬 제어 폰은 즉시 재생 후 서버 경유로 전체 전파
Server → Multicast / Client → Server → Multicast - ShouldSkipReplicatedSound로 이미 로컬에서 재생한 폰은 멀티캐스트 재생을 skip
착지음은 서버·클라 양쪽 호출되므로 IsLocallyControlled() 가드로 중복 제거
효과
- 사운드 재생 경로가 GameplayCue 하나로 통합되어 호출 규약이 단순해짐
액터마다 붙던 사운드 컴포넌트가 사라져 메모리·복제 부담 감소
사운드·VFX 연출을 큐 단위로 묶어 관리 가능
26년 5월
4주차
AMFAbilityCondition이 리플리케이트되지 않도록 변경
기존에는 AMFAbilityCondition과 그 하위 클래스(AMFACCharge)가 액터 리플리케이션을 통해 활성화 조건 상태를 클라이언트에 동기화하는데, 방식은 Condition 액터 자체를 리플리케이트해야 하고, AMFACCharge에서는 Tick으로 GameplayEffect 스택을 폴링하는 비효율을 제거하기 위함
GameplayEffect Stack을 UMFGameplayEffectManagerComponent에서 Client RPC로 직접 동기화하도록 변경하여, Condition 액터의 리플리케이션 부담을 제거
Tick 폴링 → 온디맨드 조회: 0.3초마다 스택을 확인하는 대신, 필요할 때 직접 조회하므로 불필요한 연산 제거
액터 리플리케이션 → Client RPC: Condition 액터 전체를 리플리케이트하는 대신, 실제 필요한 데이터(GameplayEffect Stack)만 RPC로 전달하여 네트워크 비용 절감
EffectStacks 캡슐화: 직접 접근을 차단하고 변경 경로를 통일하여, 스택 변경 시 클라이언트 동기화가 누락되는 실수를 방지
2주차
리스폰 무적 부여 방식을 GameplayEffect로 전환
기존 리스폰 무적 로직은 RespawnComponent 내부에서 타이머와 델리게이트를 플레이어 ID별로 직접 관리하는 방식으로 구현됨
GameplayEffect와 기능이 대부분 유사했고, 중복 적용 방지 분기로 인한 복잡도 때문에 리팩토링 진행
UMFInvincibilityGameplayEffect 클래스를 새로 만들어 무적 적용
- Apply() : bRespawnInvincible = true 설정 + LifeState 변경 감지 바인딩
- Release() : bRespawnInvincible = false 설정 + 델리게이트 핸들 정리
RespawnComponent에서는 리스폰 캐릭터에 GameplayEffect 적용을 요청만 하면 되므로 간단해짐
GameplayEffect 하나가 자신의 적용/해제를 스스로 책임지는 구조가 되니, 앞으로 새로운 종류의 무적(스킬, 아이템 등)을 추가할 때도 같은 패턴을 따르면 됨
1주차
어빌리티 Tick을 기본적으로 비활성화하도록 변경
Tick이 필요한 어빌리티에서만 활성화하여 오버헤드 줄임
26년 4월
5주차
스킬이 비활성 상태일 때 횟수가 화면에 표시되도록 변경
횟수가 0이 되어 어빌리티가 비활성화된 이후, 횟수가 다시 증가해도 UI가 갱신되지 않던 문제
InitAbility 시점에 EquipmentManager의 Equipments 복제가 아직 도착하지 않은 클라이언트에서는 TryBindDeActiveEquipmentDelegate()가 실패하고, OnUpdatedEquipments로 지연 바인딩되는 흐름이 있는데, 이 지연 흐름에서는 OnInitAbility가 발동되지 않아 UI가 초기 횟수를 받아가지 못함
OnUpdatedEquipments 콜백에서 정상 바인딩 후 PostInitAbility를 호출하여 UI는 어떤 경로로든 1회 OnInitAbility를 수신하도록 변경
게임 시작/스폰 시 초기 어빌리티 횟수가 UI에 반영되지 않던 문제
UI가 "어빌리티가 초기화되었으니 횟수를 다시 한 번 동기화해라"는 신호를 받을 수 있도록 신규 이벤트 추가
AMFAbility::InitAbility() 마지막에 SendInitGameplayEvent() 호출, AMFAbility::PostInitAbility() 파생 어빌리티가 자신의 늦은 초기화 시점에 다시 한 번 이벤트를 보낼 수 있도록 함
클라이언트에서 수리검 충돌 시 정지 안 되는 현상 수정
이전 커밋에서 수리검 충돌 시 메시가 박힌 채 정지하도록 했으나, AMFSOProjectileMovement::NotifyHit은 서버에서만 실행되므로 StopMovementImmediately()도 서버에서만 호출되었다. 결과적으로 서버에서는 투사체가 정지하지만 다른 클라이언트에서는 정지-동기화 누락 현상이 발생
UMFProjectileMovementComponent::StopMovementImmediately를 NetMulticast RPC를 통해 모든 클라에서 정지되도록 변경
스킬 알림 UI 시스템(NotifyUIController) 구현
스킬마다 다른 알림 위젯을 데이터 자산으로 매핑하고, 게임플레이 이벤트를 `OnNotifySkill` / `OnEndNotifySkill` 한 쌍으로 통일하여 향후 다른 스킬에도 동일한 패턴으로 알림 UI를 붙일 수 있도록 확장성을 확보
OnNotifySkill이 들어왔을 때 NotifySkillWidgetDataAsset에서 태그에 해당하는 위젯 클래스를 조회하여 CreateWidget 후 NamedSlot에 컨텐츠로 설정
OnEndNotifySkill이 들어왔을 때 현재 표시 중인 태그와 일치하는 경우에만 슬롯 컨텐츠를 nullptr로 비움
수리검 충돌 지연 효과 적용
이즈나 수리검이 충돌한 위치에 잠시 박혀 있다가 GameplayEffect가 발동되도록 기획이 변경됨에 따라, 폭발 컴포넌트에 지연 시간을 도입하고, 투사체가 충돌 시 즉시 정지하여 메시가 박힌 채로 보이도록 처리
4주차
이즈나 실명 수리검 변경 적용
이즈나의 섬광 수리검 기획 변경에 따라, 실명(Flash) 효과의 지속 시간이 거리 + 시야각 조합으로 동적으로 계산되도록 변경
- GameplayCue / GameplayEffect 파이프라인에 `Instigator` 정보가 전파되도록 구조를 확장
- 지속시간 계산 : 거리로 DurationByDistance FloatCurve에서 기본 지속 시간을 구하고, 캐릭터 정면 기준 Instigator까지의 시야각(deg)으로 WeightByViewAngle FloatCurve에서 가중치를 구한 뒤 두 값을 곱해 최종 지속시간 산출
- 뒤를 바라봐도 섬광이 적용되도록 기존 CanApply에서 수행하던 시야각 기반 차단 로직(FOV 코사인 임계값 비교) 제거
EquipmentHandle 리플리케이트 순서 불일치 수정
기존 장비 장착 어빌리티는 서버에서 RegisterEquipment 결과로 받은 FEquipmentHandle을 자체 Replicated 프로퍼티로 보관하고, 클라이언트에서는 리플리케이트된 해당 핸들로 EquipmentManager의 장비를 조회하여 OnDeActive 델리게이트를 바인딩
그러나 어빌리티의 EquipmentHandle 복제와 UMFEquipmentManager::Equipments 배열 복제는 서로 독립적이며, 도착 순서가 보장되지 않아 EquipmentHandle이 먼저 도착하는 경우 매니저의 Equipments에는 아직 해당 장비가 없어 핸들 → 장비 조회가 실패
또한 외부에서 EquipmentHandle을 저장하고 관리하여 캡슐화가 되지 않고 있음
외부적으로 FEquipmentHandle을 사용하지 않도록 변경하고 요청은 어빌리티가 이미 알고 있는 EquipmentClass로 일원화
클라이언트에서 장비가 아직 도착하지 않았을 때를 대비해, EquipmentManager에 장비 배열 갱신 알림을 추가하고 어빌리티가 이 알림을 기다려 지연 바인딩
이즈나 수리검 투사체 충돌 규칙 변경 적용
이즈나 수리검(섬광/연막) 투사체의 충돌 동작 기획이 변경됨에 따라, 전용 콜리전 프로파일을 추가하고 투사체 기본 동작을 조정
- 수리검은 캐릭터에 직접 데미지를 주지 않고 통과하도록 변경
- 생성자에서 `bDestroyOnProjectileHit = false`를 명시적으로 설정.
수리검류 투사체가 단일 히트로 즉시 파괴되지 않도록 기본값 보장
섬광/연막 수리검은 명중 후에도 효과 부여/지면 정착 등의 추가 라이프사이클을 거치므로 자동 파괴 비활성이 필요
3주차
이즈나 수리검 스킬 사용 방법 기획 변경 적용
이즈나의 멀티 수리검 스킬 어빌리티 사용 방식이 기획 변경됨에 따라, 어빌리티가 키 재 입력으로 취소될 때 별도의 쿨타임을 적용할 수 있도록 어빌리티 시스템을 확장하고, 캐릭터가 특정 상태 태그(예: CC) 보유 시 어빌리티가 자동으로 취소되도록 하는 일반화된 메커니즘을 추가
무적 캐릭터 점멸 구현
리스폰 직후 부여되는 무적 상태를 시각적으로 표현하기 위해 캐릭터 메시 및 장착 무기에 점멸(Blinking) Overlay Material을 적용하는 기능을 구현
기존 무적 시스템에 GameplayCue 기반의 시각 피드백을 결합하고, 장착 장비 교체 시에도 점멸이 끊기지 않도록 처리
- 신규 GameplayCue를 구현하여 OnActivated에서 UMaterialInstanceDynamic을 생성하여 캐릭터 3인칭 메시 및 현재 장착 장비에 Overlay Material 적용하고, Tick에서 sin(ElapsedTime * BlinkSpeed) * 0.5 + 0.5 공식으로 Opacity 파라미터를 조절하여 부드러운 펄스 효과 구현
- 장비 Overlay 일괄 적용 인터페이스 신규 인터페이스를 도입하여 AMFHandlingObject에 신규 인터페이스 인스턴스 배열을 보유하고, RegisterOverlayTarget / UnregisterOverlayTarget / ApplyOverlayMaterial 추가하여 등록된 모든 비주얼 메시에 일괄 적용
서브웨폰 비주얼(3인칭)도 RegisterToOwnerEquipment / UnregisterFromOwnerEquipment로 등록·해제하여 보조 무기까지 점멸 적용 범위에 포함
GameplayCue 멀티캐스트 지원
- AddGameplayCue_Multicast / RemoveGameplayCue_Multicast API 추가
- ActiveMulticastCueClasses 배열을 PushModel 기반 복제(REPNOTIFY_OnChanged)로 처리하여 후속 접속/리레플리케이션 상황에서도 큐가 복원되도록 구성
서버에서 게임 시작 전에 UI의 게임 시간이 초기화되지 않는 현상 수정
게임모드의 GameTimingSettings의 OnRep_ 콜백을 통해 HandleTimingChange가 호출되는 구조인데, 서버에서는 자기 자신이 변경한 Replicated 변수에 대해 OnRep이 호출되지 않아서 OnRemainTimeChange GameplayEvent가 브로드캐스트되지 않음
또한 기존 HandleTimingChange의 switch문은 WaitingToStart와 Finished를 동일 분기로 묶어 단순히 로그만 출력하고 있어, 초기 잔여 시간을 UI에 푸시할 경로 자체가 존재하지 않음
GameMode::StartPlay()에서 GameTimingSettings를 세팅한 직후 HandleTimingChange()를 직접 호출하도록 추가하여, OnRep 누락 경로를 보완
HandleTimingChange()에서 WaitingToStart 페이즈에 별도 분기를 만들어 OnRemainTimeChange로 GameDurationTime을 브로드캐스트하도록 수정하여 UI가 게임 시작 전부터 정확한 초기 잔여 시간을 표시할 수 있게 됨
클라이언트에서 게임 시작 카운트다운 UI가 표시되지 않는 현상 수정
클라이언트에서 UI NativeConstruct() 시점에 GameState가 아직 네트워크에서 수신되지 않아 nullptr인 경우, 델리게이트 바인딩이 되지 않아 카운트다운 UI가 동작하지 않음
AMFGameState::PostNetInit()에서 클라이언트가 GameState를 네트워크에서 처음 수신 및 초기화를 완료한 시점에 OnInitializedGameState GameplayEvent를 발행
위젯 생성 시 GameState가 이미 있으면 즉시 초기화하고, 없으면 OnInitializedGameState를 구독해 GameState가 준비된 후 초기화
캐릭터 선택하지 않고 게임 시작했을 때 스폰되지 않는 현상 수정
게임이 시작되면 모든 캐릭터의 LifeState를 Alive_Grace로 전환이 되어 직후 기본 캐릭터로 자동으로 스폰될 때 LifeState가 이미 Alive_Grace이기 때문에 스폰 실패 판정
Alive_Grace 전환 조건에 플레이어 컨트롤러가 캐릭터를 Possess했는지를 추가하여 캐릭터 없을 때 LifeState가 전환되는 것을 막아서 해결
2주차
리스폰 대기 동안 체력, 스킬, 탄창 UI 안보이도록 변경
OnRespawnedPlayerCharacter와 OnDeadPlayer를 DECLARE_MULTICAST_DELEGATE에서 DECLARE_DYNAMIC_MULTICAST_DELEGATE로 변경하고, UPROPERTY(BlueprintAssignable)도 추가해 BP에서 직접 바인딩 가능하도록 변경
HUDUIController에서 해당 UI들을 Hidden 처리하는 이벤트를 OnDeadPlayer에 바인드하여 리스폰 대기하는 동안 안보이도록 변경
HUDUIController에서 해당 UI들을 Non-Hit Testable 처리하는 이벤트를 OnRespawnedPlayerCharacter에 바인드하여 리스폰이 됐을 때 보이도록 변경
1주차
리스폰 UI 구현
서버에서 플레이어 사망이 감지되면 Client_NotifyDeadPlayerCharacter RPC로 킬러 이름을 클라이언트에 전달
클라이언트는 GameplayEventSubsystem::OnDeadPlayer 델리게이트를 브로드캐스트
새로 만든 UMFRespawnUIController가 이 델리게이트를 구독해 UI를 표시하고, UMFTimerUserWidget의 StartTimer()를 호출해 리스폰 카운트다운을 시작
- 리스폰 대기 시간은 UMFRespawnComponent에서 가져와 GameState에 리플리케이트하도록 변경하여 클라이언트가 사용할 수 있도록 구현
- KillerName과 타이머의 남은 시간을 위젯 BP에서 표시
정확성을 위해 타이머가 완료될 때가 아니라 리스폰 Notify Client RPC를 통해 브로드캐스트되는 OnRespawnedPlayerCharacter 델리게이트로 UI를 숨기고 타이머를 종료
UI 타이머 공용화
UMFAbilityUserWidget에 쿨타임 로직(Tick 감소, 상태 관리, BP 프로퍼티)이 모두 박혀 있었고, 다른 UI에서도 같은 기능이 필요해지면서 중복 작성이 불가피한 상황이 됐다.
UMFTimerUserWidget을 새로 만들어 CoolTime, RemainCoolTime, NativeTick 감소 로직, StartCooldown() / EndCooldown()를 모두 여기로 이전
UMFTimerUserWidget을 합성하여 OnStartCooldown / OnEndCooldown에서 타이머 함수를 호출하는 것으로 단순화
26년 3월
4주차
캐릭터 변경으로 무적 영향 없도록 변경
캐릭터 변경할 때 리스폰 로직을 실행하면서 리스폰이 아닌 상태(LifeState가 Invalid, Dead가 아닐 때)는 무적을 설정하지 않도록 변경
무적 해제 구현
타이머 만료 및 LifeState가 Normal로 변경될 때 무적 해제
PlayerState의 FGuid로 무적 타이머와 LifeState 변경 델리게이트를 저장하고, 무적이 취소될 때 해당 PlayerState FGuid의 무적 타이머와 LifeState 변경 델리게이트를 모두 제거
람다로 타이머가 만료되거나 LfieState가 변경될 때 캐릭터의 DamageHandlerComponent에 무적 요청
플레이어를 처치한 상대방을 붉은색 Outer Glow 실루엣으로 강조
상하좌우 4방향 이웃 픽셀의 Custom Stencil 값을 샘플링해 합산한 뒤, 현재 픽셀의 스텐실 값을 빼는 방식으로 경계 마스크를 생성하고, 마스크가 1인 픽셀에 GlowColor 적용하는 포스트 프로세스 머터리얼 구현
죽었을 때 죽인 캐릭터의 CustomDepthStencilValue를 1로 설정 및 활성화, 카메라 컴포넌트에 Blendable로 머터리얼을 등록
캐릭터가 파괴될 때 죽인 캐릭터의 CustomDepth를 초기화하여 리스폰됐을 때 보이지 않도록 구현
3주차
죽었을 때 죽인 캐릭터를 카메라가 주시하도록 구현
- 죽었을 때 틱 타이머에 바인딩하여 죽인 캐릭터를 주시하는 회전값을 FindLookAtRotation으로 찾음
- RInterpTo를 사용하여 현재 회전값으로부터 보간하여 회전
- 틱 타이머에 다시 바인딩
틱 타이머에 바인딩하는 이유는 죽기 전까지 틱에서 불필요한 if문 방지와 죽은 후에는 몇 초 후 리스폰되어 기존 캐릭터가 Destroy되기 때문
죽었을 때 InputBlock, 리스폰될 때 InputBlock 해제
플레이어 컨트롤러에서 플레이어 캐릭터가 죽음/리스폰 시점에 Client RPC를 호출하여 InputManager에서 Block/Block 해제
리스폰될 때 캐릭터가 Destroy 되도록 변경, 캐릭터 Destroy를 플레이어 컨트롤러로 이동
플레이어 컨트롤러에서 캐릭터 리스폰이 되면서 이전 캐릭터를 Destroy
리스폰 컴포넌트에서 캐릭터를 스폰하고, 컨트롤러에서 캐릭터들을 관리하여 스폰과 소멸의 책임을 PlayerController 혹은 RespawnComponent에서 관리
리스폰됐을 때 HP UI가 업데이트되지 않는 현상 수정
리스폰됐을 때 UI에 델리게이트를 전달하지 않아 UI가 업데이트되지 않음
플레이어컨트롤러에 캐릭터가 없었다가 SetPawn되는 시점을 InitializePlayerCharacter로 정의하고, 이 때 델리게이트를 브로드캐스트하고, UI에서 해당 델리게이트를 바인딩하여 업데이트
- 리스폰되는 시점에 브로드캐스트하려 했으나, Client RPC가 호출되는 시점에 아직 플레이어컨트롤러에 Pawn이 리플리케이트되지 않아 InitializePlayerCharacter 시점을 새로 정의
2주차
캐릭터가 죽었을 때부터 리스폰 타이머가 시작하도록 변경
리스폰 함수를 캐릭터 Destroy 델리게이트에 바인딩하는 것을 Dead 델리게이트에 바인딩하도록 변경
캐릭터 스폰할 때 탄창 업데이트 안되는 현상 수정
기존에는 UI에서 GeyPlayerPawn 함수로 컨트롤러->캐릭터->EquipmentComponent->Equipment 순서로 참조하여 탄창을 가져오는 로직
클라에서 캐릭터가 스폰되면서 Posses되기 전에 장비 장착이 되기 때문에 캐릭터를 못 가져오는 현상 발생
EquipmentComponent에서 Equip 델리게이트를 호출할 때 장비를 전달하여 UI에서 장비를 직접 참조하여 탄창을 가져오도록 변경
InGame 매치 상태가 됐을 때 Grace LifeState가 되도록 구현
PlayerState가 변경 GameState 델리게이트에 바인딩하여 InGame이 됐을 때 LifeState를 Grace로 초기화하여 정상적으로 LifeState가 진행되도록 구현
1주차
시작할 때 캐릭터 선택을 안한 플레이어는 기본 캐릭터로 스폰되도록 구현
InGame 매치가 될 때 게임모드가 플레이어 배열을 리스폰 컴포넌트에 전달하여 캐릭터를 스폰하지 않은 플레이어는 기본 캐릭터로 스폰하도록 변경
26년 2월
3주차
캐릭터 변경했을 때 카메라 방향으로 약간 움직인 위치에서 스폰되는 현상 수정
캐릭터를 스폰할 때 캐릭터가 지형에 통과되는 상황을 방지하기 위해 ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn로 스폰하는데, 기존 캐릭터와의 충돌 때문에 점점 위치가 카메라 방향으로 움직여서 스폰되는 현상이 발생
새로운 캐릭터를 스폰하기 전에 기존 캐릭터의 충돌을 끈 후 스폰하여 현상 방지
MatchState가 Lobby일 때 캐릭터 변경 UI가 Hide되는 현상 수정
LifeState가 Normal일 때 UI가 Hide되는데, MatchState가 Lobby일 때에도 LifeState가 Grace에서 타이머가 발동하여 Normal로 전환되기 때문
MatchState가 Lobby일 때 LifeState가 Grace에서 변경되지 않도록 구현
캐릭터를 직접 변경할 때 계속 스폰되는 현상 수정
죽었을 때 실행되는 Delay 스폰 로직에서 LifeState가 Dead가 아닐 때만 호출하도록 검사 추가
캐릭터를 변경할 수 있을 때에는 죽지 않는다는 가정으로 캐릭터를 변경할 때 Delay가 아닌 즉시 스폰이 이루어지기 때문에 중복 호출 방지
하드코딩된 리스폰 DelayTime을 변수화하여 모드마다 다양한 시간을 설정할 수 있도록 변경
2주차
플레이어 캐릭터를 리스폰하는 로직을 컴포넌트로 이동
리스폰 시스템을 사용하지 않는다고 명시하거나 별도의 리스폰 시스템을 사용하는 모드를 제외한 모드는 리스폰 시스템이 적용되기 때문에 TDM 게임모드에 있던 리스폰 로직을 다른 게임모드도 사용할 수 있도록 컴포넌트로 분리하여 역할 분리 및 코드 중복 방지
캐릭터를 런타임에 바꿀 수 있도록 시스템 변경
기존에는 캐릭터와 플레이어컨트롤러가 쌍으로 구성돼야 하며, 플레이어컨트롤러에 캐릭터와 관련된 데이터가 고정되어 런타임에 캐릭터를 변경할 수 없었음
따라서 플레이어컨트롤러를 여러 캐릭터가 사용할 수 있도록 구체적인 사항들을 제거하여 범용적으로 설계하고, 캐릭터를 선택했을 때 캐릭터 타입을 바탕으로 에셋에서 캐릭터 관련 데이터를 가져와 캐릭터를 설정할 수 있도록 변경
- 캐릭터가 변경될 때 기존 어빌리티를 모두 해제하고, 새로운 어빌리티를 등록
Match 상태와 Life 상태를 정의하고, 가능한 Match 상태와 Life 상태일 때만 UI를 킬 수 있도록 OwningPlayerController를 통해 검사
매치 상태나 Life 상태가 변경될 때 릭터를 변경하지 못하도록 캐릭터 UI가 사라지도록 구현
1주차
어빌리티 액터의 Relevancy를 Owner와 관련하도록 설정
어빌리티는 서버와 autonomous Proxy만 알고 있어야 하기 때문에 리플리케이트 되는 클라이언트를 autonomous Proxy로 제한
26년 1월
4주차
서버 RPC를 위해 DamageType을 DataAsset으로 변경
기존 UObject로 구현된 DamageType은 데이터 입력 편의를 위해 DefaultToInstanced, EditInlineNew 메타데이터를 설정했으나 해당 인스턴스들을 서버가 모르는 문제 발생
DataAsset으로 구현하여 서버에서 이미 로드된 DamageType을 사용하여 패킷 비용을 줄임
데미지에 필요한 동적인 데이터는 구조체로 분리하여 따로 전달
25년 12월
2주차
데미지 타입 및 처리 리팩토링
언리얼 엔진의 기본 UDamageType 클래스 기반 데미지 시스템을 커스텀 UMFNewDamageType 시스템으로 전면 리팩토링
기존에는 TSubclassOf<UDamageType>과 FDamageEvent를 사용하여 데미지를 처리했으나, 이를 인스턴스 기반의 유연한 구조로 변경
기존 시스템의 문제점
- UDamageType은 CDO(Class Default Object) 기반으로 동작하여 런타임에 동적인 데미지 값 설정이 제한적
- FDamageEvent, FPointDamageEvent, FRadialDamageEvent 등 언리얼 기본 이벤트 구조에 의존하여 커스텀 확장이 어려움
- 데미지 계산 시 BaseDamage를 함수 파라미터로 전달해야 해서 데미지 타입과 데미지 값이 분리되어 관리됨
주요 변경 사항
- DefaultToInstanced와 EditInlineNew 지정자를 사용하여 에디터에서 인라인 인스턴스 생성이 가능하도록 선언
- BaseDamage 파라미터가 제거되고, 데미지 타입 객체 내부에 데미지 값이 포함되는 구조로 변경
- FDamageEvent 의존성을 제거하고, FDamageEvent에서 필요했던 HitBoneName을 ApplyPointDamage API에서 DamageType에 설정하도록 변경
1주차
외부에서 장비 포인터를 직접 참조하는 방식을 FEquipmentHandle로 장비 컴포넌트에 요청하는 방법으로 리팩토링
장비 컴포넌트에서 장비 Raw 포인터를 반환하고 있어 장비 컴포넌트가 장비를 완전히 제어하지 않고 있었음
따라서 외부에서 FEquipmentHandle를 갖고 있도록 구현하고, 장비 컴포넌트에 FEquipmentHandle을 전달하여 해당 장비를 컴포넌트가 제어하도록 변경
25년 11월
4주차
시로코 캐릭터는 자폭 데미지를 입지 않도록 구현
캐릭터의 상태를 관리하는 StatusComponent 기초 설계
캐릭터의 상태는 FGameplayTag로 나타냄
추후 CC 등에 활용될 수 있음
Status를 추가하는 범용 어빌리티를 설계 후, Status_SelfExplosionDamageImmunity 상태를 추가
Status_SelfExplosionDamageImmunity를 가진 캐릭터가 DamageHandlerComponent에서 데미지 처리 요청이 왔을 때 스스로의 스킬에 의한 것이라면 데미지 처리를 종료하도록 구현
수류탄의 폭발 및 섬광 기능을 ExplosionComponent로 통합 리팩토링
폭발이 수류탄에 국한됐다가 드론 미사일에도 사용해야 하므로 폭발 기능을 담당하는 액터 컴포넌트 ExplosionComponent로 통합하기로 결정
섬광탄에 맞았을 때 이속 감소 및 포스트 프로세스에 섬광 머터리얼을 적용해야 하므로 컴포넌트의 GameplayEffect가 유효할 경우 폭발에 맞은 액터에 GameplayEffect를 적용
GameplayEffect에서 이속 감소를 적용하고, 포스트 프로세스에 섬광 머터리얼을 적용하는 GameplayCue를 실행하도록 구현
수류탄의 경우 충격 신관에 의해 폭발할 경우 데미지가 반감되고, 거리가 멀어질 수록 데미지가 줄어듦
충돌돼서 폭발됐을 경우의 플래그를 인자로 전달받아 기본 데미지에 충격 신관 데미지 비율을 곱하여 계산
거리에 따른 데미지 계산은 데미지 계산을 담당하는 UMFExposionDamageType 클래스를 DamageHandlerComponent에 전달한 뒤 UMFExposionDamageType에서 계산한 거리로 UCurveFloat에서 얻은 데미지 비율로 곱하여 계산하도록 구현
- 에디터에서 UCurveFloat와 UMFExposionDamageType을 생성하고 ExplosionComponent에 주입하여 코드 수정 없이 다양한 데미지 계산이 가능
캐릭터 Die 시점에 각 컴포넌트나 인터페이스의 함수를 직접 호출하는 것을 델리게이트를 사용하여 리팩토링
캐릭터 OnDie 함수에서 각각의 컴포넌트나 인터페이스의 함수를 호출하여 함수가 비대해지고, 결합도가 높아지기 때문에 리팩토링 진행
OnDie 시점에 델리게이트를 호출하고 캐릭터의 OnDie 시점을 필요하다면 적절한 시점에 델리게이트 바인딩하여 결합도를 낮춤
UI에서 바인딩하는 델리게이트를 HUD에서 GameplayEventSubsystem으로 이동
아래의 이유로 리팩토링을 진행
- NativeConstruct, NativeOnInitialized 시점에 HUD가 초기화되는 것이 보장되지 않음
- UserWidget, UIController에서 HUD의 델리게이트를 직접 참조하는 것은 하위 모듈에서 상위 모듈을 참조하는 것은 적절하지 않음
3주차
시로코 드론 미사일 폭발 시스템 구현
ExplosionComponent에서 폭발을 담당하도록 구현
- 폭발 거리를 바탕으로 현재 오너 위치에서 구 모양으로 MultiSweep하여 Block되는 액터들에 데미지를 처리
나이아가라 이펙트 적용
- 벽, 땅 등에 사격했을 때 구멍, 연기 효과 적용
총알이 라인트레이스 판정했을 때 HitActor가 StaticMesh일 때 나이아가라 시스템 스폰 - 헤드샷/바디샷 피격 이펙트 구분하여 적용
총알 라인트레이스 판정했을 때 BodyType에 따라 구분하여 나이아가라 시스템 스폰 - 시로코 드론 미사일 폭발 이펙트 적용
Projectile이 Explode될 때 나이아가라 시스템 스폰
2주차
반응성 연막탄 렌더링 최적화
연막이 생성될 때 GPU Time 600ms -> 20ms
런타임에 연기 생성 및 구멍을 만들기 위해서는 모든 복셀의 Vanish RenderTarget을 그려서 복셀을 베이킹
기존에는 PostInitializeComponents 시점에 모든 복셀마다 RenderTarget을 그려서 드로우콜이 많이 발생하여 몇 초간 화면이 멈춤
연기가 1초 동안 퍼져나가기 때문에 해당 시간 동안 여러 Tick에 걸쳐 소멸 RenderTarget을 그리도록 변경
폭발 GPU Time 200 ms -> 20ms
기존에는 폭발에 영향을 받은 복셀마다 복셀의 중점과 반지름을 Material에 전달하여 구멍 RenderTarget을 그려 드로우콜이 많이 발생
폭발원에 해당하는 점의 위치와 폭발 거리를 통해 연막에 미치는 거리를 Material에 전달하여 구멍 RenderTarget을 한 번만 그려 한 번의 드로우콜로 폭발 구멍을 렌더링
25년 10월
5주차
서버에서 캐릭터가 리스폰될 때 무기가 장착되지 않는 현상 수정
EquipmentComponent에서 장착된 무기를 장착하라고 요청이 들어왔을 때 장착하고, 기존 장비와 같기 때문에 장비를 Activate하지 않아 발생
무기를 장착할 때 현재 장착된 장비와 같다면 요청을 종료하도록 변경
스킬을 사용한 상태에서 리스폰될 때 스킬이 활성화돼있는 현상 수정
리스폰되면서 스킬(어빌리티)가 초기화될 때 어빌리티를 비활성화하도록 변경
팀 인디케이터 구현
플레이어 캐릭터에 팀 인디케이터 Sphere 스태틱 메시 컴포넌트를 추가
캐릭터가 장애물에 가려져도 인디케이터가 보이기 위해 Disable Depth Test를 체크한 머터리얼을 적용
컴포넌트에서 아군 플레이어 캐릭터의 인디케이터가 아니라면 DestoryComponent
- WidgetComponent의 Screen 모드는 각 클라이언트의 로컬 화면에만 렌더링되는데, 서버(호스트)는 렌더링되지 않기 때문에 사용하지 않음
- HUD의 위젯은 NativeTick 시점에 Target 액터 위 위치를 업데이트하는 것이 1 프레임 지연되기 때문에 사용하지 않음
4주차
연막에 위치한 적 표시 UI 구현
연막탄을 던진 플레이어에 한해서 연막에 1초 이상 오버랩된 플레이어 캐릭터 수를 표시하는 UI 구현
연막 액터의 Instigator가 LocalPlayerController(연막탄을 던진 플레이어)일 때 OnBeginOverlap 시점에 다른 팀이 오버랩 될 경우 1초 타이머 실행 후 OverlapCount 증가, OnEndOverlap 시점에 Overlap된 액터가 있었다면 OverlapCount 감소
OverlapCount가 변경될 때마다 델리게이트를 BroadCast
어빌리티 위젯에서는 UIAbilityDataAsset에서 어빌리티 태그의 AdditionalWidget이 있다면 해당 슬롯에 추가
UMG 블프 노드에서는 델리게이트에 바인딩하고, 함수가 호출됐을 때 OverlapCount를 텍스트로 표시
클라에서 장비를 장착하는 어빌리티가 활성화되는 중에 다시 스킬을 눌렀을 때 주무기 조작이 안되는 현상 수정
스킬 키를 누르거나, 장비가 비활성화될 때 해당 어빌리티가 비활성화되고, 어빌리티가 비활성화되면 주무기를 장착함
장비 컴포넌트에서 현재 장비가 리플리케이트될 때 기존 장비를 Deactivate시키고, 현재 장착된 장비를 Activate시킴
클라에 DeAcitvate는 동기화되지만 Activate는 동기화되지 않음
동일한 장비로 두 번 장착하는 요청이 들어왔을 때 두 번째 요청 처리의 서버에서 동일한 장비를 DeActivate시키고, Activate시키는데, 변수의 수정은 없으므로 리플리케이트 콜백 함수는 호출되지 않아 DeActivate만 동기화되어 조작이 되지 않았음
따라서 기존 장비와 현재 장비가 동일할 때는 장비의 비활성화, 활성화를 하지 않도록 구현하고, 어빌리티에서는 이미 비활성화됐을 때 비활성화 처리를 또 동작하지 않도록 변경
2주차
uasset 확장자 파일 LFS 필터 적용
DamageCauser가 유효하지 않아도 데미지를 입도록 변경
자폭 같은 플레이를 허용하도록 객체가 죽거나 파괴됐을 때도 데미지를 입을 수 있도록 검증 코드 제거
1주차
이즈나 다용도 수리검 구현
장비를 장착하고 마우스 왼쪽 클릭시 실명 수리검, 오른쪽 클릭시 연막 수리검을 발사하도록 구현
ProjectileMovementComponent::ProjectileGravityScale을 0으로 설정하여 직선으로 발사되도록 설정
실명은 섬광탄으로, 연막은 반응성 연막탄으로 기존 시스템을 재활용
GameplayCue 시스템 설계
게임플레이 상태에 영향을 미치지 않는 이펙트(섬광 등)에 사용될 GameplayCue 시스템을 설계
GameplayCueComponent로 GameplayCue를 관리
- GameplayCueComponent를 통해 GameplayCue를 적용할 때 로컬에서 실행돼야 하므로 해당 캐릭터가 LocallyControll일 경우 GameplayCue를 적용, 그렇지 않고 서버 액터일 경우 클라 RPC를 통해 GameplayCue를 적용
- GameplayCueComponent에서 RPC를 실행했을 때 어썰트 발생하는 현상 수정
리플리케이트를 설정하지 않고, CreateDefaultSubObject로 생성하지 않았기 때문에 IsSupportedForNetworking 함수가 false를 반환하여 어썰트 발생
플레이어 캐릭터만 컴포넌트가 필요하므로 리플리케이트를 설정하여 해결
Throw 장비에서 TriggerType에 따라 발사하는 Projectile를 다르게 설정할 수 있도록 변경
TMap<ETriggerType, TSubclassOf<AMFProjectile>> 자료구조로 변경하고, 장비에 Trigger 이벤트가 들어왔을 때 Trigger 타입에 따른 Projectile 클래스를 스폰하도록 변경
클라이언트에서 수류탄이 흔들리면서 이동하는 현상 수정
클라이언트의 ProjectileMovement의 Velocity를 설정하지 않고, 액터의 ReplicateMovement를 True로 설정하여 서버의 위치를 동기화하면서 위치가 리플리케이트될 때마다만 액터의 위치를 설정하기 때문에 매 프레임 위치를 변경하지 않기 때문에 자연스럽게 이동하지 않고 흔들리면서 이동하게 됨
패킷 전달 횟수를 줄이기 위해 ReplicateMovement + 보간 방법 보다는 Velocity를 각 클라이언트에 동기화하는 방식을 사용하여 해결
25년 9월
5주차
수류탄이 방패에 닿을 시 Destory되는 현상 수정
Projectile이 Overlap될 때 Destroy된 코드가 있어서 발생
해당 코드는 필요없으므로 삭제
Projectile 콜리전 프리셋을 만들고 Block되게 설정, 오브젝트마다 필요하면 Overlap, Ignore를 커스텀하게 설정
수류탄이 충돌됐을 때 튕기지 않아 ProjectileMovement::bShouldBounce를 True로 설정
플레이어 캐릭터 리스폰 시 리플리케이트 타이밍 이슈로 스킬 UI를 생성하지 않는 현상 수정
클라에 컨트롤러의 Pawn보다 어빌리티가 더 빨리 리플리케이트되어 어빌리티 UI에서 컨트롤러의 폰을 찾을 수 없어 발생
UI에서 어빌리티 태그를 Pending하여 어빌리티가 리플리케이트될 때, Pawn이 리플리케이트될 때 모두 Pending된 어빌리티 태그를 처리하도록 변경
4주차
기존 float Delay 값을 이용한 타이머 기반의 수류탄 발사 로직을 AnimNotify 기반으로 변경
기존 문제점
- 부정확한 타이밍 : 애니메이션 재생 속도가 변경되거나, 다른 애니메이션과의 블렌딩 과정에서 미세한 시간 차이가 발생할 경우, 캐릭터의 손에서 수류탄이 떠나는 시점과 실제 발사 시점이 어긋나는 문제가 발생할 수 있음
- 어려운 유지보수 : 발사 타이밍을 조절하기 위해선 항상 애니메이션을 확인하고 코드의 float 값을 수정해야 ㅎ마
애니메이션 몽타주에서 해당 프레임에 SendGameplayEvent_AnimNotify를 추가
장비에서 Throw GameplayEvent를 받을 때 수류탄 발사 로직 실행
로컬에서만 애니메이션이 실행되므로 로컬에서만 이벤트를 받을 수 있도록 구현
- 정확성 향상 : 애니메이션의 특정 프레임에서 발사 로직이 실행되므로, 캐릭터의 모션과 발사 타이밍이 완벽하게 일치
- 유지보수 향상 : 이제 발사 타이밍을 조절해야 할 때 코드를 수정할 필요 없이, 애니메이션 에디터에서 노티파이의 위치만 옮기면 되므로 훨씬 직관적이고 빠른 수정이 가능
- 코드 가독성 증가 : 타이머와 관련된 복잡성이 사라지고, 각 함수의 역할이 명확해져 코드의 가독성 상승
OwnerCharacter를 캐싱하여 발생한 크래시 수정
캐릭터는 죽었을 때 Destroy된 후 리스폰될 때 생성됨
어빌리티는 Controller에서 소유하여 캐릭터를 캐싱할 경우 댕글링이 발생하므로 캐릭터가 필요할 때마다 가져오도록 수정
총과 같이 캐릭터가 소유하는 오브젝트는 보통 BeginPlay에서 캐싱하는데, 게임이 이미 시작됐으므로 클라이언트는 Owner가 리플리케이트되는 것보다 BeginPlay가 먼저 호출되므로 Owner가 nullptr인 상황이 발생하여 캐릭터를 필요할 때마다 가져오도록 수정
넉백 시스템 구현
타겟을 특정 방향, 거리, 각도로 밀어내는 넉백 시스템을 구현
포물선 운동 공식을 사용하여 LaunchCharacter에 필요한 벡터 계산
- 포물선 운동에서 유도된 R = v0^2 * sin(2θ) / g에서 v0 = sqrt(R * g / sin(2θ))를 통해 벡터 계산
- 거리와 각도가 주어졌을 때 주어진 수평 거리만큼 넉백돼야 하므로 위 수식을 사용
3주차
와카모 불여우 추적 놓치지 않아 패시브 구현
합연산 퍼센트인 AdditivePercent를 EGameplayModifyOperation에 추가
StatisticBaseValue의 수치 퍼센트만큼 증가
StatisticGameplayEffect Release 구현
StatisticGameplayEffect::Apply에서 Duration이 유효할 때 CurrentValue에 대한 변화량을 저장한 후 Release에서 해당 StatisticSet에 변화량만큼 감소하여 복구
아야네 화력지원 RPC 횟수 최적화
장비 장착, 드론 스폰, possess를 각각 클라에서 RPC로 요청하여 총 6회 RPC 실행
서버 어빌리티에서 장비 장착, 드론 스폰, possess를 요청하여 총 2회 RPC로 최적화
25년 8월
5주차
와카모 불여우 추적 스킬의 추적 모드 구현
근접 공격 보정 시스템 구현
캐릭터를 중심으로 일정 거리 이내의 Damageable 트레이스 채널로 오버랩되는 액터들을 구하고 근접 공격 조건에 부합하는지 체크
- 일정 시야각 이내인지 : AttackerForwardVector와 AttackerToTargetVector를 내적한 값이 시야각의 cos 값보다 큰 경우
- 죽지 않은 상태
- 같은 팀이 아닌 액터
- LOS가 만족하는지 : 액터의 중앙, 좌, 우 세 점을 카메라로부터 라인트레이스하여 블락되지 않는 점이 2개 이상인지
보정으로 타겟을 구했다면 플레이어가 타겟을 바라보도록 회전
- 타임라인을 사용하여 타겟을 바라보는 FRotator까지 보간하여 회전 설정
4주차
와카모 불여우 추적 스킬의 투척 모드 구현
3주차
AbilityTask, GameplayEvent 시스템 구현
어빌리티에서 각 Task를 재사용하고 유지보수를 위해 AbilityGameplayTask 시스템을 도입
static 함수를 통해 AbilityTask를 생성하고, 프로젝트의 커스텀 AbilityComponent를 사용
AbilityComponent에서 TMap<FGameplayTag, FGameplayEventDelegate> 자료구조 관리
각 객체에서 AbilityComponent의 GameplayTag 이벤트에 등록한 후 이벤트가 발생했을 때 유틸리티 함수로 AbilityComponent에 전달하여 등록된 객체까지 전달되도록 구현
WaitGameplayEvent 어빌리티 태스크로 어빌리티 컴포넌트에 등록, 해제를 할 수 있도록 RAII 기법 적용
Activate 시점에 어빌리티 컴포넌트에 델리게이트를 등록하고, DeActivate 시점에 어빌리티 컴포넌트에 델리게이트를 제거하고, 태스크 객체를 Destroy
Grenade 클래스에 있는 Throw 관련 로직을 ThrowEquipment로 이동 및 리팩토링
2주차
아야네 화력지원 드론 이동 반경 제한
스폰 위치와의 거리가 제한 거리 이상일 때 StartLocation + (ActorLocation - StartLocation).GetSafeNoraml() * MaxMoveDistance로 위치 설정하여 움직이지 못하는 것처럼 구현
아야네 화력지원 어빌리티에서 드론을 Possess하여 드론을 조종할 수 있도록 구현
FamiliarComponent에서 드론을 스폰하고, 클라에 드론이 리플리케이트될 때 델리게이트를 호출하도록 구현하여 타이밍 최적화
드론 리플리케이트 델리게이트가 호출됐을 때 PlayerController의 서버 RPC를 호출하여 드론을 Possess
드론에 MFInputComponent를 추가하여 독립적인 드론 Input 시스템 동작
어빌리티에서 드론을 Possess 하기 전 플레이어 캐릭터를 저장하고, 어빌리티가 비활성화될 때 저장한 플레이어 캐릭터를 Possess하여 복구
명령 패턴을 활용하여 Input 시스템 리팩토링
기존에는 각 InputActon마다 직접 함수를 바인딩하여 Input이 추가될 때마다 코드를 작성해야 하고, 비슷한 코드가 증가하는 문제 발생
InputConfig에서 InputAction을 GameplayTag에 바인딩하고, InputComponent에서 InputCommand를 GameplayTag에 바인딩하여 같은 GameplayTag의 InputAction이 입력됐을 때 InputCommand를 실행
연막탄이 수류탄에 반응하지 않는 버그 수정
수류탄 폭발에 영향을 받는 Voxel을 통해 Hole 머터리얼 파라미터를 설정까지만 구현돼있고 Draw Render Target이 없어서 적용되지 않음
수류탄 폭발에 영향을 받는 Voxel마다 Draw Render Target을 호출
아야네 화력지원이 끝날 때 드론 파괴되도록 변경
FamiliarComponent에서 소환수를 관리하고 있으므로 드론을 파괴하도록 FamiliarComponent의 서버 RPC를 호출
25년 7월
4주차
연막탄 Flood Fill 알고리즘 최적화
재귀->BFS, 미리 필요한 값을 캐싱하여 26ms->6ms로 개선
3주차
아야네 쉴드 전개 드론 및 쉴드 구현
시작 위치에서 타겟 위치까지 타임라인으로 보간하여 드론 3m 수직 상승을 구현
3m 수직 상승 후 드론이 쉴드를 생성
쉴드는 DamageHandlerComponent로 무적을 설정하여 모든 데미지를 흡수
등록된 StatisticSet의 데이터로 초기화할 수 있도록 변경
액터마다 다른 체력을 적용하기 위함
StatisticSet을 Blueprintable로 선언하고 한정된 데이터(ex. HP)를 블루프린트에서 입력할 수 있도록 선언
블루프린트에서 입력한 데이터를 바탕으로 Statistic을 초기화하도록 구현
연막이 수류탄 폭발에 의해 지워지도록 구현
폭발될 때 폭발의 범위에 있는 액터에 인터페이스로 폭발 함수 호출
폭발에 영향받았을 때 폭발 위치와 범위에 영향받은 Voxel 위치에 Hole 머터리얼 파라미터를 변경하여 연기가 사라지도록 구현
연기 액터의 틱마다 DeltaTime만큼 Hole 머터리얼의 Radius와 Density를 줄여 연기 복원
연막탄 Voxel에 VFX 적용
Voxel마다 Render Target을 사용하여 런타임에 머터리얼을 변경할 수 있도록 구현
임시 연기 머터리얼 사용
연막탄과 연기를 생성하는 액터 구현
연막탄이 던져지고, 연막탄이 터질 때 연기를 생성하는 오브젝트를 스폰
반응성 연막탄에 사용될 연기 생성을 위한 Flood Fill 알고리즘 구현
연막탄이 터진 지점을 연기원으로 설정하고, 해당 Voxel에서 인접한 Voxel로 퍼져나가며 볼륨을 채움
- 연기원 위치에는 조건 없이 무조건 Voxel이 생성
재귀로 6방향을 탐색하며 연기 생성 조건을 만족하는지 체크
- 최대 스모크 부피 제한
- 그리드가 장애물에 충돌되는 경우 퍼지지 않아 현실적으로 벽을 통과하지 않고 장애물을 감싸듯 채워짐을 보장
디버그 용도로 연기 Voxel마다 Cube StaticMeshComponent를 추가
2주차
아야네 화력 지원 스킬의 드론 소환 및 카메라 드론 시점 전환까지 구현
Input으로 어빌리티가 활성화될 때 태블릿 장비 장착 및 드론 소환 어빌리티 실행
드론이 소환되어 리플리케이트 됐을 때 플레이어 컨트롤러의 ViewTarget을 드론으로 설정
스킬 지속시간이 종료됐을 때 ViewTarget을 플레이어 캐릭터로 변경하고, 소환한 드론 제거 및 어빌리티 종료
소환수 관리 리팩토링
어빌리티에서 소환수를 스폰하고 있었는데 소환수에 대한 관리가 어려워 컴포넌트로 이동하고, 어빌리티는 컴포넌트에 스폰 요청
컴포넌트에서 소환수를 스폰하고, 스폰환 소환수를 저장하고, 소환수를 Destroy하도록 변경
아야네 체력&탄약 보충을 즉발로 변경
기획 변경에 따라 스킬 사용에 따른 중간 메커니즘 없이 바로 GameplayEffect가 적용될 수 있도록 변경
캐릭터 선택 순서 저장 구현
캐릭터 선택 레벨과 플레이 레벨의 게임 모드가 다르므로 캐릭터 선택 순서를 캐릭터 선택 게임 모드에서 서버 게임 인스턴스에 저장
- 플레이어마다 UnqieuNetId가 다르므로 UnqieuNetId 키로 TMap에 캐릭터 선택 데이터를 저장
플레이어의 UniqueNetId를 통해 캐릭터 선택 순서를 가져오도록 함수 생성
팀원의 목록을 표시하기 위해 GameState에서 같은 팀 캐릭터를 반환하는 함수 구현
GameState의 PlayerArray에서 전달 받은 PlayerState와 TeamIndex가 같은 PlayerState의 Controller의 캐릭터를 반환
1주차
플레이어 정보 UI를 Profile로 분리
HUDUIController에 있던 플레이어 정보 위젯들(썸네일, 베리어, 체력, 이름)을 Profile 위젯으로 분리
Profile 위젯에서 썸네일, 베리어, 체력, 이름 위젯이 각각 있을 때 해당 업데이트를 처리
위젯을 분리하여 HUD 뿐만 아니라 아야네 태블릿 UI에도 재사용하기 위함
아야네 체력&탄약 스킬 구현
Input이 Press 됐을 때 태블릿 장착 어빌리티 실행
Input이 트리거 됐을 때 태블릿 장비에서 체력&탄약 지원 어빌리티 실행
- Input Action의 Hold Time Thresold를 0.1로 설정하여 클라이언트의 장착 리플리케이트 지연에도 Trigger를 받을 수 있도록 구현
어빌리티에서 1초 타이머를 설정하고 1초 후에 체력, 탄약을 증가시키는 GameplayEffect 적용
- 1초 전에 장비에서 Release Input Trigger가 됐을 때 체력&탄약 지원 어빌리티를 취소하여 GameplayEffect가 적용되지 않도록 구현
어빌리티에 Input Start/Trigger/Complete 이벤트 전달하는 시스템 구현
더 이상 Input을 했을 때 어빌리티가 계속 활성화되지 않도록 수정
Input을 받았을 때 Input TriggerEvent(Start/Trigger/Released)를 어빌리티 컴포넌트를 통해 어빌리티에 전달
어빌리티가 각 Input 함수에 따라 로직을 재정의할 수 있도록 설계
DamageHandlerComponent에 데미지 처리 요청을 보내는 로직을 통일
기존에는 UGameplayStatics::ApplyDamage를 호출하여 액터마다 TakeDamage를 오버라이드하여 DamageHandlerComponent 함수를 호출했어야 함
우리 프로젝트 프레임워크에 맞게 DamageHandlerComponent 함수를 호출하는 UMFGameplayStatics::ApplyDamage를 구현
25년 6월
4주차
무적, Dead 처리 로직을 캐릭터에서 DamageHandlerComponent로 이동
데미지 처리 로직이 분산된 것을 역할에 맞게 DamageHandlerComponent로 이동
무적, Dead를 캐릭터 뿐만 아니라 DamageHandlerComponent를 소유한 액터도 처리할 수 있도록 변경
클라에서 어빌리티 UI에 WidgetSwitcher가 보이지 않는 현상 수정
다른 어빌리티가 등록/해제되면서 어빌리티 UI를 정렬하는데, 이 때 ClearChildren -> AddChild를 수행
AddChild가 되면서 NativeConstruct가 호출되는데, NativeConstruct에서 WidgetSwitcher를 Collapsed로 설정하고 있었기 때문에 발생
WidgetSwitcher는 UI가 초기화될 때 Collapsed로 설정해야 하므로 NativeConstruct가 아닌 NativeOnInitialized에서 설정하도록 변경
클라에서 어빌리티 UI가 추가/삭제되지 않는 현상 수정
서버에서 어빌리티가 등록됐을 때 호출하는 Notify 함수가 클라 RPC로 선언되지 않아서 HUD가 없기 때문에 발생
클라 RPC로 선언하여 패킷을 보내도록 수정
아야네 원격지원 설치 어빌리티 구현
어빌리티를 실행하면 설치 프리뷰 액터를 스폰하거나 스폰이 이미 됐다면 Activate하여 화면에 보임
한 번 더 어빌리티를 Input하면 설치 프리뷰 액터 위치에 설치할 액터를 스폰하고 설치 프리뷰 액터를 DeActivate하여 화면에 보이지 않음
설치 프리뷰 액터 움직임 구현
에임이 2m 이내의 지형을 바라봐야 프리뷰 액터가 표시되도록 구현
프리뷰 액터가 지형에 위치해도 에임에 따라 이동이 가능해야 하기 때문에 콜리전을 사용하지 않고 라인트레이스 기반의 커스텀 충돌을 구현
- ViewLocation에서 ViewRotation 방향으로 라인트레이스를 추적
- 라인 트레이스에 충돌한 오브젝트가 WorldStatic이고, 거리가 2m일 때 위치 계산 수행
위 조건을 만족하지 않는다면 프리뷰 액터가 보이지 않도록 구현 - HitResult의 ImpactNormal(평면의 법선벡터)를 활용하여 충돌된 오브젝트의 형태를 검사
- 천장과 벽에 충돌됐을 때는 충돌된 위치 + ImpactNormal * BoundExtent로 위치 설정(지형과 프리뷰 액터가 겹치기 때문에 바운딩 박스만큼 조절)
바닥에 충돌됐을 때는 바닥 높이의 캐릭터 위치에서 에임 방향으로 2m 떨어진 위치로 설정
위에서 설정한 위치로 라인트레이스를 한번 더 추적하여 벽과 충돌할 경우 위치를 변경하지 않음(에임이 2m 이내로 고정하고 캐릭터를 벽 쪽으로 이동할 경우 프리뷰 액터가 벽을 뚫는 현상 방지)
3주차
아야네 원격 지원 바리케이드 구현
액터의 기능들(피격 시 파괴, 떨어질 때 캐릭터가 맞으면 즉사) 블루프린트 그래프에서 구현
아야네 원격 지원 체력/탄약 디스펜서 구현
액터의 기능들(피격 시 파괴, 지속 시간 지난 후 파괴) 블루프린트 그래프에서 구현
설치 프리뷰 액터를 대비하기 위해 디스펜서의 장판(GameplayEffectProvider)을 Visibility를 BeginPlay에 true로 설정
GameplayEffectProvider에서 GameplayEffect를 여러 개, 여러 번 적용할 수 있도록 변경
DamageHandlerComponent 관련 리팩토링
액터도 데미지 시스템을 사용할 수 있도록 캐릭터에 있는 로직을 DamageHandlerComponent로 이동
컴포넌트에서 DamageType 객체에 데미지 계산을 요청하여 DamageType에 따라 데미지를 계산
시작할 때 플레이어 체력 UI가 동기화되지 않는 현상 수정
StatisticSet의 초기화 델리게이트에 바인딩하기 전에 브로드캐스트되어 받지 못함
따라서 UI에서 NativeConstruct 시점에 업데이트하도록 변경
어빌리티 UI의 색상을 블루프린트 그래프에서 지정할 수 있도록 변경
프로그래머의 개입 없이 UI 디자이너가 수정할 수 있게 하기 위함
어빌리티가 사용 가능/불가능 상태를 반환하는 함수를 블루프린트에서 호출할 수 있도록 제공
Hex String을 Linear Color로 변환하는 함수를 블루프린트에서 호출할 수 있도록 제공
블루프린트 그래프에서 어빌리티 사용 가능 여부에 따라 Hex String으로 어빌리티 아이콘 색상을 설정할 수 있도록 제공
2주차
동일 장비가 여러 번 등록되지 않도록 수정
같은 클래스의 장비가 2번 이상 등록되지 않도록 현재 등록된 장비들을 검사 후 등록된 장비의 클래스면 등록하지 않도록 변경
불필요한 스폰 및 메모리 감소
어빌리티 위젯이 계속 추가되는 현상 수정
어빌리티를 UnRegister했을 때 UI에 전달하여 해당 어빌리티 위젯을 삭제하도록 변경
어빌리티 UI에서 위젯을 생성할 때 해당 어빌리티가 이미 생성됐으면 생성되지 않도록 변경
어빌리티 등록을 해제할 때 어빌리티를 Destory하지 않고 재등록할 때 캐싱한 어빌리티를 등록하도록 변경
등록한 어빌리티를 캐싱하여 재등록할 때 스폰 오버헤드를 줄이기 위함
장비를 장착/해제할 때 장비 어빌리티를 등록/해제
장비와 관련있거나 장비에 의존하는 어빌리티를 장비에서 등록/해제 요청
캐릭터가 아닌 장비에 결합도를 부여하여 장비에 따라 어빌리티를 달리 가질 수 있도록 하기 위함
1주차
장비 장착 어빌리티의 사용 가능 횟수를 UI에 전달하여 표시
장비 사용 가능 횟수를 장비 장착 어빌리티에 전달하고, UI에 전달하여 표시
클라에서 장비 사용 가능 횟수를 알기 위해 어빌리티에서 관련 장비를 리플리케이트로 동기화
어빌리티 UI를 Input KeyName에 따라 등록
어빌리티의 GameplayTag를 통해 플레이어 Input에 바인딩된 KeyName을 찾고 KeyName이 등록된 어빌리티만 UI 생성
KeyName에 의해 정렬하여 HorizontalBox의 Child 순서를 조정
어빌리티를 로컬에서 실행하도록 변경
빠른 어빌리티 실행을 위해 서버에서 어빌리티 실행하던 것을 로컬에서 실행하도록 변경
어빌리티에 따라 필요할 경우 서버 RPC를 통해 동기화 수행
어빌리티를 서버에서 스폰하도록 변경
어빌리티를 서버와 클라 모두에서 스폰하여 동기화가 되지 않기 때문에 서버에서 스폰하고 리플리케이트를 통해 클라에서 스폰하도록 변경
플레이어가 로그인될 때 선택한 캐릭터에 따라 캐릭터 클래스를 DefaultPawnClass를 변경하여 캐릭터 변경
캐릭터를 선택하고 준비하면 서버로 RPC를 전달하여 해당 캐릭터 클래스를 FUniqueNetIdRepl에 따라 서버의 GameInstance에 저장
게임 모드에서 플레이어가 로그인할 때 GameInstance에서 FUniqueNetIdRepl에 따라 DefaultPawnClass를 설정하여 구현
25년 5월
5주차
시작하자마자 수류탄이 보이는 현상 수정
장비가 등록될 때 장착하기 전에는 비활성화 상태로 전환하여 작동하지 않도록 수정
컨트롤러에서 어빌리티 핸들이 관리되지 않도록 수정
컨트롤러에서는 어빌리티 태그만으로 어빌리티 컴포넌트에 요청만 하고, 어빌리티 존재 여부나 실행 여부를 모름
어빌리티 컴포넌트에서는 어빌리티 태그에 따른 어빌리티가 있는지에 따라 어빌리티를 실행
어빌리티 리플리케이트를 위해 TMap<AbilityHandle, Ability>를 TArray<AbilityContainer>로 변경
시작하자마자 수류탄이 보이는 현상 수정
장비를 등록하자마자 DeActivate하여 보이지 않도록 수정
총 ADS시 수류탄 교체하면 ADS가 유지되는 현상 수정
총이 DeActivate될 때 파츠들도 DeActivate되도록 수정
장비 장착 어빌리티가 Activate될 때마다 장비를 스폰하는 것을 초기화할 때 스폰하도록 변경
어빌리티에 의한 장비는 런타임에 해제될 일이 없기 때문에 어빌리티가 초기화될 때 스폰하여 계속 사용하도록 변경
EquipmentManager에 장비 스폰을 요청하도록 변경
4주차
수류탄 횟수를 지정하지 않을 경우 버퍼 언더플로우 발생하는 현상 수정
FMath::Max를 사용하여 0보다 작은 값을 설정하지 않도록 수정
빠르게 수류탄을 던질 경우 CookingCheckTimer에 의해 한 번 더 던지는 현상 수정
수류탄을 던지는 함수에서 쿠킹 딜레이 타이머를 클리어 안해줘서 함수가 두 번 호출되어 발생
수류탄을 던지는 함수에서 쿠킹 딜레이 타이머를 클리어하여 해결
장비의 횟수 타입 구분하도록 수정
InValid(사용하지 않음), Available(일반적인 횟수), Infinite(무한)으로 Type을 구분하여 장비에서 반환
UI에서 해당 Type에 맞게 표시하도록 변경
수류탄을 던질 수 있는 횟수를 반환하는 함수를 재정의하여 UI에 표시하도록 수정
클라에서 수류탄을 던졌을 때 서버 혹은 다른 클라에 수류탄이 보이지 않는 현상 수정
수류탄 오브젝트를 서버에서 스폰하고, 리플리케이트되도록 변경
클라에서 수류탄을 모두 사용했을 때 기본 무기로 교체되지 않는 현상 수정
클라에서 서버 RPC를 사용하여 서버에 장비가 DeActivate 되도록 수정
장비 교체했을 때 클라에서 기존 장비가 보이고, 교체된 장비가 보이지 않는 현상 수정
장비가 Activate될 때 3인칭 오브젝트를 Activate하여 수정
장비가 DeActivate될 때 3인칭 오브젝트를 DeActivate하여 수정
클라에서 수류탄이 교체되지 않는 현상 수정
서버에서 장비가 교체될 때 클라에 동기화하지 않아 발생
서버에서 현재 장착된 장비를 리플리케이트하고, 클라에서 리플리케이트 됐을 때 해당 장비를 장착
장비 장착 어빌리티로 장착된 장비가 DeActive될 때 기본 무기를 장착하도록 구현
장비가 DeActivate될 때 어빌리티에서 델리게이트를 받아 EquipmentManager에 기본 무기 장착 요청
수류탄 장비 리팩토링
기존에는 수류탄 장비에서 쿠킹 타이머가 종료됐을 때 실행하는 함수와 던지는 함수가 분리되어 복잡
쿠킹 타이머가 종료됐을 때 실행하는 함수를 던지는 함수로 통합하고, 쿠킹 타이머가 유효할 때는 쿠킹 타이머의 남아있는 시간을 수류탄 폭파 타이머로 설정하여 중복 코드 제거
3주차
장비를 장착하는 어빌리티가 장비가 DeActive될 때 어빌리티가 DeActive되도록 구현
던질 수 있는 장비(수류탄 등)에 ThrowCount 구현
Throw가 될 때 ThrowCount을 감소시키고, AddThrowCountCoolTime마다 ThrowCount가 증가
ThrowCount가 0이 될 때 장비가 DeActive 됨
HandlingObject에서 TriggerType에 따른 함수 실행
기존에는 각 장비에서 TriggerType에 따른 로직을 직접 실행하여 코드 중복이 발생
HandlingObject에서 TriggerType에 따른 함수를 호출하고, 각 장비에서 해당 함수를 오버라이드
섬광탄과 수류탄 장비 코드가 동일해서 수류탄 계열 장비 클래스로 통합
섬광탄과 수류탄 Projectile 관련 중복 코드를 Grenade 클래스로 통합
Grenade는 수류탄 계열을 의미하고, FragmentationGrenade가 일반 수류탄을 의미
장비가 Active 상태에서만 PullTrigger가 되도록 수정
장비에서 Active 상태일 때를 체크하는 것이 아닌 EquipmentComponent에서 장비의 Active 상태를 체크
GameplayEffectComponent::ApplyEffect를 Server RPC로 변경
GameplayEffect가 서버에서만 적용될 수 있으므로 클라에서 GameplayEffect 적용을 허용하기 위함
중력 스텟 구현
Jump 관련 StatisticSet에 GravityScale을 추가
GravityScale이 리플리케이트되도록 설정하여 서버와 모든 클라이언트가 적용
StatisticSet::GravityScale 값이 변경될 때 Character에서 CharacterMovement::GravityScale 값 적용
배리어 구현
배리어 스텟이 유효할 경우 HP 대신 배리어가 차감
UI에 배리어 스텟을 업데이트해주는 로직 구현
UMFStatusUserWidget::GetStatusValue을 블루프린트에서 호출 가능하도록 변경
블루프린트 UserWidget 혹은 클래스를 추가하지 않아도 스텟 UI를 업데이트할 수 있도록 변경
StatisticSet의 BaseValue와 CurrentValue 역할 재정의
BaseValue : 처음 초기화한 수치, 변경 불가능한 역할에서 기본적으로 변경할 수 있고, StatisticGameplayEffect에 의한 일시적인 영향을 제외한 값을 나타냄
CurrentValue : 사용되는 실제 수치(일시적인 값을 나타낼 수 있음)이고, StatisticGameplayEffect에 의해서만 수정됨
StatisticGameplayEffect : Duration이 있을 경우 CurrentValue를 수정하고, Duration이 없을 경우 BaseValue를 수정
2주차
발 아래로 총을 쏘면 자신이 데미지를 입는 현상 수정
트레이스를 시도할 때 캐릭터를 IgonredActor로 등록하여 트레이싱되지 않도록 해결
어빌리티 UI 구현
HUDUIController에서 AbilityUIController를 소유
AbilityUIController에서 등록된 어빌리티에 따라 AbilityUserWidget을 동적으로 생성
AbilityUserWidget에서 UWidgetSwitcher를 사용하여 어빌리티 상태에 따라 표시되는 위젯을 교체하도록 변경
어빌리티를 GameplayTag 기반으로 수정
GameplayTag를 키로 어빌리티를 저장
Input에 바인딩되는 함수를 하드코딩하는 것이 아니라 GameplayTag를 전달하여 어빌리티를 실행하도록 수정
25년 4월
4주차
StatisticGameplayEffect로 Statistic 변경할 수 있도록 구현
GameplayEffectManagerComponent에서 Effect의 조건 검사를 호출한 뒤 통과되면 GameplayEffect::Apply 실행
서버에서 Statistic 수정을 적용하고 각 클라이언트에 리플리케이트하여 일관성을 보장하도록 구현
StatisticGameplayEffect에서는 현재 캐릭터의 Statistic BaseValue를 가져와 Modfier를 수행한 결과 값을 해당 Statistic에 설정
ModifierOperation Enum 값에 따라 계산을 수행(Add만 구현하고, 필요할 때 추가 구현)
Statistic에 값을 설정하기 전 후에 OnPreBaseValueChanged, OnPostBaseValueChanged를 호출하여 StatisticSet에서 값을 변경하거나 상황에 맞게 로직을 수행할 수 있게 구현
StatisticGameplayEffect 블루프린트 에디터에서 Statistic 프로퍼티를 선택할 수 있도록 Slate UI 구현
프로퍼티 Iterator가 매크로로 선언된 변수를 인식하지 못해 직접 Statistic 변수를 선언하도록 변경
PropertyEditorModule에서 StatisticPropertyDetail을 추가한 뒤 StatisticWidget(SListView, SComboButton)을 추가
- 콤보 버튼을 클릭했을 때 Statistic 프로퍼티들을 표시하도록 구현
Statistic 프로퍼티를 선택했을 때 StatisticGameplayEffect의 Statistic 프로퍼티에 선택한 프로퍼티가 저장되도록 구현
3주차
어빌리티 쿨타임 구현
쿨타임 타이머가 실행할 때 어빌리티의 실행하지 못하도록 설정
어빌리티가 Deactive될 때 쿨타임 타이머가 실행
드론의 미사일 발사 어빌리티를 플레이어가 아닌 드론 컨트롤러가 갖고 있도록 리팩토링
드론을 스폰하는 어빌리티와 미사일 발사 어빌리티를 플레이어가 갖고 있어 기존에는 스폰 어빌리티에서 미사일 발사 어빌리티로 교체되는 구조였음
따라서 드론 스폰 어빌리티가 Destroy되기 때문에 어빌리티의 상태를 관리하기 어려움
그래서 어빌리티 교체하는 시스템을 제거하고 드론 미사일 발사 어빌리티를 드론 컨트롤러가 갖고 있도록 수정
플레이어는 드론을 스폰하고 드론 컨트롤러에게 어빌리티를 실행하도록 요청
어빌리티 객체를 유지하여 상태를 관리할 수 있고, 각 어빌리티를 기능의 주체자가 소유하여 코드가 단순해짐
HUD 총기 정보 UI 구현
HUDUIController에서 MagazineCount, TotalMagazineCount Type의 StatusUserWidget을 소유하여 각 장전 탄창 수, 보유 탄창 수를 반환할 수 있도록 함
HUDUIController에서 Fire, Reload 등 델리게이트가 호출될 때 MagazineCount, TotalMagazineCount에게 탄창 수를 반환 받아 TextBlock을 수정
AMFHOFireArm의 RPC가 제대로 호출되지 않는 현상 수정
AMFHOFireArm를 스폰할 때 Owner를 설정하지 않았는데, Owner를 설정하지 않아서 Connection을 알 수 없어 RPC가 제대로 호출되지 않음
스폰할 때 FActorSpawnParameters를 통해 Owner 설정
CurrentActivatedEquipment가 리플리케이트 되지 않는 현상 수정
CurrentActivatedEquipment가 리플리케이트 액터가 아니었기 때문에 CurrentActivatedEquipment를 리플리케이트 액터로 변경하여 수정
HUD Player HP UI 구현
HUDUIController에서 StatusUserWidget을 상속 받는 HP 블루프린트 Widget을 소유
HUDUIController에서 HP 변경 이벤트를 받으면 StatusUserWidget에게 반환된 HP 퍼센트에 맞게 HP Widget의 블록들의 색상을 설정
StatusUserWidget 구현
StatusUserWidget의 StatusType을 블루프린트 에디터에서 지정하면 해당 Enum에 맞는 스텟을 StatusUserWidget에서 반환
2주차
UI 프레임워크 및 룰 설계

MFBaseUserWidget : UI의 기본 기능 및, UserWidget의 기본 기능을 구현
MFXXUserWidget : 콘텍스트에 독립적인 UI 요소(ex. HP바, 스텟 표시, 아이템 슬롯 등)
MFBaseUIController : Controller의 기본 기능을 구현
MFXXUIController
- 각 콘텐츠를 담당하는 UI 관리자(ex. HUD, Invetory 등)
- MFUserWidget을 소유하고 관리할 수 있음
룰
- UserWidget은 AddToViewport 불가능 : MFBaseUserWidget::AddToViewport를 막기 위해 NameHiding 사용
- UserWidget 포인터를 사용한다면 막지 못하는 한계는 있음
- UI는 게임플레이 로직을 수정하지 않음
- 수정을 요청하는 함수를 호출할 수는 있지만 Setter 함수나 직접적으로 수정해서는 안됨
- 블루프린트가 바이너리이므로 코드가 많이 복잡하지 않는 이상 블루프린트 그래프를 사용하지 않음
'언리얼 > ProjectMF' 카테고리의 다른 글
| [ProjectMF] 반응성 연막탄 (3) | 2025.12.02 |
|---|---|
| ProjectMF 설치 프리뷰 액터 구현 (0) | 2025.06.30 |


