_images/logo_large.png

v2.0.0


Overview

This document discusses many of the features that RetroBlit offers. Text is good, reading is good, but be sure to also check out the DemoReel example in the scene Assets/RetroBlit/Scenes/DemoReel for some visual bliss and instant gratification.

A Game From Scratch

RetroBlit comes with a bare-bones game that you can use as a starting template. Please see Assets/RetroBlit/Scenes/MyGame Scene and the Asssets/RetroBlit/Scripts/MyGame/MyGame.cs source file.

If you’d like to construct the starting point yourself, or are simply curious then follow these simple steps:

  • Create a new empty Scene
  • Delete any GameObjects placed in the Scene by Unity. Most likely this will be just the Main Camera object.
  • Drag the RetroBlit prefab object from Assets/RetroBlit/Prefabs/RetroBlit into the Scene
  • Create a new C# script that implements the RetroBlit.IRetroBlitGame interface, see The Game Loop for an example.
  • Create a new C# script that will act as an entry point into your game, it should simply be:
public class MyGameEntry : MonoBehaviour {
    private void Awake()
    {
        // Initialize your game!
        // You can call RB.Initialize multiple times to switch between RetroBlit games!
        RB.Initialize(new MyGame());
    }
}
  • Create a new GameObject in the Scene, and add the MyGameEntry script into it.
  • … and you’re done! Press Play in the Unity Editor to run your game, of course it won’t do anything, but that’s what the rest of the manual is for!

The Game Loop

RetroBlit makes use of the classic almighty game loop, which every RetroBlit game uses by implementing the RetroBlit.IRetroBlitGame interface:

public class MyGame : RB.IRetroBlitGame
{
    // First method called by RetroBlit, only once on startup to initialize hardware
    public RB.HardwareSettings QueryHardware()
    {
        var hw = new RB.HardwareSettings();

        // Set hardware parameters

        return hw;
    }

    // Called once on startup after QueryHardware()
    public bool Initialize()
    {
        return true;
    }

    // Called at a fixed rate
    public void Update()
    {
    }

    // Called when RetroBlit is ready to render to display
    public void Render()
    {
    }
}

We will explore in detail what we can do in each of these methods in the remainder of this Features document.

Fixed Frame Rate

RetroBlit runs the IRetroBlitGame.Update at a fixed rate. The default rate is 60 FPS, but a custom rate can be set in IRetroBlitGame.QueryHardware.

public RB.HardwareSettings QueryHardware()
{
    var hw = new RB.HardwareSettings();

    hw.FPS = 30;

    return hw;
}

At any time you can query the FPS setting with RetroBlit.FPS, and you can also query the fixed interval between frames in seconds with RetroBlit.UpdateInterval, which is simply equivlaent to 1 / RetroBlit.FPS.

Graphics

New Rendering Pipeline

As you explore RetroBlit it’s important to understand that for all practical purposes RetroBlit throws out the entire Unity rendering pipeline out the window, and replaces it with it’s own rendering pipeline. This means that in terms of rendering what is not explicitly covered by RetroBlit will not be available. This includes Unity GUI, Unity Sprites, Meshes, and Animations. It helps to think of RetroBlit as an API you might have had if you were developing console games in the early 1990s.

It is however possible to take the RetroBlit final render texture and incorporate it into any Unity Scene, see Taking Over The Display

Pixel Perfect Rendering

It can be challenging to get pixel perfect rendering working in Unity. The camera has to be setup just right, texture parameters have to be set correctly, and when you’re all done you may still encounter infuriating, and non-intuitive issues with pixels going missing, or some pixels being larger than others.

RetroBlit takes care of all those issues with its own rendering framework, and includes changes to the Unity texture importer that take care of the texture setting details for you. All you have to do is set your desired destination resolution in the call to QueryHardware like this:

public RB.HardwareSettings QueryHardware()
{
    var hw = new RB.HardwareSettings();

    hw.DisplaySize = new Size2i(320, 180);

    return hw;
}

You can change the RetroBlit display size at any time by calling the RetroBlit.DisplayModeSet method. If your native display or window resolution changes at run time then RetroBlit will scale its pixels appropriately, and possibly use letterboxing, but it will not automatically change the logical resolution of your game as defined by DisplaySize. This may seem like a hindrance at first, but at the same time it is liberating to know that because the resolution will not change on you, all your game layouts and menus will also not be affected, and you can always expect the user to have the same field of view.

If the requested resolution divides evenly into the display’s native resolution then the result will be perfect, sharp pixels. However, if the resolution does not divide evenly into the display’s native resolution then by definition each pixel cannot be the same size, and still fill the display. Pixels of varying sizes are visually jarring, and so to help with this issue RetroBlit will blend together pixel edges just enough to make the pixels seems like they are equal in size, without causing too much blurring.

For example:

_images/pixelscale_perfect.png

Pixel size divides evenly into display’s native resolution - The pixel scaling is perfect.

_images/pixelscale_naive.png

Pixel size does not divide evenly into display’s native resolution - Naive scaling results in pixels of varying sizes, just look at these, ridiculous, unacceptable, get outta here.

_images/pixelscale_smooth.png

Pixel size does not divide evenly into display’s native resolution - RetroBlit smooth scaling gives an impression that the pixels are the same size, without overly blurring the image.

Pixel Style

The 1980s were a growing time for PC display technology. A lot of odd-ball video modes were developed, and not all of them thought that pixels should be square! To help emulate those systems RetroBlit has support for wide-pixels (effectively 2x1), and tall-pixels (1x2). You can set the desired pixel mode via IRetroBlitGame.QueryHardware or later via RB.DisplayModeSet.

public RB.HardwareSettings QueryHardware()
{
    var hw = new RB.HardwareSettings();

    hw.PixelStyle = RB.PixelStyle.Wide;

    return hw;
}
_images/wide_pixels.png

Wide-pixel format, each pixel is made of 2 x 1 square pixels.

_images/tall_pixels.png

Tall-pixel format, each pixel is made of 1 x 2 square pixels.

Sprites

Sprite Sheets

RetroBlit allows you to setup multiple sprite sheets, and each sprite sheet may have different sprite sizes. You can setup a sprite sheet at any time, or simply do it ahead of time in your RB.IRetroBlitGame.Initialize implementation:

public bool Initialize()
{
    RB.SpriteSheetSetup(0, "EnemySprites", new Size2i(16, 16));
    RB.SpriteSheetSetup(0, "ItemSprites", new Size2i(8, 8));
    RB.SpriteSheetSet(0);

    return true;
}

Once a sprite sheet is setup you can select it as the current sprite sheet with a simple call to RB.SpriteSheetSet() as shown above.

For your sprite sheets you should use a loss-less image format such as PNG. Sprite sheets can be any size supported by your target device and Unity. Some higher end mobile devices can comfortably support 4096x4096 textures, but if you’re targetting a wide range of mobile devices it is advisable that you try not to exceed 2048x2048.

