1
Actors
minjaesong edited this page 2025-11-24 21:24:45 +09:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:

  1. Actor — Base class for all entities
  2. ActorWithBody — Actors with position, physics, and sprites
  3. 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:

  1. Stops any music tracks associated with the actor
  2. Calls the despawn hook
  3. 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:

  1. Implement FixtureBase or similar classes
  2. Have associated UI interactions
  3. May be immobile (physProp.immobile = true)
  4. 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

  1. Always call super.reload() when overriding reload()
  2. Mark sprites and UI as @Transient — they cannot be serialised
  3. Use ArrayList for lightBoxList/shadeBoxList — other collections won't serialise properly
  4. Set canBeDespawned = false for actors that must persist (like the player)
  5. Use ActorValue for dynamic properties rather than adding many fields
  6. Clean up resources in despawn() — stop audio, dispose textures, etc.
  7. Keep actor updates efficient — updateImpl is called every frame for every actor
  8. 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