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}