Building a Rust Game Engine: Part 5 - Window Abstraction
Following the Cherno’s game engine series, we’ve implemented window abstraction - the foundation for displaying graphics.
Design Philosophy
As Cherno discusses, there’s a balance between:
- Software engineering: Logging, events, and other systems should come before windowing
- Entertainment/Education: A visible window keeps motivation high
We’ve built logging and events first, and now we can create windows that integrate with our event system.
Platform Abstraction
The key insight is that while we use winit (the Rust equivalent of GLFW) for cross-platform windowing, we still want:
- An abstract Window trait - platform-independent interface
- Platform-specific implementations - currently
winit, but we could add native Win32/DirectX support later
This mirrors Hazel’s architecture where there’s a Window base class and platform-specific implementations.
Architecture
engine/src/ggengine/
window.rs # Abstract Window trait + WindowProps
platform/
mod.rs # Platform module, create_window() factory
winit_window.rs # Winit-based implementation
Window Trait
The abstract Window trait defines what any window must provide:
pub trait Window {
fn on_update(&mut self);
fn width(&self) -> u32;
fn height(&self) -> u32;
fn set_event_callback(&mut self, callback: EventCallbackFn);
fn set_vsync(&mut self, enabled: bool);
fn is_vsync(&self) -> bool;
fn should_close(&self) -> bool;
}
WindowProps
Similar to Hazel, we have a properties struct for window creation:
pub struct WindowProps {
pub title: String,
pub width: u32,
pub height: u32,
}
impl Default for WindowProps {
fn default() -> Self {
Self {
title: "GGEngine".to_string(),
width: 1280,
height: 720,
}
}
}
Winit vs GLFW
In the Rust ecosystem, winit is the standard windowing library (rather than GLFW):
- Cross-platform: Windows, macOS, Linux, and more
- Modern event loop: Uses an application handler pattern
- Well-maintained: Active community and updates
One key difference from GLFW is that winit “owns” the event loop through run_app(). We handle this with:
pub fn run_window_loop(window: &mut WinitWindow, mut app_tick: impl FnMut()) {
let event_loop = window.event_loop.take().expect("Event loop already consumed");
let mut app = WinitApp::new(window, Some(Box::new(move || {
app_tick();
})));
event_loop.run_app(&mut app).expect("Event loop error");
}
Event Integration
The window implementation converts winit events to our engine’s event types:
fn handle_window_event(&mut self, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
self.window.data.should_close = true;
WinitWindow::dispatch_event(
&mut self.window.data,
EventKind::WindowClose(WindowCloseEvent::new()),
);
}
WindowEvent::Resized(PhysicalSize { width, height }) => {
self.window.data.width = width;
self.window.data.height = height;
WinitWindow::dispatch_event(
&mut self.window.data,
EventKind::WindowResize(WindowResizeEvent::new(width, height)),
);
}
// ... keyboard, mouse events
}
}
Key Code Mapping
We upgraded KeyCode from a simple u32 to a proper enum:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum KeyCode {
Unknown = 0,
Space = 32,
// A-Z, 0-9, F1-F24, etc.
A = 65,
Escape = 256,
LeftShift = 340,
// ...
}
This provides type safety and proper debug output when logging key events.
Usage Example
use engine::{Window, WindowProps};
use engine::platform::{create_winit_window, run_window_loop};
use engine::events::EventKind;
fn run(&mut self) {
let props = WindowProps::new("Sandbox", 1280, 720);
let mut window = create_winit_window(props);
window.set_event_callback(Box::new(|event| {
match &event {
EventKind::WindowClose(_) => {
gg::info!("Window close requested");
}
EventKind::KeyPressed(e) => {
gg::trace!("Key: {:?}", e.key_code);
}
_ => {}
}
}));
run_window_loop(&mut window, || {
// Frame callback - rendering goes here
});
}
Output
Running the sandbox now shows:
[GGEngine] INFO: GGEngine initialized
[App] INFO: Sandbox created
[App] INFO: Creating window...
[GGEngine] INFO: Creating window Sandbox (1280x720)
[GGEngine] INFO: Window created successfully
[App] INFO: Window resized to 1600x900
And when closing:
[App] INFO: Window close requested
[App] INFO: Window closed, shutting down
[GGEngine] INFO: Shutting down window
[GGEngine] INFO: GGEngine shutdown
What’s Next
With the window system in place, we can:
- Fully integrate events (next episode, as Cherno suggests)
- Add a layer system for organizing application logic
- Start setting up an OpenGL/rendering context
The foundation is now ready for graphics!
This is part 5 of a series on building a game engine in Rust.