본문 바로가기

환경 변수와 플레이어 행동 반응

날씨·시간·자원 같은 환경 변수를 플레이어 행동과 양방향으로 연결해 상황에 따라 달라지는 게임 규칙을 만듭니다.

문이나 발판처럼 오브젝트 하나의 반응을 만들었다면, 이제 레벨 전체를 움직이는 값까지 연결할 차례입니다. 날씨와 시간, 자원의 양처럼 시스템 단위의 값을 환경 변수(environmental variable)라고 부르겠습니다. 플레이어의 행동이 이 값을 바꾸고, 바뀐 값이 다시 플레이어의 전략을 바꾸게 만들면, 레벨은 이벤트의 모음이 아니라 하나의 생태계처럼 움직입니다. 같은 맵에서도 매번 다른 결과를 경험하게 되는 것입니다.

이 글에서는 날씨와 시간, 자원, 레벨의 상태, 동적 이벤트를 플레이어의 행동과 양방향으로 연결하는 개념을 정리하고, 광석을 캐면 광맥의 양이 줄고 인벤토리가 늘어나는 채집 시스템을 만들어 봅니다. 이 예에서는 특히 어떤 값을 누가 가지고 바꾸는지, 그리고 성공한 요청만 결과에 반영하는 구조를 그림과 함께 짚습니다.


환경 변수를 행동과 연결하는 이유

환경 변수와 플레이어 행동을 연결하면 세계가 동적으로 바뀝니다. 플레이어의 행동이 환경 변수에 영향을 주거나, 환경의 변화가 새로운 도전 조건이 되도록 상태를 이어 줄 수 있습니다. 또 날씨나 낮과 밤의 주기, 자원의 고갈 같은 값을 이용해 플레이어가 상황에 맞게 판단하고 전략을 세우게 만들 수 있습니다.

환경의 변화로 이야기를 진행하거나, 폭풍우 치는 밤처럼 레벨의 분위기를 살릴 수도 있습니다. 같은 레벨이라도 환경 변수와 플레이어의 선택에 따라 매번 다른 경험이 되므로, 다시 플레이할 가치도 높아집니다.

이런 연결은 입력과 조건, 결과가 나뉘어 있어야 고치고 디버깅하기 쉽습니다. 무엇이 값을 바꾸는지, 어떤 조건에서 반응하는지, 반응의 결과가 무엇인지를 따로 볼 수 있어야 문제가 생겼을 때 어디를 볼지 알 수 있습니다.


환경 변수를 연결하는 여러 방식

날씨와 시간

6장에서 만든 날씨와 시간 시스템은 가장 대표적인 환경 변수입니다. 연결은 두 방향으로 생각할 수 있습니다.

먼저 플레이어의 행동이 날씨와 시간을 바꾸는 방향입니다. 비를 부르는 주문이나 시간을 빠르게 하는 능력처럼, 플레이어의 능력이 날씨나 시간 시스템에 직접 영향을 줄 수 있습니다. 플레이어 캐릭터 블루프린트에서 입력이 들어오면 BP_WeatherSystem이나 BP_TimeOfDaySystem의 참조를 얻고, 직접 만든 함수나 커스텀 이벤트로 상태를 바꾸고 관련 표현을 갱신합니다. 여기서 예로 드는 SetCurrentWeather는 직접 만들 함수의 이름이며, 변수에 값을 넣는 것만으로 이어지는 연출이 저절로 실행되지는 않습니다. 레벨의 제어판을 조작해 인공 강우 장치를 켜거나, 발전소를 가동해 낮과 밤의 주기를 바꾸는 것처럼 오브젝트를 통해 바꿀 수도 있습니다.

