markdown
はじめに (対象読者・この記事でわかること)
この記事は、Webフロントエンドに興味がある初心者から中級者の方を対象にしています。
HTML と CSS の基本が分かっていれば、JavaScript だけで動くスロットマシンを一から実装できるようになります。
本稿を読むことで、Canvas API の使い方、乱数を利用したリールの回転ロジック、アニメーションの実装手順をマスターし、実際にブラウザ上で動く簡易スロットゲームを完成させられるようになります。
今回のネタは、プログラミング学習のモチベーション向上や、ポートフォリオに掲載できるミニゲームとしても活用できる点がポイントです。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- HTML/CSS の基本(要素の配置やスタイル指定ができること)
- JavaScript の基本文法(変数宣言、関数、イベントリスナー)
- ブラウザの開発者ツールの利用経験(コンソールやデバッガの操作)
スロットマシンの概要と背景
スロットマシンは、複数のリールが回転し、停止したときに同一シンボルが揃うと配当が得られるシンプルなゲームです。
Web では Canvas を使うことで、画像の描画やアニメーションを軽量に実装できます。
本項では、以下のポイントに注目して解説します。
- リール構造
- 各リールはシンボル画像(例: 🍒、🔔、7️⃣)を縦に並べた配列で管理し、回転時はインデックスをシフトさせて描画します。 - 乱数と停止判定
-Math.random()で回転回数を決め、一定回数シフトした後に止めることで「ランダム性」を表現します。 - アニメーション
-requestAnimationFrameによるフレーム制御で滑らかな回転を実現し、ユーザーが「Spin」ボタンを押した瞬間に開始します。 - 配当ロジック
- 最終的に表示されたシンボルの組み合わせを判定し、当たりかハズレかを判定します。
この構成をベースに、HTML(ボタンと表示領域)と JavaScript(ロジックと描画)だけで完結するミニゲームが完成します。
具体的な手順や実装方法
以下の手順に沿って、スロットマシンを実装していきます。各ステップごとにサンプルコードとポイントを示します。
ステップ1 HTML と Canvas の土台を作る
まずはページ上に Canvas と操作ボタンを配置します。
Html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>JavaScript スロットマシン</title> <style> canvas { background:#222; display:block; margin:auto; } #spinBtn { margin:20px auto; display:block; padding:10px 20px; font-size:1.2rem; } </style> </head> <body> <canvas id="slotCanvas" width="300" height="150"></canvas> <button id="spinBtn">Spin</button> <script src="slot.js"></script> </body> </html>
ポイント
- Canvas のサイズは 3 リール × 3 シンボル = 300×150px に固定。
- ボタンは #spinBtn に ID を付け、後述の JavaScript からイベントリスナーで接続します。
ステップ2 シンボル画像とリールデータの用意
slot.js で画像を事前に読み込み、各リールのシンボル配列を作ります。
Js// slot.js const canvas = document.getElementById('slotCanvas'); const ctx = canvas.getContext('2d'); const reelCount = 3; const symbols = ['🍒', '🔔', '7️⃣', '💎', '🍋']; // Unicode 文字だけでも可 const reelHeight = 3; // 表示されるシンボル数 // シンボルを画像化したい場合は Image オブジェクトを使うが、今回は文字描画で簡易化 function drawReel(reelIndex, offset) { const symbolSize = 50; // 1 シンボルあたりの高さ for (let i = 0; i < reelHeight + 1; i++) { // +1 はスクロール中に見える余剰分 const symbolIdx = (offset + i) % symbols.length; ctx.fillStyle = '#fff'; ctx.font = '40px sans-serif'; ctx.textAlign = 'center'; ctx.fillText( symbols[symbolIdx], reelIndex * symbolSize + symbolSize / 2, i * symbolSize + offsetY(offset) // 後述の offsetY で微調整 ); } } // 0~symbolSize の範囲で微妙にずらすヘルパー function offsetY(offset) { return (Math.random() * 10) - 5; // ちょっと揺らすだけ }
ポイント
- 実装をシンプルに保つため、画像の代わりに Unicode 絵文字を fillText で描画しています。
- drawReel はリールごとに現在のインデックス offset を受け取り、対応するシンボルを描画します。
ステップ3 スピンロジックとアニメーション
ボタンがクリックされたら、各リールを独立に回転させます。回転は一定フレーム数後に止め、最終的にインデックスを記録して結果を判定します。
Jslet spinning = false; const spinBtn = document.getElementById('spinBtn'); spinBtn.addEventListener('click', () => { if (spinning) return; // 連打防止 startSpin(); }); function startSpin() { spinning = true; const reels = [ { offset: 0, target: 0, speed: 0 }, { offset: 0, target: 0, speed: 0 }, { offset: 0, target: 0, speed: 0 } ]; // 各リールにランダムな停止位置とスピードを設定 reels.forEach(r => { r.target = Math.floor(Math.random() * symbols.length); r.speed = 0.5 + Math.random() * 1.0; // 0.5~1.5 行/フレーム }); function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); let allStopped = true; reels.forEach((r, i) => { if (r.offset < r.target + symbols.length * 2) { // 2 回転分余裕を持たせる r.offset += r.speed; allStopped = false; } // 描画は整数部だけ使用 drawReel(i, Math.floor(r.offset) % symbols.length); }); if (!allStopped) { requestAnimationFrame(animate); } else { spinning = false; const result = reels.map(r => symbols[Math.floor(r.offset) % symbols.length]); showResult(result); } } requestAnimationFrame(animate); } // 結果表示 function showResult(arr) { alert(`結果: ${arr.join(' ')}`); }
ポイント
- reels 配列に offset(現在位置)と target(停止位置)を持たせ、animate でフレーム毎に更新。
- requestAnimationFrame を使用することで、ブラウザの描画リズムに合わせたスムーズなアニメーションが実現できます。
- 停止判定は target + 2 回転 を超えた時点で行うので、自然な加速感が出ます。
ステップ4 配当判定ロジックの実装
結果配列 arr が揃ったかどうかを簡易判定します。今回は 3 シンボルすべて同じ で当たりとします。
Jsfunction showResult(arr) { const allSame = arr.every(sym => sym === arr[0]); if (allSame) { alert(`🎉 当たり! ${arr[0]} が揃いました!`); } else { alert(`ハズレ… ${arr.join(' ')}`); } }
ポイント
- Array.prototype.every を使うと、全要素が同一かどうかの判定が一行で書けます。
- 実際のゲームでは、シンボルごとの配当テーブルを作り、部分一致でも配当を出すように拡張可能です。
ハマった点やエラー解決
| 発生した問題 | 原因 | 解決策 |
|---|---|---|
drawReel が毎フレーム文字がずれて表示された |
offsetY が毎回乱数で呼ばれたため、文字が揺れ続けた |
offsetY の乱数生成は 1 回だけ 行い、drawReel の外で保持するように修正 |
| ボタン連打でスロットが途中で止まった | spinning フラグのチェックが不十分だった |
if (spinning) return; をクリックハンドラの最初に追加し、二重起動を防止 |
Math.random() の結果で停止位置がずれた |
target に対して余分に 2 回転分足していたが、インデックス計算がずれた |
r.offset < r.target + symbols.length * 2 の比較を整数にキャストし、Math.floor で揃えるように統一 |
解決策の詳細
-
乱数のスコープ管理
offsetYをリールごとの固定オフセットに変更し、drawReel内で毎回乱数を生成しないようにしました。これにより、文字が安定して描画されます。 -
フラグ管理
spinningフラグはグローバル変数として保持し、スピン開始前に必ずチェックすることで、途中でキャンセルされるケースを防ぎました。 -
インデックス計算の統一
フレームごとのoffsetは浮動小数点になるため、描画時は必ずMath.floorで整数化し、配列アクセス時のオフバイワンエラーを防止しました。
まとめ
本記事では、HTML と Canvas を活用したシンプルなスロットマシンの作り方 をステップバイステップで解説しました。
- リール構造と乱数による回転制御 を実装し、
requestAnimationFrameで滑らかなアニメーションを実現した - 停止位置と配当判定 をシンプルに実装し、結果をアラートで表示できるようにした
- 実装中にハマりやすいポイント(乱数管理、フラグ制御、インデックス計算)とその 解決策 を具体例で示した
この記事を通して、読者は Canvas API とフレーム制御の基本、および ゲームロジックの設計手法 を身につけ、ポートフォリオや学習プロジェクトにすぐ応用できるようになります。次回は、スコア管理やアニメーションの演出強化、サウンドエフェクト追加といった 発展的な機能 を加える方法について記事にする予定です。
参考資料
- MDN Web Docs – CanvasRenderingContext2D
- MDN Web Docs – requestAnimationFrame
- JavaScript 標準ライブラリ – Math.random()
- GameDev.js – Simple Slot Machine Example (GitHub)