10日目:数字を画像にし背景画像を追加する

10日目です。今回はオリジナルのTEN -10-と同様にタッチ/クリックする数字を画像にし、さらに背景画像を追加します。これで、まあ普通のゲームらしい感じになるはずです。グラフィックはオリジナルのものを流用するのは権利の都合で駄目だという事なので全部自前で用意することにします。画像の作り方などは最後に説明し、まずは画像に変更することによるプログラムの修正です。
これまでは数字が表示されている部分はdiv要素で囲んでいましたが、背景画像を追加する都合上スタイルシートで要素の横幅や表示する画像のURLを指定します。これはHTMLファイルに以下のようにスタイルシートを指定しました。

#stage { width : 200px; height : 320px; background-image: url(images/bg.png); }

まあ、横幅などは無理に指定しなくてもよいのですが、その場合はbackgroundで背景の繰り返しをしない(no-repeat)ようにしておけばよいでしょう。特にタッチ/クリックする数字の個数を変更する場合は幅は指定しない方が楽です。
HTMLファイルの変更はこれだけです。次にスクリプトの修正です。これまではinput要素を使っていました。要素を検索する部分などにinput要素を指定していました。その部分をimg要素に変更します。プログラム中で実際に変更した箇所を●印で示してあります。なお、コメント部分だけを修正した箇所は○印にしてあります。プログラムで変更した部分は4箇所だけです。基本的には要素検索部分の変更ですが、img要素を使って画像を表示する場合、src属性に画像のURLを指定する必要があります。大きな修正は関数setRandomValue内の最後の方にある以下のコードだけです。

ele[i].src = "images/"+num[i]+".png"; // ●画像を表示する

なお、表示する数字の画像や背景画像はimagesフォルダに入っています。また、数字は1.pngが1、8.pngが数字の8に対応しています。数字と対応しているのでランダムに求めた数字をそのままファイル名(URL)として利用することができます。数字の画像のファイル名をone.pngとかnine.pngのようにしてしまうとプログラムが面倒になります。ファイル名を上手に付けるということだけでもプログラムが簡単になります。

これでオリジナルのTEN -10-と似たような感じになりました。まだ、改良すべき点は多々ありますが、何となく画面が似てきただけでも雰囲気が変わります。まあ、雰囲気が同じになった、数字がグラフィックになったからといってゲームが格段に面白くなるわけではありません。ここらへんの改良は、また後で行うとして今回はグラフィックの作成について少し書いておきます。絵が描ける人やデザイン専門の人なら読んでも意味がありませんが、グラフィックが全く駄目なプログラマなら多少参考になるかもしれません。

まず、どのようなソフトで描くか、です。これはいろいろなソフトがありますが、ここでは有名なAdobe社のPhotoshopを使いました。まあ、慣れているからというのもあります。
最初に数字の画像の作成です。画像のサイズは48ピクセル×48ピクセルです。新規に作成する際に48×48ピクセルで作成するように指定します。また、背景は透明にしておきます。この透明にするというのがポイントです。次に文字ツールを使ってウィンドウ内をクリックします。すると文字の入力ができる状態になるので数字を入力します。1を入力します。入力したら文字を選択して書体(フォント)とサイズを変更します。今回使用した書体はPalatinoで文字の太さはboldにしてあります。また、文字の色も設定しておきます。次に位置を調整します。移動ツールを使って調整するか、カーソルキーを押して位置を微調整します。
調整が終わったら後はPNG形式で保存します。ファイルメニューからWeb用に保存する云々みたいな項目を選択すると保存する際に細かい設定ができる画面になります。PNG形式でPNG24を選択し「透明部分」にチェックを入れて保存します。チェックを入れないと数字の背景が透けなくなってしまいます。あとは、数字を変更し9まで保存すれば数字の画像ができあがります。

