モグラ叩き

20140204a

モグラは畑の農作物を食い荒らす害獣だと思っていたが、動食性(ミミズや昆虫の幼虫)であり、実際に食害しているのはモグラのトンネルを利用したネズミとのこと。

モグラは、主にミミズ(土龍)を食うので「土龍追い」などと呼ばれた。モグラを「土竜」と書くのは実は誤りらしい。確かに竜というほどスマートじゃない。

そんなこんなで、日経ソフトウェア2014年3月号「HTML5でゲームを作ろう(第4回)」に掲載されたモグラ叩きゲームを制作してみました。

今回もプログラムソースには手を加えていません。

右の画像をクリックすると実際のゲームを実行します。

「STRAT」ボタンを押して、40秒間に、穴から顔を出したモグラをなるべく多く叩くだけです。

モグラを叩けば+5点ですが、子供を叩くとスコアが減点(−15点)されます。

PCではマウスでクリック、タブレットやスマートフォンでは指でタッチすることでモグラを叩きます。

叩くと「ピッ」と音が出ますのでご注意下さい。

20140204b

モグラと子供の絵は、Inkscapeで描きました。

絵は日経コンピュータ掲載のものを見ながら描きましたが、多少デフォルメした部分もあります。

20140204e

穴の状態で、「空」「モグラが顔を出している」「モグラが叩かれた」「子供が顔を出している」「子供が叩かれた」の5種類の絵が必要です。


紙面の記述通り、Rhino+JavaScript+Java APIで、端末でコマンドを実行し、5種類の画像を合成しました。

20140204d

背景部分は当初、透明にしたのですが、残像が重なりまともな表示にならなかったので、最終的には各画像とも同じ背景を入れました。


ネット上のフリー素材から落とした back.png と同じ背景画です。



今回は、画像制作も、ソースの打ち込みも、全て Ubuntu Desktop 13.10 上で制作しました。

Ubuntu Desktop で、いろいろなソフトを試してみたり、挑戦中です。

20140204f_icon
20140204f

テキストエディタは、jEditを使ってみたところ、テキスト入力時のタブ(というか階層構造)の制御がいい感じで、好印象でした。

20140204g_icon
20140204g



「ピッ」と言う効果音はネット上(フリー素材)から入手しました。

フリーソフトのテジタルオーディオエディタ「Audacity」を使って、前後の余白を削除してみました。

SafariやIE用にmp3形式に保存することも、FireFoxやアンドロイド系タブレット・スマートフォン用にogg形式に保存することも簡単でした。

オーディオ形式の変換だけなら、わざわざソフトを入れなくても、変換サービスを提供しているサイト上でも可能ですが・・・。



ソースはこちら


AddType audio/ogg .ogg
AddType audio/mp3 .mp3
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=320,user-scalable=no">
		<style>
			* {
				padding:0;
				margin:0;
				text-align:center;
			}
			body {
				background-image:url('back.png');
			}
			button {
				width:200px;
				padding:10px;
				margin-top:20px;
			}
			h1 {
				font-size:20px;
				padding:8px;
				border-bottom:1px solid gray;
			}
			#sBtn {
				display:none;
			}
		</style>
		<script type="text/javascript" src="mogura.js"></script>
		<title>Mogura</title>
	</head>
	<body>
		<h1 id="info">Mogura</h1>
		<canvas id="aCanvas" width="320" height="320"></canvas>
		<p id="sBtn">
			<button onclick="start()">START</button>
		</p>
	</body>
</html>
// プログラム全体で使われる変数
var ctx;				// キャンバスに対する描画用コンテキスト
var resImg, backImg;	// 素材・背景画像
var piAudio;			// 効果音
var holes = [];			// 穴の状態管理
var score = 0;			// スコア
var time = 0;			// 残り時間
// 定数(事実上、定数として扱う変数)
var CHAR_W = 80;			// リソース内の画像1枚の幅
var TURN_INTERVAL	= 500;	// 1ターンの間隔(ミリ秒)
var HOLE_NONE		= 0;	// モグラ穴の状態
var HOLE_MOGU		= 1;
var HOLE_MOGU_HIT	= 2;
var HOLE_MAN		= 3;
var HOLE_MAN_HIT	= 4;
// 各種初期化
window.onload = function () {
	// 画像や効果音リソースを読み込む
	resImg = new Image();
	resImg.src = "resource.png";
	resImg.onload = imageLoadHandler;
	backImg = new Image();
	backImg.src = "back.png";
	backImg.onload = imageLoadHandler;
	piAudio = new Audio();
	if (piAudio.canPlayType) {
		if (piAudio.canPlayType("audio/ogg")) {
			piAudio.src = "pi.ogg";
		}
		if (piAudio.canPlayType("audio/mpeg")) {
			piAudio.src = "pi.mp3";
		}
	}
	// キャンバスの設定
	var c = $("aCanvas");
	ctx = c.getContext("2d");	// コンテキスト
	// onmousedownイベントのイベントハンドラを設定
	c.onmousedown = mouseHandler;
	// ontouchstartイベントのイベントハンドラを設定
	c.ontouchstart = touchHandler;
};
function imageLoadHandler(e) {
	e.target.isOK = true;
	if (resImg.isOK && backImg.isOK) ready();
}
function ready() {
	score = 0;	// スコアの初期化
	time  = 40;	// 残り時間の設定
	holes = [];	// 穴の初期化
	for (var i=0; i<16; i++) {
		holes[i] = HOLE_NONE;
	}
	draw();
	$show("sBtn");	// スタートボタンを表示
}
function start() {
	$hide("sBtn");
	setTimeout(nextTurn, TURN_INTERVAL);
}
function nextTurn() {
	time--;
	if (time < 0) { gameOver(); return; }
	for (var i=0; i<16; i++) {
		// 空の穴
		if (holes[i] == HOLE_NONE) {
			if (rnd(20) != 0) continue;
			holes[i] = (rnd(15) > 3) ? HOLE_MOGU : HOLE_MAN;
			continue;
		}
		// 空でなければ継続するか確認
		if (rnd(2) == 0) continue;
		holes[i] = HOLE_NONE;
	}
	draw();
	setTimeout(nextTurn, TURN_INTERVAL);
}
function gameOver() {
	alert("GAME OVER!\nSCORE="+score);
	ready();
}
function draw() {
	ctx.drawImage(backImg, 0, 0);	// 背景を描く
	// 16個の穴を左上から順番に描く
	for (var i=0; i<16; i++) {
		var x = (i % 4) * CHAR_W;
		var y = Math.floor(i / 4) * CHAR_W;
		var c = holes[i];
		// 読み込んだ画像から部分コピーで描画
		ctx.drawImage(resImg, c*CHAR_W, 0, CHAR_W, CHAR_W, x, y, CHAR_W, CHAR_W);
	}
	$("info").innerHTML = "Mogura - " + "Score:" + score + " - " + "Time:" + time;
}
function mouseHandler(e) {
	// キャンバス上の位置に補正
	var x = e.clientX;
	var y = e.clientY;
	var r = e.target.getBoundingClientRect();
	x -= r.left;
	y -= r.top;
	hit(x, y);
}
function touchHandler(e) {
	e.preventDefault();
	// キャンバス上の位置に補正
	var x = e.touches[0].clientX;
	var y = e.touches[0].clientY;
	var r = e.target.getBoundingClientRect();
	x -= r.left;
	y -= r.top;
	hit(x, y);
}
function hit(x, y) {
	if (time < 0) return;
	// キャンバス上の座標から穴の番号を求める
	var col = Math.floor(x / CHAR_W);
	var row = Math.floor(y / CHAR_W);
	var i = row * 4 + col;
	// 空の穴?
	if (holes[i] == HOLE_NONE) return;
	// モグラ?
	if (holes[i] == HOLE_MOGU) {
		score += 5;
		// 叩かれたモグラの絵を設定して
		holes[i] = HOLE_MOGU_HIT;
		draw();	// 描画
		piAudio.play();	// 効果音を鳴らす
		return;
	}
	// 子供?
	if (holes[i] == HOLE_MAN) {
		score -= 15;
		if (score < 0) score = 0;
		// 叩かれた子供の絵を設定して
		holes[i] = HOLE_MAN_HIT;
		draw();	// 描画
		piAudio.play();	// 効果音を鳴らす
		return;
	}
}
function $(id) { return document.getElementById(id); }
function $show(id) { $(id).style.display = "block"; }
function $hide(id) { $(id).style.display = "none"; }
function rnd(n) {
	return Math.floor(Math.random() * n);
}