lyra

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 26 Imported by: 0

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

View Source
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

View Source
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

View Source
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

View Source
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

View Source
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
View Source
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)
View Source
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() }
View Source
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() }
View Source
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) { ... }
View Source
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

func RegisterScript[T any, PT interface {
	*T
	scriptComponent
}](id string)

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

type Color struct {
	R uint8 `json:"r"`
	G uint8 `json:"g"`
	B uint8 `json:"b"`
	A uint8 `json:"a"`
}

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

func New() (*Engine, error)

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

func (e *Engine) IsPaused() bool

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

func (e *Engine) SetPause(pause bool)

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")

func (*Engine) Time

func (e *Engine) Time() *tick

Time returns the Engine's [tick], the entry point for reading the current frame's delta time, total elapsed time, and FPS.

player.X += speed * engine.Time().Delta()

func (*Engine) Window

func (e *Engine) Window() *windowManager

Window returns the Engine's [windowManager], the entry point for resizing the OS window, changing render resolution, toggling fullscreen, and reacting to window focus and resize events.

engine.Window().SetFullscreen(true)

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

func (entity *Entity) AddChild(child *Entity)

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

func (entity *Entity) Children() []*Entity

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

func (entity *Entity) Clone(cloneID string) *Entity

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

func (entity *Entity) GetChild(id string) *Entity

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

func (entity *Entity) GetCustomField(field string) any

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

func (entity *Entity) IsActive() bool

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

func (entity *Entity) RemoveChild(child *Entity, mode removeMode)

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

func (entity *Entity) SetActive(active bool)

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

func (entity *Entity) SetCustomField(field string, value any)

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())

func (*Entity) Transform

func (entity *Entity) Transform() *transform

Transform returns the entity's spatial component, or nil if it has none.

pos := entity.Transform().Position()

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

func (g *Group) Add(entity *Entity)

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

func (g *Group) GetProperty(field string) any

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

func (g *Group) IsMember(entity *Entity) bool

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

func (g *Group) Members() []*Entity

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

func (g *Group) Remove(entity *Entity)

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

func (g *Group) SetProperty(field string, value any)

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

func (g *Group) Spawn(baseEntity *Entity, cloneID string) *Entity

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

type KeyCode sdl.Scancode

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.

func (KeyCode) String

func (key KeyCode) String() string

String returns key's SDL scancode name (e.g. "Space", "A"), making a key printable directly in log output and in [inputManager]'s debug.Infof binding messages without callers needing their own lookup table.

type Rect

type Rect struct {
	X int `json:"x"`
	Y int `json:"y"`
	W int `json:"w"`
	H int `json:"h"`
}

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

func (r Rect) Bottom() int

Bottom returns the Y coordinate of the rectangle's bottom edge (Y + H).

Rect{Y: 10, H: 20}.Bottom() // 30

func (Rect) IsZero

func (r Rect) IsZero() bool

IsZero reports whether r is the zero value: every field equal to 0.

Rect{}.IsZero() // true

func (Rect) Right

func (r Rect) Right() int

Right returns the X coordinate of the rectangle's right edge (X + W).

Rect{X: 5, W: 15}.Right() // 20

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

func (r RectF) Bottom() float64

Bottom returns the Y coordinate of the rectangle's bottom edge (Y + H).

RectF{Y: 10, H: 20}.Bottom() // 30

func (RectF) IsZero

func (r RectF) IsZero() bool

IsZero reports whether r is the zero value: every field equal to 0.

RectF{}.IsZero() // true

func (RectF) Right

func (r RectF) Right() float64

Right returns the X coordinate of the rectangle's right edge (X + W).

RectF{X: 5, W: 15}.Right() // 20

type Script

type Script struct {
	Entity *Entity

	Engine *Engine
}

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

type Vector2 struct {
	X int `json:"x"`
	Y int `json:"y"`
}

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

func (v Vector2) Add(other Vector2) Vector2

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

func (v Vector2) AddValue(value int) Vector2

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

func (v Vector2) Divide(other Vector2) Vector2

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

func (v Vector2) DivideValue(value int) Vector2

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

func (v Vector2) Dot(other Vector2) int

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

func (v Vector2) IsZero() bool

IsZero reports whether both fields are exactly zero.

Vector2{}.IsZero() // true

func (Vector2) Length

func (v Vector2) Length() float64

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

func (v Vector2) LengthSquared() int

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

func (v Vector2) Mult(other Vector2) Vector2

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

func (v Vector2) MultValue(value int) Vector2

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

func (v Vector2) Sub(other Vector2) Vector2

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

func (v Vector2) SubValue(value int) Vector2

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

func (v Vector2) ToVector2F() Vector2F

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

type Vector2F struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
}

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

func (v Vector2F) Add(other Vector2F) Vector2F

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

func (v Vector2F) AddValue(value float64) Vector2F

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

func (v Vector2F) Divide(other Vector2F) Vector2F

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

func (v Vector2F) DivideValue(value float64) Vector2F

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

func (v Vector2F) Dot(other Vector2F) float64

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

func (v Vector2F) IsZero() bool

IsZero reports whether both fields are exactly zero.

Vector2F{}.IsZero() // true

func (Vector2F) Length

func (v Vector2F) Length() float64

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

func (v Vector2F) LengthSquared() float64

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

func (v Vector2F) Mult(other Vector2F) Vector2F

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

func (v Vector2F) MultValue(value float64) Vector2F

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

func (v Vector2F) Normalize() Vector2F

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

func (v Vector2F) Sub(other Vector2F) Vector2F

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

func (v Vector2F) SubValue(value float64) Vector2F

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}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL