Skip to content

Commit eb96afe

Browse files
authored
Merge pull request #3 from pnstack/copilot/create-2d-platformer-game
Implement 2D Mario-style platformer with gravity, jumping, and collision detection
2 parents 8e98e54 + eca64f5 commit eb96afe

8 files changed

Lines changed: 396 additions & 56 deletions

File tree

examples/basic_usage.rs

Lines changed: 138 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
//! Basic usage example for the Bevy game template
22
//!
33
//! This example demonstrates how to use the template's
4-
//! components, resources, and systems.
4+
//! components, resources, and systems for a 2D platformer.
55
66
use bevy::prelude::*;
7-
use template_bevy::components::{Health, Player, Speed, Velocity};
7+
use template_bevy::components::{
8+
BoxCollider, Gravity, Grounded, Health, JumpConfig, Platform, Player, Speed, Velocity,
9+
};
810
use template_bevy::resources::{GameSettings, Score};
911
use template_bevy::states::GameState;
1012

13+
/// Threshold for detecting landing on platforms (in pixels)
14+
const LANDING_THRESHOLD: f32 = 10.0;
15+
1116
fn main() {
1217
App::new()
1318
.add_plugins(DefaultPlugins)
@@ -17,7 +22,16 @@ fn main() {
1722
.add_systems(Startup, setup)
1823
.add_systems(
1924
Update,
20-
(update_player, print_score).run_if(in_state(GameState::Playing)),
25+
(
26+
player_horizontal_movement,
27+
player_jump,
28+
apply_gravity,
29+
apply_velocity,
30+
check_collisions,
31+
print_score,
32+
)
33+
.chain()
34+
.run_if(in_state(GameState::Playing)),
2135
)
2236
.add_systems(OnEnter(GameState::Loading), start_game)
2337
.run();
@@ -31,55 +45,147 @@ fn setup(mut commands: Commands) {
3145
// Spawn camera
3246
commands.spawn(Camera2d);
3347

34-
// Spawn player with components
48+
// Spawn player with platformer components
49+
let player_size = Vec2::new(40.0, 50.0);
3550
commands.spawn((
3651
Player,
37-
Speed(300.0),
52+
Speed(250.0),
3853
Health::new(100.0),
3954
Velocity::default(),
55+
Gravity::default(),
56+
Grounded(false),
57+
JumpConfig::default(),
58+
BoxCollider::new(player_size.x, player_size.y),
4059
Sprite {
41-
color: Color::srgb(0.25, 0.75, 0.25),
42-
custom_size: Some(Vec2::new(50.0, 50.0)),
60+
color: Color::srgb(0.2, 0.6, 1.0),
61+
custom_size: Some(player_size),
4362
..default()
4463
},
45-
Transform::default(),
64+
Transform::from_xyz(0.0, 100.0, 0.0),
4665
));
4766

48-
println!("Game initialized!");
49-
println!("Use WASD or Arrow keys to move");
50-
println!("Press ESC to quit");
67+
// Spawn ground
68+
let ground_size = Vec2::new(800.0, 40.0);
69+
commands.spawn((
70+
Platform,
71+
BoxCollider::new(ground_size.x, ground_size.y),
72+
Sprite {
73+
color: Color::srgb(0.4, 0.3, 0.2),
74+
custom_size: Some(ground_size),
75+
..default()
76+
},
77+
Transform::from_xyz(0.0, -250.0, 0.0),
78+
));
79+
80+
// Spawn floating platforms
81+
for (x, y, width) in [
82+
(-200.0, -100.0, 150.0),
83+
(150.0, 0.0, 120.0),
84+
(-50.0, 100.0, 180.0),
85+
] {
86+
let size = Vec2::new(width, 20.0);
87+
commands.spawn((
88+
Platform,
89+
BoxCollider::new(size.x, size.y),
90+
Sprite {
91+
color: Color::srgb(0.3, 0.5, 0.3),
92+
custom_size: Some(size),
93+
..default()
94+
},
95+
Transform::from_xyz(x, y, 0.0),
96+
));
97+
}
98+
99+
println!("2D Platformer initialized!");
100+
println!("Use A/D or Arrow keys to move left/right");
101+
println!("Press SPACE to jump (only when on ground)");
51102
}
52103

53-
fn update_player(
104+
fn player_horizontal_movement(
54105
keyboard_input: Res<ButtonInput<KeyCode>>,
55-
time: Res<Time>,
56-
mut query: Query<(&Speed, &mut Transform), With<Player>>,
57-
mut score: ResMut<Score>,
106+
mut query: Query<(&Speed, &mut Velocity), With<Player>>,
58107
) {
59-
for (speed, mut transform) in query.iter_mut() {
60-
let mut direction = Vec2::ZERO;
61-
62-
if keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp) {
63-
direction.y += 1.0;
64-
}
65-
if keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown) {
66-
direction.y -= 1.0;
67-
}
108+
for (speed, mut velocity) in query.iter_mut() {
109+
let mut direction = 0.0;
68110
if keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft) {
69-
direction.x -= 1.0;
111+
direction -= 1.0;
70112
}
71113
if keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight) {
72-
direction.x += 1.0;
114+
direction += 1.0;
73115
}
116+
velocity.0.x = direction * speed.0;
117+
}
118+
}
119+
120+
fn player_jump(
121+
keyboard_input: Res<ButtonInput<KeyCode>>,
122+
mut query: Query<(&mut Velocity, &Grounded, &JumpConfig), With<Player>>,
123+
) {
124+
for (mut velocity, grounded, jump_config) in query.iter_mut() {
125+
if keyboard_input.just_pressed(KeyCode::Space) && grounded.0 {
126+
velocity.0.y = jump_config.jump_velocity;
127+
}
128+
if keyboard_input.just_released(KeyCode::Space) && velocity.0.y > 0.0 {
129+
velocity.0.y *= jump_config.jump_cut_multiplier;
130+
}
131+
}
132+
}
133+
134+
fn apply_gravity(time: Res<Time>, mut query: Query<(&Gravity, &mut Velocity, &Grounded)>) {
135+
for (gravity, mut velocity, grounded) in query.iter_mut() {
136+
if !grounded.0 {
137+
velocity.0.y -= gravity.0 * time.delta_seconds();
138+
}
139+
}
140+
}
141+
142+
fn apply_velocity(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
143+
for (velocity, mut transform) in query.iter_mut() {
144+
transform.translation.x += velocity.0.x * time.delta_seconds();
145+
transform.translation.y += velocity.0.y * time.delta_seconds();
146+
}
147+
}
148+
149+
#[allow(clippy::type_complexity)]
150+
fn check_collisions(
151+
mut player_query: Query<
152+
(&mut Transform, &mut Velocity, &BoxCollider, &mut Grounded),
153+
With<Player>,
154+
>,
155+
platform_query: Query<(&Transform, &BoxCollider), (With<Platform>, Without<Player>)>,
156+
) {
157+
for (mut player_tf, mut velocity, player_col, mut grounded) in player_query.iter_mut() {
158+
let mut is_grounded = false;
159+
let ph_w = player_col.width / 2.0;
160+
let ph_h = player_col.height / 2.0;
161+
162+
for (platform_tf, platform_col) in platform_query.iter() {
163+
let plat_h_w = platform_col.width / 2.0;
164+
let plat_h_h = platform_col.height / 2.0;
165+
166+
let p_left = player_tf.translation.x - ph_w;
167+
let p_right = player_tf.translation.x + ph_w;
168+
let p_bottom = player_tf.translation.y - ph_h;
169+
let p_top = player_tf.translation.y + ph_h;
170+
171+
let plat_left = platform_tf.translation.x - plat_h_w;
172+
let plat_right = platform_tf.translation.x + plat_h_w;
173+
let plat_top = platform_tf.translation.y + plat_h_h;
74174

75-
let movement = direction.normalize_or_zero() * speed.0 * time.delta_seconds();
76-
transform.translation.x += movement.x;
77-
transform.translation.y += movement.y;
175+
let h_overlap = p_left < plat_right && p_right > plat_left;
78176

79-
// Add score for moving
80-
if direction != Vec2::ZERO {
81-
score.add(1);
177+
if h_overlap && velocity.0.y <= 0.0 {
178+
if p_bottom <= plat_top
179+
&& p_bottom >= plat_top - LANDING_THRESHOLD
180+
&& p_top > plat_top
181+
{
182+
player_tf.translation.y = plat_top + ph_h;
183+
velocity.0.y = 0.0;
184+
is_grounded = true;
185+
}
186+
}
82187
}
188+
grounded.0 = is_grounded;
83189
}
84190
}
85191

src/components/mod.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,62 @@ impl Collider {
7474
}
7575
}
7676

