LoginSignup
9
11

More than 3 years have passed since last update.

【GameMaker:Studio2】疑似3Dダンジョンの作り方

Posted at

この記事では、疑似3Dダンジョンの作り方を紹介します。
疑似3Dダンジョンとは、リアルタイムで3D空間の計算ができない時代に「それっぽい3Dダンジョン」の見た目を擬似的に作っていた方法です。

dungeon.gif

このようなものを作っていきます。

なお、完成したGMS2のプロジェクトファイルは以下のリンクからダウンロード可能です。
http://syun777.sakura.ne.jp/tmp/gms2/DungeonTest.yyz

プロジェクトの新規作成

まずはプロジェクトを新規作成します。
「GameMaker Language」を選びプロジェクトを作成します。
001.png

プロジェクト名は「Test_Dungeon」とします。
002.png

ダンジョン制御オブジェクトの作成

ダンジョンを制御するオブジェクトを作成します。
オブジェクトを作成して、名前を obj_Dungeon とします。
003.png

Createイベントを作成して以下のように記述します。

Createイベント
// ------------------------------
// ①ダンジョンの設定・定義情報
// ------------------------------
// ダンジョンのサイズ (7x9)
dg_width  = 7;
dg_height = 9;

// ミニマップを描画するときのチップあたりのサイズ(px)
minimap_unit = 32;

// 向きの定数
enum eDir {
  None,   // なし
  Left,   // 左
  Top,    // 上
  Right,  // 右
  Bottom, // 下
};

// ダンジョンのチップ情報
enum eChip {
  None, // 何もなし
  Wall, // 壁
};

// ------------------------------
// ②ダンジョンの作成
// ------------------------------
// ds_gridでマップ情報を管理.
map = ds_grid_create(dg_width, dg_height);

// 何もない状態にする.
ds_grid_clear(map, eChip.None);

