Criando roguelike no Unity do zero

imagem

Não há muitos tutoriais sobre como criar roguelike no Unity, então decidi escrevê-lo. Não para me gabar, mas para compartilhar conhecimento com aqueles que estão na fase em que eu já estava há um bom tempo.

Nota: Não estou dizendo que esta é a única maneira de criar um roguelike no Unity. Ele é apenas um dos . Provavelmente não é o melhor e mais eficaz, aprendi por tentativa e erro. E vou aprender algumas coisas diretamente no processo de criação de um tutorial.

Vamos supor que você conheça pelo menos o básico do Unity, por exemplo, como criar um prefab ou script, e similares. Não espere que eu lhe ensine como criar folhas de sprite, existem muitos ótimos tutoriais sobre isso. Vou me concentrar não no estudo do mecanismo, mas em como implementar o jogo que criaremos juntos. Se você tiver dificuldades, vá até uma das comunidades impressionantes do Discord e peça ajuda:

Comunidade de desenvolvedores do Unity

Roguelikes

Então, vamos começar!

Etapa 0 - planejamento


Sim está certo. A primeira coisa a criar é um plano. Será bom para você planejar o jogo, e para mim - planejar o tutorial para que, depois de um tempo, não nos distraiamos do tópico. É fácil ficar confuso nas funções do jogo, como nas masmorras roguelike.

Vamos escrever roguelike. Seguiremos principalmente os sábios conselhos do desenvolvedor da Cogmind Josh Ge aqui . Siga o link, leia a postagem ou assista ao vídeo e volte.

Qual é o objetivo deste tutorial? Obtenha um roguelike básico simples e sólido, com o qual você poderá experimentar. Deveria ter geração de masmorra, um jogador movendo-se no mapa, neblina de visibilidade, inimigos e objetos. Apenas o mais necessário. Portanto, o jogador deve poder descer as escadas vários andares. digamos, por cinco, aumente seu nível, melhore e, no final, lute com o chefe e derrote-o. Ou morra. Isso, de fato, é tudo.

Seguindo o conselho de Josh Ge, construiremos as funções do jogo para que elas nos levem ao objetivo. Portanto, temos a estrutura roguelike, que pode ser expandida ainda mais, adicione suas próprias fichas, criando exclusividade. Ou jogue tudo na cesta, aproveite a experiência adquirida e comece do zero. Vai ser incrível de qualquer maneira.

Não darei a você nenhum recurso gráfico. Desenhe você mesmo ou use os conjuntos de blocos gratuitos, que podem ser baixados aqui , aqui ou pesquisando no Google. Só não se esqueça de mencionar os autores dos gráficos no jogo.

Agora vamos listar todas as funções que estarão em nosso roguelike na ordem de sua implementação:

  1. Geração de mapas de masmorras
  2. Personagem do jogador e seu movimento
  3. Área de visibilidade
  4. Inimigos
  5. Procure uma maneira
  6. Luta, Saúde e Morte
  7. Nível do Jogador
  8. Itens (armas e poções)
  9. Fraudes do console (para teste)
  10. Pisos de masmorra
  11. Salvando e carregando
  12. Chefe final

Depois de implementar tudo isso, teremos um forte roguelike, e você aumentará bastante suas habilidades de desenvolvimento de jogos. Na verdade, era minha maneira de melhorar minhas habilidades: criar código e implementar funções. Portanto, tenho certeza de que você também pode lidar com isso.

Etapa 1 - Classe MapManager


Este é o primeiro script que criaremos e se tornará a espinha dorsal do nosso jogo. É simples, mas contém a maior parte das informações importantes para o jogo.

Portanto, crie um script .cs chamado MapManager e abra-o.

Exclua ": MonoBehaviour" porque não herdará dele e não será anexado a nenhum GameObject.

Remova as funções Start () e Update ().

No final da classe MapManager, crie uma nova classe pública chamada Tile.


