Node.js에서 멀티플레이어 텍스트 어드벤처 엔진 작성하기(1부)

게시 됨: 2022-03-10
빠른 요약 ↬ 텍스트 어드벤처에 대해 들어본 적이 있습니까? 이 일련의 기사에서 Fernando Doglio는 귀하와 귀하의 친구들이 즐기는 모든 텍스트 모험을 플레이할 수 있는 전체 엔진을 만드는 방법에 대해 설명합니다. 맞습니다. 우리는 텍스트 어드벤처 장르에 멀티플레이어를 추가하여 약간의 재미를 더할 것입니다!

텍스트 어드벤처는 게임에 그래픽이 없고 CRT 모니터의 검은 화면에서 읽은 설명과 상상만 하던 시절에 디지털 롤 플레잉 게임의 첫 번째 형태 중 하나였습니다.

향수를 불러일으키고 싶다면 Colossal Cave Adventure(또는 원래 이름 그대로 Adventure)라는 이름이 종을 울릴 수 있습니다. 그것은 최초의 텍스트 어드벤처 게임이었습니다.

당시의 실제 텍스트 모험의 사진
과거의 실제 텍스트 모험의 그림. (큰 미리보기)

위의 이미지는 현재 최고의 AAA 어드벤처 게임과는 거리가 먼 게임을 실제로 보는 방법입니다. 그렇긴 하지만, 그것들은 플레이하는 것이 재미있었고 당신이 그 텍스트 앞에 홀로 앉아 그것을 이길 방법을 알아내려고 노력하면서 수백 시간의 시간을 훔칠 것입니다.

이해할 만하게도, 텍스트 어드벤처는 더 나은 비주얼을 제공하는 게임으로 수년에 걸쳐 대체되었으며(많은 게임이 그래픽을 위해 스토리를 희생했다고 주장할 수 있지만), 특히 지난 몇 년 동안 다른 플레이어와 협업하는 능력이 향상되었습니다. 친구와 함께 놀아요. 이 특정 기능은 원본 텍스트 모험에 없는 기능이며 이 기사에서 다시 가져오고 싶은 기능입니다.

이 시리즈의 다른 부분

  • 2부: 게임 엔진 서버 설계
  • 3부: 터미널 클라이언트 만들기
  • 4부: 게임에 채팅 추가하기

점프 후 더! 아래에서 계속 읽기 ↓

우리의 목표

이 글의 제목에서 짐작할 수 있듯이 이 노력의 요점은 친구와 모험을 공유할 수 있는 텍스트 모험 엔진을 만드는 것입니다. Dungeons & Dragons 게임(좋은 텍스트 모험과 마찬가지로 볼 그래픽이 없음).

엔진을 만들 때 채팅 서버와 클라이언트는 꽤 많은 작업을 수행합니다. 이 기사에서는 엔진 이면의 아키텍처, 클라이언트가 서버와 상호 작용하는 방법, 이 게임의 규칙 등을 설명하는 디자인 단계를 보여드리겠습니다.

이것이 어떻게 생겼는지 시각적인 도움을 주기 위해 제 목표는 다음과 같습니다.

게임 클라이언트의 최종 UI를 위한 일반 와이어프레임
게임 클라이언트의 최종 UI를 위한 일반 와이어프레임(큰 미리보기)

그것이 우리의 목표입니다. 일단 도착하면 빠르고 더러운 모형 대신 스크린샷을 보게 될 것입니다. 그럼 프로세스를 알아보겠습니다. 가장 먼저 다룰 것은 전체 디자인입니다. 그런 다음 이 코드를 작성하는 데 사용할 가장 관련성이 높은 도구를 다룰 것입니다. 마지막으로 가장 관련성이 높은 몇 가지 코드를 보여 드리겠습니다(물론 전체 리포지토리에 대한 링크 포함).

바라건대, 끝까지 친구들과 함께 시도할 새로운 텍스트 모험을 만드는 자신을 발견하게 될 것입니다!

설계 단계

디자인 단계에서는 전체 청사진을 다룰 것입니다. 지루하지 않도록 최선을 다할 것이지만 동시에 첫 번째 코드 라인을 작성하기 전에 발생해야 하는 비하인드 스토리를 보여주는 것이 중요하다고 생각합니다.

여기에서 적절한 양의 세부 사항으로 다루고자 하는 네 가지 구성 요소는 다음과 같습니다.

  • 엔진
    이것은 메인 게임 서버가 될 것입니다. 게임 규칙은 여기에서 구현되며 모든 유형의 클라이언트가 사용할 수 있도록 기술적으로 불가지론적인 인터페이스를 제공합니다. 우리는 터미널 클라이언트를 구현할 것이지만 웹 브라우저 클라이언트나 원하는 다른 유형으로 동일한 작업을 수행할 수 있습니다.
  • 채팅 서버
    자체 기사를 가질 만큼 복잡하기 때문에 이 서비스에도 자체 모듈이 있습니다. 채팅 서버는 게임 중에 플레이어가 서로 통신할 수 있도록 합니다.
  • 클라이언트
    앞서 언급했듯이 이것은 이상적으로는 이전의 모형과 유사한 터미널 클라이언트가 될 것입니다. 엔진과 채팅 서버 모두에서 제공하는 서비스를 사용합니다.
  • 게임(JSON 파일)
    마지막으로 실제 게임의 정의를 살펴보겠습니다. 이것의 요점은 게임 파일이 엔진 요구 사항을 준수하는 한 모든 게임을 실행할 수 있는 엔진을 만드는 것입니다. 따라서 코딩이 필요하지는 않지만 앞으로 우리 자신의 모험을 작성하기 위해 모험 파일을 구성하는 방법을 설명하겠습니다.

엔진

게임 엔진 또는 게임 서버는 REST API가 될 것이며 필요한 모든 기능을 제공할 것입니다.

내가 REST API를 선택한 이유는 이러한 유형의 게임에서 HTTP에 의해 추가된 지연과 해당 비동기 특성이 문제를 일으키지 않기 때문입니다. 그러나 우리는 채팅 서버에 대해 다른 경로로 이동해야 합니다. 그러나 API에 대한 끝점 정의를 시작하기 전에 엔진이 무엇을 할 수 있는지 정의해야 합니다. 이제 시작하겠습니다.

특징 설명
게임에 참여 플레이어는 게임의 ID를 지정하여 게임에 참여할 수 있습니다.
새 게임 만들기 플레이어는 새 게임 인스턴스를 만들 수도 있습니다. 엔진은 ID를 반환해야 다른 사람들이 참가할 수 있습니다.
복귀 장면 이 기능은 파티가 있는 현재 장면을 반환해야 합니다. 기본적으로 모든 관련 정보(가능한 작업, 개체 등)와 함께 설명을 반환합니다.
장면과 상호 작용 이것은 가장 복잡한 것 중 하나가 될 것입니다. 클라이언트로부터 명령을 받아 해당 작업을 수행하기 때문입니다. 몇 가지 예를 들면 이동, 밀기, 가져오기, 보기, 읽기 등입니다.
재고 확인 이것은 게임과 상호 작용하는 방법이지만 장면과 직접 관련이 없습니다. 따라서 각 플레이어의 인벤토리를 확인하는 것은 다른 작업으로 간주됩니다.

움직임에 대한 한마디

모험을 통해 이동하는 것은 플레이어가 취할 수 있는 핵심 행동 중 하나이기 때문에 게임에서 거리를 측정할 방법이 필요합니다. 게임 플레이를 단순화하기 위해 이 숫자를 시간의 척도로 사용할 것입니다. 이러한 유형의 게임에는 전투와 같은 턴 기반 작업이 있으므로 실제 시계로 시간을 측정하는 것이 가장 좋지 않을 수 있습니다. 대신 거리를 사용하여 시간을 측정할 것입니다(즉, 8의 거리가 2 중 하나보다 횡단하는 데 더 많은 시간이 필요하므로 플레이어에게 정해진 양의 "거리 포인트" 동안 지속되는 효과를 추가하는 것과 같은 작업을 수행할 수 있습니다. ).

