Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/games/alien_cake_addict.rs
9367 views
1
//! Eat the cakes. Eat them all. An example 3D game.
2
3
use std::f32::consts::PI;
4
5
use bevy::prelude::*;
6
7
use rand::{Rng, SeedableRng};
8
use rand_chacha::ChaCha8Rng;
9
10
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)]
11
enum GameState {
12
#[default]
13
Playing,
14
GameOver,
15
}
16
17
#[derive(Resource)]
18
struct BonusSpawnTimer(Timer);
19
20
fn main() {
21
App::new()
22
.add_plugins(DefaultPlugins)
23
.init_resource::<Game>()
24
.insert_resource(BonusSpawnTimer(Timer::from_seconds(
25
5.0,
26
TimerMode::Repeating,
27
)))
28
.init_state::<GameState>()
29
.add_systems(Startup, setup_cameras)
30
.add_systems(OnEnter(GameState::Playing), setup)
31
.add_systems(
32
Update,
33
(
34
move_player,
35
focus_camera,
36
rotate_bonus,
37
scoreboard_system,
38
spawn_bonus,
39
)
40
.run_if(in_state(GameState::Playing)),
41
)
42
.add_systems(OnEnter(GameState::GameOver), display_score)
43
.add_systems(
44
Update,
45
game_over_keyboard.run_if(in_state(GameState::GameOver)),
46
)
47
.run();
48
}
49
50
struct Cell {
51
height: f32,
52
}
53
54
#[derive(Default)]
55
struct Player {
56
entity: Option<Entity>,
57
i: usize,
58
j: usize,
59
move_cooldown: Timer,
60
}
61
62
#[derive(Default)]
63
struct Bonus {
64
entity: Option<Entity>,
65
i: usize,
66
j: usize,
67
handle: Handle<Scene>,
68
}
69
70
#[derive(Resource, Default)]
71
struct Game {
72
board: Vec<Vec<Cell>>,
73
player: Player,
74
bonus: Bonus,
75
score: i32,
76
cake_eaten: u32,
77
camera_should_focus: Vec3,
78
camera_is_focus: Vec3,
79
}
80
81
#[derive(Resource, Deref, DerefMut)]
82
struct Random(ChaCha8Rng);
83
84
const BOARD_SIZE_I: usize = 14;
85
const BOARD_SIZE_J: usize = 21;
86
87
const RESET_FOCUS: [f32; 3] = [
88
BOARD_SIZE_I as f32 / 2.0,
89
0.0,
90
BOARD_SIZE_J as f32 / 2.0 - 0.5,
91
];
92
93
fn setup_cameras(mut commands: Commands, mut game: ResMut<Game>) {
94
game.camera_should_focus = Vec3::from(RESET_FOCUS);
95
game.camera_is_focus = game.camera_should_focus;
96
commands.spawn((
97
Camera3d::default(),
98
Transform::from_xyz(
99
-(BOARD_SIZE_I as f32 / 2.0),
100
2.0 * BOARD_SIZE_J as f32 / 3.0,
101
BOARD_SIZE_J as f32 / 2.0 - 0.5,
102
)
103
.looking_at(game.camera_is_focus, Vec3::Y),
104
));
105
}
106
107
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMut<Game>) {
108
let mut rng = if std::env::var("GITHUB_ACTIONS") == Ok("true".to_string()) {
109
// We're seeding the PRNG here to make this example deterministic for testing purposes.
110
// This isn't strictly required in practical use unless you need your app to be deterministic.
111
ChaCha8Rng::seed_from_u64(19878367467713)
112
} else {
113
ChaCha8Rng::from_os_rng()
114
};
115
116
// reset the game state
117
game.cake_eaten = 0;
118
game.score = 0;
119
game.player.i = BOARD_SIZE_I / 2;
120
game.player.j = BOARD_SIZE_J / 2;
121
game.player.move_cooldown = Timer::from_seconds(0.3, TimerMode::Once);
122
123
commands.spawn((
124
DespawnOnExit(GameState::Playing),
125
PointLight {
126
intensity: 2_000_000.0,
127
shadow_maps_enabled: true,
128
range: 30.0,
129
..default()
130
},
131
Transform::from_xyz(4.0, 10.0, 4.0),
132
));
133
134
// spawn the game board
135
let cell_scene =
136
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/tile.glb"));
137
game.board = (0..BOARD_SIZE_J)
138
.map(|j| {
139
(0..BOARD_SIZE_I)
140
.map(|i| {
141
let height = rng.random_range(-0.1..0.1);
142
commands.spawn((
143
DespawnOnExit(GameState::Playing),
144
Transform::from_xyz(i as f32, height - 0.2, j as f32),
145
SceneRoot(cell_scene.clone()),
146
));
147
Cell { height }
148
})
149
.collect()
150
})
151
.collect();
152
153
// spawn the game character
154
game.player.entity = Some(
155
commands
156
.spawn((
157
DespawnOnExit(GameState::Playing),
158
Transform {
159
translation: Vec3::new(
160
game.player.i as f32,
161
game.board[game.player.j][game.player.i].height,
162
game.player.j as f32,
163
),
164
rotation: Quat::from_rotation_y(-PI / 2.),
165
..default()
166
},
167
SceneRoot(
168
asset_server
169
.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/alien.glb")),
170
),
171
))
172
.id(),
173
);
174
175
// load the scene for the cake
176
game.bonus.handle =
177
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/AlienCake/cakeBirthday.glb"));
178
179
// scoreboard
180
commands.spawn((
181
DespawnOnExit(GameState::Playing),
182
Text::new("Score:"),
183
TextFont {
184
font_size: FontSize::Px(33.0),
185
..default()
186
},
187
TextColor(Color::srgb(0.5, 0.5, 1.0)),
188
Node {
189
position_type: PositionType::Absolute,
190
top: px(5),
191
left: px(5),
192
..default()
193
},
194
));
195
196
commands.insert_resource(Random(rng));
197
}
198
199
// control the game character
200
fn move_player(
201
mut commands: Commands,
202
keyboard_input: Res<ButtonInput<KeyCode>>,
203
mut game: ResMut<Game>,
204
mut transforms: Query<&mut Transform>,
205
time: Res<Time>,
206
) {
207
if game.player.move_cooldown.tick(time.delta()).is_finished() {
208
let mut moved = false;
209
let mut rotation = 0.0;
210
211
if keyboard_input.pressed(KeyCode::ArrowUp) {
212
if game.player.i < BOARD_SIZE_I - 1 {
213
game.player.i += 1;
214
}
215
rotation = -PI / 2.;
216
moved = true;
217
}
218
if keyboard_input.pressed(KeyCode::ArrowDown) {
219
if game.player.i > 0 {
220
game.player.i -= 1;
221
}
222
rotation = PI / 2.;
223
moved = true;
224
}
225
if keyboard_input.pressed(KeyCode::ArrowRight) {
226
if game.player.j < BOARD_SIZE_J - 1 {
227
game.player.j += 1;
228
}
229
rotation = PI;
230
moved = true;
231
}
232
if keyboard_input.pressed(KeyCode::ArrowLeft) {
233
if game.player.j > 0 {
234
game.player.j -= 1;
235
}
236
rotation = 0.0;
237
moved = true;
238
}
239
240
// move on the board
241
if moved {
242
game.player.move_cooldown.reset();
243
*transforms.get_mut(game.player.entity.unwrap()).unwrap() = Transform {
244
translation: Vec3::new(
245
game.player.i as f32,
246
game.board[game.player.j][game.player.i].height,
247
game.player.j as f32,
248
),
249
rotation: Quat::from_rotation_y(rotation),
250
..default()
251
};
252
}
253
}
254
255
// eat the cake!
256
if let Some(entity) = game.bonus.entity
257
&& game.player.i == game.bonus.i
258
&& game.player.j == game.bonus.j
259
{
260
game.score += 2;
261
game.cake_eaten += 1;
262
commands.entity(entity).despawn();
263
game.bonus.entity = None;
264
}
265
}
266
267
// change the focus of the camera
268
fn focus_camera(
269
time: Res<Time>,
270
mut game: ResMut<Game>,
271
mut transforms: ParamSet<(Query<&mut Transform, With<Camera3d>>, Query<&Transform>)>,
272
) {
273
const SPEED: f32 = 2.0;
274
// if there is both a player and a bonus, target the mid-point of them
275
if let (Some(player_entity), Some(bonus_entity)) = (game.player.entity, game.bonus.entity) {
276
let transform_query = transforms.p1();
277
if let (Ok(player_transform), Ok(bonus_transform)) = (
278
transform_query.get(player_entity),
279
transform_query.get(bonus_entity),
280
) {
281
game.camera_should_focus = player_transform
282
.translation
283
.lerp(bonus_transform.translation, 0.5);
284
}
285
// otherwise, if there is only a player, target the player
286
} else if let Some(player_entity) = game.player.entity {
287
if let Ok(player_transform) = transforms.p1().get(player_entity) {
288
game.camera_should_focus = player_transform.translation;
289
}
290
// otherwise, target the middle
291
} else {
292
game.camera_should_focus = Vec3::from(RESET_FOCUS);
293
}
294
// calculate the camera motion based on the difference between where the camera is looking
295
// and where it should be looking; the greater the distance, the faster the motion;
296
// smooth out the camera movement using the frame time
297
let mut camera_motion = game.camera_should_focus - game.camera_is_focus;
298
if camera_motion.length() > 0.2 {
299
camera_motion *= SPEED * time.delta_secs();
300
// set the new camera's actual focus
301
game.camera_is_focus += camera_motion;
302
}
303
// look at that new camera's actual focus
304
for mut transform in transforms.p0().iter_mut() {
305
*transform = transform.looking_at(game.camera_is_focus, Vec3::Y);
306
}
307
}
308
309
// despawn the bonus if there is one, then spawn a new one at a random location
310
fn spawn_bonus(
311
time: Res<Time>,
312
mut timer: ResMut<BonusSpawnTimer>,
313
mut next_state: ResMut<NextState<GameState>>,
314
mut commands: Commands,
315
mut game: ResMut<Game>,
316
mut rng: ResMut<Random>,
317
) {
318
// make sure we wait enough time before spawning the next cake
319
if !timer.0.tick(time.delta()).is_finished() {
320
return;
321
}
322
323
if let Some(entity) = game.bonus.entity {
324
game.score -= 3;
325
commands.entity(entity).despawn();
326
game.bonus.entity = None;
327
if game.score <= -5 {
328
next_state.set(GameState::GameOver);
329
return;
330
}
331
}
332
333
// ensure bonus doesn't spawn on the player
334
loop {
335
game.bonus.i = rng.random_range(0..BOARD_SIZE_I);
336
game.bonus.j = rng.random_range(0..BOARD_SIZE_J);
337
if game.bonus.i != game.player.i || game.bonus.j != game.player.j {
338
break;
339
}
340
}
341
game.bonus.entity = Some(
342
commands
343
.spawn((
344
DespawnOnExit(GameState::Playing),
345
Transform::from_xyz(
346
game.bonus.i as f32,
347
game.board[game.bonus.j][game.bonus.i].height + 0.2,
348
game.bonus.j as f32,
349
),
350
SceneRoot(game.bonus.handle.clone()),
351
children![(
352
PointLight {
353
color: Color::srgb(1.0, 1.0, 0.0),
354
intensity: 500_000.0,
355
range: 10.0,
356
..default()
357
},
358
Transform::from_xyz(0.0, 2.0, 0.0),
359
)],
360
))
361
.id(),
362
);
363
}
364
365
// let the cake turn on itself
366
fn rotate_bonus(game: Res<Game>, time: Res<Time>, mut transforms: Query<&mut Transform>) {
367
if let Some(entity) = game.bonus.entity
368
&& let Ok(mut cake_transform) = transforms.get_mut(entity)
369
{
370
cake_transform.rotate_y(time.delta_secs());
371
cake_transform.scale =
372
Vec3::splat(1.0 + (game.score as f32 / 10.0 * ops::sin(time.elapsed_secs())).abs());
373
}
374
}
375
376
// update the score displayed during the game
377
fn scoreboard_system(game: Res<Game>, mut display: Single<&mut Text>) {
378
display.0 = format!("Sugar Rush: {}", game.score);
379
}
380
381
// restart the game when pressing spacebar
382
fn game_over_keyboard(
383
mut next_state: ResMut<NextState<GameState>>,
384
keyboard_input: Res<ButtonInput<KeyCode>>,
385
) {
386
if keyboard_input.just_pressed(KeyCode::Space) {
387
next_state.set(GameState::Playing);
388
}
389
}
390
391
// display the number of cake eaten before losing
392
fn display_score(mut commands: Commands, game: Res<Game>) {
393
commands.spawn((
394
DespawnOnExit(GameState::GameOver),
395
Node {
396
width: percent(100),
397
align_items: AlignItems::Center,
398
justify_content: JustifyContent::Center,
399
..default()
400
},
401
children![(
402
Text::new(format!("Cake eaten: {}", game.cake_eaten)),
403
TextFont {
404
font_size: FontSize::Px(67.0),
405
..default()
406
},
407
TextColor(Color::srgb(0.5, 0.5, 1.0)),
408
)],
409
));
410
}
411
412