Writing My Own Pokémon Spritesheet Generator in C#

For my Living Dex project I wanted to control how the Pokémon spritesheets work. There are existing spritesheet generators, but I wanted to make my own in .NET for fun and learning.

Writing My Own Pokémon Spritesheet Generator in C#

Why?

Early 2022 I had two problems:

  1. I was originally using the output from pokedextracker/pokesprite (see my post about installing it) via GitHub for the spritesheets used in Making a Living Dex: Appendix A - The Whole Living Dex Roster however I didn't like the direction they took with the Pokémon Legends Arceus icons
  2. I really wanted a practical excuse to learn some of the newer .NET features

So tada! 🎉

GitHub - nikouu/pokesprite-spritesheet: The tilesheets for my needs for my Living Dex project via https://github.com/msikma/pokesprite
The tilesheets for my needs for my Living Dex project via https://github.com/msikma/pokesprite - GitHub - nikouu/pokesprite-spritesheet: The tilesheets for my needs for my Living Dex project via ht...
Spritesheet generator!

Overview

The work is heavily based on pokedextracker/pokesprite (which in turn is based on pokesprite-gen) and written in C# I've attempted to work in a few of the newer features to give myself a practical example and play pit. The output is two files:

  1. pokesprite.css
  2. pokesprite.png
The two output files side by side.

How it works

At the time of writing this, it isn't finished or cleaned up, but it works™ - and how it works is:

  1. Get the latest sprites and metadata from the Pokesprite NPM feed
  2. Scale the sprite if required
  3. Trim the whitespace, so there's no padding around the sprite
  4. Generate the spritesheet
  5. Generate the .css file
  6. Copy to output

1. Get sprites

A fun step because I was able to use the new .NET 7 System.Formats.Tar namespace. In fact, I wrote about it here:

How to Natively Read .tgz Files With the New C# TarReader Class
In .NET 7 we can now natively decompress/unpack and open .tgz/.tar.gz files/archives without third party libraries or complex code.

We grab the .tgz file from the pokesprite-images NPM feed, then decompress and load the files into memory. Then those files are filtered and we only grab:

  1. The pokemon.json file, which contains metadata about the Pokémon
  2. The latest Pokémon sprite images, including forms
  3. All the ball sprites

All these get fed into the next step. We'll be using the Squirtle sprite to showcase from here onwards.

Squirtle sprite.

2. Scale

I'm going to be honest here, at the time I was getting a grasp on what the pokedextracker repo was doing and simply one for one threw this code into my project. In the end, none of the sprites that I pick up use this feature - however in the future I might use other sprites that do need to be scaled.

Due to the above, most sprites are just hot-potatoed to the next step without any processing.

No change in the sprite.

3. Trim

Because we want to control the padding ourselves with CSS, we want to remove all transparent tiles all around the sprite.

4. Generate spritesheet

At this point, we have all the sprites we want. We then look for the tallest and widest sprite and use these max tall and max wide values to create the spacing each sprite will have from each other on the spritesheet.

Then each sprite is placed in number order from left to right, top to bottom in 32 columns.

Full spritesheet.

5. Generate CSS

Since we know where each Pokémon is and the trimmed size of the sprite, we can generate CSS:

  1. To point to the top left of the rectangle the sprite occupies as background-position
  2. Have a size the same as the trimmed sprite and normal width and height

Done!

6. Output

The code of this is extremely crude, but essentially it spits out the spritesheet and corresponding CSS with a little bonus: an HTML file to smoke test all the sprites.  

What I learned

The code complexity itself isn't too bad (the quality on the other hand... 😰), but I used it as an opportunity to arbitrarily cram in bits of learning. If you want to take the work I've done, feel free to revert these to something more... normal.

System.Threading.Channels

The main excuse for this project. I love the idea of channels and they're used in the sprite manipulation as a sort of asynchronous pipeline.  

I had a prototype session messing around in another GitHub repo of mine:

GitHub - nikouu/System.Threading.Channels-Learnings
Contribute to nikouu/System.Threading.Channels-Learnings development by creating an account on GitHub.

I found them easy to use and super clean. I'll try use them more in the future!

System.Formats.Tar

I originally saw this as a tweet and thought "damn that's a neat idea" and read the linked PR for the .NET repo:

[API Proposal]: APIs to support tar archives · Issue #65951 · dotnet/runtime
Background and motivation Creating a new issue to get fresh feedback. Original tar proposal Tar is an old, stable and robust archiving format that is heavily used, particularly in the Unix world. T...

