HTML5のCanvas要素を利用して、ゲーム画面のスクロール表示テストを行ってみます。
まずは、スクロール表示の基本的な流れとCanvasのビットマップ表示能力を確認するために、20*20ピクセルのマップチップを15*15並べたマップ画面を自動的に右スクロールさせてみましょう。

マップを構成するチップは、JavaScriptのコードでImageDataに作成します。4種類用意して、それぞれ乱数で特定の色系統(赤/緑/青/灰)で埋めてみました。

スクロール表示を行う処理は、まずスタイルシートで非表示にしたマップ描画Canvasに現在の表示領域(15*15チップ)とスクロールで表示範囲に入ってくる領域(1*15チップ)をあわせた16*15チップの領域を描画します。そして、このマップ描画Canvasを15*15チップの大きさの表示用Canvasに「ずらしながら描画」することで、スクロール表示を行ってみました。

スクロール描画でずらす位置は変数dxで管理し、このdxがマップチップの横幅(スクロール方向のサイズ)になると1チップ分のスクロール表示が完了したことになります。

最初の描画(マップ左端)を行った後は、30ms毎に呼び出されるloop()で繰り返し処理に入ります。loop()では、毎回dxを増やしてマップ描画Canvasに描かれたマップ画面を表示Canvasに転送して行き、dxがチップサイズに達したら新たなスクロールを開始、スクロール位置がマップ右端に達したら処理を終えるようにしてみました。

開始ボタンをクリックすると、スクロールを開始します(Canvas非対応の環境では実行できません)。


HTMLとJavaScriptのコードは以下のようになっています。

<div style="text-align: center;">
<canvas id="view_canvas" width="300" height="300"></canvas>
<canvas id="map_canvas" width="320" height="300" style="display: none;"></canvas><br>
<button id="testBtn" onclick="start()" disabled>開始</button>
</div>