반대로 날씨와 시간이 플레이어의 행동에 영향을 주는 방향도 있습니다. 비나 눈이 올 때 캐릭터 무브먼트 컴포넌트의 Max Walk Speed나 Ground Friction을 바꿔 이동을 바꾸고, 밤이나 안개 낀 날에는 후처리 볼륨의 노출이나 지수형 높이 안개의 Fog Density를 바꿔 시야를 좁힐 수 있습니다. 밤에는 더 위험한 적이 나오고 비가 오면 NPC가 실내로 피하는 것처럼, 날씨나 시간에 따라 NPC의 순찰 경로와 행동, 생성 여부를 바꿔 플레이어의 전략에 영향을 줄 수도 있습니다. 이 방향의 연결은 날씨 및 시간 효과 적용에서 본 것처럼, 렌더링 효과로 저절로 생기지 않으므로 따로 만들어야 합니다.

자원

나무나 광물, 식량처럼 플레이어가 채집하거나 쓰는 자원도 중요한 환경 변수입니다. 플레이어가 나무를 베거나 광물을 캐면 그 자원 오브젝트의 양이 줄어들고, 다 쓰면 부서져 사라지게 합니다. 이를 위해 BP_Tree나 BP_Ore 같은 자원 블루프린트에 ResourceAmount 변수를 두고, 상호작용할 때 이 값을 줄이며, 0이 되면 Destroy Actor로 없애거나 숨깁니다. 반대로 플레이어가 자원을 써서 건물을 짓거나 아이템을 만들면 인벤토리의 자원 수가 줄어듭니다.

자원의 변화가 환경에 반응을 일으키게 할 수도 있습니다. 어떤 지역의 자원이 바닥나면 그 지역의 풍경이 황폐해지거나 자원이 다시 생기기까지의 시간이 길어지는 것처럼 환경이 반응하게 하고, 한 자원이 고갈되면 그것을 먹는 동물 NPC가 줄어드는 것처럼 여러 시스템이 엮인 생태계를 만들 수도 있습니다.

레벨의 상태

레벨 자체의 상태를 변수로 두고 플레이어의 행동에 따라 바꿀 수도 있습니다. 스위치를 누르면 다리가 나타나 건널 수 있게 되거나, 레이저 함정이 꺼지는 것이 그 예입니다. BP_Bridge 같은 액터에 bIsActive 불리언 변수를 두고 스위치 블루프린트에서 이 값을 바꾸는데, 이때 직접 만든 상태 적용 함수가 bIsActive를 바꾸는 일과, 타임라인으로 다리를 움직이고 충돌을 바꾸는 일을 함께 하게 합니다. 값만 바꾸고 다리의 충돌을 그대로 두면, 보이지 않는 다리를 건너거나 보이는 다리에서 떨어지는 문제가 생기기 때문입니다.

오염과 정화 같은 상태도 만들 수 있습니다. 플레이어가 독성 물질을 물에 풀면 그 지역이 오염되고, 정화제를 쓰면 다시 깨끗해지는 식입니다. 후처리 볼륨의 색 보정을 바꿔 오염된 지역을 어둡고 탁하게 보이게 하거나, 파티클로 오염된 공기를 표현하고, 정화하면 그 값들을 원래대로 되돌립니다. 용광로나 냉방기가 켜지면 주변의 온도나 습도가 바뀌고, 그 변화가 물체가 얼거나 녹고 식물이 자라는 데 영향을 주게 할 수도 있습니다.

동적 이벤트

환경 변수나 플레이어의 행동이 세계에 예상하지 못한 이벤트를 일으키게 할 수도 있습니다. 플레이어가 특정 지역에 들어가면 지진이 일어나거나 몬스터 무리가 습격해 오고, 자정이 되면 어떤 문이 열리거나 유령이 나타나며, 특정 몬스터를 모두 처치하거나 퍼즐 조각을 모두 모으면 숨겨진 보스가 나타나거나 새 지역이 열립니다. 이런 조건은 게임 모드(Game Mode)나 게임 스테이트(Game State) 블루프린트에 CurrentQuestState나 NumEnemiesKilled 같은 변수를 두고, 값이 조건을 만족하면 Spawn Actor From Class, Create Level Sequence Player로 만든 플레이어의 Play, Set Visibility 같은 이벤트를 실행해 만듭니다.


