After a bit of re-factoring, we’re back with a second tutorial on embedding Tilengine into the Unity 3D game engine. This time, we’re taking things back to the drawing board, and starting from scratch. That means less functionality. But it also means a better, stronger base for moving forward. It also means we will be providing a nifty example of wrapping an unmannaged native Plugin inside of a C#-friendly, memory-managed class.
When I first attempted to embed Tilengine in Unity, I noticed that certain circumstances and function calls seemed to crash the Unity editor. In particular, attempting to clear the memory by calling the TLN_DeleteContext function or the TLN_Deinit function especially messed things up. It would also prove disastrous to attempt to run TLN_Init with different settings. So long as you stuck to one resolution, things ran smoothly. But I was plagued by doubts ability the stability of using a low-level C library, and the possibility of introducing a memory leak.
So I decided to strip the whole project down to only the most base essentials, and did some more research on how to handle low-level memory assignment from within a garbage-collection focused language like C#. What I found was the IDisposable class, and a decent guide on how to use it properly. So let’s dig into the results. Here’s a link to the Unity project that I cooked up.
This project is more self-contained, and doesn’t require downloading the Tilengine C# wrapper project. Instead it includes its own C# mapping for Tilengine. Let’s dig into that file first.
TilengineMapping.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
namespace MadWonder.Tutorial02 {
public static class TilengineMapping
{
[DllImport("Tilengine")]
public static extern IntPtr TLN_Init(int hres, int vres, int numlayers, int numsprites, int numanimations);
[DllImport("Tilengine")]
[return: MarshalAsAttribute(UnmanagedType.I1)]
public static extern bool TLN_SetContext(IntPtr context);
[DllImport("Tilengine")]
[return: MarshalAsAttribute(UnmanagedType.I1)]
public static extern bool TLN_DeleteContext(IntPtr context);
[DllImport("Tilengine")]
public static extern void TLN_Deinit();
[DllImport("Tilengine")]
public static extern void TLN_SetBGColor(byte r, byte g, byte b);
[DllImport("Tilengine")]
public static extern void TLN_SetRenderTarget(byte[] data, int pitch);
[DllImport("Tilengine")]
public static extern void TLN_UpdateFrame(int time);
[DllImport("Tilengine")]
public static extern void TLN_SetLoadPath(string path);
}
}
This file is based on the Tilengine C# wrapper project, but only includes some of the most basic, essential functions. We only have enough here to initilize a Tilengine instance, delete an instance, set the background color, set the render target, and update a basic frame. It is enough to get started with, but only just barely. This is a proof-of-concept prototype for integrating proper memory management, so we’ll only be covering the basics. We have a using statement that incorporates the System.Runtime.InteropServices library. And we have DLLImport attributes for the various functions that map to the Tilengine library. I have defined this as a static class with static functions so that it will be available wherever we need it. The more advanced Tilengine structures are not necessary for the time being. This will do for now. Let’s move on to the manager class.
EngineManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MadWonder.Tutorial02 {
public class EngineManager : IDisposable
{
private int width;
private int height;
private int layers;
private int sprites;
private int animations;
private IntPtr engineHandle;
private bool disposed;
private int frameCounter;
public EngineManager(int inputWidth, int inputHeight, int inputLayers, int inputSprites, int inputAnimations) {
this.width = inputWidth;
this.height = inputHeight;
this.layers = inputLayers;
this.sprites = inputSprites;
this.animations = inputAnimations;
this.frameCounter = 0;
this.disposed = false;
this.engineHandle = TilengineMapping.TLN_Init(this.width, this.height, this.layers, this.sprites, this.animations);
}
public virtual void UpdateFrame() {
bool contextSet = TilengineMapping.TLN_SetContext(this.engineHandle);
if (contextSet) {
TilengineMapping.TLN_UpdateFrame(this.frameCounter);
this.frameCounter++;
}
}
public virtual void SetRenderTarget(byte[] pixelArray) {
bool contextSet = TilengineMapping.TLN_SetContext(this.engineHandle);
if (contextSet) {
TilengineMapping.TLN_SetRenderTarget(pixelArray, this.width * 4);
}
}
public virtual void SetBackgroundColor(Color32 colorToSet) {
bool contextSet = TilengineMapping.TLN_SetContext(this.engineHandle);
if (contextSet) {
TilengineMapping.TLN_SetBGColor(colorToSet.r, colorToSet.g, colorToSet.b);
}
}
public void Dispose() {
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool manualDispose) {
if (!this.disposed) {
if (manualDispose) {
// Here's where you put any code for clearing out regular managed resources.
}
bool contextSet = TilengineMapping.TLN_SetContext(this.engineHandle);
if (contextSet) {
bool engineCleared = TilengineMapping.TLN_DeleteContext(this.engineHandle);
if (engineCleared) {
this.engineHandle = IntPtr.Zero;
}
}
this.disposed = true;
}
}
~EngineManager() {
this.Dispose(false);
}
}
}
For our Tilengine-equivalent class, we create a standard C# class, and then have it implement the IDisposable interface. This is a standard interface to use for wrapping up and dealing with unmannaged libraries. We define some variables for our class, pretty much all private. A Tilengine instance, once created, cannot have it’s settings altered, so we won’t need public access for most of those variables. Any data that needs to be passed out can have dedicated functions. For the class constructor, we define all of the standard arguments that Tilengine will require for initial setup. It is also where we call the TLN_Init function. This creates an instance of our unmannaged Tilengine structure, and assigns it a memory address. We snatch that memory address and store it in the private IntPtr variable we defined for our class. We can use this IntPtr to manage the Tilengine context that we’ve created.
Further down, we come across the IDisposable-required Dispose method, as well as an overridable overload function that takes a boolean argument. We’ll be doing most of the heavy lifting in the overloaded function. Here we check against the private disposed boolean, we only want to be running this function once. Then we check against the manualDispose argument. If it is true, then we will need to free the memory for any complex classes that we are using in our class. At the moment, we aren’t, so it’s safe to leave this area blank. Below that is where we actually do some low-level memory clean up. First, we use the TLN_SetContext function to switch the current Tilengine context to the one we created in this instance of our class. Then, when that succeeds, we call TLN_DeleteContext on that same IntPtr to clear out the unmannaged memory for our Tilengine context. Finally, we set our IntPtr to zero, just to keep things neat. The IntPtr wasn’t pointing to anything in particular, so it is no longer needed.
Finally, we call our overloaded Dispose method in the manual Dispose function, as well as in our class destructor. We include a call to GC.SuppressFinalize in the manual Dispose function to let the garbage collector know that we’ve got things handled. This covers the basic memory management. Now whether our class gets forcefully discarded, or automatically cleaned up by the garbage collector, we can be confident that the correct calls will be made to the Tilengine library to free up the assigned memory. Our EngineManager class is prepared for use in Unity. The other three functions, UpdateFrame, SetRenderTarget, and SetBackgroundColor, are all there to make use of mapped Tilengine functions that don’t dedicate additional memory. They are just there to provide us with a little extra functionality that we will make use of for demonstration purposes.
Engine.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MadWonder.Tutorial02 {
public class Engine : MonoBehaviour
{
public Material targetMaterial;
[ColorUsage(false)]
public Color32 backgroundColor = new Color32(184, 184, 184, 255);
[HideInInspector]
public Texture2D canvasTexture;
[Range(1.0f, 4096.0f)]
public int pixelWidth = 320;
[Range(1.0f, 2160.0f)]
public int pixelHeight = 180;
[Range(1.0f, 1024.0f)]
public int numberOfLayers = 8;
[Range(1.0f, 1024.0f)]
public int numberOfSprites = 32;
[Range(1.0f, 1024.0f)]
public int numberOfAnimations = 64;
public bool displayQuad;
private EngineManager engineRef;
private byte[] renderPixels;
public void Awake()
{
this.engineRef = new EngineManager(this.pixelWidth, this.pixelHeight, this.numberOfLayers, this.numberOfSprites, this.numberOfAnimations);
this.canvasTexture = new Texture2D(this.pixelWidth, this.pixelHeight, TextureFormat.BGRA32, false);
if (this.displayQuad) {
this.SetupDisplayQuad();
}
if (this.targetMaterial != null) {
this.targetMaterial.mainTexture = this.canvasTexture;
}
int byteCount = this.pixelWidth * this.pixelHeight * 4;
this.renderPixels = new byte[byteCount];
this.engineRef.SetRenderTarget(this.renderPixels);
this.engineRef.SetBackgroundColor(this.backgroundColor);
this.engineRef.UpdateFrame();
this.canvasTexture.LoadRawTextureData(this.renderPixels);
this.canvasTexture.Apply();
}
void Start() { }
void Update()
{
this.engineRef.UpdateFrame();
this.canvasTexture.LoadRawTextureData(this.renderPixels);
this.canvasTexture.Apply();
}
private void SetupDisplayQuad() {
GameObject freshQuad = GameObject.CreatePrimitive(PrimitiveType.Quad);
freshQuad.name = "RenderQuad";
freshQuad.transform.parent = this.transform;
freshQuad.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f);
Renderer quadAlterRender = freshQuad.GetComponent<Renderer>();
Shader freshUnlit = Shader.Find("Unlit/Texture");
Material alterMat = new Material(freshUnlit) { mainTexture = this.canvasTexture };
quadAlterRender.material = alterMat;
float targetRatio = (float)this.pixelWidth / (float)this.pixelHeight;
float verticalScale = 1.0f;
Camera snatchCam = GameObject.FindWithTag("MainCamera").GetComponent<Camera>();
if (snatchCam != null && snatchCam.orthographic)
{
verticalScale = snatchCam.orthographicSize * 2.0f;
}
freshQuad.transform.localScale = new Vector3(verticalScale * targetRatio, verticalScale, 1.0f);
}
}
}
At last we’re finally to the Unity script itself. We create our class, and have it extend the MonoBehaviour class. (like all basic Unity scripts do) For our script properties, it is fine for most of them to be public. We cover most of the basic data for generating a Tilengine instance. But we also throw in a Color32 property, for defining the background color, and a Texture2D property, which will serve as the end-product of our little experiment. For the private variables, we have an instance of our EngineManager class, as well as a byte array.
In our Awake function, we can define everything we want to have happen when our script initially runs. The first thing is to call the constructor for our EngineManager class, and feed in the public variables we defined. Next up is to call the constructor for our Texture2D instance, feeding it the same width and height variables. You’ll notice for the TextureFormat I specifically chose BGRA32. This is the format that works best with Tilengine, as it matches how Tilengine processes the pixel colors. I also set mip-mapping to false. This is a preference, and can be set to true. But I find that disabling mip-mapping does tend to make for sharper, cleaner pixel graphcis.
After that we need to set up our byte array for the rendering. First we determine the needed size of the array by determining the total number of pixels, and then multiplying that by four. Then we initialize our byte array with the calculated value. A call to our EngineManager’s SetRenderTarget function with the byte array as argument insures that our Tilengine context knows what to render to. A quick call to the SetBackgroundColor function feeds our chosen background color into our Tilengine context. And finally a call to the UpdateFrame function actually renders the current Tilengine context to the pixel array we just created. All this gets rounded out by assigning our rendered byte array to our Texture2D object using the LoadRawTextureData function, and then calling the Apply function to refresh our Texture2D and insure that it is ready to be passed to the GPU for rendering. That basically has us good to go.
In the Update function, where most of the scripts constant calls are posted, all we have to do is call the UpdateFrame function on our EngineManager object, and then refresh the Texture2D’s pixel assignment.
I included a final function called SetupDisplayQuad. It just keys off a public boolean that we defined for the class, and creates a Quad primitive that it assigns as a child of our GameObject. The Quad primitive in question is given a basic material with a basic unlit material, and then the results of our Texture2D output are assigned to it. It can be used to immediately display what is being rendered.
Now that we’ve reviewed all of this, it’s time to actually use it. Thankfully, this is all extremely basic. Create a GameObject in the scene hierarchy, and assign our created script to it. There should already be default values for all of the properties, so you should be able to fun it right away. But before you do, go ahead and select the “Display Quad” option in the inspector. When you hit the play button, you should see a rectangle show up. Stop the preview, and now go to the “Background Color” property in the inspector. Select whatever color you want. Now it the play button again. The rectangle should now be the color you selected. Make changes to the Pixel Width and Pixel Height properties to see the rectangle change proportions.
While not particularly flashy, what is happenign behind the scenes is that a Tilengine instance is getting created at the size and with the background color specified, along with the necessary memory for the number of layers, sprites, and animations desiered. And all of this is also getting freed from memory when appropriate. We now have a means of defining multiple Tilengine instances safely within Unity 3D, and running them concurrently, neatly wrapped within a standard Unity script.