Node.jsでマルチプレイヤーテキストアドベンチャーエンジンを作成する(パート1)

公開: 2022-03-10
簡単な要約↬テキストアドベンチャーについて聞いたことがありますか? このシリーズの記事では、Fernando Doglioが、自分や友達が楽しむテキストアドベンチャーをプレイできるエンジン全体を作成するプロセスについて説明します。 そうです、テキストアドベンチャーのジャンルにマルチプレイヤーを追加することで、少しスパイスを効かせます!

テキストアドベンチャーは、デジタルロールプレイングゲームの最初の形式の1つでした。当時、ゲームにはグラフィックがなく、CRTモニターの黒い画面で読んだ自分の想像力と説明しかありませんでした。

ノスタルジックになりたいのなら、コロッサルケーブアドベンチャー(または元々の名前のとおりアドベンチャー)という名前がベルを鳴らしているかもしれません。 これは、これまでに作成された最初のテキストアドベンチャーゲームでした。

当時の実際のテキストアドベンチャーの写真
当時の実際のテキストアドベンチャーの写真。 (大プレビュー)

上の画像は、実際にゲームをどのように見るかを示しています。これは、現在のトップAAAアドベンチャーゲームとはかけ離れています。 そうは言っても、彼らは遊ぶのが楽しく、あなたがそのテキストの前に一人で座って、それを打ち負かす方法を見つけようとして、あなたの時間を何百時間も盗むでしょう。

当然のことながら、テキストアドベンチャーは、長年にわたって、より優れたビジュアルを提供するゲームに置き換えられてきました(ただし、それらの多くはグラフィックスのストーリーを犠牲にしていると主張することができます)。特に過去数年間で、他の人とのコラボレーション能力が向上しています。友達と一緒に遊ぶ。 この特定の機能は、元のテキストアドベンチャーに欠けていた機能であり、この記事で持ち帰りたい機能です。

このシリーズの他の部分

  • パート2:ゲームエンジンサーバーの設計
  • パート3:ターミナルクライアントの作成
  • パート4:ゲームにチャットを追加する

ジャンプした後もっと! 以下を読み続けてください↓

私たちの目的

この記事のタイトルからお察しのとおり、この取り組みの要点は、友達と冒険を共有できるテキストアドベンチャーエンジンを作成することです。これにより、友達と同じようにコラボレーションできるようになります。ダンジョンズ&ドラゴンズのゲーム(古き良きテキストの冒険と同じように、見るべきグラフィックはありません)。

エンジンを作成する際、チャットサーバーとクライアントは非常に多くの作業を行います。 この記事では、設計フェーズを紹介し、エンジンの背後にあるアーキテクチャ、クライアントがサーバーと対話する方法、このゲームのルールなどについて説明します。

これがどのように見えるかを視覚的に支援するために、ここに私の目標があります。

ゲームクライアントの最終UIの一般的なワイヤーフレーム
ゲームクライアントの最終UIの一般的なワイヤーフレーム(大プレビュー)

それが私たちの目標です。 そこに着くと、すばやく汚いモックアップの代わりにスクリーンショットが表示されます。 それでは、プロセスに取り掛かりましょう。 最初に取り上げるのは、全体のデザインです。 次に、これをコーディングするために使用する最も関連性の高いツールについて説明します。 最後に、最も関連性の高いコードの一部を紹介します(もちろん、完全なリポジトリへのリンクもあります)。

うまくいけば、最後には、友達と一緒に試すための新しいテキストアドベンチャーを作成していることに気付くでしょう!

設計段階

設計段階では、全体的な青写真について説明します。 私はあなたを死に至らしめないように最善を尽くしますが、同時に、コードの最初の行を配置する前に発生する必要のある舞台裏のことをいくつか示すことが重要だと思います。