광석 채집 시스템 만들기

이제 간단한 채집 시스템으로 환경 변수와 플레이어 행동을 직접 연결해 봅니다. 이 예에서 광석의 양은 광맥 액터가 가지고 있고, 입력은 플레이어가 받아 광맥의 채집 함수에 넘깁니다.

준비

먼저 조종할 수 있는 캐릭터와 Interact 입력, 그리고 사용자 인터페이스와 환경 연동에서 만든, 지금 바라보는 대상을 찾는 트레이스를 준비합니다. 아래에 나오는 PlayerInventory는 엔진이 자동으로 주는 변수가 아닙니다. 처음 확인할 때는 플레이어에 Integer 변수 OreCount를 0으로 만들어 두고, 채집에 성공했을 때만 1을 더해 Print String으로 확인해도 됩니다. 이렇게 수량만으로 동작을 확인한 뒤에, 칸과 용량, 아이템 종류를 가진 인벤토리로 넓혀 갑니다.

광맥이 다 캐졌을 때 겉모습도 바꾸려면 재질을 준비합니다. M_Ore 재질에 Texture Sample Parameter 2D를 만들어 이름을 OreStateTexture로 정하고, 그 RGB를 Base Color에 연결합니다. InitialTexture와 MinedTexture에는 직접 고른 두 Texture2D 에셋을 지정합니다. BeginPlay에서 M_Ore를 부모로 동적 재질 인스턴스를 만들어 그 반환값을 OreMID에 저장하고, OreMesh의 재질 슬롯 0에 지정합니다. 처음 상태와 다 캐진 상태를 바꿀 때는 OreMID를 Target으로, 같은 OreStateTexture 이름으로 텍스처 파라미터를 정합니다. 같은 이름의 파라미터가 없거나, 만든 인스턴스를 메시 슬롯에 지정하지 않았다면 양은 줄어도 보이는 표면은 바뀌지 않습니다.

광맥 블루프린트

BP_OreVein 블루프린트를 만들고, 광석 모델을 지정한 OreMesh 스태틱 메시 컴포넌트와, 겹침만 감지하는 InteractionTrigger 상자 충돌을 추가합니다. 트리거는 Custom에서 Pawn에만 Overlap, 나머지에는 Ignore로 응답하게 하고 양쪽의 Generate Overlap Events를 켭니다. 변수로는 남은 광석의 양인 ResourceAmount(Integer, 기본 3)와, 앞에서 정한 MinedTexture, InitialTexture를 둡니다. BeginPlay에서는 앞에서 말한 대로 동적 재질 인스턴스를 만들어 저장하고, 텍스처 파라미터를 InitialTexture로 초기화합니다. 트리거의 BeginOverlap과 EndOverlap에서는 플레이어로 캐스트한 뒤 안내를 띄우고 숨깁니다.

핵심은 MineResource 함수입니다. 요청한 플레이어를 입력으로 받고, 성공했는지를 Success라는 Boolean으로 돌려줍니다. 함수는 먼저 요청한 플레이어의 참조가 유효한지, 그 플레이어가 InteractionTrigger에 겹쳐 있는지를 확인하고, 아니면 Success를 False로 돌려줍니다. 다음으로 ResourceAmount가 0보다 큰지 확인하고, 아니면 역시 False를 돌려줍니다.

두 검사를 통과하면 ResourceAmount를 1 줄이고, Print String으로 "광물 채집! 남은 광물: [ResourceAmount]"를 출력합니다. 채집 파티클은 나이아가라라면 Spawn System at Location으로, 예전 캐스케이드 파티클이라면 Spawn Emitter at Location으로 만들고, Play Sound at Location으로 소리를 냅니다.

