PECTONG
@PECTONG

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

自動生成マップの床と壁の境界線を描画したい

解決したいこと

現在2Dのローグライク(ローグライト)のゲームを作っています。
自動生成のマップでマップチップを利用して壁と床の境界線を表現したいのですがうまくいきません。
例えば、壁が上と、床が右、左、下と接している壁のマップチップは
UP.png
壁が上、右と、床が左、下と接している壁のマップチップは
UPRIGHT.png
壁が上、左、下と、床が右と接している壁のマップチップは
UPLEFTDOWN.png
全ての面が壁に接している壁のマップチップは
CENTER.png
床のマップチップは
FLOOR.png
といったような具合です。

一通りGoogleなどで調べた結果、
https://tanisugames.com/thin_wall_maze/
このページで紹介されているビットフィールド(ビットマスク)が適切な手法というところまではわかったんですが、このページを参考にソースコードを書き換えてもうまくいきません。
おそらく上記のページで紹介されているソースコードは薄い壁を作成するアルゴリズムであるということと、自分の作成した方向を決める式(下記「ダンジョン自動生成スクリプト」内384行目「void MazeToHex()」参照)が間違っているのが問題点と思われます。

具体的な解決方法(ソースコードなど)を教えていただけるとありがたいです。
何卒ご教授よろしくお願いします。

