Playing with a single snake is fun, but letting the user select their character is a time-tested game mechanic.
We can add a new snake_index
setting to the GameSettings
. and default the value to whatever we want. In this case I’ve chosen the index 8
to show it being different than the 0
I had there before.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GameSettings {
pub audio: AudioSettings,
pub snake_index: usize,
}
impl Default for GameSettings {
fn default() -> Self {
GameSettings {
audio: AudioSettings::ON,
snake_index: 8,
}
}
}
In snake.rs
we can query for the GameSettings
resource and use the snake index as our snake_texture_index
. Since we built this function in a way that set the indexes based off of this index already, we have no more work to do.
pub fn render_snake_segments(
...
settings: Res<GameSettings>,
) {
let snake_texture_index = settings.snake_index;
A cargo run
will show the new snake texture being used.
Allowing User Selection
Let’s get to work in a new snake_selector
submodule. In ui.rs
:
mod button;
mod checkbox;
mod settings;
mod snake_selector;
and in snake_selector.rs
we’ll start off with a SnakeHead
widget.
The SnakeHead
widget, when clicked, will set the GameSettings.snake_index
value to the relevant snake head’s index.
The SnakeHead
will be a 30x30 pixel square that shows which head you’re choosing.
It accepts a tuple containing the snake_index
in the first slot and the handle to the snake image in the second. This second value is the one that’s returned from the image manager.
#[derive(WidgetProps, Clone, Debug, Default, PartialEq)]
struct SnakeHeadProps {
handle: (usize, u16),
}
#[widget]
fn SnakeHead(props: SnakeHeadProps) {
let image_styles = Style {
width: StyleProp::Value(Units::Pixels(30.0)),
height: StyleProp::Value(Units::Pixels(30.0)),
..Default::default()
};
let on_event = OnEvent::new(move |ctx, event| {
match event.event_type {
EventType::Click(..) => {
ctx.query_world::<ResMut<GameSettings>, _, _>(
|mut settings| {
settings.snake_index = props.handle.0 * 4;
},
);
}
_ => (),
}
});
rsx! {
<NinePatch
on_event={Some(on_event)}
styles={Some(image_styles.clone())}
border={Edge::all(1.0)}
handle={props.handle.1}
/>
}
}
SnakeSelector widget
We can take advantage of this widget to build out the SnakeSelector
.
The SnakeSelector
is going to order the SnakeHead
s into a grid.
There is a grid layout in kayak, but it’s a bit buggy at the moment so we’re going to manually construct our own rows and columns. This will be a bit more manual work typing but renders out what we need.
To build up all of the image handles we’ll take a slightly different approach than usual.
Bevy, as of 0.7, doesn’t currently support getting handles for individual sprites in a TextureAtlas
, so we have to resort to loading in a set of additional images.
Each image is in the assets folder under snake_heads/snake_sprites_{:02}.png
. so we can build a range from 1 to 30 and iterate over it.
We grab an exclusive reference to the world and use it to get the Bevy AssetServer
.
The AssetServer
can then be used to load each image individually.
We use a format string that pads the numbers we’re passing in to two places. This means for single digit numbers we get a 0
placed before them in the filename, while two-digit numbers are left as-is.
After grabbing all the images, we .enumerate
over the iterator, which matches each item in the iterator with the index it exists at. The first item will get index 0, and so on.
#[widget]
pub fn SnakeSelector() {
let container: Vec<(usize, u16)> = (1..31)
.into_iter()
.map(|num| {
let mut world =
context.get_global_mut::<World>().unwrap();
let asset_server = world
.get_resource::<AssetServer>()
.unwrap();
let handle: Handle<
bevy::render::texture::Image,
> = asset_server.load(&format!(
"snake_heads/snake_sprites_{:02}.png",
num
));
let mut image_manager = world
.get_resource_mut::<ImageManager>()
.unwrap();
let image = image_manager.get(&handle);
image
})
.enumerate()
.collect();
...
We set up the styles for the overall SnakeSelector
container and each of the rows inside of it.
The math in the width
and height
fields of the snake_container_styles
is calculating the width and height of the containing box based on the width of the SnakeHead
squares plus the gap space between them.
let row_styles = Style {
col_between: StyleProp::Value(Units::Pixels(5.0)),
layout_type: StyleProp::Value(LayoutType::Row),
..Default::default()
};
let snake_container_styles = Style {
width: StyleProp::Value(Units::Pixels(
6.0 * 30.0 + 5.0 * 5.0,
)),
height: StyleProp::Value(Units::Pixels(
5.0 * 30.0 + 4.0 * 15.0,
)),
row_between: StyleProp::Value(Units::Pixels(15.0)),
layout_type: StyleProp::Value(LayoutType::Column),
..Default::default()
};
This next bit of code is an artifact of the lack of support both in Bevy and Kayak for what we’re trying to achieve here. We have to manually set up each of the SnakeHead
widgets and clone the associated data.
It’s unfortunate, and if you’re a person who writes out all the code from the workshops: I suggest you copy paste this section. I am confident it will get better in the near future though so check the lesson details for updates.
let one = container[0].clone();
let two = container[1].clone();
let three = container[2].clone();
let four = container[3].clone();
let five = container[4].clone();
let six = container[5].clone();
let seven = container[6].clone();
let eight = container[7].clone();
let nine = container[8].clone();
let ten = container[9].clone();
let eleven = container[10].clone();
let twelve = container[11].clone();
let thirteen = container[12].clone();
let fourteen = container[13].clone();
let fifteen = container[14].clone();
let sixteen = container[15].clone();
let seventeen = container[16].clone();
let eighteen = container[17].clone();
let nineteen = container[18].clone();
let twenty = container[19].clone();
let twentyone = container[20].clone();
let twentytwo = container[21].clone();
let twentythree = container[22].clone();
let twentyfour = container[23].clone();
let twentyfive = container[24].clone();
let twentysix = container[25].clone();
let twentyseven = container[26].clone();
let twentyeight = container[27].clone();
let twentynine = container[28].clone();
let thirty = container[29].clone();
rsx! {
<Element styles={Some(snake_container_styles)}>
<Element styles={Some(row_styles)}>
<SnakeHead handle={one}/>
<SnakeHead handle={two}/>
<SnakeHead handle={three}/>
<SnakeHead handle={four}/>
<SnakeHead handle={five}/>
<SnakeHead handle={six}/>
</Element>
<Element styles={Some(row_styles)}>
<SnakeHead handle={seven}/>
<SnakeHead handle={eight}/>
<SnakeHead handle={nine}/>
<SnakeHead handle={ten}/>
<SnakeHead handle={eleven}/>
<SnakeHead handle={twelve}/>
</Element>
<Element styles={Some(row_styles)}>
<SnakeHead handle={thirteen}/>
<SnakeHead handle={fourteen}/>
<SnakeHead handle={fifteen}/>
<SnakeHead handle={sixteen}/>
<SnakeHead handle={seventeen}/>
<SnakeHead handle={eighteen}/>
</Element>
<Element styles={Some(row_styles)}>
<SnakeHead handle={nineteen}/>
<SnakeHead handle={twenty}/>
<SnakeHead handle={twentyone}/>
<SnakeHead handle={twentytwo}/>
<SnakeHead handle={twentythree}/>
<SnakeHead handle={twentyfour}/>
</Element>
<Element styles={Some(row_styles)}>
<SnakeHead handle={twentyfive}/>
<SnakeHead handle={twentysix}/>
<SnakeHead handle={twentyseven}/>
<SnakeHead handle={twentyeight}/>
<SnakeHead handle={twentynine}/>
<SnakeHead handle={thirty}/>
</Element>
</Element>
}
}
Snake texture selection is now just a cargo run
away!
Feel free to try to make your own snake assets. I’ve included the .psd file, but you could also just paint over the png in another program like Figma.