1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スバラシティ風パズルゲームをReactで作った話🎮 — バイク38車種が合体するブラウザゲーム

1
Posted at

スバラシティ風パズルゲームをReactで作った話🎮 — バイク38車種が合体するブラウザゲーム

はじめに

バイクポータルサイト「MotoHub」を個人開発しています。

MotoHubにはすでに3つのブラウザゲーム(わらしべ長者・クイズ・2048パズル)がありますが、GWに4つ目としてスバラシティ風のブロック合体パズルを作りました。

5×5のグリッドで同じ色のバイクをタップして合体させ、レベルアップさせていくゲームです。

遊べます → バイクガレージパズル

image.png


環境

さくらVPS 4GB
├── Docker Compose
│   ├── Nginx
│   ├── PHP-FPM(Laravel 12 / PHP 8.3)
│   ├── MySQL 8.0
│   ├── Redis
│   └── Meilisearch
├── Cloudflare(Free プラン)
└── フロントエンド
    ├── React(JSX単一ファイル)
    ├── Vite
    └── Tailwind CSS

ゲーム設計

本家スバラシティとの違い

スバラシティは「同じ色のブロックをタップして合体、レベルアップして大きな建物に変化」というシンプルなルール。これをバイクに置き換えました。

要素 本家 バイク版
テーマ 街づくり バイクガレージ
ブロック 色付き建物 メーカー別バイク
3色→4色 ホンダ(赤)・ヤマハ(青)・カワサキ(緑)・スズキ(黄)
レベルアップ 建物が大きくなる 上位バイクに変化
スコア 人口 ガレージ総額(万円)
特殊能力 市長マーク メカニックポイント

メーカー別レベル設計

各メーカー9段階+海外バイク3段階で、合計38車種が登場します。

const BIKE_NAMES = {
  0: ['スーパーカブ50','モンキー125','CT125','PCX','レブル250','GB350','CB400SF','CBR600RR','CB1300SF'],
  1: ['JOG','シグナスX','セロー250','YZF-R3','MT-07','XSR700','MT-09','YZF-R1','VMAX'],
  2: ['KLX230','Z250','Ninja250','Ninja400','Z650','Z900RS','ZX-6R','ZX-10R','H2'],
  3: ['アドレス','ジクサー150','GSX250R','SV650','V-Strom','GSX-S750','GSX-S1000','ハヤブサ','カタナ'],
};
const WORLD_BIKES = { 10: 'BMW R1250GS', 11: 'Panigale V4', 12: 'ゴールドウイング' };

30ターン目からスズキ(黄色)が追加。「スズキ参戦!」の演出も入れました。

image.png

image.png


合体アニメーションの実装

一番苦労したのがアニメーションです。「タップして瞬間に変わる」だとゲーム感がないので、1秒のアニメーションシーケンスを組みました。

アニメーションの流れ

0ms    → スライド開始(隣接ブロックがタップ位置に向かって移動)
300ms  → フラッシュ(合体位置が光る)
500ms  → グリッド更新 + ポップ(新ブロック出現)+ 重力落下 + 新ブロック補充
~1000ms → 完了、入力受付再開

L字移動(斜め禁止)

最初は直線で移動させていましたが、斜めに動くと不自然。グリッドに沿ったL字移動にしました。

@keyframes sc-slide-to {
  0%   { transform: translate(0, 0); }
  50%  { transform: translate(var(--sc-dx), 0); }      /* 横移動 */
  100% { transform: translate(var(--sc-dx), var(--sc-dy)); } /* 縦移動 */
}

横→縦の2段階で動くので、同じ列なら縦のみ、同じ行なら横のみの移動になります。

image.png

落下アニメーション

合体後の空きマスに上からブロックが落ちてくるアニメーション。カスケード(段差をつけた順番)で落下させると気持ちいい。

// 重力落下:既存ブロックが空きマスに滑り落ちる
// stagger: 30ms/block
const gravityDelay = blockIndex * 30;

// 新規補充:グリッド上端の外側から落ちてくる
// stagger: 40ms/block、重力完了後に開始
const fillDelay = totalGravityDuration + newBlockIndex * 40;

