Project Arc (Project CM)

Project Arc (Project CM)

프로젝트 개요


image.png

  • 프로젝트 명: Project Arc (Project CM)
  • 역할
    • GameMode 및 State 구현
    • 로딩 시퀀스 및 클라이언트 동기화 시스템 구현
    • 절차적 맵 생성 구현
    • Tree형 NPC 대화 및 액션(상호작용) 구현
    • 서버권위 상점 구현
  • 개발 환경
    • Unreal 5.6.1
    • C++
    • PC/Windows
  • 기간: 2025.10.28 ~ 2026.01.12
  • 인원: 8명

핵심 기능

1. 로비 및 클라이언트 동기화 시스템

안정적인 멀티플레이 로비 환경을 위해 클라이언트 로딩 시퀀스를 제어하고, 네트워크 비동기성으로 인한 동기화 경합 문제를 해결한 시스템을 설계/구현했습니다.

기술적 포인트

  • 클라이언트 로딩 시퀀스 제어 (로딩 3단계 분리)
    • 동기화 대상 컴포넌트 준비 완료 후 OnLoadBegin → OnLoadUI → OnLoadCompleted 3단계 순차 로딩 진행
    • 각 단계를 NextTick으로 예약 및 브로드캐스트하여 안정적인 초기화 보장
    • 팀원들이 로딩 시퀀스에 기능을 쉽게 추가할 수 있도록 DECLARE_MULTICAST_DELEGATE를 통한 바인딩 지원
    • 블루프린트에 노출되지 않는 C++ 전용 델리게이트를 사용함으로써, 무분별한 외부 접근으로 인해 객체지향의 캡슐화가 훼손되고 의존성이 꼬이는 문제를 방지
  • PlayerState Replicated 변수 동기화 경합 해결 (상세 내용은 트러블 슈팅 1번 항목 참조)
    • BeginPlay와 OnRep 간 네트워크 초기화 비동기 문제로 인한 로비 접속자 리스트 누락/중복 현상 해결
    • SetTimerForNextTick을 활용, GameState 초기화 완료 시점까지 처리 지연 및 안전한 목록 추가 보장
    • bAddedToLobby 플래그 도입으로 REPNOTIFY_Always 재호출 상황에서도 멱등성 확보
  • 안정적인 플레이어 세션(입퇴장) 관리
    • 서버 Authority 환경 및 특수 로그인 흐름에서 누락되는 기존 AddPlayerState / RemovePlayerState 구조 개선
    • GameMode의 PostLogin / Logout 오버라이드 및 이벤트를 통한 GameState 브로드캐스트 적용

핵심 기여

  • 클라이언트 로딩 시퀀스 및 멀티플레이 로비 제어 구조 전체 설계 및 구현
  • 비동기 네트워크 환경에서의 상태 동기화 및 렌더링 경합 문제 해결
  • 서버 권한 기반의 안정적인 세션 입장/퇴장 파이프라인 구축

실행 결과

로딩 시퀀스 순차 실행 Sync가 모두 완료된 후 차례대로 Load 메소드를 실행하는 것을 볼 수 있습니다

로비 플레이어 리스트 출력 본인을 제외한 다른 클라이언트들이 로비 리스트에 정상적으로 출력됩니다

입퇴장 호출 검증 GameMode PostLogin / Logout을 통해 입장 및 퇴장 이벤트가 확실히 호출됨을 검증했습니다

구현 과정

[Project CM] 클라이언트 동기화를 위한 로딩 시퀀스 구현

[Project CM] PlayerState 추가/제거 함수 문제 발견 및 해결 방안

2. 시드 기반 Procedural Map 생성

실제 생성된 지형

시드 기반 Procedural Map + 데이터 기반 맵 생성 구조를 통해, 확장성과 성능을 동시에 확보한 맵 생성 시스템을 설계/구현했습니다.

