Spaceship and bullets
To begin with we’ll add graphics for the spaceship that the player controls. It will be animated with two different sprites and will also have different animations for when the spaceship moves to the left or right. We’ll also add a texture with animation for the bullets that the spaceship shoots.
Implementation
Import
The animation support in Macroquad is considered an experimental feature. It might change in a future version of Macroquad. It is not included in the prelude that we have imported, so we will have to import it explicitly.
Import the structs AnimatedSprite
and Animation
at the top of main.rs
file.
use macroquad::experimental::animation::{AnimatedSprite, Animation};
Configure assets directory
We need to start by defining where Macroquad should read the resources. We’ll
use the function set_pc_assets_folder()
that takes the path to the assets
directory relative to the root directory of the game. This is needed for
platforms that might place files in other places and also has the added
benefit that we don’t need to add the directory name for every file we load.
Add the following code in the main
function above the game loop:
set_pc_assets_folder("assets");
Load textures
Load the image files used for the animation textures of the ship and bullets.
Use the function load_texture()
to load a texture, which takes the name of
the file to load. This function is async, because it supports loading files
over HTTP in WebAssembly, so we need to call await
to get the result.
Since loading files can fail, this function will return a Result
. We will
call expect()
on the result to stop the program if it wasn’t possible to
load the file. This can happen if the file is missing, or it has wrong read
permissions. On WebAssembly it is possible that the HTTP request failed.
After loading the texture we’ll set which kind of filter to use when scaling
the texture using the method set_filter()
. We will use the filter
FilterMode::Nearest
because we want to keep the pixelated look of the
sprites. This needs to be done on every texture that is loaded. For high
resolution textures it would be better to use FilterMode::Linear
which gives
a linear scaling of the texture.
We’ll load the file ship.png
that contains the animations for the spaceship,
and the file laser-bolts.png
that contains animations for two different
kinds of bullets.
let ship_texture: Texture2D = load_texture("ship.png").await.expect("Couldn't load file");
ship_texture.set_filter(FilterMode::Nearest);
let bullet_texture: Texture2D = load_texture("laser-bolts.png")
.await
.expect("Couldn't load file");
bullet_texture.set_filter(FilterMode::Nearest);
Build a texture atlas
After loading all the textures we’ll call the Macroquad function
build_textures_atlas()
that will build an atlas containing all loaded
textures. This will ensure that all calls to draw_texture()
and
draw_texture_ex()
will use the texture from the atlas instead of each
separate texture, which is much more efficient. All textures need to be loaded
before this function is called.
build_textures_atlas();
Bullet animation
The image laser-bolts.png
is composed of four sprites, in two rows. These
make up the animations for two different types of bullets. We will name the
first one bullet
and the second one bolt
. Each animation is one row with
two frames each and they should be shown at 12 frames per second. The size of
the sprites is 16x16 pixels.
Each animation in a spritesheet is placed in a separate row, with the frames
next to each other horizontally. Each Animation
should have a descriptive
name, define which row in the spritesheet it is, how many frames it has,
and how many frames should be displayed each second.
Create an AnimatedSprite
with the tile_width
and tile_height
set to
16
, and an array with an Animation
struct for each of the two rows in the
spritesheet. The first one should be named bullet
and have the row 0
and
the second one should have the name bolt
and the row 1
. Both should have
frames
set to 2
and fps
set to 12
.
We will only use the second animation, so we’ll use the method
set_animation()
to define that we will be using the animation on row 1
.
let mut bullet_sprite = AnimatedSprite::new(
16,
16,
&[
Animation {
name: "bullet".to_string(),
row: 0,
frames: 2,
fps: 12,
},
Animation {
name: "bolt".to_string(),
row: 1,
frames: 2,
fps: 12,
},
],
true,
);
bullet_sprite.set_animation(1);
Spaceship animation
The spritesheet for the spaceship is in the image ship.png
and we need to
define how the animations in the spritesheet should be displayed. We have to
create an AnimatedSprite
for the ship as well. The size of each frame of the
spaceship spritesheet is 16x24 pixels, so we’ll set tile_width
to 16
and
tile_height
to 24
. After that is an array with an Animation
struct for
each animation in the spritesheet that we want to use.
There are five animations available in the spritesheet, with the first one in
the top row. We will only use three of the spritesheet animations in our
AnimatedSprite
, the second and fourth row from the top in the spritesheet
are unused. The first one is used when flying up or down, so add an
Animation
in the AnimatedSprite
with row
defined as 0
as the indexes
are 0-based, and the name
set to idle
. The ship will keep pointing up
regardless of if it moves up or down. The second Animation
is for moving the
spaceship to the left, which will use the row with index 2
in the
spritesheet. Finally, the third Animation
is used when moving the spaceship
to the right, and has the row
index 4
. There are two frames
in each
Animation
and the fps
should be set to 12 frames per second.
Finally we set playing
to true
so that the animation will be active.
let mut ship_sprite = AnimatedSprite::new(
16,
24,
&[
Animation {
name: "idle".to_string(),
row: 0,
frames: 2,
fps: 12,
},
Animation {
name: "left".to_string(),
row: 2,
frames: 2,
fps: 12,
},
Animation {
name: "right".to_string(),
row: 4,
frames: 2,
fps: 12,
},
],
true,
);
Animate direction
For the spaceship we need to set which animation to use based on the direction
movement. In the code for moving the spaceship we will add a line where we use
the method set_animation()
on the ship_sprite
. We start by setting the
animation to 0
if it isn’t turning in any direction, if it is moving to the
right we’ll set the animation to 2
, and if it moves to the left we’ll set
the animation to 1
. These numbers are indexes in the array of Animation
structs we defined in the AnimatedSprite
for the spaceship, which means they
are 0-based.
ship_sprite.set_animation(0);
if is_key_down(KeyCode::Right) {
circle.x += MOVEMENT_SPEED * delta_time;
direction_modifier += 0.05 * delta_time;
ship_sprite.set_animation(2);
}
if is_key_down(KeyCode::Left) {
circle.x -= MOVEMENT_SPEED * delta_time;
direction_modifier -= 0.05 * delta_time;
ship_sprite.set_animation(1);
}
Change bullet size
Since the graphics for the bullets are larger than the tiny circle we used to draw for them, we need to change the size and starting position when creating a bullet.
if is_key_pressed(KeyCode::Space) {
bullets.push(Shape {
x: circle.x,
y: circle.y - 24.0,
speed: circle.speed * 2.0,
size: 32.0,
collided: false,
});
}
Update animations
In order for Macroquad to animate the textures, we need to call the method
update()
on every sprite inside our game loop. Add the following two lines
below the code that updates the positions of enemies and bullets.
for square in &mut squares {
square.y += square.speed * delta_time;
}
for bullet in &mut bullets {
bullet.y -= bullet.speed * delta_time;
}
ship_sprite.update();
bullet_sprite.update();
Draw bullet animations
Now we can use the function draw_texture_ex
to draw each frame of the
animation. Remove the lines that draw a circle for each bullet and insert
instead the code below. First we call the method frame()
on the
bullet_sprite
to get the current animation frame and set it to the variable
bullet_frame
.
Inside the loop that draws all the bullets we’ll call draw_texture_ex
to
draw the bullet frame. It takes the bullet_texture
as argument, and an x
and y
position based on the size of the bullet. We also add the struct
DrawTextureParams
with the fields dest_size
and source_rect
. The field
dest_size
defines in which size the texture will be drawn, so we will use a
Vec2
with the size of the bullet for both x
and y
. Finally we’ll use
bullet_frame.source_rect
, which is a reference to where in the texture the
current frame is placed.
let bullet_frame = bullet_sprite.frame();
for bullet in &bullets {
draw_texture_ex(
&bullet_texture,
bullet.x - bullet.size / 2.0,
bullet.y - bullet.size / 2.0,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(bullet.size, bullet.size)),
source: Some(bullet_frame.source_rect),
..Default::default()
},
);
}
By using
DrawTextureParams
it is possible to change how the texture should be drawn. It is possible to
draw the texture rotated or mirrored with the fields rotation
, pivot
,
flip_x
, and flip_y
.
Draw the spaceship frames
Finally it’s time to replace the circle with the texture for the spaceship. It
works in the same way as for the bullets. First we’ll retrieve the current
frame from the animation sprite, and then we’ll draw it using
draw_texture_ex()
.
Because the spaceship animation isn’t the same size in width and height, we’ll
use ship_frame.dest_size
to define which size should be drawn. To make it a
bit bigger we’ll double the size.
let ship_frame = ship_sprite.frame();
draw_texture_ex(
&ship_texture,
circle.x - ship_frame.dest_size.x,
circle.y - ship_frame.dest_size.y,
WHITE,
DrawTextureParams {
dest_size: Some(ship_frame.dest_size * 2.0),
source: Some(ship_frame.source_rect),
..Default::default()
},
);
If everything works correctly, there should be animated graphics for both the spaceship and the bullets when running the game.
Improve loading times
Adding the following snippet at the end of the Cargo.toml
file will ensure
that the assets are loaded much faster when running on a desktop computer.
[profile.dev.package.'*']
opt-level = 3