個人開発で対戦テトリスを作っている話(設計と現状)

この記事について

今、個人開発で ブラウザ上で遊べる対戦テトリス を作っています。

まだ完成品というよりは、遊べる形にしながら設計を固めている途中です。
ただ、単なるミニゲームではなく、ルームに入って相手と対戦し、ライン消去で攻撃を送り合うところまでは動くようになってきました。

この記事では、現在の構成、リアルタイム通信の考え方、おじゃまブロックの処理、作っていて難しかった部分、今後直したい部分をまとめます。

作っているもの

作っているのは、Webブラウザで動く対戦型のテトリスです。

基本の流れは次のようなものです。

  1. プレイヤーが同じルーム名を入力する
  2. 2人が同じルームに入る
  3. サーバーがマッチ開始イベントを送る
  4. それぞれのブラウザでゲームが開始する
  5. 盤面情報や攻撃情報を Socket.io で送り合う
  6. 片方がゲームオーバーになったら勝敗を表示する

最初から完璧なオンラインゲームを作ろうとすると進まなくなるので、まずは「1対1で対戦できること」を最優先にしています。

使用している技術

現在の構成はかなりシンプルです。

フロントエンド側では、HTML、CSS、JavaScript を使っています。
ゲーム画面の描画には Canvas を使い、ブロック、盤面、NEXT、HOLD、相手のミニ盤面を描画しています。

バックエンド側では、Node.js、Express、Socket.io を使っています。
Express はサイト全体の配信、Socket.io は対戦中のリアルタイム通信を担当します。

大きく分けると、役割はこうです。

ブラウザ
  - 入力処理
  - Canvas描画
  - 自分のゲーム進行
  - 相手盤面の表示

Node.js / Express
  - HTMLや記事ページの配信
  - Socket.ioサーバーの起動

Socket.io
  - ルーム参加
  - 対戦開始通知
  - 盤面同期
  - 攻撃送信
  - ゲームオーバー通知

通信設計

今回の対戦機能では、サーバーがゲームのすべてを管理するのではなく、各プレイヤーのブラウザが自分のゲームを進める形にしています。

サーバーは主に中継役です。

プレイヤーAのブラウザ
  ↓ joinRoom / myBoard / attack / gameOver
Socket.ioサーバー
  ↓ opponentBoard / garbage / opponentGameOver
プレイヤーBのブラウザ

この方式のメリットは、実装が軽く、個人開発でも進めやすいことです。
一方で、厳密な同期やチート対策は弱くなります。

本格的な対戦ゲームにするなら、サーバー側で盤面や入力を検証する設計も必要になります。
ただ、今の段階では「個人サイト上で遊べる対戦ゲーム」として、まず形にすることを優先しています。

ルーム管理

対戦はルーム名を使って管理しています。

プレイヤーがルーム名を入力すると、ブラウザからサーバーへ joinRoom を送ります。
サーバー側では、そのルームに何人いるかを確認します。

現在のルールはシンプルです。

  • ルームが空なら、最初のプレイヤーとして待機
  • ルームに1人いるなら、2人目として参加して対戦開始
  • すでに2人いるなら、満員として参加させない

2人そろったタイミングで、サーバーから startMatch を送信します。
それを受け取った各ブラウザがゲームを開始します。

この設計にした理由は、URLやアカウントを使わなくても、合言葉のようなルーム名だけで簡単に対戦できるからです。

盤面同期の仕組み

対戦画面では、自分の盤面だけでなく、相手の盤面も小さく表示します。

ただし、相手のゲームをこちらで完全に再現しているわけではありません。
自分のブラウザが現在の盤面データを myBoard としてサーバーに送り、サーバーが相手に opponentBoard として転送します。

つまり、相手盤面は「相手が今送ってきた盤面を表示している」状態です。

自分の盤面が変わる
  ↓
myBoard を送信
  ↓
サーバーが同じルームの相手へ転送
  ↓
相手画面のミニ盤面を更新

