Skip to main content

input_and_camera/
input_and_camera.rs

1use primback::prelude::*;
2
3struct InputCameraApp {
4    yaw: f32,
5    pitch: f32,
6    mouse_locked: bool,
7}
8
9impl PrimbackApp for InputCameraApp {
10    fn init(&mut self, update_ctx: &mut UpdateContext) {
11        // Set initial camera position
12        let camera = Camera::look_at(vec3(0.0, 2.0, 5.0), vec3(0.0, 1.0, 0.0), Vec3::Y);
13        update_ctx.set_camera(camera);
14
15        // Lock cursor on startup
16        update_ctx.set_cursor_grab(true);
17        update_ctx.set_cursor_visible(false);
18    }
19
20    fn update(&mut self, update_ctx: &mut UpdateContext) {
21        // Toggle cursor lock with Escape key
22        if update_ctx.is_key_pressed(KeyCode::Escape) {
23            self.mouse_locked = !self.mouse_locked;
24            update_ctx.set_cursor_grab(self.mouse_locked);
25            update_ctx.set_cursor_visible(!self.mouse_locked);
26        }
27
28        // Camera rotation (only when mouse is locked)
29        if self.mouse_locked {
30            let delta = update_ctx.mouse_delta();
31            let sensitivity = 0.15;
32            self.yaw -= delta.x * sensitivity;
33            self.pitch -= delta.y * sensitivity;
34            // Clamp pitch to prevent turning upside down
35            self.pitch = self.pitch.clamp(-89.0, 89.0);
36
37            // Update camera rotation
38            let mut camera = *update_ctx.camera();
39            camera.transform.rotation = Quat::from_euler(
40                EulerRot::YXZ,
41                self.yaw.to_radians(),
42                self.pitch.to_radians(),
43                0.0,
44            );
45            update_ctx.set_camera(camera);
46        }
47
48        // Camera movement (WASD + Space/Shift for Up/Down)
49        let mut camera = *update_ctx.camera();
50        let move_speed = 5.0 * update_ctx.delta_time();
51        let mut move_dir = Vec3::ZERO;
52
53        let forward = camera.transform.forward();
54        // Project forward vector on XZ plane to keep movement horizontal
55        let forward_horizontal = vec3(forward.x, 0.0, forward.z).normalize_or_zero();
56        let right = camera.transform.right();
57
58        if update_ctx.is_key_down(KeyCode::KeyW) {
59            move_dir += forward_horizontal;
60        }
61        if update_ctx.is_key_down(KeyCode::KeyS) {
62            move_dir -= forward_horizontal;
63        }
64        if update_ctx.is_key_down(KeyCode::KeyA) {
65            move_dir -= right;
66        }
67        if update_ctx.is_key_down(KeyCode::KeyD) {
68            move_dir += right;
69        }
70        if update_ctx.is_key_down(KeyCode::Space) {
71            move_dir += Vec3::Y;
72        }
73        if update_ctx.is_key_down(KeyCode::ShiftLeft) {
74            move_dir -= Vec3::Y;
75        }
76
77        camera.transform.position += move_dir.normalize_or_zero() * move_speed;
78        update_ctx.set_camera(camera);
79
80        // Play synth sound when left-clicking
81        if update_ctx.is_mouse_button_pressed(MouseButton::Left) {
82            update_ctx.play_synth(Synth::coin());
83        }
84    }
85
86    fn draw(&mut self, draw_ctx: &mut DrawContext) {
87        draw_ctx.clear(Color::new(0.1, 0.1, 0.12));
88
89        // Draw a simple 3D checkerboard floor
90        let grid_size = 10;
91        let spacing = 2.0;
92        for x in -grid_size..=grid_size {
93            for z in -grid_size..=grid_size {
94                let color = if (x + z) % 2 == 0 {
95                    Color::new(0.2, 0.2, 0.22)
96                } else {
97                    Color::new(0.15, 0.15, 0.17)
98                };
99                let pos = vec3(x as f32 * spacing, 0.0, z as f32 * spacing);
100                draw_ctx.draw_shape(
101                    Shape::plane(),
102                    Transform::new_position(pos).scale(vec3(spacing, 1.0, spacing)),
103                    color,
104                );
105            }
106        }
107
108        // Draw some colorful pillars as reference points in 3D space
109        for i in 0..8 {
110            let angle = (i as f32) * (std::f32::consts::PI / 4.0);
111            let radius = 6.0;
112            let px = angle.cos() * radius;
113            let pz = angle.sin() * radius;
114            let color = Color::from_hsv((i as f32) * 45.0, 0.8, 0.8);
115            draw_ctx.draw_shape(
116                Shape::cylinder().radius(0.3).height(2.0),
117                Transform::new_position(vec3(px, 1.0, pz)),
118                color,
119            );
120        }
121
122        // Draw 2D UI instructions on screen
123        let screen_w = draw_ctx.render_size().x;
124
125        // Draw guidance box
126        let ui_rect = Rect::new(vec2(280.0, 110.0))
127            .color(Color::new(0.0, 0.0, 0.0))
128            .radius(8.0)
129            .shadow_offset(vec2(2.0, 2.0));
130        let ui_trans = RectTransform::new_position(vec2(10.0, 10.0));
131        draw_ctx.draw_rect(ui_rect, ui_trans);
132
133        // Render instructions text inside the box
134        let instructions = [
135            "FPS Camera Controls:",
136            "- WASD: Move horizontally",
137            "- Space / Shift: Fly Up / Down",
138            "- Mouse: Look around",
139            "- Left Click: Play coin sound",
140            "- ESC: Toggle Mouse Lock",
141        ];
142
143        for (idx, line) in instructions.iter().enumerate() {
144            let text_trans = RectTransform::new_position(vec2(20.0, 18.0 + (idx as f32) * 14.0));
145            let color = if idx == 0 {
146                Color::new(1.0, 0.8, 0.2) // Highlight title
147            } else {
148                Color::WHITE
149            };
150            draw_ctx.draw_text(Text::new(*line).scale(1.0), text_trans, color);
151        }
152
153        // Display current lock status at the bottom center
154        let status_str = if self.mouse_locked {
155            "Mouse Locked (ESC to Unlock)"
156        } else {
157            "Mouse Unlocked (ESC to Lock)"
158        };
159        let status_color = if self.mouse_locked {
160            Color::new(0.2, 0.8, 0.2)
161        } else {
162            Color::new(0.8, 0.2, 0.2)
163        };
164        let text_size = draw_ctx.measure_text(&Text::new(status_str));
165        let status_trans = RectTransform::new_position(vec2(
166            (screen_w - text_size.x) * 0.5,
167            draw_ctx.render_size().y - 25.0,
168        ));
169        draw_ctx.draw_text(
170            Text::new(status_str)
171                .scale(1.0)
172                .shadow_offset(vec2(1.0, 1.0)),
173            status_trans,
174            status_color,
175        );
176    }
177}
178
179fn main() {
180    let config = PrimbackConfig {
181        window_title: "Primback - Input and Camera Demo".to_string(),
182        render_width: 640,
183        render_height: 480,
184        ..Default::default()
185    };
186
187    Primback::run(
188        config,
189        InputCameraApp {
190            yaw: 0.0,
191            pitch: 0.0,
192            mouse_locked: true,
193        },
194    );
195}