Skip to content

Commit 39f5b7d

Browse files
committed
build: initial follow mode
1 parent c8b5711 commit 39f5b7d

File tree

9 files changed

+356
-16
lines changed

9 files changed

+356
-16
lines changed

.cargo/config.toml

+3
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ run_wasm = "run --release --package run_wasm --"
33
# Other crates use the alias run-wasm, even though crate names should use `_`s not `-`s
44
# Allow this to be used
55
run-wasm = "run_wasm"
6+
7+
[target.'cfg(target_family = "wasm")']
8+
rustflags = ["--cfg=web_sys_unstable_apis"]

Cargo.toml

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,42 @@
1-
[package]
2-
name = "bevy_ogle"
1+
[workspace]
2+
resolver = "2"
3+
members = ["examples/run_wasm", "examples/simple"]
4+
5+
[workspace.package]
36
edition = "2021"
47
version = "0.1.0"
58
license = "MIT OR Apache-2.0"
6-
description = "A multi-mode camera library for 2d games"
79
repository = "https://github.com/loopystudios/bevy_ogle"
10+
11+
[package]
12+
name = "bevy_ogle"
13+
version.workspace = true
14+
license.workspace = true
15+
edition.workspace = true
16+
repository.workspace = true
17+
description = "A multi-mode camera library for 2d games"
818
authors = ["Spencer C. Imbleau"]
919
keywords = ["bevy"]
1020
categories = ["game-development", "wasm"]
1121
readme = "README.md"
1222

1323
[lib]
1424

25+
26+
[workspace.dependencies]
27+
bevy = { version = "0.14.0", default-features = false }
28+
1529
[dependencies]
16-
bevy = { version = "0.14", default-features = false }
17-
bevy_pancam = "0.11.1"
30+
bevy = { workspace = true, default-features = false, features = [
31+
"bevy_state",
32+
"bevy_core_pipeline",
33+
] }
34+
bevy_pancam = { git = "https://github.com/tomara-x/bevy_pancam", features = [
35+
"bevy_egui",
36+
] }
37+
#bevy_pancam = "0.12.0"
1838
dolly = "0.5.0"
39+
mint = "0.5.8"
1940

2041
[dev-dependencies]
2142
wasm-bindgen-test = "0.3.42"

examples/simple.rs

-1
This file was deleted.

examples/simple/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "simple"
3+
version.workspace = true
4+
license.workspace = true
5+
edition.workspace = true
6+
repository.workspace = true
7+
publish = false
8+
9+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10+
[dependencies]
11+
bevy = { workspace = true, default-features = true }
12+
bevy_ogle = { path = "../.." }
13+
bevy_egui = { version = "0.28.0" }
14+
rand = "0.8.5"

examples/simple/src/main.rs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use bevy::{color::palettes::css, prelude::*};
2+
use bevy_egui::{egui, EguiContexts, EguiPlugin};
3+
use bevy_ogle::{prelude::*, OglePlugin};
4+
use rand::random;
5+
6+
#[derive(Component)]
7+
struct ThingToFollow;
8+
9+
fn main() {
10+
App::new()
11+
.add_plugins(DefaultPlugins)
12+
.add_plugins(EguiPlugin)
13+
.add_plugins(OglePlugin)
14+
.add_systems(Startup, setup_background)
15+
.add_systems(Startup, |mut commands: Commands| {
16+
commands.spawn(Camera2dBundle::default());
17+
// Create target, begin following it.
18+
let entity = commands
19+
.spawn((ThingToFollow, SpatialBundle::default()))
20+
.id();
21+
commands.ogle_change_mode(OgleMode::Frozen);
22+
commands.ogle_target_entity(entity);
23+
})
24+
.add_systems(Update, move_target)
25+
.add_systems(Update, control_camera_ui)
26+
.run();
27+
}
28+
29+
fn setup_background(mut commands: Commands) {
30+
let n = 20;
31+
let spacing = 50.;
32+
let offset = spacing * n as f32 / 2.;
33+
let custom_size = Some(Vec2::new(spacing, spacing));
34+
for x in 0..n {
35+
for y in 0..n {
36+
let x = x as f32 * spacing - offset;
37+
let y = y as f32 * spacing - offset;
38+
let color = Color::hsl(240., random::<f32>() * 0.3, random::<f32>() * 0.3);
39+
commands.spawn(SpriteBundle {
40+
sprite: Sprite {
41+
color,
42+
custom_size,
43+
..default()
44+
},
45+
transform: Transform::from_xyz(x, y, 0.),
46+
..default()
47+
});
48+
}
49+
}
50+
}
51+
52+
fn move_target(
53+
time: Res<Time>,
54+
mut query_thing: Query<&mut Transform, With<ThingToFollow>>,
55+
mut gizmos: Gizmos,
56+
) {
57+
let mut transform = query_thing.single_mut();
58+
transform.translation.x += time.delta_seconds() * (random::<f32>() * 500.0 - 500.0 / 2.0);
59+
transform.translation.y += time.delta_seconds() * (random::<f32>() * 500.0 - 500.0 / 2.0);
60+
gizmos.rect_2d(transform.translation.xy(), 0.0, (5.0, 5.0).into(), css::RED);
61+
}
62+
63+
fn control_camera_ui(
64+
mut contexts: EguiContexts,
65+
query_thing: Query<Entity, With<ThingToFollow>>,
66+
mode: Res<State<OgleMode>>,
67+
mut next_mode: ResMut<NextState<OgleMode>>,
68+
) {
69+
let window = egui::Window::new("Camera Controls")
70+
.anchor(egui::Align2::LEFT_TOP, [25.0, 25.0])
71+
.resizable(false)
72+
.title_bar(true);
73+
window.show(contexts.ctx_mut(), |ui| {
74+
let mut set_mode = mode.clone();
75+
if ui
76+
.radio_value(&mut set_mode, OgleMode::Frozen, "Frozen")
77+
.clicked()
78+
|| ui
79+
.radio_value(&mut set_mode, OgleMode::Following, "Following")
80+
.clicked()
81+
|| ui
82+
.radio_value(&mut set_mode, OgleMode::Choreographed, "Choreographed")
83+
.clicked()
84+
|| ui
85+
.radio_value(&mut set_mode, OgleMode::Pancam, "Pancam")
86+
.clicked()
87+
{
88+
next_mode.set(set_mode);
89+
}
90+
});
91+
}