ここでかなり詳細に説明したい4つのコンポーネントは次のとおりです。

  • そのエンジン
    これがメインのゲームサーバーになります。 ゲームルールはここに実装され、あらゆるタイプのクライアントが使用できる技術にとらわれないインターフェイスを提供します。 ターミナルクライアントを実装しますが、Webブラウザクライアントまたはその他の任意のタイプでも同じことができます。
  • チャットサーバー
    独自の記事を持つのに十分複雑であるため、このサービスにも独自のモジュールがあります。 チャットサーバーは、ゲーム中にプレーヤーが互いに通信できるようにします。
  • クライアント
    前に述べたように、これはターミナルクライアントであり、理想的には、以前のモックアップに似ています。 エンジンとチャットサーバーの両方が提供するサービスを利用します。
  • ゲーム(JSONファイル)
    最後に、実際のゲームの定義について説明します。 これの要点は、ゲームファイルがエンジンの要件に準拠している限り、任意のゲームを実行できるエンジンを作成することです。 したがって、これにはコーディングは必要ありませんが、将来、独自のアドベンチャーを作成するために、アドベンチャーファイルをどのように構成するかを説明します。

そのエンジン

ゲームエンジンまたはゲームサーバーはRESTAPIになり、必要なすべての機能を提供します。

このタイプのゲームでは、HTTPによって追加される遅延とその非同期性によって問題が発生しないという理由だけで、RESTAPIを選択しました。 ただし、チャットサーバーには別のルートを使用する必要があります。 ただし、APIのエンドポイントの定義を開始する前に、エンジンで何ができるかを定義する必要があります。 それでは、それに取り掛かりましょう。

特徴説明
ゲームに参加するプレイヤーはゲームのIDを指定することでゲームに参加できます。
新しいゲームを作成するプレーヤーは、新しいゲームインスタンスを作成することもできます。 他の人がIDを使用して参加できるように、エンジンはIDを返す必要があります。
リターンシーンこの機能は、パーティーが行われている現在のシーンを返す必要があります。 基本的に、関連するすべての情報(可能なアクション、その中のオブジェクトなど)を含む説明が返されます。
シーンと対話するこれは、クライアントからコマンドを受け取り、そのアクション(移動、プッシュ、取得、検索、読み取りなど)を実行するため、最も複雑なものの1つになります。
在庫を確認するこれはゲームを操作する方法ですが、シーンに直接関係するものではありません。 したがって、各プレイヤーのインベントリを確認することは、異なるアクションと見なされます。

ムーブメントについて一言

冒険を移動することはプレイヤーが実行できるコアアクションの1つであるため、ゲーム内の距離を測定する方法が必要です。 ゲームプレイを単純化するために、この数値を時間の尺度として使用します。 これらのタイプのゲームには戦闘などのターンベースのアクションがあることを考えると、実際の時計で時間を測定するのは最善ではないかもしれません。 代わりに、距離を使用して時間を測定します(つまり、距離が8の場合、2の1つよりも移動に時間がかかるため、一定量の「距離ポイント」の間持続するエフェクトをプレーヤーに追加するなどのことができます。 )。

動きについて考慮すべきもう1つの重要な側面は、私たちが一人で遊んでいないということです。 簡単にするために、エンジンはプレイヤーにパーティーを分割させません(ただし、それは将来の興味深い改善になる可能性があります)。 このモジュールの初期バージョンでは、パーティーの大多数が決定した場所にのみ全員が移動できます。 したがって、移動はコンセンサスによって行われる必要があります。つまり、すべての移動アクションは、パーティの大多数が要求するのを待ってから実行されます。

戦闘

戦闘は、これらのタイプのゲームのもう1つの非常に重要な側面であり、エンジンへの追加を検討する必要があります。 そうでなければ、私たちはいくつかの楽しみを逃してしまうでしょう。

正直なところ、これは再発明する必要があるものではありません。 ターン制のパーティ戦闘は何十年も前から行われているので、そのメカニズムのバージョンを実装するだけです。 戦闘をもう少しダイナミックに保つために、ランダムな数字を転がして、「イニシアチブ」というダンジョンズ&ドラゴンズのコンセプトと混ぜ合わせます。

言い換えれば、戦いに関与するすべての人が自分の行動を選択する順序はランダム化され、それは敵を含みます。

最後に(これについては以下で詳しく説明しますが)、設定された「ダメージ」番号で拾うことができるアイテムがあります。 これらは、戦闘中に使用できるアイテムです。 その特性を持たないものは、敵に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を使用します(この記事ではそうします)。

次の図は、サービス間の依存関係を示しています。

クライアントアプリとゲームエンジン間の相互作用
クライアントアプリとゲームエンジン間の相互作用(大プレビュー)

チャットサーバー

