70 %
Chris Biscardi

Why do Bevy sprites spawn with the center at 0,0?

Spawning two sprites in Bevy will spawn at the center of the transform. So if we have a black 3x3 block and a white 1x1 block, the smaller block will spawn at the center of the larger block, which spawned at the global (0,0) point.

1x1-on-3x3
rust
use bevy::prelude::*;
const BLOCK_SIZE: f32 = 25.0;
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.run();
}
fn setup(
commands: &mut Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands
.spawn(Camera2dBundle::default())
.spawn(SpriteBundle {
material: materials.add(Color::BLACK.into()),
sprite: Sprite::new(Vec2::new(
3.0 * BLOCK_SIZE,
3.0 * BLOCK_SIZE,
)),
..Default::default()
})
.spawn(SpriteBundle {
material: materials.add(Color::WHITE.into()),
sprite: Sprite::new(Vec2::new(
BLOCK_SIZE, BLOCK_SIZE,
)),
..Default::default()
});
}

1x1 on 3x3

This is more clearly apparent if we make the 3x3 block a 2x2 block.

1x1 on 2x2

If we're making a game like Tetris, this is a problem because we want a set grid with 1 block offsets, not half-block offsets.

Transforms

So if the offset is set by the center of the transform, we can do some calculations to set the offset to make it look like the origin is in a different location.

1x1 on two 2x2s

We'll spawn another square, the same size this time with a transform, which will offset the sprite relative to it's origin.

rust
.spawn(SpriteBundle {
material: materials.add(Color::BLUE.into()),
sprite: Sprite::new(Vec2::new(
2.0 * BLOCK_SIZE,
2.0 * BLOCK_SIZE,
)),
transform: Transform::from_translation(
Vec3::new(
0.5 * BLOCK_SIZE,
0.5 * BLOCK_SIZE,
0.0,
),
),
..Default::default()
})

Mesh offsets

The reason the we have to do math to calculate half the width and offset is that the default meshes in bevy spawn at the (0,0) point of the transform, which places .5 of the sprite to the left and .5 to the right of (0,0) . To fix this and place the bottom left of the mesh at (0,0) we can use a custom mesh.

rust
fn make_mesh() -> Mesh {
let mut mesh: Mesh =
shape::Box::new(1.0, 1.0, 0.0).into();
let vs =
if let VertexAttributeValues::Float3(vertices) =
mesh.attribute(Mesh::ATTRIBUTE_POSITION).expect(
"expected vertices from stdlib shape::Box",
)
{
vertices
.iter()
.map(|v| {
let mut points = v.clone();
for i in 0..v.len() {
points[i] = if points[i] != 0.0 {
points[i] + 0.5
} else {
0.0
};
}
points
})
.collect::<Vec<[f32; 3]>>()
} else {
panic!("should have worked")
};
mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, vs);
mesh
}

Bevy gives us the ability to create a default Box, which we can turn into a Mesh for use with the SpriteBundle. This Mesh has an attribute called POSITION that indicates the vertices on a unit box. The default box has vertices at places like (-0.5, -0.5, 0.0) (it's specified in 3 dimensions even though z is always 0), which indicates that half of the box will be on one side of the x or y axis and half will be on the other side. To fix this we can map over the vertices and add 0.5 to each value that is 0.5 or -0.5, which will put the original point (-0.5, -0.5, 0.0) at (0.0, 0.0, 0.0) and the farthest point, originally (0.5, 0.5, 0.0), at (1.0, 1.0, 0.0). This is what offsets the mesh against the transform.

The interior of the Vec needs to be a 3 element array, so we clone and mutate locally

The type signature of setup() needs to change. If you're unfamiliar this will likely feel a bit magical, as we get the meshes argument by declaring that we want it in the setup function arguments as ResMut<Assets<Mesh>>. Diving into why this happens is a topic for another post.

rust
fn setup(
commands: &mut Commands,
mut materials: ResMut<Assets<ColorMaterial>>,
mut meshes: ResMut<Assets<Mesh>>,
) {...}

We can now use our new mesh to spawn the same sized block as the black and blue blocks. This one will be pink.

We include the transform for clarity, but the default transform is the same as this one, so we could omit it if we wanted to

rust
.spawn(SpriteBundle {
mesh: meshes.add(make_mesh()),
material: materials.add(Color::PINK.into()),
sprite: Sprite::new(Vec2::new(
2.0 * BLOCK_SIZE,
2.0 * BLOCK_SIZE,
)),
transform: Transform::from_translation(
Vec3::new(0.0, 0.0, 0.0),
),
..Default::default()
})

We can also now confirm that a new purple 1x1 block at (0,0) will spawn squarely in the first quadrant of the pink 2x2 block.

rust
.spawn(SpriteBundle {
mesh: meshes.add(make_mesh()),
material: materials.add(Color::PURPLE.into()),
sprite: Sprite::new(Vec2::new(
BLOCK_SIZE, BLOCK_SIZE,
)),
..Default::default()
})