Chapter 1: Mach Core - Windowing & Events
(
mach.Core
) — Mach Engine 0.4
CAUTION: THIS TUTORIAL WAS AI-GENERATED AND MAY CONTAIN ERRORS. IT IS NOT AFFILIATED WITH OR ENDORSED BY HEXOPS/MACH.
Welcome to the Mach engine tutorial! We’re excited to have you on board. Let’s start our journey by looking at the very first thing you usually see and interact with in a graphical application or game: the window!
The Stage for Your Application
Imagine you’re directing a play. You need a stage, right? A place for your actors (graphics) to perform and for the audience (user) to see what’s happening. You also need a way for the audience to react (like clapping, which is kind of like user input) and for the stage manager to coordinate everything – lights, curtains, scene changes.
In the world of computer graphics and games, mach.Core
is like that essential stage manager. It handles:
- Creating the Stage (Window): It sets up the application window on the user’s screen. It doesn’t matter if the user is on Windows, macOS, or Linux;
mach.Core
figures out how to talk to the operating system (OS) to create that window. - Listening to the Audience (Input): It listens for user actions like pressing keys on the keyboard, moving the mouse, or clicking mouse buttons.
- Managing the Show (Event Loop): It runs the main loop of your application, constantly checking for user input or window events (like resizing or closing) and telling your application what happened.
- Setting the Scene (Window Properties): It lets you control things like the window’s title, size, and whether it’s fullscreen.
Without mach.Core
, your cool graphics wouldn’t have a place to be shown, and you wouldn’t be able to interact with your application!
Key Concepts
Let’s break down the main ideas within mach.Core
:
- Window: This is the rectangular area on your screen managed by the operating system where your application draws its visuals.
mach.Core
lets you create one or more of these. - Input: Actions performed by the user, primarily using the keyboard and mouse.
- Event: A specific thing that happens, like
key_press
,mouse_motion
, or the user clicking the close button (close
).mach.Core
collects these events. - Event Loop: The heart of a real-time application. It’s a continuous cycle that does roughly this:
- Check for any new events (input, window changes).
- Process those events (run your code to react to them).
- Update the application state (move characters, calculate physics).
- Draw the next frame.
- Repeat!
mach.Core
manages this loop and provides the events to your application code.
Putting mach.Core
to Work
Let’s see how you actually use mach.Core
. In the “Getting Started” guide, you saw a basic App.zig
file.
1. Including mach.Core
First, you need to tell your Mach application that you intend to use the Core module. This is done by adding mach.Core
to the Modules
list in your App.zig
:
// src/App.zig (Simplified)
const mach = @import("mach");
// ... other App struct details ...
// Tell Mach we want to use the Core module and our own App module.
pub const Modules = mach.Modules(.{
mach.Core, // <-- Here it is!
App,
});
// ... rest of the App code ...
This line registers mach.Core
with the engine, making its features available to your application.
2. Creating a Window
Inside your init
function (which runs once at the start), you can ask mach.Core
to create a window:
// src/App.zig (Inside the init function)
pub fn init(
core: *mach.Core, // Mach gives us access to the Core module
app: *App,
app_mod: mach.Mod(App),
) !void {
// Tell the Core what function to call every frame (tick)
core.on_tick = app_mod.id.tick;
// Tell the Core what function to call when exiting
core.on_exit = app_mod.id.deinit;
// Ask core to create a new window with a title
const window = try core.windows.new(.{
.title = "My First Mach Window!",
});
// Store the window's ID so we can refer to it later
app.window = window;
// ... other setup ...
}
- We get a
core: *mach.Core
parameter, giving us access to the Core module’s functions. core.windows.new(...)
is the command to create a new window. We pass options like the.title
.- This returns a
window
identifier, which is anObjectID
. We’ll learn more aboutObjectID
s in Chapter 2: Mach Object System. For now, just think of it as a unique name or handle for our window.
3. Handling Events in the Loop
The tick
function is called repeatedly, once for each frame (or update cycle). Inside tick
, we need to check for and handle any events that mach.Core
has collected since the last tick.
// src/App.zig (Inside the tick function)
pub fn tick(app: *App, core: *mach.Core) void {
// Check for events until there are no more left for this tick
while (core.nextEvent()) |event| {
// Figure out what kind of event it is
switch (event) {
// If the user clicked the close button...
.close => {
std.log.info("Close button clicked! Exiting.", .{});
core.exit(); // Tell the core to stop the application
},
// If a key was pressed...
.key_press => |ev| {
std.log.info("Key pressed: {any}", .{ev.key});
},
// If the mouse moved...
.mouse_motion => |ev| {
std.log.info("Mouse moved to: ({d:.1}, {d:.1})", .{ev.pos.x, ev.pos.y});
},
// We can ignore other events for now
else => {},
}
}
// ... rest of the tick logic (like drawing) ...
}
core.nextEvent()
retrieves the oldest unhandled event. Thewhile
loop continues as long as there are events waiting.- The
switch
statement lets us react differently based on theevent
type (e.g.,.close
,.key_press
,.mouse_motion
). core.exit()
tellsmach.Core
to begin the shutdown process.
This event loop is fundamental. It’s how your application stays responsive to the user and the windowing system.
4. Checking Input State Directly
Sometimes, instead of waiting for an event, you want to know the current state of a key or mouse button (e.g., “Is the ‘W’ key held down right now?”). mach.Core
provides functions for this too:
// src/App.zig (Inside the tick function, after the event loop)
// Check if the spacebar is currently pressed down
if (core.keyPressed(.space)) {
std.log.debug("Spacebar is held down!", .{});
// Maybe make the character jump!
}
// Get the current mouse position
const mouse_pos = core.mousePosition();
// std.log.debug("Current mouse position: {any}", .{mouse_pos});
// ... rest of tick ...
These functions (keyPressed
, mousePosition
, etc.) give you a snapshot of the input state at the exact moment you call them.
Under the Hood: How mach.Core
Works
It seems simple to create a window and get input, but mach.Core
is doing a lot of work behind the scenes!
High-Level Idea:
mach.Core
acts as a translator between your Mach application code (which is the same for all platforms) and the specific way each operating system handles windows and input.
- When you call
core.windows.new()
,mach.Core
figures out if you’re on Windows, macOS, or Linux. - It then calls the specific OS functions needed to create a window (e.g., using Win32 API on Windows, Cocoa on macOS, X11 or Wayland on Linux).
- Similarly, during the event loop (
core.nextEvent()
),mach.Core
asks the OS if any new input or window events have happened. - It translates these OS-specific events into the standard
mach.Core.Event
format that your application understands.
This abstraction layer means you don’t have to worry about the low-level details of each platform!
A Simple Sequence:
Here’s a simplified view of what happens when you create a window and handle a close event:
sequenceDiagram
participant App as Your App
participant Core as mach.Core
participant OS as Operating System
App->>Core: core.windows.new("My Window")
Core->>OS: Create OS-specific window("My Window")
OS-->>Core: Window created successfully (handle=123)
Core-->>App: Return window ObjectID (e.g., 1)
Note over App, OS: Time passes, user clicks close button...
App->>Core: core.nextEvent() ?
Core->>OS: Any new events?
OS-->>Core: Yes, window close event (handle=123)
Core-->>App: Return Event.close
App->>Core: core.exit()
Core->>OS: Initiate shutdown
Code Glance:
src/Core.zig
: This is the main file formach.Core
.- It defines the
Core
struct itself, holding state like the event queue. - It defines the
Event
union and related input types (Key
,MouseButton
,Position
, etc.). - It contains the cross-platform logic like
nextEvent()
andexit()
. - It defines
windows: mach.Objects(...)
which uses the Mach Object System to manage data for each window.
// src/Core.zig (Simplified Snippet) pub const Core = @This(); // Manages all the windows created by the application windows: mach.Objects( // ... options ... struct { // Data stored per window title: [:0]const u8 = "Mach Window", width: u32 = 960, height: u32 = 540, // ... other window properties ... native: ?Platform.Native = null, // OS-specific data }, ), // Queue for storing events received from the OS events: EventQueue, // ... other Core state ... // Returns the next event from the queue pub inline fn nextEvent(core: *@This()) ?Event { return core.events.readItem(); }
- It defines the
src/core/Windows.zig
,src/core/Darwin.zig
,src/core/Linux.zig
: These files contain the platform-specific implementation details.Core.zig
calls functions within these files (likePlatform.tick()
orPlatform.initWindow()
) to interact with the underlying OS. You generally don’t need to look into these unless you’re contributing to the engine itself.
Conclusion
You’ve learned about mach.Core
, the fundamental module in Mach for creating windows and handling user input. It acts as your application’s stage manager, providing a consistent way to interact with the user and display graphics, regardless of the operating system. You saw how to include it in your app, create a window, process events in the tick
loop, and check input state directly.
mach.Core
manages windows as objects. To understand how Mach represents and manages data like windows, cameras, or game entities, we need to dive into the Mach Object System.
Let’s move on to Chapter 2: Mach Object System.
Generated by AI Codebase Knowledge Builder