그다음 ResourceAmount가 0이 되었는지 확인합니다. 0이 되었다면 저장해 둔 동적 재질 인스턴스의 텍스처 파라미터를 MinedTexture로 바꿉니다. Texture2D는 재질이 아니므로, 재질을 받는 Set Material에 바로 연결할 수 없다는 점에 주의합니다. 그리고 Set Collision Enabled로 InteractionTrigger를 No Collision으로 바꿔 더 이상 상호작용할 수 없게 하고, 플레이어의 지금 후보가 이 광맥이라면 후보와 안내를 바로 비웁니다. 필요하면 일정 시간 뒤에 Destroy Actor로 없앱니다. 성공한 경로의 끝에서는 Success를 True로 돌려줍니다.

플레이어 블루프린트

PlayerCharacter 블루프린트에서는 Interact 입력이 들어오면, 트레이스로 얻은 유효한 Hit Actor가 BP_OreVein인지 확인한 뒤 MineResource(Self)를 호출합니다. 그리고 Success가 True인 경우에만 PlayerInventory에 광석 1개를 더합니다. 이 예는 인벤토리가 늘 받을 수 있는 단일 플레이어를 전제로 합니다. 인벤토리에 용량 제한이 있다면, 광맥의 양을 줄이기 전에 받을 수 있는지도 함께 확인해야 합니다.

누가 값을 바꾸고 누가 결과를 쓰는가

광맥이 수량을 바꾸고 성공한 요청의 자원만 인벤토리에 더합니다

광맥 요청 검사→자기 수량을 3에서 2로 차감→True 반환→인벤토리 1 증가라는 소유자별 상태 전이 과정이다. 실패는 둘 다 유지하며 마지막 1의 고갈도 보존한다.

RESOURCE OWNER · REQUEST VALIDATION · SUCCESS

광맥이 수량을 바꾸고 성공한 요청만 인벤토리에 더합니다

본문의 단일 플레이어·항상 수용 가능한 인벤토리 예제입니다. 자원 수량은 BP_OreVein이 소유하고, 플레이어는 MineResource(Self)의 성공 결과를 받아 인벤토리를 갱신합니다.

광맥 소유자의 검사와 성공 결과에 따른 인벤토리 변경플레이어가 현재 광맥에 MineResource와 요청 플레이어를 전달한다. 광맥은 유효 참조·범위·남은 수량을 검사한다. 성공이면 자기 수량을1줄이고0이면고갈처리를한후True를반환한다. 플레이어는True일때만인벤토리에1을더한다. 검사실패는False를반환하며광맥수량과인벤토리는그대로다. 단일 플레이어·항상 수용 가능한 인벤토리 조건이며 멀티플레이 트랜잭션은 구현하지 않았다.광맥 · ResourceAmount 소유플레이어 · 요청현재 광맥 · Self 전달MineResource요청 수용 가능?유효 참조 · Overlap남은 수량 > 0예광맥 수량 −10이면 고갈 처리Success = TrueTrue 반환플레이어 · 반영인벤토리 +1아니오실패 결과광맥 수량 유지Success = False인벤토리 +0
광맥 소유자의 검사와 성공 결과에 따른 인벤토리 변경플레이어가 현재 광맥에 MineResource와 요청 플레이어를 전달한다. 광맥은 유효 참조·범위·남은 수량을 검사한다. 성공이면 자기 수량을1줄이고0이면고갈처리를한후True를반환한다. 플레이어는True일때만인벤토리에1을더한다. 검사실패는False를반환하며광맥수량과인벤토리는그대로다. 단일 플레이어·항상 수용 가능한 인벤토리 조건이며 멀티플레이 트랜잭션은 구현하지 않았다.광맥 · 수량 소유플레이어 · 요청현재 광맥 · Self 전달MineResource요청 수용 가능?유효 참조 · Overlap남은 수량 > 0예광맥 수량 −10이면 고갈 처리Success = TrueTrue 반환플레이어 · 반영인벤토리 +1아니오실패 결과광맥 수량 유지Success = False인벤토리 +0

유효 요청의 수량 3은 2로 줄고 인벤토리는 1 증가합니다. 마지막 1개에 성공하면 수량 0과 고갈 상태를 확정한 뒤 인벤토리 1을 더합니다. 범위 밖·무효·이미 고갈된 요청에서는 수량과 인벤토리를 모두 유지합니다.

