Ga naar inhoud

Zeshoek Grid

Doelstelling

Nu dat wij de zeshoek helpers hebben moeten ze toegepast worden om een echte grid te hebben, dat de speler kan zien.

Toegepaste oplossing

Client

Base Grid

Als eerste is hier de basis grid gameobject, dat de originele grid object vervangt. Omdat deze natuurlijk een compleet andere soort tiles heeft.

Deze grid staat ook niet in de Engine namespace, omdat het echt voor het spel zelf is. Eigenlijk had de originele grid ook daar niet moeten staan.

Dit is een abstracte grid zodat het ook als basis gebruikt kan worden door de grid voor eenheden en gebouwen.

public abstract class BaseGrid<T>(int radius = 4, float cellRadius = 20f, int layer = 0, string id = "") : GameObject(layer, id), IPositioningGrid where T : BaseCell
{
    public BaseHexGrid<T> grid = new HashHexGrid<T>();

    public int radius = radius;
    public float cellRadius = cellRadius;

    public Vector2 TileSize {
        get => new(
            (float)Math.Sqrt(3) * 2f * cellRadius,
            4f * cellRadius
        );
    }

Hier zijn een paar algemene grid methods om de grid te gebruiken.

Het eerste deel zijn methods om posities van specifieke zeshoeken te berekenen, wat hier word gedaan in plaats van in de tile omdat de interne grid ervoor nodig is.

Het tweede deel zijn meer algemene methods die overgenomen zijn uit de originele grid. Om tiles op te halen, toe te voegen en verwijderen.

    public Vector2 GetPosAtHex(HexVector pos)
        => grid.HexToPos(pos) * cellRadius * 2f;

    public Vector2 GetGlobalPosAtHex(HexVector pos)
        => GetPosAtHex(pos) + GlobalPosition;

    public Rectangle GetBoundingBoxAtHex(HexVector pos)
    {
        var vec = GetPosAtHex(pos);
        var size = TileSize;

        return new(
            (int)(vec.X - size.X/2),
            (int)(vec.Y - size.Y/2),
            (int)size.X,
            (int)size.Y
        );
    }

    public Rectangle GetGlobalBoundingBoxAtHex(HexVector pos)
    {
        var relative = GetBoundingBoxAtHex(pos);
        relative.Offset(GlobalPosition);
        return relative;
    }

    public bool IsInRange(HexVector pos)
        => pos.HexMagnitude() <= radius;

    public void Add(T cell)
    {
        if (Get(cell.pos) != null)
            throw new ArgumentException($"Tried adding tile at pos {cell.pos} which is already in use.", nameof(cell));

        cell.Parent = this;
        grid.Add(cell.pos, cell);
    }

    public bool Remove(HexVector pos)
        => grid.Remove(pos);

    public bool Remove(T cell)
    {
        if (cell.Parent != this)
            throw new ArgumentException("Tried to remove cell from different grid.", nameof(cell));

        if (Get(cell.pos) != cell)
            throw new ArgumentException("Cell is not at cell's position in grid", nameof(cell));

        return Remove(cell.pos);
    }

    public T? Get(HexVector pos)
    {
        if (!IsInRange(pos))
        {
            throw new ArgumentException($"Tried to get pos {pos} but it is outside of the grid's range", nameof(pos));
        }

        return grid.ElementAt(pos);
    }

Hieronder worden worden er methods van game objects ge-override, en daarin worden dezelfde methods voor elke tile opgeroepen. Net zoals in de originele grid gameobject.

    #region GameObject overrides
    public override void HandleInput(InputHelper inputHelper)
    {
        base.HandleInput(inputHelper);

        foreach (var tile in grid.Values)
        {
            tile.HandleInput(inputHelper);
        }
    }

    public override void Update(GameTime gameTime)
    {
        base.Update(gameTime);

        foreach (var tile in grid.Values)
        {
            tile.Update(gameTime);
        }
    }

    public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    {
        base.Draw(gameTime, spriteBatch);

        foreach (var tile in grid.Values)
        {
            tile.Draw(gameTime, spriteBatch);
        }
    }

    public override void Reset()
    {
        base.Reset();

        foreach (var tile in grid.Values)
        {
            tile.Reset();
        }
    }
    #endregion
}

Positioning Grid