Turns out this project is a great fit because NPM uses .tar files and I grab the sprites via NPM. I found this new class easy to use and even wrote a post about it:

How to Natively Read .tgz Files With the New C# TarReader Class
In .NET 7 we can now natively decompress/unpack and open .tgz/.tar.gz files/archives without third party libraries or complex code.

Specific use cases for using declarations

In .NET it often feels like IDisposable types are heavily paired with using statements. Then as of C# 8 (.NET Core 3.x+) we got a new feature: using declarations. With these you don't explicitly set the scope and these IDisposable objects are disposed when the outer scope is completed.

See:

// using statements
using (var gzip = new GZipStream(tgzStream, CompressionMode.Decompress))
{
	using (var unzippedStream = new MemoryStream())
	{
		await gzip.CopyToAsync(unzippedStream);
		unzippedStream.Seek(0, SeekOrigin.Begin);

		using (var reader = new TarReader(unzippedStream))
		{

		}
	}
}

vs

// using declarations
using var gzip = new GZipStream(tgzStream, CompressionMode.Decompress);

using var unzippedStream = new MemoryStream();
await gzip.CopyToAsync(unzippedStream);
unzippedStream.Seek(0, SeekOrigin.Begin);

using var reader = new TarReader(unzippedStream);

I found that when I was doing work with the Graphics object, I needed to explicitly dispose the object for the data to be written to the Bitmap object - meaning if I use the using declaration, the object won't be disposed until the end of the function, which is too late because I want the updated Bitmap before then.

using var imageStream = new MemoryStream(item.Image);
using var pokemonImage = new Bitmap(imageStream);

using (var graphics = Graphics.FromImage(spritesheet))
{
	graphics.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceOver;
	graphics.DrawImage(pokemonImage, column * maxWidth, row * maxHeight);
}  

I now better understand when to use statements vs declarations.

Local functions

Introduced in C# 7 (Framework and Core 2.x+) were local functions. I don't think the project has a good use case for it, but I wedged it into Npm.cs to get what the most recent version of the NPM package is.

I'm still not sure about them - outside of ragged program.cs files.

See below with the local function GetLatestVersionAsync()

public async ValueTask<MemoryStream> GetTarball(string packageName, string? packageVersion = null)
        {
            var version = packageVersion ?? await GetLatestVersionAsync();

            var packageMetadata = await GetPackageMetadataAsync<NpmPackageModel>(packageName, version);

            var httpClient = new HttpClient();

            // returns non seekable stream, is this fine for my purposes?
            // if not, can just copy to a memorystream
            // https://stackoverflow.com/a/3373614
            using var stream = await httpClient.GetStreamAsync(packageMetadata.Dist.Tarball);
            var memoryStream = new MemoryStream();
            stream.CopyTo(memoryStream);
            memoryStream.Seek(0, SeekOrigin.Begin);

            return memoryStream;

            // messing with local functions
            async ValueTask<string> GetLatestVersionAsync()
            {
                var latestVersion = await GetPackageMetadataAsync<NpmPackageQueryModel>(packageName, null);
                return latestVersion.DistTags.Latest;
            }
        }

ValueTask

This one is more for me to remember they exist. I've seen that some .NET experts are using this almost as their default Task object instead. So I've tried it here and it works well, except I ran into the problem that Task.WhenAll() requires a Task and not ValueTask.

Raw string literals

I got to mess around with the C# 11 preview feature: Raw String Literals. They're strings that let you put all sorts of characters in it like quotes or backslashes without escaping them - because you cannot escape anything inside a raw string literal.

private string RootClass = """
    .pkicon {
        @include crisp-rendering();
        display: inline-block;
        background-image: url("pokesprite.png");
        background-repeat: no-repeat;
    }
    """;

I then used the interpolated form to create the CSS classes.

var cssClass = $$""".pkicon.pkicon-{{item.Number}}{{FormData(item.Form)}} { width: {{item.TrimmedWidth}}px; height: {{item.TrimmedHeight}}px; background-position: -{{column * maxWidth}}px -{{row * maxHeight}}px }""";

Future plans 🤔

Technically I could finish up the code by:

  1. Cleaning it up
  2. Use System.Commandline
  3. Make it extensible to manipulate all sorts of sprites, rather than just Pokémon and balls

But we'll see, it works as it is right now. If you want to take it and make it great, go ahead!

To Conclude

Thank you for joining me on this little tour through sprite making. I hope you enjoyed it and found something useful along the way.