움직임에 대해 고려해야 할 또 다른 중요한 측면은 우리가 혼자 노는 것이 아니라는 것입니다. 간단하게 하기 위해 엔진은 플레이어가 파티를 분할할 수 없도록 합니다(비록 향후에는 흥미로운 개선 사항이 될 수 있음). 이 모듈의 초기 버전에서는 파티의 과반수가 결정하는 곳으로만 ​​모든 사람이 이동할 수 있습니다. 따라서 이동은 합의에 의해 이루어져야 합니다. 즉, 모든 이동 조치는 발생하기 전에 당사자의 과반수가 요청할 때까지 기다려야 합니다.

전투

전투는 이러한 유형의 게임에서 매우 중요한 또 다른 측면이며 엔진에 추가하는 것을 고려해야 합니다. 그렇지 않으면 우리는 재미를 놓치게 될 것입니다.

이것은 솔직히 말해서 재발명되어야 하는 것이 아닙니다. 턴 기반 파티 전투는 수십 년 동안 존재해 왔으며, 따라서 우리는 해당 메커니즘의 버전을 구현하기만 하면 됩니다. 우리는 전투를 좀 더 역동적으로 유지하기 위해 임의의 숫자를 굴리는 "주제"의 Dungeons & Dragons 개념과 혼합할 것입니다.

즉, 전투에 참여하는 모든 사람이 자신의 행동을 선택하는 순서가 무작위로 지정되며 여기에는 적도 포함됩니다.

마지막으로(아래에서 더 자세히 다루겠지만) 설정된 "데미지" 숫자로 선택할 수 있는 항목이 있습니다. 이것들은 전투 중에 사용할 수 있는 아이템입니다. 해당 속성이 없는 것은 적에게 0의 피해를 줍니다. 당신이 그 물건을 사용하여 싸우려고 할 때 메시지를 추가하여 당신이 하려는 것이 의미가 없다는 것을 알게 될 것입니다.

클라이언트-서버 상호작용

이제 주어진 클라이언트가 이전에 정의된 기능을 사용하여 서버와 상호 작용하는 방법을 살펴보겠습니다(아직 끝점에 대해 생각하지 않지만 잠시 후에 도착할 것입니다).

(큰 미리보기)

클라이언트와 서버(서버의 관점에서)의 초기 상호작용은 새로운 게임의 시작이며, 그 단계는 다음과 같습니다.

  1. 새 게임을 만듭니다 .
    클라이언트는 서버에 새 게임 생성을 요청합니다.
  2. 채팅방을 만듭니다 .
    이름에 명시되어 있지는 않지만 서버는 채팅 서버에 채팅방을 만드는 것뿐만 아니라 여러 플레이어가 모험을 통해 플레이할 수 있도록 필요한 모든 것을 설정합니다.
  3. 게임의 메타 데이터를 반환합니다 .
    서버에서 게임을 만들고 플레이어를 위한 채팅방이 마련되면 클라이언트는 후속 요청을 위해 해당 정보가 필요합니다. 이것은 대부분 클라이언트가 자신과 참가하려는 현재 게임을 식별하는 데 사용할 수 있는 ID 집합입니다(자세한 내용은 잠시 후에 설명).
  4. 수동으로 게임 ID를 공유 합니다.
    이 단계는 플레이어가 직접 수행해야 합니다. 우리는 일종의 공유 메커니즘을 생각해 낼 수 있지만 향후 개선을 위해 희망 목록에 남겨 두겠습니다.
  5. 게임에 참여하세요 .
    이것은 매우 간단합니다. 모든 사람이 게임 ID를 가지고 있으므로 클라이언트 애플리케이션을 사용하여 모험에 참여할 것입니다.
  6. 채팅방에 참여하십시오 .
    마지막으로 플레이어의 클라이언트 앱은 게임의 메타데이터를 사용하여 모험의 대화방에 참여합니다. 이것은 게임 전 필요한 마지막 단계입니다. 이 모든 작업이 완료되면 플레이어는 모험을 시작할 준비가 됩니다!
기존 게임에 대한 작업 순서
기존 게임에 대한 작업 순서(큰 미리보기)

전제 조건이 모두 충족되면 플레이어는 모험을 시작하고 파티 채팅을 통해 생각을 공유하고 스토리를 진행할 수 있습니다. 위의 다이어그램은 이를 위해 필요한 4단계를 보여줍니다.

다음 단계는 게임 루프의 일부로 실행됩니다. 즉, 게임이 끝날 때까지 계속 반복됩니다.

  1. 요청 장면 .
    클라이언트 앱은 현재 장면에 대한 메타데이터를 요청합니다. 이것은 루프의 모든 반복에서 첫 번째 단계입니다.
  2. 메타 데이터를 반환합니다 .
    그러면 서버는 현재 장면에 대한 메타데이터를 다시 보냅니다. 이 정보에는 일반적인 설명, 그 안에 있는 개체 및 서로 관련되는 방식과 같은 항목이 포함됩니다.
  3. 명령을 보냅니다 .
    여기서부터 재미가 시작됩니다. 이것은 플레이어의 주요 입력입니다. 여기에는 수행하려는 작업과 해당 작업의 대상(예: 촛불 불기, 돌 잡기 등)이 포함됩니다.
  4. 보낸 명령에 대한 반응을 반환합니다 .
    이것은 단순히 2단계일 수 있지만 명확성을 위해 추가 단계로 추가했습니다. 가장 큰 차이점은 2단계는 이 루프의 시작으로 간주될 수 있다는 것입니다. 반면 이 단계에서는 이미 플레이하고 있다는 점을 고려하므로 서버는 이 작업이 누구에게 영향을 미칠지 이해해야 합니다(단일 플레이어 또는 모든 플레이어).

추가 단계로, 실제로 흐름의 일부는 아니지만 서버는 클라이언트와 관련된 상태 업데이트에 대해 클라이언트에게 알립니다.

이 추가 반복 단계의 이유는 플레이어가 다른 플레이어의 작업에서 받을 수 있는 업데이트 때문입니다. 한 장소에서 다른 장소로 이동해야 하는 요구 사항을 상기하십시오. 전에 말했듯이, 대다수의 플레이어가 방향을 선택하면 모든 플레이어가 이동합니다(모든 플레이어의 입력이 필요하지 않음).

여기서 흥미로운 점은 HTTP(서버가 REST API가 될 것이라고 이미 언급한 바 있음)가 이러한 유형의 동작을 허용하지 않는다는 것입니다. 따라서 우리의 옵션은 다음과 같습니다.

  1. 클라이언트에서 X초마다 폴링을 수행하고,
  2. 클라이언트-서버 연결과 병렬로 작동하는 일종의 알림 시스템을 사용합니다.

제 경험상 저는 옵션 2를 선호하는 경향이 있습니다. 사실 저는 이런 종류의 행동에 Redis를 사용할 것입니다(이 기사에서는 그렇게 할 것입니다).

다음 다이어그램은 서비스 간의 종속성을 보여줍니다.

클라이언트 앱과 게임 엔진 간의 상호 작용
클라이언트 앱과 게임 엔진 간의 상호 작용(큰 미리보기)

채팅 서버

이 모듈의 디자인에 대한 세부 사항은 개발 단계(이 기사의 일부가 아님)를 위해 남겨두겠습니다. 즉, 우리가 결정할 수 있는 사항이 있습니다.