Notice that when setting up a sprite sheet you also specify the size of each sprite as can be seen in the code above. The sprite size is used when drawing individual sprites from the sprite sheet (it’s also possible to copy from arbitrary rectangular region in a sprite sheet), and more importantly the sprite size also represents the tile size in a tilemap layer. Each layer can use a different sprite sheet, so each layer does not need to have the same tile size.

You should never have to worry about your texture settings in Unity, RetroBlit hooks into the texture import pre-processor and takes care of the settings for you.

When you no longer need a particular sprite sheet you can free up resources with RB.SpriteSheetDelete.

Drawing

Drawing a sprite is simplicity itself. You may be used to having to create GameObjects in Unity and putting your sprites into the Scene tree. With RetroBlit you simply use the RB.DrawSprite inside of the IRetroBlitGame.Render method. For example:

public void Render()
{
    RB.Clear(new Color32(32, 32, 32, 255));

    RB.DrawSprite(0, /* Sprite index */
                   new Vector2i(100, 100));
}

In this example a sprite at index 0 (the first cell in your sprite sheet) is drawn at position 100, 100.

To help deal with large sprite sheets you may use RB.SpriteIndex to help you calculate the sprite index given the sprite column and row it appears in.

Instead of drawing by sprite index, you may also copy arbitrary rectangular region from the sprite sheet like this:

public void Render()
{
    RB.Clear(new Color32(32, 32, 32, 255));

    RB.DrawCopy(new Rect2i(0, 0, 64, 128),
                 new Vector2i(100, 100));
}

This example copies a rectangular region from the sprite sheet starting a position 0, 0, 64 pixels wide, and 128 pixels high, to the position 100, 100 on the display.

There are a few other method overloads to DrawSprite and DrawCopy that allow for scaling, rotation, and horizontal and vertical flipping. Have a look at the DemoReel example project, and the API Reference

Nine-Slice Sprite

RetroBlit also supports 9-slice sprites. This kind of sprite is made out of 9 pieces, and can scale itself by repeating some of the pieces, while keeping others fixed. These sprites are often used for UI and dialogs.

public void Render()
{
    RB.Clear(new Color32(49, 49, 49));
    RB.DrawNineSlice(
        new Rect2i(16, 16, 200, 100),   // Destination
        new Rect2i(80, 0, 8, 8),        // A
        new Rect2i(88, 0, 8, 8),        // B
        new Rect2i(96, 0, 8, 8),        // C
        new Rect2i(80, 8, 8, 8),        // E
        new Rect2i(88, 8, 8, 8),        // X
        new Rect2i(96, 8, 8, 8),        // F
        new Rect2i(80, 16, 8, 8),       // G
        new Rect2i(88, 16, 8, 8),       // H
        new Rect2i(96, 16, 8, 8));      // I
}
_images/nineslice.png

This large 9-slice sprite is made of these tiny pieces:

_images/nineslice_parts.png

The pieces A, C, G, and I are always drawn in the corners of the 9-slice sprite. B and H are repeated horizontally. E and F are repeated vertically. Finally, X is repeated in both directions to fill the middle of the 9-slice sprite.

For best results each of these groups of pieces should should be the same height:

  • A, B, C
  • E, F
  • G, H, I

These groups should be the same width:

  • A, E, G
  • B, H
  • C, F, I

The piece X can be any size.

It is recommended that you don’t make these pieces too small to limit the amount of times they have to be repeated to fill your nine-slice sprite.

You might notice that this particular 9-slice sprite is symmetric. For convenience you could use this short-hand method instead to render it:

public void Render()
{
    RB.Clear(new Color32(49, 49, 49));
    RB.DrawNineSlice(
        new Rect2i(16, 16, 200, 100),   // Destination
        new Rect2i(80, 0, 8, 8),        // A, C, G, I
        new Rect2i(88, 0, 8, 8),        // B, E, F, H
        new Rect2i(96, 0, 8, 8));       // X
}

RetroBlit will simply mirror the remaining pieces of the 9-slice image.

Primitives

It can often be useful to draw geometric primitives, and RetroBlit allows you to do so very easily. There are methods for drawing pixels, rectangles, ellipses, triangles, and lines. For example:

public void Render()
{
    RB.Clear(new Color32(32, 32, 32, 255));

    RB.DrawPixel(new Vector2i(12, 12), new Color32(255, 96, 0, 255));
    RB.DrawRect(new Rect2i(25, 2, 21, 21), new Color32(0, 255, 0, 255));
    RB.DrawRectFill(new Rect2i(48, 2, 21, 21), new Color32(0, 255, 0, 255));
    RB.DrawEllipse(new Vector2i(12, 35), new Vector2i(10, 10), new Color32(0, 96, 255, 255));
    RB.DrawEllipseFill(new Vector2i(35, 35), new Vector2i(10, 10), new Color32(0, 96, 255, 255));
    RB.DrawEllipseInvertedFill(new Vector2i(58, 35), new Vector2i(10, 10), new Color32(0, 96, 255, 255));
    RB.DrawTriangle(new Vector2i(2, 48), new Vector2i(34, 58), new Vector2i(2, 68), new Color32(228, 170, 64, 255));
    RB.DrawTriangleFill(new Vector2i(37, 58), new Vector2i(69, 48), new Vector2i(69, 68), new Color32(228, 170, 64, 255));
    RB.DrawLine(new Vector2i(2, 71), new Vector2i(68, 81), Color.white);
}

These should be pretty self explanatory, perhaps with the exception of RB.DrawEllipseInvertedFill which fills the area outside of the ellipse rather than inside.

_images/draw_primitives.png

Fonts

Pixel font rendering is also easy to do with RetroBlit.

Built-in RetroBlit Font

For simple cases, or for development purposes you can print text with the built-in RetroBlit font right out of the box:

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.Print(new Vector2i(4, 4), new Color32(255, 255, 0, 255), "Hello there!");
}
_images/builtin_font.png

Custom Font

Defining custom pixel font is also easy. There are a few requirements.

  • The font glyphs have to be somewhere in your sprite sheet, layed out in a grid.
  • Each grid cell must be the same size (don’t worry RetroBlit will automatically trim the horizontal whitespace).
  • The glyphs must be in ascending ASCII order, with no ASCII character gaps between the first and last characters.
_images/custom_font_spritesheet.png

In this example the font appears in the sprite sheet starting at position 0, 16. Each glyph is 12x12 pixels in size. The grid is 10 columns wide. The first ASCII character is A and the last is Z. The font in this sprite sheet can be setup like this:

public bool Initialize()
{
    RB.FontSetup(0, (int)'A', (int)'Z', new Vector2i(0, 16), 0, new Size2i(12, 12), 10, 1, 2, false);

    return true;
}

This will setup the font at font index 0 with the parameters shown in the sprite sheet above. Additionally we specify that character spacing is 1 pixel and line spacing is 2 pixels, and that this is not a mono-spaced font. This font also uses sprite sheet index 0.

Once setup we can print with this font just as easily as with the built-in font, adding only the font index.

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.Print(0, new Vector2i(4, 4), new Color32(255, 255, 0, 255), "HELLO THERE!");
}
_images/custom_font.png

Note that the exclamation mark ! did not render because it was not part of the font as it was defined, unsupported glyphs are shown as empty spaces.

Inline String Coloring

As shown above you can specify which color to print with, and you can also specify inline color changes in your text string!

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.Print(new Vector2i(4, 4), new Color32(255, 255, 255, 255), "HP: @FF404010@-/@40FF40100");
}
_images/font_inline_color.png

The syntax for inline color changes is @RRGGBB where RRGGBB are 2 digit hex color codes between 00 and FF for the red, green, and blue color channels.

Additionally you may revert the color back to the color originally specified in the RB.Print parameters by using the sequence @-. In this example the original color was Color32(255, 255, 255, 255).

To print the @ character you can use the sequence @@.

Sometimes it may be advantageous to ignore inline color changes, for example you might want to use the same string to print your text, and to print its drop shadow. For this use the flag RB.NO_INLINE_COLOR

string str = "@FF6000C@60FF00o@6000FFl@FFAF00o@60FFFFr@FF60FFs";
RB.Print(new Vector2i(32, 33), Color.black, RB.NO_INLINE_COLOR, str);
RB.Print(new Vector2i(32, 32), Color.black, str);
_images/font_noinline.png

Inline Font Effects

RetroBlit also has two inline font effects, a wavy text effect, and a shaky text effect!

Wavy text effect syntax uses the format w### where the 3 digits are each 0-9 values which correspond to wave amplitude, period, and speed. The default value is w000 which indicates no waviness at all.

The shaky effect syntax uses the format s# where the digit is a value between 0-9 that indicates the magnitude of the shake. The default value is s0, no shake.

RB.Print(new Vector2i(0, 0), Color.black, "This text is @w244wavy@w000!");
RB.Print(new Vector2i(0, 16), Color.black, "This one is @s1shaky@s0!");
RB.Print(new Vector2i(0, 32), Color.black, "This text is @w244@s1both@w000@s0!");
_images/font_effect.gif

Note that RB.PrintMeasure ignores font effects for the purpose of measuring the bounding are of a text string. The measurements will be done as if the font effects were not applied at all.

Inline Font Changes

RetroBlit also allows you to change the font face inline! The syntax for font changes is simply @g## where the number represents the two digit font index of your font. The default setting is @g99, where the special value 99 refers to built-in RetroBlit system font.

RB.Print(new Vector2i(0, 0), Color.black, "Inline font @g02changes@g99 are great!");
_images/font_change.png

When changing font inline the new font must have glyphs of the same height as the previous font, but the widths may vary.

Text Alignment

Instead of specifying a position at which to print text you can instead specify a rectangular region to place the text in, and apply text alignment.

For example:

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.Print(new Rect2i(4, 4, 80, 40), new Color32(255, 255, 255, 255),
        RB.ALIGN_H_CENTER | RB.ALIGN_V_CENTER,
        "Kind stranger, please\nkill 10 rats\nin my basement!");
}
_images/text_align.png

Different combinations of the flags RB.ALIGN_H_LEFT, RB.ALIGN_H_CENTER, RB.ALIGN_H_RIGHT, RB.ALIGN_V_TOP, RB.ALIGN_V_CENTER, RB.ALIGN_V_BOTTOM.

Text Clipping

If you specify a rectangular text area then you may also want to specify how the text will behave if it does not fit in the rectangular area. You could just let it overflow beyond the rectangular area, you could clip it, or use automatic text wrapping.

For example:

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.Print(new Rect2i(4, 4, 20, 80), new Color32(255, 255, 255, 255),
        RB.ALIGN_H_CENTER | RB.ALIGN_V_CENTER | RB.TEXT_OVERFLOW_CLIP | TEXT_OVERFLOW_WRAP,
        myText);
}
_images/text_clip.png

Different combinations of the flags RB.TEXT_OVERFLOW_CLIP, TEXT_OVERFLOW_WRAP.

Tilemaps

RetroBlit supports orthogonal tilemaps with multiple layers. Each tile in a tilemap layer is the same size as it’s sprite sheets sprite size. The tilemap has a few other configurable parameters that can be set in QueryHardware:

public RB.HardwareSettings QueryHardware()
{
    var hw = new RB.HardwareSettings();

    hw.MapSize = new Size2i(256, 256);
    hw.MapLayers = 8;

    return hw;
}

MapSize is the maximum size of a single tilemap layer, and MapLayers defines the total amount of layers the tilemap supports. Ideally you should keep these numbers close to the minimum requirement for your game, to conserve system memory.

RetroBlit internally optimizes tilemaps into Mesh chunks to minimze the amount of draw calls to the GPU. You don’t have to worry about what that means, you just need to know that using tilemaps is far more efficient than drawing individual tiles by hand using RB.DrawSprite.

Drawing

Tilemaps are drawn one layer at a time, giving you control of where and when each layer is drawn. This allows you to easily layer other drawing between the layers of the tilemap. For example:

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.DrawMapLayer(0);
    RB.DrawSprite(0, new Vector2i(100, 100));
    RB.DrawMapLayer(1);
}

This code draws tilemap layer 0 first, then draws a sprite at position 100, 100, and then draws tilemap layer 1 on top of the sprite.

_images/tilemap.png

Changing Layer Sprite Sheet

By default all tile map layers use sprite sheet at index 0, you can change this at any time like so:

public void Update()
{
    RB.MapLayerSpriteSheetSet(0, 2);
}

In this example we set the tile map layer 0 to use sprite sheet 2. Note that if the new sprite sheet has different sized sprites than the previous sprite sheet then this layer’s tiles will be resized to fit the new sprites.

Setting/Getting Tile Info

You can change the content of a tilemap at run-time. For example:

public void Update()
{
    RB.MapSpriteSet(1, new Vector2i(4, 8), 0);
    RB.MapDataSet<MyStruct>(1, new Vector2i(4, 8), myStructInstance);
}

This code sets the tile at position 4, 8 in tilemap layer 1 to use sprite at index 0. It also sets the data object for layer 1, at position 4, 8 to be myStructInstance. What’s a data object? It’s anything you want it to be, or nothing at all. For example if you’re developing a dungeon crawler you may want the data object to contain the collision flags for this tile, and a list of items that may be laying on the ground.

Likewise you can read back the parameters you’ve set for your tile:

public void Update()
{
    int spriteIndex = RB.MapSpriteGet(1, new Vector2i(4, 8));
    MyStruct s = RB.MapDataGet<MyStruct>(1, new Vector2i(4, 8));
}

Tiled TMX Support

RetroBlit has support for loading orthogonal tilemaps for Tiled .tmx file. As with other resources the .tmx files (and any accompaning .tsx and .tx files) should be located under a Resources folder. When RetroBlit editor extension script sees a .tmx file it will import it into its own binary format that it can very quickly load at runtime. If you encounter .tmx loading issues please try to re-import your .tmx file in case something has gone wrong during the .tmx to binary format conversion.

