16日目:オブジェクト指向にする

16日目です。今回はこれまでのプログラムを大幅に変更します。これまでは中央集権型のプログラムでしたが、これをやめて自律型というか分散型に変更します。規模が大きくなると中央集権型では管理が大変になってきます。まあ、一般的な組織でも似たような感じだとは思いますが、規模が小さいうちは社長が全部取り仕切ることができるでしょう。でも、規模が大きく複雑になってくると社長一人では限度があります。そこで権限を委譲して部下にやらせるわけです。どこまで権限を委譲するかは難しい部分があります。
今回のゲームではクリックされる数字をオブジェクト化して、クリック時の処理などを自分自身で行うように変更します。これまではクリックされたら座標を割り出して、それぞれの数字を読み出し処理していました。今回は数字のオブジェクトを作り、そのオブジェクトがクリックされた時に自分で処理するようにします。そのために、数字オブジェクトを作成します。一般的にはクラス(Class)と呼ばれるひな形(JavaScriptの場合はfunctionを使用)を定義します。

実際のクラス部分は以下のようになります。これまではグローバル変数で管理していた値を数字オブジェクト自身が持つようにします。自分自身で値を持つようにするためにthisを使って定義します。thisを使うと自分自身に各種プロパティを定義し値を保持できるようになります。これでグローバル変数が減ります。するとどうなるかというと中央でコントロールする、把握しなければならない部分が減るので管理が楽になるわけです。これによりバグ(不具合)も減らすことができるという仕組みです。でも、まあ他の部分でバグが出ることがありますが。