A classe Tile conterá todas as informações de um único bloco. Até agora, não precisamos de muito, apenas as posições x e y, bem como um objeto de jogo localizado nessa posição do mapa.


Portanto, temos informações básicas sobre blocos. Vamos criar um mapa a partir deste bloco. É simples, precisamos apenas de uma matriz bidimensional de objetos Tile. Parece complicado, mas não há nada de especial nisso. Simplesmente adicione a variável Tile [,] à classe MapManager:


Voila! Nós temos um mapa!

Sim, está vazio. Mas este é um mapa. Cada vez que algo se move ou muda de estado no mapa, as informações neste mapa serão atualizadas. Ou seja, se, por exemplo, um jogador tentar mudar para um novo bloco, a classe verificará o endereço do bloco de destino no mapa, a presença do inimigo e sua perviedade. Graças a isso, não precisamos verificar milhares de colisões a cada turno e não precisamos de coletores para cada objeto do jogo, o que facilitará e simplificará o trabalho com o jogo.

O código resultante é assim:

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

public class MapManager 
{
    public static Tile[,] map; // the 2-dimensional map with the information for all the tiles
}

public class Tile { //Holds all the information for each tile on the map
    public int xPosition; // the position on the x axis
    public int yPosition; // the position on the y axis
    public GameObject baseObject; // the map game object attached to that position: a floor, a wall, etc.
}

A primeira etapa está concluída, vamos continuar preenchendo o cartão. Agora vamos começar a criar um gerador de masmorra.

Etapa 2 - algumas palavras sobre a estrutura de dados


Mas antes de começar, deixe-me compartilhar as dicas que surgiram graças ao feedback recebido após a publicação da primeira parte. Ao criar uma estrutura de dados, você precisa pensar desde o início como manterá o estado do jogo. Caso contrário, mais tarde será muito mais caótico. O usuário do Discord st33d, desenvolvedor do Star Shaped Bagel (você pode jogar este jogo de graça aqui ), disse que, a princípio, ele criou o jogo, pensando que ele não salvaria estados. Gradualmente, o jogo começou a ficar maior, e seu fã pediu suporte para o mapa salvo. Mas, devido ao método escolhido para criar a estrutura de dados, era muito difícil salvar os dados, então ele não pôde fazer isso.


Nós realmente aprendemos com nossos erros. Apesar de eu colocar a parte de salvar / carregar no final do tutorial, eu penso nelas desde o começo e ainda não as expliquei. Nesta parte, falarei um pouco sobre eles, mas para não sobrecarregar os desenvolvedores inexperientes.

Salvaremos coisas como uma matriz de variáveis ​​da classe Tile na qual o mapa está armazenado. Salvaremos todos esses dados, exceto as variáveis ​​da classe GameObject, que estão dentro da classe Tile. Por quê? Só porque GameObjects não pode ser serializado com o Unity para dados armazenados.

Portanto, de fato, não precisamos salvar os dados armazenados no GameObjects. Todos os dados serão armazenados em classes como Tile, e mais tarde também Player, Inemy, etc. Em seguida, teremos o GameObjects para simplificar o cálculo de itens como visibilidade e movimento, além de desenhar sprites na tela. Portanto, dentro das classes, haverá variáveis ​​GameObject, mas o valor dessas variáveis ​​não será salvo e carregado. Ao carregar, forçaremos a gerar o GameObject novamente a partir dos dados salvos (posição, sprite, etc.).

Então, o que precisamos fazer agora? Bem, basta adicionar duas linhas à classe Tile existente e uma na parte superior do script. Primeiro, adicionamos "using System;" ao título do script e, em seguida, [Serializable] na frente de toda a classe e [NonSerialized] na frente da variável GameObject. Como isso:



Vou contar mais sobre isso quando chegarmos à parte do tutorial sobre como salvar / carregar. Por enquanto, vamos deixar tudo e seguir em frente.

