1use bevy::{
4 color::palettes::basic::{GRAY, RED, WHITE},
5 input::mouse::{AccumulatedMouseMotion, MouseButtonInput},
6 math::StableInterpolate,
7 prelude::*,
8};
9use rand::{Rng, SeedableRng};
10use rand_chacha::ChaCha8Rng;
11
12fn 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#[derive(Component, Default)]
23struct Ship {
24 target_transform: Transform,
26
27 in_motion: bool,
29}
30
31#[derive(Component)]
32struct RandomAxes(Dir3, Dir3);
33
34#[derive(Component)]
35struct Instructions;
36
37#[derive(Resource)]
38struct MousePressed(bool);
39
40#[derive(Resource)]
41struct SeededRng(ChaCha8Rng);
42
43fn setup(
46 mut commands: Commands,
47 mut meshes: ResMut<Assets<Mesh>>,
48 mut materials: ResMut<Assets<StandardMaterial>>,
49 asset_server: Res<AssetServer>,
50) {
51 let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
54
55 commands.spawn((
57 Camera3d::default(),
58 Transform::from_xyz(3., 2.5, 4.).looking_at(Vec3::ZERO, Vec3::Y),
59 ));
60
61 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 commands.spawn((
70 PointLight {
71 shadows_enabled: true,
72 ..default()
73 },
74 Transform::from_xyz(4.0, 7.0, -4.0),
75 ));
76
77 let first = seeded_rng.random();
79 let second = seeded_rng.random();
80 commands.spawn(RandomAxes(first, second));
81
82 commands.spawn((
84 SceneRoot(
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 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
119fn draw_ship_axes(mut gizmos: Gizmos, ship_transform: Single<&Transform, With<Ship>>) {
123 let z_ends = arrow_ends(*ship_transform, Vec3::NEG_Z, 1.5);
125 gizmos.arrow(z_ends.0, z_ends.1, RED);
126
127 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
132fn 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
139fn 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
158fn 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 let first = seeded_rng.0.random();
169 let second = seeded_rng.0.random();
170 **random_axes = RandomAxes(first, second);
171
172 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
190fn 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 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 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
215fn 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
222fn 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}