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