15日目:関数のパラメーター受け渡し方法を改良する

15日目です。今回は見た目の変更はなく、関数の呼び出し部分のみの改良です。前回の最後にも書きましたが、現状のまま改良していくと近いうちにプログラムがぐちゃぐちゃ(スパゲッティプログラム)になるか、どこかでバグ(不具合)が出て破綻する可能性が大きくなります。でも、まあ1980年代のBASICなどは、こんな感じで作っていくしかなかったのですが、当時はメモリが少なく作れるプログラムの長さにも限度があったので、ぐちゃぐちゃだけど何とか把握できる程度ではありました。が、メモリが増えプログラムが巨大になっていくと、もうお手上げ。頑張って関数やサブルーチン化してもお手上げ。まあ、そんな時に流れてきたのがオブジェクト指向だったわけですが、今回はそこはナシで、また次回以降にします。
さて、今回改良するのは「数字を指定座標に描画する」関数「drawNumber」です。通常、関数にはパラメーターが渡されます。前回は以下のようにしてパラメーターを渡していました。(以下は関数側の先頭コード)

function drawNumber(n, x, y, opacity){

この場合、関数には指定した順番にパラメーターを渡さないといけません。つまり「数値、X座標、Y座標、不透明度」の順番にパラメーターを指定してdrawNumber関数を呼び出す必要があります。順番を間違えてしまうと予期せぬ結果になります。が、だいたい順番を間違えたりしてプログラムがうまく動作しなくなるわけです。バグの温床の1つになるわけです。単純にこの方法はよくないという事です。そこで、関数にパラメーターを順番に渡すのをやめてオブジェクトを1つだけ渡すようにします。数値やX座標などは、このオブジェクトのプロパティに入れて渡すことになります。つまり以下のように関数を呼び出す際のパラメーターの受け渡しが変わります。

drawNumber({ // ●オブジェクトで渡す
n : num[x+y*w],
x : x*48,
y : y*48
});


JavaScriptでは{と}で囲んで、その中にプロパティ名と値を指定することができます(初期のJavaScriptでは、この表記はできませんでした)。この場合、順番は関係ありません。つまり上記の呼び出しは以下のように順番を変えても問題ありません。

drawNumber({ // ●オブジェクトで渡す
y : y*48,
n : num[x+y*w],
x : x*48
});


それでは次に関数側の処理です。これも受け取るパラメーターは1つになります。パラメーターはオブジェクトなのでプロパティ値を読み出すだけです。この場合も読み出す順番は関係ありません。

function drawNumber(obj){ // ●オブジェクトからプロパティを読み出し変数に入れる
var n = obj.n; // ●数値を読み出す
var x = obj.x; // ●X座標を読み出す
var y = obj.y; // ●Y座標を読み出す
var opacity = obj.opacity; // ●不透明度を読み出す
if (!n){ n = 1; } // ●数値が指定されていない場合は1にする
if (!x){ x = 0; } // ●X座標が指定されていない場合は0にする
if (!y){ y = 0; } // ●Y座標が指定されていない場合は0にする
if (!opacity){ opacity = 1.0; } // 不透明度が指定されていない場合は100%の不透明度にする

この改良によるメリットは以下のようになります。

  1. 関数を呼び出す際のパラメーターの順番を気にしなくてよい
  2. 任意のプロパティを追加して渡しても影響がない

まあ、これで改良に対する耐性が少しついたというところです。しかし、これでは実質機能追加、改良に耐えることはできないので根本的にプログラムを見直す必要があります。ということで、次回はさらにプログラムを改良し変更に耐えられるようにします。
ちなみに上記のコードはJavaScriptでは冗長なので

var n = obj.n; // ●数値を読み出す
if (!n){ n = 1; } // ●数値が指定されていない場合は1にする

は、以下のように1行にまとめられることが多くあります。

var n = obj.n | 1; // ●数値を読み出す。数値が指定されていない場合は1にする
また、ECMAScript 6thだとデフォルトパラメーターを指定する機能があります。

[サンプルを実行する]

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

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 prevNumber = 0;	// 直前にクリックした数字を入れる変数
var prevNumberX;	// 直前にクリックした数字のX座標を入れる変数
var prevNumberY;	// 直前にクリックした数字のY座標を入れる変数
var prevPtr;	// 直前にクリックした数字の配列内での位置を入れる変数
var time = 0;	// ゲームオーバーまでの時間
var timerID = null;	// カウントダウンタイマーのIDを入れる変数
var firstClickFlag = false;	// 最初にクリックしたかどうかのフラグ
var gameFlag = true;	// ゲーム中かどうかのフラグ。trueならゲーム中。
var num = [ ];	// 数字を入れる配列変数を用意
var numXCount = 4;	// 横の数字の個数
var numYCount = 6;	// 縦の数字の個数
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";	// ステージクリアメッセージを消す
	for(var y=0; y<h; y++){	// 数字の数だけ繰り返す
		for(var x=0; x<w; x++){
			drawNumber({	// ●オブジェクトで渡す
				n : num[x+y*w],
				x : x*48,
				y : y*48
			});
		}
	}
	return len;	// 数字の総数を返す
}
// ●数字を指定座標に描画する
function drawNumber(obj){
	// ●オブジェクトからプロパティを読み出し変数に入れる
	var n = obj.n;	// ●数値を読み出す
	var x = obj.x;	// ●X座標を読み出す
	var y = obj.y;	// ●Y座標を読み出す
	var opacity = obj.opacity;	// ●不透明度を読み出す
	if (!n){ n = 1; }	// ●数値が指定されていない場合は1にする
	if (!x){ x = 0; }	// ●X座標が指定されていない場合は0にする
	if (!y){ y = 0; }	// ●Y座標が指定されていない場合は0にする
	if (!opacity){ opacity = 1.0; }	// 不透明度が指定されていない場合は100%の不透明度にする
	var imgObj = document.getElementById("N"+n);	// HTMLファイル内の画像を指定する
	var saveOpacity = ctx.globalAlpha;	// 現在の不透明度を変数に保存する
	ctx.globalAlpha = opacity;	// 不透明度を設定する
	ctx.drawImage(imgObj, x, y);	// 指定した座標に数字の画像を描画する
	ctx.globalAlpha = saveOpacity;	// 元の不透明度に戻す
}
// 指定座標の範囲を消す
function eraseNumber(x, y){
	ctx.clearRect(x, y, 48, 48);	// 指定された範囲を消す
}
// 2つの数字の合計が10かどうか調べる
function addNumber(evt){
	// ゲーム中かどうかを調べる
	if (gameFlag === false){ return; }	// ゲーム中でない場合は以後の処理をしない
	// 初めてのクリックかどうか調べる
	if (firstClickFlag === false){
		firstClickFlag = true;	// クリックされたらtrueにする
		timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
	}
	// クリック座標を求める
	var clickX = parseInt(evt.offsetX / 48);
	var clickY = parseInt(evt.offsetY / 48);
	var ptr = clickX + clickY*numXCount;	// 数字の配列内の位置を計算する
	var clickNum = num[ptr];	// 配列から数字を読み出す
	if (!clickNum){ return; } 	// 数字以外の範囲をクリックした場合は以後の処理をしない
	// 1個目のクリックの場合
	if (prevNumber === 0){
		prevNumber = clickNum;	// クリックされた数字を変数に入れる
		prevNumberX = clickX;	// クリックされた数字のX座標を変数に入れる
		prevNumberY = clickY;	// クリックされた数字のY座標を変数に入れる
		prevPtr = ptr;	// クリックされた数字の配列内での位置を変数に入れる
		eraseNumber(clickX*48, clickY*48);	// 現在の数字を消す
		drawNumber({	// ●現在の数字を半透明で表示する
			n : clickNum,
			x : clickX*48,
			y : clickY*48,
			opacity : 0.5
		});
		return;	// 関数から抜ける
	}
	// 2個目のクリックの場合
	var total = prevNumber + parseInt(clickNum);	// 直前の数値とクリックされた数値を加算する
	if (total === 10){	// 合計が10の場合の処理
		eraseNumber(clickX*48, clickY*48);	// 現在の数字を非表示にする
		eraseNumber(prevNumberX*48, prevNumberY*48);	// 前にクリックした数字を非表示にする
		num[prevPtr] = null;	// 直前の数字を消す。忘れると何度もクリックできてしまう
		num[ptr] = null;	// 数字を消す。忘れると何度もクリックできてしまう
		prevNumber = 0;	// 数値を0にする
		score = score + 1;	// スコア(得点)を追加する
		document.getElementById("score").innerHTML = score;	// スコア(得点)を表示する
		count = count - 2;	// 合計から2を引く
		if (count === 0){	// 全部消したかどうか調べる
			document.getElementById("clearMsg").style.visibility = "visible";	// ステージクリアメッセージを表示する
			clearInterval(timerID);	// タイマーを停止する
			setTimeout(function(){	// 2秒後に表示されているメッセージを消すためのタイマー
				count = setRandomValue(numXCount, numYCount);	// 乱数値を設定する
				timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
			}, 2000);
		}
		return;	// 関数から抜ける
	}
	// 合計が10でなかった場合
	eraseNumber(prevNumberX*48, prevNumberY*48);	// 前にクリックした数字を消す
	drawNumber({	// ●前にクリックした数字を表示する
		n : prevNumber,
		x : prevNumberX*48,
		y : prevNumberY*48
	});
	prevNumber = 0;	// 数値を0にする
}
// タイマーの表示処理
function displayTimer(){
	time = time - 1;
	document.getElementById("timeString").innerHTML = time + "秒";
	if (time < 1){	// 時間が1より少なくなったらゲームオーバー
		gameFlag = false;	// ゲーム停止をフラグで示す。falseならゲーム中ではない
		clearInterval(timerID);	// タイマーを停止する
		alert("ゲームオーバー");
	}
}
window.onload = init;	// データが完全に読み込まれたら初期化処理を呼び出す