この方式は単純ですが、対戦している感覚を出すには十分効果があります。
今後は送信頻度を調整したり、差分だけを送ったりすると、通信量を減らせそうです。

おじゃまブロックと攻撃

対戦テトリスらしさを出すために、ラインを消したときに相手へ攻撃を送る仕組みを入れています。

現在は、ライン消去の内容に応じて攻撃量を決めています。

例として、次のような考え方です。

  • 2ライン消しなら少し攻撃
  • 3ライン消しならもう少し攻撃
  • TETRISなら大きめの攻撃
  • T-SpinやBack-to-Backならボーナス
  • コンボが続いたら追加攻撃

攻撃が発生すると、ブラウザから attack イベントを送ります。
サーバーは同じルームの相手へ garbage として転送します。

受け取った側では、すぐに盤面へおじゃまブロックを入れるのではなく、いったん garbageQueue に溜めています。

これは、対戦テトリスでよくある「攻撃の予告」や「相殺」に近い考え方です。

攻撃の相殺

対戦の気持ちよさを出すうえで、攻撃の相殺はかなり重要です。

相手からおじゃまが来ている状態で自分もラインを消した場合、すぐ相手に攻撃を返すのではなく、まず自分に溜まっているおじゃまを減らします。

例えば、相手から4ライン分のおじゃまが来ていて、自分が3ライン分の攻撃を作った場合はこうなります。

受けているおじゃま: 4
自分の攻撃: 3

結果:
  おじゃまが1だけ残る
  相手への攻撃は送らない

逆に、相手から2ライン分のおじゃまが来ていて、自分が5ライン分の攻撃を作った場合はこうです。

受けているおじゃま: 2
自分の攻撃: 5

結果:
  おじゃまを2相殺
  残り3を相手へ送る

この処理が入るだけで、ただ一方的に攻撃が飛んでくるゲームではなくなります。
ピンチのときにラインを消して耐える、という対戦ゲームらしい駆け引きが生まれます。

ゲーム本体の設計

ゲーム本体は、かなり基本的なテトリスの構造で作っています。

主な状態は次のようなものです。

  • 盤面データ
  • 現在操作中のミノ
  • NEXTミノ
  • HOLDミノ
  • スコア
  • レベル
  • コンボ数
  • Back-to-Back状態
  • 受信済みのおじゃま量
  • ゲーム中かどうか
  • ゲームオーバーかどうか

毎フレーム、入力、落下、衝突判定、固定、ライン消去、描画を進めます。

特に大事なのは、盤面を「見た目」ではなく「データ」として持つことです。
Canvasに描いているブロックはあくまで表示で、実際の判定は配列で管理しています。

この分離をしておくと、相手に盤面を送るときも、AIに盤面を評価させるときも扱いやすくなります。

Canvasで描画しているもの

Canvasでは、次の要素を描画しています。

  • 自分のメイン盤面
  • 落下中のミノ
  • 固定済みブロック
  • NEXT
  • HOLD
  • 相手のミニ盤面

DOMでブロックを大量に並べる方法もありますが、テトリスのように頻繁に描き直すゲームでは Canvas のほうが扱いやすいです。

特に、ブロックの色、影、枠線、ミニ盤面などをまとめて描けるので、ゲーム画面としての一体感も出しやすくなります。

操作まわり

PCではキーボード操作を中心にしています。
左右移動、回転、ソフトドロップ、ハードドロップ、HOLDを割り当てています。

さらに、スマホでも最低限操作できるように、画面上のボタンも用意しています。

ただ、スマホ操作はまだ課題が多いです。
対戦テトリスは操作速度がかなり大事なので、ボタン配置、誤タップ、長押し、画面サイズへの対応はもっと詰める必要があります。

今できていること

