GitHub - lapras-inc/gather-tag-game: GatherのAPIを利用して鬼ごっこを実現するシステム
GatherのAPIを利用して鬼ごっこを実現するシステム. Contribute to lapras-inc/gather-tag-game development by creating an account on GitHub.
github.com
こんにちは、LAPRAS にて業務委託で開発に参画している余湖(よご)と申します。
2022年3月-4月にかけてLAPRAS恒例のモブプロ生配信企画の新シリーズとして、
Gather のAPIを使って、4人で4時間で「鬼ごっこ」を実装する生配信を行いました。
一連のYouTubeのプレイリストはこちら
参照: Gather公式HP
Gather とは、RPGのような可愛らしいバーチャル空間上で、キャラクターたちを自由に歩き回らせたり、チャットしたり、ビデオ通話したりできるアプリケーションです。
特にコロナのご時世ではリモートワークの需要も急激に増していく中で、
無味乾燥になりがちなオンラインミーティングに隙間の会話を生んでくれるツールだと、僕は思っています。
LAPRASでは半年ほど前から全社的にこのツールを導入し、現在では社内のほとんどの会議がGather上で行われています。
GatherにはHTTP APIとWebsocket API があるのですが、
後者の方がやれることが多いので、今回の配信では、そちらを使うことになりました。
そして、あらかじめ初回配信までに僕が少しだけAPIを触ってみていたので、このAPIを使ってできることをみんなに共有することで、作るもののイメージを膨らませました。
↑ teleport
APIを使ってキャラクターをランダムな座標に飛ばしてみている様子
なにを作るか決まったところで、みんなでfigjam上でブレインストーミングし、
最後に投票した結果、「Gather上で鬼ごっこができるようにする」ことが開発テーマに決まりました。
そして、このテーマを実現するための開発を、次回以降の配信からスタートして合計4時間以内に達成する のが私たちの目標となりました。
ちなみに、この時にブレインストーミングに使ったfigjamのボードはこちらから誰でもご覧になることができます。
「鬼ごっこをつくる」 と一口に言っても、この時点ではみんなの頭の中にあるものはまだ曖昧でした。
そこで、ユーザーストーリーマッピングを作ることで、
をしました。
@gathertown/gather-game-client
を npm install
する(配信時点では、v34.0.0
) を使用しました
この時点で、下記のような状態になりました。
import { Game } from '@gathertown/gather-game-client';
global.WebSocket = require("isomorphic-ws");
const game = new Game(undefined, () => Promise.resolve({ apiKey: getApiKey() }));
game.init(getSpaceId());
game.connect();
game.subscribeToConnection(
(connected) => {
// 無事に接続できると、このコールバックが呼ばれる
console.log('is connected: ', connected)
game.subscribeToEvent('info', console.info)
game.subscribeToEvent('warn', console.warn)
game.subscribeToEvent('error', console.error)
}
)
WebSocket
オブジェクトに暗黙的にライブラリが依存していて、それがないと動かない状態でした。 ソースコードを読んだ結果、isomorphic-ws を npm install
し、 global
オブジェクトにプロパティを追加することで解消することがわかったので、そのように対応しました。game.subscribeToEvent('error', callback)
などで明示的にハンドラーを登録しておく必要がありましたsetInterval(()=> {
if (Object.keys(game.players).length) {
console.log('teleport!!!!')
game.teleport('rw-6', 10, 20, Object.keys(game.players)[0])
}
}, 1000)
このようなコードで、game.players
の先頭に入っているプレーヤー(おそらくそのマップに最初に入室したプレーヤー)が1秒ごとに x: 10, y: 20
の座標に飛ばされるプログラムが完成しました。
そして、不幸にもこの時その選ばれたプレーヤーになってしまった @to_ryo_endo さんは、蟻地獄にはまった蟻のように抜け出せなくなってしまいましたが、しかし、ひとまず実装初回としては 無事にAPIが叩けるようになったのが大きな成果でした。
この回からいよいよプロダクトバックログのアイテムに取りかかります。
第1回で計画した通りの順番で、まずは鬼が鬼だとわかるようにすることがこの回の目標となりました。
Gather APIではアバターの着せ替えは以下のメソッドで行うことができます。
game.setOutfitString(outfitString, playerId)
できました🎉
outfitString
というのは、アバターの各パーツ(服や髪の色など)の状態を表したjson文字列なのですが、
この outfitString
文字列をコードで書いてつくるのは大変です。
そこで、player.outfitString
から現在のoutfitStringを取り出すことができるので、
GatherのGUI上で鬼用のアバターを作った上で、それをコピーして定数に保存しておくことになりました。
そして、完成した鬼がこちら
さて、いよいよ 当たり判定 の実装に入ります。
game.subscribeToEvent('playerMoves', callback)
このAPIを使うことで、全てのプレーヤーの移動に対してハンドラーを登録することができるので、
方針としては、
このような感じで実装を目指します。
game.subscribeToEvent('playerMoves', () => {
if (!Object.keys(game.players).length) return
// 今いる player の座標を全部出す
const players = Object.values(game.players)
const [oni, ...humans] = players
humans.forEach(human => {
const xDiff = oni.x - human.x
const yDiff = oni.y - human.y
console.log(`${human.name} to oni: ${xDiff} ${yDiff}`)
})
})
以上のようなコードを実行した状態で、プレーヤーが動くと、
このようにログに各プレーヤーから鬼までのx座標・y座標の差分が出力されました 🎉
そして、この回は、ここで時間切れとなりました。
当たり判定を実装するにあたり、
任意の地点Aから任意の地点Bまでの距離を計算するのには、
上図のように、三平方の定理 (懐かしいですね…)を利用して、(鬼と人のX座標の差分)の2乗 + (鬼と人のY座標の差分)の2乗
を計算し、
その平方根を求めればそれが距離である、ということができそうです。
その上で、この距離が閾値にしたい値(ここでは 1
)よりも小さい時を 当たり
とするロジックを組む方向で実装を進めました。
const r = Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2))
if (r <= 1){
// 当たり!
game.setOutfitString(oniOutfitString, playerId)
}
これで当たり判定ができました 🎉
ここで、そもそも「ゲームの状態(誰が鬼か)を保存する仕組みがまだない」「ゲームはどのようにしてスタートするのか」という問題が浮上しました。
話し合いの結果、「ゲーム内の特定座標を誰かが踏んだら、その瞬間にその人を鬼としてゲームが始まる」 という仕様が新たに決まったので、GameState
という簡単な変数を作り、そこに鬼のプレーヤーの id
を保存するような実装を追加しました。
初期化処理については、私たちがテストで使っていたGatherのデフォルトのマップでは、「P」と書いてあるマスがちょうどよさそうな場所だったので、
そこを踏んだら初期化がはじまるように実装をしました。
↑ リセットポイントを踏むと鬼になり、ゲームが始まる様子 🎉
リセットポイントを踏んだ時に、鬼以外の人のアバターを強制的に人間のアバターに着せ替えさせる処理も追加しました。
前回までの配信で実装した範囲では、全てのプレイヤーの動きにトリガーして、
当たり判定が行われてしまうため、いわゆる「タッチ返し」があまりにもシビアに行われてしまう、
という問題が発生していました。
そこで、「最後に鬼交代が行われた時刻より、一定時間経過していなければタッチ判定をしない」 という仕様が追加されました。
これに伴い、GameState.lastOniChangedAt
というプロパティを追加し、鬼変更時刻を保持しておきつつ、
const now = new Date().getTime()
// 鬼交代の後に一定時間の間は鬼交代を無効にする
if (now - gameState.lastOniChangedAt! < 500) return
このような処理を実装しました。
ここで、MVPで目標としていた全ての開発タスクを完了し、
必要最低限の鬼ごっこの機能が完成しました 🎉
前回までの実装だと、あくまで「鬼から人の距離」のみを当たりの判定に使っていました。
しかしこれでは、鬼の後ろを通った人すらもタッチされてしまうので、
「鬼が向いている方向に人間がいて、かつ距離が近い時のみタッチにする」仕様が追加されました。player
オブジェクトには .direction
というプロパティがあり、そこに向きを示すenumの値が入っているので、
それを用いて、当たり判定の精度を向上させました。
(そしてこの非常に煩雑な分岐を書くために、30分ほどみんなで頭を悩ませるつらい時間が続きました…)
なにはともあれ、これで「鬼が通っても人の方を向いていなければタッチにならない」を実現することができました 🎉
そして、最後にみんなでしばらく完成した鬼ごっこを遊んだあとに、
シリーズを通した振り返りをfigjam上で行いました。
振り返りは「熱気球」というフレームワークに則って、
の観点に分けて、それぞれ考えて発表しました。
GatherのAPIはまだまだ仕様の変更も多く、ドキュメントに書いていないことも多かったですが、
ソースコードを読み、API実装者の意図を推測しながら目的のAPIを探す作業は、さながら宝探し💎のようで
楽しい体験でした。
ちなみに、今回のモブプロ生配信で作成したプログラムのソースコードはGitHub上に公開されていますので、
「我が社もリモートワークに遊び心を導入したい」という会社さまはご自由にお使い頂いてかまいません。