diff --git a/CREDITS.md b/CREDITS.md index b646a0662dbf7..da6c38d6a8ba1 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -20,3 +20,4 @@ * Cake from [Kenney's Food Kit](https://www.kenney.nl/assets/food-kit) (CC0 1.0 Universal) * Ground tile from [Kenney's Tower Defense Kit](https://www.kenney.nl/assets/tower-defense-kit) (CC0 1.0 Universal) * Game icons from [Kenney's Game Icons](https://www.kenney.nl/assets/game-icons) (CC0 1.0 Universal) +* Space ships from [Kenny's Simple Space Kit](https://www.kenney.nl/assets/simple-space) (CC0 1.0 Universal) diff --git a/Cargo.toml b/Cargo.toml index 812bcd4adf157..9e0b659358b7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,10 @@ path = "examples/2d/contributors.rs" name = "many_sprites" path = "examples/2d/many_sprites.rs" +[[example]] +name = "2d_rotation" +path = "examples/2d/rotation.rs" + [[example]] name = "mesh2d" path = "examples/2d/mesh2d.rs" diff --git a/assets/textures/simplespace/License.txt b/assets/textures/simplespace/License.txt new file mode 100644 index 0000000000000..bdba93fb4f136 --- /dev/null +++ b/assets/textures/simplespace/License.txt @@ -0,0 +1,22 @@ + + + Simple Space + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 03-03-2021 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/assets/textures/simplespace/enemy_A.png b/assets/textures/simplespace/enemy_A.png new file mode 100644 index 0000000000000..7db03b81a3320 Binary files /dev/null and b/assets/textures/simplespace/enemy_A.png differ diff --git a/assets/textures/simplespace/enemy_B.png b/assets/textures/simplespace/enemy_B.png new file mode 100644 index 0000000000000..fbc35f06bf6a6 Binary files /dev/null and b/assets/textures/simplespace/enemy_B.png differ diff --git a/assets/textures/simplespace/ship_C.png b/assets/textures/simplespace/ship_C.png new file mode 100644 index 0000000000000..4f5e493701a1a Binary files /dev/null and b/assets/textures/simplespace/ship_C.png differ diff --git a/examples/2d/rotation.rs b/examples/2d/rotation.rs new file mode 100644 index 0000000000000..9ca891fd116c9 --- /dev/null +++ b/examples/2d/rotation.rs @@ -0,0 +1,251 @@ +use bevy::{ + core::FixedTimestep, + math::{const_vec2, Vec3Swizzles}, + prelude::*, +}; + +const TIME_STEP: f32 = 1.0 / 60.0; +const BOUNDS: Vec2 = const_vec2!([1200.0, 640.0]); + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system_set( + SystemSet::new() + .with_run_criteria(FixedTimestep::step(TIME_STEP as f64)) + .with_system(player_movement_system) + .with_system(snap_to_player_system) + .with_system(rotate_to_player_system), + ) + .add_system(bevy::input::system::exit_on_esc_system) + .run(); +} + +/// player component +#[derive(Component)] +struct Player { + /// linear speed in meters per second + movement_speed: f32, + /// rotation speed in radians per second + rotation_speed: f32, +} + +/// snap to player ship behavior +#[derive(Component)] +struct SnapToPlayer; + +/// rotate to face player ship behavior +#[derive(Component)] +struct RotateToPlayer { + /// rotation speed in radians per second + rotation_speed: f32, +} + +/// Add the game's entities to our world and creates an orthographic camera for 2D rendering. +/// +/// The Bevy coordinate system is the same for 2D and 3D, in terms of 2D this means that: +/// +/// * X axis goes from left to right (+X points right) +/// * Y axis goes from bottom to top (+Y point up) +/// * Z axis goes from far to near (+Z points towards you, out of the screen) +/// +/// The origin is at the center of the screen. +fn setup(mut commands: Commands, asset_server: Res) { + let ship_handle = asset_server.load("textures/simplespace/ship_C.png"); + let enemy_a_handle = asset_server.load("textures/simplespace/enemy_A.png"); + let enemy_b_handle = asset_server.load("textures/simplespace/enemy_B.png"); + + // 2D orthographic camera + commands.spawn_bundle(OrthographicCameraBundle::new_2d()); + + let horizontal_margin = BOUNDS.x / 4.0; + let vertical_margin = BOUNDS.y / 4.0; + + // player controlled ship + commands + .spawn_bundle(SpriteBundle { + texture: ship_handle, + ..Default::default() + }) + .insert(Player { + movement_speed: 500.0, // metres per second + rotation_speed: f32::to_radians(360.0), // degrees per second + }); + + // enemy that snaps to face the player spawns on the bottom and left + commands + .spawn_bundle(SpriteBundle { + texture: enemy_a_handle.clone(), + transform: Transform::from_xyz(0.0 - horizontal_margin, 0.0, 0.0), + ..Default::default() + }) + .insert(SnapToPlayer); + commands + .spawn_bundle(SpriteBundle { + texture: enemy_a_handle, + transform: Transform::from_xyz(0.0, 0.0 - vertical_margin, 0.0), + ..Default::default() + }) + .insert(SnapToPlayer); + + // enemy that rotates to face the player enemy spawns on the top and right + commands + .spawn_bundle(SpriteBundle { + texture: enemy_b_handle.clone(), + transform: Transform::from_xyz(0.0 + horizontal_margin, 0.0, 0.0), + ..Default::default() + }) + .insert(RotateToPlayer { + rotation_speed: f32::to_radians(45.0), // degrees per second + }); + commands + .spawn_bundle(SpriteBundle { + texture: enemy_b_handle, + transform: Transform::from_xyz(0.0, 0.0 + vertical_margin, 0.0), + ..Default::default() + }) + .insert(RotateToPlayer { + rotation_speed: f32::to_radians(90.0), // degrees per second + }); +} + +/// Demonstrates applying rotation and movement based on keyboard input. +fn player_movement_system( + keyboard_input: Res>, + mut query: Query<(&Player, &mut Transform)>, +) { + let (ship, mut transform) = query.single_mut(); + + let mut rotation_factor = 0.0; + let mut movement_factor = 0.0; + + if keyboard_input.pressed(KeyCode::Left) { + rotation_factor += 1.0; + } + + if keyboard_input.pressed(KeyCode::Right) { + rotation_factor -= 1.0; + } + + if keyboard_input.pressed(KeyCode::Up) { + movement_factor += 1.0; + } + + // create the change in rotation around the Z axis (perpendicular to the 2D plane of the screen) + let rotation_delta = Quat::from_rotation_z(rotation_factor * ship.rotation_speed * TIME_STEP); + // update the ship rotation with our rotation delta + transform.rotation *= rotation_delta; + + // get the ship's forward vector by applying the current rotation to the ships initial facing vector + let movement_direction = transform.rotation * Vec3::Y; + // get the distance the ship will move based on direction, the ship's movement speed and delta time + let movement_distance = movement_factor * ship.movement_speed * TIME_STEP; + // create the change in translation using the new movement direction and distance + let translation_delta = movement_direction * movement_distance; + // update the ship translation with our new translation delta + transform.translation += translation_delta; + + // bound the ship within the invisible level bounds + let extents = Vec3::from((BOUNDS / 2.0, 0.0)); + transform.translation = transform.translation.min(extents).max(-extents); +} + +/// Demonstrates snapping the enemy ship to face the player ship immediately. +fn snap_to_player_system( + mut query: Query<&mut Transform, (With, Without)>, + player_query: Query<&Transform, With>, +) { + let player_transform = player_query.single(); + // get the player translation in 2D + let player_translation = player_transform.translation.xy(); + + for mut enemy_transform in query.iter_mut() { + // get the vector from the enemy ship to the player ship in 2D and normalize it. + let to_player = (player_translation - enemy_transform.translation.xy()).normalize(); + + // get the quaternion to rotate from the initial enemy facing direction to the direction + // facing the player + let rotate_to_player = Quat::from_rotation_arc(Vec3::Y, Vec3::from((to_player, 0.0))); + + // rotate the enemy to face the player + enemy_transform.rotation = rotate_to_player; + } +} + +/// Demonstrates rotating an enemy ship to face the player ship at a given rotation speed. +/// +/// This method uses the vector dot product to determine if the enemy is facing the player and +/// if not, which way to rotate to face the player. The dot product on two unit length vectors +/// will return a value between -1.0 and +1.0 which tells us the following about the two vectors: +/// +/// * If the result is 1.0 the vectors are pointing in the same direction, the angle between them +/// is 0 degrees. +/// * If the result is 0.0 the vectors are perpendicular, the angle between them is 90 degrees. +/// * If the result is -1.0 the vectors are parallel but pointing in opposite directions, the angle +/// between them is 180 degrees. +/// * If the result is positive the vectors are pointing in roughly the same direction, the angle +/// between them is greater than 0 and less than 90 degrees. +/// * If the result is negative the vectors are pointing in roughly opposite directions, the angle +/// between them is greater than 90 and less than 180 degrees. +/// +/// It is possible to get the angle by taking the arc cosine (`acos`) of the dot product. It is +/// often unnecessary to do this though. Beware than `acos` will return `NaN` if the input is less +/// than -1.0 or greater than 1.0. This can happen even when working with unit vectors due to +/// floating point precision loss, so it pays to clamp your dot product value before calling +/// `acos`. +fn rotate_to_player_system( + mut query: Query<(&RotateToPlayer, &mut Transform), Without>, + player_query: Query<&Transform, With>, +) { + let player_transform = player_query.single(); + // get the player translation in 2D + let player_translation = player_transform.translation.xy(); + + for (config, mut enemy_transform) in query.iter_mut() { + // get the enemy ship forward vector in 2D (already unit length) + let enemy_forward = (enemy_transform.rotation * Vec3::Y).xy(); + + // get the vector from the enemy ship to the player ship in 2D and normalize it. + let to_player = (player_translation - enemy_transform.translation.xy()).normalize(); + + // get the dot product between the enemy forward vector and the direction to the player. + let forward_dot_player = enemy_forward.dot(to_player); + + // if the dot product is approximately 1.0 then the enemy is already facing the player and + // we can early out. + if (forward_dot_player - 1.0).abs() < f32::EPSILON { + continue; + } + + // get the right vector of the enemy ship in 2D (already unit length) + let enemy_right = (enemy_transform.rotation * Vec3::X).xy(); + + // get the dot product of the enemy right vector and the direction to the player ship. + // if the dot product is negative them we need to rotate counter clockwise, if it is + // positive we need to rotate clockwise. Note that `copysign` will still return 1.0 if the + // dot product is 0.0 (because the player is directly behind the enemy, so perpendicular + // with the right vector). + let right_dot_player = enemy_right.dot(to_player); + + // determine the sign of rotation from the right dot player. We need to negate the sign + // here as the 2D bevy co-ordinate system rotates around +Z, which is pointing out of the + // screen. Due to the right hand rule, positive rotation around +Z is counter clockwise and + // negative is clockwise. + let rotation_sign = -f32::copysign(1.0, right_dot_player); + + // limit rotation so we don't overshoot the target. We need to convert our dot product to + // an angle here so we can get an angle of rotation to clamp against. + let max_angle = forward_dot_player.clamp(-1.0, 1.0).acos(); // clamp acos for safety + + // calculate angle of rotation with limit + let rotation_angle = rotation_sign * (config.rotation_speed * TIME_STEP).min(max_angle); + + // get the quaternion to rotate from the current enemy facing direction towards the + // direction facing the player + let rotation_delta = Quat::from_rotation_z(rotation_angle); + + // rotate the enemy to face the player + enemy_transform.rotation *= rotation_delta; + } +} diff --git a/examples/README.md b/examples/README.md index 1ddbd01a583fa..e5330e31cb752 100644 --- a/examples/README.md +++ b/examples/README.md @@ -92,6 +92,7 @@ Example | File | Description `text2d` | [`2d/text2d.rs`](./2d/text2d.rs) | Generates text in 2d `sprite_flipping` | [`2d/sprite_flipping.rs`](./2d/sprite_flipping.rs) | Renders a sprite flipped along an axis `texture_atlas` | [`2d/texture_atlas.rs`](./2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites +`rotation` | [`2d/rotation.rs`](./2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions ## 3D Rendering