new character format
This commit is contained in:
BIN
assets/models/characters/angry demonstrator.glb
Normal file
BIN
assets/models/characters/angry demonstrator.glb
Normal file
Binary file not shown.
BIN
assets/models/characters/field medic.glb
Normal file
BIN
assets/models/characters/field medic.glb
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/models/characters/mig pilot.glb
Normal file
BIN
assets/models/characters/mig pilot.glb
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
81
src/alien.rs
81
src/alien.rs
@@ -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
102
src/character.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user