リアルタイムのマルチユーザーゲームをゼロから構築する方法

公開: 2022-03-10
簡単な要約↬この記事では、リアルタイムゲームAutowuzzlerの構築の背後にあるプロセス、技術的な決定、および教訓に焦点を当てています。 Colyseusを使用してリアルタイムで複数のクライアント間でゲームの状態を共有する方法、Matter.jsを使用して物理計算を行う方法、Supabase.ioにデータを保存する方法、SvelteKitを使用してフロントエンドを構築する方法を学びます。

パンデミックが長引くにつれて、私が一緒に働いている突然離れたチームはますますフーズボールを奪われました。 リモートでフーズボールをプレイする方法を考えましたが、画面上でフーズボールのルールを再構築するだけでは面白くないことは明らかでした。

楽しいの、おもちゃの車を使ってボールを蹴ることです。これは、2歳の子供と遊んでいたときに実現したものです。 同じ夜、私はAutowuzzlerになるゲームの最初のプロトタイプの作成に着手しました。

アイデアは単純です。プレーヤーは、フーズボールテーブルに似たトップダウンアリーナで仮想のおもちゃの車を操縦します。 10ゴールを決めた最初のチームが勝ちます。

もちろん、車を使ってサッカーをするというアイデアはユニークではありませんが、 Autowuzzlerを際立たせる2つの主なアイデアがあります。物理的なフーズボールテーブルでプレーするルックアンドフィールの一部を再構築したかったのです。友達やチームメートを簡単なカジュアルゲームに招待するのはできるだけ簡単です。

この記事では、私が選択したツールとフレームワークであるAutowuzzlerの作成の背後にあるプロセスについて説明し、いくつかの実装の詳細と私が学んだ教訓を共有します。

フーズボールテーブルの背景、2つのチームの6台の車と1つのボールを示すゲームユーザーインターフェイス。
Autowuzzler(ベータ版)。2つのチームに6人の同時プレイヤーがいます。 (大プレビュー)

最初に機能する(ひどい)プロトタイプ

最初のプロトタイプは、オープンソースのゲームエンジンPhaser.jsを使用して構築されました。これは、主に付属の物理エンジン用であり、すでにある程度の経験があるためです。 ゲームステージはNext.jsアプリケーションに埋め込まれていました。これも、Next.jsをしっかりと理解していて、主にゲームに集中したかったためです。

ゲームはリアルタイムで複数のプレーヤーをサポートする必要があるため、私はExpressをWebSocketブローカーとして利用しました。 ただし、ここで注意が必要になります。

Phaserゲームではクライアントで物理計算が行われたため、単純ですが明らかに欠陥のあるロジックを選択しました。最初に接続されたクライアントには、すべてのゲームオブジェクトの物理計算を実行し、結果をExpressサーバーに送信するという疑わしい特権がありました。次に、更新された位置、角度、および力を他のプレーヤーのクライアントにブロードキャストしました。 その後、他のクライアントは変更をゲームオブジェクトに適用します。

これにより、最初のプレーヤーが物理学がリアルタイムで発生しているのを確認できるようになり(結局、ブラウザーでローカルに発生している)、他のすべてのプレーヤーは少なくとも30ミリ秒(私が選択したブロードキャストレート)遅れていました。 )、または—最初のプレーヤーのネットワーク接続が遅い場合—かなり悪い。

これがあなたにとって貧弱なアーキテクチャのように聞こえるなら—あなたは絶対に正しいです。 しかし、私はこの事実を受け入れて、ゲームが実際にプレイするのが楽しいかどうかを判断するために、すぐにプレイ可能なものを入手することに賛成しました。

アイデアを検証し、プロトタイプをダンプします

実装には欠陥がありましたが、最初の試乗に友人を招待することは十分にプレイ可能でした。 フィードバックは非常に好意的で、主な関心事は(当然のことながら)リアルタイムのパフォーマンスでした。 その他の固有の問題には、最初のプレーヤー(すべてを担当するプレーヤー)がゲームを離れたときの状況が含まれていました。誰が引き継ぐ必要がありますか? この時点ではゲームルームは1つしかないため、誰でも同じゲームに参加できます。 また、Phaser.jsライブラリが導入したバンドルサイズについても少し心配していました。

プロトタイプをダンプして、新しいセットアップと明確な目標から始める時が来ました。

プロジェクトの設定