ダンジョン自動生成スクリプト

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MapGenerator
{

	private const int MINIMUM_RANGE_WIDTH = 15;

	private int mapSizeX;
	private int mapSizeY;
	private int maxRoom;
	public object Value { get; set; }
	private List<Range> roomList = new List<Range>();
	private List<Range> rangeList = new List<Range>();
	private List<Range> passList = new List<Range>();
	private List<Range> roomPassList = new List<Range>();

	private bool isGenerated = false;
	string[,] hexMaze { get; set; }
	public string[,] HexMaze { get { return hexMaze; } }
	int[,] map;
	public MapGenerator(int mapSizeX, int mapSizeY)
	{
		this.mapSizeX = mapSizeX;
		this.mapSizeY = mapSizeY;

		map = new int[mapSizeX, mapSizeY];

		
	}
		public int[,] GenerateMap(int maxRoom)
	{
		MazeToHex();
		
		CreateRange(maxRoom);
		CreateRoom();

		// ここまでの結果を一度配列に反映する
		foreach (Range pass in passList)
		{
			for (int x = pass.Start.X; x <= pass.End.X; x++)
			{
				for (int y = pass.Start.Y; y <= pass.End.Y; y++)
				{
					map[x, y] = 1;
				}
			}
		}
		foreach (Range roomPass in roomPassList)
		{
			for (int x = roomPass.Start.X; x <= roomPass.End.X; x++)
			{
				for (int y = roomPass.Start.Y; y <= roomPass.End.Y; y++)
				{
					map[x, y] = 1;
				}
			}
		}
		foreach (Range room in roomList)
		{
			for (int x = room.Start.X; x <= room.End.X; x++)
			{
				for (int y = room.Start.Y; y <= room.End.Y; y++)
				{
					map[x, y] = 1;
				}
			}
		}

		TrimPassList(ref map);
		
		return map;

	}

	public void CreateRange(int maxRoom)
	{
		// 区画のリストの初期値としてマップ全体を入れる
		rangeList.Add(new Range(0, 0, mapSizeX - 1, mapSizeY - 1));

		bool isDevided;
		do
		{
			// 縦 → 横 の順番で部屋を区切っていく。一つも区切らなかったら終了
			isDevided = DevideRange(false);
			isDevided = DevideRange(true) || isDevided;

			// もしくは最大区画数を超えたら終了
			if (rangeList.Count >= maxRoom)
			{
				break;
			}
		} while (isDevided);

	}

	public bool DevideRange(bool isVertical)
	{
		bool isDevided = false;

		// 区画ごとに切るかどうか判定する
		List<Range> newRangeList = new List<Range>();
		foreach (Range range in rangeList)
		{
			// これ以上分割できない場合はスキップ
			if (isVertical && range.GetWidthY() < MINIMUM_RANGE_WIDTH * 2 + 1)
			{
				continue;
			}
			else if (!isVertical && range.GetWidthX() < MINIMUM_RANGE_WIDTH * 2 + 1)
			{
				continue;
			}

			System.Threading.Thread.Sleep(1);

			// 40%の確率で分割しない
			// ただし、区画の数が1つの時は必ず分割する
			if (rangeList.Count > 1 && RogueUtils.RandomJadge(0.5f))
			{
				continue;
			}

			// 長さから最少の区画サイズ2つ分を引き、残りからランダムで分割位置を決める
			int length = isVertical ? range.GetWidthY() : range.GetWidthX();
			int margin = length - MINIMUM_RANGE_WIDTH * 2;
			int baseIndex = isVertical ? range.Start.Y : range.Start.X;
			int devideIndex = baseIndex + MINIMUM_RANGE_WIDTH + RogueUtils.GetRandomInt(1, margin) - 1;

			// 分割された区画の大きさを変更し、新しい区画を追加リストに追加する
			// 同時に、分割した境界を通路として保存しておく
			Range newRange = new Range();
			if (isVertical)
			{
				passList.Add(new Range(range.Start.X, devideIndex, range.End.X, devideIndex));
				newRange = new Range(range.Start.X, devideIndex + 1, range.End.X, range.End.Y);
				range.End.Y = devideIndex - 1;
			}
			else
			{
				passList.Add(new Range(devideIndex, range.Start.Y, devideIndex, range.End.Y));
				newRange = new Range(devideIndex + 1, range.Start.Y, range.End.X, range.End.Y);
				range.End.X = devideIndex - 1;
			}

			// 追加リストに新しい区画を退避する。
			newRangeList.Add(newRange);

			isDevided = true;
		}

		// 追加リストに退避しておいた新しい区画を追加する。
		rangeList.AddRange(newRangeList);

		return isDevided;
	}

	private void CreateRoom()
	{
		// 部屋のない区画が偏らないようにリストをシャッフルする
		rangeList.Sort((a, b) => RogueUtils.GetRandomInt(0, 1) - 1);

		// 1区画あたり1部屋を作っていく。作らない区画もあり。
		foreach (Range range in rangeList)
		{
			System.Threading.Thread.Sleep(1);
			// 30%の確率で部屋を作らない
			// ただし、最大部屋数の半分に満たない場合は作る
			if (roomList.Count > maxRoom / 2 && RogueUtils.RandomJadge(0f))
			{
				continue;
			}

			// 猶予を計算
			int marginX = range.GetWidthX() - MINIMUM_RANGE_WIDTH + 1;
			int marginY = range.GetWidthY() - MINIMUM_RANGE_WIDTH + 1;

			// 開始位置を決定
			int randomX = RogueUtils.GetRandomInt(1, marginX);
			int randomY = RogueUtils.GetRandomInt(1, marginY);

			// 座標を算出
			int startX = range.Start.X + randomX;
			int endX = range.End.X - RogueUtils.GetRandomInt(0, (marginX - randomX)) - 1;
			int startY = range.Start.Y + randomY;
			int endY = range.End.Y - RogueUtils.GetRandomInt(0, (marginY - randomY)) - 1;

			// 部屋リストへ追加
			Range room = new Range(startX, startY, endX, endY);
			roomList.Add(room);

			// 通路を作る
			CreatePass(range, room);
		}
	}

	private void CreatePass(Range range, Range room)
	{
		List<int> directionList = new List<int>();
		if (range.Start.X != 0)
		{
			// Xマイナス方向
			directionList.Add(0);
		}
		if (range.End.X != mapSizeX - 1)
		{
			// Xプラス方向
			directionList.Add(1);
		}
		if (range.Start.Y != 0)
		{
			// Yマイナス方向
			directionList.Add(2);
		}
		if (range.End.Y != mapSizeY - 1)
		{
			// Yプラス方向
			directionList.Add(3);
		}

		// 通路の有無が偏らないよう、リストをシャッフルする
		directionList.Sort((a, b) => RogueUtils.GetRandomInt(0, 1) - 1);

		bool isFirst = true;
		foreach (int direction in directionList)
		{
			System.Threading.Thread.Sleep(1);
			// 80%の確率で通路を作らない
			// ただし、まだ通路がない場合は必ず作る
			if (!isFirst && RogueUtils.RandomJadge(0f))
			{
				continue;
			}
			else
			{
				isFirst = false;
			}

			// 向きの判定
			int random;
			switch (direction)
			{
				case 0: // Xマイナス方向
					random = room.Start.Y + RogueUtils.GetRandomInt(1, room.GetWidthY()) - 1;
					roomPassList.Add(new Range(range.Start.X, random, room.Start.X - 1, random));
					break;

				case 1: // Xプラス方向
					random = room.Start.Y + RogueUtils.GetRandomInt(1, room.GetWidthY()) - 1;
					roomPassList.Add(new Range(room.End.X + 1, random, range.End.X, random));
					break;

				case 2: // Yマイナス方向
					random = room.Start.X + RogueUtils.GetRandomInt(1, room.GetWidthX()) - 1;
					roomPassList.Add(new Range(random, range.Start.Y, random, room.Start.Y - 1));
					break;

				case 3: // Yプラス方向
					random = room.Start.X + RogueUtils.GetRandomInt(1, room.GetWidthX()) - 1;
					roomPassList.Add(new Range(random, room.End.Y + 1, random, range.End.Y));
					break;
			}
		}

	}

	private void TrimPassList(ref int[,] map)
	{
		
		// どの部屋通路からも接続されなかった通路を削除する
		for (int i = passList.Count - 1; i >= 0; i--)
		{
			Range pass = passList[i];

			bool isVertical = pass.GetWidthY() > 1;

			// 通路が部屋通路から接続されているかチェック
			bool isTrimTarget = true;
			if (isVertical)
			{
				int x = pass.Start.X;
				for (int y = pass.Start.Y; y <= pass.End.Y; y++)
				{
					if (map[x - 1, y] == 1 || map[x + 1, y] == 1)
					{
						isTrimTarget = false;
						break;
					}
				}
			}
			else
			{
				int y = pass.Start.Y;
				for (int x = pass.Start.X; x <= pass.End.X; x++)
				{
					if (map[x, y - 1] == 1 || map[x, y + 1] == 1)
					{
						isTrimTarget = false;
						break;
					}
				}
			}

			// 削除対象となった通路を削除する
			if (isTrimTarget)
			{
				passList.Remove(pass);

				// マップ配列からも削除
				if (isVertical)
				{
					int x = pass.Start.X;
					for (int y = pass.Start.Y; y <= pass.End.Y; y++)
					{
						map[x, y] = 0;
					}
				}
				else
				{
					int y = pass.Start.Y;
					for (int x = pass.Start.X; x <= pass.End.X; x++)
					{
						map[x, y] = 0;
					}
				}
			}
		}

		// 外周に接している通路を別の通路との接続点まで削除する
		// 上下基準
		for (int x = 0; x < mapSizeX - 1; x++)
		{
			if (map[x, 0] == 1)
			{
				for (int y = 0; y < mapSizeY; y++)
				{
					if (map[x - 1, y] == 1 || map[x + 1, y] == 1)
					{
						break;
					}
					map[x, y] = 0;
				}
			}
			if (map[x, mapSizeY - 1] == 1)
			{
				for (int y = mapSizeY - 1; y >= 0; y--)
				{
					if (map[x - 1, y] == 1 || map[x + 1, y] == 1)
					{
						break;
					}
					map[x, y] = 0;
				}
			}
		}
		// 左右基準
		for (int y = 0; y < mapSizeY - 1; y++)
		{
			if (map[0, y] == 1)
			{
				for (int x = 0; x < mapSizeY; x++)
				{
					if (map[x, y - 1] == 1 || map[x, y + 1] == 1)
					{
						break;
					}
					map[x, y] = 0;
				}
			}
			if (map[mapSizeX - 1, y] == 1)
			{
				for (int x = mapSizeX - 1; x >= 0; x--)
				{
					if (map[x, y - 1] == 1 || map[x, y + 1] == 1)
					{
						break;
					}
					map[x, y] = 0;
				}
			}
		}
	}
	void MazeToHex()
	{

		hexMaze = new string[(mapSizeX - 1), (mapSizeY - 1)];
		for (int y = 1; y < mapSizeY - 1; y += 2)
		{
			for (int x = 1; x < mapSizeX - 1; x += 2)
			{
				int cellValue = 0;
				// 上のセルを確認
				if (y > 0 && map[y - 1, x] == 0) cellValue += 1;
					// 右のセルを確認
				if (x < mapSizeX - 1 && map[y, x + 1] == 0) cellValue += 2;
				if (y > 0 && x < mapSizeX - 1 && map[y - 1, x + 1] == 0) cellValue += 3;
				// 下のセルを確認
				if (y < mapSizeY - 1 && map[y + 1, x] == 0) cellValue += 4;
				if (y > 0 && y < mapSizeY - 1 && map[y , x] == 0) cellValue += 5;
				if (x < mapSizeX - 1 && y < mapSizeY - 1 && map[y + 1, x + 1] == 0) cellValue += 6;
				if (y > 0 && x < mapSizeX - 1 && y < mapSizeY - 1 && map[y, x + 1] == 0) cellValue += 7;
				// 左のセルを確認
				if (x > 0 && map[y, x - 1] == 0) cellValue += 8;
				if (y > 0 && x > 0 && map[y - 1, x - 1] == 0) cellValue += 9;
				if (x < mapSizeX - 1 && x > 0 && x > 0 && map[y, x] == 0) cellValue += 10;
				if (y > 0 && x < mapSizeX - 1 && x > 0 && map[y - 1, x] == 0) cellValue += 11;
				if (y < mapSizeY - 1 && map[y + 1, x] == 0 && x > 0 && map[y, x - 1] == 0) cellValue += 12;
				if (y > 0 && y < mapSizeY - 1 && x > 0 && x > 0 && map[y , x - 1] == 0) cellValue += 13;
				if (x < mapSizeX - 1 && y < mapSizeY - 1 && x > 0 && map[y + 1, x] == 0) cellValue += 14;
				if (y > 0 && y < mapSizeY - 1 && x < mapSizeX - 1 && x > 0 && map[y, x] == 0) cellValue += 15;
				hexMaze[(x - 1), (y - 1)] = cellValue.ToString("X");
				

			}
		}
	}

}

