1use bevy::{input::mouse::MouseMotion, prelude::*};
13use chunky_bevy::prelude::*;
14
15fn main() {
16 App::new()
17 .add_plugins(DefaultPlugins)
18 .add_plugins(ChunkyPlugin::default())
19 .init_state::<UnloadStrategy>()
20 .add_systems(Startup, setup)
21 .add_systems(
22 Update,
23 (
24 fly_camera,
25 cycle_unload_strategy,
26 update_unload_resources,
27 update_ui,
28 log_unload_events,
29 ),
30 )
31 .run();
32}
33
34#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35enum UnloadStrategy {
36 #[default]
37 None,
38 DistanceOnly,
39 LimitOnly,
40 Hybrid,
41}
42
43impl UnloadStrategy {
44 fn next(self) -> Self {
45 match self {
46 Self::None => Self::DistanceOnly,
47 Self::DistanceOnly => Self::LimitOnly,
48 Self::LimitOnly => Self::Hybrid,
49 Self::Hybrid => Self::None,
50 }
51 }
52
53 fn label(self) -> &'static str {
54 match self {
55 Self::None => "None (chunks never unload)",
56 Self::DistanceOnly => "Distance Only (unload when out of range)",
57 Self::LimitOnly => "Limit Only (LRU eviction at 50 chunks)",
58 Self::Hybrid => "Hybrid (out of range AND over limit)",
59 }
60 }
61}
62
63#[derive(Component)]
64struct FlyCam {
65 speed: f32,
66 sensitivity: f32,
67 pitch: f32,
68 yaw: f32,
69}
70
71impl Default for FlyCam {
72 fn default() -> Self {
73 Self {
74 speed: 30.0,
75 sensitivity: 0.003,
76 pitch: -0.3,
77 yaw: 0.0,
78 }
79 }
80}
81
82#[derive(Component)]
83struct StatsText;
84
85#[derive(Component)]
86struct Player;
87
88fn setup(mut commands: Commands, mut visualizer: ResMut<NextState<ChunkBoundryVisualizer>>) {
89 commands.spawn((
91 Camera3d::default(),
92 Transform::from_xyz(0.0, 20.0, 50.0).looking_at(Vec3::ZERO, Vec3::Y),
93 FlyCam::default(),
94 Player,
95 ChunkLoader(IVec3::new(3, 1, 3)),
96 ChunkUnloadRadius(IVec3::new(5, 2, 5)),
97 ));
98
99 commands.spawn((
101 DirectionalLight {
102 illuminance: 15000.0,
103 shadows_enabled: true,
104 ..default()
105 },
106 Transform::from_xyz(50.0, 100.0, 50.0).looking_at(Vec3::ZERO, Vec3::Y),
107 ));
108
109 commands.spawn((
111 Text::new(""),
112 TextFont {
113 font_size: 18.0,
114 ..default()
115 },
116 TextColor(Color::WHITE),
117 Node {
118 position_type: PositionType::Absolute,
119 top: Val::Px(12.0),
120 left: Val::Px(12.0),
121 ..default()
122 },
123 StatsText,
124 ));
125
126 visualizer.set(ChunkBoundryVisualizer::On);
127}
128
129fn fly_camera(
130 time: Res<Time>,
131 keyboard: Res<ButtonInput<KeyCode>>,
132 mouse_buttons: Res<ButtonInput<MouseButton>>,
133 mut mouse_motion: MessageReader<MouseMotion>,
134 mut query: Query<(&mut Transform, &mut FlyCam)>,
135) {
136 let Ok((mut transform, mut cam)) = query.single_mut() else {
137 return;
138 };
139
140 if mouse_buttons.pressed(MouseButton::Right) {
142 for motion in mouse_motion.read() {
143 cam.yaw -= motion.delta.x * cam.sensitivity;
144 cam.pitch -= motion.delta.y * cam.sensitivity;
145 cam.pitch = cam.pitch.clamp(-1.5, 1.5);
146 }
147 } else {
148 mouse_motion.clear();
149 }
150
151 transform.rotation = Quat::from_euler(EulerRot::YXZ, cam.yaw, cam.pitch, 0.0);
152
153 let mut velocity = Vec3::ZERO;
155 let forward = transform.forward().as_vec3();
156 let right = transform.right().as_vec3();
157
158 if keyboard.pressed(KeyCode::KeyW) {
159 velocity += forward;
160 }
161 if keyboard.pressed(KeyCode::KeyS) {
162 velocity -= forward;
163 }
164 if keyboard.pressed(KeyCode::KeyD) {
165 velocity += right;
166 }
167 if keyboard.pressed(KeyCode::KeyA) {
168 velocity -= right;
169 }
170 if keyboard.pressed(KeyCode::KeyE) {
171 velocity += Vec3::Y;
172 }
173 if keyboard.pressed(KeyCode::KeyQ) {
174 velocity -= Vec3::Y;
175 }
176
177 if velocity != Vec3::ZERO {
178 transform.translation += velocity.normalize() * cam.speed * time.delta_secs();
179 }
180}
181
182fn cycle_unload_strategy(
183 keyboard: Res<ButtonInput<KeyCode>>,
184 current: Res<State<UnloadStrategy>>,
185 mut next: ResMut<NextState<UnloadStrategy>>,
186) {
187 if keyboard.just_pressed(KeyCode::Space) {
188 next.set(current.get().next());
189 }
190}
191
192fn update_unload_resources(
193 mut commands: Commands,
194 strategy: Res<State<UnloadStrategy>>,
195 distance_res: Option<Res<ChunkUnloadByDistance>>,
196 limit_res: Option<Res<ChunkUnloadLimit>>,
197) {
198 if !strategy.is_changed() {
199 return;
200 }
201
202 if distance_res.is_some() {
204 commands.remove_resource::<ChunkUnloadByDistance>();
205 }
206 if limit_res.is_some() {
207 commands.remove_resource::<ChunkUnloadLimit>();
208 }
209
210 match strategy.get() {
212 UnloadStrategy::None => {}
213 UnloadStrategy::DistanceOnly => {
214 commands.insert_resource(ChunkUnloadByDistance);
215 }
216 UnloadStrategy::LimitOnly => {
217 commands.insert_resource(ChunkUnloadLimit { max_chunks: 50 });
218 }
219 UnloadStrategy::Hybrid => {
220 commands.insert_resource(ChunkUnloadByDistance);
221 commands.insert_resource(ChunkUnloadLimit { max_chunks: 50 });
222 }
223 }
224}
225
226fn update_ui(
227 mut text_query: Query<&mut Text, With<StatsText>>,
228 chunks: Query<&ChunkPos, With<Chunk>>,
229 pinned: Query<(), With<ChunkPinned>>,
230 player: Query<&Transform, With<Player>>,
231 strategy: Res<State<UnloadStrategy>>,
232 chunk_manager: Res<ChunkManager>,
233) {
234 let Ok(mut text) = text_query.single_mut() else {
235 return;
236 };
237 let Ok(player_transform) = player.single() else {
238 return;
239 };
240
241 let chunk_count = chunks.iter().count();
242 let pinned_count = pinned.iter().count();
243 let player_chunk = chunk_manager.get_chunk_pos(&player_transform.translation);
244
245 **text = format!(
246 "Chunks: {} (pinned: {})\n\
247 Player chunk: {}\n\
248 \n\
249 Strategy: {}\n\
250 \n\
251 [Space] Cycle strategy\n\
252 [WASD/QE] Move\n\
253 [Right-click + drag] Look",
254 chunk_count,
255 pinned_count,
256 player_chunk,
257 strategy.get().label(),
258 );
259}
260
261fn log_unload_events(mut events: MessageReader<ChunkUnloadEvent>) {
262 for event in events.read() {
263 info!("Chunk {:?} unloaded: {:?}", event.chunk_pos, event.reason);
264 }
265}