기술적 포인트

  • DFS + BFS 혼합 구조 설계

    코드 스니펫

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
      // 거리 계산(BFS)로 시작점에서의 거리 파악
      TMap<FCMRoomPosition, int32> Distance;
      Distance.Add(Start, 0);
      TQueue<FCMRoomPosition> Q;
      Q.Enqueue(Start);
      while (!Q.IsEmpty())
      {
          FCMRoomPosition Curr;
          Q.Dequeue(Curr);
          const TArray<FCMRoomPosition>* NeighPtr = Adjacency.Find(Curr);
          if (!NeighPtr) continue;
          for (const FCMRoomPosition& N : *NeighPtr)
          {
              if (!Distance.Contains(N))
              {
                  Distance.Add(N, Distance[Curr] + 1);
                  Q.Enqueue(N);
              }
          }
      }
    
      // 리프 노드(차수 1, 시작 제외)를 거리 내림차순으로 정렬
      TArray<FCMRoomPosition> Leaves;
      Leaves.Reserve(Adjacency.Num());
      for (const TPair<FCMRoomPosition, TArray<FCMRoomPosition>>& Pair : Adjacency)
      {
          const FCMRoomPosition& Pos = Pair.Key;
          const int32 Degree = Pair.Value.Num();
          if (Degree == 1 && !(Pos.X == Start.X && Pos.Y == Start.Y))
          {
              Leaves.Add(Pos);
          }
      }
      Leaves.Sort([&Distance](const FCMRoomPosition& A, const FCMRoomPosition& B)
      {
          const int32 DA = Distance.Contains(A) ? Distance[A] : MAX_int32;
          const int32 DB = Distance.Contains(B) ? Distance[B] : MAX_int32;
          return DA > DB; // 먼 순서대로
      });
    
      // 보스/보물 방 배치 결정
      TSet<FCMRoomPosition> BossPositions;
      TSet<FCMRoomPosition> TreasurePositions;
    
      const int32 BossToPlace = FMath::Clamp(DesiredBoss, 0, Leaves.Num());
      for (int32 i = 0; i < BossToPlace; ++i)
      {
          BossPositions.Add(Leaves[i]);
      }
    
  • Data-driven 설계 (기획 친화적 구조)

    image.png

  • Actor 기반 월드 구성 자동화
  • 재현 가능한 Procedural 시스템 구축

핵심 기여

  • Procedural Map Generator 전체 구조 설계 및 구현
  • Room Graph → World 배치 파이프라인 구축
  • 데이터 기반 콘텐츠 확장 구조 설계

구현 과정

[Project Arc] 데이터 기반 Procedural Map Generator + 레벨 스트리밍 구현

3. NPC Dialogue & Action 시스템 구현

Tree 기반 Dialogue 구조와 Action 시스템을 결합하여, 분기형 대화와 게임 로직 실행을 동시에 처리하는 데이터 기반 NPC 상호작용 시스템을 설계/구현했습니다.

56.gif

58.gif

기술적 포인트

  • Tree 기반 Dialogue Graph 구조
    • Node (대사) + Edge (선택지) 구조
    • 부모-자식 관계 기반 분기 처리
    • DFS/순회 기반 Dialogue 흐름 탐색

    → 복잡한 분기형 스토리 구조를 안정적으로 표현

  • Dialogue + Action 분리 설계
    • Dialogue: “무엇을 보여줄 것인가”
    • Action: “무엇을 실행할 것인가”
    • 구조
      • Dialogue Node → Action Trigger 포함
      • 선택 시 특정 게임 로직 실행
      • 예시) 아이템 상점

    → 대화 시스템이 단순 UI가 아닌 게임 로직 트리거 역할 수행

  • Data-driven Dialogue 시스템

    image.png

    • DataTable / 구조체 기반 대화 정의
    • 코드 수정 없이 콘텐츠 확장 가능

    → 기획자가 직접 Dialogue 제작 가능

  • 상태 기반 흐름 제어 (Stateful Dialogue)
    • 현재 노드 상태 기반 진행
    • 선택지에 따라 다음 노드 동적 결정

    → FSM(상태 머신)과 유사한 안정적인 흐름 관리

  • UI 자동 생성 및 이벤트 바인딩
    • 선택지 개수에 따라 UI 동적 생성
    • 버튼 ↔ Dialogue Node 자동 연결

    → UI 수정 없이 대화 구조 변경 가능

  • Action Execution Pipeline
    • Dialogue → Action → Game System 연결 구조
    • 이벤트 기반 실행 (Delegate / Callback 구조)

    → Dialogue → Gameplay 자연스럽게 연결

