Table of Contents
- Actors
- Overview
- Actor Base Class
- ActorWithBody
- Position and Hitbox
- Physics Properties
- Velocity
- Mass and Scale
- Sprites and Animation
- Lighting
- Stationary vs. Mobile
- Actor Interfaces
- Specialised Actor Types
- Creating Custom Actors
- Actor Management in IngameInstance
- Best Practises
- Common Pitfalls
- See Also
Actors
Actors are the fundamental interactive entities in the Terrarum engine. Everything that can be updated and rendered in the game world—players, NPCs, creatures, particles, projectiles, furniture, and more—is an Actor.
Overview
The Actor system is built on a hierarchy of classes providing increasing levels of functionality:
- Actor — Base class for all entities
- ActorWithBody — Actors with position, physics, and sprites
- Specialised Actor Types — Players, NPCs, fixtures, and more
Actor Base Class
All actors inherit from net.torvald.terrarum.gameactors.Actor.
Key Properties
Reference ID
Every actor has a unique Reference ID (ActorID), an integer between 16,777,216 and 0x7FFF_FFFF. This ID is used to track and retrieve actors throughout their lifecycle.
val referenceID: ActorID
The player actor always has a special reference ID: Terrarum.PLAYER_REF_ID (0x91A7E2).
Render Order
Determines which visual layer the actor is drawn on. From back to front:
- FAR_BEHIND — Wires and conduits
- BEHIND — Tapestries, some particles (obstructed by terrain)
- MIDDLE — Standard actors (players, NPCs, creatures)
- MIDTOP — Projectiles, thrown items
- FRONT — Front walls (blocks that obstruct actors)
- OVERLAY — Screen overlays, not affected by lightmap
var renderOrder: RenderOrder
Actor Lifecycle
Update Cycle
Actors implement the updateImpl(delta: Float) method, which is called every frame:
abstract fun updateImpl(delta: Float)
The base update() method automatically handles despawn logic before calling your implementation.
Despawning
Actors can be flagged for removal from the game world:
var flagDespawn: Boolean
val canBeDespawned: Boolean // override to prevent despawning
When an actor is despawned, the despawn() method is called, which:
- Stops any music tracks associated with the actor
- Calls the despawn hook
- Marks the actor as despawned
@Transient var despawnHook: (Actor) -> Unit = {}
Serialisation and Reload
When saving the game, actors are serialised. Fields marked with @Transient are not saved and must be reconstructed when loading.
The reload() method is called after deserialisation:
open fun reload() {
actorValue.actor = this
// Reconstruct transient fields here
}
ActorValue System
Actors have a flexible property system called ActorValue, similar to Elder Scrolls' actor value system. It stores various attributes and stats:
var actorValue: ActorValue
ActorValue stores key-value pairs and notifies the actor when values change via the event handler:
abstract fun onActorValueChange(key: String, value: Any?)
Common ActorValue keys are defined in AVKey (e.g., AVKey.SCALE, AVKey.BASEMASS, AVKey.NAME).
Audio
Actors can play music and sound effects with spatial positioning:
// Music tracks associated with this actor
val musicTracks: HashMap<MusicContainer, TerrarumAudioMixerTrack>
// Start playing music
fun startMusic(music: MusicContainer)
By default, music stops when the actor despawns. Override stopMusicOnDespawn to change this behaviour:
open val stopMusicOnDespawn: Boolean = true
ActorWithBody
ActorWithBody extends Actor with physical presence, including position, collision, physics, and sprite rendering.
Position and Hitbox
ActorWithBody has a precise pixel-based position system:
val hitbox: Hitbox // position and dimensions in pixels
The hitbox defines the actor's:
- Position (top-left corner in pixels)
- Dimensions (width and height in pixels)
- Collision area
Setting Position and Size
Use these methods to configure the actor's spatial properties:
fun setHitboxDimension(width: Int, height: Int, translateX: Int, translateY: Int)
fun setPosition(x: Double, y: Double)
fun translatePosition(deltaX: Double, deltaY: Double)
Tilewise Hitboxes
For tile-based calculations, ActorWithBody provides tilewise hitbox conversions:
// Half-integer tilewise hitbox (for physics)
val hIntTilewiseHitbox: Hitbox
// Integer tilewise hitbox (for block occupation checks)
val intTilewiseHitbox: Hitbox
When iterating over tiles occupied by an actor:
for (x in actor.intTilewiseHitbox.startX.toInt()..actor.intTilewiseHitbox.endX.toInt()) {
for (y in actor.intTilewiseHitbox.startY.toInt()..actor.intTilewiseHitbox.endY.toInt()) {
// Process tile at (x, y)
}
}
Physics Properties
ActorWithBody contains a PhysProperties object defining physical characteristics:
var physProp: PhysProperties
PhysProperties includes:
- immobile — Whether the actor can move
- usePhysics — Whether physics simulation applies
- movementType — Determines physics behaviour (see PhysicalTypes)
The physics simulation applies acceleration, velocity, and collision. Use the following constants:
- METER — 1 metre = 25 pixels
- PHYS_TIME_FRAME — Time step for physics simulation
Velocity
ActorWithBody has two velocity systems:
External Velocity
For physics-driven movement (gravity, collisions, forces):
internal val externalV: Vector2
Controller Velocity
For player/AI-controlled movement:
var controllerV: Vector2? // non-null only for Controllable actors
Velocity units are pixels per frame (at 60 FPS). The engine automatically scales velocity for different frame rates using:
val adjustedVelocity = velocity * (Terrarum.PHYS_REF_FPS * delta)
Mass and Scale
ActorWithBody supports dynamic mass and size:
val mass: Double // Calculated from base mass × scale³
val scale: Double // Apparent scale (base scale × scale buffs)
Mass is stored in kilograms. Default mass is defined in the physics properties.
Sprites and Animation
Actors can have multiple sprite layers:
@Transient var sprite: SpriteAnimation? // Main sprite
@Transient var spriteGlow: SpriteAnimation? // Glow layer
@Transient var spriteEmissive: SpriteAnimation? // Emissive layer
Sprites must be reconstructed in the reload() method as they are transient.
The drawMode property controls sprite blending:
var drawMode: BlendMode = BlendMode.NORMAL
Lighting
ActorWithBody actors can emit and block light:
open var lightBoxList: ArrayList<Lightbox> // Light emission areas
open var shadeBoxList: ArrayList<Lightbox> // Light blocking areas
Lightboxes are relative to the actor's position and define RGB+UV light values.
Important: These lists must be marked @Transient and use ArrayList (which has a no-arg constructor for serialisation).
Stationary vs. Mobile
Actors can be flagged as stationary to optimise rendering and physics:
open var isStationary: Boolean = true
Stationary actors don't move and can be culled more aggressively.
Actor Interfaces
The engine provides several interfaces to add specific capabilities to actors:
Controllable
For actors that can be controlled by players or AI:
interface Controllable {
var controllerV: Vector2?
var moveState: MoveState
// ... control methods
}
AvailableControllers:
- Player input
- AI controller
- Scripted movement
Factionable
For actors belonging to factions:
interface Factionable {
var faction: String?
// Faction relationships affect AI behaviour
}
See also: Faction
AIControlled
For actors with AI behaviour:
interface AIControlled {
fun aiUpdate(delta: Float)
}
Luminous
For actors that emit light (integrated into ActorWithBody via lightBoxList).
NoSerialise
A marker interface for actors that should not be saved:
interface NoSerialise
Specialised Actor Types
BlockMarkerActor
A special actor that marks block positions without physical interaction. Used internally for debug visualisation and block tracking.
WireActor
Represents wire/conduit connections in the game world. Rendered in the FAR_BEHIND layer.
Fixtures
Fixtures are special ActorWithBody instances representing world objects like:
- Doors
- Chests
- Crafting stations
- Furniture
- Interactive machinery
Fixtures typically:
- Implement
FixtureBaseor similar classes - Have associated UI interactions
- May be immobile (
physProp.immobile = true) - Often use
isStationary = true
Particles
Particles are lightweight actors for visual effects:
- Typically have short lifespans
- May not use full physics
- Often rendered in BEHIND or MIDTOP layers
- Usually set
canBeDespawned = true
Projectiles
Projectiles (arrows, bullets, thrown items):
- Extend ActorWithBody
- Typically use MIDTOP render order
- Implement collision detection
- Self-despawn on impact or timeout
Creating Custom Actors
Basic Actor Example
class MyActor : Actor(RenderOrder.MIDDLE, null) {
override fun updateImpl(delta: Float) {
// Update logic here
}
override fun onActorValueChange(key: String, value: Any?) {
// Respond to actor value changes
}
override fun run() {
// Thread execution if needed
}
}
ActorWithBody Example
class MyPhysicalActor : ActorWithBody(
RenderOrder.MIDDLE,
PhysProperties.HUMANOID_DEFAULT(),
null // auto-generate ID
) {
init {
setHitboxDimension(32, 48, 0, 0) // 32×48 pixel hitbox
setPosition(100.0, 100.0)
}
override fun updateImpl(delta: Float) {
// Physics is handled automatically
// Add custom behaviour here
}
override fun onActorValueChange(key: String, value: Any?) {
when (key) {
AVKey.SCALE -> {
// Respond to scale changes
}
}
}
override fun reload() {
super.reload()
// Reconstruct sprite and other transient fields
sprite = loadMySprite()
}
}
Actor Management in IngameInstance
Actors are managed by the IngameInstance which maintains actor lists and handles updates:
// Get an actor by ID
val actor = INGAME.getActorByID(actorID)
// Add an actor to the world
INGAME.addNewActor(myActor)
// Remove an actor
actor.flagDespawn = true
Best Practises
- Always call
super.reload()when overriding reload() - Mark sprites and UI as
@Transient— they cannot be serialised - Use ArrayList for lightBoxList/shadeBoxList — other collections won't serialise properly
- Set
canBeDespawned = falsefor actors that must persist (like the player) - Use ActorValue for dynamic properties rather than adding many fields
- Clean up resources in
despawn()— stop audio, dispose textures, etc. - Keep actor updates efficient — updateImpl is called every frame for every actor
- Use appropriate RenderOrder — incorrect ordering causes visual glitches
Common Pitfalls
- Forgetting to reconstruct transient fields in reload() leads to null sprite crashes
- Modifying hitbox directly instead of using setPosition/setHitboxDimension causes physics issues
- Not accounting for delta time makes behaviour frame-rate dependent
- Creating circular actor references can prevent serialisation
See Also
- Glossary — Actor-related terminology
- Development:Actors — In-depth actor implementation details
- Modules:Actors — Creating actors in modules
- Faction — Faction system for actors
- Creature RAW — Data-driven creature definitions