function NumClass(obj){
this.id = obj.id; // IDを設定する
this.x = obj.x | 1; // X座標を設定する。指定がない場合は1にする。
this.y = obj.y | 0; // Y座標を設定する。指定がない場合は0にする。
this.num = obj.num | 0; // 数値を設定する。指定がない場合は0にする。
this.opacity = obj.opacity | 1.0; // 不透明度を設定する。指定がない場合は1.0にする。
this.width = 48; // 数字の画像の横幅
this.height = 48; // 数字の画像の縦幅
this.flag = true; // 消されたかどうかを示すフラグ。falseなら消された、trueなら消されていない

権限を委譲してもデータ(値)だけあっても仕方ありません。自分自身で処理できるようにメソッドを定義します。今回定義したのは「描画処理」「消去処理」「クリック時の処理」の3つのメソッドです。それぞれthis.drawNumber、this.eraseNumber、this.checkとして定義します。

描画処理は以下のようになります。前回と微妙に違うのはxとなっていた部分がthis.xのようにthis.がついている点です。これは数字オブジェクト自身のプロパティであるxを参照することになります。

// ●描画処理を行うメソッド
this.drawNumber = function(){
if (!this.flag){ return; } // 消されているので表示処理はしない
ctx.clearRect(this.x, this.y, this.width, this.height); // 指定された範囲を消す
var imgObj = document.getElementById("N"+this.num); // HTMLファイル内の画像を指定する
var saveOpacity = ctx.globalAlpha; // 現在の不透明度を変数に保存する
ctx.globalAlpha = this.opacity; // 不透明度を設定する
ctx.drawImage(imgObj, this.x, this.y); // 指定した座標に数字の画像を描画する
ctx.globalAlpha = saveOpacity; // 元の不透明度に戻す
}

消去処理も同様でxだった部分がthis.xになります。

// ●消去処理を行うメソッド
this.eraseNumber = function(){
if (!this.flag){ return; } // 消されているので消去処理はしない
ctx.clearRect(this.x, this.y, this.width, this.height); // 指定された範囲を消す
}

次にクリックの処理です。DOM要素なら自動的に座標などがブラウザでチェックされ対応する要素のイベントハンドラが呼び出されます。が、残念ながら今回はCanvasに描いているのと、真面目(?)に判定するのでif命令を使っています。Canvasだとパス内かどうかチェックするisPointInPath()メソッドがありますが、今回はパスなしで処理しているので、まあこんな具合になります。

// ●クリックのチェックを行うメソッド
this.check = function(x, y){
if (!this.flag){ return; } // 消されているのでチェック処理はしない
if ((x < this.x) || (y < this.y) || (x > (this.x+this.width)) || (y > (this.y+this.height))){
return; // 範囲外ないので何もしない
}
NumStack.push(this.id); // クリックされた自分自身のIDをスタックに積む
this.opacity = 0.5;
this.drawNumber();
checkNumber();
}
クリック時の処理で実際に数字判定を行う部分はcheckNumber()という関数を呼び出すようにしています。checkNumber()は、これまでも出てきたクリックした数字の合計が10かどうかを調べる関数です。
ここで今回からオリジナルのTEN -10-と同様に複数の数字をクリックして合計が10の場合もOKとします。つまり、2+3+5の3つの数字をクリックした場合、合計が10になるので、この3つの数字を消す処理が必要になります。これまではクリックした2つの数字の合計が10だった場合のみ処理していましたが、今回からは複数の値の合計が10でもOKというわけです。
複数の値の合計処理は面倒そうに思えますが、2つの値を加算して処理するよりも簡単です。方法としてはクリックされたらクリックされた数字オブジェクトをスタックに積みます。スタックというのは入れ場所の1つで、まあ物置みたいなものです。スタックは井戸のような感じで、どんどんデータを放り込んでいきます。最後に入れたデータは最初に取り出せます。最初に入れたデータは、これまでに放り込んだデータを全部取り出さないと、取り出すことができません。最初に入れたデータが最後に出て来るのでFILO (First In Last Out) になります。ちなみにスタックはデータが増えてきた場合にタフではないので、キューが使われる場合もあります。キューの場合はスタックと違って最初に入れたデータを最初に取り出すことができます。このためキューはFIFO (First In First Out) になります。
さて、クリックした数字オブジェクトをスタックに積んでいる部分は以下のコードです。

NumStack.push(this.id); // クリックされた自分自身のIDをスタックに積む

よく見ると数字オブジェクトそのものをスタックには積んでいません。this.idのidですが、これが数字オブジェクトのIDになります。このIDは自分自身が定義され格納されている配列変数の順番と同じになっています。つまり、NumObj[NumStack[this.id]]としてスタックに入っている数字オブジェクトを取り出すことができます。まあ、他にも方法はありますが、今回はこの方法にしました。

次に数字の合計を求める部分ですが、以下のようになります。forを使ってスタックに積まれた数字オブジェクトの数字を読み出して加算します。

for(var i=0; i<NumStack.length; i++){
total = total + NumObj[NumStack[i]].num; // スタックに積まれている数字オブジェクトの値を読み出し加算する
}

次に合計の判定部分です。合計が10未満の場合は何もせず関数から抜けます。

if (total < 10){ return; } // 合計が10未満なら何もしない

合計が10より大きい場合はNGなのでクリックした数字を元の不透明度に戻します。また、NGなのでスタックを空にしておきます。空にしないと二度と数字を消すことができなくなってしまいます。常に合計が10を超えてしまうためです。

if (total > 10){ // 合計が10より大きい場合はNGなので、これまでクリックした数字を元の不透明度に戻す
for(var i=0; i<NumStack.length; i++){
NumObj[NumStack[i]].opacity = 1.0;
NumObj[NumStack[i]].drawNumber();
}
NumStack = [ ]; // スタックを空にする
return;
}

最後に合計が10だった場合です。これは単純に数字を消して、消したことを示すフラグ(変数flag)を設定します。

for(var i=0; i<NumStack.length; i++){
NumObj[NumStack[i]].eraseNumber();
NumObj[NumStack[i]].flag = false;
}

今回の修正で数字が画面を動き回ってもクリックするといった処理もできるようになります。
ただ、今回の改良によりステージがクリアできない、という致命的な問題も抱えたことになります。オリジナルのTEN -10-では、これを解消するために数字の残りが少なくなると合計が10になるための数字を新たに生成するようになっています。
次回はステージクリア可能にするための処理を追加したいと思います。

[サンプルを実行する]

[サンプルをダウンロード]

HTMLファイルの内容

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TEN</title>
<style>
img { width:1px; height: 1px; }
#stage { width : 200px; height : 320px; background-image: url(images/bg.png); }
#clearMsg { position : absolute; left: 10px; top: 200px; font-size: 18pt; color: red; visibility: hidden; }
</style>
</head>
<body>
<h1>TEN</h1>
<div>SCORE : <span id="score">0</span> TIME : <span id="timeString">0</span></div>
<canvas id="stage" width="200" height="320"></canvas>
<div id="clearMsg">ステージクリア!</div>
<img src="images/1.png" id="N1"><img src="images/2.png" id="N2"><img src="images/3.png" id="N3">
<img src="images/4.png" id="N4"><img src="images/5.png" id="N5"><img src="images/6.png" id="N6">
<img src="images/7.png" id="N7"><img src="images/8.png" id="N8"><img src="images/9.png" id="N9">
<script src="main.js"></script>
</body>
</html>

JavaScriptファイル (main.js) の内容

// TEN HTML5 version
// (c) 2014 Cybermuse
// Arranger 古籏一浩
var score = 0;			// スコア(得点)を入れる変数
var count = 0;		// 数字オブジェクトの合計を入れる変数
var time = 0;	// ゲームオーバーまでの時間
var timerID = null;	// カウントダウンタイマーのIDを入れる変数
var firstClickFlag = false;	// 最初にクリックしたかどうかのフラグ
var gameFlag = true;	// ゲーム中かどうかのフラグ。trueならゲーム中。
var num = [ ];	// 数字を入れる配列変数を用意
var numXCount = 4;	// 横の数字の個数
var numYCount = 6;	// 縦の数字の個数
var NumObj = [ ];	// ●数値オブジェクトを入れるための配列変数
var NumStack = [ ];	// ●数字オブジェクトを入れるための配列。スタックとして使用する。
var canvas = document.getElementById("stage");	// canvas要素を取得する
var ctx = canvas.getContext("2d");	// 2dコンテキストを取得する
// 初期化処理
function init(){
	canvas.onclick = addNumber;	// canvas要素にクリックイベントを割り当てる
	count = setRandomValue(numXCount, numYCount);	// 戻り値は要素の総数
	time = 30;	// 30秒に設定する
	document.getElementById("timeString").innerHTML = time + "秒";	// 時間を表示する
}
// ランダムに値を設定する関数
function setRandomValue(w, h){
	num = [ ];	// 配列内容を消去する
	var len = w * h;	// 数字の総数
	// ランダムな値を設定する
	for(var i=0; i<len; i+=2){	// 数字の数だけ繰り返す
		var n = Math.floor(Math.random() * 9) + 1;	// 1〜9までの数値をランダムに生成する
		num.push(n);
		num.push(10 - n);
	}
	// 配列要素をシャッフルする
	for(var i=0; i<len; i++){
		var p1 = Math.floor(Math.random() * len);	// 要素の数の範囲で乱数を発生させる
		var p2 = Math.floor(Math.random() * len);	// 要素の数の範囲で乱数を発生させる
		var n = num[p1];	// 2つの要素の内容を入れ替える
		num[p1] = num[p2];
		num[p2] = n;
	}
	// 表示処理
	document.getElementById("clearMsg").style.visibility = "hidden";	// ステージクリアメッセージを消す
	NumObj = [ ];	// スタックを空にする
	for(var y=0; y<h; y++){	// 数字の数だけ繰り返す
		for(var x=0; x<w; x++){
			NumObj.push(new NumClass({	// ●オブジェクトを生成する
				num : num[x+y*w],
				x : x * 48 + 4,
				y : y * 48 + 10,
				id : x+y*w	// ●IDを配列変数の参照番号に対応させる
			}));
		}
	}
	return len;	// 数字の総数を返す
}
// ●数字オブジェクトのひな形(クラス)を定義する
function NumClass(obj){
	this.id = obj.id;	// IDを設定する
	this.x = obj.x | 1;	// X座標を設定する。指定がない場合は1にする。
	this.y = obj.y | 0;	// Y座標を設定する。指定がない場合は0にする。
	this.num = obj.num | 0;	// 数値を設定する。指定がない場合は0にする。
	this.opacity = obj.opacity | 1.0;	// 不透明度を設定する。指定がない場合は1.0にする。
	this.width = 48;	// 数字の画像の横幅
	this.height = 48;	// 数字の画像の縦幅
	this.flag = true;	// 消されたかどうかを示すフラグ。falseなら消された、trueなら消されていない
	// ●描画処理を行うメソッド
	this.drawNumber = function(){
		if (!this.flag){ return; }	// 消されているので表示処理はしない
		ctx.clearRect(this.x, this.y, this.width, this.height);	// 指定された範囲を消す
		var imgObj = document.getElementById("N"+this.num);	// HTMLファイル内の画像を指定する
		var saveOpacity = ctx.globalAlpha;	// 現在の不透明度を変数に保存する
		ctx.globalAlpha = this.opacity;	// 不透明度を設定する
		ctx.drawImage(imgObj, this.x, this.y);	// 指定した座標に数字の画像を描画する
		ctx.globalAlpha = saveOpacity;	// 元の不透明度に戻す
	}
	// ●消去処理を行うメソッド
	this.eraseNumber = function(){
		if (!this.flag){ return; }	// 消されているので消去処理はしない
		ctx.clearRect(this.x, this.y, this.width, this.height);	// 指定された範囲を消す
	}
	// ●クリックのチェックを行うメソッド
	this.check = function(x, y){
		if (!this.flag){ return; }	// 消されているのでチェック処理はしない
		if ((x < this.x) || (y < this.y) || (x > (this.x+this.width)) || (y > (this.y+this.height))){
			return;	// 範囲外ないので何もしない
		}
		NumStack.push(this.id);	// クリックされた自分自身のIDをスタックに積む
		this.opacity = 0.5;
		this.drawNumber();
		checkNumber();
	}
	this.drawNumber();
}
// ●Canvasがクリックされた時に呼び出す関数
function addNumber(evt){
	// ゲーム中かどうかを調べる
	if (gameFlag === false){ return; }	// ゲーム中でない場合は以後の処理をしない
	// 初めてのクリックかどうか調べる
	if (firstClickFlag === false){
		firstClickFlag = true;	// クリックされたらtrueにする
		timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
	}
	// クリック座標をオブジェクトに渡す
	for(var i=0; i<NumObj.length; i++){
		NumObj[i].check(evt.offsetX, evt.offsetY);
	}
}
// ●合計を調べる関数
function checkNumber(){
	var total = 0;
	for(var i=0; i<NumStack.length; i++){
		total = total + NumObj[NumStack[i]].num;	// スタックに積まれている数字オブジェクトの値を読み出し加算する
	}
	if (total < 10){ return; }	// 合計が10未満なら何もしない
	if (total > 10){	// 合計が10より大きい場合はNGなので、これまでクリックした数字を元の不透明度に戻す
		for(var i=0; i<NumStack.length; i++){
			NumObj[NumStack[i]].opacity = 1.0;
			NumObj[NumStack[i]].drawNumber();
		}
		NumStack = [ ];	// スタックを空にする
		return;
	}
	// 合計が10なのでクリックした数字オブジェクトを消去する
	for(var i=0; i<NumStack.length; i++){
		NumObj[NumStack[i]].eraseNumber();
		NumObj[NumStack[i]].flag = false;
	}
	score = score + 1;	// スコア(得点)を追加する
	document.getElementById("score").innerHTML = score;	// スコア(得点)を表示する
	count = count - NumStack.length;	// 合計からクリックされた数を引く
	if (count === 0){	// 全部消したかどうか調べる
		document.getElementById("clearMsg").style.visibility = "visible";	// ステージクリアメッセージを表示する
		clearInterval(timerID);	// タイマーを停止する
		setTimeout(function(){	// 2秒後に表示されているメッセージを消すためのタイマー
			count = setRandomValue(numXCount, numYCount);	// 乱数値を設定する
			timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
		}, 2000);
	}
	NumStack = [ ];	// スタックを空にする
	return;	// 関数から抜ける
}
// タイマーの表示処理
function displayTimer(){
	time = time - 1;
	document.getElementById("timeString").innerHTML = time + "秒";
	if (time < 1){	// 時間が1より少なくなったらゲームオーバー
		gameFlag = false;	// ゲーム停止をフラグで示す。falseならゲーム中ではない
		clearInterval(timerID);	// タイマーを停止する
		alert("ゲームオーバー");
	}
}
window.onload = init;	// データが完全に読み込まれたら初期化処理を呼び出す