Hier tussenin heb ik ook nog een interface moeten maken, met methods om een tile te positioneren.

Dit was nodig omdat in C# een andere generic altijd als een andere type word gezien, zelfs als de generic buiten de generic gecast kan worden.

Kan gecast worden naar BaseGrid<BaseCell>
TileGrid<BaseCell>
TileGrid<Tile>
BaseGrid<Tile>

Terwijl Tile zelf wel gecast kan worden naar BaseCell.

Dit word gebruikt in Basis Cell om te checken dat de parent de juiste methods heeft, zodat die gebruikt kunnen worden om de cell zelf te positioneren.

public interface IPositioningGrid
{
    public Vector2 TileSize { get; }

    public Vector2 GetPosAtHex(HexVector pos);
    public Vector2 GetGlobalPosAtHex(HexVector pos);

    public Rectangle GetBoundingBoxAtHex(HexVector pos);
    public Rectangle GetGlobalBoundingBoxAtHex(HexVector pos);

    public bool IsInRange(HexVector pos);
}

Base Cell

Hier is de basis cell game object. Het gaat ervan uit dat het altijd kind van een grid is, omdat anders speciale logica dat erin staat niet zou kunnen werken.

Dit is ook abstract om net zoals met de basis grid het gemakkelijk te maken om eenheden en gebouwen versies hiervan te maken.

Het is zeer simpel dankzij de Positioning Grid interface.

public abstract class BaseCell(HexVector pos, int layer = 0, string id = "") : GameObject(layer, id)
{
    public HexVector pos = pos;

    public IPositioningGrid PositioningGrid
        => parent is IPositioningGrid grid
                ? grid
                : throw new ArgumentNullException(nameof(parent), "Parent of cell is not a grid.");

    public override Vector2 Position => PositioningGrid.GetPosAtHex(pos);
    public override Vector2 GlobalPosition => PositioningGrid.GetGlobalPosAtHex(pos);

    public Vector2 Size => PositioningGrid.TileSize;
    public override Rectangle BoundingBox => PositioningGrid.GetBoundingBoxAtHex(pos);
    public override Rectangle GlobalBoundingBox => PositioningGrid.GetGlobalBoundingBoxAtHex(pos);
}

TileGrid

Hier is de echte grid met echte tiles.

Het voegt nu alleen nog een beetje extra logica toe voor het testen. Het krijgt de zeshoek onder de muis en verkleurt het op basis van wat er op dat coördinaat staat. Daarnaast tekent het ook een lege zeshoek voor elke lege coördinaat binnen het bereik.

public class TileGrid(int radius = 4, float tileRadius = 20f, int layer = 0, string id = "") : BaseGrid<Tile>(radius, tileRadius, layer, id)
{
    public HexVector MousePos { get; protected set; }

    public override void HandleInput(InputHelper inputHelper)
    {
        base.HandleInput(inputHelper);

        MousePos = grid.PosToHex((
            (inputHelper.MousePosition - GlobalPosition) / (cellRadius * 2f)
        ).ToNumerics());
    }

    public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    {
        var tileSprite = App.AssetManager.GetSprite("Images/EmptyTile");
        foreach (var pos in HexVector.GetInRange(radius))
        {
            if (grid.ElementAt(pos) == null)
            {
                spriteBatch.BetterDraw(
                    tileSprite,
                    GetGlobalPosAtHex(pos),
                    Vector2.One * TileSize.Y // square like the sprite
                );
            }
        }

        base.Draw(gameTime, spriteBatch);

        spriteBatch.BetterDraw(
            tileSprite,
            GetGlobalPosAtHex(MousePos),
            Vector2.One * TileSize.Y, // square like the sprite
            0f,
            !IsInRange(MousePos)
                ? Color.Red
                : grid.ElementAt(MousePos) != null
                    ? Color.Orange
                    : Color.Green
        );
    }
}

Tile

Als laatste is hier de echte tile. Voor nu heeft het een simpele kleur, wat later vervangen zal worden door een type zijde voor elke zijde.

public class Tile(HexVector pos, int layer = 0, string id = "") : BaseCell(pos, layer, id)
{
    protected int rotation;
    public int Rotation {
        get => rotation;
        set => rotation = MathExtras.Mod(value, 6);
    }

