Skip to content

Custom modifiers

Typically, modifiers are where you'll write a majority of your gameplay code. Naturally, that means you need to be able to add modifiers of your own to the mix. These modifiers become available in the editor, and can be used from code to combine and create complex behaviours in a modular way.

The modifiers have been rewritten and are just now landing in 2024.x. This means there may be bugs, gaps or other mysteries. Please let us know if you run into unexpected behaviour!

Modifiers exist in a world, and there is one system per world for a modifier type. A modifier system in a world will see and modify ALL entities that have the modifier attached. This is a little different from a 1:1 relationship, and enables gameplay code to be expressive about multiple things at once easily, but will still be familiar as you typically are still operating per entity.

As an example, a door modifier will be attached to 20 entities in a world, but in that world there is only one door modifier system running. That system knows about all doors in that world, and can answer questions like how many doors are open, without a lot of effort or looping or searching.

Modifiers can also run custom code in the editor, allowing custom behaviour between editor and the game. Any of the system functions (init, attach, etc) have an editor_ prefixed equivalent as well.

Making a modifier

Step 1. A modifier.wren file

A modifier is a script asset with a modifier subtype. To make a door modifier, you would make a file named door.modifier.wren in your project, typically inside a system/door.modifier.wren by convention.

In a Wren file, This is a built in variable for "module id".

This file contains three important pieces that we'll get into below:

  • A description of your system data per entity
  • A user facing API for your system
  • The system implementation

Take a look at the example below:

import "system/door.modifier.api" for API, Modifier, APIGet, APISet

#block = data
class Data {
  var locked: Bool = false
}

#api
#desc = "**A door**. Can be locked or unlocked."
#keywords(door, entrance, lock)
class Door is API {
  static unlock(entity: Entity) {
    get(entity).locked = false
  }
}

#system
#phase(on, tick)
class System is Modifier {

  init(world: World) {
    Log.print("init `%(This)` in world `%(world)`")
  }

  attach(entity: Entity, door: Data) {
    Log.print("attached to entity `%(entity)` - locked? %(door.locked)")
  }

  detach(entity: Entity, door: Data) {
    Log.print("detached from `%(entity)`")
  }

  tick(delta: Num) {
    each {|entity: Entity, door: Data|
      //use door.*
    } 
  }

}

Currently there's a door.modifier.api.wren generated for your system alongside it, this is a temporary generated file and won't stay long term.

Use via editor

If you save your file and re-open your project in the editor (it's not able to hot reload at the moment). Once opened, on an entity you can select attach and see your Door modifier is now available.

And if attached, will show up in omni along with the other modifiers below the scene outline.

Use via code

To use the modifier in code, you import the API class from the modifier id. Ours was called system/door.modifier.wren which makes the modifier id system/door.modifier.

import "system/door.modifier" for Door

...

var door = Entity.create("door")
Door.create(door)
Door.unlock(door)

Data field types

Docs WIP

These will get documented nicely, but here is a dump of the types and examples available to systems. Some of these have rough edges and will change slightly (nothing major).

#block = data
class Data {

  #display = "Number (count)"
  var number : Num = 11
  #display = "name"
  var string : String = "luxe" 
  var enum : MyTextAlign = MyTextAlign.bottom //defined below
  var uuid : UUID = null
  var link : Link = null //a reference to an entity in the editor/scene
  var color : Color = [1,1,1,1]
  var float2 : Float2 = [1,1]
  var float3 : Float3 = [1,1,1]
  var float4 : Float4 = [1,1,1,1]
  var boolean : Bool = true

  //Asset types have a type stored in the #tag 
  //so that the editor can show just the type you care about

  #tag = "luxe: type/scene.asset"
  var scene_to_load : Asset = "some/asset"

  //Strings can be tagged to add an OS file picker
  //with `path`, `path.open`, `path.save`, and `path.folder`

  #tag = "path.folder"
  var project_location: String = ""

  //Show if can hide fields that aren't relevant
  //based on the value in another option field.
  //So this field will only be visible in the UI if the enum is 
  //set to MyTextAlign.center in the UI 

  #show_if(enum = center)
  var other : Num = 22

  //Arrays

  //They have a count for efficiency,
  //the count isn't a limit. It's how much
  //space to reserve for your common use case,
  //which will make operating on this field faster.

  //For now, we specify the type as a tag on the field,
  //this will be improved later.
  //Any primitive array can have a default

  #count = 4
  #type = Num
  var array_number : List = [22, 33, 44, 55]

  #count = 2
  #type = MyTextAlign
  var array_enum : List = [MyTextAlign.center]

  //No defaults for objects here, 
  //defaults are defined in the object itself
  var obj : Obj = Object

  //Arrays of objects are valid too,
  //and they work similarly, they can't
  //have a default here. The list can 
  //have an empty default though

  #count = 1
  #type = Obj
  var obj_array : List = []

  //Groups
  //Any consecutive fields with the same group tag 
  //will be collected together in a collapsible group

  #group="Stuff Together"
  var hey : Num = 99.0
  #group="Stuff Together"
  var hi : String = "how?" 

  #group="Other Together"
  var why : Num = 99.0
  #group="Other Together"
  var here : String = "how?" 

}

//option tag is required
#option
class MyTextAlign {
  static left { "left" }
  static center { "center" }
  static right { "right" }
  static top { "top" }
  static bottom { "bottom" }
}

//Objects have the same types as above
class Obj {
  var other: Num = 1
  var some: String = "fields"
  var here: Color = [111,112,113,114]
  var yea: Float2 = [11, 12]
}

To be continued...