핵심 기여

  • Dialogue Tree 구조 전체 설계 및 구현
  • Dialogue + Action 분리 아키텍처 설계
  • Node 기반 Action Trigger 시스템 구현
  • 데이터 기반 Dialogue 관리 구조 구축
  • UI ↔ 시스템 연결 자동화 구조 설계

구현 과정

[Project Arc] Tree 구조의 Dialogue 시스템 구현

[Project Arc] NPC Dialogue & Action 시스템 1차 구현 완료

4. 서버 권한 기반 Shop 시스템 & 데이터 기반 상점 고도화

35.gif

서버 권한 기반 구조와 데이터 중심 설계를 결합하여, 보안성과 확장성을 동시에 확보한 상점 시스템을 구현했습니다.

기술적 포인트

  • 서버 권한 기반 상점 구조 (Authoritative Server Model)
    • 구매 로직을 클라이언트가 아닌 서버에서 처리
    • 클라이언트는 요청(Request), 서버는 검증(Validate) 후 실행
    • 구조:
      • Client → RPC 요청
      • Server → 재화 검증 / 구매 처리
      • 결과 → Client 동기화

    → 치트 및 데이터 변조 방지 (신뢰성 확보)

  • RPC 기반 네트워크 처리
    • Server RPC를 통한 구매 요청 처리
    • 상태 변경 후 클라이언트 동기화
    • 핵심 처리:
      • 구매 요청 → 서버 전달
      • 서버에서 재화 체크
      • 성공 시 결과 반환

    → 멀티플레이 환경에서도 일관된 상태 유지

  • DataTable 기반 상점 데이터 구조

    image.png

    • 아이템 정보를 DataTable로 분리
    • 가격 / 설명 / 타입 등 데이터 관리

    → 코드 수정 없이 콘텐츠 확장 가능

  • UI 캐싱 구조 (성능 최적화)
    • 상점 UI 생성 시 데이터 캐싱 적용
    • 반복 생성/로드 방지

    → UI 생성 비용 감소 및 성능 향상

  • 수량 증감 시스템 (UX 개선)
    • +/- 버튼 기반 수량 조절
    • 최소/최대 제한 처리
    • 수량 기반 가격 계산

    → 사용자 구매 경험 개선

  • UI ↔ 데이터 동기화 구조
    • UI 상태와 실제 데이터 상태 일관성 유지
    • 변경 시 즉시 반영

    → UI 오류 및 상태 불일치 방지

핵심 기여

  • 서버 권한 기반 상점 구조 전체 설계 및 구현
  • RPC 기반 구매 처리 시스템 구축
  • DataTable 기반 데이터 관리 구조 설계
  • UI 캐싱을 통한 성능 최적화
  • 수량 증감 UI 및 로직 구현
  • 클라이언트-서버 동기화 구조 설계

구현 과정

[Project Arc] 서버 권한 상점 컨텐츠 구성 (UI, 캐싱, Data Table, RPC)

[Project Arc] 상점 기능 고도화 (컨텐츠 정보 반영, 개수 증감 버튼 등)


트러블 슈팅

1. 멀티플레이 환경의 PlayerState 네트워크 동기화 경합 및 중복 문제 해결

문제 상황

  • 로비에서 클라이언트 접속 시, 서버에서 클라이언트로 PlayerState의 닉네임(PendingNickname)을 동기화하는 과정에서 두 가지 치명적인 문제가 발생
  • 누락 문제: 간헐적으로 로비 접속자 리스트에 접속한 플레이어의 닉네임이 정상적으로 등록되지 않음
  • 중복 문제: 동일한 플레이어가 리스트에 여러 번 중복 추가되는 현상 발생