このモジュールの設計の詳細は、開発フェーズ(この記事の一部ではありません)に残しておきます。 そうは言っても、私たちが決めることができることがあります。

定義できることの1つは、サーバーの一連の制限です。これにより、今後の作業が簡素化されます。 また、カードを正しくプレイすると、堅牢なインターフェイスを提供するサービスが完成する可能性があります。これにより、最終的には、ゲームにまったく影響を与えることなく、実装を拡張または変更して、制限を減らすことができます。

  • パーティーごとに1つの部屋のみがあります。
    サブグループは作成されません。 これは、パーティーを分割させないことと密接に関連しています。 たぶん、その拡張機能を実装したら、サブグループとカスタムチャットルームの作成を可能にすることは良い考えです。
  • プライベートメッセージはありません。
    これは純粋に単純化の目的ですが、グループチャットを行うことですでに十分です。 今のところプライベートメッセージは必要ありません。 最小限の実行可能な製品に取り組んでいるときはいつでも、不要な機能のうさぎの穴を降りないようにしてください。 それは危険な道であり、抜け出すのは難しい道です。
  • メッセージは永続化しません。
    つまり、パーティーを離れると、メッセージが失われます。 これにより、どのタイプのデータストレージも処理する必要がなくなり、古いメッセージを保存および回復するための最適なデータ構造を決定する時間を無駄にする必要がなくなるため、タスクが大幅に簡素化されます。 それはすべてメモリに保存され、チャットルームがアクティブである限りそこにとどまります。 閉店したら、さようならを言います!
  • 通信はソケットを介して行われます。
    残念ながら、クライアントは二重の通信チャネルを処理する必要があります。ゲームエンジン用のRESTfulチャネルと、チャットサーバー用のソケットです。 これにより、クライアントの複雑さが少し増す可能性がありますが、同時に、すべてのモジュールに最適な通信方法が使用されます。 (チャットサーバーにRESTを強制したり、ゲームサーバーにソケットを強制したりすることに意味はありません。このアプローチでは、ビジネスロジックも処理するサーバー側のコードが複雑になるため、その側に焦点を当てましょう。今のところ。)

チャットサーバーは以上です。 結局のところ、少なくとも最初は複雑ではありません。 コーディングを開始するときはまだやるべきことがありますが、この記事では十分な情報です。

クライアント

これはコーディングが必要な最後のモジュールであり、私たちの最も愚かなモジュールになるでしょう。 経験則として、私はクライアントを馬鹿にし、サーバーを賢くすることを好みます。 そうすれば、サーバー用の新しいクライアントの作成がはるかに簡単になります。

同じページにいるので、これが最終的に必要な高レベルのアーキテクチャです。

開発全体の最終的な高レベルのアーキテクチャ
開発全体の最終的な高レベルのアーキテクチャ(大規模なプレビュー)

単純なClIクライアントは、非常に複雑なものを実装しません。 実際、私たちが取り組む必要がある最も複雑なビットは、テキストベースのインターフェイスであるため、実際のUIです。

そうは言っても、クライアントアプリケーションが実装しなければならない機能は次のとおりです。

  1. 新しいゲームを作成します
    できるだけシンプルにしたいので、これはCLIインターフェイスを介してのみ実行されます。 実際のUIは、ゲームに参加した後にのみ使用され、次のポイントに進みます。
  2. 既存のゲームに参加します。
    前のポイントから返されたゲームのコードを考えると、プレーヤーはそれを使用して参加できます。これもUIなしで実行できるはずなので、この機能はテキストUIの使用を開始するために必要なプロセスの一部になります。
  3. ゲーム定義ファイルを解析します
    これらについては少し説明しますが、クライアントは、表示する内容とそのデータの使用方法を知るために、これらのファイルを理解できる必要があります。
  4. 冒険と対話します。
    基本的に、これにより、プレイヤーはいつでも記述された環境と対話することができます。
  5. 各プレイヤーのインベントリを維持します。
    クライアントの各インスタンスには、アイテムのメモリ内リストが含まれます。 このリストはバックアップされます。
  6. チャットをサポートします
    クライアントアプリはチャットサーバーにも接続し、ユーザーをパーティーのチャットルームにログインさせる必要があります。

クライアントの内部構造と設計については後で詳しく説明します。 それまでの間、最後の準備であるゲームファイルで設計段階を終了しましょう。

ゲーム:JSONファイル

これまで、基本的なマイクロサービスの定義について説明してきたので、ここが興味深いところです。 それらのいくつかはRESTを話すかもしれませんし、他はソケットで動作するかもしれませんが、本質的にはそれらはすべて同じです:あなたはそれらを定義し、それらをコーディングし、そしてそれらはサービスを提供します。

この特定のコンポーネントについては、何もコーディングする予定はありませんが、設計する必要があります。 基本的に、ゲーム、ゲーム内のシーン、およびゲーム内のすべてを定義するための一種のプロトコルを実装しています。

あなたがそれについて考えるならば、テキストアドベンチャーは、基本的に、互いに接続された部屋のセットであり、それらの中には、あなたが相互作用できる「もの」があり、すべてが、うまくいけば、まともな物語と結びついています。 さて、私たちのエンジンはその最後の部分を処理しません。 その部分はあなた次第です。 しかし、残りの部分については、希望があります。

さて、相互接続された部屋のセットに戻ると、それはグラフのように聞こえます。前述の距離または移動速度の概念も追加すると、重み付きグラフが作成されます。 そして、それは、それらの間のパスを表す重み(または単なる数値-それが何と呼ばれるかについて心配しないでください)を持つノードのセットにすぎません。 これがビジュアルです(私は見ることで学ぶのが大好きなので、画像を見てください、OK?):

加重グラフの例
加重グラフの例(大きなプレビュー)

これは加重グラフです—それだけです。 そして、あなたはすでにそれを理解していると確信していますが、完全を期すために、私たちのエンジンの準備ができたら、あなたがそれをどのように行うかをお見せしましょう。

アドベンチャーの設定を開始したら、マップを作成します(下の画像の左側に表示されているように)。 次に、画像の右側に表示されているように、それを加重グラフに変換します。 私たちのエンジンはそれを拾い上げ、正しい順序でそれを通り抜けることができます。

特定のダンジョンのグラフの例
特定のダンジョンのグラフの例(大きなプレビュー)

上記の加重グラフを使用すると、プレーヤーが入り口から左翼まで移動できないようにすることができます。 それらはこれら2つの間のノードを通過する必要があり、そうすると時間がかかります。これは、接続からの重みを使用して測定できます。

さて、「楽しい」部分に移りましょう。 グラフが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つの部屋で構成されるダンジョンがあります。

あなたの仕事はそれを通り抜けてそれを探索することです。 武器を見つけることができる場所は2つあります(キッチンか暗い部屋のどちらかで、椅子を壊して)。 また、施錠されたドアに直面します。 したがって、(オフィスのような部屋の中にある)鍵を見つけたら、鍵を開けて、集めた武器で上司と戦うことができます。

あなたはそれを殺すことによって勝つか、それによって殺されることによって負けるでしょう。

次に、JSON構造全体とその3つのセクションのより詳細な概要を見ていきましょう。

グラフ

これには、ノード間の関係が含まれます。 基本的に、このセクションは、前に見たグラフに直接変換されます。

このセクションの構造は非常に単純です。 これはノードのリストであり、すべてのノードが次の属性で構成されています。

  • ゲーム内の他のすべてのノードの中でノードを一意に識別するID。
  • 名前。基本的には人間が読める形式のIDです。
  • 他のノードへのリンクのセット。 これは、「北」、「南」、「東」、「西」の4つの可能なキーの存在によって証明されます。 これら4つの組み合わせを追加することで、最終的にさらに方向性を追加することができます。 すべてのリンクには、関連するノードのIDとその関係の距離(または重み)が含まれています。

ゲーム

このセクションには、一般的な設定と条件が含まれます。 特に、上記の例では、このセクションには勝ちと負けの条件が含まれています。 つまり、これら2つの条件で、ゲームがいつ終了できるかをエンジンに通知します。

簡単にするために、次の2つの条件を追加しました。

  • あなたは上司を殺すことによって勝ちます、
  • または殺されることによって失う。

部屋

ここが163行のほとんどが由来する場所であり、セクションの中で最も複雑です。 ここでは、冒険のすべての部屋とその中のすべてについて説明します。

