Structs
Structs let you group related data into a single type. They’re the building block for everything from game entities to configuration objects.
Defining a Struct
struct Player {
name:str
health:i32
score:f32
}
fn main() {
var p = Player{"Alice", 100, 0.0}
print("Name: {p.name}\n")
print("Health: {p.health}\n")
print("Score: {p.score}\n")
}
Fields are listed with name:type, one per line. Initialize by passing values in order inside braces.
Modifying Fields
Access and change fields with dot notation:
struct Enemy {
name:str
health:i32
damage:i32
}
fn main() {
var goblin = Enemy{"Goblin", 50, 5}
print("{goblin.name}: {goblin.health} HP\n")
// Take damage
goblin.health = goblin.health - 20
print("After hit: {goblin.health} HP\n")
// Change name
goblin.name = "Elite Goblin"
print("Evolved into: {goblin.name}\n")
}
Extension Methods
Add methods to a struct using ex blocks:
struct Vec2 {
x:f32
y:f32
}
ex Vec2 {
fn print_pos() {
print("({x}, {y})\n")
}
fn add:Vec2(other:Vec2) {
return Vec2{x + other.x, y + other.y}
}
fn scale:Vec2(factor:f32) {
return Vec2{x * factor, y * factor}
}
}
fn main() {
var a = Vec2{3.0, 4.0}
var b = Vec2{1.0, 2.0}
a.print_pos() // (3.0, 4.0)
var c = a.add(b)
c.print_pos() // (4.0, 6.0)
var d = a.scale(2.0)
d.print_pos() // (6.0, 8.0)
}
Inside an ex block, you can access the struct’s fields directly — x and y refer to the instance the method is called on.
Multiple Ex Blocks
You can split methods across multiple ex blocks:
struct Player {
name:str
health:i32
max_health:i32
score:i32
}
// Combat methods
ex Player {
fn take_damage(amount:i32) {
health = health - amount
if (health < 0) {
health = 0
}
}
fn heal(amount:i32) {
health = health + amount
if (health > max_health) {
health = max_health
}
}
fn is_alive:bool() {
return health > 0
}
}
// Display methods
ex Player {
fn status() {
print("{name}: {health}/{max_health} HP | Score: {score}\n")
}
}
fn main() {
var hero = Player{"Knight", 100, 100, 0}
hero.status()
hero.take_damage(35)
hero.status()
hero.heal(20)
hero.status()
print("Alive: {hero.is_alive()}\n")
}
Structs as Parameters
Pass structs to functions:
struct Rect {
x:f32
y:f32
width:f32
height:f32
}
fn area:f32(r:Rect) {
return r.width * r.height
}
fn describe(r:Rect) {
print("Rect at ({r.x}, {r.y}) size {r.width}x{r.height} area={area(r)}\n")
}
fn main() {
var box = Rect{10.0, 20.0, 100.0, 50.0}
describe(box)
}
Nested Structs
Structs can contain other structs:
struct Position {
x:f32
y:f32
}
struct Entity {
name:str
pos:Position
health:i32
}
fn main() {
var enemy = Entity{"Dragon", Position{100.0, 200.0}, 500}
print("{enemy.name} at ({enemy.pos.x}, {enemy.pos.y})\n")
print("Health: {enemy.health}\n")
}
Try it — Create a struct with extension methods in the Playground.
Expert Corner
Why ex blocks instead of methods inside the struct? This is a deliberate design decision. The struct declaration is purely about data layout — what fields exist and their types. Methods are behavior, and they’re defined separately. This separation has two benefits: (1) data layout is immediately clear at a glance, and (2) you can add methods to any type, including types you didn’t define. It’s similar to Go’s receiver methods or Rust’s impl blocks, but with the twist that you can extend any struct from anywhere.
C-compatible memory layout: GX structs compile to typedef struct { ... } Name; in C. The field order and alignment match C exactly. This means you can pass GX structs directly to C libraries — no marshalling, no conversion, no overhead. A struct Vec2 { x:f32 y:f32 } is exactly 8 bytes, laid out as two consecutive floats.
No inheritance, by design: GX has no classes, no virtual methods, no vtables. If you need polymorphism, use composition (embed one struct in another) or function pointers. This eliminates an entire class of complexity — no diamond inheritance, no fragile base class problems, no hidden virtual dispatch costs. You always know exactly what code runs when you call a method.
self is implicit: Inside an ex block, field names refer to the instance directly. Under the hood, the compiler passes a pointer to the struct as the first parameter (like C’s explicit self or Go’s receiver). Writing health = health - amount compiles to self->health = self->health - amount in the generated C.