-
Notifications
You must be signed in to change notification settings - Fork 5
3. Drawing Sprites
In this section it will be explained how to draw 2D textures on the screen using the built-in SpriteBatch
class.
📖 Want to jump right in? Learn with a code example.
To use the implementations refered on this section you must include the following module(s):
- pixel-core
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 theSpriteBatch
and can be used for other purposes - in this scenario, theSpriteBatch
uses theCamera2D
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 totrue
(enabled by default). If you disable the automatic clear, you can clean up the screen manually by using thePixelWindow
.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.
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.
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.
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();
}