現時点でできていることは次の通りです。

  • ブラウザ上でテトリスを遊べる
  • ルーム名で対戦相手を待てる
  • 2人そろうと対戦を開始できる
  • 自分の盤面を相手に送れる
  • 相手の盤面をミニ表示できる
  • ライン消去で攻撃を送れる
  • おじゃまブロックを受け取れる
  • 攻撃の相殺ができる
  • 片方がゲームオーバーになると勝敗を表示できる
  • 相手が切断したときに通知できる

まだ荒い部分は多いですが、「対戦として成立する最低ライン」は超えられたと思っています。

まだ弱いところ

一方で、まだ作り込みが足りない部分も多いです。

特に気になっているのは、同期の正確さです。

今の方式では、各ブラウザが自分のゲームを進めています。
そのため、通信が遅れたり、片方の処理が重くなったりすると、見えている状況と実際の状況にズレが出る可能性があります。

また、サーバー側で盤面の正しさを検証していないため、厳密な意味でのチート対策はできていません。
公開ミニゲームとしては許容できますが、ランキングやレート戦を入れるならこのままでは弱いです。

他にも課題があります。

  • 再戦機能がまだ弱い
  • 観戦機能がない
  • 切断後の復帰ができない
  • スマホ操作の完成度が低い
  • 攻撃量のバランス調整が必要
  • ルーム名が推測されやすい
  • 同時接続が増えた場合の負荷検証ができていない

完成に近づけるには、ゲームの面白さだけでなく、通信、UI、運用まで見ていく必要があります。

AI版との関係

このサイトでは、対戦テトリスだけでなく、テトリスAIの実験ページも作っています。

AI版では、盤面を評価して、どの場所にミノを置くかを判断する方向で試しています。
対戦版とAI版は別ページですが、内部的には近い考え方があります。

どちらも重要なのは、盤面をデータとして扱うことです。

盤面データがきれいに扱えれば、次のような発展ができます。

  • AIに現在盤面を評価させる
  • 最善手を表示する
  • CPU対戦を作る
  • リプレイを保存する
  • 対戦後にミスを分析する

最終的には、人間同士の対戦だけでなく、人間対AI、AI同士の比較、練習用のアシスト機能まで広げられると面白いと思っています。

作っていて学んだこと

テトリスは見た目だけなら単純に見えます。
でも実際に作ると、かなり多くの設計が必要です。

例えば、ただブロックを落とすだけでも、衝突判定、回転、固定タイミング、ライン消去、スコア、HOLD、NEXT、ゲームオーバー判定が必要です。

さらに対戦にすると、そこに通信、ルーム管理、相手盤面、おじゃま、相殺、切断処理が乗ってきます。

作ってみて一番感じたのは、ゲーム制作は「画面を作る」だけではなく、状態管理を作る作業だということです。

今どの状態なのか。
次に何が起きるのか。
相手には何を送るのか。
どのタイミングでゲームを止めるのか。

このあたりを整理しないと、少し機能を足しただけで壊れやすくなります。

今後やりたいこと

今後は、次の順番で改善していきたいです。

  1. 再戦機能をわかりやすくする
  2. ルーム参加画面を改善する
  3. 攻撃量のバランスを調整する
  4. スマホ操作を改善する
  5. 対戦終了後の結果表示を作り込む
  6. CPU対戦を試す
  7. AIの評価ロジックとつなげる
  8. リプレイやログ保存を検討する

特に、再戦とスマホ操作は優先度が高いです。
遊んでもらうページにするなら、「始めやすい」「もう一回やりやすい」ことが大事だからです。

まとめ

対戦テトリスは、個人開発の題材としてかなり面白いです。

Canvasでゲームを描画する練習にもなりますし、Socket.ioでリアルタイム通信を扱う練習にもなります。
さらに、AIや対戦バランスまで考え始めると、かなり奥が深いです。

今はまだ開発途中ですが、基本的な対戦、盤面同期、攻撃、おじゃま、相殺までは動くようになりました。

ここからは、ただ機能を増やすだけでなく、遊びやすさ、通信の安定性、対戦としての気持ちよさを少しずつ磨いていきます。