new character format

This commit is contained in:
2025-04-21 15:36:40 +02:00
parent 2dcc396666
commit 478efedd6c
15 changed files with 153 additions and 183 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,81 +0,0 @@
use crate::{GameState, loading_assets::GameAssets};
use bevy::prelude::*;
use std::time::Duration;
#[derive(Resource)]
pub struct Animations {
pub animations: Vec<AnimationNodeIndex>,
pub graph: Handle<AnimationGraph>,
}
pub fn plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
Update,
(setup_once_loaded, toggle_animation).run_if(in_state(GameState::Playing)),
);
}
fn setup(
mut commands: Commands,
assets: Res<GameAssets>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
// Build the animation graph
let (graph, node_indices) = AnimationGraph::from_clips([
assets.animations_alien.get("Animation2").cloned().unwrap(),
assets.animations_alien.get("Animation1").cloned().unwrap(),
assets.animations_alien.get("Animation0").cloned().unwrap(),
]);
// Insert a resource with the current scene information
let graph_handle = graphs.add(graph);
commands.insert_resource(Animations {
animations: node_indices,
graph: graph_handle,
});
}
fn setup_once_loaded(
mut commands: Commands,
animations: Res<Animations>,
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
) {
for (entity, mut player) in &mut players {
let mut transitions = AnimationTransitions::new();
// Make sure to start the animation via the `AnimationTransitions`
// component. The `AnimationTransitions` component wants to manage all
// the animations and will get confused if the animations are started
// directly via the `AnimationPlayer`.
transitions
.play(&mut player, animations.animations[1], Duration::ZERO)
.repeat();
commands
.entity(entity)
.insert(AnimationGraphHandle(animations.graph.clone()))
.insert(transitions);
}
}
fn toggle_animation(
animations: Res<Animations>,
mut transitions: Query<(&mut AnimationTransitions, &mut AnimationPlayer)>,
keys: Res<ButtonInput<KeyCode>>,
mut animation_index: Local<u32>,
) {
if keys.just_pressed(KeyCode::KeyT) {
for (mut transition, mut player) in &mut transitions {
transition
.play(
&mut player,
animations.animations[*animation_index as usize],
Duration::from_secs(1),
)
.repeat();
}
*animation_index ^= 1; // Toggle between 0 and 1
}
}

102
src/character.rs Normal file
View File

@@ -0,0 +1,102 @@
use crate::{GameState, heads_database::HeadsDatabase, loading_assets::GameAssets};
use bevy::{prelude::*, utils::HashMap};
use std::time::Duration;
#[derive(Component, Debug)]
pub struct AnimatedCharacter(pub usize);
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
pub struct CharacterAnimations {
pub idle: AnimationNodeIndex,
pub run: AnimationNodeIndex,
pub jump: AnimationNodeIndex,
pub graph: Handle<AnimationGraph>,
}
pub fn plugin(app: &mut App) {
app.add_systems(
Update,
(spawn, setup_once_loaded).run_if(in_state(GameState::Playing)),
);
}
fn spawn(
mut commands: Commands,
mut query: Query<(Entity, &AnimatedCharacter), Added<AnimatedCharacter>>,
gltf_assets: Res<Assets<Gltf>>,
assets: Res<GameAssets>,
heads_db: Res<HeadsDatabase>,
) {
for (entity, character) in &mut query {
let key = heads_db.head_key(character.0);
let handle = assets
.characters
.get(format!("{key}.glb").as_str())
.unwrap_or_else(|| {
//TODO: remove once we use the new format for all
info!("Character not found, using default [{}]", key);
&assets.characters["angry demonstrator.glb"]
});
let asset = gltf_assets.get(handle).unwrap();
commands.entity(entity).insert((
Transform::from_translation(Vec3::new(0., -1.45, 0.)).with_scale(Vec3::splat(1.2)),
SceneRoot(asset.scenes[0].clone()),
));
}
}
fn setup_once_loaded(
mut commands: Commands,
mut query: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
parent_query: Query<&Parent>,
animated_character: Query<&AnimatedCharacter>,
assets: Res<GameAssets>,
gltf_assets: Res<Assets<Gltf>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
for (entity, mut player) in &mut query {
let Some(_animated_character) = parent_query
.iter_ancestors(entity)
.find_map(|ancestor| animated_character.get(ancestor).ok())
else {
continue;
};
let (_, handle) = assets.characters.iter().next().unwrap();
let asset = gltf_assets.get(handle).unwrap();
let animations = asset
.named_animations
.iter()
.map(|(name, animation)| (name.to_string(), animation.clone()))
.collect::<HashMap<_, _>>();
let (graph, node_indices) = AnimationGraph::from_clips([
animations["idle"].clone(),
animations["run"].clone(),
animations["jump"].clone(),
]);
// // Insert a resource with the current scene information
let graph_handle = graphs.add(graph);
let animations = CharacterAnimations {
idle: node_indices[0],
run: node_indices[1],
jump: node_indices[2],
graph: graph_handle.clone(),
};
let mut transitions = AnimationTransitions::new();
transitions
.play(&mut player, animations.idle, Duration::ZERO)
.repeat();
commands
.entity(entity)
.insert(AnimationGraphHandle(animations.graph.clone()))
.insert(transitions)
.insert(animations);
}
}