우리가 정의할 수 있는 한 가지는 서버에 대한 제한 집합이며, 이는 우리의 작업을 간소화할 것입니다. 그리고 우리가 카드를 올바르게 사용하면 강력한 인터페이스를 제공하는 서비스로 끝날 수 있으므로 궁극적으로 게임에 전혀 영향을 미치지 않으면서 더 적은 제한을 제공하도록 구현을 확장하거나 변경할 수 있습니다.

  • 파티당 하나의 방만 있을 것입니다.
    우리는 하위 그룹이 생성되는 것을 허용하지 않습니다. 이것은 파티가 분열되지 않도록 하는 것과 관련이 있습니다. 개선 사항을 구현하면 하위 그룹 및 사용자 지정 대화방 생성을 허용하는 것이 좋습니다.
  • 비공개 메시지는 없을 것입니다.
    이것은 순전히 단순화를 위한 것이지만 그룹 채팅을 하는 것만으로도 이미 충분합니다. 우리는 지금 개인 메시지가 필요하지 않습니다. 최소한의 실행 가능한 제품을 작업할 때마다 불필요한 기능의 토끼굴에 빠지지 않도록 하십시오. 위험하고 빠져나오기 힘든 길이다.
  • 우리는 메시지를 지속하지 않을 것입니다.
    즉, 파티에서 나가면 메시지를 잃게 됩니다. 이렇게 하면 모든 유형의 데이터 저장소를 처리할 필요가 없고 오래된 메시지를 저장하고 복구하기 위한 최상의 데이터 구조를 결정하는 데 시간을 낭비할 필요가 없기 때문에 작업이 크게 간소화됩니다. 모두 메모리에 저장되며 채팅방이 활성화되어 있는 동안에는 그대로 유지됩니다. 그것이 닫히면, 우리는 단순히 그들에게 작별 인사를 할 것입니다!
  • 통신은 소켓을 통해 수행됩니다 .
    슬프게도 클라이언트는 게임 엔진을 위한 RESTful 채널과 채팅 서버를 위한 소켓이라는 이중 통신 채널을 처리해야 합니다. 이것은 클라이언트의 복잡성을 약간 증가시킬 수 있지만 동시에 모든 모듈에 대해 최상의 통신 방법을 사용합니다. (채팅 서버에서 REST를 강제 실행하거나 게임 서버에서 소켓을 강제 실행하는 것은 실질적인 의미가 없습니다. 이러한 접근 방식은 비즈니스 로직도 처리하는 서버 측 코드의 복잡성을 증가시키므로 그 측면에 집중하겠습니다. 지금은.)

채팅 서버용입니다. 결국, 적어도 처음에는 복잡하지 않을 것입니다. 코딩을 시작할 때가 되면 해야 할 일이 더 많지만 이 기사에서는 충분한 정보 이상입니다.

클라이언트

이것은 코딩이 필요한 마지막 모듈이며, 가장 멍청한 모듈이 될 것입니다. 경험상, 나는 내 클라이언트는 멍청하게 만들고 내 서버는 똑똑하게 하는 것을 선호합니다. 그렇게 하면 서버에 대한 새 클라이언트를 만드는 것이 훨씬 쉬워집니다.

우리가 같은 페이지에 있기 때문에 여기에 우리가 끝내야 할 고수준 아키텍처가 있습니다.

전체 개발의 최종 고수준 아키텍처
전체 개발의 최종 고수준 아키텍처(큰 미리보기)

우리의 간단한 CLI 클라이언트는 매우 복잡한 것을 구현하지 않습니다. 사실, 우리가 다루어야 할 가장 복잡한 부분은 실제 UI입니다. 왜냐하면 그것이 텍스트 기반 인터페이스이기 때문입니다.