Etapa 3 - um pouco mais sobre a estrutura de dados


Eu recebi outra revisão sobre a estrutura de dados que quero compartilhar aqui.

De fato, existem muitas maneiras de implementar dados em um jogo. O primeiro que eu uso e que será implementado neste tutorial: todos os dados do bloco estão na classe Tile e todos eles são armazenados em uma matriz. Essa abordagem tem muitas vantagens: é mais fácil ler, tudo o que você precisa está em um só lugar, os dados são mais fáceis de manipular e exportar para um arquivo salvo. Mas, do ponto de vista da memória, não é tão eficaz. Você precisará alocar muita memória para variáveis ​​que nunca serão usadas no jogo. Por exemplo, mais tarde, colocaremos a variável Enemy GameObject na classe Tile, para que possamos apontar diretamente do mapa para o GameObject do inimigo que está sobre esse bloco, para simplificar todos os cálculos relacionados à batalha. Mas isso significa que cada bloco no jogo terá espaço alocado na memória para a variável GameObject,mesmo se não houver inimigo neste bloco. Se houver 10 inimigos em um mapa de 2500 blocos, haverá 2490 vazios, mas variáveis ​​de GameObject alocadas - você pode ver quanta memória é desperdiçada.

Um método alternativo seria usar estruturas para armazenar os dados básicos dos blocos (por exemplo, posição e tipo), e todos os outros dados seriam armazenados em hashmap-s, que seriam gerados apenas se necessário. Isso economizaria muita memória, mas o retorno seria uma implementação um pouco mais complicada. Na verdade, seria um pouco mais avançado do que eu gostaria neste tutorial, mas se você quiser, no futuro eu posso escrever um post mais detalhado sobre isso.

Além disso, se você quiser ler uma discussão sobre este tópico, isso pode ser feito no Reddit .

Etapa 4 - Algoritmo de geração de masmorra


Sim, esta é outra seção na qual falarei e não começaremos a programar nada. Mas isso é importante, o planejamento cuidadoso dos algoritmos nos poupará muito tempo de trabalho no futuro.

Existem várias maneiras de criar um gerador de masmorra. O que iremos implementar juntos não é o melhor nem o mais eficaz ... é apenas uma maneira fácil e inicial. É muito simples, mas os resultados serão muito bons. O principal problema serão muitos corredores sem saída. Posteriormente, se você quiser, posso publicar outro tutorial sobre algoritmos melhores.

Em geral, o algoritmo que usamos funciona da seguinte maneira: digamos que temos um mapa inteiro preenchido com valores zero - um nível que consiste em uma pedra. No começo, cortamos uma sala no centro. A partir desta sala, atravessamos o corredor em uma direção e adicionamos outros corredores e salas, sempre iniciando aleatoriamente a partir de uma sala ou corredor existente, até atingirmos o número máximo de corredores / salas indicados no início. Ou até que o algoritmo encontre um novo local para adicionar uma nova sala / corredor, o que ocorrer primeiro. E assim temos uma masmorra.

Então, vamos descrever isso de uma maneira mais parecida com um algoritmo, passo a passo. Por conveniência, chamarei cada detalhe do mapa (corredor ou sala) de um elemento para que eu não precise dizer “sala / corredor” toda vez.

  1. Corte a sala no centro do mapa
  2. Selecione aleatoriamente uma das paredes
  3. Atravessamos o corredor nesta parede
  4. Selecione aleatoriamente um dos elementos existentes.
  5. Selecione aleatoriamente uma das paredes deste elemento
  6. Se o último item selecionado for uma sala, geramos um corredor. Se o corredor, escolha aleatoriamente se o próximo elemento será uma sala ou outro corredor
  7. Verifique se há espaço suficiente na direção selecionada para criar o item desejado
  8. Se houver um local, crie um elemento; caso contrário, retorne à etapa 4
  9. Repita da etapa 4

