JavaScript:落ちものゲームの消去予定ブロックをsetTimeoutで200ミリ秒別画像で表示してから消す

JavaScript

落ちものゲームで消去予定のブロックは、すぐに消してしまうとどれが消えたか分からない。
どのブロックが消えたのか分かるようにするために、消す前の画像として×マークのような画像を一旦表示しておき、200ミリ秒後に消すような処理は、setTimeoutで実現できる。

状態遷移のイメージ

game.stateは、ゲーム中の状態を表している。
200ミリ秒待ってから、game.stateを変更することで一定時間消去予定のブロックが表示される仕組み。

	:
if(game.state == DROP){
	// ブロックを落とす
	if(dropBlock() == 0){	// 落とすブロックが無くなったら
		game.state = CHECK_BLOCK;
	}
}	
else if(game.state == CHECK_BLOCK){
	// 消せるブロックをチェックする
	if(checkBlock() != 0){
		game.state = DELETE_BLOCK;
	}
	else{
		game.state = WAITING;	// 消すブロックがないなら入力待ち
	}
}
else if(game.state == DELETE_BLOCK){
	// ブロックを消去
	setTimeout( function(){
		deleteBlock();
		game.state = DROP;
	}, 200);	// すぐに消去せず、どれが消去ブロックが分かるように200ミリ秒の間×画像を表示している
}
	:
	:
ソースコード

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>OTIMONO(08ブロックを配置すると上からランダムに落ちてくる)</title>
	<style>
		*{
			margin: 0;
			padding: 0;
		}
	</style>
	<script src="main.js" type="text/javascript"></script>
</head>
<body>
	<canvas id="canvas" width="512" height="512"></canvas>
</body>
</html>

main.js

// 08ブロックを配置すると上からランダムに落ちてくる
let canvas = null;
let g = null;
let pos = {x:0, y:0};		// キャンバス上のマウス座標
let cpos = {x:0, y:0};		// マウス座標から得られるセル位置
// let isMouseDown = false;	// マウスを押しているか?
let dropSpeed = 3;			// 落下速度(10-->1秒, 1-->0.1秒)
let gameTime = {start: 0, end: 0};	// 落下速度計測用

const CELL_SIZE = 64;							// パズルキャラのサイズ(64x64ピクセル)
const ROW_SIZE = COLUMN_SIZE = 512 / CELL_SIZE;	// 行と列に何個並べるか?
const CELL_LENGTH = ROW_SIZE * COLUMN_SIZE;		// パズルキャラの数

// ゲームの状態(game.stateで使用: Symbolの引数文字列は状態表示に利用)
const TITLE = Symbol('title');		// タイトル画面(未使用)
const WAITING = Symbol('waiting');	// ブロック配置待ち
const SET_BLOCK = Symbol('set_block');	// ブロック配置
const DROP = Symbol('drop');		// 落下中
const CHECK_BLOCK = Symbol('check_block');	// 消せるブロックをチェック
const DELETE_BLOCK = Symbol('delete_block');	// ブロックを消去

let game = {
	state: WAITING,
	hiscore: 0,	// 未使用
};

// ゲームに使う画像ファイル
const image_files = ["space.png", "alien.png", "apple.png", "dog.png", "obake.png", "ultra.png", "usi.png", "delete.png"];

// 画像オブジェクト(画像ファイルをゲームで利用するため)
let images = [];

// ステージ情報配列
let stageState = new Array(CELL_LENGTH);

/*
 * マウスを動かしている時の処理
 */
const mouseMoveListener = (e) => {
	// オフセット位置:キャンバスがブラウザの左上からどれくらいの位置にあるか?(rect.left, rect.top)
	const rect = e.target.getBoundingClientRect();

	// e.clientXとe.clientYはブラウザ左上からのキャンバスクリック位置なのでオフセット分を引くとキャンバス上の座標が分かる
	pos.x = e.clientX - rect.left;
	pos.y = e.clientY - rect.top;

	// マウス座標からセル位置を求める
	cpos.x = Math.floor(pos.x / CELL_SIZE);
	cpos.y = Math.floor(pos.y / CELL_SIZE);
};

/*
 * マウスボタンを押した時の処理
 */
const mouseDownListener = () => {
	// ブロックが配置できる条件は、全てのブロックが落下して消去ブロックが無い場合
	if(game.state == WAITING) game.state = SET_BLOCK;
};

/*
 * マウスボタンを離した時の処理(未使用)
 */
/*
const mouseUpListener = () => {
	isMouseDown = false;
};
*/

