Skip to content

Behaviours

A behaviour is a Ruby class that inherits from Behaviour. Attached to an object, it gets one instance for that object, and the engine calls its methods as the game runs.

class Door < Behaviour
  def start; end                      # once, before the first update
  def update(dt); end                 # every frame; dt is seconds since the last one
  def fixed_update(dt); end           # 60 times per second, with the physics
  def on_trigger_enter(other); end    # something entered this object's trigger (or it entered one)
  def on_trigger_exit(other); end
  def on_collision_enter(other); end  # this object's body touched another
  def on_collision_exit(other); end
  def on_destroy; end
end

Define only the ones you need. other is the other object (a GameObject).

Files and class names

The file name decides the class name: camera_follow.rb must define CameraFollow. Scripts live in the game's scripts/ folder.

You never require another script. The first time you use a class name, RedGem loads the file it should be in (Patrol loads scripts/patrol.rb). That works for plain helper classes too.

Attaching a behaviour

There are three ways, and they mix freely:

  • In the editor: Inspector > Add Component, or drag a script from the Project panel onto an object.
  • In Blender: the custom property script = "door.rb" (several: "door.rb, sound.rb").
  • From Ruby: object.add_component(Door, speed: 2.0).

Settings

A behaviour reads its settings with prop:

def start
  @speed = prop(:speed, 3.0)   # 3.0 when nothing sets it
end

prop(:speed, default) returns, in this order: what add_component was given, the object's custom property speed (set in the Inspector or in Blender), or the default. So the same script can be tuned per object without changing code.

Reaching things

Inside a behaviour:

You write You get
game_object, transform, name this object, its transform, its name
character, rigidbody, vehicle this object's physics body of that kind, or nil
find("Hero"), find_all(:coin) an object by name, all objects with a tag
get_component(Health) another behaviour on the same object
other.get_component(Player) a behaviour on another object
destroy or destroy(other) removes an object from the game

Objects that are gone

destroy takes an object out at the end of the step: its on_destroy still runs, and until then it can be used as usual. After that, a reference you kept (in a variable, a global, a timer's block) stands for that object only. alive? says false, name and tag still answer, and anything else raises game object "Coin" (id 12) was destroyed, even once a newly spawned object sits in the same place (its id) or the level was switched. A kept reference never starts acting on another object.

@target = nil unless @target&.alive?   # forget an enemy someone else destroyed

Setting a level up in Ruby

Instead of attaching scripts object by object, a level can have a scene script: scripts/main_scene.rb for scene.glb, scripts/level2_scene.rb for scenes/level2.glb. Its setup runs when the level loads, before any behaviour starts.

class MainScene < SceneScript
  ENEMY_SPEEDS = { "Slime" => 1.5, "Bat" => 4.0 }

  def setup
    find!("Player").add_component(Player, speed: 6.0)
    ENEMY_SPEEDS.each { |name, speed| find!(name).add_component(Patrol, speed: speed) }
    find_all("coin").each { |coin| coin.add_component(Coin) }
  end
end

find! is find that stops with a clear error when the object is missing. With a scene script the level file says what is there and where, and Ruby says what it does.

When a script fails

An error in a behaviour does not stop the game. That behaviour is switched off, and the Console shows the error with the file and line. Fix it, press Play again.

A script that never returns

Every call the engine makes into the scripts (a behaviour's start and update, a contact, a scene script, a network handler) has a budget of time: 1000 ms unless the game's game.json says otherwise. A call still running past it (a loop {} with no break, a while true waiting for something that never comes) is stopped with a ScriptTimeout error. The behaviour is switched off like one that raised, the Console says which behaviour and method it was and how long it ran, and the game goes on. The game freezes for the length of the budget, once.

{ "id": "com.mystudio.mygame", "title": "My Game", "script_budget_ms": 2000 }

script_budget_ms is a whole number of milliseconds from 1 to 60000. It applies the same way to a game you play, to Play in the editor and to every room of a dedicated server, where each room has its own scripts and its own budget. Raise it for a scene script that builds a large level in one go; a frame's update should never come near it.

ScriptTimeout is not a StandardError, so rescue => e does not catch it. A call over its budget is stopped once, where it stands, and has 50 ms more to unwind: its ensure clauses run, so it can put back what it changed. If it is still running after that (an ensure that loops, a rescue Exception that carries on), it is stopped again at every step until it has returned.

What counts is the time spent in the call's own Ruby. Not counted: the work of the engine known to take long (spawning objects, placing scatter copies), and the calls the engine makes into the scripts from inside it, such as the start of a behaviour a spawned object brings, which each have a budget of their own. Up to four budgets of that are forgiven in one call: a loop that does nothing but spawn is stopped after about five. The engine's methods are never cut off halfway: the call stops at its next line of Ruby.

A recursion that never ends stops on its own, with SystemStackError, long before the program's stack runs out, and is handled the same way: its behaviour is switched off. redgem-test runs a file of Ruby tests with no budget, as one call.