A struct groups related fields into a named type. Structs are value types, they behave like integers and floats, not like objects or pointers.
Defining a Struct
defstruct Vec2 do
x :: float
y :: float
endFields are listed one per line: name :: type. Order is significant, it determines the order in the struct literal.
Creating Instances
Write the struct name followed by { field: value, ... }:
pos = Vec2{ x: 3.0, y: 4.0 }
origin = Vec2{ x: 0.0, y: 0.0 }All fields must be provided unless they have default values (see below).
Value Semantics, What "Value Type" Means
This is the most important thing to understand about structs. When you assign a struct to another variable, or pass it to a function, a full copy is made. There are no shared references.
a = Vec2{ x: 1.0, y: 2.0 }
b = a # b is an independent copy of a
b = b with { x: 99.0 } # modifying b does NOT affect a
print(a.x) # 1.0, a is unchanged
print(b.x) # 99.0This is different from most scripting languages where objects are references. In Chasm, b = a means "give b its own private copy of everything in a". This makes reasoning about game state much simpler, there are no aliasing surprises.
Default Field Values
Fields can declare a default value. When a field is omitted in a struct literal, the default is used:
defstruct Bullet do
x :: float = 0.0
y :: float = 0.0
speed :: float = 5.0
active :: int = 0
end
b1 = Bullet{} # all defaults: x=0, y=0, speed=5, active=0
b2 = Bullet{ x: 10.0 } # x=10, rest use defaults
b3 = Bullet{ x: 5.0, speed: 8.0 }Bullet{} is valid because every field has a default. Without defaults, all fields are required.
Struct Update with with
The with keyword creates a modified copy of a struct. Only the listed fields change, all others are copied from the original:
pos = Vec2{ x: 1.0, y: 2.0 }
pos2 = pos with { x: 10.0 } # Vec2{ x: 10.0, y: 2.0 }This is the idiomatic way to update a struct. It is equivalent to writing the full literal by hand:
# These two are identical:
pos2 = pos with { x: 10.0 }
pos2 = Vec2{ x: 10.0, y: pos.y }with is especially useful when updating a struct stored in an array. Read it, modify it, write it back:
b = @bullets.get(i)
@bullets.set(i, b with { x: b.x + b.speed * dt })Reading Fields
Access a field with .:
pos = Vec2{ x: 3.0, y: 4.0 }
len = sqrt(pos.x * pos.x + pos.y * pos.y) # 5.0Passing Structs to Functions
Structs are passed by value, the function receives its own copy:
defp length(v :: Vec2) :: float do
sqrt(v.x * v.x + v.y * v.y)
end
defp normalized(v :: Vec2) :: Vec2 do
len = length(v)
Vec2{ x: v.x / len, y: v.y / len }
end
def main() do
pos = Vec2{ x: 3.0, y: 4.0 }
n = normalized(pos)
print(n.x) # 0.6
print(n.y) # 0.8
endThe function cannot mutate the caller's struct, it works on its own copy.
Arrays of Structs
Use array_new(Type, cap) for local struct collections inside functions:
defp find_nearby(entities :: []Entity, px :: float, py :: float, r :: float) :: []Entity do
result = array_new(Entity, entities.len)
for e in entities do
if vec2_dist(e.x, e.y, px, py) < r do
result.push(e)
end
end
return result
endUse array_fixed(cap, StructLiteral{}) for module-level arrays:
defstruct Enemy do
x :: float = 0.0
y :: float = 0.0
hp :: int = 10
active :: int = 0
end
@enemies :: script = array_fixed(32, Enemy{})All 32 slots are pre-filled with Enemy{}, x=0, y=0, hp=10, active=0. On hot-reload the script arena resets and all slots reinitialize from the default automatically.
To update a slot, read-modify-write:
def on_tick(dt :: float) do
i = 0
while i < @enemies.len do
e = @enemies.get(i)
if e.active == 1 do
@enemies.set(i, e with { x: e.x + 2.0 * dt, hp: e.hp - 1 })
end
i = i + 1
end
endNested Structs
Struct fields can be other structs:
defstruct Transform do
pos :: Vec2
rot :: float
end
t = Transform{ pos: Vec2{ x: 5.0, y: 3.0 }, rot: 0.0 }
print(t.pos.x) # 5.0
# Update nested field with with
t2 = t with { pos: t.pos with { x: 10.0 } }
print(t2.pos.x) # 10.0
print(t2.rot) # 0.0 , unchanged