Latest commit 01aedc8

用四元数旋转 2D 实体。

use bevy::math::Vec3Swizzles;
use bevy::prelude::*;
use bevy::time::FixedTimestep;

const TIME_STEP: f32 = 1.0 / 60.0;
const BOUNDS: Vec2 = Vec2::new(1200.0, 640.0);

fn main() {
    let mut app = App::new();

    app.add_plugins(DefaultPlugins);
    app.add_startup_system(初始设置);
    app.add_system_set(
        SystemSet::new()
            .with_run_criteria(FixedTimestep::step(TIME_STEP as f64))
            .with_system(玩家运动)
            .with_system(snap_to_player_system)
            .with_system(rotate_to_player_system)
    );

    app.run();
}

#[derive(Component)]
struct Player {
    movement_speed: f32,
    rotation_speed: f32,
}

// 使敌人飞船即时面向玩家所在的位置
#[derive(Component)]
struct SnapToPlayer;

// 使敌人飞船根据旋转速度慢慢地面向玩家所在的位置
#[derive(Component)]
struct RotateToPlayer {
    rotation_speed: f32,
}

/// 添加实体、创建摄像机
/// X 轴指向右,Y 轴指向上,Z 轴指向观察者,原点位于屏幕中心点
fn 初始设置(mut commands: Commands, asset_server: Res<AssetServer>) {
    // 摄像机
    commands.spawn(Camera2dBundle::default());

    let ship_handle = asset_server.load("ship.png");
    let enemy_a_handle = asset_server.load("enemy.png");
    let enemy_b_handle = asset_server.load("enemy.png");
    let horizontal_margin = BOUNDS.x / 4.0;
    let vertical_margin = BOUNDS.y / 4.0;

    // 玩家控制的飞船
    commands.spawn((
        SpriteBundle {
            texture: ship_handle, // todo: 不用 into() 吗?
            ..default()
        },
        Player {
            movement_speed: 500.0,
            rotation_speed: f32::to_radians(360.0),
        },
    ));

    // 在左边和下边生成两个敌人
    commands.spawn((
        SpriteBundle {
            texture: enemy_a_handle.clone(),
            transform: Transform::from_xyz(0.0 - horizontal_margin, 0.0, 0.0),
            ..default()
        },
        SnapToPlayer,
    ));
    commands.spawn((
        SpriteBundle {
            texture: enemy_a_handle,
            transform: Transform::from_xyz(0.0, 0.0 - vertical_margin, 0.0),
            ..default()
        },
        SnapToPlayer,
    ));

    // 在右边和上边生成两个敌人
    commands.spawn((
        SpriteBundle {
            texture: enemy_b_handle.clone(),
            transform: Transform::from_xyz(0.0 + horizontal_margin, 0.0, 0.0),
            ..default()
        },
        RotateToPlayer {
            rotation_speed: f32::to_radians(45.0),
        },
    ));
    commands.spawn((
        SpriteBundle {
            texture: enemy_b_handle,
            transform: Transform::from_xyz(0.0, 0.0 + vertical_margin, 0.0),
            ..default()
        },
        RotateToPlayer {
            rotation_speed: f32::to_radians(90.0),
        },
    ));
}

fn 玩家运动(mut query: Query<(&Player, &mut Transform)>, keyboard_input: Res<Input<KeyCode>>) {
    let (player, 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;
    }

    // 使飞船绕 Z 轴旋转(也就是在 2D 平面上旋转)
    // 按下左键获得正值,逆时针旋转;按下右键获得负值,顺时针旋转
    transform.rotate_z(rotation_factor * player.rotation_speed * TIME_STEP);

    // 用飞船当前的旋转(四元数)乘 Y 轴正方向,得到一个新的向量,作为前进方向
    let movement_direction = transform.rotation * Vec3::Y;
    // 移动距离
    let movement_distance = movement_factor * player.movement_speed * TIME_STEP;
    // 下一帧的位置 = 方向(Vec3) * 距离(f32)
    let translation_delta = movement_direction * movement_distance;
    transform.translation += translation_delta;

    // 超出屏幕时,位置设置在屏幕内
    // let extents = Vec3::from((BOUNDS / 2.0, 0.0));
    let extents = Vec3::new(BOUNDS.x / 2.0, BOUNDS.y / 2.0, 0.0);
    transform.translation = transform.translation.min(extents).max(-extents);
}

// 敌人立即转向玩家所在位置
fn snap_to_player_system(
    mut query: Query<&mut Transform, (With<SnapToPlayer>, Without<Player>)>,
    player_query: Query<&Transform, With<Player>>,
) {
    let player_transform = player_query.single();
    // 用 Vec2 来进行相关计算
    let player_translation = player_transform.translation.xy();

    for mut enemy_transform in &mut query {
        // 获取从敌人面向玩家的方向
        let to_player = (player_translation - enemy_transform.translation.xy()).normalize();

        // 获取从“敌人的的朝向”直接旋转到“面向玩家的方向”的四元数
        // 素材的飞船面向正上方,因此用 Vec::Y
        let rotate_to_player = Quat::from_rotation_arc(Vec3::Y, to_player.extend(0.0));

        enemy_transform.rotation = rotate_to_player;
    }
}

/// 敌人以固定的旋转速度转向玩家所在位置,根据点积的值来选择旋转方式
///
/// 值为 1.0,两向量方向相同,夹角为 0 度;
/// 值为 0.0,两向量垂直,夹角为 90 度;
/// 值为 -1.0,两向量反向平行,夹角为 180 度;
/// 略...
fn rotate_to_player_system(
    mut query: Query<(&RotateToPlayer, &mut Transform), Without<Player>>,
    player_query: Query<&Transform, With<Player>>,
) {
    let player_transform = player_query.single();
    // 用 Vec2 来进行相关计算
    let player_translation = player_transform.translation.xy();

    for (config, mut enemy_transform) in &mut query {
        // 获取敌人的朝向、从敌人到玩家的方向,计算点积,
        // 如果点积约等于 1.0,说明敌人已经面向玩家,不再继续旋转
        let enemy_forward = (enemy_transform.rotation * Vec3::Y).xy();
        let to_player = (player_translation - enemy_transform.translation.xy()).normalize();
        let forward_dot_player = enemy_forward.dot(to_player);
        if (forward_dot_player - 1.0).abs() < f32::EPSILON {
            continue;
        }

        // 获取敌人默认方向、从敌人到玩家的方向,计算点积,
        // 如果点积是负数,需要逆时针旋转,如果是正数,需要顺时针旋转
        let enemy_right = (enemy_transform.rotation * Vec3::X).xy();
        let right_dot_player = enemy_right.dot(to_player);

        // 确定旋转方向,要取反方向
        let rotation_sign = -f32::copysign(1.0, right_dot_player);
        // 限制旋转
        let max_angle = forward_dot_player.clamp(-1.0, 1.0).acos();
        let rotation_angle = rotation_sign * (config.rotation_speed * TIME_STEP).min(max_angle);

        // 旋转
        enemy_transform.rotate_z(rotation_angle);
    }
}