Skip to content

Commit 12feb1f

Browse files
authored
Merge pull request #4 from pnstack/copilot/design-game-template-bevy
Add core game template features: auto-movement, collision, obstacles, camera follow, and UI
2 parents 031e2f4 + aa569cd commit 12feb1f

13 files changed

Lines changed: 689 additions & 11 deletions

File tree

README.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ A Bevy game engine template designed for indie game developers. This template pr
99
- 🧩 Modular plugin system
1010
- 🎯 Game state management (Loading, Menu, Playing, Paused, GameOver)
1111
- 🏃 Movement and physics components
12+
- 🤖 Auto-movement system for obstacles and NPCs
13+
- 💥 AABB collision detection system
14+
- 🎯 Dynamic obstacle spawning with randomization
15+
- 📷 Smooth camera follow system
16+
- 🎨 User interface with score display and health bar
1217
- ❤️ Health and combat systems
1318
- 🎵 Audio and settings resources
1419
- 🧪 Comprehensive test suite
@@ -53,7 +58,8 @@ cargo run --release
5358

5459
### Controls
5560

56-
- **WASD** or **Arrow Keys** - Move player
61+
- **A/D** or **Arrow Left/Right** - Move player horizontally
62+
- **Spacebar** - Jump (only when on ground)
5763
- **ESC** - Quit game
5864