Loading a TMX Map Layer

Loading a TMX map layer is very simple:

public bool Initialize()
{
    int mapLayer = 0;
    var map = RB.MapLoadTMX("MyMapFile");
    RB.MapLoadTMXLayer(map, "Ground Layer", mapLayer);
    RB.MapLayerSpriteSheetSet(mapLayer, 2);

    return true;
}

This code will load a map layer named Ground Layer from a TMX file called MyMapFile into RetroBlit tilemap layer 0. Notice that as with other resources the file extension is omitted.

You may also specify which sprite sheet the loaded map layer should use with RB.MapLayerSpriteSheetSet, the default sprite sheet is 0. You can change the sprite sheet at any time, for example you might have different sprite sheets for different seasons of the year.

RetroBlit only supports a single spritesheet per tile layer.

Load TMX Object Layers

RetroBlit also support loading of TMX object layers! You can use this feature to define various objects in Tiled and to mark areas of interest on your tilemap. For example you could use objects to specify spawn location of enemies, secret areas, force fields, or whatever else can be defined as a shape!

Loading these shapes is also very easy:

public bool Initialize()
{
    int mapLayer = 0;
    var map = RB.MapLoadTMX("MyMapFile");
    var objects = map.objectGroups["MyObjectLayer"].objects

    foreach (var obj in objects)
    {
        Debug.Log("Found object: " + obj.name +
                  " of shape: " + obj.shape.ToString() +
                  " and rectangular area: " + obj.rect);
    }

    return true;
}

RetroBlit supports the following TMX object shapes:

Type Dimensions
TMXObject.Shape.Rectangle TMXObject.Rect
TMXObject.Shape.Ellipse TMXObject.Rect
TMXObject.Shape.Point TMXObject.Rect.x, TMXObject.Rect.y
TMXObject.Shape.Polygon TMXObject.Points a List of Vector2i
TMXObject.Shape.Polyline TMXObject.Points a List of Vector2i

Reading TMX Properties

TMX properties are also supported, in their various forms. You can read built-in properties such as layer dimensions, offsets, opacity and more. As well as custom properties on the map, layers, object groups, objects, and tiles!

public bool Initialize()
{
    int mapLayer = 0;
    var map = RB.MapLoadTMX("MyMapFile");

    // Some built-in properties
    Debug.Log("Map size: " + map.size);
    Debug.Log("Map backgroundColor: " + map.backgroundColor);
    Debug.Log("Layer offset: " + map.layers["MyLayer"].offset);
    Debug.Log("Object Group visible: " + map.objectGroups["MyObjGroup"].visible);
    Debug.Log("Object name: " + map.objectGroups["MyObjGroup"][0].name);

    // Custom properties
    Debug.Log("Map description: " + map.properties.GetString("MyDesc"));
    Debug.Log("Level depth: " + map.layers["MyLayer"].properties.GetInt("MyDepth"));
    Debug.Log("Object Group secret: " + map.objectGroups["MyObjGroup"].properties.GetBool("IsSecret"));
    Debug.Log("Object light level: " + map.objectGrousp["MyObjGroup"].properties.GetFloat("LightLevel"));

    // Custom properties on tiles
    RB.MapLoadTMXLayer(map, "Ground Layer", mapLayer);
    var tileProps = RB.MapDataGet<TMXProperties>(mapLayer, new Vector(3, 5));
    Debug.Log("Tile collider: " + tileProps.GetBool("Collider"));

    return true;
}

Built-in and custom properties are straight forward. Tile properties are a little different, in the Tiled TMX format all tiles of the same tile index share the same properties and RetroBlit allows you to get at those properties for any loaded tile using the RB.MapDataGet<TMXProperties>() method.

TMX Infinite Maps

RetroBlit also supports TMX infinite maps. Infinite maps are so called because they can be extremely large, to a point where they are practically infinite. A very large map can’t be loaded into memory at once, not only because of it size, but also because of how long it would take to load. Moreover, the player will only be interacting with a small section of such a map at any one time, and so it would not be wise to try to load it all in at once.

To get around this issue TMX format saves these maps in chunks. Each chunk is by default 16x16 tiles large, but these dimensions can be adjusted in both Tiled and RetroBlit. At runtime your game can figure out what chunks are important to the game at a particular moment, and load those chunks specifically like so:

public bool Initialize()
{
    int mapLayer = 0;
    var map = RB.MapLoadTMX("MyMapFile");

    if (!map.infinite) {
        Debug.Log("Not an infinite map!");
        return false;
    }

    Vector2i mapDestPos = new Vector2i(100, 100);
    Vector2i sourceChunkPos = new Vector2i(20000, 50000);

    RB.MapLoadTMXLayerChunk(map, "Ground", mapLayer, mapDestPos, sourceChunkPos);

    return true;
}

If the player is scrolling around the map at some point he will scroll out of bounds of some of your currently loaded map chunks and you will have to load new chunks to show to the player. To do this you could clear the entire map and load the new chunks in, but this is not optimal because chances are that the player only scrolled away from some of the loaded chunks, while others are still valid and should not have to be reloaded again. To help with this RetroBlit provides the RB.MapShiftChunks(layer, offset) method. You can use this method to shift the loaded map by the given amount of chunks, in the given direction. It may help to think of this operation as an equivalent of shifting elements in 2 dimensional array.

public void Update()
{
    int mapLayer = 0;
    Vector2i shift = Vector2i.zero;

    if (RB.ButtonPressed(RB.BTN_LEFT)) {
        shift.x--;
    }

    if (RB.ButtonPressed(RB.BTN_RIGHT)) {
        shift.x++;
    }

    if (RB.ButtonPressed(RB.BTN_UP)) {
        shift.y--;
    }

    if (RB.ButtonPressed(RB.BTN_DOWN)) {
        shift.y++;
    }

    RB.MapShiftChunks(mapLayer, shift);
_images/mapshift.gif

This tilemap is shifted by calling RB.MapShiftChunks(mapLayer, new Vector2i(1, 1)). 8 of the original chunks are preserved, 7 new chunks become vacant and are loaded from the TMX map using RB.MapLoadTMXLayerChunk()

Color Tinting

RetroBlit allows you to set a tint color while rendering which will mix your sprite colors with the tint color.

public void Render()
{
    RB.DrawSprite(0, new Vector2i(0, 0));

    RB.TintColorSet(new Color32(79, 21, 29, 255));
    RB.DrawSprite(0, new Vector2i(24, 0));

    RB.TintColorSet(new Color32(54, 150, 104, 255));
    RB.DrawSprite(0, new Vector2i(48, 0));
}
_images/rgb_tint.png

Alpha Transparency

RetroBlit.AlphaSet can be used to set alpha transparency that applies to all drawing that follows until alpha is set again, or RB.IRetroBlitGame.Render exits.

_images/alpha.png
public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.DrawSprite(1, new Vector2i(0, 0));
    RB.AlphaSet(127);
    RB.DrawSprite(0, new Vector2i(0, 0));
}
_images/spacer.png