Isso é tudo. Obteremos um mapa simples da masmorra, no qual existem apenas salas e corredores, sem portas e elementos especiais, mas este será o nosso começo. Mais tarde, vamos preenchê-lo com baús, inimigos e armadilhas. E você pode até personalizá-lo: aprenderemos como adicionar elementos interessantes que você precisa.

Etapa 5 - cortar a sala


Finalmente, prossiga para a codificação! Vamos cortar o nosso primeiro quarto.

Primeiro, crie um novo script e chame-o de DungeonGenerator. Ele será herdado do Monobehaviour, portanto você precisará anexá-lo ao GameObject posteriormente. Em seguida, precisaremos declarar várias variáveis ​​públicas na classe para que possamos definir os parâmetros da masmorra pelo inspetor. Essas variáveis ​​serão a largura e a altura do mapa, a altura e a largura mínima e máxima das salas, o comprimento máximo dos corredores e o número de elementos que devem estar no mapa.


Em seguida, precisamos inicializar o gerador de masmorra. Fazemos isso para inicializar as variáveis ​​que serão preenchidas pela geração. Até agora, será apenas um mapa. E, e também exclua as funções Start () e Update () que o Unity gera para o novo script, não precisaremos delas.



Aqui, inicializamos a variável map da classe MapManager (que criamos na etapa anterior), passando a largura e a altura do mapa, definidas pelas variáveis ​​acima como parâmetros das duas dimensões da matriz. Graças a isso, teremos um mapa de tamanho x horizontal (largura) e tamanho vertical y (altura), e podemos acessar qualquer célula do mapa digitando MapManager.map [x, y]. Isso será muito útil ao manipular a posição dos objetos.

Agora vamos criar uma função para renderizar a primeira sala. Vamos chamá-lo FirstRoom (). Tornamos InitializeDungeon () uma função pública, porque será lançada por outro script (Game Manager, que criaremos em breve; centralizará o gerenciamento de todo o processo de lançamento do jogo). Não precisamos de nenhum script externo para ter acesso ao FirstRoom (), portanto, não o tornamos público.

Agora, para continuar, criaremos três novas classes no script MapManager para que você possa criar uma sala. Essas são as classes Feature, Wall e Position. A classe Position conterá as posições x e y para que possamos rastrear onde está tudo. A parede terá uma lista de posições, a direção na qual ela "olha" em relação ao centro da sala (norte, sul, leste ou oeste), comprimento e a presença de um novo elemento criado a partir dela. O elemento terá uma lista de todas as posições em que consiste, o tipo do elemento (sala ou corredor), uma matriz de variáveis ​​de parede e sua largura e altura.



Agora vamos à função FirstRoom (). Vamos voltar ao script DungeonGenerator e criar uma função logo abaixo de InitializeDungeon. Ela não precisará receber nenhum parâmetro, então vamos deixar simples (). Em seguida, dentro da função, primeiro precisamos criar e inicializar a variável Room e sua lista de variáveis ​​Position. Fazemos assim:


Agora vamos definir o tamanho da sala. Ele receberá um valor aleatório entre a altura e a largura mínima e máxima declaradas no início do script. Enquanto estiverem vazios, porque não definimos um valor para eles no inspetor, mas não se preocupe, faremos isso em breve. Definimos valores aleatórios assim:


Em seguida, precisamos declarar onde o ponto inicial da sala estará localizado, ou seja, onde o ponto da sala 0.0 estará localizado na grade do mapa. Queremos começar no centro do mapa (meia largura e meia altura), mas talvez não exatamente no centro. Pode valer a pena adicionar um pequeno randomizador para que ele se mova ligeiramente para a esquerda e para baixo. Portanto, definimos xStartingPoint como metade da largura do mapa e yStartingPoint como metade da altura do mapa e, em seguida, pegamos o roomWidth e roomHeight, obtemos um valor aleatório de 0 a essa largura / altura e subtraímos dos x e y iniciais. Como isso:



Em seguida, na mesma função, adicionaremos paredes. Precisamos inicializar a matriz de paredes que estão na variável de ambiente recém-criada e, em seguida, inicializar cada variável de parede dentro dessa matriz. E, em seguida, inicialize cada lista de posições, defina o comprimento da parede como 0 e insira a direção na qual cada parede "parecerá".

Depois que a matriz é inicializada, fazemos um loop em torno de cada elemento da matriz no loop for (), inicializamos as variáveis ​​de cada parede e, em seguida, usamos a opção que nomeia a direção de cada parede. É escolhido arbitrariamente, precisamos apenas lembrar o que eles significam.


Agora, executaremos dois loops aninhados imediatamente após colocar as paredes. No loop externo, contornamos todos os valores y na sala e, no loop aninhado, todos os valores x. Dessa forma, verificaremos cada célula x na linha y para que possamos implementá-la.


A primeira coisa a fazer é encontrar o valor real da posição da célula na escala do mapa a partir da posição da sala. Isso é bem simples: temos os pontos de partida x e y. Eles estarão na posição 0,0 na grade da sala. Então, se precisarmos obter o valor real de x, y a partir de qualquer local x, y, adicionaremos o local x e y com as posições iniciais x e y. Em seguida, salvamos esses valores reais de x, y na variável Position (de uma classe criada anteriormente) e os adicionamos à Lista <> das posições da sala.


O próximo passo é adicionar essas informações ao mapa. Antes de alterar os valores, lembre-se de inicializar a variável Tile.


Agora vamos fazer uma alteração na classe Tile. Vamos para o script MapManager e adicione uma linha à definição da classe Tile: “public string type;”. Isso nos permitirá adicionar uma classe de bloco declarando que o bloco em x, y é uma parede, piso ou outra coisa. Em seguida, voltemos ao ciclo em que fizemos o trabalho e adicionamos uma grande construção if-else, que nos permitirá não apenas determinar cada parede, seu comprimento e todas as posições nessa parede, mas também especificar no mapa global o que é um bloco específico - um muro ou sexo.


E nós já fizemos algo. Se a variável y (controle da variável no loop externo) for 0, o bloco pertence à linha de células mais baixa da sala, ou seja, é a parede sul. Se x (controle da variável do loop interno) for 0, o bloco pertence à coluna de células mais à esquerda, ou seja, é a parede ocidental. E se estiver na linha superior, pertence à parede norte e à direita - a parede leste. Subtraímos 1 das variáveis ​​roomWidth e roomHeight, porque esses valores foram calculados a partir de 1, e as variáveis ​​x e y do ciclo começaram a partir de 0, portanto, essa diferença deve ser levada em consideração. E todas as células que não atendem às condições não são paredes, ou seja, são o chão.


Ótimo, estamos quase terminando com o primeiro quarto. Está quase pronto, precisamos apenas colocar os últimos valores na variável Feature que criamos. Saímos do loop e finalizamos a função assim:


Bem! Nós temos um quarto!

Mas como entendemos que tudo funciona? Precisa testar. Mas como testar? Podemos gastar tempo e adicionar ativos para isso, mas será uma perda de tempo e também nos distrairá da conclusão do algoritmo. Hmm, mas isso pode ser feito usando ASCII! Sim, ótima ideia! O ASCII é uma maneira simples e de baixo custo de desenhar um mapa para que possa ser testado. Além disso, se desejar, você pode pular a parte com sprites e efeitos visuais, que estudaremos mais adiante, e criar todo o seu jogo em ASCII. Então, vamos ver como isso é feito.

Etapa 6 - desenhar a primeira sala