5965
## Project Structure
@@ -132,6 +138,60 @@ use template_bevy::states::GameState;
132138
app.add_systems(Update, gameplay_system.run_if(in_state(GameState::Playing)));
133139
```
134140

141+
## Core Systems
142+
143+
The template includes several ready-to-use systems:
144+
145+
### Auto-Movement
146+
147+
Entities with the `AutoMove` component will automatically move in the specified direction:
148+
149+
```rust
150+
use template_bevy::components::{AutoMove, Obstacle};
151+
152+
// Create an obstacle that moves left
153+
commands.spawn((
154+
Obstacle,
155+
AutoMove::left(150.0), // Speed of 150 pixels/second
156+
// ... other components
157+
));
158+
```
159+
160+
### Obstacle Spawning
161+
162+
The `spawn_obstacles` system automatically spawns obstacles at regular intervals with random properties (position, size, speed).
163+
164+
### Camera Follow
165+
166+
The camera automatically follows the player with smooth interpolation:
167+
168+
```rust
169+
use template_bevy::components::{CameraFollow, MainCamera};
170+
171+
// Camera will smoothly follow the target
172+
commands.spawn((
173+
MainCamera,
174+
CameraFollow::new(player_entity)
175+
.with_offset(Vec3::new(0.0, 50.0, 0.0))
176+
.with_smoothing(0.05),
177+
Camera2dBundle::default(),
178+
));
179+
```
180+
181+
### Collision Detection
182+
183+
AABB collision detection between player and obstacles:
184+
185+
- Platform collisions for landing and ground detection
186+
- Obstacle collisions that apply damage to the player
187+
188+
### User Interface
189+
190+
Built-in UI components for displaying game information:
191+
192+
- **Score Display**: Shows current score in the top-left corner
193+
- **Health Bar**: Visual health indicator with color changes based on health level
194+
135195
## Development
136196

137197
### Prerequisites

src/components/mod.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use bevy::prelude::*;
77
use serde::{Deserialize, Serialize};
88

9+
use crate::game::constants::camera::{MAX_SMOOTHING, MIN_SMOOTHING};
10+
911
/// Marker component for the player entity
1012
#[derive(Component, Debug, Default)]
1113
pub struct Player;
@@ -130,6 +132,125 @@ impl Default for JumpConfig {
130132
}
131133
}
132134

135+
/// Marker component for obstacle entities
136+
#[derive(Component, Debug, Default)]
137+
pub struct Obstacle;
138+
139+
/// Auto-movement component for entities that move automatically
140+
#[derive(Component, Debug, Clone)]
141+
pub struct AutoMove {
142+
/// Direction of movement (normalized)
143+
pub direction: Vec2,
144+
/// Movement speed
145+
pub speed: f32,
146+
}
147+
148+
impl Default for AutoMove {
149+
fn default() -> Self {
150+
Self {
151+
direction: Vec2::new(-1.0, 0.0), // Move left by default
152+
speed: 150.0,
153+
}
154+
}
155+
}
156+
157+
impl AutoMove {
158+
pub fn new(direction: Vec2, speed: f32) -> Self {
159+
Self {
160+
direction: direction.normalize_or_zero(),
161+
speed,
162+
}
163+
}
164+
165+
/// Creates a left-moving auto-move component
166+
pub fn left(speed: f32) -> Self {
167+
Self::new(Vec2::new(-1.0, 0.0), speed)
168+
}
169+
170+
/// Creates a right-moving auto-move component
171+
pub fn right(speed: f32) -> Self {
172+
Self::new(Vec2::new(1.0, 0.0), speed)
173+
}
174+
}
175+
176+
/// Marker component for the main camera that follows the player
177+
#[derive(Component, Debug, Default)]
178+
pub struct MainCamera;
179+
180+
/// Camera follow configuration component
181+
#[derive(Component, Debug, Clone)]
182+
pub struct CameraFollow {
183+
/// Target entity to follow (usually the player)
184+
pub target: Option<Entity>,
185+
/// Offset from the target position
186+
pub offset: Vec3,
187+
/// Smoothing factor (0.0 = instant, 1.0 = very smooth/slow)
188+
pub smoothing: f32,
189+
}
190+
191+
impl Default for CameraFollow {
192+
fn default() -> Self {
193+
Self {
194+
target: None,
195+
offset: Vec3::ZERO,
196+
smoothing: 0.1,
197+
}
198+
}
199+
}
200+
201+
impl CameraFollow {
202+
pub fn new(target: Entity) -> Self {
203+
Self {
204+
target: Some(target),
205+
..Default::default()
206+
}
207+
}
208+
209+
pub fn with_offset(mut self, offset: Vec3) -> Self {
210+
self.offset = offset;
211+
self
212+
}
213+
214+
pub fn with_smoothing(mut self, smoothing: f32) -> Self {
215+
self.smoothing = smoothing.clamp(MIN_SMOOTHING, MAX_SMOOTHING);
216+
self
217+
}
218+
}
219+
220+
/// Marker component for UI root node
221+
#[derive(Component, Debug, Default)]
222+
pub struct GameUI;
223+
224+
/// Marker component for score display UI
225+
#[derive(Component, Debug, Default)]
226+
pub struct ScoreDisplay;
227+
228+
/// Marker component for health bar UI
229+
#[derive(Component, Debug, Default)]
230+
pub struct HealthBar;
231+
232+
/// Marker component for health bar fill (the colored part)
233+
#[derive(Component, Debug, Default)]
234+
pub struct HealthBarFill;
235+
236+
/// Damage value component for obstacles
237+
#[derive(Component, Debug, Clone)]
238+
pub struct DamageOnContact {
239+
pub damage: f32,
240+
}
241+
242+
impl Default for DamageOnContact {
243+
fn default() -> Self {
244+
Self { damage: 10.0 }
245+
}
246+
}
247+
248+
impl DamageOnContact {
249+
pub fn new(damage: f32) -> Self {
250+
Self { damage }
251+
}
252+
}
253+
133254
#[cfg(test)]
134255
mod tests {
135256
use super::*;
@@ -211,4 +332,52 @@ mod tests {
211332
assert_eq!(config.jump_velocity, 450.0);
212333
assert_eq!(config.jump_cut_multiplier, 0.5);
213334
}
335+
336+
#[test]
337+
fn test_auto_move_default() {
338+
let auto_move = AutoMove::default();
339+
assert_eq!(auto_move.direction, Vec2::new(-1.0, 0.0));
340+
assert_eq!(auto_move.speed, 150.0);
341+
}
342+
343+
#[test]
344+
fn test_auto_move_new() {
345+
let auto_move = AutoMove::new(Vec2::new(2.0, 0.0), 200.0);
346+
assert!((auto_move.direction.x - 1.0).abs() < f32::EPSILON);
347+
assert_eq!(auto_move.speed, 200.0);
348+
}
349+
350+
#[test]
351+
fn test_auto_move_left() {
352+
let auto_move = AutoMove::left(100.0);
353+
assert_eq!(auto_move.direction, Vec2::new(-1.0, 0.0));
354+
assert_eq!(auto_move.speed, 100.0);
355+
}
356+
357+
#[test]
358+
fn test_auto_move_right() {
359+
let auto_move = AutoMove::right(100.0);
360+
assert_eq!(auto_move.direction, Vec2::new(1.0, 0.0));
361+
assert_eq!(auto_move.speed, 100.0);
362+
}
363+
364+
#[test]
365+
fn test_camera_follow_default() {
366+
let follow = CameraFollow::default();
367+
assert!(follow.target.is_none());
368+
assert_eq!(follow.offset, Vec3::ZERO);
369+
assert!((follow.smoothing - 0.1).abs() < f32::EPSILON);
370+
}
371+
372+
#[test]
373+
fn test_damage_on_contact_default() {
374+
let damage = DamageOnContact::default();
375+
assert_eq!(damage.damage, 10.0);
376+
}
377+
378+
#[test]
379+
fn test_damage_on_contact_new() {
380+
let damage = DamageOnContact::new(25.0);
381+
assert_eq!(damage.damage, 25.0);
382+
}
214383
}

src/game/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,44 @@ pub mod constants {
1111
pub const WINDOW_HEIGHT: f32 = 720.0;
1212
/// Game title
1313
pub const GAME_TITLE: &str = "2D Mario-Style Platformer";
14+
15+
/// Camera smoothing constants
16+
pub mod camera {
17+
/// Target framerate for smoothing calculations
18+
pub const TARGET_FRAMERATE: f32 = 60.0;
19+
/// Minimum smoothing factor (instant follow)
20+
pub const MIN_SMOOTHING: f32 = 0.01;
21+
/// Maximum smoothing factor (slowest follow)
22+
pub const MAX_SMOOTHING: f32 = 1.0;
23+
}
24+
25+
/// Obstacle spawning constants
26+
pub mod obstacles {
27+
/// X position where obstacles spawn (right side of screen)
28+
pub const SPAWN_X: f32 = 700.0;
29+
/// Minimum Y position for obstacle spawn
30+
pub const SPAWN_Y_MIN: f32 = -200.0;
31+
/// Maximum Y position for obstacle spawn
32+
pub const SPAWN_Y_MAX: f32 = 150.0;
33+
/// Minimum obstacle width
34+
pub const WIDTH_MIN: f32 = 30.0;
35+
/// Maximum obstacle width
36+
pub const WIDTH_MAX: f32 = 60.0;
37+
/// Minimum obstacle height
38+
pub const HEIGHT_MIN: f32 = 30.0;
39+
/// Maximum obstacle height
40+
pub const HEIGHT_MAX: f32 = 60.0;
41+
/// Minimum obstacle speed
42+
pub const SPEED_MIN: f32 = 100.0;
43+
/// Maximum obstacle speed
44+
pub const SPEED_MAX: f32 = 250.0;
45+
/// X position at which obstacles are despawned (left side of screen)
46+
pub const DESPAWN_X: f32 = -800.0;
47+
}
48+
49+
/// Scoring constants
50+
pub mod scoring {
51+
/// Points awarded for surviving obstacle collision
52+
pub const OBSTACLE_SURVIVE_POINTS: u32 = 10;
53+
}
1454
}

src/plugins/mod.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
66
use bevy::prelude::*;
77

8-
use crate::resources::{GameSettings, GameTimer, Score};
8+
use crate::resources::{GameSettings, GameTimer, ObstacleSpawnTimer, Score};
99
use crate::states::GameState;
1010
use crate::systems::{
11-
apply_gravity, apply_velocity, check_platform_collisions, player_jump, player_movement,
12-
setup_camera, spawn_platforms, spawn_player,
11+
apply_auto_movement, apply_gravity, apply_velocity, camera_follow_system,
12+
check_obstacle_collisions, check_platform_collisions, despawn_offscreen_obstacles, player_jump,
13+
player_movement, setup_camera, setup_camera_follow, spawn_game_ui, spawn_obstacles,
14+
spawn_platforms, spawn_player, update_health_bar, update_score_display,
1315
};
1416

1517
/// Main game plugin that sets up all game systems
@@ -24,18 +26,37 @@ impl Plugin for GamePlugin {
2426
.init_resource::<GameSettings>()
2527
.init_resource::<Score>()
2628
.init_resource::<GameTimer>()
29+
.init_resource::<ObstacleSpawnTimer>()
2730
// Setup systems (run once on startup)
28-
.add_systems(Startup, (setup_camera, spawn_player, spawn_platforms))
31+
.add_systems(
32+
Startup,
33+
(setup_camera, spawn_player, spawn_platforms, spawn_game_ui),
34+
)
35+
// Post-startup setup for camera follow (after player is spawned)
36+
.add_systems(OnEnter(GameState::Playing), setup_camera_follow)
2937
// Update systems (run every frame during Playing state)
30-
// Order: input -> physics -> collision -> movement
38+
// Order: input -> physics -> collision -> movement -> camera -> UI
3139
.add_systems(
3240
Update,
3341
(
42+
// Player input systems
3443
player_movement,
3544
player_jump,
45+
// Physics systems
3646
apply_gravity,
3747
apply_velocity,
48+
apply_auto_movement,
49+
// Collision systems
3850
check_platform_collisions,
51+
check_obstacle_collisions,
52+
// Obstacle spawning and cleanup
53+
spawn_obstacles,
54+
despawn_offscreen_obstacles,
55+
// Camera system
56+
camera_follow_system,
57+
// UI systems
58+
update_score_display,
59+
update_health_bar,
3960
)
4061
.chain()
4162
.run_if(in_state(GameState::Playing)),

0 commit comments

Comments
 (0)