원인 분석

  • 네트워크 복제와 월드 초기화의 비동기성 (타이밍 경합):
    • 언리얼 엔진 네트워크 흐름 상 PlayerState의 Replicated 변수들이 클라이언트에 도착하는 시점이 맵의 BeginPlay() 시점과 일치한다고 보장할 수 없음
    • 변수가 복제되어 OnRep 콜백이 정상적으로 호출되더라도, 그 시점에 로비 리스트를 관리하는 GameState가 아직 셋업되지 않았다면 (null 상태) 참조에 실패하여 리스트 추가가 무시되는 타이밍 경합이 발생함
  • 복제 알림의 재진입:
    • 클라이언트 측에서 확실한 동기화를 위해 REPNOTIFY_Always 옵션을 사용했으나, 이로 인해 변수 값이 동일하더라도 OnRep이 지속적으로 호출되며 리스트 추가 로직이 중복으로 실행될 여지가 있었음

해결 방법

1. SetTimerForNextTick을 활용한 초기화 경합 해결

  • OnRep 호출 시 GameState의 존재 여부를 먼저 확인
  • GameState가 아직 준비되지 않았다면 실패 처리하지 않고, SetTimerForNextTick을 활용해 리스트 갱신 로직 실행을 다음 프레임(Tick)으로 연기시킴으로써 월드 초기화 완료 이후의 안전한 데이터 접근을 보장

2. 가드 플래그(bAddedToLobby)를 통한 멱등성 보장

  • PlayerState 내부에 bAddedToLobby 상태 플래그를 추가
  • 로비 리스트에 이미 추가된 상태라면 조기 종료(Return) 처리하여, REPNOTIFY_Always 설정으로 인한 OnRep 다중 호출이나 지연 처리 상황에서도 단 1회만 등록되도록 방어 로직 구현

3. 초기값 명시로 확실한 복제 시점 제공

  • 서버의 BeginPlay()에서 PendingNickname의 초기값을 명확하게 셋업하여 Replication 파이프라인이 안정적으로 시작되도록 유도

결과

로비 플레이어 리스트 출력 핵심 기능 항목과 동일한 이미지이며, 이해를 돕기 위해 추가했습니다

  • 단일/다중 클라이언트 동시 접속이나 느린 초기화 시뮬레이션 환경에서도 리스트 누락이나 렌더링 꼬임 없이 유저 목록이 1회씩 정상 등록됨을 보장
  • 로비 씬과 같이 데이터 복제 직후 다른 서브시스템과 즉시 상호작용해야 하는 환경에서의 네트워크 초기화 안정성을 대폭 향상

해결 과정

[Project CM] SetTimerForNextTick을 활용한 PlayerState 동기화 시점 문제 해결

2. 맵 생성 시 Entrance 비정상 생성 문제 해결

문제 상황

  • Procedural Map 생성 과정에서 방(Room) 경계에 배치되는 Entrance(문/통로)가 비정상적으로 생성되는 문제 발생
  • 특히:
    • 같은 위치(두 방 사이의 경계)에 Entrance 액터가 여러 개 겹쳐서 생성됨
    • 방이 물리적으로 인접해 있음에도 문이 생성되지 않거나, 엉뚱한 방향에 문과 벽이 배치되는 현상 발생

원인 분석

1. 방향 인덱스 체계 불일치

  • 방 생성 로직(GenerateRoom)은 상/좌/하/우를 기준으로 동작하나, 벽/문 배치 로직은 우/하/좌/상을 기준으로 검사하여 내부적으로 인접 판단이 엇갈림.

2. 중복 스폰 방지 캐싱 로직의 연산 버그

  • 스폰된 Border 액터를 반대편 방에 공유할 때, 연산자 우선순위 실수(DirectionIndex + 2 % 4)와 잘못된 인덱스 참조 발생.
  • 이로 인해 배열 캐싱이 무너져 같은 경계를 검사할 때마다 SpawnActor가 중복 호출됨.

3. 논리적 인접(QuadTree)과 물리적 인접(Grid)의 괴리

  • 쿼드트리 구조 특성상 실제 격자 좌표계에서는 맞닿아 있지만, 트리 상에서는 다른 브랜치에 속해 있어 ConnectedRooms 배열에 인접 방으로 담기지 않는 트리 구조의 한계가 존재함.

해결 방법