<script type="text/javascript">

	// 現在位置
	var x = 0;

	// スクロール位置
	var dx = 0;

	// マップチップ画像配列
	var chipList = new Array();

	// マップデータ配列
	var map = new Array();

	// マップチップサイズ
	var tip_width = 20;
	var tip_height = 20;

	// マップサイズ
	var map_cols = 100;
	var map_rows = 15;

	// 表示領域サイズ(チップ数)
	var view_cols = 15;
	var view_rows = 15;

	// 表示領域サイズ
	var view_width = view_cols * tip_width;
	var view_height = view_rows * tip_height;

	// 表示Canvas
	var viewCanvas = document.getElementById('view_canvas');

	// マップ描画Canvas(非表示)
	var mapCanvas = document.getElementById('map_canvas');

	init();

	function init() {

		var i;
		var j;

		// 乱数でマップデータ作成
		for (i = 0;i < map_rows;i++) {
			for (j = 0;j < map_cols;j++) {
				map[j + i * 100] = Math.floor(Math.random() * 4);
			}
		}

		// CanvasとImageDataが利用できなければ戻る
		if (!mapCanvas.getContext || !mapCanvas.getContext('2d').createImageData) {
			return;
		}

		// Canvasのコンテキスト取得
		var mapContext = mapCanvas.getContext('2d');

		// ImageDataオブジェクトを作成しマップチップ画像配列に追加
		for (i = 0;i < 4;i++) {
			chipList.push(mapContext.createImageData(tip_width, tip_height));
		}

		// チップ0(赤)各ピクセルの色情報設定
		for (i = 0;i < tip_width;i++) {
			for (j = 0;j < tip_height;j++) {

				// 赤成分
				chipList[0].data[j * 4 + i * tip_width * 4] = 224 + Math.floor(Math.random() * 32);

				// 緑成分
				chipList[0].data[1 + j * 4 + i * tip_width * 4] = Math.floor(Math.random() * 32);

				// 青成分
				chipList[0].data[2 + j * 4 + i * tip_width * 4] = Math.floor(Math.random() * 32);

				// アルファ成分
				chipList[0].data[3 + j * 4 + i * tip_width * 4] = 255;

			}
		}

		// チップ1(緑)各ピクセルの色情報設定
		for (i = 0;i < tip_width;i++) {
			for (j = 0;j < tip_height;j++) {

				// 赤成分
				chipList[1].data[j * 4 + i * tip_width * 4] = Math.floor(Math.random() * 32);

				// 緑成分
				chipList[1].data[1 + j * 4 + i * tip_width * 4] = 224 + Math.floor(Math.random() * 32);

				// 青成分
				chipList[1].data[2 + j * 4 + i * tip_width * 4] = Math.floor(Math.random() * 32);

				// アルファ成分
				chipList[1].data[3 + j * 4 + i * tip_width * 4] = 255;

			}
		}

		// チップ2(青)各ピクセルの色情報設定
		for (i = 0;i < tip_width;i++) {
			for (j = 0;j < tip_height;j++) {

				// 赤成分
				chipList[2].data[j * 4 + i * tip_width * 4] = Math.floor(Math.random() * 32);

				// 緑成分
				chipList[2].data[1 + j * 4 + i * tip_width * 4] = Math.floor(Math.random() * 32);

				// 青成分
				chipList[2].data[2 + j * 4 + i * tip_width * 4] = 224 + Math.floor(Math.random() * 32);

				// アルファ成分
				chipList[2].data[3 + j * 4 + i * tip_width * 4] = 255;

			}
		}

		// チップ3(灰)各ピクセルの色情報設定
		for (i = 0;i < tip_width;i++) {
			for (j = 0;j < tip_height;j++) {

				// 赤成分
				chipList[3].data[j * 4 + i * tip_width * 4] = 160 + Math.floor(Math.random() * 32);

				// 緑成分
				chipList[3].data[1 + j * 4 + i * tip_width * 4] = 160 + Math.floor(Math.random() * 32);

				// 青成分
				chipList[3].data[2 + j * 4 + i * tip_width * 4] = 160 + Math.floor(Math.random() * 32);

				// アルファ成分
				chipList[3].data[3 + j * 4 + i * tip_width * 4] = 255;

			}
		}

		scroll(x);
		draw();

		// 開始ボタンを有効化
		document.getElementById("testBtn").disabled = false;

	}

	// スクロール開始
	function start() {

		// 開始ボタンを無効化
		document.getElementById("testBtn").disabled = true;

		// メインループ呼び出し
		setTimeout("loop()", 30);

	}

	// メインループ
	function loop() {

		// スクロール位置更新
		if (dx++ == tip_width) {

			x++;

			// マップ右端に達していなければスクロール
			if (x < map_cols - (view_cols)) {
				scroll(x);
			}

		}

		draw();

		// マップ右端に達していなければループ
		if (x < map_cols - (view_cols)) {
			setTimeout("loop()", 30);
		}

	}

	// 新規スクロール開始
	function scroll(pos) {

		x = pos;
		dx = 0;

		// Canvasのコンテキスト取得
		var mapContext = mapCanvas.getContext('2d');

		if (x == 0) {
			// マップ描画Canvasにスクロール範囲を描画
			for (i = 0;i < view_rows;i++) {
				for (j = 0;j < view_cols + 1;j++) {
					mapContext.putImageData(chipList[map[x + j + i * map_cols]], j * tip_width, i * tip_height);
				}
			}
		} else {

			mapContext.drawImage(mapCanvas, tip_width, 0, view_width, view_height, 0, 0, view_width, view_height);

			// マップ描画Canvasにスクロール範囲を描画
			for (i = 0;i < view_rows;i++) {
				mapContext.putImageData(chipList[map[x + view_cols + i * map_cols]], view_cols * tip_width, i * tip_height);
			}

		}

	}

	// 表示Canvasに描画
	function draw() {

		// Canvasのコンテキスト取得
		var viewContext = viewCanvas.getContext('2d');

		viewContext.drawImage(mapCanvas, dx, 0, 300, 300, 0, 0, 300, 300);

	}

</script>

実行してみると、変に引っかかったり描画が乱れることもなくかなりスムーズにスクロール描画されるようですね。JavaScriptでここまでスムーズな描画ができるのは、ちょっと驚きました。Webブラウザ競争の争点になっている「JavaScriptの高速化」とCanvasの効率的な実装技術のおかげでしょうか。

Canvasでも実用的なスクロール表示ができることが確認できたので、RPGやシューティングゲームなどのゲーム画面のスクロール表示に活用していきたいですね。


創作プログラミングの街 > ゲーム開発室 > Webゲーム開発室