A primeira coisa a se pensar ao implementar uma placa ASCII é qual fonte escolher. O principal fator a considerar ao escolher uma fonte para ASCII é se ela é proporcional (largura variável) ou monoespaçada (largura fixa). Precisamos de uma fonte monoespaçada para que os cartões tenham a aparência necessária (veja o exemplo abaixo). Por padrão, qualquer novo projeto do Unity usa a fonte Arial e não é monoespaçada, portanto, precisamos encontrar outro. O Windows 10 normalmente possui fontes monoespaçadas Courier New, Consolas e Lucida Console. Escolha um desses três ou faça o download de outro no local necessário e coloque-o na pasta Fontes, dentro da pasta Ativos do projeto.


Vamos preparar a cena para a saída ASCII. Para iniciantes, torne a cor de fundo da câmera principal da cena preta. Em seguida, adicionamos o objeto Canvas à cena e adicionamos o objeto Text. Defina a transformação do retângulo de texto no centro do meio e na posição 0,0,0. Defina o objeto Texto para que ele use a fonte que você escolher e a cor branca, estouro horizontal e vertical (estouro horizontal / vertical), selecione Estouro e centralize o alinhamento vertical e horizontal. Em seguida, renomeie o objeto Text para "ASCIITest" ou algo semelhante.

Agora, de volta ao código. No script DungeonGenerator, crie uma nova função chamada DrawMap. Queremos que ela obtenha um parâmetro informando qual cartão gerar - ASCII ou sprite, então crie um parâmetro booleano e chame-o de ASCII.


Depois, verificaremos se o mapa renderizado é ASCII. Se sim (por enquanto, consideraremos apenas esse caso), procuraremos um objeto de texto na cena, passaremos o nome dado a ele como parâmetro e obteremos o componente Text. Mas primeiro, precisamos dizer ao Unity que queremos trabalhar com a interface do usuário. Adicione a linha usando o UnityEngine.UI ao cabeçalho do script:


Bem. Agora podemos obter o componente Texto do objeto. O mapa será uma linha enorme que é exibida na tela como texto. É por isso que é tão fácil de configurar. Então, vamos criar uma string e inicializá-la com o valor "".


Bem. Portanto, toda vez que o DrawMap for chamado, precisaremos informar se o cartão é um ASCII. Se for assim (e sempre o faremos dessa maneira, trabalharemos com mais tarde), a função pesquisará a hierarquia da cena em busca de um objeto de jogo chamado “ASCIITest”. Se for, ele receberá seu componente Texto e o salvará na variável de tela, na qual podemos escrever o mapa com facilidade. Em seguida, ele cria uma sequência cujo valor está inicialmente vazio. Vamos preencher esta linha com o nosso mapa marcado com símbolos.

Normalmente, percorremos o mapa em um loop, começando em 0 e indo até o final de seu comprimento. Mas, para preencher a linha, começamos com a primeira linha de texto, ou seja, a linha superior. Portanto, no eixo y, precisamos nos mover em um loop na direção oposta, indo do final ao início da matriz. Mas o eixo x da matriz vai da esquerda para a direita, assim como o texto, então isso nos convém.


Nesse ciclo, verificamos cada célula do mapa para descobrir o que está nele. Até agora, apenas inicializamos as células como um novo Tile (), que cortamos para a sala, para que todos os outros retornem um erro ao tentar acessar. Então, primeiro precisamos verificar se algo nessa célula e fazemos isso verificando se há nulo na célula. Se não for nulo, continuamos a trabalhar, mas se for nulo, não há nada lá dentro, para que possamos adicionar espaço vazio ao mapa.


Portanto, para cada célula não vazia, verificamos seu tipo e adicionamos o símbolo correspondente. Queremos que as paredes sejam indicadas pelo símbolo "#" e os pisos sejam indicados pelo ".". E enquanto temos apenas esses dois tipos. Mais tarde, quando adicionarmos o jogador, monstros e armadilhas, tudo ficará um pouco mais complicado.


