Compare commits
No commits in common. "ecfa9b586428f7afbeb6b8af5a0a41fc6cea795b" and "aef303ca71e6e15869700545cc91fcaae8cb8ed7" have entirely different histories.
ecfa9b5864
...
aef303ca71
13 changed files with 261 additions and 195 deletions
|
@ -37,7 +37,7 @@ impl Default for MovementSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
accel: 20.0,
|
accel: 20.0,
|
||||||
gravity: 9.8,
|
gravity: 4.8,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,9 +53,9 @@ pub struct CatControllerSettings {
|
||||||
impl Default for CatControllerSettings {
|
impl Default for CatControllerSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
kp: 1200.0,
|
kp: 80.0,
|
||||||
kd: 10.0,
|
kd: 10.0,
|
||||||
ki: 50.0,
|
ki: 0.4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use avian3d::prelude::{PhysicsPlugins, SubstepCount};
|
use avian3d::prelude::{PhysicsPlugins, SubstepCount};
|
||||||
use bevy::{
|
use bevy::{
|
||||||
app::FixedUpdate,
|
app::Update,
|
||||||
diagnostic::FrameTimeDiagnosticsPlugin,
|
diagnostic::FrameTimeDiagnosticsPlugin,
|
||||||
ecs::reflect::ReflectResource,
|
ecs::reflect::ReflectResource,
|
||||||
prelude::{App, IntoSystemConfigs, Plugin, Resource},
|
prelude::{App, IntoSystemConfigs, Plugin, Resource},
|
||||||
|
@ -29,10 +29,15 @@ impl Plugin for CyberActionPlugin {
|
||||||
.register_type::<CyberLean>()
|
.register_type::<CyberLean>()
|
||||||
.register_type::<CatControllerSettings>()
|
.register_type::<CatControllerSettings>()
|
||||||
.add_plugins((PhysicsPlugins::default(), FrameTimeDiagnosticsPlugin))
|
.add_plugins((PhysicsPlugins::default(), FrameTimeDiagnosticsPlugin))
|
||||||
.insert_resource(SubstepCount(12))
|
.insert_resource(SubstepCount(24))
|
||||||
.add_systems(
|
.add_systems(
|
||||||
FixedUpdate,
|
Update,
|
||||||
(set_gravity, calculate_lean, apply_lean).chain(),
|
(
|
||||||
|
gravity,
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
debug_bodies,
|
||||||
|
(cyber_lean, suspension, falling_cat, input_forces).after(clear_forces),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,18 @@ use std::f32::consts::{FRAC_PI_3, FRAC_PI_4};
|
||||||
|
|
||||||
use avian3d::{
|
use avian3d::{
|
||||||
dynamics::solver::xpbd::AngularConstraint,
|
dynamics::solver::xpbd::AngularConstraint,
|
||||||
prelude::{ExternalTorque, FixedJoint, Gravity, LinearVelocity, RigidBodyQuery},
|
prelude::{
|
||||||
|
ColliderMassProperties, ExternalForce, ExternalTorque, Gravity, LinearVelocity,
|
||||||
|
PrismaticJoint, RigidBodyQuery,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
#[cfg(feature = "inspector")]
|
#[cfg(feature = "inspector")]
|
||||||
use bevy::prelude::Gizmos;
|
use bevy::prelude::Entity;
|
||||||
use bevy::prelude::{Quat, Query, Res, ResMut, Time, Transform, Vec3, With, Without};
|
use bevy::prelude::{Quat, Query, Res, ResMut, Time, Transform, Vec3, With, Without};
|
||||||
|
|
||||||
use super::{CatControllerSettings, CatControllerState, CyberLean, MovementSettings};
|
use super::{CatControllerSettings, CatControllerState, CyberLean, MovementSettings};
|
||||||
use crate::{
|
use crate::{
|
||||||
bike::{CyberBikeBody, CyberFork, CyberSteering, CyberWheel},
|
bike::{CyberBikeBody, CyberFork, CyberSpring, CyberSteering, CyberWheel, WheelConfig},
|
||||||
input::InputState,
|
input::InputState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,17 +38,47 @@ fn rotate_point(pt: &Vec3, rot: &Quat) -> Vec3 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The gravity vector points from the cyberbike to the center of the planet.
|
/// The gravity vector points from the cyberbike to the center of the planet.
|
||||||
pub(super) fn set_gravity(
|
pub(super) fn gravity(
|
||||||
xform: Query<&Transform, With<CyberBikeBody>>,
|
xform: Query<&Transform, With<CyberBikeBody>>,
|
||||||
|
|
||||||
settings: Res<MovementSettings>,
|
settings: Res<MovementSettings>,
|
||||||
mut gravity: ResMut<Gravity>,
|
mut gravity: ResMut<Gravity>,
|
||||||
) {
|
) {
|
||||||
let xform = xform.single();
|
let xform = xform.single();
|
||||||
*gravity = Gravity(xform.translation.normalize() * -settings.gravity);
|
*gravity = Gravity(xform.translation.normalize() * -settings.gravity);
|
||||||
|
|
||||||
|
// clear all external forces
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_forces(mut forces: Query<(&mut ExternalForce, &mut ExternalTorque)>) {
|
||||||
|
for (mut force, mut torque) in forces.iter_mut() {
|
||||||
|
force.clear();
|
||||||
|
torque.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn suspension(
|
||||||
|
movment_settings: Res<MovementSettings>,
|
||||||
|
wheel_config: Res<WheelConfig>,
|
||||||
|
mass: Query<&ColliderMassProperties, With<CyberBikeBody>>,
|
||||||
|
mut axels: Query<(&mut ExternalForce, &CyberSpring, &Transform)>,
|
||||||
|
) {
|
||||||
|
let mass = if let Ok(mass) = mass.get_single() {
|
||||||
|
mass.mass.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let gravity = movment_settings.gravity;
|
||||||
|
let mag = -wheel_config.stiffness * mass * gravity;
|
||||||
|
for (mut force, spring, xform) in axels.iter_mut() {
|
||||||
|
let spring = mag * spring.0;
|
||||||
|
let spring = xform.translation + spring;
|
||||||
|
let _ = force.apply_force(spring);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The desired lean angle, given steering input and speed.
|
/// The desired lean angle, given steering input and speed.
|
||||||
pub(super) fn calculate_lean(
|
pub(super) fn cyber_lean(
|
||||||
bike_state: Query<(&LinearVelocity, &Transform), With<CyberBikeBody>>,
|
bike_state: Query<(&LinearVelocity, &Transform), With<CyberBikeBody>>,
|
||||||
wheels: Query<&Transform, With<CyberWheel>>,
|
wheels: Query<&Transform, With<CyberWheel>>,
|
||||||
input: Res<InputState>,
|
input: Res<InputState>,
|
||||||
|
@ -63,7 +96,7 @@ pub(super) fn calculate_lean(
|
||||||
let v2_r = v_squared / radius;
|
let v2_r = v_squared / radius;
|
||||||
let tan_theta = (v2_r / gravity).clamp(-FRAC_PI_3, FRAC_PI_3);
|
let tan_theta = (v2_r / gravity).clamp(-FRAC_PI_3, FRAC_PI_3);
|
||||||
|
|
||||||
if tan_theta.is_normal() {
|
if tan_theta.is_finite() && !tan_theta.is_subnormal() {
|
||||||
lean.lean = tan_theta.atan().clamp(-FRAC_PI_4, FRAC_PI_4);
|
lean.lean = tan_theta.atan().clamp(-FRAC_PI_4, FRAC_PI_4);
|
||||||
} else {
|
} else {
|
||||||
lean.lean = 0.0;
|
lean.lean = 0.0;
|
||||||
|
@ -71,12 +104,11 @@ pub(super) fn calculate_lean(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PID-based controller for stabilizing attitude; keeps the cyberbike upright.
|
/// PID-based controller for stabilizing attitude; keeps the cyberbike upright.
|
||||||
pub(super) fn apply_lean(
|
pub(super) fn falling_cat(
|
||||||
mut bike_query: Query<(&Transform, &mut ExternalTorque, &mut CatControllerState)>,
|
mut bike_query: Query<(&Transform, &mut ExternalTorque, &mut CatControllerState)>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
settings: Res<CatControllerSettings>,
|
settings: Res<CatControllerSettings>,
|
||||||
lean: Res<CyberLean>,
|
lean: Res<CyberLean>,
|
||||||
#[cfg(feature = "inspector")] mut gizmos: Gizmos,
|
|
||||||
) {
|
) {
|
||||||
let (xform, mut torque, mut control_vars) = bike_query.single_mut();
|
let (xform, mut torque, mut control_vars) = bike_query.single_mut();
|
||||||
let world_up = xform.translation.normalize();
|
let world_up = xform.translation.normalize();
|
||||||
|
@ -93,53 +125,73 @@ pub(super) fn apply_lean(
|
||||||
let (derivative, integral) = control_vars.update_roll(roll_error, time.delta_seconds());
|
let (derivative, integral) = control_vars.update_roll(roll_error, time.delta_seconds());
|
||||||
let mag =
|
let mag =
|
||||||
(settings.kp * roll_error) + (settings.ki * integral) + (settings.kd * derivative);
|
(settings.kp * roll_error) + (settings.ki * integral) + (settings.kd * derivative);
|
||||||
if mag.is_normal() {
|
if mag.is_finite() {
|
||||||
let tork = mag * *xform.back();
|
torque.apply_torque(*xform.back() * mag);
|
||||||
torque.apply_torque(tork);
|
|
||||||
#[cfg(feature = "inspector")]
|
|
||||||
gizmos.arrow(
|
|
||||||
xform.translation + Vec3::Y,
|
|
||||||
xform.translation + tork + Vec3::Y,
|
|
||||||
bevy::color::Color::WHITE,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply forces to the cyberbike as a result of input.
|
/// Apply forces to the cyberbike as a result of input.
|
||||||
pub(super) fn apply_inputs(
|
pub(super) fn input_forces(
|
||||||
settings: Res<MovementSettings>,
|
settings: Res<MovementSettings>,
|
||||||
input: Res<InputState>,
|
input: Res<InputState>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
fork: Query<(&FixedJoint, &CyberFork), With<CyberSteering>>,
|
fork: Query<&PrismaticJoint, With<CyberFork>>,
|
||||||
mut hub: Query<(RigidBodyQuery, &Transform), With<CyberSteering>>,
|
mut axle: Query<(RigidBodyQuery, &Transform), With<CyberSteering>>,
|
||||||
mut braking_query: Query<(&Transform, &mut ExternalTorque), With<CyberWheel>>,
|
mut braking_query: Query<&mut ExternalTorque, With<CyberWheel>>,
|
||||||
mut body_query: Query<RigidBodyQuery, (With<CyberBikeBody>, Without<CyberSteering>)>,
|
mut body_query: Query<
|
||||||
|
(
|
||||||
|
RigidBodyQuery,
|
||||||
|
&Transform,
|
||||||
|
&ColliderMassProperties,
|
||||||
|
&mut ExternalForce,
|
||||||
|
),
|
||||||
|
(With<CyberBikeBody>, Without<CyberSteering>),
|
||||||
|
>,
|
||||||
) {
|
) {
|
||||||
let Ok(mut bike) = body_query.get_single_mut() else {
|
let Ok((mut bike, xform, mass, mut forces)) = body_query.get_single_mut() else {
|
||||||
bevy::log::debug!("no bike body found");
|
bevy::log::debug!("no bike body found");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let dt = time.delta_seconds();
|
let dt = time.delta_seconds();
|
||||||
|
|
||||||
// brake + thrust
|
// thrust
|
||||||
for (xform, mut motor) in braking_query.iter_mut() {
|
let thrust = xform.forward() * input.throttle * settings.accel;
|
||||||
let factor = input.throttle * settings.accel;
|
let point = xform.translation + *xform.back();
|
||||||
|
|
||||||
let target = dt * factor;
|
let _ = *forces.apply_force_at_point(dt * thrust, point, mass.center_of_mass.0);
|
||||||
let torque = target * *xform.right();
|
|
||||||
|
// brake + thrust
|
||||||
|
for mut motor in braking_query.iter_mut() {
|
||||||
|
let factor = if input.brake {
|
||||||
|
500.00
|
||||||
|
} else {
|
||||||
|
input.throttle * settings.accel
|
||||||
|
};
|
||||||
|
let speed = if input.brake { 0.0 } else { -70.0 };
|
||||||
|
let target = dt * factor * speed;
|
||||||
|
let torque = target * Vec3::X;
|
||||||
motor.apply_torque(torque);
|
motor.apply_torque(torque);
|
||||||
}
|
}
|
||||||
|
|
||||||
// steering
|
// steering
|
||||||
let _angle = yaw_to_angle(input.yaw);
|
let _angle = yaw_to_angle(input.yaw);
|
||||||
let (mut axle, xform) = hub.single_mut();
|
let (mut axle, xform) = axle.single_mut();
|
||||||
|
|
||||||
let cur_rot = xform.rotation;
|
let cur_rot = xform.rotation;
|
||||||
let (fork, CyberFork(axis)) = fork.single();
|
let fork = fork.single();
|
||||||
let new = Quat::from_axis_angle(*axis, _angle);
|
let axis = fork.free_axis.normalize();
|
||||||
|
let new = Quat::from_axis_angle(axis, _angle);
|
||||||
let diff = (new - cur_rot).to_scaled_axis();
|
let diff = (new - cur_rot).to_scaled_axis();
|
||||||
let mut lagrange = 1.0;
|
let mut compliance = 1.0;
|
||||||
|
|
||||||
fork.align_orientation(&mut axle, &mut bike, diff, &mut lagrange, 0.0, dt);
|
fork.align_orientation(&mut axle, &mut bike, diff, &mut compliance, 1.0, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
pub(super) fn debug_bodies(bodies: Query<(Entity, RigidBodyQuery)>) {
|
||||||
|
for (entity, rbq) in bodies.iter().filter(|(_, r)| r.rb.is_dynamic()) {
|
||||||
|
let line = format!("{entity:?} at {:?}", rbq.current_position());
|
||||||
|
bevy::log::info!(line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
core::Name,
|
|
||||||
prelude::{AssetServer, BuildChildren, Commands, Quat, Res, SpatialBundle, Transform, Vec3},
|
prelude::{AssetServer, BuildChildren, Commands, Quat, Res, SpatialBundle, Transform, Vec3},
|
||||||
scene::SceneBundle,
|
scene::SceneBundle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{spawn_wheels, CyberBikeBody, Meshterial, WheelConfig};
|
use super::{spawn_wheels, CyberBikeBody, Meshterial, WheelConfig, BIKE_BODY_COLLISION_GROUP};
|
||||||
use crate::{action::CatControllerState, planet::PLANET_RADIUS, ColliderGroups};
|
use crate::{action::CatControllerState, planet::PLANET_RADIUS};
|
||||||
|
|
||||||
pub(super) fn spawn_cyberbike(
|
pub(super) fn spawn_cyberbike(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
|
@ -33,8 +32,8 @@ pub(super) fn spawn_cyberbike(
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let bike_collision_group =
|
let (membership, filter) = BIKE_BODY_COLLISION_GROUP;
|
||||||
CollisionLayers::new(ColliderGroups::BikeBody, ColliderGroups::Planet);
|
let bike_collision_group = CollisionLayers::new(membership, filter);
|
||||||
|
|
||||||
let scene = asset_server.load("cb-no-y_up.glb#Scene0");
|
let scene = asset_server.load("cb-no-y_up.glb#Scene0");
|
||||||
|
|
||||||
|
@ -43,7 +42,6 @@ pub(super) fn spawn_cyberbike(
|
||||||
let bike = commands
|
let bike = commands
|
||||||
.spawn(SpatialBundle::from_transform(xform))
|
.spawn(SpatialBundle::from_transform(xform))
|
||||||
.insert((
|
.insert((
|
||||||
Name::new("bike body"),
|
|
||||||
RigidBody::Dynamic,
|
RigidBody::Dynamic,
|
||||||
body_collider,
|
body_collider,
|
||||||
bike_collision_group,
|
bike_collision_group,
|
||||||
|
@ -52,12 +50,13 @@ pub(super) fn spawn_cyberbike(
|
||||||
SleepingDisabled,
|
SleepingDisabled,
|
||||||
CyberBikeBody,
|
CyberBikeBody,
|
||||||
CatControllerState::default(),
|
CatControllerState::default(),
|
||||||
ColliderDensity(20.0),
|
ColliderDensity(0.6),
|
||||||
LinearDamping(0.1),
|
LinearDamping(0.1),
|
||||||
AngularDamping(2.0),
|
AngularDamping(2.0),
|
||||||
LinearVelocity::ZERO,
|
LinearVelocity::ZERO,
|
||||||
AngularVelocity::ZERO,
|
AngularVelocity::ZERO,
|
||||||
ExternalTorque::ZERO.with_persistence(false),
|
ExternalForce::ZERO,
|
||||||
|
ExternalTorque::ZERO,
|
||||||
))
|
))
|
||||||
.with_children(|rider| {
|
.with_children(|rider| {
|
||||||
rider.spawn(SceneBundle {
|
rider.spawn(SceneBundle {
|
||||||
|
@ -67,7 +66,5 @@ pub(super) fn spawn_cyberbike(
|
||||||
})
|
})
|
||||||
.id();
|
.id();
|
||||||
|
|
||||||
bevy::log::info!("bike body: {bike:?}");
|
|
||||||
|
|
||||||
spawn_wheels(&mut commands, bike, &xform, &wheel_conf, &mut meshterials);
|
spawn_wheels(&mut commands, bike, &xform, &wheel_conf, &mut meshterials);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,10 @@ pub struct CyberSteering;
|
||||||
pub struct CyberWheel;
|
pub struct CyberWheel;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Component)]
|
#[derive(Clone, Debug, Component)]
|
||||||
pub struct CyberFork(pub Vec3);
|
pub struct CyberSpring(pub Vec3);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Component)]
|
||||||
|
pub struct CyberFork;
|
||||||
|
|
||||||
#[derive(Resource, Reflect)]
|
#[derive(Resource, Reflect)]
|
||||||
#[reflect(Resource)]
|
#[reflect(Resource)]
|
||||||
|
@ -37,15 +40,15 @@ impl Default for WheelConfig {
|
||||||
Self {
|
Self {
|
||||||
front_forward: 0.8,
|
front_forward: 0.8,
|
||||||
rear_back: 1.0,
|
rear_back: 1.0,
|
||||||
y: -0.5,
|
y: -0.1,
|
||||||
limits: [-0.5, 0.1],
|
limits: [-0.5, 0.1],
|
||||||
stiffness: 3.0,
|
stiffness: 3.0,
|
||||||
damping: 8.0,
|
damping: 8.0,
|
||||||
radius: 0.40,
|
radius: 0.25,
|
||||||
thickness: 0.15,
|
thickness: 0.11,
|
||||||
friction: 0.9,
|
friction: 1.2,
|
||||||
restitution: 0.5,
|
restitution: 0.95,
|
||||||
density: 30.0,
|
density: 0.05,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ use bevy::{
|
||||||
pub(crate) use self::components::*;
|
pub(crate) use self::components::*;
|
||||||
use self::{body::spawn_cyberbike, wheels::spawn_wheels};
|
use self::{body::spawn_cyberbike, wheels::spawn_wheels};
|
||||||
|
|
||||||
pub const BIKE_BODY_COLLISION_GROUP: (u32, u32) = (1, 1);
|
pub const BIKE_BODY_COLLISION_GROUP: (u32, u32) = (0b01, 0b01);
|
||||||
pub const BIKE_WHEEL_COLLISION_GROUP: (u32, u32) = (2, 2);
|
pub const BIKE_WHEEL_COLLISION_GROUP: (u32, u32) = (0b10, 0b10);
|
||||||
|
|
||||||
type Meshterial<'a> = (
|
type Meshterial<'a> = (
|
||||||
ResMut<'a, Assets<Mesh>>,
|
ResMut<'a, Assets<Mesh>>,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::{Torus as Tire, *};
|
||||||
|
|
||||||
use super::{CyberFork, CyberSteering, CyberWheel, Meshterial, WheelConfig};
|
use super::{
|
||||||
use crate::ColliderGroups;
|
CyberFork, CyberSpring, CyberSteering, CyberWheel, Meshterial, WheelConfig,
|
||||||
|
BIKE_WHEEL_COLLISION_GROUP,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn spawn_wheels(
|
pub fn spawn_wheels(
|
||||||
commands: &mut Commands,
|
commands: &mut Commands,
|
||||||
|
@ -11,11 +13,21 @@ pub fn spawn_wheels(
|
||||||
conf: &WheelConfig,
|
conf: &WheelConfig,
|
||||||
meshterials: &mut Meshterial,
|
meshterials: &mut Meshterial,
|
||||||
) {
|
) {
|
||||||
|
let (membership, filter) = BIKE_WHEEL_COLLISION_GROUP;
|
||||||
|
let wheels_collision_group = CollisionLayers::new(membership, filter);
|
||||||
let wheel_y = conf.y;
|
let wheel_y = conf.y;
|
||||||
|
let not_sleeping = SleepingDisabled;
|
||||||
|
let ccd = SweptCcd::NON_LINEAR.include_dynamic(false);
|
||||||
|
let limits = conf.limits;
|
||||||
let (meshes, materials) = meshterials;
|
let (meshes, materials) = meshterials;
|
||||||
let rake_vec: Vec3 = Vec3::new(0.0, 1.0, 0.57).normalize(); // about 30 degrees of rake
|
let rake_vec: Vec3 = Vec3::new(0.0, 1.0, 0.57).normalize(); // about 30 degrees of rake
|
||||||
|
|
||||||
|
let friction = Friction {
|
||||||
|
dynamic_coefficient: conf.friction,
|
||||||
|
static_coefficient: conf.friction,
|
||||||
|
combine_rule: CoefficientCombine::Average,
|
||||||
|
};
|
||||||
|
|
||||||
let mut wheel_poses = Vec::with_capacity(2);
|
let mut wheel_poses = Vec::with_capacity(2);
|
||||||
|
|
||||||
// front
|
// front
|
||||||
|
@ -34,34 +46,107 @@ pub fn spawn_wheels(
|
||||||
wheel_poses.push((offset, None));
|
wheel_poses.push((offset, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mesh, collider) = gen_tire(conf);
|
|
||||||
for (offset, steering) in wheel_poses {
|
for (offset, steering) in wheel_poses {
|
||||||
let hub = wheels_helper(
|
let (mesh, collider) = gen_tires(conf);
|
||||||
commands,
|
|
||||||
meshes,
|
|
||||||
materials,
|
|
||||||
offset + xform.translation,
|
|
||||||
mesh.clone(),
|
|
||||||
collider.clone(),
|
|
||||||
conf,
|
|
||||||
);
|
|
||||||
|
|
||||||
let fork = commands
|
let material = StandardMaterial {
|
||||||
.spawn(FixedJoint::new(hub, bike).with_local_anchor_2(offset))
|
base_color: Color::linear_rgba(0.01, 0.01, 0.01, 1.0),
|
||||||
|
alpha_mode: AlphaMode::Opaque,
|
||||||
|
perceptual_roughness: 0.5,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let pbr_bundle = PbrBundle {
|
||||||
|
material: materials.add(material),
|
||||||
|
mesh: meshes.add(mesh),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let suspension_axis = if steering.is_some() {
|
||||||
|
rake_vec
|
||||||
|
} else {
|
||||||
|
Vec3::Y
|
||||||
|
};
|
||||||
|
|
||||||
|
let wheel = commands
|
||||||
|
.spawn(pbr_bundle)
|
||||||
|
.insert((
|
||||||
|
collider,
|
||||||
|
ccd,
|
||||||
|
not_sleeping,
|
||||||
|
wheels_collision_group,
|
||||||
|
friction,
|
||||||
|
CyberWheel,
|
||||||
|
ExternalForce::default(),
|
||||||
|
Restitution::new(conf.restitution),
|
||||||
|
SpatialBundle::default(),
|
||||||
|
RigidBody::Dynamic,
|
||||||
|
ColliderDensity(0.1),
|
||||||
|
AngularVelocity::ZERO,
|
||||||
|
ExternalTorque::ZERO,
|
||||||
|
))
|
||||||
|
.insert(SpatialBundle::from_transform(
|
||||||
|
xform.with_translation(xform.translation + offset),
|
||||||
|
))
|
||||||
.id();
|
.id();
|
||||||
if let Some(steering) = steering {
|
|
||||||
let axis = CyberFork(rake_vec);
|
let spring = CyberSpring(suspension_axis);
|
||||||
commands.entity(fork).insert((steering, axis));
|
let axel = if let Some(steering) = steering {
|
||||||
commands.entity(hub).insert(steering);
|
commands.spawn((
|
||||||
|
RigidBody::Dynamic,
|
||||||
|
ExternalForce::ZERO,
|
||||||
|
steering,
|
||||||
|
spring,
|
||||||
|
Collider::sphere(0.1),
|
||||||
|
ColliderDensity(0.1),
|
||||||
|
CollisionLayers::from_bits(0, 0),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
commands.spawn((
|
||||||
|
RigidBody::Dynamic,
|
||||||
|
ExternalForce::ZERO,
|
||||||
|
spring,
|
||||||
|
Collider::sphere(0.1),
|
||||||
|
ColliderDensity(0.1),
|
||||||
|
CollisionLayers::from_bits(0, 0),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
.insert(SpatialBundle::from_transform(
|
||||||
|
xform.with_translation(xform.translation + offset),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
let axel_joint = RevoluteJoint::new(axel, wheel).with_aligned_axis(Vec3::NEG_X);
|
||||||
|
commands.spawn(axel_joint);
|
||||||
|
|
||||||
|
// suspension and steering:
|
||||||
|
if steering.is_some() {
|
||||||
|
let joint = PrismaticJoint::new(axel, bike)
|
||||||
|
.with_free_axis(suspension_axis)
|
||||||
|
.with_local_anchor_1(Vec3::new(0.0, 0.0, 0.1))
|
||||||
|
.with_local_anchor_2(offset)
|
||||||
|
.with_limits(limits[0], limits[1]);
|
||||||
|
commands.spawn((joint, CyberFork));
|
||||||
|
} else {
|
||||||
|
let joint = PrismaticJoint::new(axel, bike)
|
||||||
|
.with_free_axis(suspension_axis)
|
||||||
|
.with_local_anchor_2(offset)
|
||||||
|
.with_limits(limits[0], limits[1]);
|
||||||
|
commands.spawn(joint);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_tire(conf: &WheelConfig) -> (Mesh, Collider) {
|
// do mesh shit
|
||||||
let outer_radius = conf.radius;
|
fn gen_tires(conf: &WheelConfig) -> (Mesh, Collider) {
|
||||||
let inner_radius = conf.radius - conf.thickness;
|
let wheel_rad = conf.radius;
|
||||||
let mut tire_mesh: Mesh = Torus::new(inner_radius, outer_radius).into();
|
let tire_thickness = conf.thickness;
|
||||||
let tire_verts = tire_mesh
|
let tire = Tire {
|
||||||
|
minor_radius: tire_thickness,
|
||||||
|
major_radius: wheel_rad,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut mesh = Mesh::from(tire);
|
||||||
|
let tire_verts = mesh
|
||||||
.attribute(Mesh::ATTRIBUTE_POSITION)
|
.attribute(Mesh::ATTRIBUTE_POSITION)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_float3()
|
.as_float3()
|
||||||
|
@ -75,70 +160,15 @@ fn gen_tire(conf: &WheelConfig) -> (Mesh, Collider) {
|
||||||
p.to_array()
|
p.to_array()
|
||||||
})
|
})
|
||||||
.collect::<Vec<[f32; 3]>>();
|
.collect::<Vec<[f32; 3]>>();
|
||||||
tire_mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION);
|
mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION);
|
||||||
tire_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, tire_verts);
|
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, tire_verts);
|
||||||
|
|
||||||
let collider = Collider::convex_hull_from_mesh(&tire_mesh).unwrap();
|
let mut idxs = Vec::new();
|
||||||
|
let indices = mesh.indices().unwrap().iter().collect::<Vec<_>>();
|
||||||
(tire_mesh, collider)
|
for idx in indices.as_slice().chunks_exact(3) {
|
||||||
|
idxs.push([idx[0] as u32, idx[1] as u32, idx[2] as u32]);
|
||||||
}
|
}
|
||||||
|
let wheel_collider = Collider::convex_decomposition_from_mesh(&mesh).unwrap();
|
||||||
|
|
||||||
fn wheels_helper(
|
(mesh, wheel_collider)
|
||||||
commands: &mut Commands,
|
|
||||||
meshes: &mut ResMut<Assets<Mesh>>,
|
|
||||||
materials: &mut ResMut<Assets<StandardMaterial>>,
|
|
||||||
position: Vec3,
|
|
||||||
tire_mesh: Mesh,
|
|
||||||
collider: Collider,
|
|
||||||
conf: &WheelConfig,
|
|
||||||
) -> Entity {
|
|
||||||
let wheel_material = &StandardMaterial {
|
|
||||||
base_color: Color::srgb(0.01, 0.01, 0.01),
|
|
||||||
alpha_mode: AlphaMode::Opaque,
|
|
||||||
perceptual_roughness: 0.5,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let xform = Transform::from_translation(position);
|
|
||||||
let hub_mesh: Mesh = Sphere::new(0.1).into();
|
|
||||||
|
|
||||||
let hub = commands
|
|
||||||
.spawn((
|
|
||||||
Name::new("hub"),
|
|
||||||
RigidBody::Dynamic,
|
|
||||||
MassPropertiesBundle::new_computed(&Collider::sphere(0.1), 200.0),
|
|
||||||
PbrBundle {
|
|
||||||
mesh: meshes.add(hub_mesh),
|
|
||||||
material: materials.add(wheel_material.clone()),
|
|
||||||
transform: xform,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.id();
|
|
||||||
|
|
||||||
let tire = commands
|
|
||||||
.spawn((
|
|
||||||
Name::new("tire"),
|
|
||||||
PbrBundle {
|
|
||||||
mesh: meshes.add(tire_mesh),
|
|
||||||
material: materials.add(wheel_material.clone()),
|
|
||||||
transform: xform,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
CyberWheel,
|
|
||||||
RigidBody::Dynamic,
|
|
||||||
collider,
|
|
||||||
Friction::new(conf.friction),
|
|
||||||
Restitution::new(conf.restitution),
|
|
||||||
ColliderDensity(conf.density),
|
|
||||||
CollisionLayers::new(ColliderGroups::Wheels, ColliderGroups::Planet),
|
|
||||||
ExternalTorque::ZERO.with_persistence(false),
|
|
||||||
CollisionMargin(0.05),
|
|
||||||
SweptCcd::NON_LINEAR,
|
|
||||||
))
|
|
||||||
.id();
|
|
||||||
|
|
||||||
// connect hubs and tires to make wheels
|
|
||||||
commands.spawn(RevoluteJoint::new(hub, tire).with_aligned_axis(Vec3::X));
|
|
||||||
hub
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use crate::{bike::CyberBikeBody, input::InputState, ui::setup_ui};
|
use crate::{bike::CyberBikeBody, input::InputState};
|
||||||
|
|
||||||
// 85 degrees in radians
|
// 85 degrees in radians
|
||||||
const MAX_PITCH: f32 = 1.48353;
|
const MAX_PITCH: f32 = 1.48353;
|
||||||
|
@ -38,25 +38,22 @@ impl CyberCameras {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_cybercams(mut commands: Commands, asset_server: Res<AssetServer>) {
|
fn setup_cybercams(mut commands: Commands) {
|
||||||
let hero_projection = PerspectiveProjection {
|
let hero_projection = PerspectiveProjection {
|
||||||
fov: std::f32::consts::FRAC_PI_3,
|
fov: std::f32::consts::FRAC_PI_3,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = commands
|
commands
|
||||||
.spawn(Camera3dBundle {
|
.spawn(Camera3dBundle {
|
||||||
projection: bevy::render::camera::Projection::Perspective(hero_projection),
|
projection: bevy::render::camera::Projection::Perspective(hero_projection),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.insert(CyberCameras::Hero)
|
.insert(CyberCameras::Hero);
|
||||||
.id();
|
|
||||||
|
|
||||||
commands
|
commands
|
||||||
.spawn(Camera3dBundle::default())
|
.spawn(Camera3dBundle::default())
|
||||||
.insert(CyberCameras::Debug);
|
.insert(CyberCameras::Debug);
|
||||||
|
|
||||||
setup_ui(commands, asset_server, id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn follow_cyberbike(
|
fn follow_cyberbike(
|
||||||
|
@ -87,7 +84,7 @@ fn follow_cyberbike(
|
||||||
cam_xform.rotate(Quat::from_axis_angle(*axis, angle));
|
cam_xform.rotate(Quat::from_axis_angle(*axis, angle));
|
||||||
}
|
}
|
||||||
CyberCameras::Debug => {
|
CyberCameras::Debug => {
|
||||||
let mut ncx = Transform::from_translation(bike_xform.translation);
|
let mut ncx = bike_xform.to_owned();
|
||||||
ncx.rotate(Quat::from_axis_angle(up, offset.rot.to_radians()));
|
ncx.rotate(Quat::from_axis_angle(up, offset.rot.to_radians()));
|
||||||
ncx.translation += ncx.forward() * offset.dist;
|
ncx.translation += ncx.forward() * offset.dist;
|
||||||
ncx.translation += ncx.up() * offset.alt;
|
ncx.translation += ncx.up() * offset.alt;
|
||||||
|
@ -100,15 +97,12 @@ fn follow_cyberbike(
|
||||||
|
|
||||||
fn update_active_camera(
|
fn update_active_camera(
|
||||||
state: Res<State<CyberCameras>>,
|
state: Res<State<CyberCameras>>,
|
||||||
mut cameras: Query<(Entity, &mut Camera, &CyberCameras)>,
|
mut query: Query<(&mut Camera, &CyberCameras)>,
|
||||||
mut target: Query<&mut TargetCamera>,
|
|
||||||
) {
|
) {
|
||||||
let mut target = target.single_mut();
|
|
||||||
// find the camera with the current state, set it as the ActiveCamera
|
// find the camera with the current state, set it as the ActiveCamera
|
||||||
cameras.iter_mut().for_each(|(ent, mut cam, cyber)| {
|
query.iter_mut().for_each(|(mut cam, cyber)| {
|
||||||
if cyber.eq(state.get()) {
|
if cyber.eq(&state.get()) {
|
||||||
cam.is_active = true;
|
cam.is_active = true;
|
||||||
*target = TargetCamera(ent);
|
|
||||||
} else {
|
} else {
|
||||||
cam.is_active = false;
|
cam.is_active = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ use avian3d::debug_render::PhysicsGizmos;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
color::Srgba,
|
color::Srgba,
|
||||||
gizmos::AppGizmoBuilder,
|
gizmos::AppGizmoBuilder,
|
||||||
math::Vec3,
|
|
||||||
prelude::{App, Color, GizmoConfig, Plugin},
|
prelude::{App, Color, GizmoConfig, Plugin},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,8 +20,6 @@ impl Plugin for CyberGlamorPlugin {
|
||||||
PhysicsGizmos {
|
PhysicsGizmos {
|
||||||
contact_point_color: Some(Srgba::GREEN.into()),
|
contact_point_color: Some(Srgba::GREEN.into()),
|
||||||
contact_normal_color: Some(Srgba::WHITE.into()),
|
contact_normal_color: Some(Srgba::WHITE.into()),
|
||||||
hide_meshes: true,
|
|
||||||
axis_lengths: Some(Vec3::ZERO),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
GizmoConfig::default(),
|
GizmoConfig::default(),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use avian3d::prelude::PhysicsLayer;
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
prelude::{Query, Vec3, Window, With},
|
prelude::{Query, Vec3, Window, With},
|
||||||
window::PrimaryWindow,
|
window::PrimaryWindow,
|
||||||
|
@ -13,13 +12,6 @@ pub mod lights;
|
||||||
pub mod planet;
|
pub mod planet;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
#[derive(PhysicsLayer)]
|
|
||||||
pub enum ColliderGroups {
|
|
||||||
BikeBody,
|
|
||||||
Wheels,
|
|
||||||
Planet,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disable_mouse_trap(mut window: Query<&mut Window, With<PrimaryWindow>>) {
|
pub fn disable_mouse_trap(mut window: Query<&mut Window, With<PrimaryWindow>>) {
|
||||||
let mut window = window.get_single_mut().unwrap();
|
let mut window = window.get_single_mut().unwrap();
|
||||||
window.cursor.grab_mode = bevy::window::CursorGrabMode::None;
|
window.cursor.grab_mode = bevy::window::CursorGrabMode::None;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use bevy::{pbr::CascadeShadowConfigBuilder, prelude::*};
|
||||||
|
|
||||||
use crate::planet::PLANET_RADIUS;
|
use crate::planet::PLANET_RADIUS;
|
||||||
|
|
||||||
pub const LIGHT_RANGE: f32 = 900.0;
|
pub const LIGHT_RANGE: f32 = 90.0;
|
||||||
|
|
||||||
static BLUE: Color = Color::linear_rgb(0.0, 0.0, 1.0);
|
static BLUE: Color = Color::linear_rgb(0.0, 0.0, 1.0);
|
||||||
static PINK: Color = Color::linear_rgb(199.0 / 255.0, 21.0 / 255.0, 113.0 / 255.0);
|
static PINK: Color = Color::linear_rgb(199.0 / 255.0, 21.0 / 255.0, 113.0 / 255.0);
|
||||||
|
@ -13,7 +13,7 @@ fn spawn_static_lights(
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
) {
|
) {
|
||||||
let pink_light = PointLight {
|
let pink_light = PointLight {
|
||||||
intensity: 1_000_000.0,
|
intensity: 1_00.0,
|
||||||
range: LIGHT_RANGE,
|
range: LIGHT_RANGE,
|
||||||
color: PINK,
|
color: PINK,
|
||||||
radius: 1.0,
|
radius: 1.0,
|
||||||
|
@ -22,7 +22,7 @@ fn spawn_static_lights(
|
||||||
};
|
};
|
||||||
|
|
||||||
let blue_light = PointLight {
|
let blue_light = PointLight {
|
||||||
intensity: 1_000_000.0,
|
intensity: 1_000.0,
|
||||||
range: LIGHT_RANGE,
|
range: LIGHT_RANGE,
|
||||||
color: BLUE,
|
color: BLUE,
|
||||||
radius: 1.0,
|
radius: 1.0,
|
||||||
|
@ -32,7 +32,7 @@ fn spawn_static_lights(
|
||||||
|
|
||||||
commands.insert_resource(AmbientLight {
|
commands.insert_resource(AmbientLight {
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
brightness: 8_000.0,
|
brightness: 0.2,
|
||||||
});
|
});
|
||||||
|
|
||||||
let _cascade_shadow_config = CascadeShadowConfigBuilder {
|
let _cascade_shadow_config = CascadeShadowConfigBuilder {
|
||||||
|
|
|
@ -25,9 +25,8 @@ fn spawn_planet(
|
||||||
let pcollide = (
|
let pcollide = (
|
||||||
shape,
|
shape,
|
||||||
Friction {
|
Friction {
|
||||||
static_coefficient: 0.8,
|
static_coefficient: 1.2,
|
||||||
dynamic_coefficient: 0.5,
|
..Default::default()
|
||||||
combine_rule: CoefficientCombine::Average,
|
|
||||||
},
|
},
|
||||||
Restitution::new(0.8),
|
Restitution::new(0.8),
|
||||||
);
|
);
|
||||||
|
@ -42,7 +41,7 @@ fn spawn_planet(
|
||||||
RigidBody::Static,
|
RigidBody::Static,
|
||||||
pcollide,
|
pcollide,
|
||||||
CyberPlanet,
|
CyberPlanet,
|
||||||
CollisionLayers::new(LayerMask::ALL, LayerMask::ALL),
|
CollisionLayers::new(u32::MAX, u32::MAX),
|
||||||
CollisionMargin(0.2),
|
CollisionMargin(0.2),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -137,6 +136,7 @@ fn gen_planet(radius: f32) -> (Mesh, Collider) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbg!(&colors.len());
|
||||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors);
|
||||||
|
|
||||||
(mesh, collider)
|
(mesh, collider)
|
||||||
|
|
24
src/ui.rs
24
src/ui.rs
|
@ -1,11 +1,10 @@
|
||||||
use avian3d::prelude::LinearVelocity;
|
use avian3d::prelude::LinearVelocity;
|
||||||
use bevy::{
|
use bevy::{
|
||||||
app::Update,
|
app::{Startup, Update},
|
||||||
prelude::{
|
prelude::{
|
||||||
AlignSelf, App, AssetServer, Color, Commands, Component, Entity, Plugin, Query, Res, Style,
|
AlignSelf, App, AssetServer, Color, Commands, Component, Plugin, Query, Res, Style, Text,
|
||||||
Text, TextBundle, TextSection, TextStyle, With,
|
TextBundle, TextSection, TextStyle, Transform, With,
|
||||||
},
|
},
|
||||||
ui::TargetCamera,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::bike::CyberBikeBody;
|
use crate::bike::CyberBikeBody;
|
||||||
|
@ -13,11 +12,7 @@ use crate::bike::CyberBikeBody;
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct UpText;
|
struct UpText;
|
||||||
|
|
||||||
pub(crate) fn setup_ui(
|
fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
mut commands: Commands,
|
|
||||||
asset_server: Res<AssetServer>,
|
|
||||||
target_camera: Entity,
|
|
||||||
) {
|
|
||||||
commands
|
commands
|
||||||
.spawn(TextBundle {
|
.spawn(TextBundle {
|
||||||
style: Style {
|
style: Style {
|
||||||
|
@ -39,16 +34,16 @@ pub(crate) fn setup_ui(
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.insert((UpText, TargetCamera(target_camera)));
|
.insert(UpText);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_ui(
|
fn update_ui(
|
||||||
state_query: Query<&LinearVelocity, With<CyberBikeBody>>,
|
state_query: Query<(&LinearVelocity, &Transform), With<CyberBikeBody>>,
|
||||||
mut text_query: Query<&mut Text, With<UpText>>,
|
mut text_query: Query<&mut Text, With<UpText>>,
|
||||||
) {
|
) {
|
||||||
let mut text = text_query.single_mut();
|
let mut text = text_query.single_mut();
|
||||||
let velocity = state_query.single();
|
let (velocity, xform) = state_query.single();
|
||||||
let speed = velocity.0.length();
|
let speed = velocity.dot(*xform.forward());
|
||||||
text.sections[0].value = format!("spd: {:.2}", speed);
|
text.sections[0].value = format!("spd: {:.2}", speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +51,7 @@ pub struct CyberUIPlugin;
|
||||||
|
|
||||||
impl Plugin for CyberUIPlugin {
|
impl Plugin for CyberUIPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Update, update_ui);
|
app.add_systems(Startup, setup_ui)
|
||||||
|
.add_systems(Update, update_ui);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue