Skip to content

3. Drawing Sprites

João Alves edited this page Jan 17, 2022 · 37 revisions

Introduction

In this section it will be explained how to draw 2D textures on the screen using the built-in SpriteBatch class.

sprite on the screen example

📖 Want to jump right in? Learn with a code example.

Modules

To use the implementations refered on this section you must include the following module(s):

  • pixel-core

SpriteBatch

As the name suggests, the SpriteBatch is a class that is able to draw textures (aka sprites) on the screen by grouping multiple data with the intent of reducing GPU uploads (multiple sprites can be drawn using a single OpenGL draw call).

The first step to use a SpriteBatch is to create an instance of one:

private SpriteBatch spriteBatch; 
// ...
@Override
public void load() {
    spriteBatch = new SpriteBatch(); 
}

Notice that the instance was created on the load() function of the game window. This method is where you typically load or create the data to be used on your game scene (including the resources).

The SpriteBatch works with textures (image data), so the next step is to load an image resource from the filesystem so it can be used in our game.

To load a resource you can use the ContentManager class, it loads the raw data from a given file and converts it to a usable type. Bellow is an example of the utilization of the ContentManager loading up a Texture resource:

private SpriteBatch spriteBatch; 
private ContentManager content;
private Texture spriteTexture;
// ...
@Override
public void load() {
    spriteBatch = new SpriteBatch();
    content = new ContentManager(); // create an instance of the ContentManager class
    spriteTexture = content.load("<your_image_path_here>", Texture.class); // use it to load and process the texture
}
  • Relative paths are supported/preferred (based on the "resources" folder of your project).
  • The ContentManager can load different resource types (it will be explored more in further sections).
  • The ContentManager instance keeps all loaded resources in memory (cached). If the original file is changed, you need to clear the manager cache before attempting to load it again (there is a function variant that ignores the data cache if you need).

At this point, we have the SpriteBatch and a Texture instance, but before attempting to draw anything, it's important to understand the concept of the virtual viewport. When you create the PixelWindow, you specify the window size which is directly related to the boundaries of the game window appearing in your operating system. In addition, you can also specify the virtual size of the window, the scale can be 1:1 but not necessarily. More importantly, it determines the actual drawing area of your game.

For example, when you create a PixelWindow with a virtual size of 600x480 on a 1200x960 native window, if you draw a pixel on the 800 position of the x-axis it will not be visible (in this specific configuration, the window will be 2:1 as the 600x480 resolution is scaled up 2x to fit the 1200x960 window).

Needless to say, it would be very limiting to have the drawing space to always fit the virtual size, so instead of translating our textures to the visible area, the Pixel framework provides a Camera2D class that allows you to move around the virtual space as required.

With that in mind, add a Camera2D instance to our scene:

private SpriteBatch spriteBatch; 
private ContentManager content;
private Texture spriteTexture;
private Camera2D gameCamera; // added
// ...
@Override
public void load() {
    spriteBatch = new SpriteBatch(); 
    content = new ContentManager();
    spriteTexture = content.load("<your_image_path_here>", Texture.class);

    gameCamera = new Camera2D(this); // added
}

Now that we have our camera, we can translate our view to whatever position we need (for example following the player across a level). By default, the camera centers the position [x: 0, y: 0] on the screen - you can adjust it. In addition to moving around, you can also use the camera to rotate or zoom in/out your scene.

Notice that the Camera2D is independent of the SpriteBatch and can be used for other purposes - in this scenario, the SpriteBatch uses the Camera2D properties to apply the visualization matrix (position, rotation and scale/zoom) in our sprites.

Finally, let's draw our texture on the screen! As you probably already noticed, the PixelWindow has multiple methods you can override. For drawing we are going to create our implementation of the draw(DeltaTime delta) function - where the drawing of sprites, text, primitives, etc. happens.

@Override
public void draw(DeltaTime delta) {
   // where our scene is drawn
}

Note that the SpriteBatch has its own cycle. Use the begin() function to start a new cycle and the end() function to terminate it (flush the sprites).

Call the draw() function of the SpriteBatch anywhere between the begin() and the end() to add your texture to the drawing batch. Notice that there are multiple variants for this particular function (varying position, source, scaling, overlay color, rotation, origin/pivot, etc), please refer to the code documentation (or by using the IDE auto-completion) for more details.

Example:

@Override
public void draw(DeltaTime delta) {
    // whenever you want to draw with the sprite batch, you begin a new cycle by calling the begin() function:
    spriteBatch.begin(gameCamera.getViewMatrix()); // provide the view matrix of the camera to apply the current transformations

    // the draw() function has several parameter possibilities; take a look at the SpriteBatch class for clarification.
    // you can call the draw() multiple times, but always after the begin() and before the end() 
    spriteBatch.draw(spriteTex, Vector2.ZERO, Color.WHITE, Vector2.HALF, 1f, 1f, 0f);

    // ..and when the drawing process is completed, the end() function needs to be called:
    spriteBatch.end();
}

The same SpriteBatch can be used to draw different textures on the same cycle. Take in consideration that you should pack the game textures into a texture pack in order to reduce texture binding calls (same texture for different game graphics).

All done, start the game and see the texture on the screen!

  • Note that the render screen is automatically cleared if the PixelWindow.autoWindowClear property is set to true (enabled by default). If you disable the automatic clear, you can clean up the screen manually by using the PixelWindow.clear() function.
  • Don't forget to dispose the content manager when no longer needed - note that all cached resources loaded with it are also disposed by association.

Depth sorting

By default, the SpriteBatch will draw the textures in the same order as the draw() calls. In addition to that, it is also possible to manually define the depth level in each draw call (lower values will be rendered first).

To manually assign a depth level, check the draw() function variation that includes the depth parameter.

Use manual depth sorting only if needed as it takes a bit of extra steps during the draw calls.

TexturePack

Texture packs allow you to group multiple sprites into single textures. When provided with a meta file for sprite description it can be a fast and performant way to load and draw sprites in your game.

texture pack image preview

In order to simplify the utilization of texture packs, the Pixel Framework provides a built-in TexturePack content importer. Using the current implementation, the process is initiated by loading up a JSON Texture Pack format resource file using the ContentManager:

// ...
@Override
public void load() {
    //...
    TexturePack texturePack = content.load("texture-packs/spritemap.json", TexturePack.class)
    //...
}

The above code will load the .json file and extract all the information needed so that the texture pack can be used directly (make sure that the image texture associated to the pack has the correct path). To access any given frame, it can be done as follows:

// example of retrieving a texture pack frame named "playerShip1_blue":
TextureFrame frame = texturePack.getFrame("playerShip1_blue");

A TextureFrame contains information such as source dimensions and anchor of a given frame within the original texture.

The TexturePack contains a Texture property which can be used to draw the source image, using for example, the built-in SpriteBatch:

@Override
public void draw(DeltaTime delta) {
    spriteBatch.begin(gameCamera.getViewMatrix());
    // a different override of draw() will be used in this scenario so that we can grab the frame area from the original texture:
    spriteBatch.draw(texturePack.getTexture(), Vector2.ZERO, texturePack.getFrame("playerShip1_blue").getSource(), 
           Color.WHITE, Vector2.HALF, 1f, 1f, 0f);
    // ..
    spriteBatch.end();
}
Clone this wiki locally