A example of modular characters in response to SnowdenWintermute's video.
A Component
is created for each module that the character can have.
bevy-modular-characters/src/modular/components.rs
Lines 6 to 56 in 6006e8d
pub trait ModularCharacter: Component { | |
fn id_mut(&mut self) -> &mut usize; | |
fn instance_id_mut(&mut self) -> &mut Option<InstanceId>; | |
fn entities_mut(&mut self) -> &mut Vec<Entity>; | |
fn id(&self) -> &usize; | |
fn instance_id(&self) -> Option<&InstanceId>; | |
fn entities(&self) -> &Vec<Entity>; | |
} | |
macro_rules! create_modular_segment { | |
($name:ident) => { | |
paste::paste! { | |
#[derive(Debug, Component)] | |
pub struct [<ModularCharacter $name>] { | |
pub id: usize, | |
pub instance_id: Option<InstanceId>, | |
pub entities: Vec<Entity>, | |
} | |
impl ModularCharacter for [<ModularCharacter $name>] { | |
fn id_mut(&mut self) -> &mut usize { | |
&mut self.id | |
} | |
fn instance_id_mut(&mut self) -> &mut Option<InstanceId> { | |
&mut self.instance_id | |
} | |
fn entities_mut(&mut self) -> &mut Vec<Entity> { | |
&mut self.entities | |
} | |
fn id(&self) -> &usize { | |
&self.id | |
} | |
fn instance_id(&self) -> Option<&InstanceId> { | |
self.instance_id.as_ref() | |
} | |
fn entities(&self) -> &Vec<Entity> { | |
&self.entities | |
} | |
} | |
} | |
}; | |
} | |
create_modular_segment!(Head); | |
create_modular_segment!(Body); | |
create_modular_segment!(Legs); | |
create_modular_segment!(Feet); |
Head
, Body
, Legs
, and Feet
.
An Entity
is created with all 4 Componentes
and the skeleton.
bevy-modular-characters/src/main.rs
Lines 76 to 109 in 6006e8d
fn spawn_modular( | |
mut commands: Commands, | |
mut scene_spawner: ResMut<SceneSpawner>, | |
asset_server: Res<AssetServer>, | |
) { | |
let entity = commands | |
.spawn(( | |
SpatialBundle::default(), | |
Name::new("Modular"), | |
ModularCharacterHead { | |
id: 0, | |
instance_id: Some(scene_spawner.spawn(asset_server.load(modular::HEADS[0]))), | |
entities: vec![], | |
}, | |
ModularCharacterBody { | |
id: 0, | |
instance_id: Some(scene_spawner.spawn(asset_server.load(modular::BODIES[0]))), | |
entities: vec![], | |
}, | |
ModularCharacterLegs { | |
id: 0, | |
instance_id: Some(scene_spawner.spawn(asset_server.load(modular::LEGS[0]))), | |
entities: vec![], | |
}, | |
ModularCharacterFeet { | |
id: 0, | |
instance_id: Some(scene_spawner.spawn(asset_server.load(modular::FEET[0]))), | |
entities: vec![], | |
}, | |
)) | |
.id(); | |
// Armature | |
scene_spawner.spawn_as_child(asset_server.load("Witch.gltf#Scene1"), entity); | |
} |
Cycle through the modules in response to keyboard inputs.
bevy-modular-characters/src/modular/mod.rs
Lines 246 to 275 in 6006e8d
fn cycle_modular_segment<T: ModularCharacter, const ID: usize>( | |
mut modular: Query<&mut T>, | |
key_input: Res<ButtonInput<KeyCode>>, | |
mut scene_spawner: ResMut<SceneSpawner>, | |
asset_server: Res<AssetServer>, | |
) { | |
const KEYS: [(KeyCode, KeyCode); 4] = [ | |
(KeyCode::KeyQ, KeyCode::KeyW), | |
(KeyCode::KeyE, KeyCode::KeyR), | |
(KeyCode::KeyT, KeyCode::KeyY), | |
(KeyCode::KeyU, KeyCode::KeyI), | |
]; | |
const MODULES: [&[&str]; 4] = [&HEADS, &BODIES, &LEGS, &FEET]; | |
let Ok(mut module) = modular.get_single_mut() else { | |
bevy::log::error!("Couldn't get single module."); | |
return; | |
}; | |
*module.id_mut() = if key_input.just_pressed(KEYS[ID].0) { | |
module.id().wrapping_sub(1).min(MODULES[ID].len() - 1) | |
} else if key_input.just_pressed(KEYS[ID].1) { | |
(module.id() + 1) % MODULES[ID].len() | |
} else { | |
return; | |
}; | |
if let Some(instance) = module.instance_id() { | |
scene_spawner.despawn_instance(*instance); | |
} | |
*module.instance_id_mut() = | |
Some(scene_spawner.spawn(asset_server.load(MODULES[ID][*module.id()]))); | |
} |
When Scene
finishes spawning, transfer data from it to the modular character. The critical part is the creation of
the SkinnedMesh
component. It's necessary to collect the names of the joints and search their counterpart on the skeleton.
Preserve the order of the joints is mandatory.
bevy-modular-characters/src/modular/mod.rs
Lines 126 to 244 in 6006e8d
fn update_modular<T: components::ModularCharacter>( | |
mut commands: Commands, | |
mut changed_modular: Query<(Entity, &mut T), Changed<T>>, | |
mesh_primitives_query: Query<MeshPrimitiveParamSet>, | |
children: Query<&Children>, | |
names: Query<&Name>, | |
mut scene_spawner: ResMut<SceneSpawner>, | |
mut writer: EventWriter<ResetChanged>, | |
) { | |
for (entity, mut modular) in &mut changed_modular { | |
let Some(scene_instance) = modular.instance_id().copied() else { | |
continue; | |
}; | |
if scene_spawner.instance_is_ready(scene_instance) { | |
// Delete old | |
bevy::log::trace!("Deleting old modular segment."); | |
if !modular.entities().is_empty() { | |
commands.entity(entity).remove_children(modular.entities()); | |
} | |
for entity in modular.entities_mut().drain(..) { | |
commands.entity(entity).despawn_recursive(); | |
} | |
// Get MeshPrimitives | |
let mesh_primitives = scene_spawner | |
.iter_instance_entities(scene_instance) | |
.filter(|node| mesh_primitives_query.contains(*node)) | |
.collect::<Vec<_>>(); | |
// Get Meshs | |
let mut meshs = BTreeMap::new(); | |
for mesh_primitive in mesh_primitives { | |
match mesh_primitives_query.get(mesh_primitive) { | |
Ok((parent, _, _, _, _, _)) => { | |
meshs | |
.entry(parent.get()) | |
.and_modify(|v: &mut Vec<_>| v.push(mesh_primitive)) | |
.or_insert(vec![mesh_primitive]); | |
} | |
Err(err) => { | |
bevy::log::error!( | |
"MeshPrimitive {mesh_primitive:?} did not have a parent. '{err:?}'" | |
); | |
} | |
} | |
} | |
// Rebuild Mesh Hierarchy on Modular entity | |
for (mesh, primitives) in meshs { | |
let mesh_entity = match names.get(mesh) { | |
Ok(name) => commands.spawn((SpatialBundle::default(), name.clone())), | |
Err(_) => { | |
bevy::log::warn!("Mesh {mesh:?} did not have a name."); | |
commands.spawn(SpatialBundle::default()) | |
} | |
} | |
.with_children(|parent| { | |
for primitive in primitives { | |
let Ok((_, name, skinned_mesh, mesh, material, aabb)) = | |
mesh_primitives_query.get(primitive) | |
else { | |
unreachable!(); | |
}; | |
let new_joints: Vec<_> = skinned_mesh | |
.joints | |
.iter() | |
.flat_map(|joint| { | |
names | |
.get(*joint) | |
.inspect_err(|_| { | |
bevy::log::error!("Joint {joint:?} had no name.") | |
}) | |
.ok() | |
.map(|joint_name| { | |
children.iter_descendants(entity).find(|node_on_modular| { | |
names | |
.get(*node_on_modular) | |
.ok() | |
.filter(|node_on_modular_name| { | |
node_on_modular_name | |
.as_str() | |
.eq(joint_name.as_str()) | |
}) | |
.is_some() | |
}) | |
}) | |
}) | |
.flatten() | |
.collect(); | |
parent.spawn(( | |
name.clone(), | |
PbrBundle { | |
mesh: mesh.clone(), | |
material: material.clone(), | |
..Default::default() | |
}, | |
SkinnedMesh { | |
inverse_bindposes: skinned_mesh.inverse_bindposes.clone(), | |
joints: new_joints, | |
}, | |
*aabb, | |
NoAutomaticBatching, | |
)); | |
} | |
}) | |
.id(); | |
modular.entities_mut().push(mesh_entity); | |
commands.entity(entity).add_child(mesh_entity); | |
} | |
if let Some(instance) = modular.instance_id_mut().take() { | |
scene_spawner.despawn_instance(instance); | |
} | |
} else { | |
writer.send(ResetChanged(entity)); | |
} | |
} | |
} |
If on update the Scene
has yet not finished loading, send an event to the reset_changed
system for a retry next frame.
bevy-modular-characters/src/modular/mod.rs
Lines 277 to 286 in 6006e8d
fn reset_changed<T: ModularCharacter>( | |
mut query: Query<(Entity, &mut T)>, | |
mut reader: EventReader<ResetChanged>, | |
) { | |
for entity in reader.read() { | |
if let Ok((_, mut modular)) = query.get_mut(**entity) { | |
modular.set_changed(); | |
} | |
} | |
} |
The models were taken from the Quaternius Ultimate Modular Women.
This example uses the Adventurer, SciFi, Soldier and Witch models with minor adjustments. The original models contain one (1) scene that loads the Armature and the meshes (head, torso, legs, and feet). Also included is 2 models that were used by Snowden on his video, that have most of it's content deleted.