View File

@@ -38,7 +38,6 @@ pub struct AudioAssets {
pub throw_explosion: Handle<AudioSource>, pub throw_explosion: Handle<AudioSource>,
#[asset(path = "sfx/abilities/jet.ogg")] #[asset(path = "sfx/abilities/jet.ogg")]
pub jet: Handle<AudioSource>, pub jet: Handle<AudioSource>,
#[asset(path = "sfx/ui/backpack_open.ogg")] #[asset(path = "sfx/ui/backpack_open.ogg")]
pub backpack_open: Handle<AudioSource>, pub backpack_open: Handle<AudioSource>,
#[asset(path = "sfx/ui/backpack_close.ogg")] #[asset(path = "sfx/ui/backpack_close.ogg")]
@@ -91,21 +90,11 @@ pub struct GameAssets {
#[asset(path = "models/cash.glb#Scene0")] #[asset(path = "models/cash.glb#Scene0")]
pub mesh_cash: Handle<Scene>, pub mesh_cash: Handle<Scene>,
#[asset(path = "models/alien_naked.glb#Scene0")]
pub mesh_alien: Handle<Scene>,
#[asset(
paths(
"models/alien_naked.glb#Animation0",
"models/alien_naked.glb#Animation1",
"models/alien_naked.glb#Animation2",
),
collection(mapped, typed)
)]
pub animations_alien: HashMap<AssetLabel, Handle<AnimationClip>>,
#[asset(path = "models/projectiles", collection(mapped, typed))] #[asset(path = "models/projectiles", collection(mapped, typed))]
pub projectiles: HashMap<AssetFileName, Handle<Gltf>>, pub projectiles: HashMap<AssetFileName, Handle<Gltf>>,
#[asset(path = "models/characters", collection(mapped, typed))]
pub characters: HashMap<AssetFileName, Handle<Gltf>>,
} }
pub struct LoadingPlugin; pub struct LoadingPlugin;

View File

@@ -1,11 +1,11 @@
mod abilities; mod abilities;
mod ai; mod ai;
mod aim; mod aim;
mod alien;
mod backpack; mod backpack;
mod camera; mod camera;
mod cash; mod cash;
mod cash_heal; mod cash_heal;
mod character;
mod control; mod control;
mod cutscene; mod cutscene;
mod debug; mod debug;
@@ -126,7 +126,7 @@ fn main() {
} }
app.add_plugins(ai::plugin); app.add_plugins(ai::plugin);
app.add_plugins(alien::plugin); app.add_plugins(character::plugin);
app.add_plugins(cash::plugin); app.add_plugins(cash::plugin);
app.add_plugins(player::plugin); app.add_plugins(player::plugin);
app.add_plugins(gates::plugin); app.add_plugins(gates::plugin);

View File

@@ -1,6 +1,7 @@
use crate::{ use crate::{
GameState, GameState,
ai::Ai, ai::Ai,
character::AnimatedCharacter,
head::ActiveHead, head::ActiveHead,
head_drop::HeadDrops, head_drop::HeadDrops,
heads::{ActiveHeads, HEAD_COUNT, HeadState}, heads::{ActiveHeads, HEAD_COUNT, HeadState},
@@ -44,6 +45,7 @@ fn init(mut commands: Commands, query: Query<(Entity, &EnemySpawn)>, heads_db: R
]), ]),
Ai, Ai,
)) ))
.with_child((Name::from("body-rig"), AnimatedCharacter(id)))
.observe(on_kill); .observe(on_kill);
} }
} }