ダンジョン描画スクリプト

using System;
using UnityEngine;
using System.Collections.Generic;
using UniRandom = UnityEngine.Random;

public class SceneInitializer : MonoBehaviour
{
    
    MapGenerator maze;
    public GameObject groundPrefab, ground2Prefab;
    public GameObject[] wallPrafabs;
    [SerializeField] int mapSizeX, mapSizeY;
    int width, height;
    public const int MAX_ROOM_NUMBER = 7;

    string[,] hexMapData;//16進数用
    int[,] binaryMaze;//10進数用

    public GameObject _player;

    float tileSize;
    Vector3 mapCenter;
    int[,] map;
    enum DIRECTION
    {
        UP,
        RIGHT,
        UPRIGHT,
        DOWN,
        UPDOWN,
        RIGHTDOWN,
        UPRIGHTDOWN,
        LEFT,
        UPLEFT,
        RIGHTLEFT,
        UPRIGHTLEFT,
        DOWNLEFT,
        UPDOWNLEFT,
        RIGHTDOWNLEFT,
        CENTER
    }

    [SerializeField] LayerMask blackLayer;
    [SerializeField] Vector2 size;

    void Awake()
    {

        maze = new MapGenerator(mapSizeX, mapSizeY);
        maze.GenerateMap(MAX_ROOM_NUMBER);
        hexMapData = maze.HexMaze;
        width = hexMapData.GetLength(0);
        height = hexMapData.GetLength(1);
        binaryMaze = new int[width, height];

        ConvertHexToBinary();
        PlaceTiles();
        
    }

