なぜオンライン四目並べか
Deno DeployについてLTする機会があったので
以前作ったChatGPT DALL-E3で美少女四目並べ(note記事)を元に
Deno Deploy+Websocketでオンライン四目並べができるゲームを作成しました。
できたもの
概要
プロジェクトの目的: このプロジェクトは、DenoとWebSocketを用いたリアルタイムWebアプリケーションの開発過程とその技術的な側面を理解することを目的としています。
アプリケーションの概要: 開発されたアプリケーションは、オンラインでプレイできる四目並べゲームです。プレイヤーはリアルタイムで駒を配置し、ゲームの進行を他のプレイヤーと共有します。
技術スタック: 主な技術として、フロントエンドにはJavaScriptとHTML5 Canvasを、バックエンドにはDenoとWebSocketプロトコルを使用しています。また、データ管理とリアルタイム通信にはSupabaseを活用しています。
ポイント: リアルタイムWebアプリケーションの開発には、ユーザー間のインタラクションの即時性とデータの同期が重要です。この記事では、これらの要素を如何にして実現しているかに焦点を当てます。
使用したライブラリ
Deno
- dotenv
- Oak (Webミドルウェアフレームワーク)
- supabase-js
- dejs (テンプレートエンジン)
JavaScript
未使用
Deno Deployについて
Deno Deployは、Deno(TypeScript)で書かれたWEBアプリケーションをデプロイするためのサービスです。このサービスは、無料で1日最大10万リクエストまで利用可能で、HTTPSとカスタムドメインにも対応しています。
Deno Deployにはドメイン名の変更機能やカスタムドメインへの変更機能があり、SSL証明書の自動設定も無料で提供されています。便利機能としては、リアルタイムログの閲覧、サーバーレスタイプでの動作、リリースリビジョンの保持などがあります
https://qiita.com/AKB428/items/ebebed20dbebd2b9ab78
WebSocket通信の詳細
websocket通知されるアクション
- キャラクター選択 (キャラクターの選択は対戦ゲーム雰囲気の演出のみ)
- ゲームの開始 (2名揃ったらサーバーからPush)
- ケーム中 駒をおく(自分の駒を送信、相手の駒を受信)
- ゲーム終了
クライアント側の処理
ウェブソケットの接続は、JavaScriptを使用して WebSocket オブジェクトを作成することで開始されます。
接続が開始されると、クライアントはサーバーにメッセージを送信できます。
クライアントはサーバーからのメッセージをリアルタイムで受信し、適切に処理します。
サーバー側の処理
サーバーは、DenoのWebSocket APIを利用してクライアントからの接続を受け入れます。
クライアントからのデータを受信した際には、適切な応答や処理を行い、必要に応じてクライアントにデータを送信します。
ゲームの状態更新やプレイヤー間のアクションをリアルタイムで処理し、反映させます。
通信の流れ
クライアントが行動(例: 石を置く、キャラクターを選択する)を行う。
行動データはWebSocketを介してサーバーに送信されます。
サーバーはこのデータを処理し、ゲームの状態を更新。
更新されたゲーム状態は、同じ部屋にいる他のクライアントにブロードキャストされます。
Deno&Deno Deployは標準でWebSocketに対応している
https://deno.com/blog/deploy-streams
https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_JavaScript_Deno
websocket クライアント側
const hostname = window.location.hostname;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${hostname}/ws?roomId=${roomInfo.room_id}`;
console.log(wsUrl);
const socket = new WebSocket(wsUrl);
socket.onopen = function (e) {
console.log("[open] Connection established");
};
socket.onmessage = function (event) {
const data = JSON.parse(event.data);
console.log(data);
websocketサーバー側(deno oakを使用)
async function handleWebSocket(ctx: Context) {
const sock = await ctx.upgrade();
sockets.add(sock);
const roomId = ctx.request.url.searchParams.get('roomId') || 'defaultRoom';
if (!socketsOfRoom[roomId]) {
socketsOfRoom[roomId] = [];
}
socketsOfRoom[roomId].push(sock);
//console.log(socketsOfRoom);
sock.onopen = () => {
console.log("WebSocket opened");
//sock.send(JSON.stringify({ role: role }));
}
sock.onclose = () => {
ルームの概念とWebSocketの制御
3者のブラウザは同一ルーム[akb73]にいる
四目オンラインには複数のルーム(=ゲームボード)が存在する
同一ルームの情報しかブロードキャストしないようにする制御が必要
(解決方法)websocket受信時にsocketをハッシュマップで持つ(Deno)
async function handleWebSocket(ctx: Context) {
const sock = await ctx.upgrade();
sockets.add(sock);
const roomId = ctx.request.url.searchParams.get('roomId') || 'defaultRoom';
if (!socketsOfRoom[roomId]) {
socketsOfRoom[roomId] = [];
}
socketsOfRoom[roomId].push(sock);
//console.log(socketsOfRoom);
sock.onopen = () => {
console.log("WebSocket opened");
//sock.send(JSON.stringify({ role: role }));
}
特定のルームへブロードキャスト(Deno)
function broadcastToRoom(roomId: string, message: string) {
const socketsInRoom = socketsOfRoom[roomId];
if (socketsInRoom) {
for (const socket of socketsInRoom) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
}
}
}
}
同一の部屋のクライアントがDeno Deployのリージョンをまたがる問題
Player1が先手、Playser2が後手、Player3が試合閲覧者の場合に
全員が同一ルームにいてもPlayer1~3がどこのリージョンのどのマシンにいるかがわからないので各々のサーバーがもっているWebsocketを同期できない。
Deno Deployではリージョン固定を制御できない
Supabase側でボードの駒の動き等を集約してポーリングする
Supabaseのリアルタイム機能ではテーブルの更新をキャッチしてサーバ側に通知してくれることができる。そのためDenoDeployのどのリージョンやどのマシンインスタンスの更新でもSupabase側でキャッチできるのでSupabaseに接続しているDenoDeployサーバ群は共通の更新情報をうけとることができる!🥰🥰🥰🥰
Supabaseのテーブル設定でリアルタイム機能をONにする
supabaseのリアルタイム監視機能を使う(deno実装)
// 特定のルームIDのgame_statesテーブルの変更を購読
//https://supabase.com/docs/guides/realtime/postgres-changes
supabase
.channel('game_states_channel')
.on("postgres_changes",
{
event: "INSERT",
schema: "public",
table: `game_states`, // DBのテーブル名
// filter: `room_id=eq.${roomId}`
},
(payload) => {
//console.log('データが変更されました:', payload);
const roomId = payload.new.room_id;
const player_id = parseInt(payload.new.player_id);
const placeStone = payload.new.action.placeStone;
const playerColor = PlayerColor[player_id];
const message = JSON.stringify({ action: WsSendAction.PlaceStone, playerId: player_id, playerColor: playerColor, placeStoneXY: placeStone });
console.log(`broadcastToRoom ${roomId} ${message}`);
broadcastToRoom(roomId, message);
})
.subscribe();
- eventに*をつけるとすべてのイベントをキャッチできます
- filterを使うと特定の条件の行のみ絞れます
- テーブルの更新を一括で受信するのでサーバー側がどれだけ負荷に耐えられるかは不明、場合によってはメッセージキューシステムを別に挟む必要があるかも
テストプレイ
サンドボックスとしてルームakb21-50を開放
- スマートフォン表示でやってください
- デフォルトキャラクター以外を選択しないとバグります
- 先手と後手がブッキングしないように2名でやるときはP1かP2をきめておいてください
- なんかうまく通信できなかったらリロードでなんとかなります
- 1名で2役やるときはブラウザはわけてください(cookie制御があるため)
https://yonmoku.deno.dev/room/akb21
https://yonmoku.deno.dev/room/akb50
その他
- ローカル開発ではMacのセキュリティの制限なのかport80で起動しないとwebsocketがうまく通信できなかった(開発環境のデフォは8080など)
- ChatGPT4を使いながらコーディングしたがDenoの環境周り、supabaseのリアルタイム周りの情報は古すぎて役に立たなかった。
終わりに
Deno Deployでルームに対応したオンラインボードゲームの作成方法を紹介しました。
とりあえずこの方法でなんとかなると思います。
Deno DeployもSupabaseも一定量は無料なので、Deno Deployに興味ある人は2つを組み合わせてオンライン将棋やオンライン麻雀を作ってみてはどうでしょうか。