    public Color color = Color.White;

Hier word tijdelijk de tile getekend met een simpele kleurtje, wat ook tijdelijk is. Ook als het spel voor debuggen is gecompileerd word er op de tile de coördinaten getekend.

    public override void Draw(GameTime gameTime, SpriteBatch spriteBatch)
    {
        var image = App.AssetManager.GetSprite("Images/EmptyTile");
        spriteBatch.BetterDraw(
            image,
            GlobalPosition,
            Vector2.One * Size.Y, // square like the sprite
            Rotation / 3f * MathHelper.Pi,
            color
        );


        #if DEBUG
        spriteBatch.BetterDrawString(
            App.AssetManager.Content.Load<SpriteFont>("Fonts/SpriteFont@20px"),
            pos.ToString(),
            GlobalPosition,
            Vector2.One * .5f
        );
        #endif
    }
}

Server

GameInfo

Hier heb ik een server versie van de grid toegevoegd aan de game info.

Tijdelijk word er een lijn tussen twee willekeurige punten gemaakt per spel. Dit is om te testen dat elke speler in het spel dezelfde tiles op de juiste plek ontvangt.

public class GameInfo
{
    public readonly Room room;
    public readonly Random random = new();

    public BaseHexGrid<Tile> tileGrid = new HashHexGrid<Tile>();
    public int range;

    public GameInfo(Room room, int range = 4)
    {
        this.room = room;
        this.range = range;


        foreach (var tile in HexVector
            .GetLine(
                HexVector.GetRandomInRange(random, range),
                HexVector.GetRandomInRange(random, range)
            ).Select(p => new Tile(p))
        ) {
            tileGrid.Add(tile.pos, tile);
        }
    }

    public PublicGameInfo ToPublicGameInfo()
        => new(
            range,
            tileGrid.Values.Select(t => t.ToPublicTile()).ToArray()
        );
}

GameDataPacket

Deze grid informatie word naar de speler verstuurd via de GameInfo packet, die word verstuurd wanneer het spel start in een room of wanneer een speler een al gestarte spel joined.

public class GameDataPacket() : BasePacket("GameData", PT.ToClient)
{
    public PublicGameInfo gameInfo { get; set; }
}

Publieke data

De data die verstuurd word in deze publieke GameInfo struct verstuurd.

public struct PublicGameInfo(int range, PublicTile[] tiles)
{
    public int range { get; set; } = range;
    public PublicTile[] grid { get; set; } = tiles;
}

Die gevuld is met deze publieke tile.

public struct PublicTile(HexVector pos, int rotation = 0)
{
    public HexVector pos { get; set; } = pos;
    public int rotation { get; set; } = rotation;
}

Klassendiagram

classDiagram class GameObject { <<extern>> } BaseCell --|> GameObject BaseCell --> IPositioningGrid class BaseCell { <<abstract>> +HexVector pos +GetPositioningGrid() IPositioningGrid +GetPosition() Vector2 +GetGlobalPosition() Vector2 +GetSize() Vector2 +GetBoundingBox() Rectangle +GetGlobalBoundingBox() Rectangle } class IPositioningGrid { <<interface>> +GetTileSize() Vector2* +GetPosAtHex(hex) Vector* +GetGlobalPosAtHex(hex) Vector* +GetBoundingBoxAtHex(pos) Rectangle* +GetGlobalBoundingBoxAtHex(pos) Rectangle* +IsInRange(pos) bool* } BaseGrid --|> GameObject BaseGrid --|> IPositioningGrid BaseGrid --o BaseCell class BaseGrid~T~ { <<abstract>> +BaseHexGrid~T~ grid +int radius +float cellRadius +GetTileSize() Vector2 +GetPosAtHex(hex) Vector +GetGlobalPosAtHex(hex) Vector +GetBoundingBoxAtHex(pos) Rectangle +GetGlobalBoundingBoxAtHex(pos) Rectangle +IsInRange(pos) bool +Add(cell) +Remove(pos) bool +Get(pos) T? } Tile --|> BaseCell class Tile { #int rotation +int Rotation +Draw() } TileGrid --|> BaseGrid TileGrid --o Tile class TileGrid { +HexVector MousePos +HandleInput() +Draw() }

Bronnen


Laatst geüpdatet: March 11, 2024
Gecreëerd: March 1, 2024