From 2fb787a744d4f7a37d81233d2913a5ef39122f73 Mon Sep 17 00:00:00 2001 From: Matheus Date: Thu, 28 Aug 2025 00:38:48 -0300 Subject: Comentarios --- scripts/map/DungeonGenerator.cs | 88 ++++++++++++++++++++++++++++++++++++-- scripts/map/FieldOfView.cs | 2 + scripts/map/Map.cs | 32 ++++++++++++-- scripts/map/MapData.cs | 95 ++++++++++++++++++++++++++++++++++++++++- scripts/map/MapDivision.cs | 37 +++++++++++++--- scripts/map/Tile.cs | 32 ++++++++++++++ scripts/map/TileDefinition.cs | 4 +- 7 files changed, 275 insertions(+), 15 deletions(-) (limited to 'scripts/map') diff --git a/scripts/map/DungeonGenerator.cs b/scripts/map/DungeonGenerator.cs index ebb7a3d..1da7e16 100644 --- a/scripts/map/DungeonGenerator.cs +++ b/scripts/map/DungeonGenerator.cs @@ -1,26 +1,51 @@ using Godot; +/// +/// A classe dungeonGenerator cria exatamente um andar da masmorra. +/// Ela é chamada quando necessário. +/// public partial class DungeonGenerator : Node { + /// + /// Coleção de todos os inimigos que o gerador tem acesso. + /// private static readonly Godot.Collections.Array enemies = [ GD.Load("res://assets/definitions/actor/Skeleton.tres") ]; + /// + /// Dimensões do mapa a ser criado. + /// [ExportCategory("Dimension")] [Export] private int width = 80; [Export] private int height = 60; + /// + /// Gerador de números aleatórios + /// [ExportCategory("RNG")] private RandomNumberGenerator rng = new(); + /// + /// Qual seed utilizar. + /// [Export] private ulong seed; + /// + /// Se será utilizada a nossa seed ou a seed padrão da classe RandomNumberGenerator. + /// [Export] private bool useSeed = true; + /// + /// Quantas iterações do algoritmo chamar. + /// [Export] private int iterations = 3; + /// + /// Quantidade máxima de inimigos por sala. + /// [ExportCategory("Monster RNG")] [Export] private int maxMonsterPerRoom = 2; @@ -33,6 +58,11 @@ public partial class DungeonGenerator : Node } } + /// + /// Transforma o tile da posição especificada em chão. + /// + /// o mapa + /// posição para colocar o chão. private static void CarveTile(MapData data, Vector2I pos) { Tile tile = data.GetTile(pos); @@ -41,6 +71,11 @@ public partial class DungeonGenerator : Node tile.SetDefinition(MapData.floorDefinition); } + /// + /// Preenche uma área retangular com chão. + /// + /// O mapa + /// Área para preencher com chão private static void CarveRoom(MapData data, Rect2I room) { for (int y = room.Position.Y; y < room.End.Y; y++) @@ -52,25 +87,34 @@ public partial class DungeonGenerator : Node } } + /// + /// Gera um andar da masmorra. + /// Inimigos são colocados conforme configurações. + /// O jogador é colocado na primeira sala gerada. + /// + /// Jogador. + /// O mapa gerado. public MapData GenerateDungeon(Player player) { MapData data = new MapData(width, height, player); - data.InsertActor(player); - player.Map_Data = data; - + // Divisão mestre que engloba o mapa inteiro. MapDivision root = new MapDivision(0, 0, width, height); + // Chama o algoritmo para dividir o mapa. root.Split(iterations, rng); bool first = true; + // Coloca os corredores. TunnelDivisions(data, root); + // Cria as salas com base nas divisões geradas. foreach(MapDivision division in root.GetLeaves()) { Rect2I room = new(division.Position, division.Size); + // A sala não pode oculpar a divisão inteira, senão não haveriam paredes. room = room.GrowIndividual( -rng.RandiRange(1, 2), -rng.RandiRange(1, 2), @@ -78,28 +122,40 @@ public partial class DungeonGenerator : Node -rng.RandiRange(1, 2) ); + // De fato cria a sala. CarveRoom(data, room); + // Colocamos o jogador na primeira sala. if (first) { first = false; player.GridPosition = room.GetCenter(); } + // Colocamos os inimigos na sala. PlaceEntities(data, room); } + // Feito o mapa, inicializamos o algoritmo de pathfinding. data.SetupPathfinding(); return data; } + /// + /// Popula uma sala com inimigos. + /// + /// O mapa + /// A sala. private void PlaceEntities(MapData data, Rect2I room) { + // Define quantos monstros serão colocados na sala int monsterAmount = rng.RandiRange(0, maxMonsterPerRoom); for (int i = 0; i < monsterAmount; i++) { + // Escolhe um lugar aleatório na sala. Vector2I position = new( rng.RandiRange(room.Position.X, room.End.X - 1), rng.RandiRange(room.Position.Y, room.End.Y - 1) ); + // Só podemos colocar um ator por ponto no espaço. bool canPlace = true; foreach (Actor actor in data.Actors) { if (actor.GridPosition == position) { @@ -108,6 +164,7 @@ public partial class DungeonGenerator : Node } } + // Se possível, criamos um inimigo aleatório na posição escolhida. if (canPlace) { EnemyDefinition definition = enemies.PickRandom(); Enemy enemy = new Enemy(position, data, definition); @@ -116,6 +173,13 @@ public partial class DungeonGenerator : Node } } + /// + /// Preenche uma linha horizontal com chão. + /// + /// O mapa + /// Eixo y do corredor. + /// Início do corredor + /// Final do corredor. private static void HorizontalCorridor(MapData data, int y, int xBegin, int xEnd) { int begin = (xBegin < xEnd) ? xBegin : xEnd; int end = (xEnd > xBegin) ? xEnd : xBegin; @@ -124,6 +188,13 @@ public partial class DungeonGenerator : Node } } + /// + /// Preenche uma linha vertical com chão. + /// + /// O mapa. + /// Eixo x do corredor. + /// Início do corredor + /// Final do corredor. private static void VerticalCorridor(MapData data, int x, int yBegin, int yEnd) { int begin = (yBegin < yEnd) ? yBegin : yEnd; int end = (yEnd > yBegin) ? yEnd : yBegin; @@ -132,11 +203,22 @@ public partial class DungeonGenerator : Node } } + /// + /// Cria corredores vertical e horizontal para unir dois pontos no mapa. + /// + /// O mapa + /// Ponto inicial + /// Ponto final. private void TunnelBetween(MapData data, Vector2I start, Vector2I end) { HorizontalCorridor(data, start.Y, start.X, end.X); VerticalCorridor(data, end.X, start.Y, end.Y); } + /// + /// Cria recursivamente corredores entre o centro de cada divisão do mapa. + /// + /// O mapa + /// Divisão mestre. private void TunnelDivisions(MapData data, MapDivision root) { if (root.IsLeaf) { return; diff --git a/scripts/map/FieldOfView.cs b/scripts/map/FieldOfView.cs index eee1ae9..cadaf74 100644 --- a/scripts/map/FieldOfView.cs +++ b/scripts/map/FieldOfView.cs @@ -2,6 +2,8 @@ using Godot; // Copiado e adaptado deste cara aqui: https://www.roguebasin.com/index.php?title=C%2B%2B_shadowcasting_implementation e deste também https://selinadev.github.io/08-rogueliketutorial-04/ +// Eu não vou mentir, li como o algoritmo funciona, mas mesmo assim não entendi. + public partial class FieldOfView : Node { private Godot.Collections.Array fov = []; diff --git a/scripts/map/Map.cs b/scripts/map/Map.cs index 29f5e62..e62aa21 100644 --- a/scripts/map/Map.cs +++ b/scripts/map/Map.cs @@ -1,13 +1,24 @@ using Godot; -using System; +/// +/// A parte visual do mapa. +/// public partial class Map : Node2D { + /// + /// Dados do mapa. + /// public MapData Map_Data { get; private set; } + /// + /// raio de alcance da visão do jogador. + /// [Export] private int fovRadius = 12; + /// + /// Gerador de mapas. + /// private DungeonGenerator generator; FieldOfView fieldOfView; @@ -18,37 +29,50 @@ public partial class Map : Node2D public override void _Ready() { base._Ready(); - + // Começamos obtendo nós relevantes para o mapa. generator = GetNode("Generator"); fieldOfView = GetNode("FieldOfView"); tilesNode = GetNode("Tiles"); actorsNode = GetNode("Actors"); } + /// + /// Coloca todos os tiles do mapa no mundo do jogo. + /// private void PlaceTiles() { foreach (Tile tile in Map_Data.Tiles) { tilesNode.AddChild(tile); } } + /// + /// Coloca todos os tiles do mapa no mundo do jogo. + /// private void PlaceActors() { foreach (Actor actor in Map_Data.Actors) { actorsNode.AddChild(actor); } } + /// + /// Cria um andar da masmorra utilizando o gerador de mapa. + /// + /// O gerador de mapas precisa do jogador. public void Generate(Player player) { Map_Data = generator.GenerateDungeon(player); - player.Map_Data = Map_Data; - PlaceTiles(); PlaceActors(); } + /// + /// Atualiza o campo de visão do mapa com base em uma coordenada. + /// + /// Centro de visão, normalmente é a posição do jogador. public void UpdateFOV(Vector2I pos) { fieldOfView.UpdateFOV(Map_Data, pos, fovRadius); + // Esconde ou revela atores com base no campo de visão. foreach (Actor actor in Map_Data.Actors) { actor.Visible = Map_Data.GetTile(actor.GridPosition).IsInView; } diff --git a/scripts/map/MapData.cs b/scripts/map/MapData.cs index be466ba..3b03de8 100644 --- a/scripts/map/MapData.cs +++ b/scripts/map/MapData.cs @@ -1,40 +1,74 @@ using Godot; -using System; -using System.Runtime.InteropServices; +/// +/// Classe que cuida dos dados e da parte lógica do mapa. +/// O mapa é o cenário onde as ações do jogo ocorrem. +/// Mais especificamente, o mapa é um único andar da masmorra. +/// public partial class MapData : RefCounted { public static readonly TileDefinition wallDefinition = GD.Load("res://assets/definitions/tiles/wall.tres"); public static readonly TileDefinition floorDefinition = GD.Load("res://assets/definitions/tiles/floor.tres"); + /// + /// Largura do mapa. + /// public int Width { get; private set; } + /// + /// Altura do mapa. + /// public int Height { get; private set; } + /// + /// Os tiles que compõem o mapa. + /// public Godot.Collections.Array Tiles { get; private set; } = []; + /// + /// O jogador é especial e por isso o mapa faz questão de rastreá-lo. + /// public Player Player { get; set; } + /// + /// Lista de todos os atores dentro do mapa. + /// public Godot.Collections.Array Actors { get; private set; } = []; private AStarGrid2D pathfinder; + /// + /// Objeto do Godot que utiliza do algoritmo A* para calcular + /// caminhos e rotas. + /// public AStarGrid2D Pathfinder { get => pathfinder; } + /// + /// Peso do ator no pathfinder. + /// A IA irá evitar de passar por espaços com peso alto. + /// private static float ActorWeight = 10.0f; + /// + /// Inicializa o pathfinder; + /// public void SetupPathfinding() { pathfinder = new AStarGrid2D { + //A região é o mapa inteiro. Region = new Rect2I(0, 0, Width, Height) }; + // Atualiza o pathfinder para a região definida. pathfinder.Update(); + // Define quais pontos do mapa são passáveis ou não. for (int y = 0; y < Height; y++) { for (int x = 0; x < Width; x++) { Vector2I pos = new Vector2I(x, y); Tile tile = GetTile(pos); + // Pontos sólidos são impossíveis de passar. pathfinder.SetPointSolid(pos, !tile.IsWalkable); } } + // Registra todos os atores em cena. foreach (Actor actor in Actors) { if (actor.BlocksMovement) { RegisterBlockingActor(actor); @@ -43,10 +77,21 @@ public partial class MapData : RefCounted } + /// + /// Define um peso na posição de um ator para que a IA evite de passar por lá. + /// Ênfase em evitar. Se o único caminho para o destino estiver bloqueado + /// por um ator, o jogo tentará andar mesmo assim. + /// + /// O ator em questão. public void RegisterBlockingActor(Actor actor) { pathfinder.SetPointWeightScale(actor.GridPosition, ActorWeight); } + /// + /// Remove o peso na posição de um ator. + /// Quando um ator move sua posição, devemos tirar o peso de sua posição anterior. + /// + /// O ator em questão. public void UnregisterBlockingActor(Actor actor) { pathfinder.SetPointWeightScale(actor.GridPosition, 0); } @@ -56,10 +101,19 @@ public partial class MapData : RefCounted Height = height; Player = player; + // Como o jogador é criado antes do mapa, precisamos + // atualizá-lo com o novo mapa. + player.Map_Data = this; + InsertActor(player); SetupTiles(); } + /// + /// Cria novos Tiles até preencher as dimensões do mapa. + /// É importante que estes tiles sejam paredes, o gerador de mapas + /// não cria paredes por conta própria. + /// private void SetupTiles() { for (int i = 0; i < Height; i++) { @@ -70,16 +124,31 @@ public partial class MapData : RefCounted } } + /// + /// Registra um ator no mapa. A existência de um ator não é considerada se ele não + /// estiver registrado no mapa. + /// + /// O ator em questão public void InsertActor(Actor actor) { Actors.Add(actor); } + /// + /// Converte uma coordenada em um índice para acessar a lista de tiles. + /// + /// Vetor posição + /// Índice na lista de tiles. -1 se estiver fora do mapa. private int GridToIndex(Vector2I pos) { if (!IsInBounds(pos)) return -1; return pos.Y * Width + pos.X; } + /// + /// Se uma coordenada está dentro da área do mapa. + /// + /// Vetor posição + /// Se o vetor está dentro do mapa. private bool IsInBounds(Vector2I pos) { if (pos.X < 0 || pos.Y < 0) { return false; @@ -91,6 +160,11 @@ public partial class MapData : RefCounted return true; } + /// + /// Obtém o tile na posição desejada. + /// + /// Vetor posição + /// O tile na posição, nulo se for fora do mapa. public Tile GetTile(Vector2I pos) { int index = GridToIndex(pos); @@ -99,10 +173,21 @@ public partial class MapData : RefCounted return Tiles[index]; } + /// + /// Obtém o tile na posição desejada. + /// + /// x da coordenada + /// y da coordenada + /// O tile na posição, nulo se for fora do mapa. public Tile GetTile(int x, int y) { return GetTile(new Vector2I(x, y)); } + /// + /// Obtém o ator na posição especificada. + /// + /// Vetor posição + /// O ator na posição especificada, nulo se não houver. public Actor GetBlockingActorAtPosition(Vector2I pos) { foreach (Actor actor in Actors) { if (actor.GridPosition == pos && actor.BlocksMovement) { @@ -112,6 +197,12 @@ public partial class MapData : RefCounted return null; } + /// + /// Verifica se é possível caminhar na coordenada especificada. + /// Este método será removido. + /// + /// Vetor posição + /// Se é possível caminhar nesta posição public bool IsTileWalkable(Vector2I pos) { Tile tile = GetTile(pos); diff --git a/scripts/map/MapDivision.cs b/scripts/map/MapDivision.cs index 15a6be8..3273775 100644 --- a/scripts/map/MapDivision.cs +++ b/scripts/map/MapDivision.cs @@ -1,21 +1,34 @@ -using System.Linq; -using System.Numerics; -using System.Xml.Schema; using Godot; +/// +/// Classe utilizada pelo gerador de mapas. +/// Uma divisão é uma região retangular de espaço que pode +/// conter dentro de si duas regiões menores *ou* uma sala. +/// Uma divisão é uma árvore binária que possui espaço para salas em suas folhas. +/// public partial class MapDivision : RefCounted { + /// + /// Região retangular da divisão. + /// public Vector2I Position { get; set; } public Vector2I Size { get; set; } - + public Vector2I Center { get => new(Position.X + Size.X/2, Position.Y + Size.Y/2); } + /// + /// Filhos da árvore + /// private MapDivision left; public MapDivision Left { get => this.left; } private MapDivision right; public MapDivision Right { get => this.right; } + /// + /// Se a divisão atual for uma folha. + /// As folhas representam salas. + /// public bool IsLeaf { get => left == null && right == null; } @@ -35,6 +48,10 @@ public partial class MapDivision : RefCounted { Size = new(width, height); } + /// + /// É conveniente ter acesso à todas as folhas da árvore. + /// + /// Lista com todas as folhas da árvore. public Godot.Collections.Array GetLeaves() { if (IsLeaf) { Godot.Collections.Array list = []; @@ -44,10 +61,20 @@ public partial class MapDivision : RefCounted { return left.GetLeaves() + right.GetLeaves(); } + /// + /// Algoritmo para gerar as divisões. + /// O mapa começa com uma única divisão que oculpa sua extensão completa. + /// Depois disso, ela se dividirá recursivamente n vezes. + /// As divisões nas folhas representam espaços onde pode gerar uma sala. + /// + /// Número de iterações + /// Gerador de números public void Split(int iterations, RandomNumberGenerator rng) { float SplitRatio = rng.RandfRange(0.35f, 0.65f); bool horizontalSplit = Size.X <= Size.Y; - + + // Eu defini um limite mínimo de 4 de altura e 4 de largura para divisões. + if (horizontalSplit) { int leftHeight = (int) (Size.Y * SplitRatio); if (leftHeight > 4 && Size.Y - leftHeight > 4) { diff --git a/scripts/map/Tile.cs b/scripts/map/Tile.cs index e050701..67b9be5 100644 --- a/scripts/map/Tile.cs +++ b/scripts/map/Tile.cs @@ -1,14 +1,33 @@ using Godot; using System; +/// +/// O mundo do jogo é composto por Tiles. +/// Um tile é um quadrado de 16x16 que representa uma +/// unidade discreta do cenário. Tiles podem agir como +/// parede, chão, ou outras funções. +/// public partial class Tile : Sprite2D { + /// + /// A definição do tile carrega seus valores padrão. + /// private TileDefinition definition; + /// + /// Determina se atores podem andar em cima do Tile. + /// public bool IsWalkable { get; private set; } + /// + /// Determina se o tile bloqueia visão. + /// public bool IsTransparent { get; private set; } private bool isExplored = false; + /// + /// Se o jogador já viu este tile antes. + /// Tiles não descobertos são invisíveis. + /// public bool IsExplored { get => this.isExplored; set { @@ -20,6 +39,10 @@ public partial class Tile : Sprite2D } private bool isInView = false; + /// + /// Se o jogador vê o tile neste exato momento. + /// Elementos neste tile estão dentro do campo de visão do jogador. + /// public bool IsInView { get => this.isInView; set { @@ -32,12 +55,21 @@ public partial class Tile : Sprite2D public Tile(Vector2I pos, TileDefinition definition) { + // Tile herda da classe Sprite2D. + // Por padrão, a posição do Sprite2D é no centro de sua textura. + // Para o jogo, faz mais sentido que a posição seja no + // canto superior esquerdo. Centered = false; + // Tiles começam invisíveis porque não foram vistos pelo jogador. Visible = false; Position = Grid.GridToWorld(pos); SetDefinition(definition); } + /// + /// Define as características do tile. + /// + /// Definição do tile. public void SetDefinition(TileDefinition definition) { this.definition = definition; Texture = definition.Texture; diff --git a/scripts/map/TileDefinition.cs b/scripts/map/TileDefinition.cs index fbd14a1..235508a 100644 --- a/scripts/map/TileDefinition.cs +++ b/scripts/map/TileDefinition.cs @@ -1,6 +1,8 @@ using Godot; -using System; +/// +/// Define as características de um tile. +/// [GlobalClass] public partial class TileDefinition : Resource { -- cgit v1.2.3