次に背景画像の作成です。これもPhotoshopを使います。問題は地球の画像です。フリーの画像もありますが、今回はデータビジュアライゼーションライブラリの1つであるD3.jsを使って描画した地球の画像を利用します。以下でダウンロードできるZIPファイル内にearth.psdという名前で入っています。
地球の画像は背景が白色で透明になっていません。これだとうまく合成できないのでPhotoshopの自動選択ツールを使って地球だけを選択します。その際、白い背景部分をクリックして選択した後、「選択範囲を反転」させると簡単に地球を選択できます。選択したらコピーします。
実際にゲームで表示する背景画像は200×340ピクセルなので、このサイズで新規に背景色に黒を指定し画像を作成し地球の画像をペーストします。地球の方がサイズが大きいので変形させます。コマンドキーとTキー(WindowsはコントロールキーとTキー)を押すとすぐに変形できる状態になります。四隅の□(ハンドル)をドラックしてサイズを調整します。この時にShiftキーを押したままにすると縦横比を保ったまま縮小できます。
次に宇宙を描きます。新規にレイヤーを作成しておくと楽です。ブラシでまわりがぼけているものを選択します。サイズは60とか100など大きめにします。また、不透明度を10〜20%にしておきます。適当にクリックして色を乗せていきます。濃い青や紫を使うと、それっぽくなります。
次に宇宙の星を描きますが、自前で描くのは大変なのでPhotoshopの宇宙ブラシをどこからか拾ってきます。ちなみに宇宙ブラシは以下のサイトにまとめられていますので、好きなものをダウンロードして利用するとよいでしょう。

キラキラに輝く宇宙空間をデザインするPhotoshop無料ブラシ30個まとめ

ブラシにもよりますが、目立って輝く星がない場合があります。自前で描いてもうまく描けないこともあります。そのような場合はPhotoshopのフィルタ機能を利用します。今回の背景に明るく輝く星もフィルタ機能を使って描いています。まず、新規にレイヤーを作成します。描く星のサイズに合わせて範囲を選択します。これは長方形選択ツールを使って指定します。次に選択範囲を黒色で塗り潰します。塗り潰しツールを使っても構いませんし、「編集」メニューにある「塗り潰し..」を選択し塗り潰しても構いません。
次にメニューから「フィルタ」>「描画」>「逆光」を選択します。光源の位置は中央にします。+をドラッグすると変更できます。明るさは80〜110程度にします。レンズの種類は好みのもので構いませんが50-300mmか105mmあたりを選択すればよいでしょう。OKボタンをクリックすれば描画されます。この段階では黒い部分が残ってしまいますのでレイヤーの描画モードを「覆い焼き(リニア)-加算」にします。これで黒色が消え、なおかつ明るさに合わせて背景画像の星と調和(加算合成)されるようになります。プログラム的に書くと以下のような感じで合成されることになります。なお、B、B1、B2は1つの点(ピクセル)の明るさを示す0〜255までの数値です。

var B1 = 星のレイヤーの1ピクセルの輝度;
var B2 = 背景の宇宙のレイヤーの1ピクセルの輝度;
var B = B1 + B2;
if (B > 255){ B = 255; } // Bが実際に表示される輝度になる

あとは輝く星のレイヤーをコピーして複数の星を作成します。適当な位置に移動させたりサイズを調整すればできあがりです。最後にPNG形式で保存します。なお、実際の画像ファイルもダウンロードできるZIPファイル内にbg.psdという名前で入っていますので参考にしてください。

これで今回は終わりです。文字が画像に変わっただけですが、ゲーム性はいまいちです。画像にしたからといってゲーム性が向上するわけではありませんが、グラフィックによっては難易度が変わったり多少面白くなったりすることもあります。ただ、技術的にはまりこんでしまって改良の方向がおかしな方向に向かってしまうことがあります。悪例としては「img要素を使っているから面白くないに違いない。少なくともdiv要素やcanvas要素を使うのがよいのではないか」というものです。それは違うだろ、というような場合でも、なぜか違った方向に向かってしまうこともあります。分かっていながら向かわざるを得ない状況が発生することもあります。
ということで次回はimg要素でなくdiv要素を使って数字を表示させてみます。img要素を使った場合との違いがわかる程度で技術的にも面白くないのですが、やるだけはやるということで。

[サンプルを実行する]

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

HTMLファイルの内容

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TEN</title>
<style>
#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>
<div id="stage"></div>
<div id="msg"></div>
<div id="clearMsg">ステージクリア!</div>
<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 prevEle = null;		// 直前にクリックした要素を入れる変数
var time = 0;	// ゲームオーバーまでの時間
var timerID = null;	// カウントダウンタイマーのIDを入れる変数
var firstClickFlag = false;	// 最初にクリックしたかどうかのフラグ
// 初期化処理
function init(){
	// ----- ステージを動的に生成する ------
	var w = 4;	// 横の数字の個数
	var h = 6;	// 縦の数字の個数
	var htmlString = "";
	for(var j=0; j<h; j++){
		for(var i=0; i<w; i++){
			htmlString = htmlString + '<img src="images/1.png" width="48" height="48">'; 	// ●img要素で生成
		}
		htmlString = htmlString + '<br>';
	}
	document.getElementById("stage").innerHTML = htmlString;
	// ------ ここまで ------
	var ele = document.querySelectorAll("img");	// ●クリックしたら処理するようにイベントを設定
	for(var i=0; i<ele.length; i++){	// 要素の数だけ繰り返す
		ele[i].onclick = addNumber;	// クリック時に呼び出す関数を指定する
	}
	count = ele.length;	// ○img要素の総数を入れる
	setRandomValue(ele);	// ○img要素の数を渡す
	time = 30;	// 30秒に設定する
	document.getElementById("timeString").innerHTML = time + "秒";	// 時間を表示する
}
// ランダムに値を設定する関数
function setRandomValue(ele){
	var len = ele.length;	// 要素の総数
	// ○img要素にランダムな値を設定する
	var num = [ ];
	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);
	}
	// 配列要素をele.length回シャッフルする
	for(var i=0; i<len; i++){
		var p1 = Math.floor(Math.random() * len);	// ○img要素の数の範囲で乱数を発生させる
		var p2 = Math.floor(Math.random() * len);	// ○img要素の数の範囲で乱数を発生させる
		var n = num[p1];	// 2つの要素の内容を入れ替える
		num[p1] = num[p2];
		num[p2] = n;
	}
	// ○img要素に値を設定する
	for(var i=0; i<len; i++){	// 要素の数だけ繰り返す
		ele[i].value = num[i];	// 値を設定する
		ele[i].src = "images/"+num[i]+".png";	// ●画像を表示する
	}
}
// 2つの数字の合計が10かどうか調べる
function addNumber(){
	// 初めてのクリックかどうか調べる
	if (firstClickFlag === false){
		firstClickFlag = true;	// クリックされたらtrueにする
		timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
	}
	// 1個目のクリックの場合
	if (prevNumber === 0){
		prevEle = this;	// 要素の情報を変数に入れる
		prevEle.style.backgroundColor = "red";	// 背景を赤色にする
		prevNumber = parseInt(this.value);	// クリックされた要素の番号を変数に入れる
		return;	// 関数から抜ける
	}
	// 2個目のクリックの場合
	var total = prevNumber + parseInt(this.value);	// 直前の数値とクリックされた要素の数値を加算する
	if (total === 10){	// 合計が10の場合の処理
		this.style.visibility = "hidden";	// 現在の要素を非表示にする
		prevEle.style.visibility = "hidden";	// 前の要素を非表示にする
		prevNumber = 0;	// 数値を0にする
		document.getElementById("msg").innerHTML = "OK!";	// メッセージを表示する
		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秒後に表示されているメッセージを消すためのタイマー
				document.getElementById("clearMsg").style.visibility = "hidden";	// ステージクリアメッセージを消す
				var ele = document.querySelectorAll("img");	// ●img要素を取得する
				setRandomValue(ele);	// 乱数値を設定する
				for(var i=0; i<ele.length; i++){
					ele[i].style.visibility = "visible";	// 表示する
					ele[i].style.backgroundColor = "";	// 透明にする
				}
				count = ele.length;	// input要素の総数を入れる
				document.getElementById("msg").innerHTML = "";	// メッセージをクリア
				timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
			}, 2000);
		}
		return;	// 関数から抜ける
	}
	// 合計が10でなかった場合
	prevNumber = 0;	// 数値を0にする
	prevEle.style.backgroundColor = "";	// 透明にする
	document.getElementById("msg").innerHTML = "合計が10じゃないよ!";	// メッセージを表示する
}
// タイマーの表示処理
function displayTimer(){
	time = time - 1;
	document.getElementById("timeString").innerHTML = time + "秒";
	if (time < 1){	// 時間が1より少なくなったらゲームオーバー
		clearInterval(timerID);	// タイマーを停止する
		alert("ゲームオーバー");
	}
}
init();	// 初期化処理を呼び出す