parent
7aefd727f6
commit
8c33da0051
@ -249,6 +249,10 @@ path = "examples/ecs/parallel_query.rs"
|
||||
name = "hierarchy"
|
||||
path = "examples/ecs/hierarchy.rs"
|
||||
|
||||
[[example]]
|
||||
name = "alien_cake_addict"
|
||||
path = "examples/game/alien_cake_addict.rs"
|
||||
|
||||
[[example]]
|
||||
name = "breakout"
|
||||
path = "examples/game/breakout.rs"
|
||||
|
5
assets/models/AlienCake/README.md
Normal file
5
assets/models/AlienCake/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
Assets are from [Kenney](https://www.kenney.nl)
|
||||
|
||||
- alien from [Space Kit](https://www.kenney.nl/assets/space-kit)
|
||||
- cake from [Food Kit](https://www.kenney.nl/assets/food-kit)
|
||||
- ground tile from [Tower Defense Kit](https://www.kenney.nl/assets/tower-defense-kit)
|
BIN
assets/models/AlienCake/alien.glb
Normal file
BIN
assets/models/AlienCake/alien.glb
Normal file
Binary file not shown.
BIN
assets/models/AlienCake/cakeBirthday.glb
Normal file
BIN
assets/models/AlienCake/cakeBirthday.glb
Normal file
Binary file not shown.
BIN
assets/models/AlienCake/tile.glb
Normal file
BIN
assets/models/AlienCake/tile.glb
Normal file
Binary file not shown.
@ -130,6 +130,7 @@ Example | File | Description
|
||||
|
||||
Example | File | Description
|
||||
--- | --- | ---
|
||||
`alien_cake_addict` | [`game/alien_cake_addict.rs`](./game/alien_cake_addict.rs) | Eat the cakes. Eat them all. An example 3D game
|
||||
`breakout` | [`game/breakout.rs`](./game/breakout.rs) | An implementation of the classic game "Breakout"
|
||||
|
||||
## Input
|
||||
|
384
examples/game/alien_cake_addict.rs
Normal file
384
examples/game/alien_cake_addict.rs
Normal file
@ -0,0 +1,384 @@
|
||||
use bevy::{
|
||||
core::FixedTimestep,
|
||||
prelude::*,
|
||||
render::{camera::Camera, render_graph::base::camera::CAMERA_3D},
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
const STAGE: &str = "game";
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
enum GameState {
|
||||
Playing,
|
||||
GameOver,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::build()
|
||||
.add_resource(Msaa { samples: 4 })
|
||||
.init_resource::<Game>()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_resource(State::new(GameState::Playing))
|
||||
.add_startup_system(setup_cameras.system())
|
||||
.add_stage_after(stage::UPDATE, STAGE, StateStage::<GameState>::default())
|
||||
.on_state_enter(STAGE, GameState::Playing, setup.system())
|
||||
.on_state_update(STAGE, GameState::Playing, move_player.system())
|
||||
.on_state_update(STAGE, GameState::Playing, focus_camera.system())
|
||||
.on_state_update(STAGE, GameState::Playing, rotate_bonus.system())
|
||||
.on_state_update(STAGE, GameState::Playing, scoreboard_system.system())
|
||||
.on_state_exit(STAGE, GameState::Playing, teardown.system())
|
||||
.on_state_enter(STAGE, GameState::GameOver, display_score.system())
|
||||
.on_state_update(STAGE, GameState::GameOver, gameover_keyboard.system())
|
||||
.on_state_exit(STAGE, GameState::GameOver, teardown.system())
|
||||
.add_stage_after(
|
||||
stage::UPDATE,
|
||||
"bonus_update",
|
||||
SystemStage::parallel()
|
||||
.with_run_criteria(FixedTimestep::step(5.0))
|
||||
.with_system(spawn_bonus.system()),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
struct Cell {
|
||||
height: f32,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Player {
|
||||
entity: Option<Entity>,
|
||||
i: usize,
|
||||
j: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Bonus {
|
||||
entity: Option<Entity>,
|
||||
i: usize,
|
||||
j: usize,
|
||||
handle: Handle<Scene>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Game {
|
||||
board: Vec<Vec<Cell>>,
|
||||
player: Player,
|
||||
bonus: Bonus,
|
||||
score: i32,
|
||||
cake_eaten: u32,
|
||||
camera_should_focus: Vec3,
|
||||
camera_is_focus: Vec3,
|
||||
}
|
||||
|
||||
const BOARD_SIZE_I: usize = 14;
|
||||
const BOARD_SIZE_J: usize = 21;
|
||||
|
||||
const RESET_FOCUS: [f32; 3] = [
|
||||
BOARD_SIZE_I as f32 / 2.0,
|
||||
0.0,
|
||||
BOARD_SIZE_J as f32 / 2.0 - 0.5,
|
||||
];
|
||||
|
||||
fn setup_cameras(commands: &mut Commands, mut game: ResMut<Game>) {
|
||||
game.camera_should_focus = Vec3::from(RESET_FOCUS);
|
||||
game.camera_is_focus = game.camera_should_focus;
|
||||
commands
|
||||
.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(
|
||||
-(BOARD_SIZE_I as f32 / 2.0),
|
||||
2.0 * BOARD_SIZE_J as f32 / 3.0,
|
||||
BOARD_SIZE_J as f32 / 2.0 - 0.5,
|
||||
)
|
||||
.looking_at(game.camera_is_focus, Vec3::unit_y()),
|
||||
..Default::default()
|
||||
})
|
||||
.spawn(CameraUiBundle::default());
|
||||
}
|
||||
|
||||
fn setup(commands: &mut Commands, asset_server: Res<AssetServer>, mut game: ResMut<Game>) {
|
||||
// reset the game state
|
||||
game.cake_eaten = 0;
|
||||
game.score = 0;
|
||||
game.player.i = BOARD_SIZE_I / 2;
|
||||
game.player.j = BOARD_SIZE_J / 2;
|
||||
|
||||
commands.spawn(LightBundle {
|
||||
transform: Transform::from_xyz(4.0, 5.0, 4.0),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// spawn the game board
|
||||
let cell_scene = asset_server.load("models/AlienCake/tile.glb#Scene0");
|
||||
game.board = (0..BOARD_SIZE_J)
|
||||
.map(|j| {
|
||||
(0..BOARD_SIZE_I)
|
||||
.map(|i| {
|
||||
let height = rand::thread_rng().gen_range(-0.1..0.1);
|
||||
commands
|
||||
.spawn((
|
||||
Transform::from_xyz(i as f32, height - 0.2, j as f32),
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|cell| {
|
||||
cell.spawn_scene(cell_scene.clone());
|
||||
});
|
||||
Cell { height }
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// spawn the game character
|
||||
game.player.entity = commands
|
||||
.spawn((
|
||||
Transform {
|
||||
translation: Vec3::new(
|
||||
game.player.i as f32,
|
||||
game.board[game.player.j][game.player.i].height,
|
||||
game.player.j as f32,
|
||||
),
|
||||
rotation: Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2),
|
||||
..Default::default()
|
||||
},
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|cell| {
|
||||
cell.spawn_scene(asset_server.load("models/AlienCake/alien.glb#Scene0"));
|
||||
})
|
||||
.current_entity();
|
||||
|
||||
// load the scene for the cake
|
||||
game.bonus.handle = asset_server.load("models/AlienCake/cakeBirthday.glb#Scene0");
|
||||
|
||||
// scoreboard
|
||||
commands.spawn(TextBundle {
|
||||
text: Text {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
value: "Score:".to_string(),
|
||||
style: TextStyle {
|
||||
color: Color::rgb(0.5, 0.5, 1.0),
|
||||
font_size: 40.0,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
style: Style {
|
||||
position_type: PositionType::Absolute,
|
||||
position: Rect {
|
||||
top: Val::Px(5.0),
|
||||
left: Val::Px(5.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
// remove all entities that are not a camera
|
||||
fn teardown(commands: &mut Commands, entities: Query<Entity, Without<Camera>>) {
|
||||
for entity in entities.iter() {
|
||||
commands.despawn_recursive(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// control the game character
|
||||
fn move_player(
|
||||
commands: &mut Commands,
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut game: ResMut<Game>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
) {
|
||||
let mut moved = false;
|
||||
let mut rotation = 0.0;
|
||||
if keyboard_input.just_pressed(KeyCode::Up) {
|
||||
if game.player.i < BOARD_SIZE_I - 1 {
|
||||
game.player.i += 1;
|
||||
}
|
||||
rotation = -std::f32::consts::FRAC_PI_2;
|
||||
moved = true;
|
||||
}
|
||||
if keyboard_input.just_pressed(KeyCode::Down) {
|
||||
if game.player.i > 0 {
|
||||
game.player.i -= 1;
|
||||
}
|
||||
rotation = std::f32::consts::FRAC_PI_2;
|
||||
moved = true;
|
||||
}
|
||||
if keyboard_input.just_pressed(KeyCode::Right) {
|
||||
if game.player.j < BOARD_SIZE_J - 1 {
|
||||
game.player.j += 1;
|
||||
}
|
||||
rotation = std::f32::consts::PI;
|
||||
moved = true;
|
||||
}
|
||||
if keyboard_input.just_pressed(KeyCode::Left) {
|
||||
if game.player.j > 0 {
|
||||
game.player.j -= 1;
|
||||
}
|
||||
rotation = 0.0;
|
||||
moved = true;
|
||||
}
|
||||
|
||||
// move on the board
|
||||
if moved {
|
||||
*transforms.get_mut(game.player.entity.unwrap()).unwrap() = Transform {
|
||||
translation: Vec3::new(
|
||||
game.player.i as f32,
|
||||
game.board[game.player.j][game.player.i].height,
|
||||
game.player.j as f32,
|
||||
),
|
||||
rotation: Quat::from_rotation_y(rotation),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
// eat the cake!
|
||||
if let Some(entity) = game.bonus.entity {
|
||||
if game.player.i == game.bonus.i && game.player.j == game.bonus.j {
|
||||
game.score += 2;
|
||||
game.cake_eaten += 1;
|
||||
commands.despawn_recursive(entity);
|
||||
game.bonus.entity = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// change the focus of the camera
|
||||
fn focus_camera(
|
||||
time: Res<Time>,
|
||||
mut game: ResMut<Game>,
|
||||
mut transforms: QuerySet<(Query<(&mut Transform, &Camera)>, Query<&Transform>)>,
|
||||
) {
|
||||
const SPEED: f32 = 2.0;
|
||||
// if there is both a player and a bonus, target the mid-point of them
|
||||
if let (Some(player_entity), Some(bonus_entity)) = (game.player.entity, game.bonus.entity) {
|
||||
if let (Ok(player_transform), Ok(bonus_transform)) = (
|
||||
transforms.q1().get(player_entity),
|
||||
transforms.q1().get(bonus_entity),
|
||||
) {
|
||||
game.camera_should_focus = player_transform
|
||||
.translation
|
||||
.lerp(bonus_transform.translation, 0.5);
|
||||
}
|
||||
// otherwise, if there is only a player, target the player
|
||||
} else if let Some(player_entity) = game.player.entity {
|
||||
if let Ok(player_transform) = transforms.q1().get(player_entity) {
|
||||
game.camera_should_focus = player_transform.translation;
|
||||
}
|
||||
// otherwise, target the middle
|
||||
} else {
|
||||
game.camera_should_focus = Vec3::from(RESET_FOCUS);
|
||||
}
|
||||
// calculate the camera motion based on the difference between where the camera is looking
|
||||
// and where it should be looking; the greater the distance, the faster the motion;
|
||||
// smooth out the camera movement using the frame time
|
||||
let mut camera_motion = game.camera_should_focus - game.camera_is_focus;
|
||||
if camera_motion.length() > 0.2 {
|
||||
camera_motion *= SPEED * time.delta_seconds();
|
||||
// set the new camera's actual focus
|
||||
game.camera_is_focus += camera_motion;
|
||||
}
|
||||
// look at that new camera's actual focus
|
||||
for (mut transform, camera) in transforms.q0_mut().iter_mut() {
|
||||
if camera.name == Some(CAMERA_3D.to_string()) {
|
||||
*transform = transform.looking_at(game.camera_is_focus, Vec3::unit_y());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// despawn the bonus if there is one, then spawn a new one at a random location
|
||||
fn spawn_bonus(
|
||||
mut state: ResMut<State<GameState>>,
|
||||
commands: &mut Commands,
|
||||
mut game: ResMut<Game>,
|
||||
) {
|
||||
if *state.current() != GameState::Playing {
|
||||
return;
|
||||
}
|
||||
if let Some(entity) = game.bonus.entity {
|
||||
game.score -= 3;
|
||||
commands.despawn_recursive(entity);
|
||||
game.bonus.entity = None;
|
||||
if game.score <= -5 {
|
||||
state.set_next(GameState::GameOver).unwrap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
game.bonus.i = rand::thread_rng().gen_range(0..BOARD_SIZE_I);
|
||||
game.bonus.j = rand::thread_rng().gen_range(0..BOARD_SIZE_J);
|
||||
game.bonus.entity = commands
|
||||
.spawn((
|
||||
Transform {
|
||||
translation: Vec3::new(
|
||||
game.bonus.i as f32,
|
||||
game.board[game.player.j][game.player.i].height + 0.2,
|
||||
game.bonus.j as f32,
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
GlobalTransform::default(),
|
||||
))
|
||||
.with_children(|cell| {
|
||||
cell.spawn_scene(game.bonus.handle.clone());
|
||||
})
|
||||
.current_entity();
|
||||
}
|
||||
|
||||
// let the cake turn on itself
|
||||
fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Transform>) {
|
||||
if let Some(entity) = game.bonus.entity {
|
||||
let mut cake_transform = transforms.get_mut(entity).unwrap();
|
||||
cake_transform.rotate(Quat::from_rotation_y(time.delta_seconds()));
|
||||
cake_transform.scale = Vec3::splat(
|
||||
1.0 + (game.score as f32 / 10.0 * time.seconds_since_startup().sin() as f32).abs(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// update the score displayed during the game
|
||||
fn scoreboard_system(game: Res<Game>, mut query: Query<&mut Text>) {
|
||||
for mut text in query.iter_mut() {
|
||||
text.value = format!("Sugar Rush: {}", game.score);
|
||||
}
|
||||
}
|
||||
|
||||
// restart the game when pressing spacebar
|
||||
fn gameover_keyboard(mut state: ResMut<State<GameState>>, keyboard_input: Res<Input<KeyCode>>) {
|
||||
if keyboard_input.just_pressed(KeyCode::Space) {
|
||||
state.set_next(GameState::Playing).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// display the number of cake eaten before losing
|
||||
fn display_score(
|
||||
commands: &mut Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
game: Res<Game>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
) {
|
||||
commands
|
||||
.spawn(NodeBundle {
|
||||
style: Style {
|
||||
margin: Rect::all(Val::Auto),
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
..Default::default()
|
||||
},
|
||||
material: materials.add(Color::NONE.into()),
|
||||
..Default::default()
|
||||
})
|
||||
.with_children(|parent| {
|
||||
parent.spawn(TextBundle {
|
||||
text: Text {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
value: format!("Cake eaten: {}", game.cake_eaten),
|
||||
style: TextStyle {
|
||||
color: Color::rgb(0.5, 0.5, 1.0),
|
||||
font_size: 80.0,
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user