77+
/// Gravity component for entities affected by gravity
78+
#[derive(Component, Debug, Clone)]
79+
pub struct Gravity(pub f32);
80+
81+
impl Default for Gravity {
82+
fn default() -> Self {
83+
Self(980.0) // Pixels per second squared
84+
}
85+
}
86+
87+
/// Component tracking if entity is on the ground
88+
#[derive(Component, Debug, Default, Clone)]
89+
pub struct Grounded(pub bool);
90+
91+
/// Marker component for static platform entities
92+
#[derive(Component, Debug, Default)]
93+
pub struct Platform;
94+
95+
/// Box collider for AABB collision detection
96+
#[derive(Component, Debug, Clone)]
97+
pub struct BoxCollider {
98+
pub width: f32,
99+
pub height: f32,
100+
}
101+
102+
impl BoxCollider {
103+
pub fn new(width: f32, height: f32) -> Self {
104+
Self { width, height }
105+
}
106+
}
107+
108+
impl Default for BoxCollider {
109+
fn default() -> Self {
110+
Self {
111+
width: 50.0,
112+
height: 50.0,
113+
}
114+
}
115+
}
116+
117+
/// Jump configuration component for Mario-like jumping
118+
#[derive(Component, Debug, Clone)]
119+
pub struct JumpConfig {
120+
pub jump_velocity: f32,
121+
pub jump_cut_multiplier: f32,
122+
}
123+
124+
impl Default for JumpConfig {
125+
fn default() -> Self {
126+
Self {
127+
jump_velocity: 450.0,
128+
jump_cut_multiplier: 0.5,
129+
}
130+
}
131+
}
132+
77133
#[cfg(test)]
78134
mod tests {
79135
use super::*;
@@ -122,4 +178,37 @@ mod tests {
122178
health.take_damage(25.0);
123179
assert!((health.percentage() - 0.75).abs() < f32::EPSILON);
124180
}
181+
182+
#[test]
183+
fn test_gravity_default() {
184+
let gravity = Gravity::default();
185+
assert_eq!(gravity.0, 980.0);
186+
}
187+
188+
#[test]
189+
fn test_grounded_default() {
190+
let grounded = Grounded::default();
191+
assert!(!grounded.0);
192+
}
193+
194+
#[test]
195+
fn test_box_collider_new() {
196+
let collider = BoxCollider::new(100.0, 50.0);
197+
assert_eq!(collider.width, 100.0);
198+
assert_eq!(collider.height, 50.0);
199+
}
200+
201+
#[test]
202+
fn test_box_collider_default() {
203+
let collider = BoxCollider::default();
204+
assert_eq!(collider.width, 50.0);
205+
assert_eq!(collider.height, 50.0);
206+
}
207+
208+
#[test]
209+
fn test_jump_config_default() {
210+
let config = JumpConfig::default();
211+
assert_eq!(config.jump_velocity, 450.0);
212+
assert_eq!(config.jump_cut_multiplier, 0.5);
213+
}
125214
}

src/game/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ pub mod constants {
1010
/// Default window height
1111
pub const WINDOW_HEIGHT: f32 = 720.0;
1212
/// Game title
13-
pub const GAME_TITLE: &str = "Template Bevy Game";
13+
pub const GAME_TITLE: &str = "2D Mario-Style Platformer";
1414
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ fn main() {
55
App::new()
66
.add_plugins(DefaultPlugins.set(WindowPlugin {
77
primary_window: Some(Window {
8-
title: "Template Bevy Game".to_string(),
8+
title: "2D Mario-Style Platformer".to_string(),
99
resolution: (1280.0, 720.0).into(),
1010
..default()
1111
}),

src/plugins/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use bevy::prelude::*;
77

88
use crate::resources::{GameSettings, GameTimer, Score};
99
use crate::states::GameState;
10-
use crate::systems::{apply_velocity, player_movement, setup_camera, spawn_player};
10+
use crate::systems::{
11+
apply_gravity, apply_velocity, check_platform_collisions, player_jump, player_movement,
12+
setup_camera, spawn_platforms, spawn_player,
13+
};
1114

1215
/// Main game plugin that sets up all game systems
1316
pub struct GamePlugin;
@@ -22,11 +25,18 @@ impl Plugin for GamePlugin {
2225
.init_resource::<Score>()
2326
.init_resource::<GameTimer>()
2427
// Setup systems (run once on startup)
25-
.add_systems(Startup, (setup_camera, spawn_player))
28+
.add_systems(Startup, (setup_camera, spawn_player, spawn_platforms))
2629
// Update systems (run every frame during Playing state)
30+
// Order: input -> physics -> collision -> movement
2731
.add_systems(
2832
Update,
29-
(player_movement, apply_velocity)
33+
(
34+
player_movement,
35+
player_jump,
36+
apply_gravity,
37+
apply_velocity,
38+
check_platform_collisions,
39+
)
3040
.chain()
3141
.run_if(in_state(GameState::Playing)),
3242
)

src/systems/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
mod movement;
77
mod setup;
88

9-
pub use movement::*;
10-
pub use setup::*;
9+
// Re-export specific systems for clarity
10+
pub use movement::{
11+
apply_gravity, apply_velocity, check_platform_collisions, player_jump, player_movement,
12+
};
13+
pub use setup::{setup_camera, spawn_platforms, spawn_player};

0 commit comments

Comments
 (0)