Основным генератором подземелий в C#


Я решил сделать свой собственный генератор подземелье, прочитав немного об этом. Это моя первая попытка. Я могу только сказать, что это работает, но я считаю, что есть номер для улучшения, я считаю, что самоучка кодирования навыки являются базовыми.

Чтобы воспользоваться этим кодом, звоните:

dungeonGenerator = new DungeonGenerator();
dungeonGenerator.CreateDungeonRoom(115, 32);
dungeonGenerator.CellsToGenerate = 1500;
dungeonGenerator.CreateDungeonScenery();
dungeonGenerator.LogDungeon();

Плитка.в CS

using System;
using System.Collections.Generic;
using System.Text;

namespace DungeonGenerator
{
    struct Cell
    {
        /// <summary>
        /// Location in the grid
        /// </summary>
        public CellType CellType;

        public Cell(CellType cellType)
        {
            CellType = cellType;
        }

        public override string ToString()
        {
            return "Type: " + CellType;
        }
    }
}

CellType.в CS

    /// <summary>
     /// If the cell is walkable or not
     /// </summary>
    public enum CellType
    {
        WALL, GROUND, START
    }

Направлении.в CS

enum Direction
{
   UP, DOWN, LEFT, RIGHT
}

DungeonGenerator.в CS

using System;

namespace DungeonGenerator
{
    public class DungeonGenerator
    {
        public int Width            { get; set; }
        public int Height           { get; set; }
        public int CellsToGenerate  { get; set; }
        Cell[,] DungeonCells    { get; set; }

        public void CreateDungeonRoom(int RoomWidth, int RoomHeight)
        {
            Width = RoomWidth;
            Height = RoomHeight;
            DungeonCells = new Cell[Width,Height];

            for (int y = 0; y < Height; y++)
            {
                for (int x = 0; x < Width; x++)
                {
                    CellType cellType = CellType.WALL;
                    DungeonCells[x, y] = new Cell(cellType);
                }
            }
        }
        /// <summary>
        /// Populate the insides of the room
        /// </summary>
        public void CreateDungeonScenery()
        {

            //Make it so that the outside walls are untouched
            int minValueXY = 1;
            int maxValueX = Width - 2;
            int maxValueY = Height - 2;

            //Choose a random position in the room
            Random random = new Random();
            int startX = random.Next(minValueXY, maxValueX);
            int startY = random.Next(minValueXY, maxValueY);

            //Mark it as the starting position, for player placement, maybe?
            DungeonCells[startX, startY] = new Cell(CellType.START);


            //Get directions to an array for random choosing
            Array values = Enum.GetValues(typeof(Direction));

            //From the starting position, proceed to create X number of ground cells
            int cellCount = 0;
            while (cellCount < CellsToGenerate)
            {
                //Choose a direction at random
                Direction direction = (Direction)values.GetValue(random.Next(values.Length));

                if (direction == Direction.UP)
                {
                    startY -= 1;
                    if (startY < minValueXY) { startY = minValueXY; }
                }
                else if (direction == Direction.DOWN)
                {
                    if (startY < maxValueY) { startY += 1; }
                }
                else if (direction == Direction.LEFT)
                {
                    startX -= 1;
                    if (startX < minValueXY) { startX = minValueXY; }

                }
                else if (direction == Direction.RIGHT)
                {

                    if (startX < maxValueX) { startX += 1; }
                }

                //From the position chosen, mark it as ground, if possible
                if (CreateGround(startX, startY))
                {
                    //Mark the cell as ground
                    DungeonCells[startX, startY].CellType = CellType.GROUND;
                    //Add one to cells created
                    cellCount++;
                }
            }
        }

        private bool CreateGround(int startX, int startY)
        {
            //There's not a wall there, so there's nothing to be done here
            if (DungeonCells[startX, startY].CellType != CellType.WALL)
            {
                return false;
            }

            return true;
        }

        public void LogDungeon()
        {
            Console.Clear();

            for (int y = 0; y < Height; y++)
            {
                string line = "";
                for (int x = 0; x < Width; x++)
                {
                    if (DungeonCells[x, y].CellType == CellType.GROUND)
                    {
                        line += "O";
                    }
                    else if (DungeonCells[x, y].CellType == CellType.WALL)
                    {
                        line += "█";
                    }
                    else if (DungeonCells[x, y].CellType == CellType.START)
                    {
                        line += "H";
                    }
                }
                Console.WriteLine(line);
            }
        }

    }
}

С самого начала, я понимаю, что я могу улучшить код:

Предотвращение поиска с возвратом, но перебор иногда делает некоторые хорошие трещины тут и там. Решить эту проблему, делая невозможным выбор прямо противоположное предыдущему направление? Я хочу выяснить способ, чтобы улучшить это направление выбирают часть (сейчас не знаю как).

Нарочно, я оставила номер клетки для того чтобы произвести из CreateDungeonRoom() метод, потому что я еще не решила, действительно ли я хочу сделать пользователям вставить цокольный ряд плитки, как определенное количество или процент, или enum. Мы увидим.

Вот пример подземелье с 115 ширина, 32 высота и 1500 наземные плитки и игрок стартовой позиции (это час где-то) enter image description here



654
7
задан 10 февраля 2018 в 05:02 Источник Поделиться
Комментарии
3 ответа

Очистить Ваш Код


  • Преобразования if-else лестница switch. Переключаться гораздо быстрее.

  • startY -= для startY--

  • Тоже касается LogDungeon() способ

Перед

if (direction == Direction.UP)
{
startY -= 1;
if (startY < minValueXY) { startY = minValueXY; }
}
else if (direction == Direction.DOWN)
{
if (startY < maxValueY) { startY += 1; }
}
else if (direction == Direction.LEFT)
{
startX -= 1;
if (startX < minValueXY) { startX = minValueXY; }

}
else if (direction == Direction.RIGHT)
{

if (startX < maxValueX) { startX += 1; }
}

После

switch (direction)
{
case Direction.UP:
startY--;
if (startY < minValueXY)
startY = minValueXY;
break;
case Direction.DOWN:
if (startY < maxValueY)
startY++;
break;
case Direction.LEFT:
startX--;
if (startX < minValueXY)
startX = minValueXY;
break;
case Direction.RIGHT:
if (startX < maxValueX)
startX++;
break;
}


Упростить Методы И Условия

Перед

private bool CreateGround(int startX, int startY)
{
//There's not a wall there, so there's nothing to be done here
if (DungeonCells[startX, startY].CellType != CellType.WALL)
{
return false;
}

return true;
}

После

private bool CreateGround(int startX, int startY) =>
DungeonCells[startX, startY].CellType == CellType.WALL;


Замечания

Поместите следующий код снаружи while цикл, если вы действительно не нужно это внутри цикла.

int cellCount = 0;
while (cellCount < CellsToGenerate)
{
//Choose a direction at random
Direction direction = (Direction)values.GetValue(random.Next(values.Length))
switch (direction)
{
case Direction.UP:
startY--;
if (startY < minValueXY)
startY = minValueXY;
break;
case Direction.DOWN:
if (startY < maxValueY)
startY++;
break;
case Direction.LEFT:
startX--;
if (startX < minValueXY)
startX = minValueXY;
break;
case Direction.RIGHT:
if (startX < maxValueX)
startX++;
break;
}

}


Предложения


  • Пожалуйста, избегайте использования глобальной переменной/свойства внутри класса. Вместо того чтобы использовать параметры для передачи значений внутри метода и тип возвращаемого значения. Если вам нужно вернуть несколько значений, создать новый тип и вернуть его. Это поможет избежать путаницы.

  • Использование правильных принципов именования. У вас есть некоторые неправильные названия в CreateDungeonRoom метод. Имена параметров должны быть всегда в верблюжьего.


Надеюсь, что это помогает

6
ответ дан 11 февраля 2018 в 07:02 Источник Поделиться

Именования

DungeonGeneratorдля меня будет означать, что это класс, который создает Dungeon. Но как вы используете его, DungeonGenerator это само собой, в подземелье. Я бы падение "генератор" часть имени, или рефакторинг это так, то в действительности генератор подземелий.

Кроме того, у вас есть DungeonGenerator пространства имен и DungeonGenerator класса в этом пространстве имен. Я бы избежать класса совпадает с именем содержащего его пространства имен. В противном случае вам нужно написать путаешь такие вещи как:

var x = new DungeonGenerator.DungeonGenerator(); 

Избежать Изменяемого Свойства

Ваш DungeonGenerator есть несколько внешне изменяемые свойства, что я не вижу оснований для них, чтобы быть изменчивым. Было бы лучше, если бы они не были изменчивыми:

public class DungeonGenerator
{
public int Width { get; private set; }
public int Height { get; private set; }
public int CellsToGenerate { get; private set; }
private Cell[,] DungeonCells;

public void CreateDungeonRoom(int RoomWidth, int RoomHeight) { ... }
/// <summary>
/// Populate the insides of the room
/// </summary>
public void CreateDungeonScenery() { ... }

private bool CreateGround(int startX, int startY) { ...}

public void LogDungeon(){ ... }
}

Кроме того, DungeonCells не надо быть собственность на всех, так просто сделать его частное поле.

Последовательное Документации

Вы документально CreateDungeonScenery способ, но ничего не является публичной. Вы должны быть "все-в" с документацией на методы, и сделать его описательный (не просто слово и название метода). Если метод не имеет побочных эффектов, упомянуть их в <remarks></remarks> раздел.