明らかに、「最初のクライアントがすべてを支配する」アプローチは、ゲームの状態がサーバー上に存在するソリューションに置き換える必要がありました。 私の研究では、仕事に最適なツールのように聞こえるコリセウスに出くわしました。

私が選んだゲームの他の主要な構成要素については、次のとおりです。

  • Matter.jsは、Nodeで実行され、Autowuzzlerは完全なゲームフレームワークを必要としないため、Phaser.jsではなく物理エンジンとして使用されます。
  • Next.jsではなくアプリケーションフレームワークとしてのSvelteKitは、当時パブリックベータ版に入ったばかりであるためです。 (その上:Svelteでの作業が大好きです。)
  • ユーザーが作成したゲームPINを保存するためのSupabase.io。

これらのビルディングブロックをさらに詳しく見ていきましょう。

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

Colyseusとの同期化された集中型ゲーム状態

Colyseusは、Node.jsとExpressに基づくマルチプレイヤーゲームフレームワークです。 そのコアで、それは提供します:

  • 信頼できる方法でクライアント間で状態を同期します。
  • 変更されたデータのみを送信することによる、WebSocketを使用した効率的なリアルタイム通信。
  • マルチルームのセットアップ。
  • JavaScript、Unity、Defold Engine、Haxe、Cocos Creator、Construct3のクライアントライブラリ。
  • ライフサイクルフック。たとえば、部屋の作成、ユーザーの参加、ユーザーの退会など。
  • ルーム内のすべてのユーザーまたは1人のユーザーにブロードキャストメッセージとしてメッセージを送信する。
  • 組み込みの監視パネルと負荷テストツール。

Colyseusのドキュメントでは、 npm initスクリプトとサンプルリポジトリを提供することで、必要最低限​​のColyseusサーバーを簡単に使い始めることができます。

スキーマの作成

Colyseusアプリの主要なエンティティはゲームルームであり、単一のルームインスタンスとそのすべてのゲームオブジェクトの状態を保持します。 Autowuzzlerの場合、次のようなゲームセッションです。

  • 2つのチーム、
  • 限られた数のプレーヤー、
  • 1つのボール。

