Skip to main content

optic_loop/
game.rs

1/// The high-level [`Game`] — owns all engine subsystems and drives a
2/// [`Runtime`] implementation through the winit event loop.
3///
4/// # Architecture
5///
6/// `Game` aggregates every subsystem the engine provides:
7///
8/// | Field | Type | Purpose |
9/// |---|---|---|
10/// | `renderer` | [`GPU`] | GL context, pipeline state, fallback assets |
11/// | `camera` | [`Camera`] | Active view/projection |
12/// | `events` | [`Events`] | Per-frame input collection |
13/// | `time` | [`Time`] | Delta time, FPS, elapsed |
14/// | `window` | [`Window`] | Application window |
15///
16/// # Lifecycle
17///
18/// ```text
19/// Game::new(runtime) ──► Game::run(runtime)
20///                            │
21///                            ▼
22///                     ┌─► Runtime::start  (once)
23///                     │        │
24///                     │        ▼
25///                     │   Runtime::update  (every frame)
26///                     │        │
27///                     └────────┘
28///                            │
29///                            ▼
30///                     Runtime::end  (on shutdown)
31/// ```
32///
33/// # Example
34///
35/// ```ignore
36/// use optic_loop::{Game, Runtime};
37///
38/// struct App;
39///
40/// impl Runtime for App {
41///     fn start(&mut self, game: &mut Game) {
42///         // Load assets, set up scene
43///         game.renderer.set_bg_color((0.1, 0.2, 0.3, 1.0).into());
44///     }
45///
46///     fn update(&mut self, game: &mut Game) {
47///         // Per-frame logic
48///         game.renderer.clear();
49///         // ... draw calls ...
50///     }
51///
52///     fn end(&mut self, _game: &mut Game) {
53///         // Save state, disconnect
54///     }
55/// }
56///
57/// Game::run(App);
58/// ```
59
60use gilrs::Gilrs;
61use optic_core::{log_error, CamProj, Coord2D, OpticResult, Size2D, CRIMSON};
62use optic_core::{end, end_success, ERROR, SUCCESS};
63use optic_render::{Camera, GPU};
64use optic_window::{Events, Window};
65use winit::application::ApplicationHandler;
66use winit::event::WindowEvent;
67use winit::event_loop::{ActiveEventLoop, EventLoop};
68use winit::window::WindowId;
69
70#[cfg(feature = "online")]
71use optic_online::NetworkHandle;
72
73use crate::{Runtime, Time};
74
75/// The primary game object — aggregates the renderer, camera, window, events,
76/// timing, gamepad, and user-provided [`Runtime`].
77///
78/// Create via [`Game::new`] and start via [`Game::run`]. All fields are public
79/// so that [`Runtime`] methods can access them directly.
80pub struct Game {
81    pub renderer: GPU,
82    pub camera: Camera,
83    pub events: Events,
84    pub time: Time,
85    pub window: Window,
86
87    event_loop: Option<EventLoop<()>>,
88    surface_index: usize,
89    gilrs: Gilrs,
90    runtime: Option<Box<dyn Runtime>>,
91    running: bool,
92    started: bool,
93    requested_size: Size2D,
94    resized_once: bool,
95
96    #[cfg(feature = "online")]
97    pub(crate) network: Option<NetworkHandle>,
98}
99
100impl Game {
101    /// Creates a new game with a 500×500 window and a crimson background.
102    ///
103    /// Initialises:
104    ///
105    /// - A [`GPU`] with VSync enabled and the given background colour
106    /// - A perspective [`Camera`]
107    /// - Gamepad input via `gilrs`
108    /// - The user's [`Runtime`] implementation
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the window, EGL/GLX surface, or gamepad cannot
113    /// be initialised.
114    ///
115    /// ```ignore
116    /// let game = Game::new(MyRuntime)?;
117    /// ```
118    pub fn new<R: Runtime + 'static>(runtime: R) -> OpticResult<Game> {
119        let size = Size2D::from(500,500);
120        let bg_color = CRIMSON;
121        let title = "OPTIC GAME";
122        let el = EventLoop::builder()
123           .build()
124           .map_err(|e| optic_core::OpticError::custom(&format!("event loop creation failed: {e}")))?;
125        let window = Window::new(&el, title, size);
126        let actual_size = window.size();
127        let handle = window.raw_handle()
128            .ok_or_else(|| optic_core::OpticError::custom("failed to get raw window handle"))?;
129        let display_handle = window.raw_display_handle()
130            .ok_or_else(|| optic_core::OpticError::custom("failed to get raw display handle"))?;
131
132        let mut gpu = GPU::new_windowed(handle, display_handle, actual_size)?;
133        gpu.ctx.set_vsync(true);
134        gpu.canvas_size = actual_size;
135        gpu.set_bg_color(bg_color);
136        let surface_index = 0;
137
138        let gilrs = Gilrs::new()
139            .map_err(|e| optic_core::OpticError::custom(&format!("gilrs init failed: {e}")))?;
140        Ok(Game {
141            renderer: gpu,
142            camera: Camera::new(size, CamProj::Persp),
143            events: Events::new(),
144            time: Time::new(),
145            event_loop: Some(el),
146            window,
147            surface_index,
148            gilrs,
149            runtime: Some(Box::new(runtime)),
150            running: true,
151            started: false,
152            requested_size: size,
153            resized_once: false,
154            #[cfg(feature = "online")]
155            network: None,
156        })
157    }
158
159    /// Convenience entry point: creates a [`Game`] and runs the event loop.
160    ///
161    /// On success exits with `SUCCESS`; on failure logs and exits with
162    /// `ERROR`.
163    ///
164    /// This is the simplest way to start an Optic application:
165    ///
166    /// ```ignore
167    /// Game::run(MyRuntime);
168    /// ```
169    pub fn run<R: Runtime + 'static>(runtime: R) {
170        match Game::new(runtime) {
171            Ok(game) => {
172                game.start();
173                end(SUCCESS);
174            }
175            Err(e) => {
176                log_error!("{}", e);
177                end(ERROR);
178            }
179        }
180    }
181
182    fn start(mut self) {
183        let el = self.event_loop.take().unwrap();
184        let _ = el.run_app(&mut self);
185    }
186
187    /// Signals the game loop to exit gracefully on the next frame.
188    ///
189    /// After calling this, [`Runtime::end`] will be invoked and the process
190    /// will exit with `SUCCESS`.
191    ///
192    /// ```ignore
193    /// // In your runtime:
194    /// fn update(&mut self, game: &mut Game) {
195    ///     if game.events.key_down(VirtualKeyCode::Escape) {
196    ///         game.exit();
197    ///     }
198    /// }
199    /// ```
200    pub fn exit(&mut self) {
201        self.running = false;
202    }
203
204    /// Returns a reference to the [`NetworkHandle`] if networking is enabled.
205    #[cfg(feature = "online")]
206    pub fn network(&self) -> Option<&NetworkHandle> {
207        self.network.as_ref()
208    }
209
210    /// Returns a mutable reference to the [`NetworkHandle`] if networking is enabled.
211    #[cfg(feature = "online")]
212    pub fn network_mut(&mut self) -> Option<&mut NetworkHandle> {
213        self.network.as_mut()
214    }
215
216    /// Enables networking with the given configuration.
217    ///
218    /// Spawns a background network thread. Call early in [`Runtime::start`]
219    /// before any network-dependent logic runs.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the connection cannot be established.
224    #[cfg(feature = "online")]
225    pub fn enable_networking(&mut self, config: optic_core::NetworkConfig) -> OpticResult<()> {
226        let handle = NetworkHandle::new(config)?;
227        self.network = Some(handle);
228        Ok(())
229    }
230
231}
232
233impl ApplicationHandler for Game {
234    fn resumed(&mut self, _el: &ActiveEventLoop) {
235        self.time.start_time = std::time::Instant::now();
236        self.time.prev_time = std::time::Instant::now();
237        self.time.prev_sec = std::time::Instant::now();
238    }
239
240    fn window_event(
241        &mut self,
242        _el: &ActiveEventLoop,
243        id: WindowId,
244        event: WindowEvent,
245    ) {
246        if !self.window.is_running() { return; }
247        if self.window.id().unwrap() != id { return; }
248
249        match &event {
250            WindowEvent::Resized(_size) => {
251                self.renderer.ctx.resize_window(self.surface_index, self.window.size());
252                let _ = self.renderer.ctx.make_current(self.surface_index);
253                self.renderer.canvas_size = self.window.size();
254                self.camera.set_size(self.window.size());
255                if !self.resized_once && (_size.width != self.requested_size.w || _size.height != self.requested_size.h) {
256                    self.resized_once = true;
257                    self.window.set_size(self.requested_size);
258                }
259            }
260            WindowEvent::CursorMoved { position, .. } => {
261                self.window.notify_cursor_moved(Coord2D::from(position.x, position.y));
262            }
263            WindowEvent::CursorEntered { .. } => {
264                self.window.notify_cursor_inside(true);
265            }
266            WindowEvent::CursorLeft { .. } => {
267                self.window.notify_cursor_inside(false);
268            }
269            WindowEvent::CloseRequested => {
270                self.events.close_requested = true;
271            }
272            _ => {}
273        }
274        self.events.process_window_event(&event, &self.window);
275    }
276
277    fn about_to_wait(&mut self, el: &ActiveEventLoop) {
278        if !self.running || self.window.is_closed() {
279            #[cfg(feature = "online")]
280            if let Some(mut net) = self.network.take() {
281                net.shutdown();
282            }
283            if let Some(mut runtime) = self.runtime.take() {
284                runtime.end(self);
285            }
286            end_success();
287            el.exit();
288            return;
289        }
290
291        while let Some(gilrs_event) = self.gilrs.next_event() {
292            self.events.process_gilrs_event(&gilrs_event);
293        }
294
295        #[cfg(feature = "online")]
296        if let Some(net) = &mut self.network {
297            net.poll(&mut self.events.network);
298        }
299
300        let _ = self.renderer.ctx.make_current(self.surface_index);
301        self.renderer.clear();
302        self.time.update();
303
304        self.window.update_frame();
305
306        self.camera.pre_update();
307
308        let mut runtime = self.runtime.take().unwrap();
309        if !self.started {
310            runtime.start(self);
311            self.started = true;
312            self.window.set_visible(true);
313            self.window.center_on_screen();
314        }
315        runtime.update(self);
316        self.runtime = Some(runtime);
317
318        let _ = self.renderer.ctx.swap_buffers(self.surface_index);
319        self.events.end_frame();
320        self.window.request_redraw();
321    }
322}