chunky_bevy/lib.rs
1//! A simple and efficient chunk management system for Bevy.
2//!
3//! # Quick Start
4//!
5//! ```no_run
6//! use bevy::prelude::*;
7//! use chunky_bevy::prelude::*;
8//!
9//! fn main() {
10//! App::new()
11//! .add_plugins(DefaultPlugins)
12//! .add_plugins(ChunkyPlugin::default())
13//! .add_systems(Startup, setup)
14//! .run();
15//! }
16//!
17//! fn setup(mut commands: Commands) {
18//! // Spawn a chunk loader that generates chunks around it
19//! commands.spawn((
20//! Transform::default(),
21//! ChunkLoader(IVec3::new(2, 1, 2)), // Load 5x3x5 chunks
22//! ));
23//! }
24//! ```
25//!
26//! # Features
27//!
28//! - `chunk_visualizer` (default) - Enables debug visualization of chunk boundaries
29//! - `chunk_loader` (default) - Enables automatic chunk loading around ChunkLoader entities
30//! - `chunk_info` - Logs chunk spawn/despawn events
31
32#[cfg(feature = "chunk_loader")]
33mod chunk_loader;
34#[cfg(feature = "chunk_saver")]
35mod chunk_saver;
36#[cfg(feature = "chunk_unloader")]
37mod chunk_unloader;
38#[cfg(feature = "chunk_visualizer")]
39mod chunk_visualizer;
40
41/// Utility functions for spawning chunks in bulk
42pub mod helpers;
43
44use bevy::{
45 ecs::{lifecycle::HookContext, world::DeferredWorld},
46 prelude::*,
47};
48use std::collections::HashMap;
49
50/// Re-exports of commonly used types
51pub mod prelude {
52 #[cfg(feature = "chunk_loader")]
53 pub use crate::chunk_loader::ChunkLoader;
54 #[cfg(feature = "chunk_saver")]
55 pub use crate::chunk_saver::{
56 ChunkDataRegistry, ChunkSaveConfig, ChunkSavingPlugin, RegisterChunkData, SaveError,
57 SaveStyle,
58 };
59 #[cfg(all(feature = "chunk_unloader", feature = "chunk_loader"))]
60 pub use crate::chunk_unloader::ChunkUnloadRadius;
61 #[cfg(feature = "chunk_unloader")]
62 pub use crate::chunk_unloader::{
63 ChunkLastAccess, ChunkPinned, ChunkUnloadByDistance, ChunkUnloadEvent, ChunkUnloadLimit,
64 ChunkUnloadReason,
65 };
66 #[cfg(feature = "chunk_visualizer")]
67 pub use crate::chunk_visualizer::ChunkBoundryVisualizer;
68 pub use crate::{Chunk, ChunkManager, ChunkPos, ChunkyPlugin};
69}
70
71/// The main plugin for chunk management.
72///
73/// # Example
74///
75/// ```no_run
76/// use bevy::prelude::*;
77/// use chunky_bevy::ChunkyPlugin;
78///
79/// App::new()
80/// .add_plugins(ChunkyPlugin::default()) // 10x10x10 chunks
81/// .run();
82/// ```
83pub struct ChunkyPlugin {
84 pub chunk_size: Vec3,
85}
86
87impl Plugin for ChunkyPlugin {
88 fn build(&self, app: &mut App) {
89 app.insert_resource(ChunkManager::new(self.chunk_size));
90 #[cfg(feature = "chunk_loader")]
91 app.add_plugins(chunk_loader::ChunkLoaderPlugin);
92 #[cfg(feature = "chunk_visualizer")]
93 app.add_plugins(chunk_visualizer::ChunkBoundryVisualizerPlugin);
94 #[cfg(feature = "chunk_unloader")]
95 app.add_plugins(chunk_unloader::ChunkUnloaderPlugin);
96 #[cfg(feature = "reflect")]
97 app.register_type::<ChunkPos>()
98 .register_type::<ChunkManager>();
99 }
100}
101
102impl ChunkyPlugin {
103 pub fn new(chunk_size: Vec3) -> Self {
104 Self { chunk_size }
105 }
106
107 /// Standard 3D chunk configuration with 10x10x10 sized chunks
108 pub const THREE_DIMETION: Self = Self {
109 chunk_size: vec3(10.0, 10.0, 10.0),
110 };
111}
112
113impl Default for ChunkyPlugin {
114 fn default() -> Self {
115 Self::THREE_DIMETION
116 }
117}
118
119/// Marks an entity as a chunk.
120///
121/// This component automatically:
122/// - Registers the chunk with the [`ChunkManager`] when added
123/// - Unregisters the chunk when removed
124/// - Requires [`ChunkPos`] and [`Visibility`] components
125///
126/// # Example
127///
128/// ```no_run
129/// use bevy::prelude::*;
130/// use chunky_bevy::prelude::*;
131///
132/// fn spawn_chunk(mut commands: Commands) {
133/// commands.spawn((
134/// Chunk,
135/// ChunkPos(IVec3::new(0, 0, 0)),
136/// ));
137/// }
138/// ```
139#[derive(Component)]
140#[require(ChunkPos, Visibility)]
141#[component(
142 immutable,
143 on_add= on_add_chunk,
144 on_remove = on_remove_chunk
145)]
146pub struct Chunk;
147
148/// Registers the chunk with [`ChunkManager`] when added.
149fn on_add_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
150 let chunk_pos = world.get::<ChunkPos>(entity).unwrap().0;
151 let mut chunk_manager = world.get_resource_mut::<ChunkManager>().unwrap();
152 if chunk_manager.is_loaded(&chunk_pos) {
153 warn!(
154 "New chunk at pos:{} was not spawned there was already a chunk there",
155 chunk_pos
156 );
157 return;
158 }
159
160 chunk_manager.insert(chunk_pos, entity);
161
162 #[cfg(feature = "chunk_info")]
163 info!("[ChunkInfo]ChunkPos: {chunk_pos:?}");
164}
165
166/// Unregisters the chunk from [`ChunkManager`] when removed.
167fn on_remove_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
168 let chunk_pos = world.get::<ChunkPos>(entity).unwrap().0;
169 world
170 .get_resource_mut::<ChunkManager>()
171 .unwrap()
172 .remove(&chunk_pos);
173}
174
175/// The position of a chunk in chunk-space coordinates.
176///
177/// When added to an entity, automatically updates the entity's [`Transform`]
178/// to match the chunk's world position.
179///
180/// # Example
181///
182/// ```no_run
183/// use bevy::prelude::*;
184/// use chunky_bevy::prelude::*;
185///
186/// fn spawn_chunk(mut commands: Commands) {
187/// // Spawns a chunk at chunk position (5, 0, 3)
188/// // With default 10x10x10 chunks, this will be at world position (50, 0, 30)
189/// commands.spawn((
190/// Chunk,
191/// ChunkPos(IVec3::new(5, 0, 3)),
192/// ));
193/// }
194/// ```
195#[derive(Component, Default, Deref, DerefMut)]
196#[cfg_attr(feature = "reflect", derive(Reflect))]
197#[cfg_attr(feature = "reflect", reflect(Component))]
198#[require(Transform)]
199#[component(
200 immutable,
201 on_add= on_add_chunk_pos,
202)]
203pub struct ChunkPos(pub IVec3);
204
205/// Sets the entity's [`Transform`] translation based on chunk position and size.
206fn on_add_chunk_pos(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
207 let chunk_pos = world.get::<ChunkPos>(entity).unwrap();
208 let chunk_size = world.get_resource::<ChunkManager>().unwrap().chunk_size;
209 let translation = chunk_pos.as_vec3() * chunk_size;
210 world.get_mut::<Transform>(entity).unwrap().translation = translation;
211}
212
213/// Resource for managing all chunks in the world.
214///
215/// Provides methods to query chunks by position and convert between
216/// world positions and chunk positions.
217///
218/// # Example
219///
220/// ```no_run
221/// use bevy::prelude::*;
222/// use chunky_bevy::prelude::*;
223///
224/// fn check_chunk(
225/// chunk_manager: Res<ChunkManager>,
226/// player_pos: Vec3,
227/// ) {
228/// // Get the chunk position the player is in
229/// let chunk_pos = chunk_manager.get_chunk_pos(&player_pos);
230///
231/// // Check if that chunk is loaded
232/// if chunk_manager.is_loaded(&chunk_pos) {
233/// println!("Player is in a loaded chunk!");
234/// }
235/// }
236/// ```
237#[derive(Resource, Default)]
238#[cfg_attr(feature = "reflect", derive(Reflect))]
239#[cfg_attr(feature = "reflect", reflect(Resource))]
240pub struct ChunkManager {
241 chunk_size: Vec3,
242 chunks: HashMap<IVec3, Entity>,
243}
244
245impl ChunkManager {
246 /// Creates a new chunk manager with the specified chunk size.
247 pub fn new(chunk_size: Vec3) -> Self {
248 Self {
249 chunk_size,
250 chunks: default(),
251 }
252 }
253
254 /// Returns the size of chunks in world units.
255 pub fn get_size(&self) -> Vec3 {
256 self.chunk_size
257 }
258
259 /// Inserts a new chunk into the manager.
260 ///
261 /// Returns the previous chunk entity if one already existed at this position.
262 ///
263 /// Note: Called automatically when a [`Chunk`] component is added.
264 pub fn insert(&mut self, pos: IVec3, id: Entity) -> Option<Entity> {
265 self.chunks.insert(pos, id)
266 }
267
268 /// Removes a chunk from the manager.
269 ///
270 /// Returns the chunk's entity if it existed.
271 ///
272 /// Note: Called automatically when a [`Chunk`] component is removed.
273 pub fn remove(&mut self, pos: &IVec3) -> Option<Entity> {
274 self.chunks.remove(pos)
275 }
276
277 /// Converts world coordinates into chunk position.
278 ///
279 /// # Example
280 ///
281 /// ```no_run
282 /// use bevy::prelude::*;
283 /// use chunky_bevy::prelude::*;
284 ///
285 /// fn example(chunk_manager: Res<ChunkManager>) {
286 /// let world_pos = Vec3::new(15.0, 5.0, 23.0);
287 /// let chunk_pos = chunk_manager.get_chunk_pos(&world_pos);
288 /// // With default 10x10x10 chunks, this returns IVec3(1, 0, 2)
289 /// }
290 /// ```
291 pub fn get_chunk_pos(&self, pos: &Vec3) -> IVec3 {
292 (*pos / self.chunk_size).floor().as_ivec3()
293 }
294
295 /// Gets the chunk entity at the specified chunk position if it exists.
296 pub fn get_chunk(&self, chunk_pos: &IVec3) -> Option<Entity> {
297 self.chunks.get(chunk_pos).copied()
298 }
299
300 /// Gets the chunk entity at the specified world position if it exists.
301 pub fn get_chunk_form_pos(&self, pos: &Vec3) -> Option<Entity> {
302 self.get_chunk(&self.get_chunk_pos(pos))
303 }
304
305 /// Checks if a chunk is loaded at the specified chunk position.
306 pub fn is_loaded(&self, chunk_pos: &IVec3) -> bool {
307 self.chunks.contains_key(chunk_pos)
308 }
309}