Bevy Version: | 0.13 | (outdated!) |
---|
As this page is outdated, please refer to Bevy's official migration guides while reading, to cover the differences: 0.13 to 0.14.
I apologize for the inconvenience. I will update the page as soon as I find the time.
Convert cursor to world coordinates
2D games
If you only have one window (the primary window), as is the case for most apps and games, you can do this:
Code (simple version):
use bevy::window::PrimaryWindow;
/// We will store the world position of the mouse cursor here.
#[derive(Resource, Default)]
struct MyWorldCoords(Vec2);
/// Used to help identify our main camera
#[derive(Component)]
struct MainCamera;
fn setup(mut commands: Commands) {
// Make sure to add the marker component when you set up your camera
commands.spawn((Camera2dBundle::default(), MainCamera));
}
fn my_cursor_system(
mut mycoords: ResMut<MyWorldCoords>,
// query to get the window (so we can read the current cursor position)
q_window: Query<&Window, With<PrimaryWindow>>,
// query to get camera transform
q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
) {
// get the camera info and transform
// assuming there is exactly one main camera entity, so Query::single() is OK
let (camera, camera_transform) = q_camera.single();
// There is only one primary window, so we can similarly get it from the query:
let window = q_window.single();
// check if the cursor is inside the window and get its position
// then, ask bevy to convert into world coordinates, and truncate to discard Z
if let Some(world_position) = window.cursor_position()
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
.map(|ray| ray.origin.truncate())
{
mycoords.0 = world_position;
eprintln!("World coords: {}/{}", world_position.x, world_position.y);
}
}
app.init_resource::<MyWorldCoords>();
app.add_systems(Startup, setup);
app.add_systems(Update, my_cursor_system);
If you have a more complex application with multiple windows, here is a more complex version of the code that can handle that:
Code (multi-window version):
use bevy::render::camera::RenderTarget;
use bevy::window::WindowRef;
/// We will add this to each camera we want to compute cursor position for.
/// Add the component to the camera that renders to each window.
#[derive(Component, Default)]
struct WorldCursorCoords(Vec2);
fn setup_multiwindow(mut commands: Commands) {
// TODO: set up multiple cameras for multiple windows.
// See bevy's example code for how to do that.
// Make sure we add our component to each camera
commands.spawn((Camera2dBundle::default(), WorldCursorCoords::default()));
}
fn my_cursor_system_multiwindow(
// query to get the primary window
q_window_primary: Query<&Window, With<PrimaryWindow>>,
// query to get other windows
q_window: Query<&Window>,
// query to get camera transform
mut q_camera: Query<(&Camera, &GlobalTransform, &mut WorldCursorCoords)>,
) {
for (camera, camera_transform, mut worldcursor) in &mut q_camera {
// get the window the camera is rendering to
let window = match camera.target {
// the camera is rendering to the primary window
RenderTarget::Window(WindowRef::Primary) => {
q_window_primary.single()
},
// the camera is rendering to some other window
RenderTarget::Window(WindowRef::Entity(e_window)) => {
q_window.get(e_window).unwrap()
},
// the camera is rendering to something else (like a texture), not a window
_ => {
// skip this camera
continue;
}
};
// check if the cursor is inside the window and get its position
// then, ask bevy to convert into world coordinates, and truncate to discard Z
if let Some(world_position) = window.cursor_position()
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
.map(|ray| ray.origin.truncate())
{
worldcursor.0 = world_position;
}
}
}
app.add_systems(Startup, setup_multiwindow);
app.add_systems(Update, my_cursor_system_multiwindow);
3D games
If you'd like to be able to detect what 3D object the cursor is pointing at, select
objects, etc., there is a good (unofficial) plugin:
bevy_mod_picking
.
For a simple top-down camera view game with a flat ground plane, it might be sufficient to just compute the coordinates on the ground under the cursor.
In the interactive example, there is a ground plane with a non-default position and rotation. There is a red cube, which is positioned using the global coordinates, and a blue cube, which is a child entity of the ground plane and positioned using local coordinates. They should both follow the cursor.
Code and explanation:
/// Here we will store the position of the mouse cursor on the 3D ground plane.
#[derive(Resource, Default)]
struct MyGroundCoords {
// Global (world-space) coordinates
global: Vec3,
// Local (relative to the ground plane) coordinates
local: Vec2,
}
/// Used to help identify our main camera
#[derive(Component)]
struct MyGameCamera;
/// Used to help identify our ground plane
#[derive(Component)]
struct MyGroundPlane;
fn setup_3d_scene(mut commands: Commands) {
// Make sure to add the marker component when you set up your camera
commands.spawn((
MyGameCamera,
Camera3dBundle {
// ... your camera configuration ...
..default()
},
));
// Spawn the ground
commands.spawn((
MyGroundPlane,
PbrBundle {
// feel free to change this to rotate/tilt or reposition the ground
transform: Transform::default(),
// TODO: set up your mesh / visuals for rendering:
// mesh: ...
// material: ...
..default()
},
));
}
fn cursor_to_ground_plane(
mut mycoords: ResMut<MyGroundCoords>,
// query to get the window (so we can read the current cursor position)
// (we will only work with the primary window)
q_window: Query<&Window, With<PrimaryWindow>>,
// query to get camera transform
q_camera: Query<(&Camera, &GlobalTransform), With<MyGameCamera>>,
// query to get ground plane's transform
q_plane: Query<&GlobalTransform, With<MyGroundPlane>>,
) {
// get the camera info and transform
// assuming there is exactly one main camera entity, so Query::single() is OK
let (camera, camera_transform) = q_camera.single();
// Ditto for the ground plane's transform
let ground_transform = q_plane.single();
// There is only one primary window, so we can similarly get it from the query:
let window = q_window.single();
// check if the cursor is inside the window and get its position
let Some(cursor_position) = window.cursor_position() else {
// if the cursor is not inside the window, we can't do anything
return;
};
// Mathematically, we can represent the ground as an infinite flat plane.
// To do that, we need a point (to position the plane) and a normal vector
// (the "up" direction, perpendicular to the ground plane).
// We can get the correct values from the ground entity's GlobalTransform
let plane_origin = ground_transform.translation();
let plane = Plane3d::new(ground_transform.up());
// Ask Bevy to give us a ray pointing from the viewport (screen) into the world
let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else {
// if it was impossible to compute for whatever reason; we can't do anything
return;
};
// do a ray-plane intersection test, giving us the distance to the ground
let Some(distance) = ray.intersect_plane(plane_origin, plane) else {
// If the ray does not intersect the ground
// (the camera is not looking towards the ground), we can't do anything
return;
};
// use the distance to compute the actual point on the ground in world-space
let global_cursor = ray.get_point(distance);
mycoords.global = global_cursor;
eprintln!("Global cursor coords: {}/{}/{}",
global_cursor.x, global_cursor.y, global_cursor.z
);
// to compute the local coordinates, we need the inverse of the plane's transform
let inverse_transform_matrix = ground_transform.compute_matrix().inverse();
let local_cursor = inverse_transform_matrix.transform_point3(global_cursor);
// we can discard the Y coordinate, because it should always be zero
// (our point is supposed to be on the plane)
mycoords.local = local_cursor.xz();
eprintln!("Local cursor coords: {}/{}", local_cursor.x, local_cursor.z);
}
app.init_resource::<MyGroundCoords>();
app.add_systems(Startup, setup_3d_scene);
app.add_systems(Update, cursor_to_ground_plane);
If the ground is tilted/rotated or moved, the global and local coordinates will differ, and may be useful for different use cases, so we compute both.
For some examples:
- if you want to spawn a child entity, or to quantize the coordinates to a grid (for a tile-based game, to detect the grid tile under the cursor), the local coordinates will be more useful
- if you want to spawn some overlays, particle effects, other independent game entities, at the position of the cursor, the global coordinates will be more useful