/*
 * 背景を描画
 */
const drawBackground = ()=> {
	let x = 0, y = 0;	// 描画するセル位置
	let color1 = "pink", color2 = "skyblue";	// 市松模様のステージ色

	for(i=0; i<CELL_LENGTH; i++){
		// 1段下がると先頭の色を入れ替える
		if(i % 8 == 0) [color1, color2] = [color2, color1];
		// 格子状にブロックを表示
		x = i % COLUMN_SIZE;			// x方向セル位置
		y = Math.floor(i / ROW_SIZE);	// Y方向セル位置

		// 色を交互に設定
		bgColor = (i % 2 == 0) ? color1: color2;
		g.fillStyle = bgColor;
		g.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
	}
}

/*
 * カーソルを描画
 */
const drawCursor = ()=> {
	g.fillStyle = "rgba(128, 128, 128, 0.5)";	// グレー色で透過
	g.fillRect(cpos.x * CELL_SIZE, cpos.y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
}

/*
 * ステージを表示
 */
const drawStage = ()=> {
	// キャンバスクリア
	g.clearRect(0, 0, canvas.width, canvas.height);
	// 背景描画
	drawBackground();
	// カーソル描画
	drawCursor();

	// ステージ配列に基づき、ブロック画像を表示
	for(let i=0; i<CELL_LENGTH; i++){
		x = i % COLUMN_SIZE;			// x方向セル位置
		y = Math.floor(i / ROW_SIZE);	// Y方向セル位置

		// ブロックを表示
		g.drawImage(images[stageState[i]], x*CELL_SIZE, y*CELL_SIZE);
		// ステージの状態を表す配列内容を表示(確認用)
		g.fillStyle = "#fffa";
		g.textBaseline = "top";
		g.fillText(stageState[i], x*CELL_SIZE+20, y*CELL_SIZE+20);
	}
}

/*
 * 画像ファイル読み込み
 */
const loadImages = async ()=> {
	const images = [];
	const promises = [];

	image_files.forEach((url) =>{
		const promise = new Promise((resolve)=>{
			const img = new Image();
			img.addEventListener("load", ()=>{
				console.log("loaded " + url);
				resolve();	// 読み込み完了通知
			});
			img.src = "./asset/" + url;	// 画像ファイル読み込み開始
			images.push(img);	// 画像オブジェクト配列に格納
		});
		promises.push(promise);
	});
	await Promise.all(promises);
	return images;
}

/*
 * ステージ情報を生成
 */
const createStage = (stage)=> {
	console.log(stage);
	stageState = [];
	stageState = [...stage];	// ステージ情報をコピー(stageStage <-- stage)
	console.log(stageState);
}

/*
 * ブロックを落とす
 */
const dropBlock = ()=> {
	// 落下タイミングチェック
	gameTime.end = new Date().getTime();
	const passed = (gameTime.end - gameTime.start) / 100;	// 10分の1秒に直す

	if(passed <= dropSpeed){
		return;	// 落下しない
	}

	let dropCount = 0;	// 落としたブロックの数
	for(let y=ROW_SIZE-2; y>=0; y--){	// ステージの下方向からチェック
		for(let x=0; x<COLUMN_SIZE; x++){
			const upIndex = y*ROW_SIZE+x;
			const downIndex = (y+1)*ROW_SIZE+x;
			if(stageState[upIndex] != 0 && stageState[downIndex] == 0){
				// 入れ替え
				[ stageState[upIndex], stageState[downIndex] ]= [ stageState[downIndex], stageState[upIndex] ];
				dropCount++;	// 落とすブロックの数をカウント
			}
		}
	}
	gameTime.start = new Date().getTime();	// 落下タイマー再スタート

	return dropCount;	// 落としたブロック数を返す
}

/*
 * ブロックが消去できるかチェックする
 */
const checkBlock = () =>{
	// ステージの状態をコピー
	const check = [...stageState];
	// 消すブロックの数
	let deleteBlockCount = 0;
	// 横方向チェック
	for(let y=0; y<ROW_SIZE; y++){
		for(let x=1; x<COLUMN_SIZE-1; x++){
			const currentIndex = y*ROW_SIZE + x;	// 中央
			const rightIndex = y*ROW_SIZE + (x + 1);// 右側
			const leftIndex = y*ROW_SIZE + (x - 1);	// 左側
			if(stageState[currentIndex] > 0){
				if((stageState[currentIndex] === stageState[rightIndex] && stageState[currentIndex] === stageState[leftIndex])){
					check[currentIndex] = check[rightIndex] = check[leftIndex] = 7;	// 7はこれから消す予定の×マーク画像
					deleteBlockCount++;
				}
			}
		}
	}
	// 縦方向チェック
	for(let y=1; y<ROW_SIZE-1; y++){
		for(let x=0; x<COLUMN_SIZE; x++){
			const currentIndex = y*ROW_SIZE + x;	// 中央
			const upIndex = (y-1)*ROW_SIZE + x;	// 上側
			const downIndex = (y+1)*ROW_SIZE + x;	// 下側
			if(stageState[currentIndex] > 0){
				if((stageState[currentIndex] === stageState[upIndex] && stageState[currentIndex] === stageState[downIndex])){
					check[currentIndex] = check[upIndex] = check[downIndex] = 7;	// 7はこれから消す予定の×マーク画像
					deleteBlockCount++;
				}
			}
		}
	}

	// チェック後の状態をステージ情報に戻す
	stageState = [...check];
	//game.state = DELETE_BLOCK;
	return deleteBlockCount;
}

/*
 * ブロックを消去する
 */
const deleteBlock = () =>{
	for(let i=0; i<CELL_LENGTH; i++){
		if(stageState[i] == 7){	// ×画像だったら
			stageState[i] = 0;	// 何もない画像にする
		}
	}
}

/*
 * 最上段にランダムにブロックを配置する
 */
const setRandomBlock = () =>{
	for(let i=0; i<COLUMN_SIZE; i++){
		const blockType = Math.floor(Math.random() * (images.length-1));
		stageState[i] = blockType;
	}
}

/*
 * ゲーム状態を表示(デバッグ用)
 */
const showInfo = () =>{
	g.fillStyle = "yellow";
	g.fillText(game.state.description, 220, 220);	// Symbolのdescriptionを表示する
}

/*
 * ゲームのメイン処理(少しだけゲームっぽくなってきている)
 */
const GameMain = () =>{
	// ステージ表示
	drawStage();
	// ゲーム状態を表示(デバッグ用)
	showInfo();

	if(game.state == DROP){
		// ブロックを落とす
		if(dropBlock() == 0){	// 落とすブロックが無くなったら
			game.state = CHECK_BLOCK;
		}
	}
	else if(game.state == CHECK_BLOCK){
		// 消せるブロックをチェックする
		if(checkBlock() != 0){
			game.state = DELETE_BLOCK;
		}
		else{
			game.state = WAITING;	// 消すブロックがないなら入力待ち
		}
	}
	else if(game.state == DELETE_BLOCK){
		// ブロックを消去
		setTimeout( function(){
			deleteBlock();
			game.state = DROP;
		}, 200);	// すぐに消去せず、どれが消去ブロックが分かるように200ミリ秒の間×画像を表示している
	}
	else if(game.state == SET_BLOCK){
		// ステージ最上段にランダムにブロックを配置
		setRandomBlock();
		// ステージのクリック位置に画像ブロックを設定
		//const index = Math.floor(Math.random() * (images.length-1)) + 1;	// ランダム
		stageState[cpos.x + cpos.y * COLUMN_SIZE] = 1;	// とりあえず1番目のブロック(インベーダー)をセット
		console.log(cpos);

		// ブロックを落とす
		game.state = DROP;
	}
	else if(game.state == WAITING){
		// 次に落下するブロックを表示
		//showNextBlock();
	}
	// フレーム再描画
	requestAnimationFrame(GameMain);
}

/*
 * 起動時の処理
 */
window.addEventListener("load", async ()=>{
	canvas = document.getElementById("canvas");
	g = canvas.getContext("2d");
	g.font = "bold 24px System";	// フォントサイズ・フォント種類 設定

	// マウスイベント設定
	canvas.addEventListener("mousemove", mouseMoveListener, false);
	canvas.addEventListener("mousedown", mouseDownListener, false);
	//canvas.addEventListener("mouseup", mouseUpListener, false);

	// 画像ファイルを読み込む
	images = await loadImages(image_files);
	console.log(images);

	// ステージ情報を生成する
	const stage1 = [
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0
	];
	createStage(stage1);

	// 落下タイマースタート
	gameTime.start = new Date().getTime();

	// 最上段にランダムにブロックを配置
	setRandomBlock();
	game.state = DROP;	// 落下モード

	// ゲーム開始
	GameMain();
});

関連記事

コメント

タイトルとURLをコピーしました