Além disso, precisamos executar quebras de linha ao chegar ao final da linha da matriz, para que as células com a mesma posição x fiquem diretamente uma abaixo da outra. Iremos verificar a cada iteração do loop se a célula é a última da linha e adicionar uma quebra de linha com o caractere especial "\ n".


Isso é tudo. Em seguida, saímos do loop para poder adicionar essa linha após a conclusão ao objeto de texto na cena.



Parabéns! Você concluiu o script que cria a sala e o exibe na tela. Agora só precisamos colocar essas linhas em ação. Não usamos Start () no script DungeonGenerator, porque queremos ter um script separado para controlar tudo o que é executado no início do jogo, incluindo a geração do mapa, mas também a configuração do jogador, inimigos, etc. Portanto, esse outro script conterá a função Start () e, se necessário, chamará as funções do nosso script. O script DungeonGenerator possui uma função Initialize, que é pública, e FirstRoom e DrawMap não são públicos. Initialize simplesmente inicializa as variáveis ​​para personalizar o processo de geração de masmorra, por isso precisamos de outra função que chame o processo de geração, que deve ser pública para que possa ser chamada de outros scripts.Por enquanto, ela chamará apenas a função FirstRoom () e, em seguida, a função DrawMap (), passando um valor verdadeiro para que ela desenhe um mapa ASCII. Ah, ou não, é ainda melhor - vamos criar uma variável pública isASCII, que pode ser incluída no inspetor, e passar essa variável como parâmetro para a função. Bem.


Então, agora vamos criar um script do GameManager. Será o próprio script que controla todos os elementos de alto nível do jogo, por exemplo, criando um mapa e o curso dos movimentos. Vamos remover a função Update (), adicionar uma variável do tipo DungeonGenerator chamada dungeonGenerator e criar uma instância dessa variável na função Start ().


Depois disso, simplesmente chamamos as funções InitializeDungeon () e GenerateDungeon () do dungeonGenerator, nessa ordem . Isso é importante - primeiro você precisa inicializar as variáveis ​​e somente depois disso começar a construir com base nas mesmas.


Nesta parte, o código está completo. Precisamos criar um objeto de jogo vazio no painel de hierarquia, renomeá-lo para GameManager e anexar os scripts GameManager e DungeonGenerator a ele. E então defina os valores do gerador de masmorra no inspetor. Você pode tentar esquemas diferentes para o gerador, e eu decidi sobre isso:


Agora basta clicar em play e assistir a mágica! Você deve ver algo semelhante na tela do jogo:


Parabéns, agora temos um quarto!

Eu queria que colocássemos o personagem do jogador lá e o fizesse se mexer, mas o post já era bastante longo. Portanto, na próxima parte, podemos prosseguir diretamente para a implementação do restante do algoritmo da masmorra, ou podemos colocar um jogador nele e ensiná-lo a se mover. Vote no que você mais gosta nos comentários do artigo original.

MapManager.cs:

using System.Collections;
using System; // So the script can use the serialization commands
using System.Collections.Generic;
using UnityEngine;

public class MapManager {
    public static Tile[,] map; // the 2-dimensional map with the information for all the tiles
}

[Serializable] // Makes the class serializable so it can be saved out to a file
public class Tile { // Holds all the information for each tile on the map
    public int xPosition; // the position on the x axis
    public int yPosition; // the position on the y axis
    [NonSerialized]
    public GameObject baseObject; // the map game object attached to that position: a floor, a wall, etc.
    public string type; // The type of the tile, if it is wall, floor, etc
}

[Serializable]
public class Position { //A class that saves the position of any cell
    public int x;
    public int y;
}

[Serializable]
public class Wall { // A class for saving the wall information, for the dungeon generation algorithm
    public List<Position> positions;
    public string direction;
    public int length;
    public bool hasFeature = false;
}

[Serializable]
public class Feature { // A class for saving the feature (corridor or room) information, for the dungeon generation algorithm
    public List<Position> positions;
    public Wall[] walls;
    public string type;
    public int width;
    public int height;
}

