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.