Последовательные Имена Параметров Метода

Вы получили имена в CreateGround как camelCased но у вас есть имена в CreateDungeonRoom как PascalCased. По словам .Чистая принципов именования вы должны использовать PascalCased метод/свойство имена и camelCased для параметров метода.

Используйте Код

Я не вижу никаких причин, почему вы хотите создать новый DungeonGenerator и не запускать CreateDungeonRoomтак почему бы не сделать это в конструкторе?

public class DungeonGenerator
{
public int Width { get; private set; }
public int Height { get; private set; }
public int CellsToGenerate { get; private set; }
private Cell[,] dungeonCells;

public void DungeonGenerator(int roomWidth, int roomHeight, int cellsToGenerate) { ... }
/// <summary>
/// Populate the insides of the room
/// </summary>
public void CreateDungeonScenery() { ... }

private bool CreateGround(int startX, int startY) { ...}

public void LogDungeon(){ ... }
}

Кроме того, конструктор может вызвать CreateDungeonScenery способ также. Так что теперь вместо оригинальным образом, вы можете закрепить код:

var dungeon = new DungeonGenerator(115, 32, 1500);

Имя Метода

В LogDungeon метод название сбивает с толку, потому что слово "отчет" для большинства разработчиков означает какую-то диагностическую процедуру, которая записывает данные в файл или консоль чисто для отладки. Вы используете его, чтобы взять на консоль (сняв его) и выводить текст графических подземелья к консоли. Я думаю, что лучшее название было бы нечто вроде FlushToConsole или что-то подобное.

Однако я думаю, что вы не должны быть Console методы в этом классе, позвольте пользователю решать, что с ней делать. Если класс имеет действительный string представление, вы должны переопределить ToString() способ вместо того, чтобы LogDungeon становится ToString():

        public override void ToString()
{
StringBuilder dungeon = new StringBuilder();
for (int y = 0; y < Height; y++)
{
StringBuilder line = new StringBuilder();
for (int x = 0; x < Width; x++)
{
switch (DungeonCell[x, y].CellType)
{
case CellType.GROUND:
line.Append("O"); break;
case CellType.WALL:
line.Append("█"); break;
case CellType.START:
line.Append("H"); break;
}
}
dungeon.AppendLine(line.ToString());
}
return dungeon.ToString();
}

Теперь пользователь вашего класса можете решить, чтобы сделать что-то еще с ним, например, записать его в файл, отправить его по сети, распечатать его на сайте и т. д. Пользователь больше не привязан к консоли.

4
ответ дан 13 февраля 2018 в 08:02 Источник Поделиться

Некоторые общие замечания о том, как ваш код "читает":

Использование

Вы писали: "чтобы использовать этот код, звонок:"

dungeonGenerator = new DungeonGenerator();
dungeonGenerator.CreateDungeonRoom(115, 32);
dungeonGenerator.CellsToGenerate = 1500;
dungeonGenerator.CreateDungeonScenery();
dungeonGenerator.LogDungeon();

Во-первых, позвольте мне сказать, что я поддерживаю идею о том, что подземелья генератор и подземелья объекта должны быть разными. Код для случайного создания комнат не имеет ничего общего с кодом, который решает, имеет ли пользователь наступил на ловушку и т. д. Поэтому мне нравится идея отдельного класса/объекта/функции.

Что сказал, Это очень неудобный набор вызовов. Это не "сканирование" хорошо (если использовать термин из литературы). Я подозреваю, что все эти мелкие детали должны быть завернуты в один заводские функции , которые пользователь может вызвать:

cavern = Dungeon.Generate(width: 115, height: 32, min_cells: 1500);
cavern.WriteTo(Console.Out);

CellType

Ваш тип клетки является перечислением, которое кажется очевидным, но на самом деле не очень полезно. Что вы в основном делаете с Тип ячейки индикации, и, к сожалению, в C# перечисления не имеют char как их базовый тип. Но вы можете обойти это с помощью int и просто присвоить char значения:

public enum CellType 
{
WALL = '█',
GROUND = 'O',
START = 'H'
};

Только будьте осторожны, о том, что размер символов вы используете (16 бит). Если вы должны использовать по полной строки, рассмотрим решение в этот ответ.