Clip Region

Sometimes you may want to restrict which region of the display RetroBlit is allowed to draw to. We call this the clipping region, and in RetroBlit you can set it with:

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.ClipSet(new Rect2(0, 0, 8, 8));
    RB.DrawSprite(0, new Vector2i(0, 0));
}

In this example the clip region is set to the upper left 8 by 8 pixel rectangle, and so the following DrawSprite call will only draw a portion of the sprite to the display. The clip region remains in effect for all following draw calls until the Render method exits, or until it is changed again.

Here is another example in action:

_images/clip.png

The blue area is in the clip region, the dark gray area is outside the clip region.

Clip Region Debugging

It can be difficult to debug rendering issues with clip regions. You may sometimes be unsure if your sprite is rendering at all, or if the clip region is in the wrong place. The RB.ClipDebugEnable and RB.ClipDebugDisable methods can assist with these types of issues.

Drawing into a Sprite Sheet

It can be occasionally useful to be able to draw to into a sprite sheet (also called offscreen rendering), and then copy all, or parts of this sprite sheet to the display. You can create target sprite sheets of various sizes, and you can even copy from one sprite sheet to another.

public bool Initialize()
{
    RB.SpriteSheetSetup(0, new Size2i(256, 256));

    return true;
}

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.Offscreen(0);
    RB.Clear(0);

    RB.Print(new Vector2i(0, 0), Color.white, myLargeString);

    RB.Onscreen();

    RB.SpriteSheetSet(0);
    RB.DrawCopy(new Rect2i(0, 0, 256, 256), new Vector2i(0, 0), RB.FLIP_H);
}

This example first switches rendering to a sprite sheet with the Offscreen method. Next it clears the sprite sheet surface, and draws a large text string to the sprite sheet surface.

Once done the example then switches back to drawing on the display with the Onscreen method. Finally it copies a 256 by 256 pixel area from the sprite sheet surface to the display, flipping it horizontally in the process, because why not?

Note that you do not need to clear the sprite sheet surface on every update. In this example we could have drawn the text on the sprite sheet surface and left it there for subsequent Render calls to copy from. If the text was sufficiently long enough then this could result in significant performance gains because RetroBlit would no longer be rendering the text glyph by glyph ever frame, but instead it would copy it as a single rectangular region.

_images/offscreen.png

Here we have everyone’s favourite placeholder text “Lorem ipsum” printed on a sprite sheet surface, and rendered to display, flipped horizontally.

When you no longer need a sprite sheet you can free up it’s resources by calling RB.SpriteSheetDelete.

Animating by Drawing into a Sprite Sheet

One neat use of the ability to draw into a sprite sheet is global animation. You could animate sprites by drawing over them in the sprite sheet and the change would be immediately reflected anywhere the sprite is then drawn. For example you could animate the water in your tilemap by changing the water tile sprite each frame.

_images/spritesheet_draw_anim.gif

Camera

RetroBlit supports a concept of a very simple 2D camera. All drawing in RetroBlit is simply offset by the camera position. By default the camera position is 0, 0, so you may have not even realized there is a concept of a camera if you haven’t used it. While simple, this camera can be extremely useful when your game has a scrolling play area.

public void Render()
{
    RB.Clear(new Color32(96, 96, 96, 255));

    RB.CameraSet(new Vector2i(-32, 0));
    RB.DrawSprite(0, new Vector2i(0, 0));
}

Here we draw a sprite at position 0, 0, but because the camera has moved 32 pixels to the left the sprite will actually be draw at position 32, 0 on the display.

_images/camera.png

Moving camera to the left shifts everything on the display to the right, this image should help visualize how that works.

Post-Processing Effects

Post-Processing effect are a mixed bag of rendering effects that can be applied to the entire display after the Render method exits. It’s up to you how you want to use these effects, if at all.

public bool Initialize()
{
    RB.EffectSet(RB.Effect.Scanlines, 0.2f);

    return true;
}

This example applies a subtle retro CRT scanline effect to your display.

You can layer multiple effects together by setting them one by one.

There are a bunch of other effects, have a look at all of them in the API Reference, or better yet check out the DemoReel.cs demo for a live preview of all the available effects.

_images/scanlines.png

Super Flag Run demo game uses Scanlines and Noise effects to give a bit more grit and retroness.

Post-Processing Early

Normally post-processing effects are applied at the end of the render frame. In practice this means that everything you draw to the screen will have the same post-processing effects applied. There are scenarios where this might be undesirable. For example you may want to apply a Effect.Scanlines and Effect.Shake effects to your game scene, but only Effect.Scanlines to your GUI. To achieve this you may use the RB.EffectApplyNow() method to tell RetroBlit to immediately apply post-processing effects and then let you continue to draw on top of them!

public void Render()
{
    RB.EffectSet(RB.Effect.Shake, 0.5f);
    RB.EffectSet(RB.Effect.Scanlines, 0.25f);

    RB.DrawMapLayer(0);

    RB.EffectApplyNow();
    RB.EffectSet(RB.Effect.Shake, 0);

    DrawMyGUI();
}
_images/pp_applynow.png

Noise and desaturation effect applied to the game scene, but not to the character nor the text field.

Taking Over The Display

For even more control over how your game is displayed you can take over the game rendering surface and render it in a Unity scene in any way you want! To do this you simply get the rendering surface Texture using RB.DisplaySurface, and apply it to some object in your Unity scene. You will also want to tell RetroBlit to stop running its own presentation code by calling RB.PresentDisable().

_images/presentenable.png

Super Flag Run demo game running in a custom scene. Have a look at the OldDays demo to see this in action.

Shaders (Advanced Topic)

RetroBlit supports custom shaders when drawing, and when appyling Post-Processing Effects. Shaders can be very powerful, and enable all sorts of potential for custom effect. However shaders can be intimidating for new game developers and are considered a more advanced topic. Nonetheless RetroBlit strives to make shaders as simple as possible, and there are troves of general shader tutorials available online!

Loading a Shader

Unity stores shaders like resources, each shader has a .shader extension and should live somewhere under a Resources folder. Shaders are loaded just like all other RetroBlit resources, with ShaderSetup(). If you’re familiar with Unity you may be wondering where Materials come in to play, well they don’t, RetroBlit will take care of that for you!

public bool Initialize()
{
    RB.ShaderSetup(0, "AmazingShader");

    return true;
}

Enabling a Shader

Once a shader is loaded you can enable it for rendering with a simple call to ShaderSet(). The shader will then by applied to all subsequent drawing operations, until a different shader is set, ShaderReset() is called, or the frame ends.

public void Render()
{
    RB.DrawSprite(0, new Vector2i(0, 0));

    RB.ShaderSet(0);

    RB.DrawSprite(1, new Vector2i(64, 0));

    RB.ShaderReset();

    RB.DrawSprite(2, new Vector2i(128, 0));
}

In the example above the first sprite is drawn using the default RetroBlit shader, the second sprite is drawn using the custom shader loaded into shader index 0, and finally the third sprite is again drawn with the default RetroBlit shader.