src/commands.rs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use bevy::{ecs::world::Command, prelude::*};
2+
3+
use crate::{OgleMode, OgleTarget};
4+
5+
pub enum OgleCommand {
6+
TargetPosition(Vec2),
7+
TargetEntity(Entity),
8+
ClearTarget,
9+
ChangeMode(OgleMode),
10+
}
11+
12+
pub trait OgleCommandExt {
13+
fn ogle_target_position(&mut self, pos: Vec2);
14+
fn ogle_target_entity(&mut self, entity: Entity);
15+
fn ogle_clear_target(&mut self);
16+
fn ogle_change_mode(&mut self, mode: OgleMode);
17+
}
18+
19+
impl<'w, 's> OgleCommandExt for Commands<'w, 's> {
20+
fn ogle_target_position(&mut self, pos: Vec2) {
21+
self.add(OgleCommand::TargetPosition(pos));
22+
}
23+
24+
fn ogle_target_entity(&mut self, entity: Entity) {
25+
self.add(OgleCommand::TargetEntity(entity));
26+
}
27+
28+
fn ogle_clear_target(&mut self) {
29+
self.add(OgleCommand::ClearTarget);
30+
}
31+
32+
fn ogle_change_mode(&mut self, mode: OgleMode) {
33+
self.add(OgleCommand::ChangeMode(mode));
34+
}
35+
}
36+
37+
impl Command for OgleCommand {
38+
fn apply(self, world: &mut World) {
39+
match self {
40+
OgleCommand::TargetEntity(entity) => {
41+
let mut target = world.resource_mut::<OgleTarget>();
42+
*target = OgleTarget::Entity(entity);
43+
}
44+
OgleCommand::TargetPosition(pos) => {
45+
let mut target = world.resource_mut::<OgleTarget>();
46+
*target = OgleTarget::Position(pos);
47+
}
48+
OgleCommand::ClearTarget => {
49+
let mut target = world.resource_mut::<OgleTarget>();
50+
*target = OgleTarget::None;
51+
}
52+
OgleCommand::ChangeMode(mode) => {
53+
let mut next_mode = world.resource_mut::<NextState<OgleMode>>();
54+
next_mode.set(mode);
55+
}
56+
}
57+
}
58+
}

src/lib.rs

+37-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1-
pub fn add(left: usize, right: usize) -> usize {
2-
left + right
3-
}
1+
use bevy::prelude::*;
2+
3+
mod rig;
4+
5+
mod commands;
6+
pub use commands::OgleCommandExt;
47

5-
#[cfg(test)]
6-
mod tests {
7-
use super::*;
8+
mod plugin;
9+
pub use plugin::OglePlugin;
10+
11+
#[derive(Resource, Debug)]
12+
pub enum OgleTarget {
13+
Position(Vec2),
14+
Entity(Entity),
15+
None,
16+
}
817

9-
#[test]
10-
fn it_works() {
11-
let result = add(2, 2);
12-
assert_eq!(result, 4);
18+
impl Default for OgleTarget {
19+
fn default() -> Self {
20+
Self::Position(Vec2 { x: 0.0, y: 0.0 })
1321
}
1422
}
23+
24+
#[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)]
25+
pub enum OgleMode {
26+
/// The camera will not move under normal circumstances.
27+
#[default]
28+
Frozen,
29+
/// The camera should exponentially follow its target.
30+
Following,
31+
/// The camera is being choreographed and should mirror its target position exactly.
32+
///
33+
/// This is useful when the camera follows a spline, or you don't want loose following behavior.
34+
Choreographed,
35+
/// The camera is in a detached pancam mode.
36+
Pancam,
37+
}
38+
39+
pub mod prelude {
40+
pub use super::{OgleCommandExt, OgleMode, OgleTarget};
41+
}

src/plugin.rs

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use crate::{rig::OgleRig, OgleMode, OgleTarget};
2+
use bevy::prelude::*;
3+
use bevy_pancam::{PanCam, PanCamPlugin};
4+
use dolly::prelude::*;
5+
6+
// TODO: These should be configurable and play well with pancam
7+
const MIN_ZOOM: f32 = 0.01;
8+
const MAX_ZOOM: f32 = 3.0;
9+
const ZOOM_SPEED: f32 = 2.0;
10+
11+
#[derive(Default)]
12+
pub struct OglePlugin;
13+
14+
impl Plugin for OglePlugin {
15+
fn build(&self, app: &mut App) {
16+
app.init_resource::<OgleRig>()
17+
.init_resource::<OgleTarget>()
18+
.init_state::<OgleMode>()
19+
.add_plugins(PanCamPlugin)
20+
.add_systems(Startup, setup)
21+
.add_systems(Update, follow_target.run_if(in_state(OgleMode::Following)))
22+
.add_systems(
23+
Update,
24+
choreograph_target.run_if(in_state(OgleMode::Choreographed)),
25+
)
26+
.add_systems(OnEnter(OgleMode::Pancam), on_enter_pancam)
27+
.add_systems(OnExit(OgleMode::Pancam), on_exit_pancam);
28+
}
29+
}
30+
31+
fn setup(mut commands: Commands) {
32+
// TODO: Settings for pancam handled consistently with other ogle settings
33+
commands.spawn(bevy_pancam::PanCam::default());
34+
}
35+
36+
fn on_enter_pancam(mut query: Query<&mut PanCam>) {
37+
let mut pancam = query.single_mut();
38+
info!("Enabling pancam");
39+
pancam.enabled = true;
40+
}
41+
42+
fn on_exit_pancam(mut query: Query<&mut PanCam>) {
43+
let mut pancam = query.single_mut();
44+
info!("Disabling pancam");
45+
pancam.enabled = false;
46+
}
47+
48+
fn choreograph_target() {
49+
todo!("handle camera choreographs, like following a spline")
50+
}
51+
52+
fn follow_target(
53+
time: Res<Time>,
54+
target: Res<OgleTarget>,
55+
mut rig: ResMut<OgleRig>,
56+
query_transform: Query<&Transform, Without<Camera>>,
57+
mut query_cam: Query<&mut Transform, With<Camera>>,
58+
mut proj_query: Query<&mut OrthographicProjection>,
59+
keyboard_input: Res<ButtonInput<KeyCode>>,
60+
) {
61+
let mut proj = proj_query.single_mut();
62+
if keyboard_input.pressed(KeyCode::ArrowUp) {
63+
rig.driver_mut::<Arm>().offset.z -= proj.scale * ZOOM_SPEED * time.delta_seconds();
64+
}
65+
if keyboard_input.pressed(KeyCode::ArrowDown) {
66+
rig.driver_mut::<Arm>().offset.z += proj.scale * ZOOM_SPEED * time.delta_seconds();
67+
}
68+
rig.driver_mut::<Arm>().offset.z = rig.driver_mut::<Arm>().offset.z.clamp(MIN_ZOOM, MAX_ZOOM);
69+
70+
match *target {
71+
OgleTarget::Position(pos) => {
72+
rig.driver_mut::<Position>().position = mint::Point3 {
73+
x: pos.x,
74+
y: pos.y,
75+
z: 0.0,
76+
};
77+
}
78+
OgleTarget::Entity(e) => {
79+
// TODO: Handle errors
80+
let transform = query_transform
81+
.get(e)
82+
.expect("entity target has no transform");
83+
rig.driver_mut::<Position>().position = mint::Point3 {
84+
x: transform.translation.x,
85+
y: transform.translation.y,
86+
z: 0.0,
87+
};
88+
}
89+
OgleTarget::None => {}
90+
}
91+
92+
rig.update(time.delta_seconds());
93+
94+
if let Some(mut camera_transform) = query_cam.iter_mut().next() {
95+
camera_transform.translation.x = rig.final_transform.position.x;
96+
camera_transform.translation.y = rig.final_transform.position.y;
97+
camera_transform.rotation = Quat::from_xyzw(
98+
rig.final_transform.rotation.v.x,
99+
rig.final_transform.rotation.v.y,
100+
rig.final_transform.rotation.v.z,
101+
rig.final_transform.rotation.s,
102+
);
103+
proj.scale = rig.final_transform.position.z;
104+
}
105+
}

0 commit comments

Comments
 (0)