DungeonGenerator.cs:

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

public class DungeonGenerator : MonoBehaviour
{
    public int mapWidth;
    public int mapHeight;

    public int widthMinRoom;
    public int widthMaxRoom;
    public int heightMinRoom;
    public int heightMaxRoom;

    public int maxCorridorLength;
    public int maxFeatures;

    public bool isASCII;

    public void InitializeDungeon() {
        MapManager.map = new Tile[mapWidth, mapHeight];
    }

    public void GenerateDungeon() {
        FirstRoom();
        DrawMap(isASCII);
    }

    void FirstRoom() {
        Feature room = new Feature();
        room.positions = new List<Position>();

        int roomWidth = Random.Range(widthMinRoom, widthMaxRoom);
        int roomHeight = Random.Range(heightMinRoom, heightMaxRoom);

        int xStartingPoint = mapWidth / 2;
        int yStartingPoint = mapHeight / 2;

        xStartingPoint -= Random.Range(0, roomWidth);
        yStartingPoint -= Random.Range(0, roomHeight);

        room.walls = new Wall[4];

        for (int i = 0; i < room.walls.Length; i++) {
            room.walls[i] = new Wall();
            room.walls[i].positions = new List<Position>();
            room.walls[i].length = 0;

            switch (i) {
                case 0:
                    room.walls[i].direction = "South";
                    break;
                case 1:
                    room.walls[i].direction = "North";
                    break;
                case 2:
                    room.walls[i].direction = "West";
                    break;
                case 3:
                    room.walls[i].direction = "East";
                    break;
            }
        }

        for (int y = 0; y < roomHeight; y++) {
            for (int x = 0; x < roomWidth; x++) {
                Position position = new Position();
                position.x = xStartingPoint + x;
                position.y = yStartingPoint + y;

                room.positions.Add(position);

                MapManager.map[position.x, position.y] = new Tile();
                MapManager.map[position.x, position.y].xPosition = position.x;
                MapManager.map[position.x, position.y].yPosition = position.y;

                if (y == 0) {
                    room.walls[0].positions.Add(position);
                    room.walls[0].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (y == (roomHeight - 1)) {
                    room.walls[1].positions.Add(position);
                    room.walls[1].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (x == 0) {
                    room.walls[2].positions.Add(position);
                    room.walls[2].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else if (x == (roomWidth - 1)) {
                    room.walls[3].positions.Add(position);
                    room.walls[3].length++;
                    MapManager.map[position.x, position.y].type = "Wall";
                }
                else {
                    MapManager.map[position.x, position.y].type = "Floor";
                }
            }
        }

        room.width = roomWidth;
        room.height = roomHeight;
        room.type = "Room";
    }

    void DrawMap(bool isASCII) {
        if (isASCII) {
            Text screen = GameObject.Find("ASCIITest").GetComponent<Text>();

            string asciiMap = "";

            for (int y = (mapHeight - 1); y >= 0; y--) {
                for (int x = 0; x < mapWidth; x++) {
                    if (MapManager.map[x,y] != null) {
                        switch (MapManager.map[x, y].type) {
                            case "Wall":
                                asciiMap += "#";
                                break;
                            case "Floor":
                                asciiMap += ".";
                                break;
                        }
                    } else {
                        asciiMap += " ";
                    }

                    if (x == (mapWidth - 1)) {
                        asciiMap += "\n";
                    }
                }
            }

            screen.text = asciiMap;
        }
    }
}

GameManager.cs:

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

public class GameManager : MonoBehaviour
{
    DungeonGenerator dungeonGenerator;
    
    void Start() {
        dungeonGenerator = GetComponent<DungeonGenerator>();

        dungeonGenerator.InitializeDungeon();
        dungeonGenerator.GenerateDungeon();
    }
}

All Articles