Я предлагаю это потому, что ваш исходный код, было бы полезно иметь "конкретных" ценностей. (Остальной ваш код не довольны CellType быть абстрактной перечисление.)

                switch (DungeonCell[x, y].CellType)
{
case CellType.GROUND:
line.Append("O"); break;

Примечание: как правило, если вы окажетесь выполнения switch на внутренней данные, это хороший показатель, который следует рассмотреть нового типа. Переключение на внешние данные могли бы быть правильный способ справиться с пользовательского ввода. Но переключение на внутренние данные часто означает "эти вещи должны быть различные предметы, или различным классам". В данном случае, значения перечислений, выступающей в качестве прокси для объектов, которые имеют свой собственный выход строковое представление.

Было бы лучше, ИМО, просто излучают ценность, или метод по стоимости (как в связанном ответа):

line.Append(DungeonCell[x, y].cellType);

// -or-

line.Append(DungeonCell[x, y].cellType.ToFriendlyString());

(но см. ниже об этом x, y вещь!)

Направление

Другой enum, что не совсем то, что вам нужно. В настоящее время, вы пишете такой код (код изменен на размер):

//Choose a direction at random
Direction direction = (Direction)values.GetValue(random.Next(values.Length));

if (direction == Direction.UP)
if (startY > minValueXY)
--startY;
else if (direction == Direction.DOWN)
if (startY < maxValueY)
++startY;
else if (direction == Direction.LEFT)
if (startX > minValueXY)
--startX;
else if (direction == Direction.RIGHT)
if (startX < maxValueX)
++startX;

//From the position chosen, mark it as ground, if possible

Вместо того, чтобы делать Direction в enumчто, если вы делаете это класс? Вы можете затем Direction.AtRandom() вернуть то, что вы хотите без того, чтобы бросить его.

Более того, подумайте, что вы делаете: вы немедленно расшифровать направление X или Y смещение. Вы тогда либо измените переменную startX или startY.

Почему эти две переменные разных? Что startX? Что startY? Разве они не часть большего целого, под названием Position (или Location или Coords что-то)?

Если у вас Position типа, вы могли бы просто иметь start вместо startX и startY. Возможно, вам придется отрегулировать start.X и start.Yно, наверное, нет, потому что Direction должен быть вектор.

Если Direction.North вектор определяется как { δx = 0, δy = -1 } затем можно определить добавлением Position и Vector в очевидный способ (х+ДХ, у+δy) и затем упростить код:

public static Position operator +(Position pos, Vector v)
{
return new Position(pos.x + v.δx, pos.y + v.δy);
}

Затем:

// Choose a direction at random (but don't go out-of-bounds)

dir = Direction.AtRandom();

if (dungeon.Contains(start + dir))
start += dir;

// From the position chosen, mark it as ground, if possible

Примечание: .Contains может быть неправильное название, так как есть, что неявное правило
о не изменении внешних краев. Может Быть, "CanBeRoom()"?

Положение

Если определить индексатор для вашего подземелья, вы можете использовать Position напрямую вместо того, чтобы достигнуть в x и y значения. Это будет стоить некоторых производительности, но, вероятно, не имеет значения во время формирования. Кроме того, вы можете просто написать Dungeon.At(Position) метод.

Правила Подземелье

Я видел одного "неявные" правила в коде: наружные стены подземелья неприкосновенны. Я предлагаю вам сделать эти правила явно и кодирования операций в настоящее время реализуется только пункт или два кода:

//Make it so that the outside walls are untouched
int minValueXY = 1;
int maxValueX = Width - 2;
int maxValueY = Height - 2;

//Choose a random position in the room
Random random = new Random();
int startX = random.Next(minValueXY, maxValueX);
int startY = random.Next(minValueXY, maxValueY);

Это может быть заменено на определение методов:

topLeft = dungeon.PlayerTopLeft();      // returns a Position, (1,1)?
botRight = dungeon.PlayerBottomRight(); // returns a Position.

start = Position.AtRandom(min: topLeft, max: botRight);

Итераторы

Я предлагаю писать как минимум одну позицию итератора для Dungeon. Что-то вроде Dungeon.PositionsInViewOrder()что бы yield в ячейки в правильном порядке на экран.

(Примечание: если объекты сотового знал свою позицию, я предлагаю просто писать сотового итератора и пропуская позиции. Но как обстоят дела, это путь.)

Вы могли бы переписать ваш LogDungeon функции Dungeon метод:

public void WriteTo(System.IO.TextWriter out)
{
// Only care about Y
int last_pos = TopLeft() + Vector(δx:0, δy:-1);

for (Position pos in PositionsInViewOrder())
{
if (last_pos.y != pos.y)
out.WriteLine();

last_pos = pos;
out.Write(dungeon[pos].cellType);
}

out.WriteLine();
}

Вы можете обнаружить, что многие подземелье Создание задачи облегчается определение Position итераторы. Например, если вы хотите, чтобы случайным образом генерировать номерах и коридорах можно определить Position.WithinRect(topLeft:Position, bottomRight:Position) и Position.AlongLine(from:Position, to:Position). (Будьте последовательны в конечном положении: инклюзивное или эксклюзивное!)

2
ответ дан 14 февраля 2018 в 02:02 Источник Поделиться