Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial draft of the entities-from-manifests example #25

Merged
merged 8 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions assets/tiles.ron
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(
tiles: [
(
name: "Gotham",
color: (0.0, 1.0, 0.0),
tile_type: City,
),
(
name: "River",
color: (0.0, 0.0, 1.0),
tile_type: Water,
),
(
name: "Dark Forest",
color: (1.0, 0.0, 0.0),
tile_type: Wilderness,
),
],
)
230 changes: 218 additions & 12 deletions examples/entities_from_manifests.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,222 @@
//! Spawning entities based on the contents of a manifest resource is one of the most common use cases for `leafwing_manifest`.
//! This PR demonstrates how to spawn entities based on manifest entries by constructing a bundle based on the data in the manifest.
//!
//! These might be monsters, levels, or any other kind of game object.
//! While there are several possible ways you could achieve this goal, we've found that simply creating a custom bundle
//! and defining a constructor that takes the manifest data is a very effective way to ensure that all of your entities
//! have the right components, no matter where they're spawned.
//!
//! There are three main patterns, each of which are showcased in this example:
//! Generally speaking, you'll want to create a custom bundle type for each manifest that you have.
//! Store a handle to *all* of the assets that you need in the bundle:
//! this will allow you to avoid passing in references to the asset storage at every call site.
//!
//! 1. Item-as-bundle: Each item in the manifest is a complete bundle of components, which are added to a single entity when it is spawned.
//! 2. Item-as-partial-bundle: Each item in the manifest contains the configurable elements of an entity's bundle, from which the final bundle is constructed.
//! 3. Item-as-scene: Each item in the manifest is a scene containing a hierarchy of entities.
//!
//!
//! The item-as-bundle pattern is the simplest, and is suitable for cases where you don't have much duplicated data between items.
//! The item-as-partial-bundle pattern is more flexible, and is suitable for cases where you have a lot of duplicated data between items that you don't want to bloat your manifest with.
//! The item-as-scene pattern is the most complex, and is suitable for cases (such as 3D models) where you actually want to spawn an entire entity hierarchy.
//! If you need to spawn a scene hierarchy (such as for levels or 3D models), storing a handle to that scene can work well,
//! or a scene bundle can be added to your custom bundle type.

use bevy::{prelude::*, sprite::Mesh2dHandle, utils::HashMap};
use leafwing_manifest::{
asset_state::SimpleAssetState,
identifier::Id,
manifest::{Manifest, ManifestFormat},
plugin::{AppExt, ManifestPlugin},
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct RawTile {
name: String,
/// An RGB color in float form.
color: [f32; 3],
// Serializing enums works just fine,
// and there are often *some* properties that should be an exhaustive list of options.
tile_type: TileType,
}

#[derive(Debug)]
pub struct Tile {
name: String,
// We convert the supplied u32 color into a `ColorMaterial` during manifest processing.
color_material: Handle<ColorMaterial>,
// The same square mesh is used for all tiles,
// and can be procedurally generated during .
mesh: Mesh2dHandle,
tile_type: TileType,
}

#[derive(Component, Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
enum TileType {
City,
Water,
Wilderness,
}

// Creating a custom bundle allows us to ensure that all of our tile objects have the right components,
// no matter where they're spawned.
#[derive(Bundle)]
pub struct TileBundle {
// Storing the `Id<Tile>` directly on the bundle allows us to easily look up particularly heavy data later.
// It also serves as a nice way to filter for tiles in queries.
id: Id<Tile>,
tile_type: TileType,
material: Handle<ColorMaterial>,
mesh: Mesh2dHandle,
// Add all of the components needed to render the tile.
spatial_bundle: SpatialBundle,
}

impl TileBundle {
// When defining constructors, you'll typically find that you need to pass in both
// the manifest data (describing the fundamental properties of the entity)
// and information about the exact location and dynamic properties of the entity required.
// Other fields (such as Visibility here) will *always* be the same,
// so we don't need to duplicate the data in the manifest.
fn new(transform: Transform, tile: &Tile) -> Self {
Self {
id: Id::from_name(&tile.name),
tile_type: tile.tile_type,
// We can use weak clones here and save a tiny bit of work,
// since the manifest will always store a canonical strong handle to the assets.
material: tile.color_material.clone_weak(),
// While the value of the mesh is the same for all tiles, passing around `&Assets<Mesh>` everywhere
// is miserable. Instead, we sacrifice a little bit of memory to redundantly store the mesh handle in the manifest:
// like always, the mesh itself is only stored once in the asset storage.
mesh: tile.mesh.clone(),
spatial_bundle: SpatialBundle::from_transform(transform),
}
}
}

#[derive(Asset, Serialize, Deserialize, TypePath, Debug, PartialEq)]
pub struct RawTileManifest {
tiles: Vec<RawTile>,
}

#[derive(Resource, Default)]
pub struct TileManifest {
tiles: HashMap<Id<Tile>, Tile>,
}

impl Manifest for TileManifest {
type Item = Tile;
type RawItem = String;
type RawManifest = RawTileManifest;
type ConversionError = std::convert::Infallible;

const FORMAT: ManifestFormat = ManifestFormat::Ron;

fn get(&self, id: Id<Tile>) -> Option<&Self::Item> {
self.tiles.get(&id)
}

fn from_raw_manifest(
raw_manifest: Self::RawManifest,
world: &mut World,
) -> Result<Self, Self::ConversionError> {
let mut meshes = world.resource_mut::<Assets<Mesh>>();
let mesh = meshes.add(Mesh::from(Rectangle::new(1.0, 1.0)));
// This is a thin wrapper around a `Handle<Mesh>`, used in 2D rendering.
let mesh_2d = Mesh2dHandle::from(mesh.clone());

let mut color_materials = world.resource_mut::<Assets<ColorMaterial>>();

let mut manifest = TileManifest::default();

for raw_tile in raw_manifest.tiles {
// This is a very simple example of procedurally generated assets,
// driven by hand-tuned parameters in the manifest.
// In a real game, you might use a more complex system to generate the assets,
// but the general pattern is very effective for creating cohesive but varied content.
let color_material = color_materials.add(Color::rgb_from_array(raw_tile.color));

manifest.tiles.insert(
Id::from_name(&raw_tile.name),
Tile {
name: raw_tile.name,
color_material,
// We need to store strong handles here: otherwise the procedural mesh will be dropped immediately
// when the original declaration goes out of scope.
mesh: mesh_2d.clone(),
tile_type: raw_tile.tile_type,
},
);
}

Ok(manifest)
}
}

pub fn spawn_tiles(mut commands: Commands, tile_manifest: Res<TileManifest>) {
// 2D camera scales are measured in pixels per unit.
const SCALE: f32 = 128.;
// Space the tiles out a bit.
const SPACING: f32 = 1.5;

info!("Spawning tiles...");

// Remember to add the camera bundle to the world, or you won't see anything!
commands.spawn(Camera2dBundle::default());

for (i, tile) in tile_manifest.tiles.values().enumerate() {
info!("{:?}", tile);
sixfold-origami marked this conversation as resolved.
Show resolved Hide resolved

// Space out the spawned tiles for demonstration purposes.
let translation = Vec3::X * i as f32 * SCALE * SPACING;
let transform = Transform::from_translation(translation).with_scale(Vec3::splat(SCALE));

commands.spawn(TileBundle::new(transform, tile));
}
}

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_state::<SimpleAssetState>()
.add_plugins(ManifestPlugin::<SimpleAssetState>::default())
.register_manifest::<TileManifest>("tiles.ron")
.add_systems(OnEnter(SimpleAssetState::Ready), spawn_tiles)
.run();
}

