Skip to main content

blockgame/
blockgame.rs

1// Block Game, Enes Altun, 2025, MIT License
2
3use cuneus::compute::*;
4use cuneus::prelude::*;
5use winit::event::ElementState;
6
7#[repr(C)]
8#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
9struct BlockGameParams {
10    // 0=menu, 1=playing, 2=game_over
11    game_state: i32,
12    score: u32,
13    current_block: u32,
14    total_blocks: u32,
15
16    block_x: f32,
17    block_y: f32,
18    block_z: f32,
19
20    block_width: f32,
21    block_height: f32,
22    block_depth: f32,
23
24    movement_speed: f32,
25    movement_range: f32,
26    drop_triggered: i32,
27
28    camera_height: f32,
29    camera_angle: f32,
30    camera_scale: f32,
31
32    // Game mech
33    perfect_placement: i32,
34    game_over: i32,
35
36    _padding: [f32; 2],
37}
38
39impl Default for BlockGameParams {
40    fn default() -> Self {
41        Self {
42            game_state: 0,
43            score: 0,
44            current_block: 0,
45            total_blocks: 1,
46
47            block_x: 0.0,
48            block_y: 1.0,
49            block_z: 0.0,
50
51            block_width: 3.0,
52            block_height: 0.6,
53            block_depth: 3.0,
54
55            movement_speed: 2.0,
56            movement_range: 2.5,
57            drop_triggered: 0,
58
59            camera_height: 0.0,
60            camera_angle: 0.0,
61            camera_scale: 65.0,
62
63            perfect_placement: 0,
64            game_over: 0,
65
66            _padding: [0.0; 2],
67        }
68    }
69}
70
71impl UniformProvider for BlockGameParams {
72    fn as_bytes(&self) -> &[u8] {
73        bytemuck::bytes_of(self)
74    }
75}
76
77struct BlockTowerGame {
78    base: RenderKit,
79    compute_shader: ComputeShader,
80    last_mouse_click: bool,
81    game_params: BlockGameParams,
82}
83
84impl ShaderManager for BlockTowerGame {
85    fn init(core: &Core) -> Self {
86        let base = RenderKit::new(core);
87
88        // Create single-pass compute shader with mouse, fonts, and game storage
89        let config = ComputeShader::builder()
90            .with_entry_point("main")
91            .with_mouse()
92            .with_fonts()
93            .with_audio(1024) // Used for game state storage, not audio
94            .with_workgroup_size([8, 8, 1])
95            .with_texture_format(COMPUTE_TEXTURE_FORMAT_RGBA16)
96            .with_label("Block Tower Game Unified")
97            .build();
98
99        let compute_shader = cuneus::compute_shader!(core, "shaders/blockgame.wgsl", config);
100
101
102        Self {
103            base,
104            compute_shader,
105            last_mouse_click: false,
106            game_params: BlockGameParams::default(),
107        }
108    }
109
110    fn update(&mut self, core: &Core) {
111        let current_time = self.base.controls.get_time(&self.base.start_time);
112        let delta = 1.0 / 60.0;
113        self.compute_shader
114            .set_time(current_time, delta, &core.queue);
115        self.compute_shader
116            .update_mouse_uniform(&self.base.mouse_tracker.uniform, &core.queue);
117
118        self.update_camera_in_shader(&core.queue);
119        let mouse_buttons = self.base.mouse_tracker.uniform.buttons[0];
120        let mouse_pressed = mouse_buttons & 1 != 0;
121        self.last_mouse_click = mouse_pressed;
122    }
123
124    fn resize(&mut self, core: &Core) {
125        self.base.default_resize(core, &mut self.compute_shader);
126    }
127
128    fn render(&mut self, core: &Core) -> Result<(), cuneus::SurfaceError> {
129        let mut frame = self.base.begin_frame(core)?;
130        let _controls_request = self
131            .base
132            .controls
133            .get_ui_request(&self.base.start_time, &core.size, self.base.fps_tracker.fps());
134
135        let full_output = if self.base.key_handler.show_ui {
136            self.base.render_ui(core, |ctx| {
137                RenderKit::apply_default_style(ctx);
138                egui::Window::new("Block Tower")
139                    .collapsible(true)
140                    .resizable(true)
141                    .default_width(220.0)
142                    .show(ctx, |ui| {
143                        egui::CollapsingHeader::new("Camera")
144                            .default_open(true)
145                            .show(ui, |ui| {
146                                ui.add(
147                                    egui::Slider::new(
148                                        &mut self.game_params.camera_height,
149                                        0.0..=20.0,
150                                    )
151                                    .text("Height"),
152                                );
153                                ui.add(
154                                    egui::Slider::new(
155                                        &mut self.game_params.camera_angle,
156                                        -3.14159..=3.14159,
157                                    )
158                                    .text("Angle"),
159                                );
160                                ui.add(
161                                    egui::Slider::new(
162                                        &mut self.game_params.camera_scale,
163                                        20.0..=200.0,
164                                    )
165                                    .text("Scale"),
166                                );
167
168                                ui.separator();
169                                ui.label("Controls:");
170                                ui.label("Q/E: Move up/down");
171                                ui.label("W/S: Rotate left/right");
172
173                                ui.separator();
174                                ui.label("Scale presets:");
175                                ui.horizontal(|ui| {
176                                    if ui.button("1080p").clicked() {
177                                        self.game_params.camera_scale = 50.0;
178                                    }
179                                    if ui.button("1440p").clicked() {
180                                        self.game_params.camera_scale = 65.0;
181                                    }
182                                    if ui.button("4K").clicked() {
183                                        self.game_params.camera_scale = 100.0;
184                                    }
185                                });
186
187                                if ui.button("Reset Camera").clicked() {
188                                    self.game_params.camera_height = 8.0;
189                                    self.game_params.camera_angle = 0.0;
190                                    self.game_params.camera_scale = 65.0;
191                                }
192                            });
193                    });
194            })
195        } else {
196            self.base.render_ui(core, |_ctx| {})
197        };
198
199
200        self.compute_shader.dispatch(&mut frame.encoder, core);
201
202        self.base.renderer.render_to_view(&mut frame.encoder, &frame.view, &self.compute_shader.get_output_texture().bind_group);
203
204        self.base.end_frame(core, frame, full_output);
205        Ok(())
206    }
207
208    fn handle_input(&mut self, core: &Core, event: &WindowEvent) -> bool {
209        let ui_handled = self
210            .base
211            .egui_state
212            .on_window_event(core.window(), event)
213            .consumed;
214
215        if self.base.handle_mouse_input(core, event, ui_handled) {
216            return true;
217        }
218
219        if let WindowEvent::KeyboardInput { event, .. } = event {
220            if let winit::keyboard::PhysicalKey::Code(key_code) = event.physical_key {
221                if event.state == ElementState::Pressed {
222                    let camera_speed = 0.5;
223
224                    match key_code {
225                        winit::keyboard::KeyCode::KeyQ => {
226                            self.game_params.camera_height += camera_speed;
227                            return true;
228                        }
229                        winit::keyboard::KeyCode::KeyE => {
230                            self.game_params.camera_height -= camera_speed;
231                            return true;
232                        }
233                        winit::keyboard::KeyCode::KeyW => {
234                            self.game_params.camera_angle += 0.1;
235                            return true;
236                        }
237                        winit::keyboard::KeyCode::KeyS => {
238                            self.game_params.camera_angle -= 0.1;
239                            return true;
240                        }
241                        _ => {}
242                    }
243                }
244            }
245            return self
246                .base
247                .key_handler
248                .handle_keyboard_input(core.window(), event);
249        }
250
251        false
252    }
253}
254
255impl BlockTowerGame {
256    fn update_camera_in_shader(&self, queue: &wgpu::Queue) {
257        if let Some(audio_buffer) = self.compute_shader.get_audio_buffer() {
258            let camera_data = [
259                self.game_params.camera_height,
260                self.game_params.camera_angle,
261                self.game_params.camera_scale,
262            ];
263
264            let camera_data_bytes = bytemuck::cast_slice(&camera_data);
265            let offset = 5 * std::mem::size_of::<f32>();
266
267            queue.write_buffer(audio_buffer, offset as u64, camera_data_bytes);
268        }
269    }
270}
271
272fn main() -> Result<(), Box<dyn std::error::Error>> {
273    env_logger::init();
274    cuneus::gst::init()?;
275
276    let (app, event_loop) = ShaderApp::new("Block Tower Game", 600, 800);
277
278    app.run(event_loop, BlockTowerGame::init)
279}