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

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>,
#[asset(path = "sfx/abilities/jet.ogg")]
pub jet: Handle<AudioSource>,
#[asset(path = "sfx/ui/backpack_open.ogg")]
pub backpack_open: Handle<AudioSource>,
#[asset(path = "sfx/ui/backpack_close.ogg")]
@@ -91,21 +90,11 @@ pub struct GameAssets {
#[asset(path = "models/cash.glb#Scene0")]
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))]
pub projectiles: HashMap<AssetFileName, Handle<Gltf>>,
#[asset(path = "models/characters", collection(mapped, typed))]
pub characters: HashMap<AssetFileName, Handle<Gltf>>,
}
pub struct LoadingPlugin;

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
use crate::{
GameState,
alien::Animations,
camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent},
character::{AnimatedCharacter, CharacterAnimations},
control::controller_common::{CharacterControllerBundle, PlayerMovement},
global_observer,
head::ActiveHead,
heads::{ActiveHeads, HeadChanged, HeadState},
heads_database::{HeadControls, HeadsDatabase},
hitpoints::Hitpoints,
loading_assets::{AudioAssets, GameAssets},
loading_assets::AudioAssets,
physics_layers::GameLayer,
sounds::PlaySound,
tb_entities::SpawnPoint,
@@ -29,9 +29,6 @@ pub struct Player;
#[derive(Component, Default)]
struct PlayerAnimations;
#[derive(Component, Default)]
struct PlayerHeadMesh;
#[derive(Component, Default)]
pub struct PlayerBodyMesh;
@@ -56,7 +53,6 @@ fn spawn(
mut commands: Commands,
asset_server: Res<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>,
assets: Res<GameAssets>,
heads_db: Res<HeadsDatabase>,
) {
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 mesh = asset_server
.load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb"));
let gravity = Vector::NEG_Y * 40.0;
let collider = Collider::capsule(0.9, 1.2);
@@ -94,21 +87,13 @@ fn spawn(
.with_children(|parent| {
parent
.spawn((
Name::from("body rig"),
Name::new("player-rig"),
Transform::default(),
Visibility::default(),
PlayerBodyMesh,
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((
Name::from("head"),
PlayerHeadMesh,
Transform::from_translation(Vec3::new(0., 1.6, 0.))
.with_scale(Vec3::splat(0.7)),
SceneRoot(mesh),
));
.with_child(AnimatedCharacter(0));
});
commands.spawn((
@@ -176,29 +161,33 @@ fn setup_animations_marker_for_player(
for ancestor in parent_query.iter_ancestors(entity) {
if player.contains(ancestor) {
commands.entity(entity).insert(PlayerAnimations);
return;
}
}
}
}
fn toggle_animation(
animations: Res<Animations>,
mut transitions: Query<
(&mut AnimationTransitions, &mut AnimationPlayer),
(
&mut AnimationTransitions,
&mut AnimationPlayer,
&CharacterAnimations,
),
With<PlayerAnimations>,
>,
movement: Res<PlayerMovement>,
) {
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
.play(
&mut player,
animations.animations[index],
Duration::from_millis(100),
)
.play(&mut player, index, Duration::from_millis(100))
.repeat();
}
}
@@ -207,13 +196,12 @@ fn toggle_animation(
fn on_update_head(
trigger: Trigger<HeadChanged>,
mut commands: Commands,
asset_server: Res<AssetServer>,
head: Query<Entity, With<PlayerHeadMesh>>,
body_mesh: Query<Entity, With<PlayerBodyMesh>>,
mut player_head: Query<&mut ActiveHead, With<Player>>,
head_db: Res<HeadsDatabase>,
audio_assets: Res<AudioAssets>,
) {
let Ok(head) = head.get_single() else {
let Ok(body_mesh) = body_mesh.get_single() else {
return;
};
@@ -227,27 +215,21 @@ fn on_update_head(
commands.trigger(PlaySound::Head(head_str.to_string()));
//TODO: load from dynamic asset collection? or keep lazy?
let mesh = asset_server
.load(GltfAssetLabel::Scene(0).from_asset(format!("models/heads/{}.glb", head_str)));
commands.entity(body_mesh).despawn_descendants();
commands.entity(head).despawn_descendants();
commands.entity(head).insert(SceneRoot(mesh));
commands
.entity(body_mesh)
.with_child(AnimatedCharacter(trigger.0));
//TODO: make part of full character mesh later
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(head)
.with_child((Transform::from_xyz(0., -1., 0.), SceneRoot(mesh)))
.with_child((
Name::new("sfx"),
AudioPlayer::new(audio_assets.jet.clone()),
PlaybackSettings {
mode: bevy::audio::PlaybackMode::Loop,
..Default::default()
},
));
commands.entity(body_mesh).with_child((
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 {
fn on_add(mut world: DeferredWorld, entity: Entity, _id: ComponentId) {
let Some(asset_server) = world.get_resource::<AssetServer>() else {
return;
};
let Some(assets) = world.get_resource::<GameAssets>() else {
//TODO: figure out why this crashes if removed
let Some(_assets) = world.get_resource::<GameAssets>() else {
return;
};
@@ -135,38 +132,17 @@ impl EnemySpawn {
let mut this_transform = *this_transform;
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();
world
.commands()
.entity(entity)
.insert((
this_transform,
Name::from(format!("enemy [{}]", head)),
Visibility::default(),
RigidBody::Dynamic,
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),
));
});
world.commands().entity(entity).insert((
this_transform,
Name::from(format!("enemy [{}]", head)),
Visibility::default(),
RigidBody::Dynamic,
Collider::capsule(0.4, 2.),
CollisionLayers::new(LayerMask(GameLayer::Npc.to_bits()), LayerMask::ALL),
LockedAxes::new().lock_rotation_z().lock_rotation_x(),
));
}
}