クライアント間で同期する必要があるゲームオブジェクトのすべてのプロパティに対してスキーマを定義する必要があります。 たとえば、ボールを同期させたいので、ボールのスキーマを作成する必要があります。

 class Ball extends Schema { constructor() { super(); this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; } } defineTypes(Ball, { x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number" });

上記の例では、Colyseusが提供するスキーマクラスを拡張する新しいクラスが作成されています。 コンストラクターでは、すべてのプロパティが初期値を受け取ります。 ボールの位置と動きは、 xyanglevelocityX, velocityYの5つのプロパティを使用して記述されます。 さらに、各プロパティのタイプを指定する必要があります。 この例ではJavaScript構文を使用していますが、もう少しコンパクトなTypeScript構文を使用することもできます。

プロパティタイプは、プリミティブタイプのいずれかです。

  • string
  • boolean
  • number (およびより効率的な整数型と浮動小数点型)

または複雑なタイプ:

  • ArraySchema (JavaScriptの配列に似ています)
  • MapSchema (JavaScriptのMapに似ています)
  • SetSchema (JavaScriptのSetに似ています)
  • CollectionSchema (ArraySchemaに似ていますが、インデックスを制御できません)

上記のBallクラスには、タイプnumberの5つのプロパティがあります。座標( xy )、現在のangle 、および速度ベクトル( velocityXvelocityY )です。

プレーヤーのスキーマは似ていますが、プレーヤーの名前とチームの番号を格納するためのプロパティがいくつか含まれています。これらのプロパティは、プレーヤーインスタンスを作成するときに指定する必要があります。

 class Player extends Schema { constructor(teamNumber) { super(); this.name = ""; this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; this.teamNumber = teamNumber; } } defineTypes(Player, { name: "string", x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number", angularVelocity: "number", teamNumber: "number", });

最後に、 Autowuzzler Roomのスキーマは、以前に定義されたクラスを接続します。1つの部屋インスタンスには複数のチームがあります(ArraySchemaに格納されています)。 また、単一のボールが含まれているため、RoomSchemaのコンストラクターで新しいBallインスタンスを作成します。 プレーヤーはMapSchemaに保存され、IDを使用してすばやく取得できます。

 class RoomSchema extends Schema { constructor() { super(); this.teams = new ArraySchema(); this.ball = new Ball(); this.players = new MapSchema(); } } defineTypes(RoomSchema, { teams: [Team], // an Array of Team ball: Ball, // a single Ball instance players: { map: Player } // a Map of Players });
Teamクラスの定義は省略されています。

マルチルームセットアップ(「マッチメイキング」)

有効なゲームPINがあれば、誰でもAutowuzzlerゲームに参加できます。 Colyseusサーバーは、最初のプレーヤーが参加するとすぐにゲームセッションごとに新しいRoomインスタンスを作成し、最後のプレーヤーが部屋を離れると部屋を破棄します。

プレイヤーを希望のゲームルームに割り当てるプロセスは、「マッチメイキング」と呼ばれます。 Colyseusを使用すると、新しい部屋を定義するときにfilterByメソッドを使用してセットアップを非常に簡単にできます。

 gameServer.define("autowuzzler", AutowuzzlerRoom).filterBy(['gamePIN']);

これで、同じgamePIN (後で「参加」する方法を説明します)でゲームに参加するプレーヤーは、同じゲームルームに入ることになります。 状態の更新やその他のブロードキャストメッセージは、同じ部屋のプレーヤーに限定されます。

Colyseusアプリの物理学

Colyseusは、信頼できるゲームサーバーをすぐに起動して実行するための多くの機能を提供しますが、物理学を含む実際のゲームメカニズムを作成するのは、開発者に任されています。 プロトタイプで使用したPhaser.jsは、ブラウザー以外の環境では実行できませんが、Phaser.jsの統合物理エンジンMatter.jsはNode.jsで実行できます。

Matter.jsを使用して、サイズや重力などの特定の物理プロパティを使用して物理世界を定義します。 これは、質量、衝突、摩擦を伴う動きなど、物理法則(シミュレートされた)を順守することによって相互作用するプリミティブ物理オブジェクトを作成するためのいくつかの方法を提供します。 実世界と同じように、力を加えることでオブジェクトを動かすことができます。

Matter.jsの「世界」はAutowuzzlerゲームの中心にあります。 これは、車の移動速度、ボールの弾力性、ゴールの位置、および誰かがゴールを撃った場合に何が起こるかを定義します。

 let ball = Bodies.circle( ballInitialXPosition, ballInitialYPosition, radius, { render: { sprite: { texture: '/assets/ball.png', } }, friction: 0.002, restitution: 0.8 } ); World.add(this.engine.world, [ball]);

Matter.jsのステージに「ボール」ゲームオブジェクトを追加するための簡略化されたコード。

ルールが定義されると、Matter.jsは、実際に画面に何かをレンダリングする場合としない場合で実行できます。 Autowuzzlerの場合、この機能を利用して、サーバークライアントの両方で物理ワールドコードを再利用していますが、いくつかの重要な違いがあります。

サーバー上の物理学の世界:

  • Colyseusを介してユーザー入力(車を操縦するためのキーボードイベント)を受け取り、ゲームオブジェクト(ユーザーの車)に適切な力を適用します。
  • 衝突の検出を含む、すべてのオブジェクト(プレーヤーとボール)のすべての物理計算を実行します。
  • 各ゲームオブジェクトの更新された状態をColyseusに通知し、Colyseusはそれをクライアントにブロードキャストします。
  • Colyseusサーバーによってトリガーされ、16.6ミリ秒(= 60フレーム/秒)ごとに更新されます。

クライアントの物理学の世界:

  • ゲームオブジェクトを直接操作しません。
  • Colyseusから各ゲームオブジェクトの更新された状態を受け取ります。
  • 更新された状態を受け取った後、位置、速度、角度の変更を適用します。
  • ユーザー入力(車を操縦するためのキーボードイベント)をColyseusに送信します。
  • ゲームのスプライトをロードし、レンダラーを使用して物理学の世界をキャンバス要素に描画します。
  • 衝突検出をスキップします(オブジェクトのisSensorオプションを使用)。
  • requestAnimationFrameを使用して更新します。理想的には60fpsです。
ColyseusサーバーアプリとSvelteKitアプリの2つの主要なブロックを示す図。 ColyseusサーバーアプリにはAutowuzzlerRoomブロックが含まれ、SvelteKitアプリにはColyseusクライアントブロックが含まれます。両方のメインブロックは、Physics World(Matter.js)という名前のブロックを共有します
Autowuzzlerアーキテクチャの主な論理ユニット:Physics Worldは、ColyseusサーバーとSvelteKitクライアントアプリの間で共有されます。 (大プレビュー)

これで、サーバーですべての魔法が発生し、クライアントは入力のみを処理し、サーバーから受け取った状態を画面に描画します。 1つの例外を除いて:

クライアントでの補間

クライアントで同じMatter.js物理学の世界を再利用しているので、簡単なトリックで経験豊富なパフォーマンスを向上させることができます。 ゲームオブジェクトの位置を更新するだけでなく、オブジェクトの速度も同期します。 このようにして、サーバーからの次の更新に通常よりも時間がかかっても、オブジェクトはその軌道上を移動し続けます。 そのため、オブジェクトを位置Aから位置Bに個別のステップで移動するのではなく、オブジェクトの位置を変更して特定の方向に移動させます。

ライフサイクル

Autowuzzler Roomクラスは、Colyseusルームのさまざまなフェーズに関連するロジックが処理される場所です。 Colyseusは、いくつかのライフサイクルメソッドを提供します。

  • onCreate :新しい部屋が作成されたとき(通常は最初のクライアントが接続したとき)。
  • onAuth :部屋への入室を許可または拒否するための認証フックとして。
  • onJoin :クライアントが部屋に接続したとき。
  • onLeave :クライアントが部屋から切断したとき。
  • onDispose :部屋が破棄されたとき。

Autowuzzlerルームは、物理ワールドの新しいインスタンスを作成し( onCreate )、クライアントが接続すると( onJoin )、プレイヤーをワールドに追加します(onCreate)。 次に、 setSimulationIntervalメソッド(メインのゲームループ)を使用して、物理ワールドを1秒間に60回(16.6ミリ秒ごと)更新します。

 // deltaTime is roughly 16.6 milliseconds this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));

物理オブジェクトはColyseusオブジェクトから独立しているため、同じゲームオブジェクト(ボールなど)の2つの順列、つまり物理世界のオブジェクトと同期可能なColyseusオブジェクトが残ります。

物理オブジェクトが変更されるとすぐに、その更新されたプロパティをColyseusオブジェクトに適用し直す必要があります。 これは、Matter.jsのafterUpdateイベントをリッスンし、そこから値を設定することで実現できます。

 Events.on(this.engine, "afterUpdate", () => { // apply the x position of the physics ball object back to the colyseus ball object this.state.ball.x = this.physicsWorld.ball.position.x; // ... all other ball properties // loop over all physics players and apply their properties back to colyseus players objects })

処理する必要のあるオブジェクトのコピーがもう1つあります。それは、ユーザー向けゲームのゲームオブジェクトです

ゲームオブジェクトの3つのバージョンを示す図:Colyseusスキーマオブジェクト、Matter.js物理オブジェクト、クライアントMatter.js物理オブジェクト。 Matter.jsはオブジェクトのColyseusバージョンを更新し、ColyseusはClientMatter.jsPhysicsオブジェクトに同期します。
Autowuzzlerは、各物理オブジェクトの3つのコピー、1つの信頼できるバージョン(Colyseusオブジェクト)、Matter.js物理ワールドのバージョン、およびクライアントのバージョンを維持します。 (大プレビュー)

クライアント側アプリケーション

サーバー上に複数の部屋のゲーム状態の同期と物理計算を処理するアプリケーションができたので、Webサイトと実際のゲームインターフェイスの構築に焦点を当てましょう。 Autowuzzlerフロントエンドには次の責任があります。

  • ユーザーがゲームPINを作成および共有して、個々の部屋にアクセスできるようにします。
  • 作成されたゲームPINを永続化のためにSupabaseデータベースに送信します。
  • プレーヤーがゲームのPINを入力するためのオプションの「ゲームに参加する」ページを提供します。
  • プレーヤーがゲームに参加するときにゲームのPINを検証します。
  • 共有可能な(つまり一意の)URLで実際のゲームをホストおよびレンダリングします。
  • Colyseusサーバーに接続し、状態の更新を処理します。
  • ランディング(「マーケティング」)ページを提供します。

これらのタスクを実装するために、次の理由でNext.jsではなくSvelteKitを選択しました。

なぜSvelteKitなのか?

neolightsoutを作成して以来、Svelteを使用して別のアプリを開発したいと思っていました。 SvelteKit(Svelteの公式アプリケーションフレームワーク)がパブリックベータに移行したとき、私はそれを使用してAutowuzzlerを構築し、新しいベータの使用に伴う頭痛の種を受け入れることにしました。Svelteを使用する喜びは明らかにそれを補います。

これらの重要な機能により、ゲームフロントエンドの実際の実装にNext.jsではなくSvelteKitを選択しました。

  • SvelteはUIフレームワークおよびコンパイラーであるため、クライアントランタイムなしで最小限のコードを出荷します。
  • Svelteには、表現力豊かなテンプレート言語とコンポーネントシステム(個人的な好み)があります。
  • Svelteには、すぐに使用できるグローバルストア、トランジション、アニメーションが含まれています。つまり、グローバル状態管理ツールキットとアニメーションライブラリを選択する際の決断疲労はありません。
  • Svelteは、単一ファイルコンポーネントでスコープ付きCSSをサポートします。
  • SvelteKitは、SSR、シンプルでありながら柔軟なファイルベースのルーティング、およびAPIを構築するためのサーバー側ルートをサポートしています。
  • SvelteKitを使用すると、各ページでサーバー上でコードを実行できます。たとえば、ページのレンダリングに使用されるデータをフェッチできます。
  • ルート間で共有されるレイアウト。
  • SvelteKitは、サーバーレス環境で実行できます。

ゲームPINの作成と保存

ユーザーがゲームのプレイを開始する前に、まずゲームのPINを作成する必要があります。 PINを他の人と共有することで、全員が同じゲームルームにアクセスできます。

Autowuzzler Webサイトの新しいゲームセクションの開始のスクリーンショット。ゲームのPIN751428と、ゲームのPINとURLをコピーして共有するためのオプションが表示されています。
生成されたゲームPINをコピーして新しいゲームを開始するか、ゲームルームへの直接リンクを共有します。 (大プレビュー)

これは、SveltesonMount関数と組み合わせたSvelteKitsサーバー側エンドポイントの優れたユースケースです。エンドポイント/api/createcodeはゲームPINを生成し、それをSupabase.ioデータベースに保存し、ゲームPINを応答として出力します。 これは、「作成」ページのページコンポーネントがマウントされるとすぐにフェッチされる応答です。

ページの作成、createcodeエンドポイント、およびSupabase.ioの3つのセクションを示す図。ページの作成は、onMount関数でエンドポイントをフェッチし、エンドポイントはゲームPINを生成し、それをSupabase.ioに保存して、ゲームPINで応答します。次に、[作成]ページにゲームのPINが表示されます。
ゲームPINはエンドポイントで作成され、Supabase.ioデータベースに保存され、「作成」ページに表示されます。 (大プレビュー)

Supabase.ioを使用したゲームPINの保存

Supabase.ioは、Firebaseのオープンソースの代替手段です。 Supabaseを使用すると、PostgreSQLデータベースを非常に簡単に作成し、クライアントライブラリの1つまたはRESTを介してデータベースにアクセスできます。

JavaScriptクライアントの場合、 createClient関数をインポートし、データベースの作成時に受け取ったパラメーターsupabase_urlおよびsupabase_keyを使用して実行します。 createcodeエンドポイントへの呼び出しごとに作成されたゲームPINを保存するには、次の単純なinsertクエリを実行するだけです。

 import { createClient } from '@supabase/supabase-js' const database = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_KEY ); const { data, error } = await database .from("games") .insert([{ code: 123456 }]);

supabase_urlsupabase_keyは.envファイルに保存されます。 SvelteKitの中心となるビルドツールであるViteのため、SvelteKitでアクセスできるようにするには、環境変数の前にVITE_を付ける必要があります。

ゲームへのアクセス

リンクをたどるのと同じくらい簡単にAutowuzzlerゲームに参加できるようにしたかったのです。 したがって、すべてのゲームルームには、以前に作成したゲームPINに基づいた独自のURL (https://autowuzzler.com/play/12345など)が必要でした。

SvelteKitでは、動的ルートパラメータを持つページは、ページファイルに名前を付けるときにルートの動的部分を角かっこで囲むことによって作成されます: client/src/routes/play/[gamePIN].svelte 。 その後、 gamePINパラメーターの値がページコンポーネントで使用可能になります(詳細については、SvelteKitのドキュメントを参照してください)。 playルートでは、Colyseusサーバーに接続し、物理ワールドをインスタンス化して画面にレンダリングし、ゲームオブジェクトの更新を処理し、キーボード入力をリッスンし、スコアなどの他のUIを表示する必要があります。

Colyseusに接続して状態を更新する

Colyseusクライアントライブラリを使用すると、クライアントをColyseusサーバーに接続できます。 まず、Colyseusサーバー(開発中のws://localhost:2567 )をポイントして、新しいColyseus.Clientを作成しましょう。 次に、前に選択した名前( autowuzzler )とrouteパラメーターからのgamePINを使用して部屋に参加します。 gamePINパラメーターは、ユーザーが正しいルームインスタンスに参加することを確認します(上記の「マッチメイキング」を参照)。

 let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN });

SvelteKitは最初にサーバー上でページをレンダリングするため、ページの読み込みが完了した後にのみ、このコードがクライアント上で実行されることを確認する必要があります。 ここでも、そのユースケースにonMountライフサイクル関数を使用します。 (Reactに精通している場合、 onMountは空の依存関係配列を持つuseEffectフックに似ています。)

 onMount(async () => { let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN }); })

Colyseusゲームサーバーに接続したので、ゲームオブジェクトへの変更を聞き始めることができます。

これは、部屋に参加しているプレーヤーonAdd )を聞いて、このプレーヤーの状態の更新を連続して受信する方法の例です。

 this.room.state.players.onAdd = (player, key) => { console.log(`Player has been added with sessionId: ${key}`); // add player entity to the game world this.world.createPlayer(key, player.teamNumber); // listen for changes to this player player.onChange = (changes) => { changes.forEach(({ field, value }) => { this.world.updatePlayer(key, field, value); // see below }); }; };

物理学の世界のupdatePlayerメソッドでは、ColyseusのonChangeがすべての変更されたプロパティのセットを提供するため、プロパティを1つずつ更新します。

ゲームオブジェクトはColyseusサーバーを介して間接的にのみ操作されるため、この関数はクライアントバージョンの物理学の世界でのみ実行されます。

 updatePlayer(sessionId, field, value) { // get the player physics object by its sessionId let player = this.world.players.get(sessionId); // exit if not found if (!player) return; // apply changes to the properties switch (field) { case "angle": Body.setAngle(player, value); break; case "x": Body.setPosition(player, { x: value, y: player.position.y }); break; case "y": Body.setPosition(player, { x: player.position.x, y: value }); break; // set velocityX, velocityY, angularVelocity ... } }

同じ手順が他のゲームオブジェクト(ボールとチーム)にも適用されます。それらの変更を聞いて、変更された値をクライアントの物理ワールドに適用します。

これまでのところ、キーボード入力をリッスンしてサーバーに送信する必要があるため、オブジェクトは移動していません。 すべてのキーダウンイベントでイベントを直接送信する代わりに、現在押されているキーのマップを維持し、50ミリ秒のループでイベントをkeydownサーバーに送信します。 このようにして、複数のキーを同時に押すことをサポートし、キーが押されたままのときに最初の連続したキーkeydownイベントの後に発生する一時停止を軽減できます。

 let keys = {}; const keyDown = e => { keys[e.key] = true; }; const keyUp = e => { keys[e.key] = false; }; document.addEventListener('keydown', keyDown); document.addEventListener('keyup', keyUp); let loop = () => { if (keys["ArrowLeft"]) { this.room.send("move", { direction: "left" }); } else if (keys["ArrowRight"]) { this.room.send("move", { direction: "right" }); } if (keys["ArrowUp"]) { this.room.send("move", { direction: "up" }); } else if (keys["ArrowDown"]) { this.room.send("move", { direction: "down" }); } // next iteration requestAnimationFrame(() => { setTimeout(loop, 50); }); } // start loop setTimeout(loop, 50);

これでサイクルは完了です。キーストロークをリッスンし、対応するコマンドをColyseusサーバーに送信して、サーバー上の物理学の世界を操作します。 次に、Colyseusサーバーは、新しい物理プロパティをすべてのゲームオブジェクトに適用し、データをクライアントに伝播して、ゲームのユーザー向けインスタンスを更新します。

マイナーな妨害

振り返ってみると、誰も教えてくれなかったが、誰かが思いついたはずのカテゴリの2つのことが思い浮かびました。

  • 物理エンジンがどのように機能するかをよく理解することは有益です。 物理プロパティと制約を微調整するのにかなりの時間を費やしました。 以前、Phaser.jsとMatter.jsを使用して小さなゲームを作成しましたが、オブジェクトを想像どおりに動かすための試行錯誤がたくさんありました。
  • リアルタイムは難しい—特に物理ベースのゲームでは。 わずかな遅延はエクスペリエンスを大幅に悪化させます。クライアント間で状態をColyseusと同期することはうまく機能しますが、計算と送信の遅延を取り除くことはできません。

SvelteKitの落とし穴と警告

ベータオーブンから取り出したばかりのときにSvelteKitを使用したので、指摘したいいくつかの落とし穴と警告がありました。

  • SvelteKitで使用するには、環境変数の前にVITE_を付ける必要があることを理解するのに少し時間がかかりました。 これは、FAQに適切に文書化されています。
  • Supabaseを使用するには、package.jsonのdependenciesリストとdevDependenciesリストの両方にSupabaseを追加する必要がありました。 私はこれがもはや事実ではないと信じています。
  • SvelteKits load機能はサーバークライアントの両方で実行されます!
  • 完全なホットモジュール交換(状態の保持を含む)を有効にするには、ページコンポーネントにコメント行<!-- @hmr:keep-all -->を手動で追加する必要があります。 詳細については、FAQを参照してください。

他の多くのフレームワークもぴったりでしたが、このプロジェクトにSvelteKitを選択したことを後悔していません。 これにより、非常に効率的な方法でクライアントアプリケーションを操作できるようになりました。これは主に、Svelte自体が非常に表現力があり、ボイラープレートコードの多くをスキップするためですが、Svelteにはアニメーション、トランジション、スコープ付きCSS、グローバルストアなどが組み込まれているためです。 SvelteKitは、必要なすべてのビルディングブロック(SSR、ルーティング、サーバールート)を提供し、まだベータ版ですが、非常に安定していて高速であると感じました。

展開とホスティング

最初は、HerokuインスタンスでColyseus(Node)サーバーをホストし、WebSocketとCORSを機能させるために多くの時間を無駄にしました。 結局のところ、小さな(無料の)Heroku dynoのパフォーマンスは、リアルタイムのユースケースには十分ではありません。 その後、ColyseusアプリをLinodeの小さなサーバーに移行しました。 クライアント側のアプリケーションは、SvelteKitsアダプター-netlifyを介してNetlifyによってデプロイされ、ホストされます。 ここで驚くことはありません:Netlifyはうまく機能しました!

結論

アイデアを検証するための非常に単純なプロトタイプから始めることは、プロジェクトがフォローする価値があるかどうか、そしてゲームの技術的な課題がどこにあるかを理解するのに大いに役立ちました。 最終的な実装では、Colyseusは、複数の部屋に分散された複数のクライアント間でリアルタイムに状態を​​同期するという面倒な作業をすべて処理しました。 スキーマを適切に記述する方法を理解すれば、Colyseusを使用してリアルタイムマルチユーザーアプリケーションを迅速に構築できることは印象的です。 Colyseusの組み込みの監視パネルは、同期の問題のトラブルシューティングに役立ちます。

このセットアップを複雑にしたのは、ゲームの物理レイヤーでした。これは、維持する必要のある各物理関連のゲームオブジェクトの追加コピーを導入したためです。 SvelteKitアプリからSupabase.ioにゲームPINを保存するのは非常に簡単でした。 後から考えると、SQLiteデータベースを使用してゲームのPINを保存することもできましたが、新しいことを試すことは、サイドプロジェクトを構築するときの楽しみの半分です。

最後に、ゲームのフロントエンドを構築するためにSvelteKitを使用することで、私はすばやく移動することができました。

さあ、先に進んで、友達をAutowuzzlerのラウンドに招待しましょう!

SmashingMagazineでさらに読む

  • 「モグラたたきゲームを構築してReactを始めよう」JheyTompkins
  • 「リアルタイムマルチプレイヤーバーチャルリアリティゲームを構築する方法」、Alvin Wan
  • 「Node.jsでマルチプレイヤーテキストアドベンチャーエンジンを作成する」、Fernando Doglio
  • 「モバイルWebデザインの未来:ビデオゲームのデザインとストーリーテリング」、Suzanne Scacca
  • 「バーチャルリアリティでエンドレスランナーゲームを構築する方法」、Alvin Wan