/// This module is used to generate the tile manifest.
///
/// While manifests *can* be hand-authored, it's often more convenient to generate them using tooling of some kind.
/// Serde's [`Serialize`] and [`Deserialize`] traits are a good fit for this purpose.
/// `ron` is a straightforward human-readable format that plays well with Rust's type system, and is a good point to start.
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn generate_raw_item_manifest() {
let mut raw_tile_manifest = RawTileManifest {
tiles: Vec::default(),
};

raw_tile_manifest.tiles.push(RawTile {
name: "Gotham".to_string(),
color: [0.0, 1.0, 0.0],
tile_type: TileType::City,
});

raw_tile_manifest.tiles.push(RawTile {
name: "River".to_string(),
color: [0.0, 0.0, 1.0],
tile_type: TileType::Water,
});

raw_tile_manifest.tiles.push(RawTile {
name: "Dark Forest".to_string(),
color: [1.0, 0.0, 0.0],
tile_type: TileType::Wilderness,
});

let serialized =
ron::ser::to_string_pretty(&raw_tile_manifest, Default::default()).unwrap();
println!("{}", serialized);

// Save the results, to ensure that our example has a valid manifest to read.
std::fs::write("assets/tiles.ron", &serialized).unwrap();

let deserialized: RawTileManifest = ron::de::from_str(&serialized).unwrap();

fn main() {}
assert_eq!(raw_tile_manifest, deserialized);
}
}
4 changes: 3 additions & 1 deletion src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ pub fn check_if_manifests_are_processed<S: AssetLoadingState>(
if raw_manifest_tracker.processing_status() == ProcessingStatus::Failed {
next_state.set(S::FAILED);
} else if raw_manifest_tracker.processing_status() == ProcessingStatus::Ready {
info!("All manifests have been processed successfully.");
next_state.set(S::READY);
}
}
Expand Down Expand Up @@ -300,7 +301,8 @@ pub fn process_manifest<M: Manifest>(
world: &mut World,
system_state: &mut SystemState<(Res<RawManifestTracker>, ResMut<Assets<M::RawManifest>>)>,
) {
info!("Process manifest");
info!("Processing manifest of type {}.", type_name::<M>());

let (raw_manifest_tracker, mut assets) = system_state.get_mut(world);
let Some(status) = raw_manifest_tracker.status::<M>() else {
error_once!(
Expand Down