Documentation
¶
Overview ¶
Package lyra is a 2D game engine built on SDL3. The package contains the whole engine. There are no submodules. This is done for several reasons, but primarily for a better DX. Typing lyra in an editor with gopls should show all engine systems and functions to enable a quick overview of what exists.
An Engine owns all lyra engine systems and modules. Create one with New, which reads lyra.config.json to configure it (see [loadConfig] for where that file is resolved from), and hand control to it with Engine.Run:
engine, err := lyra.New()
if err != nil {
log.Fatal(err)
}
engine.Run()
Index ¶
- Constants
- Variables
- func RegisterScript[T any, PT interface{ ... }](id string)
- type AxisCode
- type Color
- type Engine
- func (e *Engine) Assets() *assetManager
- func (e *Engine) Audio() *audioMixer
- func (e *Engine) Camera() *camera
- func (e *Engine) Debug() *debugger
- func (e *Engine) Entities() *entityManager
- func (e *Engine) Groups() *groupManager
- func (e *Engine) Input() *inputManager
- func (e *Engine) IsPaused() bool
- func (e *Engine) Physics() *physicsSystem
- func (e *Engine) Quit()
- func (e *Engine) Run()
- func (e *Engine) SetPause(pause bool)
- func (e *Engine) State() *stateManager
- func (e *Engine) Time() *tick
- func (e *Engine) Window() *windowManager
- type Entity
- func (entity *Entity) AddChild(child *Entity)
- func (entity *Entity) Children() []*Entity
- func (entity *Entity) Clone(cloneID string) *Entity
- func (entity *Entity) GetChild(id string) *Entity
- func (entity *Entity) GetCustomField(field string) any
- func (entity *Entity) IsActive() bool
- func (entity *Entity) PhysicalBody() *physicalBody
- func (entity *Entity) RemoveChild(child *Entity, mode removeMode)
- func (entity *Entity) Renderable() *renderable
- func (entity *Entity) SetActive(active bool)
- func (entity *Entity) SetCustomField(field string, value any)
- func (entity *Entity) SetPhysicalBody(physicalBody *physicalBody)
- func (entity *Entity) SetRenderable(renderable *renderable)
- func (entity *Entity) SetTransform(transform *transform)
- func (entity *Entity) Transform() *transform
- type GamepadCode
- type Group
- func (g *Group) Add(entity *Entity)
- func (g *Group) GetProperty(field string) any
- func (g *Group) IsMember(entity *Entity) bool
- func (g *Group) Members() []*Entity
- func (g *Group) Remove(entity *Entity)
- func (g *Group) SetProperty(field string, value any)
- func (g *Group) Spawn(baseEntity *Entity, cloneID string) *Entity
- type KeyCode
- type Rect
- type RectF
- type Script
- type Vector2
- func (v Vector2) Add(other Vector2) Vector2
- func (v Vector2) AddValue(value int) Vector2
- func (v Vector2) Divide(other Vector2) Vector2
- func (v Vector2) DivideValue(value int) Vector2
- func (v Vector2) Dot(other Vector2) int
- func (v Vector2) IsZero() bool
- func (v Vector2) Length() float64
- func (v Vector2) LengthSquared() int
- func (v Vector2) Mult(other Vector2) Vector2
- func (v Vector2) MultValue(value int) Vector2
- func (v Vector2) Sub(other Vector2) Vector2
- func (v Vector2) SubValue(value int) Vector2
- func (v Vector2) ToVector2F() Vector2F
- type Vector2F
- func (v Vector2F) Add(other Vector2F) Vector2F
- func (v Vector2F) AddValue(value float64) Vector2F
- func (v Vector2F) Divide(other Vector2F) Vector2F
- func (v Vector2F) DivideValue(value float64) Vector2F
- func (v Vector2F) Dot(other Vector2F) float64
- func (v Vector2F) IsZero() bool
- func (v Vector2F) Length() float64
- func (v Vector2F) LengthSquared() float64
- func (v Vector2F) Mult(other Vector2F) Vector2F
- func (v Vector2F) MultValue(value float64) Vector2F
- func (v Vector2F) Normalize() Vector2F
- func (v Vector2F) Sub(other Vector2F) Vector2F
- func (v Vector2F) SubValue(value float64) Vector2F
Constants ¶
const Version = "1.0.0"
Version is the current lyra engine version string. It is exposed as a constant for user code to log or display, and is also logged to the console during engine startup by [newDebugger].
fmt.Println("Running lyra version:", lyra.Version)
Variables ¶
var Backend = backendValues{
Default: backendDefault,
Direct3D12: backendDirect3D12,
Direct3D11: backendDirect3D11,
Metal: backendMetal,
Vulkan: backendVulkan,
OpenGL: backendOpenGL,
OpenGLES: backendOpenGLES,
}
Backend is every render backend the engine can target. Backend.Default lets SDL choose automatically; any other field requests one specific graphics API, and fails Engine construction if that API isn't available on the machine the engine is running on.
config.RenderBackend = lyra.Backend.Vulkan
shared state; see backendValues
var DebugLevel = debugLevelValues{
None: debugLevelNone,
Info: debugLevelInfo,
Warn: debugLevelWarn,
Error: debugLevelError,
}
DebugLevel is every console log severity the engine recognizes. DebugLevel.None disables console logging entirely; each other field raises the minimum severity a [debugger] writes, in increasing order of severity.
config.DebugLevel = lyra.DebugLevel.Warn
shared state; see debugLevelValues
var Direction = directionValues{
None: directionNone,
Left: directionLeft,
Right: directionRight,
Top: directionTop,
Bottom: directionBottom,
}
Direction is every cardinal face [physicsSystem] can report a collision contact on.
if engine.Physics().IsCollidingIn(player, lyra.Direction.Bottom) {
grounded = true
}
shared state; see directionValues
var Flip = flipValues{
None: flipNone,
Horizontal: flipHorizontal,
Vertical: flipVertical,
}
Flip is every mirroring mode [Renderable.Flip] accepts. Flip.None draws content unmirrored; Flip.Horizontal and Flip.Vertical mirror it along one axis.
entity.Renderable.Flip = lyra.Flip.Horizontal
var GamepadAxis = gamepadAxisValues{
LeftX: gamepadAxisLeftX,
LeftY: gamepadAxisLeftY,
RightX: gamepadAxisRightX,
RightY: gamepadAxisRightY,
LeftTrigger: gamepadAxisLeftTrigger,
RightTrigger: gamepadAxisRightTrigger,
}
GamepadAxis is the table of every gamepad axis the engine recognizes, addressed by friendly field name (GamepadAxis.LeftX, GamepadAxis.LeftTrigger) the same way Keys addresses keyboard keys; see Keys for why this lookup-table-as-struct pattern exists.
engine.Input().BindAxis("MoveX", lyra.GamepadAxis.LeftX, 1)
var GamepadButton = gamepadButtonValues{
South: gamepadButtonSouth,
East: gamepadButtonEast,
West: gamepadButtonWest,
North: gamepadButtonNorth,
Back: gamepadButtonBack,
Guide: gamepadButtonGuide,
Start: gamepadButtonStart,
LeftStick: gamepadButtonLeftStick,
RightStick: gamepadButtonRightStick,
LeftShoulder: gamepadButtonLeftShoulder,
RightShoulder: gamepadButtonRightShoulder,
DPadUp: gamepadButtonDPadUp,
DPadDown: gamepadButtonDPadDown,
DPadLeft: gamepadButtonDPadLeft,
DPadRight: gamepadButtonDPadRight,
Misc1: gamepadButtonMisc1,
RightPaddle1: gamepadButtonRightPaddle1,
LeftPaddle1: gamepadButtonLeftPaddle1,
RightPaddle2: gamepadButtonRightPaddle2,
LeftPaddle2: gamepadButtonLeftPaddle2,
Touchpad: gamepadButtonTouchpad,
Misc2: gamepadButtonMisc2,
Misc3: gamepadButtonMisc3,
Misc4: gamepadButtonMisc4,
Misc5: gamepadButtonMisc5,
Misc6: gamepadButtonMisc6,
}
GamepadButton is the table of every gamepad button the engine recognizes, addressed by friendly field name (GamepadButton.South, GamepadButton.LeftShoulder) the same way Keys addresses keyboard keys; see Keys for why this lookup-table-as-struct pattern exists, and [inputManager.BindGamepadButtons] for how a field from this table is bound to an action.
gp, _ := engine.Input().Gamepad(0)
if gp.IsDown(lyra.GamepadButton.South) { jump() }
var Keys = keyValues{}/* 108 elements not displayed */
Keys is the table of every keyboard key the engine recognizes, addressed by friendly field name (Keys.Space, Keys.Left) rather than by a raw sdl.Scancode constant. It is how key values of the unexported [key] type reach calling code at all: a caller never writes a [key] type name, only reads a value off this struct and passes it straight on to methods like [inputManager.IsKeyDown] or [inputManager.ActionDown].
if engine.Input().IsKeyDown(lyra.Keys.Left) { moveLeft() }
var MouseButton = mouseButtonValues{
Left: mouseButtonLeft,
Middle: mouseButtonMiddle,
Right: mouseButtonRight,
Back: mouseButtonBack,
Forward: mouseButtonForward,
}
MouseButton is the table of every mouse button the engine recognizes, addressed by friendly field name (MouseButton.Left, MouseButton.Back) the same way Keys addresses keyboard keys; see Keys for why this lookup-table-as-struct pattern exists.
if engine.Input().IsMouseDown(lyra.MouseButton.Left) { ... }
var RemoveMode = removeModeValues{
Promote: removeModePromoteChildren,
Recursive: removeModeRemoveSubtree,
}
RemoveMode selects what [entityManager.Remove] and Entity.RemoveChild do with the children of the entity being removed.
engine.Entities().Remove(enemy, lyra.RemoveMode.Recursive)
shared state; see removeModeValues
Functions ¶
func RegisterScript ¶
RegisterScript registers a script type T for entity ID id. When an entity with that ID is loaded from JSON, [scriptFor] will create a fresh instance of T and attach it to the entity. The script type T must implement [scriptComponent] (the interface with OnStart, OnUpdate, and OnDestroy methods).
The type parameter PT is a Go type constraint that ensures T is a pointer type and implements scriptComponent. This allows the registry to store a factory function that returns scriptComponent (the interface) while still creating concrete T instances. The PT(new(T)) expression creates a zero-value T (via new) and converts it to PT (the pointer type), which automatically satisfies scriptComponent.
Registration typically happens in an init function in the game's main package or a dedicated scripts package:
func init() {
lyra.RegisterScript[PlayerScript]("player")
lyra.RegisterScript[EnemyScript]("enemy")
}
Each call to RegisterScript overwrites any previous registration for the same ID. There's no protection against registering multiple scripts for the same ID; last registration wins.
RegisterScript is safe to call from multiple goroutines, but in practice it should only be called during initialization, before any Engine is created and starts loading entities.
Types ¶
type AxisCode ¶
type AxisCode uint8
AxisCode identifies a gamepad analog axis (a stick half or a trigger) by SDL's abstract name, the axis counterpart to [gamepadButton]. See GamepadAxis for how values of it reach calling code, and [gamepad.Axis] for how a raw axis reading becomes the [-1, 1] float game code actually reads.
type Color ¶
Color is an RGBA color with 8 bits per channel. It's used wherever the engine needs a draw color, such as [Label.Color] for text.
The fields are plain uint8, not a packed uint32 or a float-per-channel representation, so a Color can be built with a struct literal (Color{R: 255, G: 0, B: 0, A: 255}).
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine drives the main loop: window creation, event polling, per-frame update, and rendering. It owns the all engine systems is the single entry point user code interacts with.
Engine deliberately keeps the [engineConfig] it loaded as an unexported snapshot rather than exposing it live (see New): once running, the supported way to change window or render behavior is through Window's setters, not by editing lyra.config.json and expecting a running Engine to pick it up. That keeps there from being two copies of the same setting that can silently drift apart.
func New ¶
New creates an Engine: it reads lyra.config.json (see [loadConfig]), loads the SDL3 shared libraries, initializes SDL, and creates the OS window and renderer the config describes. The returned Engine is not yet visible; call Engine.Run to show the window and start the loop.
The loaded config is read once here and copied into the Engine, and copied again into the [debugger] and [windowManager] New constructs internally; nothing re-reads lyra.config.json afterward, so editing the file while an Engine is running has no effect until the next launch. A single immutable snapshot taken at construction time is simpler to reason about than a config that's "live" in some places and stale in others; see the Engine doc comment.
New returns an error if loading the config file, loading the SDL3 libraries, initializing SDL, or creating the window/renderer fails. Each of those steps releases whatever it had already acquired before returning, so a non-nil error never leaves SDL resources behind for the caller to clean up.
engine, err := lyra.New()
if err != nil {
log.Fatal(err)
}
func (*Engine) Assets ¶
func (e *Engine) Assets() *assetManager
Assets returns the Engine's [assetManager], the entry point for looking up textures, fonts, sounds, atlases, and tilemaps that were loaded from the assets directory at startup.
tex := engine.Assets().GetTexture("player")
func (*Engine) Audio ¶
func (e *Engine) Audio() *audioMixer
Audio returns the Engine's [audioMixer], the entry point for playing, looping, pausing, and mixing loaded [sound] assets.
engine.Audio().Play(explosionSound)
func (*Engine) Camera ¶
func (e *Engine) Camera() *camera
Camera returns the Engine's [camera], the entry point for following an entity, zooming, and constraining the visible world with bounds or a deadzone.
engine.Camera().Follow(player)
func (*Engine) Debug ¶
func (e *Engine) Debug() *debugger
Debug returns the Engine's [debugger], the entry point for writing leveled log lines and toggling the in-window debug overlay.
engine.Debug().Infof("wave %d cleared", wave)
func (*Engine) Entities ¶
func (e *Engine) Entities() *entityManager
Entities returns the Engine's [entityManager], the entry point for looking up entities by id and adding or removing them from the running scene.
goblin := engine.Entities().Get("goblin")
func (*Engine) Groups ¶
func (e *Engine) Groups() *groupManager
Groups returns the Engine's [groupManager], the entry point for creating and looking up Group values, the game-defined categories (e.g. "enemies") that sit orthogonal to the scene graph [entityManager] tracks.
enemies := engine.Groups().New("enemies")
func (*Engine) Input ¶
func (e *Engine) Input() *inputManager
Input returns the Engine's [inputManager], the entry point for reading keyboard, mouse, and gamepad state and for binding physical inputs to named actions.
if engine.Input().IsKeyPressed(lyra.Keys.Space) { jump() }
func (*Engine) IsPaused ¶
IsPaused reports whether the Engine is currently paused. See Engine.SetPause for exactly what pausing does and does not stop.
Game code typically calls this from a [Script.OnUpdate] to skip gameplay-only logic, like a timer or spawn schedule, while a pause menu is open, without needing to track the paused state itself separately.
if !engine.IsPaused() {
enemy.RespawnTimer -= engine.Time().Delta()
}
func (*Engine) Physics ¶
func (e *Engine) Physics() *physicsSystem
Physics returns the Engine's [physicsSystem], the entry point for querying collisions between entities and groups.
if engine.Physics().IsColliding(player) { takeDamage() }
func (*Engine) Quit ¶
func (e *Engine) Quit()
Quit signals Engine.Run to stop after finishing the current frame: its loop condition is checked once per iteration, so control returns from Run only once processEvents, update, and render have all completed for the frame Quit was called on, not immediately.
This is the supported way for game code, a [Script.OnUpdate], or a window close request to end the program cleanly, since Run's deferred [Engine.shutdown] only runs once the loop actually exits; killing the process instead would skip releasing SDL resources.
if engine.Input().IsKeyPressed(lyra.Keys.Escape) {
engine.Quit()
}
func (*Engine) Run ¶
func (e *Engine) Run()
Run shows the window and blocks the calling goroutine, repeatedly polling events, advancing [tick], rendering a frame, and then waiting out any remaining [Config.MaxFPS] budget, until Engine.Quit is called. When the loop exits, Run tears down the window and releases SDL resources before returning.
Run must be called from the same goroutine that called New. SDL's video and event APIs are only safe to use from the OS thread that initialized them, and go-sdl3 takes care of pinning that thread for you: importing the sdl package runs an init function that calls runtime.LockOSThread on whatever goroutine is executing program startup, which in practice is the main goroutine. As long as New and Run both run on that same goroutine (the normal way to write a main function), there's nothing further for callers to do here.
engine, err := lyra.New()
if err != nil {
log.Fatal(err)
}
engine.Run()
func (*Engine) SetPause ¶
SetPause pauses or unpauses the Engine's simulation. While paused, [Engine.update] skips the [entityLoader], [physicsSystem], and [camera] updates for that frame: hot-reloaded entity changes are held off, no physics step runs, and the camera stops following its target.
Script lifecycle and animation playback are deliberately left running: [entityManager.update] runs unconditionally every frame regardless of isPaused, so a pause menu implemented as a Script on its own Entity can keep reading input and calling SetPause(false) to resume, and any [renderable]'s [animated] keeps advancing instead of freezing behind the menu. SetPause has no way to tell "gameplay" scripts from "menu" scripts, so a game that wants its own OnUpdate to go idle while paused must check Engine.IsPaused itself, as shown in that method's example.
SetPause reads and writes isPaused with no synchronization of its own; call it from the same goroutine that runs Engine.Run, same as Engine.Quit.
engine.SetPause(true)
func (*Engine) State ¶
func (e *Engine) State() *stateManager
State returns the Engine's [stateManager], the entry point for saving and loading the running scene's entity state to and from disk.
engine.State().Save("slot1")
type Entity ¶
type Entity struct {
// contains filtered or unexported fields
}
Entity is a node in the scene graph: an identity (ID), a place in the hierarchy (parent and children), and a set of optional component pointers (script, transform, renderable, physicalBody) that give it behavior, position, appearance, or physics, reached through Entity.Transform/Entity.Renderable/Entity.PhysicalBody. A nil component means the entity doesn't participate in that aspect of the engine; a purely logical entity, for instance, can have a nil transform.
Entities are only ever created by loading a JSON entity file (see [entityLoader] and the assets/entities directory); there's no exported way to construct one from scratch.
func (*Entity) AddChild ¶
AddChild attaches child to entity, making entity its Parent. If child is already attached elsewhere, it's detached from its current parent first, so a child never ends up registered under two parents at once.
parent.AddChild(child)
func (*Entity) Children ¶
Children returns a copy of entity's direct children, in attach order. It only returns entity's immediate children, not deeper descendants; walk the returned entities recursively (as [entityManager.collectSubtree] does internally) to reach a whole subtree.
A copy, rather than entity's own backing slice, is returned so appending to or reordering the result can never corrupt entity's real children; the Entity pointers themselves are still shared and mutable, only the slice that holds them is independent. This is the intended way to reach a generated tilemap layer's per-tile entities from game code (e.g. to call [physicalBody.SetBoundingBoxInset] on each), since their IDs are derived from grid coordinates and aren't known ahead of time the way Entity.GetChild's id argument must be.
for _, tile := range dangerLayer.Children() {
tile.PhysicalBody().SetBoundingBoxInset(lyra.Vector2F{X: 10, Y: 25})
}
func (*Entity) Clone ¶
Clone creates an independent copy of entity and its whole subtree of children under the new id cloneID, registers the copy with the engine's [entityManager], and returns it. It returns nil, after logging a warning through the [Debugger], if cloneID is already registered, since two entities can never share an id.
Clone exists for spawning many runtime instances of an authored shape (bullets, particles, a wave of enemies) without re-reading and re-parsing the source JSON entity file through [entityLoader] for each one. This relieves GC pressure. The copy [Entity.cloneTree] builds is deep enough that mutating a clone's transform, physics body, renderable content, or custom fields never reaches back into entity or any other clone made from it; see cloneTree for exactly what is duplicated versus shared. Checking cloneID's availability through [entityManager.Get] means a normal, successful call also trips that method's own not-found log line, an Info rather than the Warn logged here on an actual collision.
goblin, _ := engine.Entities().Get("goblin")
second := goblin.Clone("goblin_2")
func (*Entity) GetChild ¶
GetChild looks up entity's direct child by id, returning nil if no such child is currently attached. Unlike [entityManager.Get], it only searches entity's own childRegistry, not the whole scene, so it's the right call when id is known ahead of time (an authored entity file name) rather than derived at runtime the way a generated tilemap layer's per-tile children are; see Entity.Children for reaching those instead.
sword := player.GetChild("sword")
func (*Entity) GetCustomField ¶
GetCustomField returns the value stored under field by Entity.SetCustomField, or nil if field was never set. Since the value is stored as any, callers need a type assertion to use it as anything more specific; GetCustomField itself doesn't validate that field holds the type the caller expects.
if hp, ok := goblin.GetCustomField("hp").(int); ok {
goblin.SetCustomField("hp", hp-10)
}
func (*Entity) IsActive ¶
IsActive reports whether entity is currently active. An inactive entity's script does not receive OnStart or OnUpdate calls from [entityManager.update], but the entity itself stays in the scene: it remains reachable by ID and keeps its place in the hierarchy
if entity.IsActive() {
body := entity.PhysicalBody()
body.SetVelocity(body.Velocity().Add(lyra.Vector2F{X: force}))
}
func (*Entity) PhysicalBody ¶
func (entity *Entity) PhysicalBody() *physicalBody
PhysicalBody returns the entity's physics component, or nil if it has none.
entity.PhysicalBody().SetVelocity(lyra.Vector2F{X: 100})
func (*Entity) RemoveChild ¶
RemoveChild detaches child from entity. mode selects what happens to child's own children: RemoveMode.Recursive detaches the whole subtree along with child, while RemoveMode.Promote reparents them onto entity, becoming entity's direct children in child's place, instead of being detached themselves.
parent.RemoveChild(child, lyra.RemoveMode.Promote)
func (*Entity) Renderable ¶
func (entity *Entity) Renderable() *renderable
Renderable returns the entity's rendering component, or nil if it has none.
entity.Renderable().SetVisible(false)
func (*Entity) SetActive ¶
SetActive sets whether entity is active, taking effect from the next call to [entityManager.update]. Deactivating an entity does not remove it, detach its children, or hide it from rendering; it only stops [entityManager.update] from running the entity's script (see Entity.IsActive), so a deactivated entity is a cheap way to pause a behavior without tearing down the entity it belongs to.
enemy.SetActive(false)
func (*Entity) SetCustomField ¶
SetCustomField stores value under field in entity's free-form data, creating the backing map on first use. This exists as an escape hatch for game-specific data that doesn't warrant its own field on Entity or a dedicated component type; it's also how a value round-trips through an entity JSON file's customFields object (see [entityDTO]) and through a save file (see [stateManager.applyEntity]) without the engine needing to know its shape.
entity.SetCustomField("hp", 100)
func (*Entity) SetPhysicalBody ¶
func (entity *Entity) SetPhysicalBody(physicalBody *physicalBody)
SetPhysicalBody sets the entity's physics component; see Entity.PhysicalBody.
entity.SetPhysicalBody(other.PhysicalBody())
func (*Entity) SetRenderable ¶
func (entity *Entity) SetRenderable(renderable *renderable)
SetRenderable sets the entity's rendering component; see Entity.Renderable.
entity.SetRenderable(other.Renderable())
func (*Entity) SetTransform ¶
func (entity *Entity) SetTransform(transform *transform)
SetTransform sets the entity's spatial component; see Entity.Transform.
entity.SetTransform(other.Transform())
type GamepadCode ¶
type GamepadCode uint8
GamepadCode identifies a gamepad button by SDL's abstract, layout- independent button names (South, East, ...) rather than by the labels printed on any one vendor's controller (A/B on Xbox pads, Cross/Circle on PlayStation pads map to the same South/East buttons).
type Group ¶
type Group struct {
// contains filtered or unexported fields
}
Group is a named, game-defined collection of entities, created through [groupManager.New] and never directly. Membership is orthogonal to the scene graph [entityManager] tracks: an Entity can belong to any number of groups (or none) independent of its parent/child relationships, which is what lets game code answer questions like "every entity in the 'enemies' group" without walking the tree or re-tagging entities that already have an unrelated parent.
Group also carries a free-form properties map (see Group.SetProperty and Group.GetProperty) for data that belongs to the group as a whole rather than to any one member, such as a spawn budget or difficulty tier, following the same escape-hatch pattern as Entity.SetCustomField.
func (*Group) Add ¶
Add registers entity and every descendant of entity (walked via [entityManager.collectSubtree]) as a member of g. Adding the whole subtree in one call, rather than just entity, mirrors [entityManager.Add]: a group like "destructible" is meant to describe an object as a whole, so its child hitboxes or particle emitters should join the group along with it instead of being left out and needing a separate Add call each.
enemies.Add(goblin)
func (*Group) GetProperty ¶
GetProperty returns the value stored under field by Group.SetProperty, or nil if field was never set on group. As with Entity.GetCustomField, the value is stored as any, so callers need their own type assertion to use it as anything more specific.
if budget, ok := enemies.GetProperty("waveNumber").(int); ok {
enemies.SetProperty("waveNumber", budget+1)
}
func (*Group) IsMember ¶
IsMember reports whether entity is currently a member of group, looked up by entity's id. Group.Spawn uses it to enforce that only entities already tagged into group can act as its spawn prototype.
if enemies.IsMember(goblin) {
enemies.SetMembersField("alerted", true)
}
func (*Group) Members ¶
Members returns every Entity currently in group, as a freshly built slice. Membership is stored as a map keyed by entity id, the same structure Group.IsMember needs for an O(1) lookup, so producing a slice for iteration means the order is not stable: two calls to Members can return the members in different orders even if g's membership hasn't changed, since Go deliberately randomizes map iteration order.
for _, goblin := range enemies.Members() {
goblin.SetActive(false)
}
func (*Group) Remove ¶
Remove unregisters entity and every descendant of entity from a groups membership, the exact inverse of Group.Add. It only forgets the grouping; entity itself is untouched and remains registered with [entityManager] until something calls [entityManager.Remove] on it.
enemies.Remove(goblin)
func (*Group) SetProperty ¶
SetProperty stores value under field in group's own free-form data, creating the backing map on first use. Unlike [Group.SetMembersField], this data belongs to the group itself rather than to any member, for values like a spawn budget or wave counter that don't correspond to a single entity.
enemies.SetProperty("waveNumber", 3)
func (*Group) Spawn ¶
Spawn clones baseEntity under cloneID (see Entity.Clone) and adds the clone to group, returning the new Entity. It requires baseEntity to already be a member of group, logging a warning through the [Debugger] and returning nil otherwise: baseEntity is meant to act as group's prototype, so every clone Spawn produces is born into the same group as the entity it was copied from, without the caller having to call Group.Add itself.
Spawn exists so a group can double as a simple runtime spawner: tag one authored entity into a group once, then call Spawn repeatedly to populate it with independent copies (e.g. a wave of enemies), rather than every caller re-deriving ids and re-adding clones by hand. Spawn itself only needs to add the clone to g: Entity.Clone already registers the clone with [entityManager], so a groups own membership map is the only bookkeeping left for Spawn to do.
second := enemies.Spawn(goblin, "goblin_2")
type KeyCode ¶
KeyCode identifies a keyboard key by SDL's scancode: the key's physical position on the keyboard, not the glyph the OS's active layout happens to print on it, so Keys.W always names the same physical key regardless of which layout the player has selected. User code never builds a KeyCode from a raw number; it reaches one through the Keys table, e.g. lyra.Keys.Space.
type Rect ¶
Rect is an integer rectangle, used for pixel-exact regions: sprite atlas frames, animation frames ([Animation.CurrentFrame]), and other axis-aligned boxes that are defined in, or serialized from, whole pixels. Its fields carry json tags so it round-trips through formats like TexturePacker's atlas JSON. X and Y are the rectangle's top-left corner; W and H extend right and down from it.
frame := Rect{X: 0, Y: 0, W: 32, H: 32}
func (Rect) Bottom ¶
Bottom returns the Y coordinate of the rectangle's bottom edge (Y + H).
Rect{Y: 10, H: 20}.Bottom() // 30
type RectF ¶
type RectF struct {
X float64 `json:"x"`
Y float64 `json:"y"`
W float64 `json:"w"`
H float64 `json:"h"`
}
RectF is a float64 rectangle, used for world- or screen-space regions that need sub-pixel precision: [camera]'s bounds (world space, the area the view is allowed to scroll within) and deadzone (screen space, the area a followed target can move in before the camera reacts). Like Rect, X and Y are the top-left corner and W and H extend right and down from it
bounds := RectF{X: 0, Y: 0, W: 800, H: 600}
func (RectF) Bottom ¶
Bottom returns the Y coordinate of the rectangle's bottom edge (Y + H).
RectF{Y: 10, H: 20}.Bottom() // 30
type Script ¶
Script is the base type every user-defined script embeds to take part in an Entity's lifecycle. Embedding it gives the embedding type access to the Entity it's attached to, directly through Entity, and to the Engine running it, through Script.Engine; both are populated once, by [Script.bind], when the owning entity is added to an [entityManager].
Script supplies no lifecycle behavior of its own; OnStart, OnUpdate, and OnDestroy (see [scriptComponent]) are left for the embedding type to implement, the same way an embedded base type supplies shared state while the embedder supplies the methods that act on it:
type PlayerScript struct {
lyra.Script
}
// runs once, before the first OnUpdate
func (p *PlayerScript) OnStart() { }
// runs once per frame
func (p *PlayerScript) OnUpdate() { }
// runs when the entity is removed
func (p *PlayerScript) OnDestroy() { }
type Vector2 ¶
Vector2 is an integer 2D point, used wherever a value must stay pixel-exact: window and render resolutions and sizes, and anything else that gets serialized to lyra.config.json (see [engineConfig.WindowSize] and [engineConfig.BaseResolution]). Its fields carry json tags for that reason, even though Vector2 itself has no other notion of persistence.
Vector2 is kept separate from Vector2F rather than folding both into one generic vector type because they guarantee different things: Vector2's int fields can't accumulate the fractional drift float arithmetic would introduce, which matters for values like a window's pixel size that must always land on a whole pixel.
res := Vector2{X: 1280, Y: 720}
func (Vector2) Add ¶
Add returns the elementwise sum of v and other.
Vector2{X: 1, Y: 2}.Add(Vector2{X: 3, Y: 4}) // Vector2{X: 4, Y: 6}
func (Vector2) AddValue ¶
AddValue returns v with value added to both fields, the scalar form of Vector2.Add for callers that want to offset X and Y by the same amount rather than by another vector's components.
Vector2{X: 1, Y: 2}.AddValue(3) // Vector2{X: 4, Y: 5}
func (Vector2) Divide ¶
Divide returns the elementwise quotient of v and other, using Go's integer division: each result component truncates toward zero rather than rounding, the same caveat Vector2.DivideValue documents. Neither of other's fields must be zero; Divide does not guard against it, so a zero component panics, same as a bare int division would.
Vector2{X: 6, Y: 9}.Divide(Vector2{X: 2, Y: 3}) // Vector2{X: 3, Y: 3}
func (Vector2) DivideValue ¶
DivideValue returns v with both fields divided by divisor, using Go's integer division: the result truncates toward zero rather than rounding, so dividing an odd value by 2 loses the remainder instead of rounding it. Callers that need an exact split (for example, halving a resolution) should make sure divisor evenly divides X and Y, or accept the truncation. divisor must not be zero; DivideValue does not guard against it, so a zero divisor panics, same as a bare int division would.
Vector2{X: 1280, Y: 720}.DivideValue(2) // Vector2{X: 640, Y: 360}
func (Vector2) Dot ¶
Dot returns the scalar dot product of v and other (X*oX + Y*oY). A positive result means the vectors point in roughly the same direction; zero means perpendicular; negative means opposing.
Vector2{X: 1, Y: 0}.Dot(Vector2{X: 1, Y: 0}) // 1
func (Vector2) IsZero ¶
IsZero reports whether both fields are exactly zero.
Vector2{}.IsZero() // true
func (Vector2) Length ¶
Length returns the Euclidean length of v as a float64. When only a magnitude comparison is needed, prefer Vector2.LengthSquared to avoid the square root.
Vector2{X: 3, Y: 4}.Length() // 5
func (Vector2) LengthSquared ¶
LengthSquared returns X*X + Y*Y as an int. Comparing LengthSquared against a squared threshold avoids the square root that Vector2.Length requires, making it cheaper for distance checks where the exact length is not needed.
Vector2{X: 3, Y: 4}.LengthSquared() // 25
func (Vector2) Mult ¶
Mult returns the elementwise product of v and other, used wherever one vector scales another component-by-component rather than by a single scalar, such as [renderer.renderText] applying a [transform]'s scale to a text texture's pixel size.
Vector2{X: 2, Y: 3}.Mult(Vector2{X: 4, Y: 5}) // Vector2{X: 8, Y: 15}
func (Vector2) MultValue ¶
MultValue returns v with both fields multiplied by value, the scalar form of Vector2.Mult for callers that want to scale X and Y by the same factor rather than by another vector's components.
Vector2{X: 2, Y: 3}.MultValue(4) // Vector2{X: 8, Y: 12}
func (Vector2) Sub ¶
Sub returns the elementwise difference v minus other.
Vector2{X: 4, Y: 6}.Sub(Vector2{X: 1, Y: 2}) // Vector2{X: 3, Y: 4}
func (Vector2) SubValue ¶
SubValue returns v with value subtracted from both fields, the scalar form of Vector2.Sub for callers that want to shrink X and Y by the same amount rather than by another vector's components.
Vector2{X: 4, Y: 5}.SubValue(3) // Vector2{X: 1, Y: 2}
func (Vector2) ToVector2F ¶
ToVector2F converts v to the float64-based Vector2F. It exists because code that mixes pixel-exact sizes with continuous world-space math needs to cross from one vector type to the other without a manual field-by-field conversion at every call site.
Vector2{X: 1280, Y: 720}.ToVector2F() // Vector2F{X: 1280, Y: 720}
type Vector2F ¶
Vector2F is a float64 2D point, used for world-space positions, sizes, and offsets: anything that needs to move smoothly, such as [camera] position or an Entity's [transform], rather than jump between whole-pixel values.
pos := Vector2F{X: 12.5, Y: 4}
func (Vector2F) Add ¶
Add returns the elementwise sum of v and other.
Vector2F{X: 1, Y: 2}.Add(Vector2F{X: 3, Y: 4}) // Vector2F{X: 4, Y: 6}
func (Vector2F) AddValue ¶
AddValue returns v with value added to both fields, the scalar form of Vector2F.Add for callers that want to offset X and Y by the same amount rather than by another vector's components.
Vector2F{X: 1, Y: 2}.AddValue(3) // Vector2F{X: 4, Y: 5}
func (Vector2F) Divide ¶
Divide returns the elementwise quotient of v and other. Neither of other's fields must be zero: Divide does not guard against it, and a zero component produces +Inf, -Inf, or NaN per IEEE 754 float division, the same caveat Vector2F.DivideValue documents.
Vector2F{X: 6, Y: 9}.Divide(Vector2F{X: 2, Y: 3}) // Vector2F{X: 3, Y: 3}
func (Vector2F) DivideValue ¶
DivideValue returns v with both fields divided by value. value must not be zero: DivideValue does not guard against it, and a zero divisor produces +Inf, -Inf, or NaN per IEEE 754 float division rather than a panic, unlike Vector2.DivideValue's integer counterpart.
Vector2F{X: 6, Y: 9}.DivideValue(3) // Vector2F{X: 2, Y: 3}
func (Vector2F) Dot ¶
Dot returns the scalar dot product of v and other (X*oX + Y*oY). A positive result means the vectors point in roughly the same direction; zero means they are perpendicular; negative means they oppose each other.
Vector2F{X: 1, Y: 0}.Dot(Vector2F{X: 0.5, Y: 0.5}) // 0.5 .
func (Vector2F) IsZero ¶
IsZero reports whether both fields are exactly zero.
Vector2F{}.IsZero() // true
func (Vector2F) Length ¶
Length returns the Euclidean length of v (sqrt(X^2 + Y^2)). When only a magnitude comparison is needed, prefer Vector2F.LengthSquared to avoid the square root.
Vector2F{X: 3, Y: 4}.Length() // 5
func (Vector2F) LengthSquared ¶
LengthSquared returns X*X + Y*Y, the squared Euclidean length of v. Comparing LengthSquared against a squared threshold is cheaper than comparing Vector2F.Length against the threshold itself, because it avoids the square root.
Vector2F{X: 3, Y: 4}.LengthSquared() // 25
func (Vector2F) Mult ¶
Mult returns the elementwise product of v and other, used wherever one vector scales another component-by-component rather than by a single scalar.
Vector2F{X: 2, Y: 3}.Mult(Vector2F{X: 4, Y: 5}) // Vector2F{X: 8, Y: 15}
func (Vector2F) MultValue ¶
MultValue returns v with both fields multiplied by value, the scalar form of Vector2F.Mult for callers that want to scale X and Y by the same factor rather than by another vector's components.
Vector2F{X: 2, Y: 3}.MultValue(4) // Vector2F{X: 8, Y: 12}
func (Vector2F) Normalize ¶
Normalize returns a unit vector in the same direction as v (length 1). It returns the zero vector when v has zero length, so callers can apply it without a nil-check; the zero vector propagates harmlessly through subsequent multiplications.
Vector2F{X: 3, Y: 4}.Normalize() // Vector2F{X: 0.6, Y: 0.8}
func (Vector2F) Sub ¶
Sub returns the elementwise difference v minus other.
Vector2F{X: 4, Y: 6}.Sub(Vector2F{X: 1, Y: 2}) // Vector2F{X: 3, Y: 4}
func (Vector2F) SubValue ¶
SubValue returns v with value subtracted from both fields, the scalar form of Vector2F.Sub for callers that want to shrink X and Y by the same amount rather than by another vector's components.
Vector2F{X: 4, Y: 5}.SubValue(3) // Vector2F{X: 1, Y: 2}
Source Files
¶
- asset_atlas.go
- asset_font.go
- asset_loaders.go
- asset_manager.go
- asset_sound.go
- asset_texture.go
- asset_tilemap.go
- audio_mixer.go
- comp_animated.go
- comp_animation.go
- comp_label.go
- comp_physical_body.go
- comp_renderable.go
- comp_sprite.go
- comp_transform.go
- config.go
- config_backend.go
- config_debug_level.go
- constants.go
- core_context.go
- core_engine.go
- core_tick.go
- debug_debugger.go
- entity.go
- entity_dto_struct.go
- entity_dto_tilemap.go
- entity_loader.go
- entity_loader_json.go
- entity_manager.go
- entity_struct_dto.go
- group.go
- group_manager.go
- input_gamepad.go
- input_manager.go
- input_map.go
- physics_direction.go
- physics_entity.go
- physics_greedy_meshing.go
- physics_system.go
- render_camera.go
- render_renderer.go
- script.go
- script_registry.go
- state.go
- util_color.go
- util_flip.go
- util_rect.go
- util_vector2.go
- window_api.go
- window_manager.go
- window_render.go