광맥 수량을 바꾸는 책임과 성공 결과를 소비하는 책임을 나눴습니다. 용량 제한이 있으면 차감 전에 수용 여부도 확인하는 설계가 필요합니다. 멀티플레이에서는 서버 상태 변경·복제·RPC 소유권과 실패 처리를 별도로 구현합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석광맥이 수량을 바꾸고 성공한 요청의 자원만 인벤토리에 더합니다
관계·조건 설명플레이어가 현재 광맥에 MineResource와 요청 플레이어를 전달한다. 광맥은 유효 참조·범위·남은 수량을 검사한다. 성공이면 자기 수량을 1 줄이고, 0이면 고갈 처리를 한 후 True를 반환한다. 플레이어는 True일 때만 인벤토리에 1을 더한다. 검사에 실패하면 False를 반환하며 광맥 수량과 인벤토리는 그대로다. 단일 플레이어·항상 수용 가능한 인벤토리 조건이며 멀티플레이 트랜잭션은 구현하지 않았다.
노드·연결선 라벨아니요
본문·주석유효 요청의 수량 3은 2로 줄고 인벤토리는 1 증가합니다. 마지막 1개의 채광에 성공하면 수량 0과 고갈 상태를 확정한 뒤 인벤토리 1을 더합니다. 범위 밖·무효·이미 고갈된 요청에서는 수량과 인벤토리를 모두 유지합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
요청 검사

광맥 소유자가 채광 요청을 검증한다.

  • 광맥 수량: 3
  • 인벤토리: 이전 값
  • 요청자만 검사하고 자원 소유자를 우회해 차감하지 않는다.
소유자 성공

광맥이 자기 수량을 2로 만들고 True를 반환한다.

  • 광맥: 3→2
  • 반환: True
  • 성공 결과는 실제 차감 뒤에 나온다.
획득 반영

성공 때만 인벤토리에 1을 더한다.

  • 인벤토리 변화: +1
  • 실패 때: 광맥·인벤토리 유지
  • Success=True는 광맥 소유자의 차감 완료를 뜻하므로 인벤토리를 1 늘린다. False는 두 수량을 유지한다. 마지막 1개는 수량 0과 고갈 상태를 확정한 뒤 성공을 반환한다.

광맥이 수량을 바꾸고 성공한 요청의 자원만 인벤토리에 더합니다

광맥 요청 검사→자기 수량을 3에서 2로 차감→True 반환→인벤토리 1 증가라는 소유자별 상태 전이 과정이다. 실패는 둘 다 유지하며 마지막 1의 고갈도 보존한다.

RESOURCE OWNER · REQUEST VALIDATION · SUCCESS

광맥이 수량을 바꾸고 성공한 요청만 인벤토리에 더합니다

본문의 단일 플레이어·항상 수용 가능한 인벤토리 예제입니다. 자원 수량은 BP_OreVein이 소유하고, 플레이어는 MineResource(Self)의 성공 결과를 받아 인벤토리를 갱신합니다.