// 壁を配置.
map[# 1, 2] = eChip.Wall;
map[# 2, 2] = eChip.Wall;
map[# 4, 4] = eChip.Wall;
map[# 5, 3] = eChip.Wall;
map[# 4, 2] = eChip.Wall;

// ------------------------------
// ③プレイヤーの情報を設定
// ------------------------------
// プレイヤーの初期座標を設定
xpos = 0;
ypos = 0;
// 向きを設定
dir = eDir.Left;

少し長いコードなので簡単に説明すると、「①ダンジョンの設定・定義情報」ではダンジョンのサイズ(ここでは 7x9)や方向、マップチップを定義し、「②ダンジョンの作成」で、ds_grid というデータ構造を生成し、壁情報を設定しています。
001.png

「③プレイヤーの情報を設定」はプレイヤーの初期座標と向きを設定しています。

ミニマップの描画 (Draw GUIイベント)

作成手順としては、いきなり疑似3Dダンジョンの描画に入る前に「ミニマップ」を実装するほうが作りやすいです。理由としては、ダンジョンデータやプレイヤーの情報がどのようになっているのかがわかりやすく、うまく動作しなかったときのデバッグがやりやすいからです。
002.png

ということで、ミニマップの描画です。「Draw GUIイベント」を作成し、以下のコードを入力します。

DrawGUIイベント
/// @description ミニマップの描画
draw_set_color(c_silver);

// 1チップあたりのサイズ
var unit   = minimap_unit;

// 描画範囲
var left   = 640; // 左
var top    = 32; // 上
var right  = left + (dg_width  * unit); // 右
var bottom = top  + (dg_height * unit); // 下

// ①区切り線の描画.
for(var i = 0; i < dg_width+1; i++) {
  var x1 = left + (unit * i);
  var y1 = top;
  draw_line(x1, y1, x1, bottom);
}
for(var j = 0; j < dg_height+1; j++) {
  var x1 = left;
  var y1 = top + (unit * j);
  draw_line(x1, y1, right, y1);
}

// ②チップの描画.
for(var j = 0; j < dg_height; j++) {
  for(var i = 0; i < dg_width; i++) {
    var v = map[# i, j];
    if(v == eChip.None) {
      continue;
    }

    // チップの種類に対応する色を設定.
    switch(v) {
    case eChip.Wall:
      // 壁の描画.
      draw_set_color(c_silver);
      break;
    default:
      continue;
    }

    var x1 = left + (unit * i);
    var y1 = top  + (unit * j);
    var x2 = x1 + unit;
    var y2 = y1 + unit;
    draw_rectangle(x1, y1, x2, y2, false);
  }
}

// ③プレイヤーの描画
{
  // 中心座標を計算
  var cx = left + (unit * (xpos + 0.5));
  var cy = top  + (unit * (ypos + 0.5));
  // 現在の向きを角度に変換
  var rot = dir_to_rot(dir);
  var radius = unit / 2;
  var x1 = cx + lengthdir_x(radius, rot); // front x.
  var y1 = cy + lengthdir_y(radius, rot); // front y.
  var x2 = cx + lengthdir_x(radius*1.2, rot-140); // back1 x.
  var y2 = cy + lengthdir_y(radius*1.2, rot-140); // back1 y.
  var x3 = cx + lengthdir_x(radius*1.2, rot+140); // back2 x.
  var y3 = cy + lengthdir_y(radius*1.2, rot+140); // back2 y.
  draw_set_color(c_blue);
  draw_triangle(x1, y1, x2, y2, x3, y3, false);
}

draw_set_color(c_white);

// ---------------------------------------------
// デバッグ情報を描画.
var px = 8;
var py = 8;
var dy = 24;
draw_text(px, py, "x: " + string(xpos)); py += dy;
draw_text(px, py, "y: " + string(ypos)); py += dy;
draw_text(px, py, "direction: " + string(dir)); py += dy;

長めのコードなので、少しずつ説明をしていきます。

1. 区切り線の描画

まずは区切り線の描画です。

DrawGUIイベント
// 1チップあたりのサイズ
var unit   = minimap_unit;

// a. 描画範囲
var left   = 640; // 左
var top    = 32; // 上
var right  = left + (dg_width  * unit); // 右
var bottom = top  + (dg_height * unit); // 下

// ①区切り線の描画.
for(var i = 0; i < dg_width+1; i++) {
  var x1 = left + (unit * i);
  var y1 = top;
  draw_line(x1, y1, x1, bottom);
}
for(var j = 0; j < dg_height+1; j++) {
  var x1 = left;
  var y1 = top + (unit * j);
  draw_line(x1, y1, right, y1);
}

「a. 描画範囲」でミニマップがどの範囲内に描画されるかを計算しています。
003.png

(left, top) が左上座標で、(right, bottom) が右下の座標です。この範囲を決めることで、区切り線の長さを決定できることになります。

  • left = 640
  • top = 32
  • right = 640 + (7 * 32) = 864
  • bottom = 32 + (9 * 32) = 320

となり、(640,32)から(864,320)の領域がミニマップを描画する範囲となります。

2. 壁の描画

つづけてチップの描画を見ていきます。

DrawGUIイベント
// ②チップの描画.
for(var j = 0; j < dg_height; j++) {
  for(var i = 0; i < dg_width; i++) {
    var v = map[# i, j];
    if(v == eChip.None) {
      continue;
    }

    // チップの種類に対応する色を設定.
    switch(v) {
    case eChip.Wall:
      // 壁の描画.
      draw_set_color(c_silver);
      break;
    default:
      continue;
    }

    var x1 = left + (unit * i);
    var y1 = top  + (unit * j);
    var x2 = x1 + unit;
    var y2 = y1 + unit;
    draw_rectangle(x1, y1, x2, y2, false);
  }
}

マップの幅 (dg_width) と高さ (`dg_height') をループで回して、ダンジョンマップの各データを調べます。

for(var j = 0; j < dg_height; j++) {
  for(var i = 0; i < dg_width; i++) {
    var v = map[# i, j];

ここの v の値が壁 (eChip.Wall) であれば壁チップとして灰色の四角形を描きます。
もし今後、階段やアイテム、敵などを配置する場合は

    // チップの種類に対応する色を設定.
    switch(v) {
    case eChip.Wall:
      // 壁の描画.
      draw_set_color(c_silver);
      break;
    default:
      continue;
    }

ここに分岐を入れて色を変えたり、別途用意したアイコン画像を描画するようにします。
004.png

3.プレイヤーの描画

続けてプレイヤーの描画です。プレイヤー三角形とし、先端の部分を正面として描画します。

DrawGUIイベント
// プレイヤーの描画
{
  // ①中心座標を計算
  var cx = left + (unit * (xpos + 0.5));
  var cy = top  + (unit * (ypos + 0.5));
  // ②向きを角度に変換
  var rot = dir_to_rot(dir);
  var radius = unit / 2;
  var x1 = cx + lengthdir_x(radius, rot); // 正面X.
  var y1 = cy + lengthdir_y(radius, rot); // 正面Y.
  var x2 = cx + lengthdir_x(radius*1.2, rot-140); // 右後方X.
  var y2 = cy + lengthdir_y(radius*1.2, rot-140); // 右後方Y.
  var x3 = cx + lengthdir_x(radius*1.2, rot+140); // 左後方X.
  var y3 = cy + lengthdir_y(radius*1.2, rot+140); // 左後方Y.
  draw_set_color(c_blue);
  // ③それぞれの頂点を結んだ三角形を描画
  draw_triangle(x1, y1, x2, y2, x3, y3, false);
}

疑似3Dダンジョン.png

  1. 中心を決めて正面の位置を求める
  2. 正面から±140°の方向にある位置を求める
  3. それぞれの頂点を結ぶ

ここで使用している dir_to_rot() は以下の実装となります。

dir_to_rot()
/// @description 向きを角度に変換する
/// @param 向き(eDir)
/// @return 向きに対応する角度
var dir = argument0; // 方向 (eDir).
switch(dir) {
case eDir.Left:   return 180; // 左
case eDir.Top:    return 90;  // 上
case eDir.Right:  return 0;   // 右
case eDir.Bottom: return 270; // 下
default:          return -1;
}

向き(eDir)に対応する角度を返すスクリプトとなります。

プレイヤーの旋回・前進と後退 (Stepイベント)

次にプレイヤーの移動処理を作っていきます。
Stepイベントに以下のように記述します。

Stepイベント
/// @description プレイヤーの移動処理
// 現在の位置を保持しておく
var xnext = xpos;
var ynext = ypos;
if(keyboard_check_pressed(vk_left)) {
  // 左を向く
  dir = dir_turn(dir, true);
}
else if(keyboard_check_pressed(vk_right)) {
  // 右を向く
  dir = dir_turn(dir, false);
}
else if(keyboard_check_pressed(vk_up)) {
  // 前進する.
  var vec = dir_to_vec(dir);
  xnext += vec[0];
  ynext += vec[1];
}
else if(keyboard_check_pressed(vk_down)) {
  // 後退する.
  var vec = dir_to_vec(dir);
  xnext -= vec[0];
  ynext -= vec[1];
}

if(xpos != xnext or ypos != ynext) {
  // 座標に変化があったので移動したことになる
  if(0 <= xnext and xnext < dg_width and 0 <= ynext and ynext < dg_height) {
    // マップの領域内
    var v = map[# xnext, ynext];
    if(v != eChip.Wall) {
      // move position.
      xpos = xnext;
      ypos = ynext;
    }
    else {
      // can't move.
      show_debug_message("opps!");
    }
  }
}

少し長いので、「①向きの旋回」と「②前進・後退」を分けて説明します。

1. 向きの旋回

左右キーを押したときに、現在の向きに対応する向きに旋回を行います。

Stepイベント
if(keyboard_check_pressed(vk_left)) {
  // 左を向く
  dir = dir_turn(dir, true);
}
else if(keyboard_check_pressed(vk_right)) {
  // 右を向く
  dir = dir_turn(dir, false);
}

dir_turn() スクリプトは以下の実装となります。

dir_turn()
/// @description 向きを左右に旋回したときの向きを返す
/// @param dir 現在の向き
/// @param is_left 左回りかどうか (false: 右回り)
var dir = argument0; // 現在の向き
var is_left = argument1; // 左回りかどうか

if(is_left) {
  // 左を向く.
  switch(dir) {
  case eDir.Left:  return eDir.Bottom;
  case eDir.Top:   return eDir.Left;
  case eDir.Right: return eDir.Top;
  default:         return eDir.Right;
  }
}
else {
  // 右を向く.
  switch(dir) {
  case eDir.Left:  return eDir.Top;
  case eDir.Top:   return eDir.Right;
  case eDir.Right: return eDir.Bottom;
  default:         return eDir.Left;
  }
}

2. 前進・後退

前進・後退の実装については、まずは移動後の座標を計算します。

Stepイベント
// 現在の位置を保持しておく
var xnext = xpos;
var ynext = ypos;
if(keyboard_check_pressed(vk_up)) {
  // 前進する.
  var vec = dir_to_vec(dir);
  xnext += vec[0];
  ynext += vec[1];
}
else if(keyboard_check_pressed(vk_down)) {
  // 後退する.
  var vec = dir_to_vec(dir);
  xnext -= vec[0];
  ynext -= vec[1];
}

上キーで前進、下キーで後退です。向きに対応する移動量の取得のために dir_to_vec() というスクリプトを用意しています。
dir_to_vec() の実装は以下の通りです。

dir_to_vec()
/// @description 向きから移動ベクトルを取得 (array[0]:x, array[1]:y)
/// @param dir 現在の向き
/// @return array([0]:x, [1]:y) 移動ベクトル
var dir = argument0; // 現在の向き
switch(dir) {
case eDir.Left:   return [-1, 0];
case eDir.Top:    return [0, -1];
case eDir.Right:  return [1, 0];
case eDir.Bottom: return [0, 1];
default:          return [0, 0];
}

左に進む場合は (x, y) = (-1, 0) の移動量となります。上の場合は (x, y) = (0, -1)、右は (x, y) = (1, 0) 下は (x, y) = (0, 1) となります。

次に、移動先がプレイヤーの進める場所かどうかをチェックしています。

Stepイベント
if(xpos != xnext or ypos != ynext) {
  // 座標に変化があったので移動したことになる
  if(0 <= xnext and xnext < dg_width and 0 <= ynext and ynext < dg_height) {
    // マップの領域内
    var v = map[# xnext, ynext];
    if(v != eChip.Wall) {
      ……
  1. 新しい座標(xnext, ynext)が前回(xpos, ypos)と同じ座標でない
  2. 移動先がダンジョンの範囲内 (0 <= xnext < dg_width, 0 <= ynext < dg_height) である
  3. 移動先が壁ではない

この3つの条件を満たしている場合のみ移動可能(移動する)で、(xpos, ypos) に移動先の座標を設定しています。

疑似3Dダンジョンの壁の描画 (Drawイベント)

ここまでできたら、ようやく疑似3Dダンジョンの描画ができます。

壁のデータ

壁のデータはDrawイベントに以下のように定義します。

Drawイベント
// 壁の描画情報を定義
var back_left     = [0, 5, 5, 5, 6, 6, 6,10, 5,11, 0,11]
var back_center   = [5, 5,11, 5,11,11, 5,11]
var back_right    = [10,6,11, 5,16, 5,16,11,11,11,10,10]
var middle_left   = [0, 3, 3, 3, 5, 5, 5,11, 3,13, 0,13]
var middle_center = [3, 3,13, 3,13,13, 3,13]
var middle_right  = [11,5,13, 3,16, 3,16,13,13,13,11,11];
var front_left    = [0, 0, 3, 3, 3,13, 0,16];
var front_center  = [0, 0,16, 0,16,16, 0,16]; // YOU ARE IN ROCK!
var front_right   = [13,3,16, 0,16,16,13,13];

// 通路の定義
var bg_back  = [0, 0,16, 0,16,10, 0,10];
var bg_floor = [6,10,10,10,16,16, 0,16];

唐突にたくさんの数字を定義していますが、これには理由があります。

0011.png

これは、視界を奥「2」まで、左右を「1」までとしたものです。この場合視界は、3×3マスで表現することができます。そして、0~2が奥、3~5が中距離、6・8が近く、となります。遠近法を使えば、近くにあれば大きく、遠くにあるほど小さく映るので、一番近い壁を「3」、中距離の壁を「2」、遠い壁を「1」の比率で考えます。

この図に合わせて壁を作っていきます。

// 1単位あたりのサイズ(px).
var wall_unit = 32;

// ①壁リストの作成.
var wall_list = [
  back_left, back_center, back_right,
  middle_left, middle_center, middle_right,
  front_left, front_center, front_right
];

// ②視点の取得
var view_tbl = dir_get_viewtbl(dir);

// ③壁の描画
for(var i = 0; i < array_length_1d(view_tbl); i+=2) {
  var px = xpos + view_tbl[i];
  var py = ypos + view_tbl[i+1];
  var is_out_range = (px < 0 || dg_width <= px || py < 0 || dg_height <= py);
  if(map[# px, py] == eChip.Wall or is_out_range) {
    var idx = i/2;
    var brightness = 48 + (16 * floor(idx/3)); // (0,1,2 => 48) (3,4,5 => 64) (6,7,8 => 80)
    var color = make_color_hsv(0, 0, brightness);
    draw_set_color(color);
    var wall = wall_list[idx];
    dg_draw_wall_from_array(0, 0, wall, wall_unit);
  }
}

draw_set_color(c_white);

少し難しい処理をしているので細かく説明していきます。

まず、①の部分では視界の位置に対応する壁を配列 wall_list に入れています。

var wall_list = [
  back_left /* 0 */, back_center /* 1 */, back_right /* 2 */,
  middle_left /* 3 */, middle_center /* 4 */, middle_right /* 5 */,
  front_left /* 6 */, front_center /* 7 */, front_right /* 8 */
];

名称未設定.png

②では現在の向き dir に対応する視界の情報を dir_get_viewtbl() スクリプトから取得しています。

dir_get_viewtbl()
/// @description 視界テーブルの取得
/// @param dir 向き(eDir)
/// @return viewtbl 視界テーブル
var dir = argument0; // 向き
switch(dir) {
case eDir.Left:
  return [
    -2,  1,-2,  0,-2, -1,
    -1,  1,-1,  0,-1, -1,
     0,  1, 0,  0, 0, -1
  ];

case eDir.Top:
  return [
    -1, -2, 0, -2, 1, -2,
    -1, -1, 0, -1, 1, -1,
    -1,  0, 0,  0, 1,  0
  ];

case eDir.Right:
  return [
     2, -1, 2,  0, 2,  1,
     1, -1, 1,  0, 1,  1,
     0, -1, 0,  0, 0,  1
  ];

case eDir.Bottom:
default:
  return [
     1,  2, 0,  2,-1,  2,
     1,  1, 0,  1,-1,  1,
     1,  0, 0,  0,-1,  0
  ];
}

名称未設定.png

例えば向きが eDir.Top であれば、

case eDir.Top:
  return [
    -1, -2, 0, -2, 1, -2, // (-1, -2), (0, -2), (1, -2)
    -1, -1, 0, -1, 1, -1, // (-1, -1), (0, -1), (1, -1)
    -1,  0, 0,  0, 1,  0  // (-1,  0), (0,  0), (1,  0)
  ];

となっており、向きに対応する現在の位置からの相対距離を取得できるスクリプトとなっています。

最後に③です。
ここでは先程取得した視界テーブル (view_tbl) からチェックする位置を求め、それが壁であれば dg_draw_wall_from_array() で壁を描画します。
描画を行っている dg_draw_wall_from_array() は以下のスクリプトです。

/// @description 壁の描画
/// @param xbase 基準座標(X)
/// @param ybase 基準座標(Y)
/// @param arrray 頂点配列
/// @param wall_unit 頂点の単位(px)
var xbase     = argument0; // 基準座標(X)
var ybase     = argument1; // 基準座標(Y)
var array     = argument2; // 頂点配列
var wall_unit = argument3; // 頂点の単位(px)

// 最初の頂点.
var x1 = array[0]; var y1 = array[1];
// 次の頂点.
var x2 = array[2]; var y2 = array[3];
for(var i = 4; i < array_length_1d(array); i+=2) {
  // 三番目の頂点.
  var x3 = array[i];
  var y3 = array[i+1];

  // 三角形を描画
  draw_triangle(
    xbase + x1*wall_unit, ybase + y1*wall_unit, 
    xbase + x2*wall_unit, ybase + y2*wall_unit, 
    xbase + x3*wall_unit, ybase + y3*wall_unit, 
  false);

  // triangle fan.
  // 三番目の頂点は次の二番目の頂点になる
  x2 = x3;
  y2 = y3;
}

ここの描画の仕組みについて説明します。例えば、以下のような6角形を描画するとします。
005.png

この頂点の先頭から順番に三角形を描いていきます。
006.png

次の三角形は前回の1番目・3番目の頂点を使って描画します。
007.png

このように前回の三角形の3番目の頂点が、次の三角形の2番目の頂点となるため、以下の記述で頂点の入れ替えを行っています。

  // Triangle fan.
  x2 = x3; // 3番目を次の2番目として使用する
  y2 = y3; // 3番目を次の2番目として使用する

これが多角形の壁の描画方法のしくみとなります。
ここからは壁の定義情報についての説明となります。

0(左・奥)の壁

まずは左側の奥にある壁です。
003-1.png

先程のコードで、

Drawイベント
var back_left = [0, 5, 5, 5, 6, 6, 6,10, 5,11, 0,11]

という定義をしましたが、これが「0(左・奥)」の壁です。

  • (0, 5), (5, 5), (6, 6), (6, 10), (5, 11), (0, 11)

左上の頂点から右回りに番号を定義しています。

1(真ん中・奥) の壁

004.png

1(真ん中・奥)の壁は以下の配列です。

Drawイベント
var back_center = [5, 5,11, 5,11,11, 5,11]

この配列が真ん中・奥の壁の頂点データとなります。

2(右・奥) の壁

005.png

2(右・奥)の壁は以下の配列です。

Drawイベント
var back_right = [10,6,11, 5,16, 5,16,11,11,11,10,10]

3(左・中) の壁

006.png

4(真ん中・中) の壁

007.png

5(右・中) の壁

0081.png

6(左・手前) の壁

009.png

7(中・手前) の壁

この壁は通常描画されません。理由はプレイヤーが現在いる座標だからです。

8(右・手前) の壁

0101.png

通路の描画

見た目を良くする方法として、通路を描画します。
002-1.png

壁だけ描画すると、足元や奥行きがわかりにくいので通路を描画するとよいです。これは最背面(壁よりも後ろ)に常に描画します。

var bg_back  = [0, 0,16, 0,16,10, 0,10];
var bg_floor = [6,10,10,10,16,16, 0,16];

// size.
var wall_unit = 32;

// 通路を描画する.
draw_set_color(make_color_hsv(0, 0, 24));
dg_draw_wall_from_array(0, 0, bg_back, wall_unit);
dg_draw_wall_from_array(0, 0, bg_floor, wall_unit);

Drawイベントのコードをすべてまとめたもの

実装順が前後してしまったので、Drawイベントをすべてまとめたものは以下の通りとなります。

Drawイベント
/// @description Draw Dungeon.
var back_left     = [0, 5, 5, 5, 6, 6, 6,10, 5,11, 0,11]
var back_center   = [5, 5,11, 5,11,11, 5,11]
var back_right    = [10,6,11, 5,16, 5,16,11,11,11,10,10]
var middle_left   = [0, 3, 3, 3, 5, 5, 5,11, 3,13, 0,13]
var middle_center = [3, 3,13, 3,13,13, 3,13]
var middle_right  = [11,5,13, 3,16, 3,16,13,13,13,11,11];
var front_left    = [0, 0, 3, 3, 3,13, 0,16];
var front_center  = [0, 0,16, 0,16,16, 0,16]; // YOU ARE IN ROCK!
var front_right   = [13,3,16, 0,16,16,13,13];

var bg_back  = [0, 0,16, 0,16,10, 0,10];
var bg_floor = [6,10,10,10,16,16, 0,16];

// size.
var wall_unit = 32;

// draw passage.
draw_set_color(make_color_hsv(0, 0, 24));
dg_draw_wall_from_array(0, 0, bg_back, wall_unit);
dg_draw_wall_from_array(0, 0, bg_floor, wall_unit);

// draw wall.
var wall_list = [
  back_left, back_center, back_right,
  middle_left, middle_center, middle_right,
  front_left, front_center, front_right
];

var view_tbl = dir_get_viewtbl(dir);

for(var i = 0; i < array_length_1d(view_tbl); i+=2) {
  var px = xpos + view_tbl[i];
  var py = ypos + view_tbl[i+1];
  var is_out_range = (px < 0 || dg_width <= px || py < 0 || dg_height <= py);
  if(map[# px, py] == eChip.Wall or is_out_range) {
    var idx = i/2;
    var brightness = 48 + (16 * floor(idx/3)); // (0,1,2 => 48) (3,4,5 => 64) (6,7,8 => 80)
    var color = make_color_hsv(0, 0, brightness);
    draw_set_color(color);
    var wall = wall_list[idx];
    dg_draw_wall_from_array(0, 0, wall, wall_unit);
  }
}

draw_set_color(c_white);

少し説明が煩雑になってしまったので、もし、うまく動かない場合は、こちらのプロジェクトをダウンロードして比較していみるとよいかもしれません
http://syun777.sakura.ne.jp/tmp/gms2/DungeonTest.yyz

改造ポイント

最後に、このコードをどのように改造していくと良いのかの方針です。

生成されるダンジョンのサイズを変えたい

サンプルでは、Createイベントで生成してしまっているので、例えばステージごとにダンジョンの壁の位置を変えたい場合に工夫が必要です。

方法としては以下が考えられます。

  1. globalにステージ番号を設定してから instance_create_*() を呼び出す
  2. 状態変数をもたせて、Stepイベントでダンジョン生成を行う

1の方法は、ダンジョンを生成する処理を以下のように記述します。

// ダンジョンのステージ番号を1に設定
global.dungeon_stage = 1;

// ダンジョンを生成
instance_create_depth(0, 0, 0, obj_Dungeon);

そして、obj_Dugeon の Createイベントで以下のように記述します。

obj_DugeonのCreateイベント
switch(global.dungeon_stage) {
case 1:
  // ステージ1の生成処理.
  ・・・
  break;
case 2:
  // ステージ2の生成処理.
  ・・・
  break;

caes 3:
  // ステージ3の生成処理.
  ・・・
  break;
}

2の方法としては、globalを使わずにダンジョン生成タイミングを遅らせることで対応します。

obj_DungeonのCreateイベント

enum eState {
  CreateDungeon, // ダンジョン生成.
  Main, // メイン処理.
}

state = eState.CreateDungeon;
stage = 1;
obj_DungeonのStepイベント
switch(state) {
case eState.CreateDungeon:
  // ダンジョンの生成処理
  switch(stage) {
  case 1: // ステージ1の生成.
    ・・・
    break;
  case 2: // ステージ2の生成.
    ・・・
    break;
  }
  state = eState.Main; // メイン処理へ

case eState.Main:
  // 旋回、前進、後退処理.
  ・・・
  break;
}

このように Createイベントではなく、Stepイベントで生成処理をすると、外部からパラメータを渡してダンジョンを生成することができます。

// 呼び出し側
var _inst = instance_create_depth(0, 0, 0, obj_Dungeon);
_inst.stage = 2; // ステージ番号の設定 

ダンジョンを自動生成する

ローグライクのようにダンジョンを自動生成する場合は、穴掘り法が楽に実装できます。

上記のリンクは Unity(C#) で実装されていますが、わかりやすく図解されているので理解するのは難しくないと思います。

また、トルネコのように部屋をしっかりと分けていく方法としては「区域分割法」というのがありますが、こちらはなかなか複雑なのでプログラムに慣れてから挑戦したほうがよいかもしれません。

バトルやフロア移動を実装する

バトルやフロア移動を実装するには、状態遷移(FSM)を使って状態を切り替えるようにすると実装しやすいです。

例えば以下の定数を定義しておきます。

Createイベント
enum eState {
  CreateDungeon, // ダンジョン生成
  Move, // 移動処理
  Battle, // 戦闘
  NextFloor, // 次のフロアに進む
};

// 初期状態を設定
state = eState.CreateDungeon;

この状態変数(state)を使用して、Stepイベントで以下のように状態遷移させます。
疑似3Dダンジョン.png

Stepイベント
switch(state) {
case eState.CreateDungeon: // ダンジョン生成.
  // ダンジョン生成処理.
  state = eStateMove;
  break;

case eState.Move: // 移動処理.
  // 移動処理.
  // エンカウント判定.
  if(/* エンカウント判定成立 */) {
    // 敵にエンカウントした.
    state = eState.Battle;
    break;
  }
  // 階段チェック.
  else if(/* 階段接触判定 */) {
    // 階段に接触した.
    state = eState.NextFloor;
    break;
  }
  break;

case eState.Battle:
  // バトル処理.
  if(/* バトル終了 */) {
    // バトル終了.
    state = eState.Move;
  }
  break;

case eState.NextFloor:
  // 次のフロアに進む
  instance_destroy();
  break;
}

9
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11