top of page

Game development on MAUI (Part 1: Animation)

After I encountered Skia on .Net MAUI, I started thinking about game development on this platform. MAUI is a framework for developing different types of cross-platform apps. On the internet, you can find some guides on how to create simple games like tic-tac-toe, memory games, or Sudoku. Occasionally, you might see something resembling Space Invaders or Asteroids. I've even seen someone attempt to create their own game engine based on .Net MAUI. Looking at all of this, I thought, "Why hasn't anyone tried to make something more interesting?" . With this question in mind, I decided to create something myself.


Main image

I thought about porting classic games like Mario or Contra. However, after realizing how challenging it would be to implement the physics of movement or recreate complex bosses like the one from the waterfall level in Contra, I decided to look at older JRPGs like Final Fantasy, Dragon Quest, or Pokémon.



For my first project, I aimed to create something straightforward to gain a better understanding of how everything works and to showcase the main aspects of game development on MAUI. My debut game will feature a hydra that eats spiders to survive longer and achieve a high score. To make the game more interesting, I will add a temporary multiplier that will increase the score and the value of each spider.


First, I needed to figure out how to work with tile sets in .Net MAUI. I started searching free assets on itch.io and found an isometric three-headed dragon (sprites for the hydra) and an isometric spider, which will be part of my game.



To begin, let's play one of the animations for the hydra. To do this, we need to divide the tile set into separate tiles and draw them in a loop, clearing the canvas before the next frame.


Hydra set of tiles

First, let's read our image and divide it into sprites. I created a Tile structure that contains a SKRect, which represents the coordinates of the tile on the bitmap and its size.


internal struct Tile
{
    public readonly int Width;
    public readonly int Height;
    public readonly SKRect TileRect;

    public bool IsEmpty => TileRect.IsEmpty;

    public Tile(int width, int height, SKRect tileData)
    {
        Height = height;
        Width = width;
        TileRect = tileData;
    }

    public static Tile Empty()
    {
        return new Tile();
    }

    public Tile()
    {
        Height = 0;
        Width = 0;
        TileRect = SKRect.Empty;
    }
}

To create tiles more easily in the future, I created a tile factory.


internal static class TileFactory
{
    internal static Tile CreateTile(int tileWidth, int tileHeight, int widthInTiles, int heightInTiles, int number)
    {
        var xTile = number;
        var yTile = 0;

        while (xTile >= widthInTiles)
        {
            xTile -= widthInTiles;
            yTile++;
            if (xTile < widthInTiles)
            {
                break;
            }

            if (yTile > heightInTiles)
            {
                return Tile.Empty();
            }
        }

        float left = xTile * tileWidth;
        float top = yTile * tileHeight;
        float right = left + tileWidth;
        float bottom = top + tileHeight;

        return new Tile(tileWidth, tileHeight, new SKRect(left, top, right, bottom));
    }
}

Then, I created a TileSet class that contains a list of tiles, the size of the image in pixels, the size of the tile, and a bitmap of the tile set image.


internal class TileSet
{
    public readonly int HeightInPixels;

    public readonly int WidthInPixels;

    public readonly int TileWidthInPixels;

    public readonly int TileHeightInPixels;

    public int TilesCount => TilesData.Count;

    public readonly List<Tile> TilesData;

    public readonly SKBitmap TilesBitmap;

    public TileSet(int tileWidthInPixels, int tileHeightInPixels, int widthInPixels, int heightInPixels, List<Tile> tilesData, SKBitmap tilesBitmap)
    {
        TileHeightInPixels = tileHeightInPixels;
        TileWidthInPixels = tileWidthInPixels;
        TilesData = tilesData;
        WidthInPixels = widthInPixels;
        HeightInPixels = heightInPixels;
        TilesBitmap = tilesBitmap;
    }
}

To create tile sets more easily in the future, I created a tile set factory.


internal static class TileSetFactory
{
    internal static TileSet CreateTileSet(int tileWidthInPixels, int tileHeightInPixels, byte[] imageSetData)
    {
        var imageBitmap = SKBitmap.Decode(imageSetData);
        var widthInTiles = imageBitmap.Width / tileWidthInPixels;
        var heightInTiles = imageBitmap.Height / tileHeightInPixels;
        var tileData = new List<Tile>();
        while (tileData.Count < widthInTiles * heightInTiles)
        {
            var tile = TileFactory.CreateTile(tileWidthInPixels, tileHeightInPixels, widthInTiles,
                heightInTiles, tileData.Count);
            if (!tile.IsEmpty)
            {
                tileData.Add(tile);
            }
        }

        return new TileSet(tileWidthInPixels, tileHeightInPixels, imageBitmap.Width, imageBitmap.Height, tileData,
            imageBitmap);
    }
}

Hydra first tile

To play an animation, we start a timer on the dispatcher with a timespan equal to 33.3 milliseconds, which is equivalent to 30 frames per second.


Dispatcher.StartTimer(TimeSpan.FromMilliseconds(AnimationCycleTime), () =>
{
    HydraCanvas.InvalidateSurface();

    AnimationIndex++;

    return _pageIsActive;
});

To make the animation loop, we need to reset the animation index every time it becomes equal to the number of tiles in the tile set.


private int _animationIndex;

private int AnimationIndex
{
    get => _animationIndex;
    set => _animationIndex = _animationIndex < _tileSet.TilesCount - 1 ? value : 0;
}

To draw these tiles, we need to place a SKCanvasView on the page and register the PaintSurface method on the PaintSurface event of this canvas.


private void HydraCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs args)
{
    var surface = args.Surface;
    var canvas = surface.Canvas;

    canvas.Clear();

    canvas.DrawBitmap(_tileSet.TilesBitmap, 
				    _tileSet.TilesData[AnimationIndex].TileRect, 
                      new SKRect(0, 0, TileSize * 4, TileSize * 4));
}

Hydra animation

Summary


In conclusion, working on creating a game using .NET MAUI and Skia is an exciting process that requires not only learning the tools, but also a creative approach. In this article, I covered the first steps: from choosing a concept to working with tile sets and animations.


This project is just the beginning. In the next parts, I will continue sharing my progress, diving into game mechanics development, object interactions, and adding new features. Eventually, we will create a fully functional game.


The knowledge and skills gained throughout this article will not only provide a deeper understanding of game development, but also broaden general technical expertise in other fields of software engineering. Stay tuned for more!


The full code from this article will be here.

コメント


Leave us a message and we'll get back to you.

Thanks for submitting!

bottom of page