JavaScript:パズルゲームのゲームオーバーからタイトルに戻る処理

JavaScript

ブロックが最上段まで積みあがるとゲームオーバーとしている。
ゲームオーバー時は、5秒間「Game Over!」と表示してからタイトル画面に戻る。

Dateオブジェクトを2回生成して経過時間を得る

ゲームオーバー後は、あるていど余韻を残してからタイトル画面に戻りたい

現在時刻を得るためのnew Date()を用いることで、ゲームオーバー時からの経過時間をチェックすることができる。

サンプルコードでは、ゲームオーバー時に変数timer.startに現在時刻を保存している。

timer.start = new Date();

あとはフレーム処理(サンプルでは関数GameMain)ごとにtimer.endに現在時刻を取得させ、以下のような式でゲームオーバー後の経過秒数をもとめている。

// 経過秒数をカウント
timer.end = new Date();
const passedSeconds = (timer.end - timer.start) / 1000;	// 経過秒数

差分を1000で割っているのは、時刻がミリ秒(1000分の1秒)単位で計算されるためである。
1000で割ることで秒数に直している。

求めた経過秒数passedSecondsを使えば、

// 5秒経過したらタイトル画面へ
if(passedSeconds >= 5.0){
	// 経過後の処理を記述
}

のように経過時間による判定ができるようになる。

ソースコード

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(11ゲーム画面からタイトル画面へ:ゲームオーバー処理)</title>
	<style>
		*{
			margin: 0;
			padding: 0;
		}
	</style>
	<script src="main.js" type="text/javascript"></script>
</head>
<body>
	<canvas id="canvas" width="512" height="640"></canvas>
</body>
</html>

main.js

// 11ゲーム画面からタイトル画面へ:ゲームオーバー処理
let isDebug = false;			// デバッグモードON/OFF
let canvas = null;
let g = null;
let pos = {x:0, y:0};		// キャンバス上のマウス座標
let dropSpeed = 3;			// 落下速度(10-->1秒, 1-->0.1秒)
let timer = {start: 0, end: 0};	// ブロック落下・画面遷移の経過時間用
const TITLE_TEXT = "otimono";		// ゲームタイトル
const GAMEOVER_TEXT = "Game Over!";	// ゲームオーバー時の文字列

const BLOCK_SIZE = 64;								// 1ブロックのサイズ(=パズルキャラのサイズ)64x64ピクセル
const ROW_SIZE = 8;									// 行数
const COLUMN_SIZE = 8;								// 列数
const BLOCK_LENGTH = ROW_SIZE * COLUMN_SIZE;		// ステージ全体のブロック長(行数×列数)

// ゲームの状態(game.stateで使用: Symbolの引数文字列はshowInfo()の状態表示に利用)
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");	// ブロックを消去
const GAME_OVER = Symbol("game_over");			// ゲームオーバー

// ステージ背景色(市松模様)
const BACKGROUND_COLOR1 = "rosybrown";	// "pink";
const BACKGROUND_COLOR2 = "azure";		// "skyblue";

// ゲーム全体の設定や情報
let game = {
	state: TITLE,		// ゲーム中の状態
	hiscore: 0,			// 未使用
	offsetY: 128,		// キャンバス左上からのステージ座標オフセット値(Y座標)
};

let currentBlockType = 0;	// 配置するブロックタイプ(1~6)
let nextBlockType = 0;	// 次に配置するブロックタイプ(1~6)

// ゲームに使う画像ファイル
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(BLOCK_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;
};

/*
 * マウスボタンを押した時の処理
 */
const mouseDownListener = () => {
	// ブロックが配置できる条件
	//		1)ステージ上にマウスポインタがある かつ
	//		2)全てのブロックが落下して消去ブロックが無い場合
	if(pos.y >= game.offsetY && game.state == WAITING){
		game.state = SET_BLOCK;
	}
	else if(game.state == TITLE){	// タイトル画面の時はゲーム開始
		console.log("Game Start!");
		gameInit();			// 初期化
	}
};

/*
 * 背景を描画
 */
const drawBackground = ()=> {
	let x = 0, y = 0;	// ステージ上のブロック座標

	// 市松模様のステージ色
	let color1 = BACKGROUND_COLOR1;
	let color2 = BACKGROUND_COLOR2;

	// 市松模様を描画
	for(i=0; i<BLOCK_LENGTH; i++){
		// 1段下がると先頭の色を入れ替える
		if(i % COLUMN_SIZE == 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 * BLOCK_SIZE, y * BLOCK_SIZE + game.offsetY, BLOCK_SIZE, BLOCK_SIZE);
	}
}

/*
 * カーソルを描画
 */
const drawCursor = ()=> {
	// ステージ外ではなにもしない
	if(pos.y < game.offsetY) return;

	// マウス座標からブロック位置を求める
	const x = Math.floor(pos.x / BLOCK_SIZE);
	const y = Math.floor(pos.y / BLOCK_SIZE);

	g.fillStyle = "rgba(128, 128, 128, 0.5)";	// グレー色で透過
	g.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}

/*
 * ステージを表示
 */
const drawStage = ()=> {
	// キャンバスクリア
	//g.clearRect(0, 0, canvas.width, canvas.height);
	g.fillStyle = "snow";
	g.fillRect(0, 0, canvas.width, canvas.height);

	// 背景描画
	drawBackground();
	// カーソル描画
	drawCursor();
	// 次のブロックを表示
	showNextBlock();

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

		// ブロックを表示
		g.drawImage(images[stageState[i]], x*BLOCK_SIZE, y*BLOCK_SIZE + game.offsetY);

		// ゲーム状態を表す配列内容を表示(デバッグ用)
		if(isDebug){
			g.fillStyle = "#fffa";
			g.fillText(stageState[i], x*BLOCK_SIZE+20, y*BLOCK_SIZE+20+game.offsetY);
		}
	}
}

/*
 * 画像ファイル読み込み
 */
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)=> {
	stageState = [];
	stageState = [...stage];	// ステージ情報をコピー(stageStage <-- stage)
	console.log(stageState);
}

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

	if(passedTime <= 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++;	// 落とすブロックの数をカウント
			}
		}
	}
	timer.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];
	return deleteBlockCount;
}

/*
 * ブロックを消去する
 */
const deleteBlock = () =>{
	for(let i=0; i<BLOCK_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));	// 0~6
		stageState[i] = blockType;
	}
}

/*
 * 次のブロックをランダムで決定する
 */
const setNextBlock = () =>{
	// 次のブロックタイプをランダムで設定
	nextBlockType = Math.floor(Math.random() * (images.length-2)) + 1;	// 1~6
	console.log("NEXT: " + image_files[nextBlockType]);
}

/*
 * 次のブロックを表示する
 */
const showNextBlock = () =>{
	// NEXTの文字
	g.fillStyle = "rosybrown";
	g.fillText("NEXT", 324, 50);

	// 次のブロック画像を表示
	g.drawImage(images[nextBlockType], 424, 35, 50, 50);
}

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

/*
 * タイトル画面を表示
 */
const drawTitle = () =>{
	g.fillStyle = "rosybrown";
	g.fillRect(0, 0, canvas.width, canvas.height);

	g.fillStyle = "snow";
	const textObj = g.measureText(TITLE_TEXT);	// キャンバス文字列の情報を得る

	// タイトル文字列を中央にくるように配置
	g.fillText(TITLE_TEXT, canvas.width/2-textObj.width/2, canvas.height/2-15);
}

/*
 * ゲームオーバーを表示
 */
const drawGameover = () =>{
	// ステージ全体を薄く塗る
	g.fillStyle = "rgba(128, 128, 128, 0.7)";
	g.fillRect(0, 0, canvas.width, canvas.height);

	// ゲームオーバーの文字を描画
	g.fillStyle = "snow";
	const textObj = g.measureText(GAMEOVER_TEXT);	// キャンバス文字列の情報を得る

	// タイトル文字列を中央にくるように配置
	g.fillText(GAMEOVER_TEXT, canvas.width/2-textObj.width/2, canvas.height/2-15);
}

/*
 * ゲームオーバーかどうかチェックする
 */
const isGameOver = () =>{
	// 最上段にブロックがあればゲームオーバー
	for(let i=0; i<COLUMN_SIZE; i++){
		if(stageState[i] != 0){
			return true;
		}
	}
	return false;
}

/*
 * ゲームの初期化
 */
const gameInit = () =>{
	// ステージ情報を生成する
	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);

	// 最上段にランダムにブロックを配置
	setRandomBlock();

	// 最初のブロックを決定
	setNextBlock();

	// 落下モードに移行
	game.state = DROP;

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

/*
 * ゲームのメイン処理(少しだけゲームっぽくなってきている)
 */
const GameMain = () =>{

	// ゲーム状態を表示(デバッグ用)
	if(isDebug){
		showGameState();
	}

	// ゲーム状態(game.state)による分岐処理
	if(game.state == DROP){
		// ステージ表示
		drawStage();
		// ブロックを落とす
		if(dropBlock() == 0){	// 落とすブロックが無くなったら
			game.state = CHECK_BLOCK;
		}
	}
	else if(game.state == CHECK_BLOCK){
		// ステージ表示
		drawStage();
		// 消せるブロックをチェックする
		if(checkBlock() != 0){
			game.state = DELETE_BLOCK;
		}
		else{
			game.state = WAITING;	// 消すブロックがないなら入力待ち
		}
	}
	else if(game.state == DELETE_BLOCK){
		// ステージ表示
		drawStage();
		// ブロックを消去
		setTimeout( function(){
			deleteBlock();
			game.state = DROP;
		}, 200);	// すぐに消去せず、どれが消去ブロックが分かるように200ミリ秒の間×画像を表示している
	}
	else if(game.state == SET_BLOCK){
		// ステージ表示
		drawStage();
		// ステージ最上段にランダムにブロックを配置
		setRandomBlock();
		// ブロックタイプを更新
		currentBlockType = nextBlockType;
		// 次のブロックタイプを設定
		setNextBlock();
		// ステージのクリック位置に画像ブロックを設定
		const blockPos = {
			x: Math.floor(pos.x / BLOCK_SIZE),					//ブロックx座標
			y: Math.floor((pos.y-game.offsetY) / BLOCK_SIZE)	// ブロックのy座標
		};

		stageState[blockPos.x + blockPos.y * COLUMN_SIZE] = currentBlockType;
		console.log(blockPos);

		// ブロックを落とすモードに移行
		game.state = DROP;
	}
	else if(game.state == WAITING){
		// ブロック配置待ち

		// ステージ表示
		drawStage();
		// ゲームオーバーチェック
		if(isGameOver()){
			game.state = GAME_OVER;
			console.log(GAMEOVER_TEXT);
			drawGameover();		// ゲームオーバー文字列を描画
			timer.start = new Date();	// ゲームオーバー後の経過時間計測のためタイマースタート
		}
	}
	else if(game.state == TITLE){
		// タイトル画面へ
		drawTitle();
	}
	else if(game.state == GAME_OVER){
		// 経過秒数をカウント
		timer.end = new Date();
		const passedSeconds = (timer.end - timer.start) / 1000;	// 経過秒

		// 5秒経過したらタイトル画面へ
		if(passedSeconds >= 5.0){
			game.state = TITLE;
		}
	}
	// フレーム再描画
	requestAnimationFrame(GameMain);
}

/*
 * 起動時の処理
 */
window.addEventListener("load", async ()=>{
	// キャンバス取得
	canvas = document.getElementById("canvas");
	g = canvas.getContext("2d");
	g.font = "normal 30px Impact";	// フォントサイズ・フォント種類 設定
	g.textBaseline = "top";			// テキスト描画時のベースライン

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

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

	// タイトル画面からスタート
	game.state = TITLE;

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

関連記事

コメント

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