광맥 소유자의 검사와 성공 결과에 따른 인벤토리 변경플레이어가 현재 광맥에 MineResource와 요청 플레이어를 전달한다. 광맥은 유효 참조·범위·남은 수량을 검사한다. 성공이면 자기 수량을1줄이고0이면고갈처리를한후True를반환한다. 플레이어는True일때만인벤토리에1을더한다. 검사실패는False를반환하며광맥수량과인벤토리는그대로다. 단일 플레이어·항상 수용 가능한 인벤토리 조건이며 멀티플레이 트랜잭션은 구현하지 않았다.광맥 · ResourceAmount 소유플레이어 · 요청현재 광맥 · Self 전달MineResource요청 수용 가능?유효 참조 · Overlap남은 수량 > 0예광맥 수량 −10이면 고갈 처리Success = TrueTrue 반환플레이어 · 반영인벤토리 +1아니오실패 결과광맥 수량 유지Success = False인벤토리 +0
광맥 소유자의 검사와 성공 결과에 따른 인벤토리 변경플레이어가 현재 광맥에 MineResource와 요청 플레이어를 전달한다. 광맥은 유효 참조·범위·남은 수량을 검사한다. 성공이면 자기 수량을1줄이고0이면고갈처리를한후True를반환한다. 플레이어는True일때만인벤토리에1을더한다. 검사실패는False를반환하며광맥수량과인벤토리는그대로다. 단일 플레이어·항상 수용 가능한 인벤토리 조건이며 멀티플레이 트랜잭션은 구현하지 않았다.광맥 · 수량 소유플레이어 · 요청현재 광맥 · Self 전달MineResource요청 수용 가능?유효 참조 · Overlap남은 수량 > 0예광맥 수량 −10이면 고갈 처리Success = TrueTrue 반환플레이어 · 반영인벤토리 +1아니오실패 결과광맥 수량 유지Success = False인벤토리 +0

유효 요청의 수량 3은 2로 줄고 인벤토리는 1 증가합니다. 마지막 1개에 성공하면 수량 0과 고갈 상태를 확정한 뒤 인벤토리 1을 더합니다. 범위 밖·무효·이미 고갈된 요청에서는 수량과 인벤토리를 모두 유지합니다.

광맥 수량을 바꾸는 책임과 성공 결과를 소비하는 책임을 나눴습니다. 용량 제한이 있으면 차감 전에 수용 여부도 확인하는 설계가 필요합니다. 멀티플레이에서는 서버 상태 변경·복제·RPC 소유권과 실패 처리를 별도로 구현합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석광맥이 수량을 바꾸고 성공한 요청의 자원만 인벤토리에 더합니다
관계·조건 설명플레이어가 현재 광맥에 MineResource와 요청 플레이어를 전달한다. 광맥은 유효 참조·범위·남은 수량을 검사한다. 성공이면 자기 수량을 1 줄이고, 0이면 고갈 처리를 한 후 True를 반환한다. 플레이어는 True일 때만 인벤토리에 1을 더한다. 검사에 실패하면 False를 반환하며 광맥 수량과 인벤토리는 그대로다. 단일 플레이어·항상 수용 가능한 인벤토리 조건이며 멀티플레이 트랜잭션은 구현하지 않았다.
노드·연결선 라벨아니요
본문·주석유효 요청의 수량 3은 2로 줄고 인벤토리는 1 증가합니다. 마지막 1개의 채광에 성공하면 수량 0과 고갈 상태를 확정한 뒤 인벤토리 1을 더합니다. 범위 밖·무효·이미 고갈된 요청에서는 수량과 인벤토리를 모두 유지합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
요청 검사

광맥 소유자가 채광 요청을 검증한다.

  • 광맥 수량: 3
  • 인벤토리: 이전 값
  • 요청자만 검사하고 자원 소유자를 우회해 차감하지 않는다.
소유자 성공

광맥이 자기 수량을 2로 만들고 True를 반환한다.

  • 광맥: 3→2
  • 반환: True
  • 성공 결과는 실제 차감 뒤에 나온다.
획득 반영

성공 때만 인벤토리에 1을 더한다.

  • 인벤토리 변화: +1
  • 실패 때: 광맥·인벤토리 유지
  • Success=True는 광맥 소유자의 차감 완료를 뜻하므로 인벤토리를 1 늘린다. False는 두 수량을 유지한다. 마지막 1개는 수량 0과 고갈 상태를 확정한 뒤 성공을 반환한다.

위 그림은 이 채집 시스템에서 값이 어떻게 바뀌는지를 보여 줍니다. 플레이어는 지금 바라보는 광맥에 MineResource를 부르며 자기 자신을 넘깁니다. 광맥은 요청을 받을 수 있는지, 즉 참조가 유효하고 겹쳐 있는지, 남은 양이 0보다 큰지를 검사합니다. 받을 수 있다면 위쪽 갈래로 가서 자기 양을 1 줄이고, 0이 되었다면 고갈 처리를 한 뒤 Success를 True로 돌려주며, 플레이어는 그 결과를 받아 인벤토리에 1을 더합니다. 받을 수 없다면 아래쪽 갈래로 가서, 광맥의 양은 그대로 두고 Success를 False로 돌려주며, 인벤토리에도 아무것도 더하지 않습니다.

