ライツアウトは5×5などで構成されたマス(ライト)を全部消すゲームです。
スマホ以降のカジュアルゲームかと思いきやウィキペディアによると意外と古いらしいです。
非常にシンプルなのでプログラミング初心者向けの題材としてよく使われてる気がします。
今回はそのライツアウトを素のJavaScriptだけで作ります。
サンプルはCodePenに公開しています。CodePen上で編集できるのでご自由に改造して遊んでください。
See the Pen
ライツアウトのゲーム作ったよ by いんわん (@inwan78)
on CodePen.
プログラム
プログラムはこんな感じになっています。
const CORE = { setup: function(w, h){ this.engine = new Engine(w, h); } } class Engine { constructor(w, h){ const canvas = document.getElementById("canvas"); canvas.width = w; canvas.height = h; canvas.style.backgroundColor = "#000000"; CORE.canvas = canvas; CORE.ctx = canvas.getContext("2d"); window.addEventListener('resize', () => {this.resizeCanvas();}); this.resizeCanvas(); } _requestFrame(){ this.update(); requestAnimationFrame(()=>{this._requestFrame();}); } resizeCanvas(){ const canvas = CORE.canvas; const canvasHeightRatio = canvas.height / canvas.width; const windowHeightRatio = window.innerHeight / window.innerWidth; let width; let height; if (windowHeightRatio > canvasHeightRatio) { width = window.innerWidth; height = window.innerWidth * (canvas.height / canvas.width); } else { width = window.innerHeight * (canvas.width / canvas.height); height = window.innerHeight; } canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; CORE.resolutionRatio = height / canvas.height; } preload(data){ const length = Object.keys(data).length; let count = 0; const assets = []; for(let key in data) { assets[key] = new Image(); assets[key].src = data[key]; assets[key].onload = ()=>{ if(++count == length){ this.onload(); } } } CORE.assets = assets; } clearCanvas(){//画面をクリア(前の画面描画を削除) CORE.ctx.clearRect(0, 0, CORE.canvas.width, CORE.canvas.height); } start(){ this.init(); requestAnimationFrame(()=>{this._requestFrame();}); } onload(){} init(){} update(){} } function rectFill(x, y, w, h, color){ const ctx = CORE.ctx; ctx.fillStyle = color; ctx.fillRect(x, y, w, h); } /*********************** ここからライツアウトのプログラム ************************/ const CANVAS_HEIGHT = 550; const CANVAS_WIDTH = 550; const table = [ [0, 0, 0, 0, 0], [0, 1, 0, 1, 0], [0, 0, 0, 0, 0], [0, 1, 0, 1, 0], [0, 0, 0, 0, 0] ]; const tileSize = 100; const margin = 10; const tiles = []; window.onload = function(){ CORE.setup(CANVAS_WIDTH, CANVAS_HEIGHT); for(let i = 0, len = table.length, id = 0; i < len; i++){ tiles[i] = []; for(let j = 0, len2 = table[i].length; j < len2; j++){ const tile = new Tile(); tile.x = j * tileSize + j * margin + margin * 0.5; tile.y = i * tileSize + i * margin + margin * 0.5; tile.row = i; tile.col = j; if(table[i][j]){ tile.color = "#cc0000"; tile.draw(); }else{ tile.color = "#000088" tile.draw(); } tiles[i][j] = tile; } } CORE.canvas.addEventListener("pointerdown", (e) => { checkTiles(e); if(isClear()){ const ctx = CORE.ctx; ctx.font = '96px serif'; ctx.fillStyle = 'rgba(255, 255, 0, 1)' ctx.fillText("CLEAR", 110, 300); } }); } function isClear(){ for(let i = 0, len = tiles.length; i < len; i++){ for(let j = 0, len2 = tiles[i].length; j < len2; j++){ if(tiles[i][j].color == "#cc0000")return false; } } return true; } function checkTiles(e){ const x = e.offsetX / CORE.resolutionRatio; const y = e.offsetY / CORE.resolutionRatio; for(let i = 0, len = tiles.length; i < len; i++){ for(let j = 0, len2 = tiles[i].length; j < len2; j++){ const tile = tiles[i][j]; if(x >= tile.x && x <= tile.x + tileSize && y >= tile.y && y <= tile.y + tileSize){ const row = tile.row; const col = tile.col; if(tile.color == "#cc0000"){ tile.color = "#000088"; tile.draw(); relateTileChek(row, col) }else{ tile.color = "#cc0000"; tile.draw(); relateTileChek(row, col) } } } } } function relateTileChek(row, col, flag){ const pos = [ /* 斜め [-1, -1], [1, -1], [-1, 1], [1, 1] */ [0, -1], [-1, 0], [1, 0], [0, 1] ]; for(let i = 0; i < pos.length; i++){ const r = row + pos[i][1]; const c = col + pos[i][0]; if( r < 0 || r >= tiles.length || c < 0 || c >= tiles[0].length)continue; const tile = tiles[r][c]; if(tile.color == "#cc0000"){ tile.color = "#000088" tile.draw(); }else{ tile.color = "#cc0000"; tile.draw(); } } } class Tile { constructor() { this.x = 0; this.y = 0; this.row = 0; this.col = 0; this.color = "#cc0000"; } draw(){ rectFill(this.x, this.y, tileSize, tileSize, this.color); } }
ゲームエンジン的な部分のCoreオブジェクトとEngineクラスについてはこっちの記事を参照してください。
ライツアウトのプログラムは75行目あたりからになっています。
そのあたりから解説します。
データは配列で管理
ライツアウトは5×5のマスを使うのでこのデータを配列を使って管理します。
table配列はゲーム開始時のライトの状態が入っています。0が消灯で1が点灯です。
ここの0、1を入れ替えるとゲーム開始時のライトのつき方が変わります。
このサンプルでは問題が1つですが3次元配列にして複数ステージ扱えるようにもできます。
マスの作成
tilesという配列にTileというクラスを作って入れています。
Tileクラス本体はプログラムの一番最後にあります。
Tileクラスは座標とマス内の位置を示すrow、colというプロパティを持っています。
draw()はマスを描画するメソッドで点灯なら赤、消灯なら青色に描画します。
色の指定は#000088みたいな感じで指定します。これはcssの色の指定と同じです。他の色を指定したい場合はこちらのサイトが便利です ▷ カラーコード一覧表
タッチ処理
タッチ処理は
CORE.canvas.addEventListener("pointerdown", (e) => { });
の部分で行っています。
引数の「e」にタッチした座標などが入っているんですが、どのタイルをタッチしたのかはタイルの座標と比較しないとわかりません。
なのでcheckTiles(e)で調べています。
checkTiles()の最初の部分
const x = e.offsetX / CORE.resolutionRatio; const y = e.offsetY / CORE.resolutionRatio;
これはタッチした座標とゲーム内の座標が違うのを直しています。
表示されている画面はウィンドウのサイズに合うように変更されています。そのためe.offsetX(e.offsetY)の値とはズレています。そのためゲーム内の座標と合うようにリサイズの時に計算している解像度の比率で割っています。
タッチした位置のタイルは色を反転し対応するtable配列のデータも上書きします。
relateTileChek()
ライツアウトは押したマスとその上下左右のマスが反転します。その処理をrelateTileChek()でしています。
posはtable配列上の位置(一つ上や一つ右など)となるような数字を入れています(コメントアウトしてるのは斜めの位置になるバージョン)。
これをfor文内でチェックします。
if( r < 0 || r >= tiles.length || c < 0 || c >= tiles[0].length)continue;
この部分でおかしな位置になる場合を取り除いています。
クリアのチェック
isClear()はクリアしたかどうかのチェックです。
全部のマスが消灯したらクリアなのでtable配列のデータが全部0ならクリアです。
一つでも1があればクリアではありません。
おしまい
かなりさっくりですがライツアウトの作り方でした。
プログラムは自由に改造してもらって構わないのであちこちいじって遊んでください。
問題増やしてステージ制にしてみたり色々してみてください。
ただ、今回は画像も使わずループ処理も無かったのでゲームエンジンがリサイズくらいしか仕事していないのがちょっと残念でした(;^ω^)