前に定義したIDを使用して、すべての部屋にキーがあります。 また、すべての部屋には、説明、アイテムのリスト、出口(またはドア)のリスト、およびプレイできないキャラクター(NPC)のリストがあります。 これらのプロパティのうち、必須である必要があるのは説明だけです。これは、エンジンが表示内容を通知するために必要なためです。 それらの残りは、何かを示すことがある場合にのみそこにあります。

これらのプロパティがゲームに何をもたらすかを見てみましょう。

説明

状況によって部屋の見え方が変わることがあるので、思ったほどシンプルではありません。 たとえば、最初の部屋の説明を見ると、もちろん、火のついたトーチを持っていない限り、デフォルトでは何も見えないことに気付くでしょう。

したがって、アイテムを拾い上げて使用すると、ゲームの他の部分に影響を与えるグローバルな状態が発生する可能性があります。

アイテム

これらはすべてのものを表しています」あなたが部屋の中に見つけることができます。 すべてのアイテムは、グラフセクションのノードが持っていたのと同じIDと名前を共有します。

また、「目的地」プロパティもあります。これは、受け取ったアイテムをどこに保管するかを示します。 これは、手に持っているアイテムが1つだけであるのに対し、インベントリには好きなだけアイテムを持っていることができるため、関連性があります。

最後に、これらのアイテムのいくつかは、プレイヤーがそれらをどうするかによって、他のアクションやステータスの更新をトリガーする可能性があります。 この一例は、入り口から点火された松明です。 それらの1つを取得すると、ゲームのステータス更新がトリガーされ、ゲームに次の部屋の別の説明が表示されます。

アイテムには「サブアイテム」を含めることもできます。これは、元のアイテムが破棄されると(たとえば、「ブレーク」アクションによって)機能します。 アイテムはいくつかのアイテムに分割でき、それは「サブアイテム」要素で定義されます。

基本的に、この要素は新しいアイテムの配列であり、作成をトリガーできる一連のアクションも含まれています。 これにより、基本的に、元のアイテムに対して実行するアクションに基づいて、さまざまなサブアイテムを作成できるようになります。

最後に、いくつかのアイテムには「ダメージ」プロパティがあります。 したがって、アイテムを使用してNPCをヒットした場合、その値はそれらからライフを差し引くために使用されます。

出口

これは、出口の方向とそのプロパティ(検査する場合は説明、名前、場合によってはステータス)を示す一連のプロパティです。

出口は、ステータスに基づいて実際にそれらをトラバースできるかどうかをエンジンが理解する必要があるため、アイテムとは別のエンティティです。 ロックされている出口は、ステータスをロック解除に変更する方法を理解しない限り、それらを通過できません。

NPC

最後に、NPCは別のリストの一部になります。 これらは基本的に、エンジンがそれぞれの動作を理解するために使用する統計を含むアイテムです。 この例で定義したのは、ヘルスポイントを表す「hp」と、武器と同様に、各ヒットがプレイヤーのヘルスから差し引く数である「damage」です。

私が作成したダンジョンは以上です。 はい、それはたくさんあります。将来的には、JSONファイルの作成を簡素化するために、ある種のレベルエディターを作成することを検討するかもしれません。 しかし今のところ、それは必要ありません。

まだ気付いていない方のために説明すると、このようなファイルでゲームを定義することの主な利点は、スーパーファミコン時代のカートリッジのようにJSONファイルを切り替えることができることです。 新しいファイルをロードして、新しい冒険を始めてください。 簡単!

まとめ

これまで読んでくれてありがとう。 アイデアを実現するために私が行った設計プロセスを楽しんでいただけたと思います。 ただし、これは私が行っているときに作成していることを忘れないでください。後で、今日定義したものが機能しないことに気付く可能性があります。その場合は、バックトラックして修正する必要があります。

ここに提示されたアイデアを改善し、エンジンの1つの地獄を作る方法はたくさんあると確信しています。 しかし、それは私が誰にとっても退屈にすることなく記事に入れることができるよりもはるかに多くの言葉を必要とするでしょう、それで私たちは今のところそれをそのままにしておきます。

このシリーズの他の部分

  • パート2:ゲームエンジンサーバーの設計
  • パート3:ターミナルクライアントの作成
  • パート4:ゲームにチャットを追加する