二重再生バグ

アニメーションが2回再生されるバグに悩みました。原因はReactのstate更新でアニメーション用のCSSクラスが消え、ブロックが元の位置に戻ってから再度アニメーションが走ること。

解決策は、フラッシュフェーズ中もスライド用のCSS custom propertiesを維持し、グリッドの実データ更新と同時にクラスを除去すること。

// Phase 2(フラッシュ)で slideConnected を消していた → バグの原因
// 修正:Phase 3(グリッド更新)まで slideConnected を維持

画像アセット — Gemini で38枚生成

バイク画像はGemini(Google)で1枚ずつ生成しました。

共通プロンプト

サイドビュー(横向き、右向き)
デフォルメされたイラスト調(アニメ風、太い輪郭線)
黒背景(透過ではなく黒単色)
バイクの下に薄い水色の影(楕円形の床反射)
右下に小さなキラキラ(✦)マーク
1024×1024px

背景透過処理

ゲーム内では色付きブロックの上に画像を載せるので、背景を透過にする必要がありました。Pythonで一括処理。

from PIL import Image

def remove_background(img_path):
    img = Image.open(img_path).convert('RGBA')
    # 四隅のピクセルから背景色を推定
    corners = [img.getpixel((0,0)), img.getpixel((img.width-1,0)),
               img.getpixel((0,img.height-1)), img.getpixel((img.width-1,img.height-1))]
    bg_color = max(set(corners), key=corners.count)
    # flood fill で背景を透過
    # ...

黒背景・白背景・水色背景が混在していましたが、四隅から背景色を自動検出して対応。

image.png
image.png
image.png
image.png

.gitignore の罠

画像をgitに入れようとしたら入らない。原因は .gitignore*.png があったこと。

git check-ignore -v backend/public/images/subaracity/honda_1.png
# .gitignore:40:*.png

# 対処:例外を追加
echo '!backend/public/images/subaracity/*.png' >> .gitignore
git add -f backend/public/images/subaracity/

MotoHubとの連携

ゲームとしてだけでなく、MotoHubのバイク検索サイトへの導線としても機能させています。

  • ゲームオーバー画面に「登場バイクの実際の相場を見る」リンク
  • Xシェア時にMotoHubのURLが含まれる
  • TOPページにゲームセクションを追加(4ゲームのカード表示)
  • ナビゲーションバーの「その他」にゲームリンク

フリーゲーム投稿サイト(夢現)にも登録し、外部からの流入経路を作っています。

image.png

image.png

image.png


技術的なポイントまとめ

項目 詳細
フレームワーク React(JSX単一ファイル、約400行)
ビルド Vite(Laravelプロジェクト内)
アニメーション CSS @keyframes + React state制御
同色判定 BFS(幅優先探索)で隣接ブロック検出
音声 Web Audio API(BGM + SE、ミュート切替)
データ永続化 localStorage(ハイスコア・最高レベル)
画像 Gemini生成 + Python背景透過(38枚)

学んだこと

1. アニメーションは「感触」を決める

瞬間で切り替わるのと、1秒かけてアニメーションするのでは、ゲームの気持ちよさが全然違う。L字移動、カスケード落下、ポップエフェクト。パズルゲームはアニメーションが命。

2. 画像生成AIは「1枚ずつ、同じチャットで」

Geminiで19枚生成しましたが、まとめて依頼すると処理が重くてエラーになる。1枚ずつ、参照画像を添付した同じチャットで生成するとスタイルが安定する。

3. .gitignoreは最初に確認

*.png が除外されていることに気づかず、デプロイしても画像が表示されないバグで30分ロスした。新しいファイル種別を追加するときは .gitignore を最初にチェック。


前回の記事:Claude APIでバイクニュースを全自動生成 → X投稿まで自動化した話

🏍 MotoHub: https://motohub.jp
🎮 バイクガレージパズル: https://motohub.jp/games/subaracity
X: https://x.com/motohub_jp
GitHub: https://github.com/ausssxi/MotoHub

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?