그림 아래의 설명이 정리하듯이, 유효한 요청이 오면 양 3은 2가 되고 인벤토리는 1 늘어납니다. 마지막 1개에 성공하면 양 0과 고갈 상태를 확정한 뒤 인벤토리에 1을 더합니다. 범위 밖의 요청이나 무효한 요청, 이미 고갈된 광맥에 온 요청은 양과 인벤토리를 모두 그대로 둡니다. 이렇게 광맥의 양을 바꾸는 일은 광맥이 하고, 그 성공 결과를 쓰는 일은 플레이어가 하도록 나눠 둔 것이 이 구조의 요점입니다. 단계 그림은 광맥이 요청을 검사하는 장면과, 양을 2로 만들고 True를 돌려주는 장면을 차례로 보여 줍니다.

그림의 마지막 카드는 이 예의 한계도 짚습니다. 인벤토리에 용량 제한이 있다면 양을 줄이기 전에 받을 수 있는지 확인하는 설계가 필요하고, 멀티플레이어에서는 서버에서 상태를 바꾸고, 복제하고, 원격 호출(RPC)의 소유권과 실패 처리를 따로 만들어야 합니다.

실패한 요청도 시험하기

채집을 시험할 때는 성공하는 경우만이 아니라, 범위 밖에서의 요청, 이미 고갈된 광맥에 대한 요청, 빠르게 연속으로 누른 입력도 시험합니다. 그리고 실패한 요청이 광맥의 양과 인벤토리를 바꾸지 않는지 확인합니다.


연결할 때 고려할 것

먼저 각 상태를 누가 가지는지 정합니다. 광맥 하나의 자원은 그 광맥이, 모두가 함께 쓰는 날씨는 관리자 액터가 가지는 식입니다. 멀티플레이어에서 게임 모드는 서버에만 있으므로, 클라이언트가 알아야 하는 공유 상태는 게임 스테이트나 복제되는 액터에 둡니다.

업데이트도 효율적으로 합니다. Event Tick에서 모든 환경 변수를 매 프레임 갱신하는 것은 비효율적이므로, Set Timer by Event를 쓰거나 날씨 전환이나 플레이어 상호작용 같은 이벤트가 생겼을 때만 갱신합니다. 환경이 바뀔 때는 파티클과 후처리 같은 시각 효과와 소리를 함께 써서 플레이어가 변화를 분명히 알아차리게 합니다.

멀티플레이어 게임에서는 환경 변수의 상태와 그에 따른 오브젝트의 변화가 모든 클라이언트에 정확히 맞아야 합니다. 액터의 복제, 서버에서의 상태 변경, 필요한 변수의 Replicated와 RepNotify 설정, 원격 호출의 소유권을 구분해 만듭니다. 블루프린트에서 이것들을 설정하는 방법은 블루프린트에서의 리플리케이션에서 다룹니다. 변수에 복제를 켜는 것만으로 모든 이벤트와 연출이 저절로 복제되지는 않습니다. 복잡한 상호작용 시스템을 만들기 전에는, 어떤 블루프린트가 어떤 데이터를 관리하고 어떤 이벤트를 일으킬지 전체 구조를 먼저 설계합니다.


