| 1 |
|
| 2 |
|
| 3 |
use bevy::{ |
| 4 |
color::palettes::basic::{GRAY, RED, WHITE}, |
| 5 |
input::mouse::{AccumulatedMouseMotion, MouseButtonInput}, |
| 6 |
math::StableInterpolate, |
| 7 |
prelude::*, |
| 8 |
}; |
| 9 |
use chacha20::ChaCha8Rng; |
| 10 |
use rand::{RngExt, SeedableRng}; |
| 11 |
|
| 12 |
fn main() { |
| 13 |
App::new() |
| 14 |
.add_plugins(DefaultPlugins) |
| 15 |
.add_systems(Startup, setup) |
| 16 |
.add_systems(Update, (draw_ship_axes, draw_random_axes)) |
| 17 |
.add_systems(Update, (handle_keypress, handle_mouse, rotate_ship).chain()) |
| 18 |
.run(); |
| 19 |
} |
| 20 |
|
| 21 |
|
| 22 |
#[derive(Component, Default)] |
| 23 |
struct Ship { |
| 24 |
|
| 25 |
target_transform: Transform, |
| 26 |
|
| 27 |
|
| 28 |
in_motion: bool, |
| 29 |
} |
| 30 |
|
| 31 |
#[derive(Component)] |
| 32 |
struct RandomAxes(Dir3, Dir3); |
| 33 |
|
| 34 |
#[derive(Component)] |
| 35 |
struct Instructions; |
| 36 |
|
| 37 |
#[derive(Resource)] |
| 38 |
struct MousePressed(bool); |
| 39 |
|
| 40 |
#[derive(Resource)] |
| 41 |
struct SeededRng(ChaCha8Rng); |
| 42 |
|
| 43 |
|
| 44 |
|
| 45 |
fn setup( |
| 46 |
mut commands: Commands, |
| 47 |
mut meshes: ResMut<Assets<Mesh>>, |
| 48 |
mut materials: ResMut<Assets<StandardMaterial>>, |
| 49 |
asset_server: Res<AssetServer>, |
| 50 |
) { |
| 51 |
|
| 52 |
|
| 53 |
let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712); |
| 54 |
|
| 55 |
|
| 56 |
commands.spawn(( |
| 57 |
Camera3d::default(), |
| 58 |
Transform::from_xyz(3., 2.5, 4.).looking_at(Vec3::ZERO, Vec3::Y), |
| 59 |
)); |
| 60 |
|
| 61 |
|
| 62 |
commands.spawn(( |
| 63 |
Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), |
| 64 |
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), |
| 65 |
Transform::from_xyz(0., -2., 0.), |
| 66 |
)); |
| 67 |
|
| 68 |
|
| 69 |
commands.spawn(( |
| 70 |
PointLight { |
| 71 |
shadow_maps_enabled: true, |
| 72 |
..default() |
| 73 |
}, |
| 74 |
Transform::from_xyz(4.0, 7.0, -4.0), |
| 75 |
)); |
| 76 |
|
| 77 |
|
| 78 |
let first = seeded_rng.random(); |
| 79 |
let second = seeded_rng.random(); |
| 80 |
commands.spawn(RandomAxes(first, second)); |
| 81 |
|
| 82 |
|
| 83 |
commands.spawn(( |
| 84 |
WorldAssetRoot( |
| 85 |
asset_server |
| 86 |
.load(GltfAssetLabel::Scene(0).from_asset("models/ship/craft_speederD.gltf")), |
| 87 |
), |
| 88 |
Ship { |
| 89 |
target_transform: random_axes_target_alignment(&RandomAxes(first, second)), |
| 90 |
..default() |
| 91 |
}, |
| 92 |
)); |
| 93 |
|
| 94 |
|
| 95 |
commands.spawn(( |
| 96 |
Text::new( |
| 97 |
"The bright red axis is the primary alignment axis, and it will always be\n\ |
| 98 |
made to coincide with the primary target direction (white) exactly.\n\ |
| 99 |
The fainter red axis is the secondary alignment axis, and it is made to\n\ |
| 100 |
line up with the secondary target direction (gray) as closely as possible.\n\ |
| 101 |
Press 'R' to generate random target directions.\n\ |
| 102 |
Press 'T' to align the ship to those directions.\n\ |
| 103 |
Click and drag the mouse to rotate the camera.\n\ |
| 104 |
Press 'H' to hide/show these instructions.", |
| 105 |
), |
| 106 |
Node { |
| 107 |
position_type: PositionType::Absolute, |
| 108 |
top: px(12), |
| 109 |
left: px(12), |
| 110 |
..default() |
| 111 |
}, |
| 112 |
Instructions, |
| 113 |
)); |
| 114 |
|
| 115 |
commands.insert_resource(MousePressed(false)); |
| 116 |
commands.insert_resource(SeededRng(seeded_rng)); |
| 117 |
} |
| 118 |
|
| 119 |
|
| 120 |
|
| 121 |
|
| 122 |
fn draw_ship_axes(mut gizmos: Gizmos, ship_transform: Single<&Transform, With<Ship>>) { |
| 123 |
|
| 124 |
let z_ends = arrow_ends(*ship_transform, Vec3::NEG_Z, 1.5); |
| 125 |
gizmos.arrow(z_ends.0, z_ends.1, RED); |
| 126 |
|
| 127 |
|
| 128 |
let x_ends = arrow_ends(*ship_transform, Vec3::X, 1.5); |
| 129 |
gizmos.arrow(x_ends.0, x_ends.1, Color::srgb(0.65, 0., 0.)); |
| 130 |
} |
| 131 |
|
| 132 |
|
| 133 |
fn draw_random_axes(mut gizmos: Gizmos, random_axes: Single<&RandomAxes>) { |
| 134 |
let RandomAxes(v1, v2) = *random_axes; |
| 135 |
gizmos.arrow(Vec3::ZERO, 1.5 * *v1, WHITE); |
| 136 |
gizmos.arrow(Vec3::ZERO, 1.5 * *v2, GRAY); |
| 137 |
} |
| 138 |
|
| 139 |
|
| 140 |
fn rotate_ship(ship: Single<(&mut Ship, &mut Transform)>, time: Res<Time>) { |
| 141 |
let (mut ship, mut ship_transform) = ship.into_inner(); |
| 142 |
|
| 143 |
if !ship.in_motion { |
| 144 |
return; |
| 145 |
} |
| 146 |
|
| 147 |
let target_rotation = ship.target_transform.rotation; |
| 148 |
|
| 149 |
ship_transform |
| 150 |
.rotation |
| 151 |
.smooth_nudge(&target_rotation, 3.0, time.delta_secs()); |
| 152 |
|
| 153 |
if ship_transform.rotation.angle_between(target_rotation) <= f32::EPSILON { |
| 154 |
ship.in_motion = false; |
| 155 |
} |
| 156 |
} |
| 157 |
|
| 158 |
|
| 159 |
fn handle_keypress( |
| 160 |
mut ship: Single<&mut Ship>, |
| 161 |
mut random_axes: Single<&mut RandomAxes>, |
| 162 |
mut instructions_viz: Single<&mut Visibility, With<Instructions>>, |
| 163 |
keyboard: Res<ButtonInput<KeyCode>>, |
| 164 |
mut seeded_rng: ResMut<SeededRng>, |
| 165 |
) { |
| 166 |
if keyboard.just_pressed(KeyCode::KeyR) { |
| 167 |
|
| 168 |
let first = seeded_rng.0.random(); |
| 169 |
let second = seeded_rng.0.random(); |
| 170 |
**random_axes = RandomAxes(first, second); |
| 171 |
|
| 172 |
|
| 173 |
ship.in_motion = false; |
| 174 |
ship.target_transform = random_axes_target_alignment(&random_axes); |
| 175 |
} |
| 176 |
|
| 177 |
if keyboard.just_pressed(KeyCode::KeyT) { |
| 178 |
ship.in_motion ^= true; |
| 179 |
} |
| 180 |
|
| 181 |
if keyboard.just_pressed(KeyCode::KeyH) { |
| 182 |
if *instructions_viz.as_ref() == Visibility::Hidden { |
| 183 |
**instructions_viz = Visibility::Visible; |
| 184 |
} else { |
| 185 |
**instructions_viz = Visibility::Hidden; |
| 186 |
} |
| 187 |
} |
| 188 |
} |
| 189 |
|
| 190 |
|
| 191 |
fn handle_mouse( |
| 192 |
accumulated_mouse_motion: Res<AccumulatedMouseMotion>, |
| 193 |
mut mouse_button_inputs: MessageReader<MouseButtonInput>, |
| 194 |
mut camera_transform: Single<&mut Transform, With<Camera>>, |
| 195 |
mut mouse_pressed: ResMut<MousePressed>, |
| 196 |
) { |
| 197 |
|
| 198 |
for mouse_button_input in mouse_button_inputs.read() { |
| 199 |
if mouse_button_input.button != MouseButton::Left { |
| 200 |
continue; |
| 201 |
} |
| 202 |
*mouse_pressed = MousePressed(mouse_button_input.state.is_pressed()); |
| 203 |
} |
| 204 |
|
| 205 |
|
| 206 |
if !mouse_pressed.0 { |
| 207 |
return; |
| 208 |
} |
| 209 |
if accumulated_mouse_motion.delta != Vec2::ZERO { |
| 210 |
let displacement = accumulated_mouse_motion.delta.x; |
| 211 |
camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 75.)); |
| 212 |
} |
| 213 |
} |
| 214 |
|
| 215 |
|
| 216 |
|
| 217 |
fn arrow_ends(transform: &Transform, axis: Vec3, length: f32) -> (Vec3, Vec3) { |
| 218 |
let local_vector = length * (transform.rotation * axis); |
| 219 |
(transform.translation, transform.translation + local_vector) |
| 220 |
} |
| 221 |
|
| 222 |
|
| 223 |
|
| 224 |
fn random_axes_target_alignment(random_axes: &RandomAxes) -> Transform { |
| 225 |
let RandomAxes(first, second) = random_axes; |
| 226 |
Transform::IDENTITY.aligned_by(Vec3::NEG_Z, *first, Vec3::X, *second) |
| 227 |
} |