コンテンツにスキップ

マップをマスターしよう

【メモ】未完箇所

  • やってみよう3 の
    (参照:[課題集「迷路でプレーヤーを動かそう(オブジェクト指向版)」](https://www.notion.so/aeffc3a0649a46d4aaa60f80df813a61?pvs=21)
    のリンク

1.マップを表示しよう

パックマンや横スクロールゲーム、もしくは迷路など、背景にマップ(地図)を使う場合があります。一般的に、マップデータは下記の例のように 2次元配列 に記録されます。

周囲が壁で囲まれたマップを作ってみましょう。ここでは プレイヤーが歩ける場所を0 として、 壁を1 として書いています。

int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1}};

思った通りになっているか、画面に表示して確認してみよう。

下記では プレイヤーが歩ける場所(=0)を白 で、 壁(=1)を赤 で表示しています。

int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1}};
size(450,250);
for (int i = 0; i < 5; i++){ // 2次元配列の縦の長さ分繰り返す
for (int j = 0; j < 9; j++){ // 2次元配列の横の長さ分繰り返す
if (map[i][j] == 0){
fill(255, 255, 255);
} else if (map[i][j] == 1){
fill(255, 0, 0);
}
rect(j * 50, i * 50, 50, 50); //1マスは50の大きさ
}
}

表示すると赤の壁に囲まれたマップができました!

Untitled

やってみよう1★☆☆

  • 迷路らしくしましょう。

マップデータ(二次元配列)を書き換えて内部に壁を追加しましょう。また、ゴールも設置しましょう。下記の例では、ゴールの位置はマップデータを「2」にして、画面では「緑」で表示するようにしているよ!

Untitled

  • プレイヤーを追加してみよう。

プレーヤはマップデータ(二次元配列)に書き込むのではなく、マップを表示した後に(上に)表示するようにしましょう。下の画像の例では、青色がプレイヤーだよ!

Untitled

サンプルコード(クリックすると表示されます)
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
int pi = 1;
int pj = 1;
size(450, 250);
//マップの描画
for (int i = 0; i < 5; i++) { // 2次元配列の縦の長さ分繰り返す
for (int j = 0; j < 9; j++) { // 2次元配列の横の長さ分繰り返す
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
//プレーヤーの表示
fill(0, 0, 255);
rect(pj * 50, pi * 50, 50, 50); //1マス50の大きさ

2.プレイヤーを動かそう

矢印キーを押して上下左右に動けるようにしてみよう。

※ここからはキー入力関数( keyPressed() )を使うので、 setup()draw() が必要になります。

(1)プレーヤーがマス(行・列)で動く場合

矢印キーで動かそう

プレーヤーの位置は行番号・列番号で管理します。

キー入力でプレーヤーの位置(行・列)の値を更新します。

最初は、 壁の有無を無視して自由に動く ようなコードを書いてみよう。

サンプルコード(クリックすると表示されます)
//①マスで移動・当たり判定なし
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
int pi, pj;
void setup() {
size(450, 250);
pi = 1; //プレーヤーのスタート位置(行)
pj = 1; //プレーヤーのスタート位置(列)
}
void draw() {
//マップの描画
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
//プレーヤーの表示
fill(0, 0, 255);
rect(pj*50, pi*50, 50, 50);
}
void keyPressed() {
// 上方向
if (keyCode == UP) {
pi -= 1;
}
// 下方向
if (keyCode == DOWN) {
pi += 1;
}
// 左方向
if (keyCode == LEFT) {
pj -= 1;
}
// 右方向
if (keyCode == RIGHT) {
pj += 1;
}
}

壁がある場所に移動できないようにしよう

今の状態だと壁のある場所にも移動できてしまいます。条件文に 「壁ではない場合」 という条件を付け足し、壁ではない場合だけ動けるようにしましょう。

それには、キー入力があった時に、移動先のマスをマップデータでチェックします。そこが壁ではないなら、プレーヤーの位置を更新します。

例えば上方向の場合、 keyPressed() 関数の定義の中で if 文を下記のように変更します。

void keyPressed() {
// 上方向
if (keyCode == UP && map[pi-1][pj] != 1) {
pi -= 1;
}

やってみよう2★★☆

  • 前述の「壁がある場所に移動できないようにしよう」のサンプルコードでは、UP(上方向)の場合のみでした。DOWN、LEFT、RIGHTの場合も作成して、プログラムを完成させよう。
  • ゴールに到達したときに、クリアの文字を表示させてみよう
サンプルコード(クリックすると表示されます)
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
int pi = 1;
int pj = 1;
boolean clrFlg;
void setup() {
size(450, 250);
}
void draw() {
//マップの描画
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
//プレーヤーの表示
fill(0, 0, 255);
rect(pj*50, pi*50, 50, 50);
if (map[pi][pj] == 2) {
clrFlg = true;
}
//ステージクリア判定
if (clrFlg) {
textSize(80);
text("CLEAR", 120, 120);
}
}
void keyPressed() {
if (!clrFlg) {
// 上方向
if (keyCode == UP && map[pi-1][pj] != 1) {
pi -= 1;
}
// 下方向
if (keyCode == DOWN && map[pi+1][pj] != 1) {
pi += 1;
}
// 左方向
if (keyCode == LEFT && map[pi][pj-1] != 1) {
pj -= 1;
}
// 右方向
if (keyCode == RIGHT && map[pi][pj+1] != 1) {
pj += 1;
}
}
}

やってみよう3★★★

  • オブジェクト指向を学んだ人は、プレーヤーをクラス(オブジェクト指向)で作って動かしてみよう。
サンプルコード(クリックすると表示されます)
class Player {
int px;
int py;
Player(int x1, int y1) { // コンストラクターを使ってプレーヤの最初の位置を指定しよう。
px = x1;
py = y1;
}
void display() { // プレイヤーを表示するためのメソッド
fill(0, 0, 255);
rect(px*50, py*50, 50, 50);
}
void move(int inputKey) { //プレイヤーを上下左右に動かすメソッド
// 上方向
if ((inputKey == 0) && (map[py-1][px] != 1)) {
py -= 1;
}
// 下方向
if ((inputKey == 1) && (map[py+1][px] != 1)) {
py += 1;
}
// 左方向
if ((inputKey == 2) && (map[py][px-1] != 1)) {
px -= 1;
}
// 右方向
if ((inputKey == 3) && (map[py][px+1] != 1)) {
px += 1;
}
}
}
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
Player p = new Player(1, 1);
void setup() {
size(450, 250);
}
void draw() {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
p.display();
}
void keyPressed() {
// 押されたキーに応じてプレイヤークラスのmove()メソッドを呼び出そう。
if (keyCode == UP) {
// 上方向
p.move(0);
}
if (keyCode == DOWN) {
// 下方向
p.move(1);
}
if (keyCode == LEFT) {
// 左方向
p.move(2);
}
if (keyCode == RIGHT) {
// 右方向
p.move(3);
}
}

(2)プレーヤーがドット(x座標・y座標)で動く場合

矢印キーで動かそう

プレーヤーの位置はx座標・y座標で管理します。

キー入力でプレーヤーの位置(x座標・y座標)の値を更新します。

最初は、 壁の有無を無視して自由に動く ようなコードを書いてみよう。

サンプルコード(クリックすると表示されます)
//③ドットで移動・当たり判定なし
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
float px, py;
float speed;
void setup() {
size(450, 250);
px = 50; //プレーヤーのスタート位置(x座標)
py = 50; //プレーヤーのスタート位置(y座標)
speed = 5;
}
void draw() {
//マップの描画
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
//プレーヤーの描画
fill(0, 0, 255);
rect(px, py, 50, 50);
}
void keyPressed() {
if (keyCode == UP) {
// 上方向
py -= speed;
}
if (keyCode == DOWN) {
// 下方向
py += speed;
}
if (keyCode == LEFT) {
// 左方向
px -= speed;
}
if (keyCode == RIGHT) {
// 右方向
px += speed;
}
}

壁がある場所に移動できないようにしよう

今の状態だと壁のある場所にも移動できてしまいます。条件文に 「壁ではない場合」 という条件を付け足しましょう。

ただし、先程の「プレーヤーがマスで動く」場合と異なり、ドットで動く場合は下記のような場合も考えられます。下記の場合は、左上頂点だけを見ると移動後も「壁ではない」なので移動できると判定されますが、実際には右上頂点が引っかかります。つまり、左上頂点だけではなく、右上頂点もチェックしなければなりません。

※px+49のように、+50ではないことにも注意! プレーヤーの右上頂点のx座標は、左上頂点のx座標(px)にプレーヤーの横幅(50)を足して1を引いた値になります。

このため、プレーヤーが実際に移動する前に(位置が更新される前に)、 移動先の プレーヤーの 各頂点の位置にあるマス(行・列)を求めて 、そのマスをマップデータで壁かどうかをチェックします。そこが壁ではないなら、プレーヤーの位置を更新します。

なお、「各頂点の位置にあるマスを求めて」としていますが、上下左右のキーで動かすなら、いつも4頂点全部についてチェックする必要はありません。上に動くなら左上と右上だけ、左に動くなら左上と左下だけ、、・・・で大丈夫です。

// 上方向
if (keyCode == UP
&& (map[int((py-speed) / 50)][int(px / 50)] != 1 ) //左上頂点
&& (map[int((py-speed) / 50)][int((px+49) / 50)] != 1 ) ) { //右上頂点
py -= speed;
}
:

やってみよう4★★☆

  • 前述の「壁がある場所に移動できないようにしよう」のサンプルコードでは、UPの場合のみでした。DOWN、LEFT、RIGHTの場合も作成して、プログラムを完成させよう。
  • ゴールに到達したときに、クリアの文字を表示させてみよう

プレーヤーがドットで動く場合

サンプルコード(クリックすると表示されます)
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
float px = 50;
float py = 50;
float speed = 5;
int gi, gj;
boolean clrFlg = false;
void setup() {
size(450, 250);
}
void draw() {
//マップの描画
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
//ゴール判定
if (map[int(py / 50)][int(px / 50)] == 2 ) { //左上頂点
clrFlg = true;
gi = int(py / 50) ;
gj = int(px / 50);
} else if (map[int(py / 50)][int((px+49) / 50)] == 2 ) { //右上頂点
clrFlg = true;
gi = int(py / 50);
gj = int((px+49) / 50);
} else if (map[int((py+49) / 50)][int(px / 50)] == 2 ) { //左下頂点
clrFlg = true;
gi = int((py+49) / 50) ;
gj = int(px / 50) ;
} else if (map[int((py+49) / 50)][int((px+49) / 50)] == 2 ) { //右下頂点
clrFlg = true;
gi = int((py+49) / 50) ;
gj = int((px+49) / 50) ;
}
//プレーヤーの表示
fill(0, 0, 255);
if (clrFlg) {
rect(gj*50+3, gi*50+3, 44, 44);
textSize(80);
text("CLEAR", 120, 120);
} else {
rect(px, py, 50, 50);
}
}
void keyPressed() {
if (!clrFlg) {
// 上方向
if (keyCode == UP
&& (map[int((py-speed) / 50)][int(px / 50)] != 1 ) //左上頂点
&& (map[int((py-speed) / 50)][int((px+49) / 50)] != 1 ) ) { //右上頂点
py -= speed;
}
// 下方向
if (keyCode == DOWN
&& (map[int((py+49+speed) / 50)][int(px / 50)] != 1 ) //左下頂点
&& (map[int((py+49+speed) / 50)][int((px+49) / 50)] != 1 ) ) { //右下頂点
py += speed;
}
// 左方向
if (keyCode == LEFT
&& (map[int(py / 50)][int((px-speed) / 50)] != 1 ) //左上頂点
&& (map[int((py+49) / 50)][int((px-speed) / 50)] != 1 ) ) { //左下頂点
px -= speed;
}
// 右方向
if (keyCode == RIGHT
&& (map[int((py) / 50)][int((px+49+speed) / 50)] != 1 ) //右上頂点
&& (map[int((py+49) / 50)][int((px+49+speed) / 50)] != 1 ) ) { //右下頂点
px += speed;
}
}
}
//注:このサンプルコードは、理解しやすいように簡単に作っています。
//このため、キーでプレーヤーを動かした時にぎこちない動きになります。
//プレーヤーをスムーズに動かすためには後述の「(応用)プレーヤーをスムーズに
//動かす」を参照してください。

(応用)プレーヤーをスムーズに動かす

上記のサンプルコードは、理解しやすいように簡単に作っています。このため、キーでプレーヤーを動かした時に ぎこちない動き になります。プレーヤーをスムーズに動かすには、「キーでスムーズに図形を動かす小技」などを参照して、上記のサンプルコードを改造すると良いでしょう。なお、それに伴いプレーヤーと壁との当たり判定の部分も変更する必要があります。これまでの当たり判定はkeyPressed()関数の中で行っていましたが、スムーズに動かす場合は、当たり判定はdraw()関数の中で行います。

プレーヤをスムーズに動かす

サンプルコード(クリックすると表示されます)
//ドットで移動・当たり判定あり(ゴール判定もあり)・スムーズ
int[][] map =
{{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 1, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 2, 1, 1}};
float px, py, next_px, next_py;
float ue, shita, migi, hidari;
float speed;
int gi, gj; //プレーヤーがゴールに達した時のゴールの位置(行・列)
boolean goalFlg;
void setup() {
size(450, 250);
px = 50; //プレーヤーのスタート位置(x座標)
py = 50; //プレーヤーのスタート位置(y座標)
next_px = px;
next_py = py;
speed = 5;
goalFlg = false;
}
void draw() {
//マップの描画
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 0) {
fill(255, 255, 255);
} else if (map[i][j] == 1) {
fill(255, 0, 0);
} else if (map[i][j] == 2) {
fill(0, 255, 0);
}
rect(j * 50, i * 50, 50, 50); //1マス50の大きさ
}
}
//プレーヤーの上下左右の移動処理
next_px = px + migi + hidari;
next_py = py + ue + shita;
if ((map[int(next_py / 50)][int(next_px / 50)] != 1 ) //左上頂点
&& (map[int(next_py / 50)][int((next_px+49) / 50)] != 1 ) //右上頂点
&& (map[int((next_py+49) / 50)][int(next_px / 50)] != 1 ) //左下頂点
&& (map[int((next_py+49) / 50)][int((next_px+49) / 50)] != 1 ) ) { //右下頂点
px = next_px;
py = next_py;
} else {
next_px = px;
next_py = py;
}
//ゴール判定
if (map[int(py / 50)][int(px / 50)] == 2 ) { //左上頂点
goalFlg = true;
gi = int(py / 50) ;
gj = int(px / 50);
} else if (map[int(py / 50)][int((px+49) / 50)] == 2 ) { //右上頂点
goalFlg = true;
gi = int(py / 50);
gj = int((px+49) / 50);
} else if (map[int((py+49) / 50)][int(px / 50)] == 2 ) { //左下頂点
goalFlg = true;
gi = int((py+49) / 50) ;
gj = int(px / 50) ;
} else if (map[int((py+49) / 50)][int((px+49) / 50)] == 2 ) { //右下頂点
goalFlg = true;
gi = int((py+49) / 50) ;
gj = int((px+49) / 50) ;
}
//プレーヤーの表示&ステージクリア判定
fill(0, 0, 255);
if (goalFlg) {
rect(gj*50+3, gi*50+3, 44, 44);
textSize(80);
text("CLEAR", 120, 120);
noLoop();
} else {
rect(px, py, 50, 50);
}
}
void keyPressed() {
if (!goalFlg) {
// 上方向
if (keyCode == UP) {
ue = -speed;
}
// 下方向
if (keyCode == DOWN) {
shita = speed;
}
// 左方向
if (keyCode == LEFT) {
hidari = -speed;
}
// 右方向
if (keyCode == RIGHT) {
migi = speed;
}
}
}
void keyReleased() {
if (!goalFlg) {
if (keyCode == UP) {
ue = 0;
}
if (keyCode == DOWN) {
shita = 0;
}
if (keyCode == RIGHT) {
migi = 0;
}
if (keyCode == LEFT) {
hidari = 0;
}
}
}

