Skip to content

Commit f993f73

Browse files
Copilotnpv2k1
andcommitted
Add core game template features: auto-movement, obstacles, camera follow, and UI
Co-authored-by: npv2k1 <73846954+npv2k1@users.noreply.github.com>
1 parent b5219d9 commit f993f73

12 files changed

Lines changed: 638 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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,125 @@ impl Default for JumpConfig {
130130
}
131131
}
132132

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

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)),

src/resources/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ impl GameTimer {
8282
}
8383
}
8484

85+
/// Obstacle spawn timer resource
86+
#[derive(Resource, Debug)]
87+
pub struct ObstacleSpawnTimer {
88+
pub timer: Timer,
89+
}
90+
91+
impl Default for ObstacleSpawnTimer {
92+
fn default() -> Self {
93+
Self {
94+
timer: Timer::from_seconds(2.0, TimerMode::Repeating),
95+
}
96+
}
97+
}
98+
99+
impl ObstacleSpawnTimer {
100+
pub fn new(interval_seconds: f32) -> Self {
101+
Self {
102+
timer: Timer::from_seconds(interval_seconds, TimerMode::Repeating),
103+
}
104+
}
105+
}
106+
85107
#[cfg(test)]
86108
mod tests {
87109
use super::*;

src/systems/auto_movement.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! Auto-movement systems for entities that move automatically
2+
3+
use bevy::prelude::*;
4+
5+
use crate::components::AutoMove;
6+
7+
/// Applies automatic movement to entities with AutoMove component
8+
pub fn apply_auto_movement(time: Res<Time>, mut query: Query<(&AutoMove, &mut Transform)>) {
9+
for (auto_move, mut transform) in query.iter_mut() {
10+
let movement = auto_move.direction * auto_move.speed * time.delta_seconds();
11+
transform.translation.x += movement.x;
12+
transform.translation.y += movement.y;
13+
}
14+
}

0 commit comments

Comments
 (0)