오늘은 크게 두 갈래로 진행함 — ① 아이템을 먹어도 아무 효과가 없던 걸 발견해서 실제 효과 적용 로직 구현, ② HP/MP/공격력/이동속도를 화면에 보여주는 스탯 UI 구현. 그 과정에서 "Sync 프로퍼티인데 게임을 켜자마자는 초기값이 화면에 안 뜨는" 버그를 만났고, 원인을 추적하다 MSW 생명주기 함수들의 보장 범위를 다시 제대로 이해하게 됨.
ItemEffectData의 Apply/Revert가 전부 function(playerEntity, value) end짜리 빈 스텁이었음. 포션을 먹으면 수량은 줄어드는데 HP/MP/이동속도/공격력은 아무것도 안 바뀌는 상태. 추가로 PlayerEffect.ApplyBuff를 다시 보니, 지속시간 안에 같은 버프를 재사용하면 Revert 없이 Apply만 한 번 더 불려서 효과가 영구히 남는 실제 버그도 같이 발견함.
HP(PlayerComponent.Hp)와 이동속도(MovementComponent.InputSpeed)는 엔진 네이티브 값이라 그대로 쓰면 되는데, MP와 공격력은 MSW에 대응하는 값 자체가 없었음. API 문서를 뒤져봐도 PlayerComponent에 Mp 프로퍼티가 없고, Components 목록에도 스탯 개념의 컴포넌트가 아예 없음 — MSW는 HP 외의 RPG 스탯을 제공하지 않는 범용 플랫폼이라는 걸 확인. 처음엔 기존 컴포넌트에 얹으려다, SRP 위반이라 판단해 PlayerStats라는 새 컴포넌트로 분리함.
MP/공격력 UI 갱신 방식을 두고 "커스텀 이벤트"로 설계했다가, "그냥 Sync만 걸고 바뀐 시점만 알려주면 안 되나?"라는 생각을 함. 처음엔 "인벤토리도 이벤트 방식이니까"로 반박했는데, 다시 확인해보니 인벤토리(InventoryContainer.slots)는 애초에 table 타입이라 Sync 자체가 불가능한 자료구조였던 거고, 스칼라 값(HP/MP 등)에는 해당 안 되는 얘기였음. 원칙을 잘못 일반화하고 있었던 것.
OnSyncProperty 발견Sync된 값이 바뀌었을 때 신호를 주는 방법을 고민하던 중, 넥슨 강의 스크립트에서 몬스터 체력바 예제를 확인함 — "싱크 프로퍼티"라는 콜백으로 프로퍼티 이름을 받아 분기하는 코드였음. 공식 문서에서 정확한 시그니처(OnSyncProperty(string name, any value))를 확인하고, 커스텀 이벤트 계획을 폐기하고 이걸로 통일함.
PlayerComponent를 Extend할 수 있는가HP UI도 같은 패턴으로 만들려고 PlayerComponent를 Extend하려 했는데, 넥슨 공식 포럼에서 "기본 플레이어 엔티티에 이미 있는 PlayerComponent가 삭제되지 않아 적용이 안 된다"는 글을 발견. 넥슨 자체 기본 템플릿(UIMyInfo)도 OnSyncProperty 대신 OnUpdate 폴링을 쓰는 걸 보고 "Extend가 막혀있어서 그런 것"이라 결론 내리고 폴링 방식으로 설계를 바꿈. 그런데 실제로 에디터에서 시도해보니 Extend가 되는 걸 확인해서, 폴링 버전을 버리고 다시 OnSyncProperty + Extend 버전으로 되돌림.
ExtendPlayerComponent/ExtendMovementComponent/PlayerStats까지 다 붙이고 실행했는데, 게임을 막 켰을 때 HP/MP 바가 초기 상태(빈 값)로 안 그려짐. 원인은 OnSyncProperty가 "값이 실제로 바뀌는 순간"에만 호출되는 콜백이라, 최초 상태(기본값)는 애초에 "변경"이 아니라서 한 번도 안 불렸던 것.
OnSyncProperty(string name, any value)의 정확한 시그니처와 호출 시점(값이 실제로 바뀔 때만)을 확인한 문서. OnBeginPlay가 "존재는 보장하지만 다른 컴포넌트가 계산해 넣은 값은 보장 안 한다"는 차이도 여기서 확인함isvalid()가 컴포넌트 메서드가 아니라 전역 함수이고, nil까지 안전하게 처리해준다는 걸 확인한 문서