    private void Start()
    {
        SponePlayer();
    }

    void PlaceTiles()
    {   
        tileSize = groundPrefab.GetComponent<SpriteRenderer>().bounds.size.x;
        mapCenter = new Vector3(width * tileSize / 2, height * tileSize / 2 - (tileSize / 2), 0);
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                Vector3 position = GetWorldPositionFromCell(x, y);
                Instantiate(groundPrefab, position, Quaternion.Euler(0, 0, 0.0f), transform);

                int cellValue = binaryMaze[x, y];
                Instantiate(groundPrefab, position, Quaternion.Euler(0, 0, 0.0f), transform);
                if ((cellValue & 1) != 0) InstantiateWall(position, DIRECTION.UP);
                if ((cellValue & 2) != 0) InstantiateWall(position, DIRECTION.RIGHT);
                if ((cellValue & 3) != 0) InstantiateWall(position, DIRECTION.UPRIGHT);
                if ((cellValue & 4) != 0) InstantiateWall(position, DIRECTION.DOWN);
                if ((cellValue & 5) != 0) InstantiateWall(position, DIRECTION.UPDOWN);
                if ((cellValue & 6) != 0) InstantiateWall(position, DIRECTION.RIGHTDOWN);
                if ((cellValue & 7) != 0) InstantiateWall(position, DIRECTION.UPRIGHTDOWN);
                if ((cellValue & 8) != 0) InstantiateWall(position, DIRECTION.LEFT);
                if ((cellValue & 9) != 0) InstantiateWall(position, DIRECTION.UPLEFT);
                if ((cellValue & 10) != 0) InstantiateWall(position, DIRECTION.RIGHTLEFT);
                if ((cellValue & 11) != 0) InstantiateWall(position, DIRECTION.UPRIGHTLEFT);
                if ((cellValue & 12) != 0) InstantiateWall(position, DIRECTION.DOWNLEFT);
                if ((cellValue & 13) != 0) InstantiateWall(position, DIRECTION.UPDOWNLEFT);
                if ((cellValue & 14) != 0) InstantiateWall(position, DIRECTION.RIGHTDOWNLEFT);
                if ((cellValue & 15) != 0) InstantiateWall(position, DIRECTION.CENTER);
            }
        }
        
    }
    void InstantiateWall(Vector3 postiosn, DIRECTION dir)
    {
        Instantiate(wallPrafabs[(int)dir], postiosn, Quaternion.Euler(0, 0, 0), transform);
    }
    Vector3 GetWorldPositionFromCell(int x, int y)
    {
        Vector3 pos = new Vector3(x * tileSize, (height - 1 - y) * tileSize, 0) - mapCenter;
        return pos;
    }
    private void SponePlayer()
    {
        Vector3 halfExtents = new Vector3(1f, 1f, 1f);

        for (int i = 0; i < 1; i++)
        {
            Vector3 pos = UniRandom.insideUnitCircle * 1000;

            Collider2D col = Physics2D.OverlapBox(pos, size, 0f, blackLayer);


            if (col)
            {
                i--;                                  // カウンターを減らして、再生成する
                continue;

            }
            else
            {
                _player.transform.position = new Vector3(pos.x, pos.y, 0);
            }
        }
    }
    void ConvertHexToBinary()
    {
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                string hexValue = hexMapData[x, y];
                binaryMaze[x, y] = Convert.ToInt32(hexValue, 16);
            }
        }
    }
}