_images/shader_example.png

Setting Shader Variables

You can set shader global variables using any of the RB.Shader*Set() methods.

public void Render()
{
    RB.ShaderSet(0);

    RB.ShaderFloatSet(0, "Wave", Mathf.Sinf(t));
    RB.DrawSprite(0, new Vector2i(0, 0));

    RB.ShaderApplyNow();

    RB.ShaderFloatSet(0, "Wave", 0);
    RB.DrawSprite(1, new Vector2i(64, 0));
}

This example assumes that the shader loaded into index 0 has a global property named Wave. The first sprite is rendered with the Wave property set to Mathf.Sinf(t), and the second sprite is rendered with the Wave property set to 0.

You may be wondering what the RB.ShaderApplyNow() call is all about. If that call was taken out the first sprite would be rendered with the Wave property set to 0 as well! This is because sprites are not rendered immediately but are batched for later rendering. If the value of the Wave property changes between the time RB.DrawSprite is called and when it is actually renendered then only the most recent value of the Wave property will be used. RB.ShaderApplyNow flushes any batched sprites immediately, ensuring they use the most recently set shader property values at that time that RB.ShaderApplyNow is called. Ideally you will want to limit how many times you call RB.ShaderApplyNow or switch shaders, because each time that you do you will interrupt a sprite batch and cause a flush of the rendering pipepline, which reduces performance.

For optimal performance consider using RB.ShaderPropertyID to prefetch shader property IDs, instead of passing them as strings. For example:

private int mWaveID;

public bool Initialize()
{
    RB.ShaderSetup(0, "AmazingShader");
    mWaveID = RB.ShaderPropertyID("Wave");

    return true;
}

public void Render()
{
    RB.ShaderSet(0);

    RB.ShaderFloatSet(0, mWaveID, Mathf.Sinf(t));
    RB.DrawSprite(0, new Vector2i(0, 0));
}

Spirte Sheets in Shaders

Many shaders will want to access (known as sampling) the pixels of more than one sprite sheet. This can be done using the RB.ShaderSpriteSheetTextureSet() method in which you can pass the sprite sheet index. You may also use RB.ShaderSpriteSheetFilterSet() to specify how the sprite sheet should be sampled, either using the default RB.Filter.Nearest, or RB.Filter.Linear which interpolates pixels colors based on neighbouring pixels.

public void Render()
{
    RB.ShaderSet(0);

    RB.ShaderSpriteSheetTextureSet(0, "OtherSpriteSheet", 0);
    RB.ShaderSpriteSheetFilterSet(0, 0, RB.Filter.Linear);
    RB.DrawSprite(0, new Vector2i(0, 0));
}

Writing a RetroBlit Shader

Custom shaders in RetroBlit should build on top of existing RetroBlit Shaders because RetroBlit internals still need to be satisified by your shaders as well. Refer to the table below for suggestions on which shader to base your custom shader on.

Scenario Base Shader
Rendering RetroBlit/Internal/Materials/DrawShaderRGB.shader
Post-Processing RetroBlit/Internal/Materials/PresentBasicShader.shader

The shaders are documented, and make suggestions on where you should add your custom shader source, and properties. For an example of custom shaders please have a look at the DemoReel example!

Below is a shader from the DemoReel example that creates a masking and wavy effect. The meat of the code is in the section commented with:

/*** Insert custom fragment shader code here ***/

The properties Wave and Mask used by this shader were added to the section:

/*** Insert custom shader properties here ***/

Shader "Unlit/WavyMaskShader"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100
        Ztest never
        Zwrite off
        Blend SrcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // Lowest target for greatest cross-platform compatiblilty
            #pragma target 2.0

            #include "UnityCG.cginc"

            struct vert_in
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float2 flags : TEXCOORD1;
                fixed4 color : COLOR;
            };

            struct frag_in
            {
                float4 vertex : POSITION;
                float3 uv : TEXCOORD0;
                float2 screen_pos : TEXCOORD2;
                fixed4 color : COLOR;
            };

            sampler2D _SpritesTexture;

            float2 _DisplaySize;
            float4 _Clip;
            float4 _GlobalTint;

            /*** Insert custom shader properties here ***/
            sampler2D Mask;
            float Wave;

            frag_in vert(vert_in i)
            {
                frag_in o;
                o.uv = float3(i.uv, i.vertex.z);
                o.color = i.color;

                // Get onscreen position of the vertex
                o.vertex = UnityObjectToClipPos(float4(i.vertex.xy, 0, 1));
                o.screen_pos = ComputeScreenPos(o.vertex) * float4(_DisplaySize.xy, 1, 1);

                /*** Insert custom vertex shader code here ***/

                return o;
            }

            // Performs test against the clipping region
            float clip_test(float2 p, float2 bottom_left, float2 top_right)
            {
                float2 s = step(bottom_left, p) - step(top_right, p);
                return s.x * s.y;
            }

            float4 frag(frag_in i) : SV_Target
            {
                // 0 if we're drawing from a spritesheet texture, 1 if not
                float solid_color_flag = 1 - i.uv.z;

                float4 sprite_pixel_color = (tex2D(_SpritesTexture, i.uv) * (1 - solid_color_flag)) + (float4)solid_color_flag;

                // Perform clip test on the pixel
                sprite_pixel_color.a *= clip_test(i.screen_pos.xy, _Clip.xy, _Clip.zw);

                // Multiply in vertex alpha and current global alpha setting
                sprite_pixel_color *= i.color;
                sprite_pixel_color.a *= _GlobalTint;

                /*** Insert custom fragment shader code here ***/

                // Sample the mask texture
                i.uv.x += sin(Wave + i.uv.y * 8) * 0.025;
                i.uv.y += cos(Wave - i.uv.x * 8) * 0.015;
                float4 mask_color = tex2D(Mask, i.uv).rgba;

                // Multiply the sprite pixel by mask color
                return sprite_pixel_color * mask_color;
            }
            ENDCG
        }
    }
}

Post-Processing Shaders

Shaders can also be used at the post-processing stage. These shaders are different in that they work in the native window resoution of your game, whereas the drawing shaders work in the RB.DisplaySize resolution of your game. For post-processing shaders you will still use many of the same shader methods, but you set the post processing effect shader like this:

public void Render()
{
    RB.EffectShader(0);

    RB.ShaderFloatSet(0, "Wave", Mathf.Sin(t));
}

Note that custom post processing shaders bypass many of the built-in RetroBlit post processing effects, and so only the following effects will continue to work while a custom post processing shader is active:

  • Pinhole
  • Inverted Pinhole
  • Slide
  • Wipe
  • Shake
  • Zoom
  • Rotation

At any time you can revert back to built-in shaders by calling the RB.EffectReset method.

_images/pp_shader.png

Custom post processing shader with a ripple effect

Shader Limitations

Not everything is possible in RetroBlit shaders. RetroBlit still manages your vertex data for you, so you will not be able to send any custom vertex data to the vertex shader.