(応用)プレーヤーを画像にしてみよう

  • プレーヤーをrect(四角形図形)ではなく、画像にしてみよう。
  • 動く方向によって画像を切り替えて表示するようにしよう。例えば、右に動く時は右向きの画像、左に動く時は左向きの画像を表示するようにしよう。より本物のゲームらしくなるよ。

(応用)画像での当たり判定を考える

プレーヤーもしくは壁を四角ではないもの、例えば下記のような画像にすると、当たり判定に影響が出てきます。つまり、下記のように、四角の領域は当たっているけど画像は当たっていない場合でも、これまでのやり方では当たりと判定されてしまいます。

Untitled

これを回避するにはかなり複雑な判定処理が必要になるので、ここでは割愛します。興味がある方はネットで検索するなどして皆さんで考えてみてください。いろいろな実現方法がありますが、例えばPImageクラスの様々なメソッド(.get()や.loadPixel()など)が使うと良いかもしれません。自分でオリジナルの当たり判定関数を作ってしまうのも良いでしょう。挑戦してみてください。

(参考)グラフィック座標の図形同士での当たり判定について

この教材では、壁などはマス(行・列)の二次元 配列データ で管理されていて、当たり判定は割と楽に行えました。しかし、壁もドット単位のグラフィック座標(x座標・y座標)で管理される場合は、当たり判定処理がより一層複雑になります。詳しくは「11.当たり判定をしよう」を参照してください。