Skip to content

hukasu/bevy-modular-characters

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A example of modular characters in response to SnowdenWintermute's video.

Explanation

A Component is created for each module that the character can have.

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);
In this example there are 4 modules, Head, Body, Legs, and Feet.

An Entity is created with all 4 Componentes and the skeleton.

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.

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.

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.

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();
}
}
}

Models

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published