정리

  • 환경 변수는 날씨, 시간, 자원, 레벨의 상태처럼 시스템 단위의 값이며, 플레이어 행동과 양방향으로 연결합니다.
  • 플레이어가 날씨를 바꾸는 쪽은 직접 만든 함수로 상태와 표현을 함께 갱신하고, 날씨가 플레이어에게 주는 영향은 이동과 시야, NPC 규칙에 따로 연결합니다.
  • 레벨의 상태를 바꿀 때는 값과 함께 움직임과 충돌도 같은 함수에서 바꿔야 겉모습과 동작이 어긋나지 않습니다.
  • 채집에서 자원의 양은 광맥이 가지고 바꾸며, 플레이어는 MineResource가 돌려준 Success가 True일 때만 인벤토리를 늘립니다.
  • 다 캐진 겉모습은 동적 재질 인스턴스의 같은 이름의 텍스처 파라미터로 바꾸며, Texture2D를 Set Material에 바로 넣을 수 없습니다.
  • 실패한 요청은 양과 인벤토리를 바꾸지 않아야 하며, 멀티플레이어에서는 상태의 소유와 복제, 원격 호출을 따로 설계합니다.

연습 문제

1. 요청을 여러 번 보내면

ResourceAmount가 3이고 OreCount가 0입니다. 유효한 요청 2번, 범위 밖 요청 1번, 다시 유효한 요청 2번을 차례로 보내면 광맥의 양과 OreCount는 어떻게 될까요?

해설 보기

처음 두 번은 성공하므로 광맥은 1개, OreCount는 2가 됩니다. 범위 밖의 요청은 실패하므로 둘 다 그대로입니다. 다음 유효한 요청에서 마지막 1개를 캐므로 광맥은 0, OreCount는 3이 되고 고갈 처리가 확정됩니다. 마지막 요청은 이미 고갈된 광맥에 대한 것이라 실패하므로, 광맥 0과 OreCount 3이 그대로 유지됩니다.

만약 실패한 요청까지 모두 인벤토리에 더했다면 OreCount가 5가 되었을 것입니다. 이것은 입력이 들어왔다는 사실과 채집에 성공했다는 결과를 혼동한 것입니다.

2. 양은 줄었는데 겉모습이 그대로

마지막 광석을 캐서 ResourceAmount가 0이 되었고, 텍스처 파라미터를 MinedTexture로 바꾸는 노드도 실행되었습니다. 그런데 광맥의 겉모습이 바뀌지 않습니다. 무엇을 확인해야 할까요?

해설 보기

먼저 M_Ore 재질에 정확히 OreStateTexture라는 이름의 텍스처 파라미터가 있고, 그것이 Base Color에 연결되어 있는지 확인합니다. 노드에 적은 이름과 재질의 파라미터 이름이 다르면 값이 들어가지 않습니다.

다음으로 BeginPlay에서 만든 동적 재질 인스턴스를 OreMesh의 재질 슬롯 0에 실제로 지정했는지, 그리고 텍스처를 바꾸는 노드의 Target이 그 저장한 OreMID인지 확인합니다. 인스턴스를 만들기만 하고 메시에 지정하지 않았다면, 메시는 여전히 원래 재질을 보여 줍니다.

3. 용량이 찬 인벤토리

인벤토리에 광석을 최대 10개까지만 넣을 수 있게 바꿨습니다. 인벤토리가 이미 10개일 때 채집을 시도하면 지금 구조에서는 어떤 문제가 생길까요? 어떻게 고쳐야 할까요?

해설 보기

지금 구조에서는 광맥이 먼저 양을 줄이고 True를 돌려준 뒤, 플레이어가 인벤토리에 더하려 하다가 용량 때문에 넣지 못합니다. 그러면 광맥의 광석은 사라졌는데 인벤토리에는 들어오지 않아 광석 하나를 잃게 됩니다.

광맥의 양을 줄이기 전에 인벤토리가 받을 수 있는지를 먼저 확인해야 합니다. 예를 들어 플레이어가 MineResource를 부르기 전에 남은 용량을 확인하거나, MineResource에 받을 수 있는지를 함께 넘겨, 받을 수 없으면 광맥이 양을 줄이지 않고 False를 돌려주게 합니다.

다음 글 환경 상호작용의 디버깅에서는 지금까지 만든 문과 안내, 발판, 채집이 의도와 다르게 동작할 때, 어디서부터 확인해 원인을 좁히는지 정리합니다.