Batch/Flush Debugging

Sometimes it can be useful to understand how many GPU draw operations are being performed by RetroBlit in the background, and why. Unity already lets you see how many draw batches are being used, but RetroBlit can further break down the batches into groups. To enable this feature simply call RB.BatchDebugEnable and give it the font and background colors to use while rendering this information:

public bool Initialize()
{
    RB.BatchDebugEnable(Color.white, Color.black);

    return true;
}
_images/batch_debug.png

Interpolation

RetroBlit provides an interpolation method that allows you to interpolate values like numbers, colors, vectors and more using 30 different interpolation curves. These interpolations can enable you to create smoother animations.

Values are interpolated on a time scale of 0.0 to 1.0, an example:

public void Update()
{
    float num = Ease.Interpolate(Ease.Func.BounceIn, 0, height, time);
    Color color = Ease.Interpolate(Ease.Func.CubicOut, Color.black, Color.white, time);
    Vector2 size = Ease.Interpolate(Ease.Func.BackIn, Vector2.zero, new Vector2(8, 8), time);

    return true;
}
_images/interpolate.gif

Visualization of all the different interpolation curves supported by RetroBlit.

Audio

RetroBlit makes playing audio also trivial. You can have a variety of sound effects playing, and a single music track playing at the same time.

Sound

First step of playing a sound is setting it up in a sound slot.

public bool Initialize()
{
    RB.SoundSetup(0, "MyBeepBloop");

    return true;
}

Note that the sound files must be located somewhere in your Assets under a Resources folder.

Now you can play the sound like this:

public void Update()
{
    var soundRef = RB.SoundPlay(0, 0.75f, 1.25f);
}

This example plays the sound we setup at 75% volume level, and at 125% pitch. RB.SoundPlay also returns a RB.SoundReference. While the sound is still playing you can use the sound reference to change its volume, it’s pitch, it’s looping mode, and you can use it to stop the sound. Once the sound stops the SoundReference is no longer valid and can be disposed of. You can check if the sound is still playing using the RB.SoundIsPlaying method.

public void Update()
{
    RB.SoundVolumeSet(mySoundRef, 0.25f);
}

When you no longer need a sound clip you can free up resources by calling RB.SoundDelete.

Music

Music works much like sounds, with the exception that there is only ever one music track playing, and so there is no need for an equivalent of RB.SoundReference.

public bool Initialize()
{
    RB.MusicSetup(0, "MyAmazingMusic");

    return true;
}

public void Update()
{
    RB.MusicPlay(0);
    RB.MusicVolumeSet(0.25f);
}

If you play a new music track while the old one is still playing then RetroBlit will smoothly cross-fade the music for you. That’s all there is to know about music, simple!

When you no longer need a music track you can free up resources by calling RB.MusicDelete.

Input

RetroBlit simplifies input handling a little bit. You’re of course free to use your own input handling with Unity’s existing Input class, but hopefully you’ll find RetroBlit input a little more approachable.

While you can mix Unity Input handling with RetroBlit Input handling in general there is one caveat to be aware of. RetroBlit calls IRetroBlitGame.Update at a fixed rate, and in the back end this behaviour relies on MonoBehaviour.FixedUpdate. As you may already know it is not advisable to call UnityEngine.Input.GetKeyDown and UnityEngine.Input.GetKeyUp inside of MonoBehaviour.FixedUpdate because these input events could happen in-between fixed updates, and will be missed entirely. RetroBlit provides it’s own Input methods to handle this issue.

Gamepads

Gamepad handling is especially easy in RetroBlit. Take a look at this:

public void Update()
{
    if (RB.ButtonPressed(RetroBlit.BTN_A | RetroBlit.BTN_B, RetroBlit.PLAYER_ONE | RetroBlit.PLAYER_TWO) {
        RB.SoundPlay(0);
    }
}

Button codes like RB.BTN_A are bitmasks in RetroBlit, so you can logically OR them together. In this case the if statement is true if either player one or player two released either button A or button B since the last call to IRetroBlitGame.Update.

Gamepad Input Override

RetroBlit maps Player One and Player Two gamepads to keyboard strokes so gamepads are not necessary for these players. Player Three and Player Four are not mapped to the keyboard. By default the keyboard mapping is such that on standard sized keyboard two people should be able to play at the same time. Beware though that an average PC keyboard only supports 4 simultaneous key presses, some better keyboards support 6, and fancy gaming keyboards can sometime support 7+. Some modifier keys like KeyCode.LeftShift can be exempted from these rules, but in the end it’s all up to the manufacturer, and how much money they tried to save on the wiring/traces of their keyboard.

_images/playerone_mapping.png

Player one gamepad keyboard mapping.


_images/playertwo_mapping.png

Player two gamepad keyboard mapping.

If you don’t like this mapping you can easily provide your own keyboard mapping through the RB.InputOverrideMethod delegate, like this:

public bool Initialize()
{
    RB.InputOverride(MyOverrideMethod)

    return true;
}

public bool MyOverrideMethod(int button, int player, out bool handled)
{
    if (player & RB.PLAYER_ONE) {
        if (button & RB.BTN_A) {

            handled = true;

            if (Input.GetKey(KeyCode.LeftControl))
            {
                return true;
            }

            return false;
        }
    }

    handled = false;
    return false;
}

In this example we remap RB.BTN_A for RB.PLAYER_ONE to the KeyCode.LeftControl key. First we tell RetroBlit about our override method with the call to RB.InputOverride. Next when RetroBlit calls MyOverrideMethod we check which button it’s looking for, and for which player. If it’s RB.BTN_A for RB.PLAYER_ONE, and the key KeyCode.LeftControl is pressed then we return true to indicate that RB.BTN_A is pressed, otherwise we return false. We also need to set the handled out variable to indicate whether we handled this button ourselves or if we want to fall back on the RetroBlit default mapping.

Keyboard

The keyboard API is straight-forward, and similar to the UnityEngine.Input APIs. For example:

public void Update()
{
    if (RB.KeyDown(KeyCode.Z))
    {
        // Handle Z down
    }

    if (RB.KeyPressed(KeyCode.Z))
    {
        // Handle Z pressed in last Update() call
    }

    if (RB.KeyReleased(KeyCode.Z))
    {
        // Handle Z released in last Update() call
    }

    myInputStr += RB.InputString();
}

This code should be self explanatory, the only interesting part is the last line. The RB.InputString method returns the string typed since last frame. Usually this will only contain 0-1 characters, but if the user is typing very quickly it may contain more. You may think that you could achieve the same functionality if you just check every KeyCode and add it to myInputStr if it’s pressed. Consider what would happen if the user is typing quickly and they pressed both the A and B keys in the last frame. How would you know if they meant to type AB or BA? You can’t know, but RB.InputString does!

Pointer

The pointer input represents either mouse input, or touch screen input (single touch only). It’s simple really:

public void Update()
{
    pointerPos = RB.PointerPos();

    if (RB.ButtonPressed(BTN_POINTER_A))
    {
        // Handle left mouse button or touch screen pressed
    }
}

That’s easy enough, and will work well for a mouse. However, what happens if the user is on the touch screen device, and is not even touching the screen? The position would not be valid. You can check if it’s valid like this:

public void Update()
{
    if (RB.PointerPosValid())
    {
        pointerPos = RB.PointerPos();
    }
}

Finally you may find it handy to check the vertical scroll wheel movement since last update:

public void Update()
{
    scrollPos += RB.PointerScrollDelta();
}

RB.PointerScrollDelta returns the scroll direction and magnitude since last Update. If the scroll wheel is not being scrolled or there is no mouse at all then 0 is returned.

Garbage Collection

Garbage Collection is great for simplifying development in general, but for game development it has some undesired side effects. If a game generates significant amount of garbage per frame then stuttering and performance hiccups are inevitable. RetroBlit is very careful not to cause any unnecessary allocations, and it generates only 40 bytes of garbage per frame. To keep your game running smoothly you should also strive to generate as little garbage as possible.

Knowing where your garbage is coming from is not always straight forward, but the Unity Profiler can be a fantastic tool for helping you locate your garbage. Learn more about the Profiler and Garbage Collection in general here: https://unity3d.com/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games

String

One of the biggest and sneakiest Garbage Collection culprits is the string type. Almost all string manipulation causes C# to spew out garbage, if this happens every frame your garbage can quickly get out of hand. For example, this will cause 100 bytes of garbage every frame!

int score = 500;
string str = "Score: " + score + "!";
RB.Print(new Vector2i(0, 0), Color.white, str);

The code may look innocent but here is what happens behind the scenes.

  1. C# treats "Score: " and "!" as literals and they are stored in your CLR assembly metadata. This is just a fancy-pants way of saying that these strings themselves do not cause any additional garbage. So far so good.
  2. C# converts the integer score to the string "500" this allocates 6 bytes for the characters (each character is stored in the unicode char which is 2 bytes), plus roughly an additional 20 bytes of mystery string object data.
  3. C# now concatenates "Score: " and "500" creating the new string "Score: 500" which takes 20 bytes for the characters and another 20 bytes of object data. The string generated in #2 is discarded and our total garbage is now roughly 22 bytes
  4. C# now concatenates "Score: 500" and "!" creating the new string "Score: 500!" which takes 22 bytes for the characters and another 20 bytes of object data. The string generated in #3 is discarded and our total garbage is now roughly 62 bytes.
  5. RB.Print does not cause any additional garbage, but once the frame ends the string "Score: 500!" is discarded, and now our total garbage is roughly 104 bytes! We’re off by 4 bytes because it’s hard to really predcit what the assembly will do, but this is a fair estimate.
_images/string_gc.png

Unity Profiler does a great job showing where the garbage is generated! This result is obtained with .NET 3.5 on Unity 2017.1 running on Windows 10 64bit. Your results may vary slightly on your platform.

FastString

To combat the evils of string RetroBlit provides the class FastString, which generates no garbage! FastString preallocates memory for the string, and keeps reusing the same memory instead of discarding it and causing garbage.

private FastString str = new FastString(128);

public void Render()
{
    int score = 500;

    str.Clear();
    str.Append("Score: ");
    str.Append(score);
    str.Append("!");

    RB.Print(new Vector2i(0, 0), Color.white, str);
}

In this code we have a preallocated a FastString that can store up to 128 characters, and instead of discarding it on every frame we call Clear() to empty it out, and then a series of Append() calls to fill in the new content. Every FastString method also returns that same instance of FastString so for convenience these calls could be chained together like this:

str.Clear().Append("Score: ").Append(score).Append("!");

The result is no garbage generated at all!

_images/faststring_gc.png

Zero garbage is a good thing.

You could even resuse the same FastString object in the same frame, in fact, you may be able to get away with a single FastString object for all your disposable strings!

str.Clear().Append("Score: ").Append(score).Append("!");
RB.Print(new Vector2i(0, 0), Color.white, str);

str.Clear().Append("HiScore: ").Append(hiscore).Append("!");
RB.Print(new Vector2i(0, 16), Color.white, str);

FastString overloads the Append method for various types like string, int, float, Color, Vector2 and more, and it also allows you to format your numbers:

str.Clear().Append("Score: ").Append(500, 8, FastString.FILL_ZEROS);  // "Score: 00000500"
str.Clear().Append("Score: ").Append(500, 8, FastString.FILL_SPACES); // "Score:      500"
str.Clear().Append("Size : " ).Append(1.50351f, 1);                   // "Size : 1.5"

When using FastString you could still fall for the evils of string, so be careful to not do something like this:

// int.ToString() generates garbage
int score = 500;
str.Append(score.ToString());

// Let FastString convert the int to a string on it's own, so no garbage is generated
str.Append(score);

// This string concatenation also generates string garbage
str.Append("Score " + "!");

// And this does not
str.Append("Score").Append("!");

All RB.Print*() methods support FastString, as well as string.

Editor Extensions

RetroBlit has three editor scripts that help with import assets

Extension Purpose
RetroBlitAudioPostProcessor Sets Audio Asset settings for best RetroBlit results. In particular it sets any sound assets longer than 10 seconds as a streaming audio asset, to prevent long load times for music or other long audio assets.
RetroBlitTexturePreProcessor Sets Texture Asset settings for best RetroBlit results. In particular it sets filter mode to Point, and turns off texture compression.
RetroBlitTMXPostProcessor Converts TMX files (.tmx, .tsx, .tx) into a RetroBlit binary format that not only can be packaged correctly by Unity but also greatly increases the load speed of these files which would otherwise have to be parsed with an XML parser.

You may sometimes want RetroBlit to ignore certain assets and not run them through its editor scripts. To do that simply put these assets under a subfolder called RetroBlit-ignore. For example these texture assets will not be modified by RetroBlit:

Assets/RetroBlit-ignore/SplashScreen.png
Assets/MyResources/RetroBlit-ignore/GameIcon128x128.png

Source Control Ignore List

When using source control you typically want to ignore some file types. At minimum I suggest ignoring these for RetroBlit

Temp
Library
.vs
*.csproj
*.pdb
*.suo
*tmx.rb*
*tmx.meta
*tsx.meta
*tx.meta

The first few would apply to any Unity project, the tmx/tsx/tx ignores should be added to ignore RetroBlit specific conversion to binary format of those files, it’s better to let RetroBlit regenerate the binary format of TMX files if the project is re-synced/cloned.

Closing Words

Thank you so much for your interest in RetroBlit. RetroBlit is a one man’s quest to create the perfect retro framework for my own projects. Along the way I realized others may find it useful if I put in a bit more effort to make it presentable (it turned out to be a lot more effort!). I sincerely hope that you enjoy the framework, and that you create something fantastic with it!

If you enjoyed RetroBlit and would like to continue supporting it’s development then please consider leaving your review on the Unity Asset Store!

Feel free to contact me at: pixeltrollgames@gmail.com