1. 전역 방향 인덱스 기준 통일

  • 시스템 전체의 방향 관련 배열과 연산을 상/좌/하/우(0~3) 기준으로 전면 통일하여 로직 간 인접 판단의 일관성 확보.

2. 공유 리소스(Border) 1회 스폰 및 양방향 캐싱 적용

  • 반대 방향 인덱스를 (DirectionIndex + 2) % 4로 정확히 보정.
  • Entrance 스폰 시, 맞닿은 두 방이 각자의 RoomBorderActors 배열에 동일한 액터 포인터를 참조하도록 수정하여 A→B, B→A 어느 쪽에서 먼저 접근해도 단 1회만 스폰되도록 구조 변경.

3. FCMRoomPosition 기반 물리적 인접성 재캐싱 (후처리)

  • 쿼드트리 생성이 끝난 후, 논리적 트리 구조에 의존하지 않고 최종 격자 좌표(FCMRoomPosition)를 기준으로 상/좌/하/우 인접성을 직접 재검증하는 후처리(Post-process) 단계 추가.
  • 실제 그리드 상에서 붙어 있는 방들만 ConnectedRooms로 다시 묶어줌으로써 맵 구조의 왜곡을 완벽히 제거.

결과

image.png

항목개선 결과
다중 스폰/중복 오류100% 제거 (단일 스폰 보장)
Entrance 배치 정확도물리적 인접 좌표와 완전 일치
맵 구조 일관성QuadTree 브랜치와 무관하게 항상 보장

해결 과정

[Project Arc] 맵 생성 시 Entrance 비정상 생성 문제 해결

3. GameStarter NPC 구현 및 Remote Client 필터링 문제 해결

57.gif

문제 상황

  • 멀티플레이 환경에서 GameStarter NPC가 특정 클라이언트에서 정상적으로 동작하지 않는 문제 발생
  • 일부 클라이언트에서는:
    • NPC가 보이지 않거나
    • 상호작용이 불가능한 상태 발생
  • 특히 Remote Client 환경에서만 문제 발생

원인 분석

1. 클라이언트 필터링 로직 문제

  • NPC 생성 및 처리 로직이 클라이언트 기준으로 필터링되고 있었음
  • 특정 조건에서 Remote Client가:
    • NPC를 생성/인식하지 않음
    • 혹은 동기화 대상에서 제외됨

2. 서버-클라이언트 책임 분리 문제

  • 일부 NPC 처리 로직이 클라이언트에 의존
  • 이로 인해:
    • 클라이언트 상태에 따라 결과가 달라짐
    • 네트워크 환경에서 일관성 깨짐

3. 동기화 범위(Scope) 설계 미흡

  • NPC가 모든 클라이언트에 동일하게 전달되지 않음
  • Remote Client는 일부 데이터만 수신

→ NPC 상태 불일치 발생

해결 방법

1. 서버 중심 구조로 전환

  • NPC 생성 및 상태 관리 책임을 서버로 완전 이관
  • 클라이언트는 단순 렌더링 및 입력 처리만 담당

구조:

  • Server → NPC 생성 및 상태 관리
  • Client → 서버 데이터 기반 표시

2. Remote Client 필터링 로직 수정

  • 클라이언트별 필터링 제거
  • 모든 클라이언트가 동일한 NPC 데이터를 수신하도록 구조 변경

3. 동기화 구조 개선

  • NPC 생성 시:
    • 전체 클라이언트에 브로드캐스트
  • 상태 변경 시:
    • 변경 사항만 전송 (Delta Sync)

4. NPC 초기화 흐름 재설계

  • 접속 시:
    • 기존 NPC 상태를 서버에서 전달
  • 이후:
    • 이벤트 기반으로 상태 업데이트

변경된 상호작용 Sequence Diagram 보기

image.png

결과

항목개선 결과
NPC 표시 문제모든 클라이언트에서 100% 동일하게 표시
상호작용 오류Remote Client에서도 정상 동작 보장
동기화 안정성클라이언트 간 상태 불일치 제거
네트워크 구조서버 중심 구조로 안정성 확보

해결 과정

[Project Arc] GameStarter NPC 구현 및 Remote Client 필터링 문제 해결