自分で試したこと

ソースコードをネットでの情報を参照し変更した。

1

3Answer

サラッと読んだだけですが
一度チェックしたところを再度チェックしているのが問題だと思います
例えば 上と右に壁がある場合は
if (y > 0 && map[y - 1, x] == 0) cellValue += 1;
でCellValue == 1になり
次に
if (x < mapSizeX - 1 && map[y, x + 1] == 0) cellValue += 2;
でCellValue == 3になり
if (y > 0 && x < mapSizeX - 1 && map[y - 1, x + 1] == 0) cellValue += 3;
でCellValue == 6になっているのが原因かと
なので重複してチェックしている箇所をなくせば動くんではないでしょうか
(しっかり読んでないので他にも原因があるかもですが…)

void MazeToHex()
{
    hexMaze = new string[(mapSizeX - 1), (mapSizeY - 1)];
    for (int y = 1; y < mapSizeY - 1; y += 2)
    {
        for (int x = 1; x < mapSizeX - 1; x += 2)
        {
            uint cellValue = 0;
            // 上のセルを確認
            if (y > 0 && map[y - 1, x] == 0) cellValue += 1;
            // 右のセルを確認
            if (x < mapSizeX - 1 && map[y, x + 1] == 0) cellValue += 2;
            // 下のセルを確認
            if (y < mapSizeY - 1 && map[y + 1, x] == 0) cellValue += 4;
            // 左のセルを確認
            if (x > 0 && map[y, x - 1] == 0) cellValue += 8;
            hexMaze[(x - 1), (y - 1)] = cellValue.ToString("X");
        }
    }
}
1Like

