조명 태그 퍼즐의 Verse 코드를 작성하기 전에, 원하는 기능을 구현하는 최선의 방법부터 생각해 보는 것이 좋습니다. 이 섹션에서는 퍼즐 메카닉을 어떻게 만들어야 할지 알아봅니다. 이 섹션을 완료하면 퍼즐을 만들기 위한 알고리즘을 표현하는 의사코드가 완성됩니다. 다음 단계에서는 UEFN과 Verse에서 이 알고리즘을 구현하는 방법을 다룹니다.
목표, 조건, 제약 명시하기
첫 단계는 목표, 조건, 제약을 명시하는 것입니다. 전체적인 목표를 세분화하면 조건을 알 수 있습니다.
| 목표 |
|
| 조건 |
|
| 제약 |
|
문제 세분화하기
목표와 구현할 기능이 무엇인지를 이해했으니 구현 방법을 떠올리기 쉽도록 문제를 작게 세분화할 차례입니다. 큰 문제를 세분화하려면 다음과 같은 질문을 던지는 것이 좋습니다.
- 플레이어는 어떤 방식으로 퍼즐과 상호작용할까요?
- 게임플레이 태그를 어떤 방식으로 사용해서 조명을 찾을까요?
- 에디터에서 수정할 수 있는 초기 조건과 해답을 어떻게 정의할까요?
- Verse 구조체에 저장된 게임 스테이트를 어떻게 하면 게임 내 시각적 요소와 일치시킬 수 있을까요?
- 플레이어 상호작용이 어떤 방식으로 특정 조명 세트를 업데이트할까요?
- 퍼즐이 해결된 후에 플레이어 상호작용을 어떻게 비활성화할까요?
다음으로, 각 문제가 잠재적으로 서로 종속될 수 있는지 확인합니다. 각 문제가 독립적으로 보이더라도, 종속성을 띠는지 확인하는 것이 도움이 됩니다.
- 질문 1, 5, 6은 서로 약간의 연관성이 있습니다.
- 질문 1, 6과 관련해서, 플레이어가 퍼즐과 상호작용하는 방법은 퍼즐이 해결된 후 해당 상호작용이 비활성화되는 방법과 관련이 없어야 합니다.
- 질문 1, 5와 관련해서, 한 번의 상호작용이 여러 조명을 한 번에 켜거나 끕니다. 상호작용을 조명에 매핑하는 데이터 구조를 결정할 때 이를 고려해야 합니다.
- 질문 2는 설계 시 고려해야 할 중요 사항입니다. 게임플레이 태그 API가 어떻게 작동하는지를 고려해 코드에서 조명을 제어하는 방법을 설계해야 합니다. 이는 게임 내 조명 스테이트를 변경해야 하는 질문 4와 5에도 영향을 미칩니다. 따라서, 일반적인 방법을 찾아 사용할 필요가 있습니다.
- 질문 3과 4는 시작, 현재, 해답 스테이트를 구현하는 데이터 구조를 선택하면 하나의 솔루션으로 수렴합니다.
솔루션 구체화하기
문제를 작은 문제들로 세분화했으니 그에 대한 답을 내놓을 차례입니다.
1. 플레이어는 어떤 방식으로 퍼즐과 상호작용할까요?
이 질문에는 다양한 답이 있습니다. 일반적으로는 플레이어가 상호작용하고 Verse가 해당 상호작용을 탐지하는 데 사용하는 모든 장치를 사용할 수 있습니다. 포크리 툴세트에는 이러한 요건을 충족하는 많은 장치가 있습니다. 예를 들면 트리거 장치, 버튼 장치뿐만 아니라, 색상 변경 타일 장치, 인식 트리거 장치도 있습니다.
이 예시에서는 버튼 장치를 사용하고, 버튼이 활성화되어 있는 동안 플레이어가 버튼과 상호작용할 때마다 디스패치되는 InteractedWithEvent 를 사용합니다. 이벤트에 대한 자세한 내용은 장치 상호작용 코딩을 참고하세요.
2. 게임플레이 태그를 어떤 방식으로 사용해서 조명을 찾을까요?
게임플레이 태그를 사용해 Verse 코드에서 정의한 커스텀 태그가 할당된 액터 그룹을 얻을 수 있습니다.
GetCreativeObjectsWithTag() 함수를 사용해 커스텀 태그가 할당된 모든 액터의 배열을 얻을 수 있습니다. 함수의 결과는 creative_object_interface 를 구현하는 모든 오브젝트의 배열입니다. customizable_light_device 는 설정 가능한 조명 장치의 Verse 버전이며 creative_object_interface 를 구현하는 클래스입니다.
GetCreativeObjectsWithTag() 가 반환하는 장치의 목록은 일정한 순서를 보장하지 않으며, 특히 레벨에 장치가 많은 경우 함수 호출을 통해 모든 장치를 반환하는 데 시간이 소요될 수 있으므로 빠른 액세스를 위해 조명을 별도로 저장해 놓는 것이 좋습니다. 이를 캐싱 이라 하며, 대개 캐싱을 사용하면 성능이 개선됩니다. 조명들은 동일한 타입의 컬렉션이므로 배열에 조명을 저장할 수 있습니다.
따라서 다음 작업을 수행할 수 있습니다.
- 새 태그
puzzle_light를 생성합니다. - 퍼즐에 사용할 모든 조명에
puzzle_light태그를 답니다. GetCreativeObjectsWithTag(puzzle_light)를 호출해puzzle_light태그가 달린 모든 액터를 가져옵니다.- 함수 호출의 결과 중
customizable_light_device인 장치를 골라냅니다. - 나중에 액세스할 수 있도록
customizable_light_device오브젝트의 목록을 배열에 저장합니다.
3. 에디터에서 수정할 수 있는 초기 조건과 해답을 어떻게 정의할까요?
조명은 두 가지 스테이트만을 가집니다. 켜짐 과 꺼짐 입니다. Verse의 logic 타입은 true 또는 false 값만을 가지므로 logic 타입을 사용해 조명의 켜짐/꺼짐 스테이트를 표현할 수 있습니다. 조명이 여러 개 있으므로 여기서도 배열을 활용할 수 있습니다. 모든 logic 값을 배열에 저장하고 조명 스테이트의 인덱스(배열 위치)와 해당 조명의 인덱스를 일치시키면 됩니다.
logic 값의 배열을 사용해 퍼즐 조명의 초기 스테이트를 정의하고 게임 도중 조명의 현재 스테이트를 저장할 수 있습니다. @editable 어트리뷰트를 사용해 에디터에 이 배열을 노출할 수 있습니다. 게임 시작 시 조명을 켜거나 꺼 배열에 저장된 스테이트와 시각적으로 일치시킬 수 있습니다.
퍼즐 해답의 타입이 조명의 현재 스테이트를 저장하는 타입과 같아야 둘을 비교해 퍼즐이 해결되었는지 확인할 수 있습니다. 따라서 조명의 현재 조건을 나타내는 배열과 퍼즐의 해답을 나타내는 배열, 이렇게 두 개의 편집 가능한 logic 배열이 필요합니다. 이렇게 구현하면 에디터에서 퍼즐 조명의 초기 스테이트와 퍼즐 해답을 변경해 퍼즐을 다른 구성으로 재사용할 수 있습니다.
4. Verse 구조체에 저장된 게임 스테이트를 어떻게 하면 게임 내 시각적 요소와 일치시킬 수 있을까요?
게임 내에서 TurnOn() 과 TurnOff() 함수를 사용해 customizable_light_device 를 켜거나 끌 수 있습니다. logic 배열에 저장하는 조명의 현재 스테이트를 업데이트할 때마다 TurnOn() 과 TurnOff() 를 호출해 게임 내 조명의 시각적 스테이트를 게임 스테이트에 맞춰야 합니다.
5. 플레이어 상호작용이 어떤 방식으로 특정 조명 세트를 업데이트할까요?
첫 번째 질문의 대답에서 플레이어가 버튼 장치를 사용해 퍼즐과 상호작용할 것이라고 결정했습니다. 버튼의 InteractedWithEvent 에 이벤트 핸들러를 등록해 플레이어가 버튼 장치와 상호작용할 때 조명의 스테이트가 바뀌도록 만들 수 있습니다. 플레이어가 사용할 수 있는 버튼이 여러 개 있으므로 버튼을 함께 저장하는 데에도 배열을 사용할 수 있습니다.
이제 어떻게 각 버튼 이벤트를 해당 버튼이 켜고 끌 조명들에 매핑할지 정해야 합니다.
customizable_light_device 배열에 저장된 조명의 순서가 조명의 스테이트를 나타내는 logic 배열의 순서와 같으므로 버튼과 해당 버튼이 영향을 주는 조명의 인덱스 간에 매핑을 만들 수 있습니다. 배열을 사용해 이 매핑을 나타낼 수 있습니다. 배열의 각 엘리먼트의 순서가 버튼의 순서와 일치하고 각 엘리먼트가 인덱스의 배열을 저장합니다.
배열을 편집 가능하게 만들어 에디터에서 버튼과 조명의 매핑을 변경하고 코드 수정 없이 퍼즐을 재사용할 수 있습니다.
6. 퍼즐이 해결된 후에 플레이어 상호작용을 어떻게 비활성화할까요?
플레이어는 버튼 장치를 통해 퍼즐과 상호작용하며, 상호작용은 InteractedWithEvent 를 통해 탐지됩니다.
퍼즐이 해결된 후에 플레이어는 퍼즐을 변경할 수 없어야 합니다. 퍼즐 장치에서 플레이어의 입력을 받는 것을 어떻게 멈출 수 있을까요?
예를 들어 이런 세 가지 방법이 있습니다.
- 퍼즐이 해결되면 게임 내 버튼을 비활성화합니다.
tagged_lights_puzzle에logic필드를 추가하고 퍼즐이 해결되면 값을 수정합니다. 게임 스테이트를 업데이트해야 할 때마다 먼저logic필드를 확인해 퍼즐이 해결되지 않았는지 확인합니다.- 퍼즐이 해결되면 버튼의
InteractedWithEvent에서 이벤트 핸들러의 등록을 해제해 더 이상 호출되지 않도록 만듭니다.
세 번째 옵션이 가장 단순하고 효율적인 최고의 선택지입니다. 새로운 필드를 생성해서 조건부 코드 실행을 확인할 필요가 없습니다. 또한 장치 이벤트에서 등록 해제하는 방법은 다른 상황에서도 활용할 수 있습니다. 일반적으로 이벤트에 대한 알림을 받고 싶으면 등록하고 더 이상 필요 없으면 등록 해제하는 것이 좋습니다. 등록 해제 구현에 대한 디테일은 튜토리얼 후반부에 설명되어 있습니다.
솔루션을 종합해 의사코드를 작성하고 계획하기
작은 문제들에 대한 솔루션을 찾아냈으니 이것들을 합쳐 원래의 큰 문제를 해결할 차례입니다. 알고리즘을 만들어서 솔루션을 의사코드로 빌드해 보겠습니다.
게임이 시작되면 무슨 일이 일어날까요? 조명이 설정됩니다. 버튼의 InteractedWithEvent 에 등록하고, puzzle_light 태그가 달린 모든 장치를 찾아 캐싱합니다. 또한 초기 LightState에 따라 게임 내 조명을 켜고 끕니다.
OnBegin:
Result of GetCreativeObjectsWithTag(puzzle_light) is stored in the variable FoundDevices
for each Device in FoundDevices:
if Device is a Customizable Light Device:
Store the Light
if ShouldLightBeOn?:
Turn on Light
else:
Turn off Light
for each Button:
Subscribe to the Button InteractedWithEvent using the handler OnButtonInteractedWith
OnButtonInteractedWith 의 의사코드는 다음과 같습니다. InteractedButtonIndex 는 button_device 배열에서 플레이어가 상호작용하는 버튼의 인덱스입니다. 튜토리얼 후반부에서 어떻게 이벤트 핸들러 내의 정보를 가져올 수 있는지 학습할 것입니다.
OnButtonInteractedWith:
Get lights associated with the button interacted with using the ButtonsToLights array and store in the variable Lights
# 조명을 켜거나 끕니다.
for each Light in Lights:
if IsLightOn?:
Set the Light game state to off
Turn off Light
else:
Set the Light game state to on
Turn on Light
if IsPuzzleSolved():
Enable Item Spawner
for each Button:
Unsubscribe from the Button InteractedWithEvent
IsPuzzleSolved 의 의사코드가 현재 조명 스테이트가 퍼즐의 해답과 일치하는지 확인합니다. 현재 스테이트가 퍼즐의 해답과 일치하지 않으면 확인이 실패하고 위 의사코드의 if IsPuzzleSolved 블록이 실행되지 않습니다. 현재 스테이트가 퍼즐의 해답과 일치하면 확인이 성공하고 if IsPuzzleSolved 블록이 실행됩니다.
IsPuzzleSolved:
for each Light:
if IsLightOn is not equal to IsLightOnInSolution
fail and return
성공
알고리즘 개발을 완료했습니다!
다음 단계
튜토리얼의 다음 단계에서는 이 알고리즘을 Verse 프로그래밍 언어로 변환하고 프로젝트를 실제로 플레이테스트해 볼 것입니다.