View File

@@ -1,15 +1,15 @@
use crate::{ use crate::{
GameState, GameState,
alien::Animations,
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent}, cash::{Cash, CashCollectEvent},
character::{AnimatedCharacter, CharacterAnimations},
control::controller_common::{CharacterControllerBundle, PlayerMovement}, control::controller_common::{CharacterControllerBundle, PlayerMovement},
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
heads::{ActiveHeads, HeadChanged, HeadState}, heads::{ActiveHeads, HeadChanged, HeadState},
heads_database::{HeadControls, HeadsDatabase}, heads_database::{HeadControls, HeadsDatabase},
hitpoints::Hitpoints, hitpoints::Hitpoints,
loading_assets::{AudioAssets, GameAssets}, loading_assets::AudioAssets,
physics_layers::GameLayer, physics_layers::GameLayer,
sounds::PlaySound, sounds::PlaySound,
tb_entities::SpawnPoint, tb_entities::SpawnPoint,
@@ -29,9 +29,6 @@ pub struct Player;
#[derive(Component, Default)] #[derive(Component, Default)]
struct PlayerAnimations; struct PlayerAnimations;
#[derive(Component, Default)]
struct PlayerHeadMesh;
#[derive(Component, Default)] #[derive(Component, Default)]
pub struct PlayerBodyMesh; pub struct PlayerBodyMesh;
@@ -56,7 +53,6 @@ fn spawn(
mut commands: Commands, mut commands: Commands,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>, query: Query<&Transform, With<SpawnPoint>>,
assets: Res<GameAssets>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
) { ) {
let Some(spawn) = query.iter().next() else { let Some(spawn) = query.iter().next() else {
@@ -65,9 +61,6 @@ fn spawn(
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.)); let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
let mesh = asset_server
.load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb"));
let gravity = Vector::NEG_Y * 40.0; let gravity = Vector::NEG_Y * 40.0;
let collider = Collider::capsule(0.9, 1.2); let collider = Collider::capsule(0.9, 1.2);
@@ -94,21 +87,13 @@ fn spawn(
.with_children(|parent| { .with_children(|parent| {
parent parent
.spawn(( .spawn((
Name::from("body rig"), Name::new("player-rig"),
Transform::default(),
Visibility::default(),
PlayerBodyMesh, PlayerBodyMesh,
CameraArmRotation, CameraArmRotation,
Transform::from_translation(Vec3::new(0., -1.45, 0.))
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI))
.with_scale(Vec3::splat(1.4)),
SceneRoot(assets.mesh_alien.clone()),
)) ))
.with_child(( .with_child(AnimatedCharacter(0));
Name::from("head"),
PlayerHeadMesh,
Transform::from_translation(Vec3::new(0., 1.6, 0.))
.with_scale(Vec3::splat(0.7)),
SceneRoot(mesh),
));
}); });
commands.spawn(( commands.spawn((
@@ -176,29 +161,33 @@ fn setup_animations_marker_for_player(
for ancestor in parent_query.iter_ancestors(entity) { for ancestor in parent_query.iter_ancestors(entity) {
if player.contains(ancestor) { if player.contains(ancestor) {
commands.entity(entity).insert(PlayerAnimations); commands.entity(entity).insert(PlayerAnimations);
return;
} }
} }
} }
} }
fn toggle_animation( fn toggle_animation(
animations: Res<Animations>,
mut transitions: Query< mut transitions: Query<
(&mut AnimationTransitions, &mut AnimationPlayer), (
&mut AnimationTransitions,
&mut AnimationPlayer,
&CharacterAnimations,
),
With<PlayerAnimations>, With<PlayerAnimations>,
>, >,
movement: Res<PlayerMovement>, movement: Res<PlayerMovement>,
) { ) {
if movement.is_changed() { if movement.is_changed() {
let index = if movement.any_direction { 0 } else { 1 }; for (mut transition, mut player, animations) in &mut transitions {
let index = if movement.any_direction {
animations.run
} else {
animations.idle
};
for (mut transition, mut player) in &mut transitions {
transition transition
.play( .play(&mut player, index, Duration::from_millis(100))
&mut player,
animations.animations[index],
Duration::from_millis(100),
)
.repeat(); .repeat();
} }
} }
@@ -207,13 +196,12 @@ fn toggle_animation(
fn on_update_head( fn on_update_head(
trigger: Trigger<HeadChanged>, trigger: Trigger<HeadChanged>,
mut commands: Commands, mut commands: Commands,
asset_server: Res<AssetServer>, body_mesh: Query<Entity, With<PlayerBodyMesh>>,
head: Query<Entity, With<PlayerHeadMesh>>,
mut player_head: Query<&mut ActiveHead, With<Player>>, mut player_head: Query<&mut ActiveHead, With<Player>>,
head_db: Res<HeadsDatabase>, head_db: Res<HeadsDatabase>,
audio_assets: Res<AudioAssets>, audio_assets: Res<AudioAssets>,
) { ) {
let Ok(head) = head.get_single() else { let Ok(body_mesh) = body_mesh.get_single() else {
return; return;
}; };
@@ -227,27 +215,21 @@ fn on_update_head(
commands.trigger(PlaySound::Head(head_str.to_string())); commands.trigger(PlaySound::Head(head_str.to_string()));
//TODO: load from dynamic asset collection? or keep lazy? commands.entity(body_mesh).despawn_descendants();
let mesh = asset_server
.load(GltfAssetLabel::Scene(0).from_asset(format!("models/heads/{}.glb", head_str)));
commands.entity(head).despawn_descendants(); commands
commands.entity(head).insert(SceneRoot(mesh)); .entity(body_mesh)
.with_child(AnimatedCharacter(trigger.0));
//TODO: make part of full character mesh later //TODO: make part of full character mesh later
if head_db.head_stats(trigger.0).controls == HeadControls::Plane { if head_db.head_stats(trigger.0).controls == HeadControls::Plane {
let mesh = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/mig.glb")); commands.entity(body_mesh).with_child((
Name::new("sfx"),
commands AudioPlayer::new(audio_assets.jet.clone()),
.entity(head) PlaybackSettings {
.with_child((Transform::from_xyz(0., -1., 0.), SceneRoot(mesh))) mode: bevy::audio::PlaybackMode::Loop,
.with_child(( ..Default::default()
Name::new("sfx"), },
AudioPlayer::new(audio_assets.jet.clone()), ));
PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
..Default::default()
},
));
} }
} }

View File

@@ -117,11 +117,8 @@ pub struct EnemySpawn {
impl EnemySpawn { impl EnemySpawn {
fn on_add(mut world: DeferredWorld, entity: Entity, _id: ComponentId) { fn on_add(mut world: DeferredWorld, entity: Entity, _id: ComponentId) {
let Some(asset_server) = world.get_resource::<AssetServer>() else { //TODO: figure out why this crashes if removed
return; let Some(_assets) = world.get_resource::<GameAssets>() else {
};
let Some(assets) = world.get_resource::<GameAssets>() else {
return; return;
}; };
@@ -135,38 +132,17 @@ impl EnemySpawn {
let mut this_transform = *this_transform; let mut this_transform = *this_transform;
this_transform.translation += Vec3::new(0., 1., 0.); this_transform.translation += Vec3::new(0., 1., 0.);
let mesh = assets.mesh_alien.clone();
let head_mesh = asset_server
.load(GltfAssetLabel::Scene(0).from_asset(format!("models/heads/{}.glb", this.head)));
let head = this.head.clone(); let head = this.head.clone();
world world.commands().entity(entity).insert((
.commands() this_transform,
.entity(entity) Name::from(format!("enemy [{}]", head)),
.insert(( Visibility::default(),
this_transform, RigidBody::Dynamic,
Name::from(format!("enemy [{}]", head)), Collider::capsule(0.4, 2.),
Visibility::default(), CollisionLayers::new(LayerMask(GameLayer::Npc.to_bits()), LayerMask::ALL),
RigidBody::Dynamic, LockedAxes::new().lock_rotation_z().lock_rotation_x(),
Collider::capsule(0.4, 2.), ));
CollisionLayers::new(LayerMask(GameLayer::Npc.to_bits()), LayerMask::ALL),
LockedAxes::new().lock_rotation_z().lock_rotation_x(),
))
.with_children(|parent| {
parent
.spawn((
Transform::from_translation(Vec3::new(0., 1., 0.)),
SceneRoot(head_mesh),
))
.with_child((
Visibility::default(),
Transform::from_translation(Vec3::new(0., -2.4, 0.))
.with_scale(Vec3::splat(1.5)),
SceneRoot(mesh),
));
});
} }
} }