Chapter 2: Mach Object System
(
mach.Objects,ObjectID) — Mach Engine 0.4
CAUTION: THIS TUTORIAL WAS AI-GENERATED AND MAY CONTAIN ERRORS. IT IS NOT AFFILIATED WITH OR ENDORSED BY HEXOPS/MACH.
In Chapter 1: Mach Core: Essentials, we saw how mach.Core manages essential tasks like creating windows and handling input. We even saw a hint of how it manages window data using core.windows, which is actually an instance of mach.Objects. Now, let’s dive into how Mach organizes and manages all sorts of data in your application or game.
Why a Special System for Data?
Imagine you’re making a game with hundreds of monsters. Each monster needs data: health points, position on the screen, maybe attack power. How do you store and manage all this information efficiently?
You could use standard lists or arrays of monster structs. However, when dealing with potentially thousands of objects, accessing and processing this data quickly becomes crucial for performance. Game engines often use specialized systems for this.
Mach provides the Mach Object System, centered around mach.Objects. It’s designed to be:
- Fast: It arranges data in memory in a way that CPUs love (called Struct-of-Arrays), making operations on many objects very quick.
- Organized: It provides a clear way to group related data together.
- Flexible: It allows relating objects to each other, even if they are in different groups.
Think of mach.Objects as a highly optimized spreadsheet or database table built specifically for game engine data. Instead of rows representing objects and columns representing data fields (like in a typical spreadsheet), mach.Objects stores all values for a single field together in memory.
Key Concepts
Let’s break down the core ideas:
mach.Objects(DataType): This is the container, the “spreadsheet.” You define it to hold objects of a specificstructtype (theDataType). For example, you might havemonsters: mach.Objects(MonsterData)or, as we saw in Chapter 1,windows: mach.Objects(WindowData). It manages a collection of these structs.- Struct-of-Arrays (SoA): This is the secret sauce for performance. Instead of storing data like
[Monster1(HP, Pos), Monster2(HP, Pos), Monster3(HP, Pos)],mach.Objectsstores it like[HP1, HP2, HP3]and[Pos1, Pos2, Pos3]separately. When your code needs to update all monster positions, it can access them contiguously in memory, which is much faster for the CPU cache. ObjectID: When you add a new object (like a new monster) to amach.Objectslist, you don’t get a direct pointer. Instead, you get anObjectID. This is a unique, stable number (an opaqueu64) that acts as a handle or identifier for that specific object within that list. You use this ID to get, set, or delete the object’s data later. Even if objects are deleted and their memory slots reused, theObjectIDhelps detect if you’re accidentally trying to use an old, invalid handle (detecting “use-after-delete”).- Object Relations: Mach allows you to create connections (like parent-child relationships) between objects, even if they live in different
mach.Objectslists. For instance, you could link aSpriteobject to aPlayerobject. (Object system: relations)
Putting mach.Objects to Work
Let’s make a simple list to manage some game items, each having a position and a name.
1. Declaring mach.Objects
First, we declare a field in our App struct (or any Mach module) using mach.Objects. We specify the struct type it will hold.
// src/App.zig (Simplified)
const mach = @import("mach");
const math = mach.math; // We'll use Mach's math library
const ItemData = struct {
name: [*:0]const u8, // A name (C-style string)
position: math.Vec2, // A 2D vector for position
};
const App = @This();
pub const Modules = mach.Modules(.{
mach.Core, // We still need Core for windowing/events
App,
});
// Our list of items!
items: mach.Objects(ItemData),
// ... rest of App struct (init, tick, etc.) ...
- We define
ItemDatato hold the data for each item. - We declare
items: mach.Objects(ItemData)as a field in ourApp. Mach automatically initializes this for us.
2. Creating Objects (.new())
Inside our init function, let’s create a couple of items.
// src/App.zig (Inside init function)
pub fn init(
core: *mach.Core,
app: *App,
app_mod: mach.Mod(App),
) !void {
// ... other setup like core.on_tick ...
std.log.info("Creating items...", .{});
// Create a 'potion' item
const potion_id = try app.items.new(.{
.name = "Potion",
.position = math.vec2(10.0, 20.0),
});
// Create a 'sword' item
const sword_id = try app.items.new(.{
.name = "Sword",
.position = math.vec2(-5.0, 15.0),
});
std.log.info("Created Potion with ID: {any}", .{potion_id});
std.log.info("Created Sword with ID: {any}", .{sword_id});
// We could store these IDs in 'app' if we need them later
// app.potion = potion_id;
// app.sword = sword_id;
}
app.items.new(...)adds a newItemDatastruct to theitemslist.- It returns an
ObjectID(likepotion_id,sword_id), which uniquely identifies the newly created item within theitemslist.
3. Accessing and Modifying Objects (.getValue(), .setValue())
Now, let’s say in our tick function, we want to read the potion’s data and maybe move the sword. We use the ObjectIDs we saved (or would have saved).
// src/App.zig (Inside tick function)
pub fn tick(app: *App, core: *mach.Core) void {
// ... event loop ...
// Let's imagine we stored the IDs from init
const potion_id: mach.ObjectID = 0; // Replace with actual stored ID
const sword_id: mach.ObjectID = 1; // Replace with actual stored ID
// ---- IMPORTANT: Locking ----
// Before accessing data, we should lock the list to prevent issues
// if other parts of the code (e.g., different threads) try to access it
// simultaneously. `defer` ensures unlock is called even if we exit early.
app.items.lock();
defer app.items.unlock();
// --------------------------
// Get the current data for the potion
const potion_data = app.items.getValue(potion_id);
std.log.debug("Potion is at: {any}", .{potion_data.position});
// Get the sword data, modify it, and save it back
var sword_data = app.items.getValue(sword_id);
sword_data.position.x += 0.1; // Move the sword slightly right
app.items.setValue(sword_id, sword_data); // Save the changes
// std.log.debug("Sword moved to: {any}", .{sword_data.position});
// ... rest of tick ...
}
- Locking:
app.items.lock()anddefer app.items.unlock()are crucial for safety, especially in multi-threaded scenarios. They ensure only one part of the code modifies the list at a time. For now, just know it’s good practice when reading and writing. app.items.getValue(object_id)retrieves a copy of the struct data associated with thatObjectID.app.items.setValue(object_id, new_data)updates the object’s data in the list with thenew_datastruct.- You can also get/set individual fields using
.get(id, .field_name)and.set(id, .field_name, value).
4. Iterating Objects (.slice())
Often, you want to process all objects in a list. mach.Objects makes this efficient.
// src/App.zig (Inside tick function, after modifying sword)
std.log.debug("Processing all items:", .{});
var item_slice = app.items.slice(); // Get an iterator
while (item_slice.next()) |item_id| {
// Get the data for the current item_id in the loop
const item_data = app.items.getValue(item_id);
std.log.debug(" - Item '{s}' at {any}", .{item_data.name, item_data.position});
// We could modify items here too:
// var data = app.items.getValue(item_id);
// data.position.y -= 0.01; // Move item up slightly
// app.items.setValue(item_id, data);
}
app.items.slice()creates an iterator (Slice) for the object list.while (item_slice.next()) |item_id|loops through all active objects, giving you theObjectIDfor each one.- Inside the loop, you use the
item_idwithgetValueorsetValueas needed. - This is very efficient because under the hood, iterating often involves walking through the tightly packed SoA arrays.
Under the Hood: The Spreadsheet Analogy
How does mach.Objects achieve this?
High-Level Idea:
Instead of storing structs one after another like [Item1][Item2][Item3], mach.Objects stores them column by column (Struct-of-Arrays):
names: ["Potion", "Sword", ...]positions: [Vec2(10, 20), Vec2(-5, 15), ...]
When you ask for items.getValue(sword_id), Mach:
- Uses the
ObjectIDto find the correct index (row number) for the sword. - Pulls the
namefrom thenamesarray at that index. - Pulls the
positionfrom thepositionsarray at that index. - Constructs the
ItemDatastruct and returns it to you.
This grouping by field (name, position) dramatically improves performance when processing many objects, as the CPU can load chunks of related data (like many positions) very efficiently.
It also uses a “recycling bin” to keep track of deleted object slots, making creating (.new()) and deleting (.delete()) objects very fast as it avoids memory allocation/deallocation overhead most of the time.
Sequence Diagram (Creating and Getting an Object):
sequenceDiagram
participant App
participant ItemsList as mach.Objects(ItemData)
participant DataArrays as (SoA Data)
participant RecyclingBin
App->>ItemsList: items.new({ name: "Potion", pos: (10,20) })
ItemsList->>RecyclingBin: Any free index?
RecyclingBin-->>ItemsList: No free index.
ItemsList->>DataArrays: Append name="Potion"
ItemsList->>DataArrays: Append position=(10,20)
DataArrays-->>ItemsList: New index = 0
ItemsList-->>App: Return ObjectID (encoding index=0, gen=0, type=...)
App->>ItemsList: items.getValue(ObjectID for index=0)
ItemsList->>DataArrays: Get name at index 0
DataArrays-->>ItemsList: Return "Potion"
ItemsList->>DataArrays: Get position at index 0
DataArrays-->>ItemsList: Return (10,20)
ItemsList-->>App: Return ItemData{ name: "Potion", pos: (10,20) }
Code Glance:
The magic happens within Mach’s internal code, primarily in src/module.zig.
-
src/module.zig: Defines theObjectsfunction that creates the specialized list type and theObjectIDtype.// src/module.zig (Highly Simplified Snippet) // Definition of the opaque ObjectID pub const ObjectID = u64; // Function that defines the Objects type pub fn Objects(options: ObjectsOptions, comptime T: type) type { return struct { // Internal state (simplified) internal: struct { allocator: std.mem.Allocator, mu: std.Thread.Mutex = .{}, // For locking type_id: u16, // Internal ID for this list type // The core data store! Holds data SoA style. data: std.MultiArrayList(T) = .{}, // Tracks deleted slots dead: std.bit_set.DynamicBitSetUnmanaged = .{}, // Tracks generation for use-after-delete detection generation: std.ArrayListUnmanaged(u16) = .{}, // Stores reusable indices recycling_bin: std.ArrayListUnmanaged(u32) = .{}, // ... other internal fields ... }, // Public functions like .new(), .getValue(), .setValue(), .slice() // operate on the 'internal' state. pub fn new(objs: *@This(), value: T) !ObjectID { // 1. Lock the mutex // 2. Try to get an index from recycling_bin // 3. If none, append to 'data' (MultiArrayList) // 4. Update 'generation', clear 'dead' flag // 5. Construct and return ObjectID (packed index, gen, type_id) // 6. Unlock mutex // ... (Simplified logic) ... } pub fn getValue(objs: *@This(), id: ObjectID) T { // 1. Lock the mutex // 2. Unpack ID to get index, generation, type_id // 3. Validate ID (correct type? correct generation? not dead?) // 4. Get data from 'data' (MultiArrayList) at index // 5. Return data // 6. Unlock mutex // ... (Simplified logic) ... } // ... other functions (.setValue, .delete, .slice, .lock, .unlock) ... }; } -
std.MultiArrayList(T): This Zig standard library type is the key component that actually implements the Struct-of-Arrays layout.mach.Objectsbuilds upon it, adding features likeObjectIDs, recycling, locking, and relations.
Conclusion
You’ve learned about the Mach Object System, Mach’s efficient way to manage collections of data using mach.Objects. You saw how it uses a Struct-of-Arrays (SoA) layout for performance, how objects are identified and accessed using opaque ObjectIDs, and how to perform basic operations like creating, accessing, modifying, and iterating over objects. We also touched on the importance of locking for data safety.
This system is fundamental to how Mach organizes everything from windows (mach.Core.windows) to graphics resources (mach.gfx) and your own game entities.
Now that we know how to store and manage data, how do we write the code that operates on this data each frame? That’s where Mach Systems come in.
Let’s move on to Chapter 3: Mach Systems.
Generated by AI Codebase Knowledge Builder