즉, 클라이언트 애플리케이션이 구현해야 하는 기능은 다음과 같습니다.

  1. 새 게임을 만듭니다 .
    가능한 한 단순하게 유지하고 싶기 때문에 CLI 인터페이스를 통해서만 이 작업을 수행합니다. 실제 UI는 게임에 참여한 후에만 사용되므로 다음 단계로 넘어갑니다.
  2. 기존 게임에 참여하십시오 .
    이전 지점에서 반환된 게임 코드가 주어지면 플레이어는 이를 사용하여 참여할 수 있습니다. 다시 말하지만, 이것은 UI 없이 할 수 있어야 하는 것이므로 이 기능은 텍스트 UI 사용을 시작하는 데 필요한 프로세스의 일부가 될 것입니다.
  3. 게임 정의 파일을 구문 분석합니다 .
    우리는 이것들에 대해 잠시 논의할 것이지만 클라이언트는 무엇을 보여줄지 그리고 그 데이터를 어떻게 사용하는지 알기 위해 이 파일들을 이해할 수 있어야 합니다.
  4. 모험과 상호 작용하십시오.
    기본적으로 이것은 플레이어가 주어진 시간에 설명된 환경과 상호 작용할 수 있는 기능을 제공합니다.
  5. 각 플레이어의 인벤토리를 유지합니다 .
    클라이언트의 각 인스턴스에는 메모리 내 항목 목록이 포함됩니다. 이 목록은 백업됩니다.
  6. 채팅 지원 .
    클라이언트 앱은 또한 채팅 서버에 연결하고 사용자를 파티의 채팅방에 로그인해야 합니다.

나중에 클라이언트의 내부 구조와 디자인에 대해 자세히 설명합니다. 그 동안 마지막 준비 단계인 게임 파일로 디자인 단계를 마무리하겠습니다.

게임: JSON 파일

지금까지 기본적인 마이크로서비스 정의를 다루었기 때문에 이것이 흥미로워집니다. 그들 중 일부는 REST를 말할 수 있고 다른 일부는 소켓으로 작동할 수 있지만 본질적으로 모두 동일합니다. 정의하고, 코딩하고, 서비스를 제공합니다.

이 특정 구성 요소의 경우 아무 것도 코딩할 계획이 없지만 디자인해야 합니다. 기본적으로 우리는 게임, 게임 안의 장면 및 그 안의 모든 것을 정의하기 위한 일종의 프로토콜을 구현하고 있습니다.

생각해보면 텍스트 어드벤처는 기본적으로 서로 연결된 일련의 방이며 그 안에 상호 작용할 수 있는 "사물"이 있으며 모두 함께 연결되어 있기를 바랍니다. 이제 우리 엔진은 마지막 부분을 처리하지 않습니다. 그 부분은 당신에게 달려 있습니다. 그러나 나머지는 희망이 있습니다.

이제 서로 연결된 방 세트로 돌아가서 제 생각에는 그래프처럼 들립니다. 앞서 언급한 거리 또는 이동 속도의 개념도 추가하면 가중치 그래프가 있습니다. 그리고 그것은 그들 사이의 경로를 나타내는 가중치(또는 단지 숫자 — 호출에 대해 걱정하지 마십시오)가 있는 노드 세트일 뿐입니다. 다음은 시각적 개체입니다.

가중 그래프 예
가중 그래프 예(큰 미리보기)

이것이 가중 그래프입니다. 이미 이해하셨으리라 확신합니다. 하지만 완성도를 위해 엔진이 준비되면 어떻게 해야 하는지 보여드리겠습니다.

모험 설정을 시작하면 지도를 만들게 됩니다(아래 이미지의 왼쪽에서 볼 수 있는 것처럼). 그런 다음 이미지 오른쪽에서 볼 수 있듯이 이를 가중치 그래프로 변환합니다. 우리의 엔진이 그것을 집어들고 당신이 올바른 순서로 그것을 통과할 수 있도록 할 것입니다.

주어진 던전에 대한 예시 그래프
주어진 던전에 대한 예시 그래프(큰 미리보기)

위의 가중치 그래프를 통해 플레이어가 입구에서 왼쪽 날개 끝까지 갈 수 없도록 할 수 있습니다. 그들은 그 둘 사이의 노드를 통과해야 하고 그렇게 하면 연결의 가중치를 사용하여 측정할 수 있는 시간이 소모됩니다.

이제 "재미" 부분으로. 그래프가 JSON 형식으로 어떻게 보이는지 봅시다. 여기서 나와 함께 참아라. 이 JSON에는 많은 정보가 포함되어 있지만 최대한 많은 정보를 살펴보겠습니다.

 { "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } } { "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } } { "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } } { "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }

많은 것처럼 보이지만 게임에 대한 간단한 설명으로 요약하면 위의 다이어그램에서 볼 수 있듯이 각각 다른 방과 연결된 6개의 방으로 구성된 던전이 있습니다.

당신의 임무는 그것을 통해 이동하고 그것을 탐험하는 것입니다. 무기를 찾을 수 있는 두 곳이 있습니다(주방이나 어두운 방에서 의자를 부숴서). 당신은 또한 잠긴 문에 직면하게 될 것입니다. 따라서 열쇠(사무실 같은 방 안에 위치)를 찾으면 열쇠를 열고 수집한 무기로 보스와 싸울 수 있습니다.

당신은 그것을 죽이면 이기거나 그것에 의해 죽임을 당할 것입니다.

이제 전체 JSON 구조와 해당 세 섹션에 대한 자세한 개요를 살펴보겠습니다.

그래프

여기에는 노드 간의 관계가 포함됩니다. 기본적으로 이 섹션은 이전에 보았던 그래프로 직접 변환됩니다.

이 섹션의 구조는 매우 간단합니다. 모든 노드가 다음 속성으로 구성된 노드 목록입니다.

  • 게임의 다른 모든 노드 중에서 노드를 고유하게 식별하는 ID입니다.
  • 기본적으로 사람이 읽을 수 있는 ID 버전인 이름.
  • 다른 노드에 대한 링크 집합입니다. 이것은 4개의 가능한 열쇠의 존재에 의해 입증됩니다: 북쪽”, 남쪽, 동쪽 및 서쪽. 우리는 결국 이 네 가지의 조합을 추가하여 더 많은 방향을 추가할 수 있습니다. 모든 링크에는 관련 노드의 ID와 해당 관계의 거리(또는 가중치)가 포함됩니다.

게임

이 섹션에는 일반 설정 및 조건이 포함됩니다. 특히 위의 예에서 이 섹션에는 승패 조건이 포함됩니다. 즉, 이 두 가지 조건으로 게임이 끝날 때 엔진에 알립니다.

일을 단순하게 유지하기 위해 두 가지 조건만 추가했습니다.

  • 보스를 죽여서 이기거나,
  • 또는 살해당하여 잃는다.

객실

여기에서 163개 라인의 대부분이 시작되며 섹션 중 가장 복잡합니다. 여기에서 모험의 모든 방과 그 안에 있는 모든 것을 설명할 것입니다.

이전에 정의한 ID를 사용하여 모든 방에 대한 키가 있습니다. 그리고 모든 방에는 설명, 아이템 목록, 출구(또는 문) 목록 및 플레이할 수 없는 캐릭터(NPC) 목록이 있습니다. 이러한 속성 중에서 필수 항목은 설명뿐입니다. 해당 속성은 엔진에서 사용자가 보고 있는 내용을 알려주는 데 필요하기 때문입니다. 나머지는 보여줄 것이 있는 경우에만 있을 것입니다.

이러한 속성이 게임에서 무엇을 할 수 있는지 살펴보겠습니다.

설명

방에 대한 관점은 상황에 따라 달라질 수 있기 때문에 이 항목은 생각만큼 간단하지 않습니다. 예를 들어 첫 번째 방에 대한 설명을 보면 기본적으로 횃불을 들고 있지 않는 한 아무 것도 볼 수 없다는 것을 알 수 있습니다.

따라서 아이템을 집어들고 사용하면 게임의 다른 부분에 영향을 미치는 전역 조건이 트리거될 수 있습니다.

아이템

이것들은 모든 것을 나타냅니다”라고 방 안에서 찾을 수 있습니다. 모든 항목은 그래프 섹션의 노드와 동일한 ID와 이름을 공유합니다.

