メインコンテンツまでスキップ

マルチプレイヤー

以下はすべて、プロジェクトでマルチプレイヤーが有効で、かつプレイヤーがルームを開いている ときだけ効きます。

それ以外のときは、これらの呼び出しは黙って何もしません。これは意図的です。マルチプレイヤー用に 書いた script も、1 人用のプレビューでそのまま問題なく動きます。2 つのバージョンは要りません。

ctx.net.id;        // この参加者の id
ctx.net.peers(); // いまルームにいる全員
ctx.net.onJoin((peer) => {});
ctx.net.onLeave((peer) => {});

自動で複製されるもの

shared と印を付けた物理ボディの位置だけです。それ以外は自分で送ります。

少なく聞こえますが、この少なさが正しいのです。木箱・ドア・ボールは、全員が同じものとして合意する 1 つのオブジェクトであるべきですが、プレイヤーキャラクターはそうであってはいけません。さもないと、 1 人が歩いただけで全員のアバターが動いてしまいます。 → 所有

メッセージ

出来事を伝えるためのものです。誰かが得点した、ラウンドが始まった、ドアが開いた、など。

ctx.net.send('score', { points: 10 });
ctx.net.on('score', ({ from, data }) => {
/* … */
});

送りっぱなしで、自分以外の全員に届きます。

共有状態

全員が合意する必要のある値のためのものです。最後の書き込みが勝つ単純なマップで、全員に複製されます。

ctx.net.state.set(ctx.net.id, { name, colour, ready: true });
ctx.net.state.get(somePeerId);
ctx.net.state.all();
ctx.net.state.on(ctx.net.id, (value) => {});

ctx.net.id をキーにするのが、プレイヤーごとの状態を扱う定石です。各自が自分のエントリーを書き、 他人のものを読みます。

全員にスポーンさせる

const id = ctx.net.spawn(props.bullet, { position, rotation });
ctx.net.despawn(id);

id はすぐ返りますが、オブジェクトが現れるのはルームが確認したあとです。この往復があるおかげで id がすべてのクライアントで同一になり、そのあと物理がそれについて合意できます。

スポーンさせたものを消せるのは、スポーンさせた本人だけです。ルームを出ると、自分のスポーンも 一緒に消えます。

他人をなめらかに描く

いちばんよく失敗するのがここなので、専用の呼び出しが用意されています。

ctx.net.follow(otherPlayerObject, { position, rotation });
ctx.net.unfollow(otherPlayerObject);

受け取った位置ごとにこれを呼んでください。毎秒 10〜20 回が普通です。あとはランタイムが毎フレーム transform を書き、バッファを少し遅らせて再生します。こうすれば、動かす先として既知の位置が常に 2 つある状態を保てます。

受け取った位置を transform に直接書かないでください

パケットはフレームよりずっと少ない頻度で届くので、オブジェクトは止まっては跳ぶ、という動きに なります。遠くのプレイヤーは歩くのではなく、飛び飛びに移動して見えます。最新のパケットへ なめらかに近づける方法も解決になりません。パケットとパケットの間には、現在より先に向かう先が ないので、止まってから急に動きます。

同じ位置を繰り返し渡してもコストはゼロなので、いま手元にある値で毎フレーム follow を呼ぶのが 想定された使いかたです。

うまくいく役割分担

ゼロから始めるなら、この分担でかなり先まで行けます。

  • プレイヤーは自分のキャラクターを自分で持つ。 ローカルのボディを、自分の入力で動かします
  • その位置を外へ送る。 net.state かチャンネルで、毎秒 10〜20 回
  • 他人のキャラクターは follow で描く
  • 世界のオブジェクトは shared なボディにする。 物理がただで整合性を保ってくれます
  • スコアとラウンドの状態は net.state に置く。 その出来事について権限を持つ側が書き込みます

次へ: トラブルシューティング — うまく動かないとき。