-
Notifications
You must be signed in to change notification settings - Fork 2
TextureManager
#TextureManager The texture manager has 3 important roles: to load, draw and unload textures.
- Loading a texture: Returns a reference counted handle to a texture. If you load the same texture two times it is only loaded into memory once. It will not be unloaded from memory until you unload it twice.
- Drawing a texture: Draws a texture using hardware acceleration. Several transformation like translate, rotate and scale can be applied to what is being drawn with the right arguments. This function has a LOT of overloads.
- Unloading a texture: Removes one from the reference count of the given texture. Once the reference count reaches 0 the textures memory is eligible to be released.
You are responsible for managing loaded textures, this means every time you load a texture you must unload it! Always unload a loaded texture using UnloadTexture.
Textures are reference counted. Don't share a reference you obtained trough LoadTexture, more on this later.
All of your textures should be powers of two. Powers of two are: 0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048. For performance reasons you don't want to go over 2048. A texture that has the same width and height is called a square texture. Your textures don't need to be square, you can have a 512x1024, 256x256 or even a 1x1 texture, mix and match. So long as both the width and height are a power of two (POT) you are fine.
Why power of two? To actually rasterize an image onto screen the GPU has to divide and multiply by 2 a lot. If you remember binary, left shifting << is the same as multiplying by two and right shifting >> is the same as dividing by two. These shifts have the same effect as a multiply or divide but are HUNDREDS of times faster. This is why having POT textures is a MUST for any decent performing game.
Let's explore how to actually use the texture manager. We are going to be starting from the skeleton created on the repository's front page.
#Initialize and shutdown
The texture manager's Initialize and shutdown functions have the same signature as the GraphicsManager's initialize and shutdown. The texture manager is unique in that it requires the GraphicsManager to be initialized first. When initializing and shutting down multiple managers, always shut down in the reverse order that you initialized. To initialize and shutdown the TextureManager in Program.cs
public static void Initialize(object sender, EventArgs e) {
GraphicsManager.Instance.Initialize(Window);
TextureManager.Instance.Initialize(Window);
}
public static void Update(object sender, FrameEventArgs e) {
// UPDATE GAME
}
public static void Render(object sender, FrameEventArgs e) {
// RENDER GAME
}
public static void Shutdown(object sender, EventArgs e) {
TextureManager.Instance.Shutdown();
GraphicsManager.Instance.Shutdown();
}
#Load and unload
Download this texture pack. Unzip into an Assets folder next to the visual studio solution (sln) file. Make sure to set the working directory in project settings.
We can load a texture using the LoadTexture member function of the TextureManager class. The signature of this function:
public int LoadTexture(string texturePath)
The function returns an integer. This is a handle to the texture you just loaded. You need to hang on to this integer as it will be used to draw and later unload the texture. We're going to load 3 textures in this demo, so let's create three static variables in Program.cs
static int texBird = -1;
static int texJack = -1;
static int texAku = -1;
These are going to be the handles to our textures. They don't HAVE to be -1 by default, but using a known value makes it easy to debug (And loaded handles will always be >= 0). Now, let's load up these textures in the Initialize method of Program.cs
public static void Initialize(object sender, EventArgs e) {
GraphicsManager.Instance.Initialize(Window);
TextureManager.Instance.Initialize(Window);
texBird = TextureManager.Instance.LoadTexture("Assets/Bird.png");
texJack = TextureManager.Instance.LoadTexture("Assets/Jack.png");
texAku = TextureManager.Instance.LoadTexture("Assets/Aku.png");
}
Get in the habit of unloading whatever you loaded as soon as you load it. To unload a texture just pass it's handle to the Unload method of the TextureManager, the signature of this method looks like this:
public void UnloadTexture(int textureId)
In that spirit, let's unload these images in the Shutdown function of Program.cs
public static void Shutdown(object sender, EventArgs e) {
TextureManager.Instance.UnloadTexture(texBird);
TextureManager.Instance.UnloadTexture(texJack);
TextureManager.Instance.UnloadTexture(texAku);
// You don't HAVE to set these to -1, but it will make debugging
// a lot easyer if you set freed references to a known value
texBird = texJack = texAku = -1;
TextureManager.Instance.Shutdown();
GraphicsManager.Instance.Shutdown();
}
Take note that we loaded the textures after the managers where initialized, so we delete the textures before they are shut down. This makes sense, after all you need a manager to unload the texture. In the next section we will explore how to draw these textures
#Draw texture
All texture rendering must be done between ClearScreen and SwapBuffers, let's get the render function ready:
public static void Render(object sender, FrameEventArgs e) {
GraphicsManager.Instance.ClearScreen(Color.CadetBlue);
// TODO: Draw textures
GraphicsManager.Instance.SwapBuffers();
}
You can draw a texture using the Draw method of the TextureManager. This can get a bit tricky, the Draw method has several overloads. We're going to go over them one at a time.
The first overload we are going to go over is the simplest, it's signature looks like this:
public void Draw(int textureId, Point screenPosition)
This method will take a texture ID, and draw the entire texture at a given screen position.
- Use the above function to draw texAku at point 0, 0
Assuming your game window is 800 x 600 your screen should look like this:
The aku texture is 1024 x 1024. If you re-size the window you can see that it doesn't really fill the entire window, it just fit in the default resolution perfectly. Each overload is going to add more and more arguments to the function. But the first few arguments are always going to be the same. Let's explore the next overload.
The next overload will let us scale the image on screen. The Bird texutre is 512 x 512, let's draw it in several different resolutions. The signature for scaling looks like:
public void Draw(int textureId, Point screenPosition, float scale)
public void Draw(int textureId, Point screenPosition, PointF scale)
You can use either overload of the function. The one that takes a single float for scale will scale both width and height uniformly. The one that takes a PointF will let you scale the X and Y axis independently. After aku
- Draw texBird at 0, 0 with a uniform scale of 0.25
- Draw texBird at 50, 0 with a uniform scale of 0.5
- Draw texBird at 170, 30 with a uniform scale of 0.75
Your screen should look like this:
We drew the bird texture 3 times, but it's only loaded into memory once! So far every time we've drawn a texture it's been the whole texture. But sometimes you only want to draw a part of a texture. The next overload we look at will allow us to do just this! If you look at Jack.png you will notice Jack is in the bottom of the image with the top being filled with trash, let's draw only the useful sub-image.
The signature for this function looks like this:
public void Draw(int textureId, Point screenPosition, float scale, Rectangle sourceSection)
public void Draw(int textureId, Point screenPosition, PointF scale, Rectangle sourceSection)
We have two rectangles because every new render functionality is just an extra paramater. When we introduced scaling we added an override, and that override must be respected here too. The sourceSection Rectangle argument is in pixels the sub-section of the texture we want to draw. We will draw only what is inside of the sourceSection rectangle to screen. After the birds
- Draw texJack at 0, 288 with a uniform scale of 1 using the following source rect:
- X: 0, Y: 200, W: 256, H: 312
- H is 312 because Y starts at 200, and total height is 512. 512 - 200 = 312
- X: 0, Y: 200, W: 256, H: 312
Your screen should look like this:
#Rotation
The lat override of the Draw method deals with rotation. The API is simple, but rotating can be rather non-trivial at times. To make things easier, all rotation works in euler angles (0 to 360 degrees). Were going to try rotation with a new texture. Lets make two new member variables in Program.cs
static int texSwirl = -1;
static float swirlAngle = 0.0f;
texSwirl is the texture we are going to be drawing, swirlAngle is the angle of the rotation, which will change from frame to frame. We will be rotating 60 degrees every second. Let's add support for this in the Update method of Program.cs
public static void Update(object sender, FrameEventArgs e) {
swirlAngle += (float)e.Time * 60.0f;
while (swirlAngle > 360.0f) {
swirlAngle -= 360.0f;
}
}
That code is pretty straight forward. Next, load Assets/Swirl.png into texSwirl, don't forget to unload it. Now we can draw with a rotation overload, the signatures look like this:
public void Draw(int textureId, Point screenPosition, float scale, Rectangle sourceSection, float rotation)
public void Draw(int textureId, Point screenPosition, PointF scale, Rectangle sourceSection, float rotation)
Add the following to the Render function of Program.cs, after having rendered jack:
TextureManager.Instance.Draw(texSwirl, new Point(300,450), 0.25f, new Rectangle(0,0,512, 512), swirlAngle);
A few things to notice, first we are not drawing the swirl at full resolution, rather scaled to 0.25. The sourceSection rectangle is (0, 0, 512, 512), where 512 is the width and height of the swirl texture. It's hard-coded here, but there is a dynamic way to get this information, we will cover how later.
By default all rotation happens around the center of the sprite being drawn. There may be times when you want to rotate around an arbitrary point. For this reason we have one last overload, its:
public void Draw(int textureId, Point screenPosition, float scale, Rectangle sourceSection, Point rotationCenter, float rotation = 0.0f)
public void Draw(int textureId, Point screenPosition, PointF scale, Rectangle sourceSection, Point rotationCenter, float rotation = 0.0f)
It's almost the same as the previous, except now we have a Point called rotationCenter. This point is in pixels and RELATIVE to to the top-left of what is being drawn. That is, it's relative to the screenPosition rect. If we pass in 0,0 then we will rotate around screenPostion. Let's try it by adding this to Render:
TextureManager.Instance.Draw(texSwirl, new Point(275,325), 0.1f, new Rectangle(0,0,512, 512), new Point(0,0), -swirlAngle);
Now there should be a second, smaler swirl rotating around it's origin. Also notice, by passing in a negative angle the sprite is rotating in the opposite direction. The final screen should look like this:
#Multiple references In our trivial examples we have no need for multiple references to the same texture, but as your game gets more complicated you may end up getting more references to the same resource. To demonstrate that a texture into memory two times only really loads it once, paste this at the end of your initialize function:
Console.WriteLine("Original aku id: " + texAku);
int newAku = TextureManager.Instance.LoadTexture("Assets/Aku.png");
Console.WriteLine("New aku id: " + newAku);
Take note how both texture handles are the exact same. Now when you shut down your game, the console will show this left over message:
This warning lets you know that there are dangling references to Aku.png, it basically means that you are leaking memory! This is because you loaded the same texture twice, but only unloaded it once.
How can you fix this? Call UnloadTexture on texAku one more time in the Shutdown function.
#Getting width & height
Getting the width and height of a texture is simple, you just need the texture id. The TextureManager class has the following helper member functions:
public int GetTextureWidth(int textureId)
public int GetTextureHeight(int textureId)
public Size GetTextureSize(int textureId)
These functions are pretty straight forward, using them should be easy.
#Room for improvement Right now the TextureManager does not batch draw calls, instead it uses what is known as immediate rendering. In immediate rendering when a Draw call is made the texture is drawn right away. Batching means to draw everything that shares the same texture in one draw call. This is achieved by building up a queue of items to draw, then sorting the queue based on texture handles.
Batching can reduce the number of draw calls being made by up to 90%. This is not an action item for you, just something to be aware of. If your applications performance starts to suffer (IE, you can't keep a solid 60 FPS) let me know and we can implement batching.
Implementing batching involves rewriting the entire Draw fnction of the TextureManager and adding some extra function calls. Initially it's being left out to keep the render code simple.