그들은 또한 한 번 집어들면 그 항목을 어디에 보관해야 하는지를 나타내는 "목적지" 속성을 가질 것입니다. 이것은 당신의 손에 하나의 아이템만 가질 수 있기 때문에 관련이 있습니다. 반면에 인벤토리에는 원하는 만큼 많이 가질 수 있습니다.

마지막으로, 이러한 항목 중 일부는 플레이어가 해당 항목으로 무엇을 하기로 결정했는지에 따라 다른 작업이나 상태 업데이트를 트리거할 수 있습니다. 이것의 한 예는 입구에서 불이 켜진 횃불입니다. 그 중 하나를 잡으면 게임에서 상태 업데이트를 트리거하고 게임에서 다음 방에 대한 다른 설명을 표시합니다.

항목에는 원래 항목이 파괴되면(예: "파기" 작업을 통해) 작동되는 "하위 항목"이 있을 수도 있습니다. 항목은 여러 항목으로 나눌 수 있으며 이는 "subitems" 요소에 정의됩니다.

기본적으로 이 요소는 새 항목의 배열일 뿐이며 항목 생성을 트리거할 수 있는 일련의 작업도 포함합니다. 이렇게 하면 기본적으로 원본 항목에서 수행하는 작업에 따라 다른 하위 항목을 만들 수 있는 가능성이 열립니다.

마지막으로 일부 항목에는 "손상" 속성이 있습니다. 따라서 아이템을 사용하여 NPC를 공격하는 경우 해당 값은 NPC에게서 생명을 빼는 데 사용됩니다.

출구

이것은 단순히 출구의 방향과 출구의 속성을 나타내는 속성 세트입니다(설명, 검사하려는 경우 이름, 경우에 따라 상태).

출구는 상태를 기반으로 실제로 통과할 수 있는지 엔진이 이해해야 하기 때문에 항목과 별개의 엔터티입니다. 잠겨 있는 출구는 상태를 잠금 해제로 변경하는 방법을 알아내지 않는 한 통과할 수 없습니다.

NPC

마지막으로 NPC는 다른 목록의 일부가 됩니다. 기본적으로 엔진이 각 항목이 어떻게 작동해야 하는지 이해하는 데 사용할 통계가 있는 항목입니다. 우리의 예에서 정의한 것은 체력을 나타내는 "hp"와 무기와 마찬가지로 각 명중이 플레이어의 체력에서 차감되는 숫자인 "데미지"입니다.

그것이 내가 만든 던전에 대한 것입니다. 그렇습니다. 앞으로 JSON 파일 생성을 단순화하기 위해 일종의 레벨 편집기를 만드는 것을 고려할 수 있습니다. 그러나 지금은 그럴 필요가 없습니다.

아직 깨닫지 못한 경우를 대비하여 이와 같은 파일에 게임을 정의하는 것의 주요 이점은 Super Nintendo 시대에 카트리지를 사용했던 것처럼 JSON 파일을 전환할 수 있다는 것입니다. 새 파일을 로드하고 새로운 모험을 시작하세요. 쉬운!

마무리 생각

지금까지 읽어주셔서 감사합니다. 아이디어를 실현하기 위해 거쳐야 하는 디자인 프로세스가 마음에 드셨기를 바랍니다. 그러나 내가 진행하면서 이것을 구성하고 있다는 것을 기억하십시오. 그래서 오늘 우리가 정의한 것이 작동하지 않을 것이라는 것을 나중에 깨달을 수 있습니다. 그런 경우에는 역추적하고 수정해야 합니다.

여기에 제시된 아이디어를 개선하고 엄청난 엔진을 만들 수 있는 방법이 많이 있다고 확신합니다. 그러나 그것은 모든 사람을 지루하게 만들지 않으면서 내가 기사에 넣을 수 있는 것보다 훨씬 더 많은 단어를 필요로 할 것이므로 지금은 그 상태로 두겠습니다.

이 시리즈의 다른 부분

  • 2부: 게임 엔진 서버 설계
  • 3부: 터미널 클라이언트 만들기
  • 4부: 게임에 채팅 추가하기