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やシューティングゲームなどのゲーム画面のスクロール表示に活用していきたいですね。