2-4 配列でマップを管理する

ゲームソフトにはそのゲーム内の世界の構造を定義したマップデータがあります。 例えばポケットモンスターやドラゴンクエストのようなロールプレイングゲームは、 フィールド、町、ダンジョンなどのマップデータが用意されています。 スーパーマリオブラザーズのようなアクションゲームであれば、 ブロックや床の配置がマップデータになっています。
本項ではマップデータを管理する方法を解説します。


(1)二次元配列でマップデータを定義する

1-8 (3) で一次元の配列について学びました。 マップデータを管理するには二次元の配列を用います。
JavaScriptで二次元配列を定義する方法はいくつかありますが、判りやすい記述の仕方は次のようになります。

例)変数名をaryとした二次元配列

var ary = [
[   1,  2,  3,  4,  5 ],
[  10, 20, 30, 40, 50 ],
[ 100,200,300,400,500 ]
];

この例では3行×5列の15個の配列を定義しています。 値を参照するには ary[Y][X] とします。 例えば ary[0][4]の値は5、ary[2][2]の値は300になります。
X→
Y↓
01234
012345
11020304050
2100200300400500

二次元配列でマップデータを定義するサンプルを見てみましょう。3マップ分のデータを定義しており、画面をタップ(クリック)するごとにマップが切り替わります。
example241.html ← 動作の確認
ソースコードは次のようになります。
01<!DOCTYPE html>
02<html lang="ja">
03<head>
04<meta charset="utf-8">
05<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0 user-scalable=no">
06<title>JavaScriptのテストプログラム</title>
07</head>
08<body>
09<canvas style="position:absolute; top:0; bottom:0; left:0; right:0; margin:auto;" id="bg"></canvas>
10<script>
11var winW = window.innerWidth;
12var winH = window.innerHeight;
13var canvas = document.getElementById("bg");
14canvas.width = winW;
15canvas.height = winH;
16var cnt = canvas.getContext("2d");
17var SCALE = winW / 384;
18cnt.scale( SCALE, SCALE );
19cnt.font = "24px monospace";
20cnt.textAlign = "center";
21cnt.textBaseline = "middle";
22
23//画像ファイル用の配列
24var img;
25var imgPre;
26
27function loadImg() {//画像を読み込む
28 imgPre = false;
29 img = new Image();
30 img.src = "example241.png";
31 img.onload = function() {
32  imgPre = true;
33  drawMap();
34 }
35}
36
37function drawChip( no, dx, dy ) {//画像を切り出し表示
38 if( imgPre != true ) return;
39 var sx = 32*no;
40 var sy = 0;
41 cnt.drawImage( img, sx, sy, 32, 32, dx, dy, 32, 32 );
42}
43
44//変数の宣言
45var mapNo = 0;
46var MAP_DATA = [
47
48[ 9, 5, 5, 9, 7, 7, 7, 7, 9, 5, 5, 9 ],
49[ 9, 6, 6, 9, 8, 8, 8, 8, 9, 6, 6, 9 ],
50[ 9, 0, 0, 9, 3, 3, 3, 3, 9, 0, 0, 9 ],
51[ 9, 0, 0, 9, 3, 3, 3, 3, 9, 0, 0, 9 ],
52[ 9, 0, 0, 5, 3, 3, 3, 3, 5, 0, 0, 9 ],
53[ 9, 0, 0, 6, 1, 1, 1, 1, 6, 0, 0, 9 ],
54[ 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 ],
55[ 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6 ],
56
57[ 5, 7, 7, 5, 5, 7, 7, 5, 5, 7, 7, 5 ],
58[ 6, 8, 8, 6, 6, 8, 8, 6, 6, 8, 8, 6 ],
59[ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 ],
60[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
61[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
62[ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 ],
63[ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 ],
64[ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 ],
65
66[ 4, 4, 4, 4, 5, 5, 5, 5, 4, 4, 4, 4 ],
67[ 4, 4, 4, 5, 6, 6, 6, 6, 5, 4, 4, 4 ],
68[ 4, 4, 4, 6, 0, 0, 0, 0, 6, 4, 4, 4 ],
69[ 4, 4, 4, 5, 0, 3, 3, 0, 5, 4, 4, 4 ],
70[ 4, 4, 4, 6, 0, 3, 3, 0, 6, 4, 4, 4 ],
71[ 4, 4, 4, 5, 0, 0, 0, 0, 5, 4, 4, 4 ],
72[ 4, 4, 4, 6, 1, 1, 1, 1, 6, 4, 4, 4 ],
73[ 4, 4, 4, 5, 1, 1, 1, 1, 5, 4, 4, 4 ],
74
75];
76
77function drawMap() {
78 var x, y;
79 for( y = 0; y < 8; y ++ ) {
80  for( x = 0; x < 12; x ++ ) drawChip( MAP_DATA[mapNo*8+y][x], x*32, y*32 );
81 }
82
83 cnt.fillStyle = "#000"; cnt.fillText( "マップの番号 " + mapNo, 197, 24 );
84 cnt.fillStyle = "#fff"; cnt.fillText( "マップの番号 " + mapNo, 197, 23 );
85}
86
87//画面をタップ(クリック)した判定
88canvas.addEventListener( "click", onClick );
89function onClick(event) {
90 event.preventDefault();
91 mapNo ++;
92 if( mapNo == 3 ) mapNo = 0;
93 drawMap();
94}
95
96window.onload = loadImg();
97</script>
98</body>
99</html>

var MAP_DATAで宣言した二次元配列がマップデータです。 今回は1つのマップを縦(行)8マス×横(列)12マスとし、3マップ分のデータを定義しています。 配列の値と画像の関係は次のようになります。

ゲームの多くはこのように分割した画像で背景を構成しており、 床や壁など1つ1つの画像をマップチップと呼びます。 今回使っているマップチップのサイズは32x32ドットです。

画像は一枚にまとめよう

マップチップは1枚の画像に並べており、読み込んでいるファイルはこの1枚です。 マップデータの値からマップチップを切り出し表示しています。 1-8 (2) で触れましたが、JavaScriptでは画像枚数が増えるほど読み込みに失敗する可能性が高くなります。 HTML5+JavaScriptで開発するゲームでは、私達の経験から、JavaScript起動時に読み込むのは数枚程度にしておくのが無難です。 コンピューターの性能や通信速度、ソフトウェアの技術は日々進歩していますので、 いずれネット上の多くの画像を瞬時に扱えるようになるでしょうが、 今のところ画像はできるだけ一つのファイルにまとめましょう。


マップデータの値は MAP_DATA[mapNo*8+y][x] として参照しており、行方向の[]の mapNo*8+y という記述がポイントです。 mapNoは何番目のマップデータかという変数で、画面をタップ(クリック)するごとに 0→1→2→0→・・・と値を変え、背景を変更しています。 タップ(クリック)判定はclickイベントで行っています。 clickはタップとマウスクリックどちらにも働きますので、簡素な入力判定で便利に使えるイベントです。


(2)マップとキャラクターの当たり判定

一般的なゲームは背景の上にキャラクターが表示され、 背景には入れる場所()と入れない場所()があるルールになっています。 床と壁を判断することをマップとの当たり判定といいます。

この当たり判定を行うサンプルを見てみましょう。 草地が床(入れる場所)、岩と木が壁(入れない場所)になっています。 画面に表示されたボタンか、パソコンであればカーソルキーでキャラクターを動かし、動作をご覧下さい。
example242.html ← 動作の確認
ソースコードは次のようになります。
01<!DOCTYPE html>
02<html lang="ja">
03<head>
04<meta charset="utf-8">
05<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0 user-scalable=no">
06<title>JavaScriptのテストプログラム</title>
07</head>
08<body>
09<canvas style="position:absolute; top:0; bottom:0; left:0; right:0; margin:auto;" id="bg"></canvas>
10<script>
11var winW = window.innerWidth;
12var winH = window.innerHeight;
13var canvas = document.getElementById("bg");
14canvas.width = winW;
15canvas.height = winH;
16var cnt = canvas.getContext("2d");
17var SCALE = winW / 384;
18cnt.scale( SCALE, SCALE );
19cnt.font = "32px monospace";
20cnt.textAlign = "center";
21cnt.textBaseline = "middle";
22
23//マウスとタップの判定
24var tapX = 0, tapY = 0, tapC = 0;
25
26canvas.addEventListener( "touchstart", touchStart );
27canvas.addEventListener( "touchmove", touchMove );
28canvas.addEventListener( "touchend", touchEnd );
29function touchStart(event) {
30 event.preventDefault();
31 var rect = event.target.getBoundingClientRect();
32 tapX = event.touches[0].clientX-rect.left;
33 tapY = event.touches[0].clientY-rect.top;
34 transformXY();
35 tapC = 1;
36}
37function touchMove(event) {
38 event.preventDefault();
39 var rect = event.target.getBoundingClientRect();
40 tapX = event.touches[0].clientX-rect.left;
41 tapY = event.touches[0].clientY-rect.top;
42 transformXY();
43}
44function touchEnd(event) { tapC = 0; }
45
46canvas.addEventListener( "mousedown", mouseDown );
47canvas.addEventListener( "mousemove", mouseMove );
48canvas.addEventListener( "mouseup", mouseUp );
49function mouseDown(event) {
50 var rect = event.target.getBoundingClientRect();
51 tapX = event.clientX-rect.left;
52 tapY = event.clientY-rect.top;
53 transformXY();
54 tapC = 1;
55}
56function mouseMove(event) {
57 var rect = event.target.getBoundingClientRect();
58 tapX = event.clientX-rect.left;
59 tapY = event.clientY-rect.top;
60 transformXY();
61}
62function mouseUp(event) { tapC = 0; }
63
64function transformXY() {//実座標→仮想座標への変換
65 tapX = toInt(tapX/SCALE);
66 tapY = toInt(tapY/SCALE);
67}
68
69//キー入力
70var key = 0;
71window.onkeydown = function(event) { key = event.keyCode; }
72window.onkeyup = function(event) { key = 0; }
73
74function toInt( val ) {//整数を返す関数
75 return parseInt(val);
76}
77
78//画像ファイル用の配列
79var img = [];
80var imgPre = [];
81
82function loadImg( n ) {//画像を読み込む
83 imgPre[n] = false;
84 img[n] = new Image();
85 img[n].src = "example242_" + n + ".png";
86 img[n].onload = function() { imgPre[n] = true; }
87}
88
89function drawImg( chip, dx, dy ) {//マップチップを切り出し表示
90 if( imgPre[0] != true ) return;
91 var sx = 1+33*chip;
92 var sy = 1;
93 cnt.drawImage( img[0], sx, sy, 32, 32, dx, dy, 32+1, 32+1 );
94}
95
96function drawImg2( x, y ) {//木の上の部分を表示
97 if( imgPre[0] != true ) return;
98 cnt.drawImage( img[0], 0, 34, 82, 88, x*32-26, y*32-80, 82, 88 )
99}
100
101function drawCharacter( dx, dy, dir, ptn ) {//キャラクターを表示
102 if( imgPre[1] != true ) return;
103 var sx = 32*ptn;
104 var sy = 54*dir;
105 cnt.drawImage( img[1], sx, sy, 32, 54, dx, dy, 32, 54 );
106}
107
108function fText( str, x, y, col ) {//文字表示
109 cnt.fillStyle = col;
110 cnt.fillText( str, x, y );
111}
112
113function fRect( x, y, w, h, col ) {//矩形
114 cnt.fillStyle = col;
115 cnt.fillRect( x, y, w, h );
116}
117
118function setAlp( per ) {//透明度
119 cnt.globalAlpha = per/100;
120}
121
122function drawBtn( str, x, y, w, h ) {//ボタン
123 fRect( x, y, w, h, "#fff" );
124 fRect( x+1, y+1, w-1, h-1, "#abb" );
125 fRect( x+1, y+1, w-2, h-2, "#cdd" );
126 fText( str, x+w/2, y+h/2, "#000" );
127 if( x < tapX && tapX < x+w && y < tapY && tapY < y+h ) {
128  if( tapC == 1 )
129   setAlp(60);
130  else
131   setAlp(20);
132  fRect( x, y, w, h, "#fff" );
133  setAlp(100);
134  return tapC;
135 }
136 return 0;
137}
138
139//変数の宣言
140var idx = 0;
141var tmr = 0;
142
143var playerX, playerY, playerD, playerStep;
144var WALK_PTN = [ 0, 1, 0, 2 ];
145
146var MAP_DATA = [
147[ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
148[ 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 2, 1 ],
149[ 1, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 1 ],
150[ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ],
151[ 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1 ],
152[ 1, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 1 ],
153[ 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1 ],
154[ 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ],
155];
156
157function hitChkBG( x, y ) {
158 var cx = toInt(x/32);
159 var cy = toInt(y/32);
160 return MAP_DATA[cy][cx];
161}
162
163window.onload = moveChr();
164function moveChr() {
165 var x, y;
166 tmr ++;
167
168 switch( idx ) {
169  case 0://初期化処理
170  loadImg(0);//背景画像の読み込み
171  loadImg(1);//キャラクター画像の読み込み
172  playerX = 5*32;
173  playerY = 4*32;
174  playerD = 1;
175  playerStep = 0;
176  idx = 1;
177  break;
178
179  case 1://移動処理
180  if( playerStep == 0 ) {//停止中
181   if( key == 38 || drawBtn( "↑",  96, 256, 192, 48 ) == 1 ) {
182    playerD = 0;
183    if( hitChkBG( playerX, playerY-32 ) == 0 ) playerStep = 4;
184   }
185   if( key == 40 || drawBtn( "↓",  96, 304, 192, 48 ) == 1 ) {
186    playerD = 1;
187    if( hitChkBG( playerX, playerY+32 ) == 0 ) playerStep = 4;
188   }
189   if( key == 37 || drawBtn( "←",   0, 256, 96, 96 ) == 1 ) {
190    playerD = 2;
191    if( hitChkBG( playerX-32, playerY ) == 0 ) playerStep = 4;
192   }
193   if( key == 39 || drawBtn( "→", 288, 256,  96, 96 ) == 1 ) {
194    playerD = 3;
195    if( hitChkBG( playerX+32, playerY ) == 0 ) playerStep = 4;
196   }
197  }
198  if( playerStep != 0 ) {//歩行中
199   if( playerD == 0 ) playerY -= 8;
200   if( playerD == 1 ) playerY += 8;
201   if( playerD == 2 ) playerX -= 8;
202   if( playerD == 3 ) playerX += 8;
203   playerStep --;
204  }
205
206  for( y = 0; y < 8; y ++ ) {
207   for( x = 0; x < 12; x ++ ) drawImg( MAP_DATA[y][x], x*32, y*32 );
208  }
209  drawCharacter( playerX, playerY-28, playerD, WALK_PTN[tmr%4] );
210  for( y = 0; y < 8; y ++ ) {
211   for( x = 0; x < 12; x ++ ) if( MAP_DATA[y][x] == 2 ) drawImg2( x, y );
212  }
213  break;
214 }
215
216 setTimeout( moveChr, 100 );
217}
218</script>
219</body>
220</html>

次の2枚の画像ファイルを読み込んでいます。

ポイントはキャラクターとマップの当たり判定、そしてキャラクターの移動処理です。

【ポイント1】 当たり判定
次のような関数を用意して判定しています。

function hitChkBG( x, y ) {
 var cx = toInt(x/32);
 var cy = toInt(y/32);
 return MAP_DATA[cy][cx];
}

この関数の引数にはキャラクターの座標を与えます。 マップチップは32×32ドットですので、与えられた座標を32で割り、その場所(升目上)のチップの値を返しています。 例えばX座標-32とY座標を与えるとキャラクターの左側にあるマップチップが判るというわけです。

【ポイント2】 キャラクターの移動処理
次の変数でキャラクターの移動を行っています。 playerX, playerYは画面(キャンバス)上の座標であり、マップチップの升目の値ではありません。
変数名用途値の説明
playerX, playerYXY座標マップチップは32ドット角なので、playerX,playerYいずれかが32変化すれば隣のチップ上に移る
playerD向き0上向き、1下向き、2左向き、3右向きとしている
playerStep移動するためのフラグ0の時は停止しており、1~4の時に移動する
180~197行で、キャラクターが停止している時、移動の入力があれば、キャラクターの向きを変え、その方向が床なら移動するためのフラグを立ててます。
198~204行で、移動のフラグが立っていればキャラクターの座標を変化させています。8ドット×4回(4フレーム)で32ドット動かし、隣のチップ上に移る仕組みです。

その他のテクニックとしてキャラクターが下を潜れる木を表示しています。 このような表現は一般的に、 キャラクターの下側に表示する背景とキャラクターの上側に表示する物体の二階層分のデータを用意します。 今回は次のような処理で、一階層分のデータだけで実現しています。

①定義したマップデータから背景を描く
 ↓
②キャラクターを描く
 ↓
③再度マップデータを調べ、上に表示するものがあればそれを描く


それから 1-10 (2) でボタンを表示する自作関数を用意したが、 今回もそこが押されているか判定できるボタン表示の関数を用意しています。

マップチップを切り出し表示した時の筋を消すテクニック
(1)の例ではブラウザやお使いの端末によっては画面に筋が表示されます。これは
①画像ファイルに並んでいる隣り合ったチップの一部が表示される
②キャンバスに表示する際にチップ間に隙間が生じる
という原因で起こります。
(2)の例では
①を回避するため画像ファイルに並べるマップチップを1ドットずつ離す(離した隙間は透明色にする)
②を回避するためチップを表示する際に縦横に1ドット伸ばして表示する
という方法で筋が入らないようにしています。



前のページへ / 次のページへ

お気軽にお問い合わせ下さい →