Comments

  1. @PECTONG

    Questioner

    ご回答ありがとうございます!
    ご指摘いただいた点と他の間違えていた点を修正して動かしてみたのですが、やはりうまく動作しませんでした……。
    現在、こちらの問題に対してRule Tileというタイルマップを自動で振り分けてくれるスクリプタブルタイルを利用して試していて、マップの生成自体はうまくいったので(また新しい問題が発生しましたが)その旨後ほど回答に記載させていただこうと思います。
    重ねて、ご回答ありがとうございました。

荒技でいいなら、マップチップを3Dのプレハブにして、上からマップ用のカメラで写したものをこちらに従って実装すれば良いと思います。スクリーンショット 2024-04-17 6.40.19.png
大雑把な手順としては、このようなオブジェクトを作り、緑色のオブジェクト(マップの壁に当たるところ)のレイヤーをWallとし、灰色のところのレイヤーをFloorとします。そして、ミニマップ用のカメラを作成し、Culling MaskをWallだけにします。レンダーテクスチャを作成し、マップ用のカメラの、TargetTextureにレンダーテクスチャをアタッチします。スクリーンショット 2024-04-17 6.56.52.png
スクリーンショット 2024-04-17 6.57.28.png
次に、UIの、RawImageを作成し、Textureに先ほど作成したレンダーテクスチャをアタッチします。マスクなどを使用していい感じに整えたらミニマップになります。リアルタイムで投影できるので、プレイヤーや敵、アイテムの位置なども写せます。複雑なコード等を書くのが面倒ならこの方法をお勧めします。私も初心者なので、間違っていたらご指摘ください。

1Like

Comments

  1. @PECTONG

    Questioner

    ご回答ありがとうございます!
    3Dを使ったやり方もあるんですね……自分には思いつきもしない発想でした。
    一応こちらの問題自己解決しまして、Rule Tileを使った方法でうまくいきました。もしご興味がおありでしたら解答欄で参考に上げているページを見てみてください。

こちらの件ですが、当初想定していたのと違った解決法を見つけひとまず解決しました。
Rule Tileという自動で登録したマップチップを切り替えてくれる仕組みがあって、それを利用したタイルマップをプレハブの代わりに呼び出すと思った通りのマップが出来上がりました。

以下、参考にしたページになります。
https://active-role-works.com/p/tilemap_3/
https://qiita.com/Tatsuoh_ggggg/items/969ca5ffbf74de820e9e
https://hiyotama.hatenablog.com/entry/2021/03/21/004058

0Like

Your answer might help someone💌