Skip to content

The luxe world

An introduction to working with the luxe world APIs.

outcome

In this tutorial we'll use the World api to put something on screen.
We'll also use a module, called Arcade for handling physics + collision. We'll load scenes and create prototype instances to populate a world, and create a custom Modifier.

We'll make a game where you play as a bee, and have to bounce on flowers.

Play it

Click first, then press Left Button, Up, W, X or Space to jump.
Press R key to reset.

Creating the project

For this tutorial, create a new project using the launcher, and when choosing an outline, select the tutorial project outline. This project is pre-configured so we can dive right in.

Create a new project from the tutorial project outline

Installing a module

In order to run our project, we first need to install a module. If you don't, you'll get errors!

You can use the launcher to install modules. Head over to the module page, and search for the arcade module. Once you find it, you can click through, and click the download arrow.

The project is configured to use version 0.0.23, install that version.

Install arcade version 0.0.23 to continue!

Using the module in a project

If you look inside of the luxe.project/modules.lx file you'll find the luxe and arcade modules referenced by version. You can use the launcher to add a module to the project using the + icon, or you can manually add it to this file.

In this case, it's already there from the outline, so let's move on!

The Transform API

In the Draw tutorial, we drew a circle in the center of the screen using an immediate style API.

With the world system, we can create things in the world that will continue to draw as long as they're alive. An Entity in the world can also have modifiers attached that perform logic, and run gameplay code.

We saw this in the original empty project template, right before deleting it!

To create an entity, we do Entity.create(world) - this gives us a blank entity, and is ready to be modified to give it meaning.

The first thing we'll do, is attach a Transform modifier. Modifiers use the same create pattern, and some modifiers add create methods with convenience arguments, like we'll see below from Sprite.create.

Let's create a new player variable in our game, and then inside ready we'll create an entity, attach a transform and a sprite to it.

world_width/world_height ?

Since our tutorial outline is based on the pixel outline, we have a fixed world size that will auto scale. This size is set in outline/settings.settings.lx and the size of the world is available in world_width and world_height. This is different from width/height, which is the window size.

add the highlighted code to ready

class Game is Ready {

  var random = Random.new()
  var draw: Draw = null

  var player = Entity.none

  construct ready() {

    super("ready! %(width) x %(height) @ %(scale)x")

    draw = Draw.create(World.render_set(world))

    player = Entity.create(world, "player")
    Transform.create(player, world_width/2, world_height/2)
    Sprite.create(player, Assets.image("image/bee"), 64, 64)

  } //ready

And just like that, we have our player in the middle of the screen.

Arcade physics

The arcade module provides collision + physics for a wide range of games, and comes with a bunch of ready to use tools.

The first important one is the Arcade modifier, which gives an entity a collider shape, and allows you to choose flags like whether it's solid or a trigger, what shape it is, change the velocity and more. It also gives us a callback for when we collide with something, so we can implement a response to overlapping or colliding with something.

Arcade import

We're gonna use the Arcade modifier from the arcade module to make our bee interact with the world. We'll import that module into the top of our game.wren code like this:

import "arcade: system/arcade.modifier" for Arcade, CollisionEvent, ShapeType

You'll see the arcade prefix on the import, this should be familiar because luxe is also a module, and we've seen the luxe: color import before. Imports without a prefix are project local.

Attach an arcade modifier

Much like a Transform or Sprite, we can attach Arcade to an entity using the same create pattern. Let's tidy up and make a create_player() function, and move our player code into it.

add the highlighted changes

construct ready() {

  super("ready! %(width) x %(height) @ %(scale)x")

  draw = Draw.create(World.render_set(world))

  create_player()

} //ready

create_player() {

  player = Entity.create(world, "player")
  Transform.create(player, world_width/2, world_height/2)
  Sprite.create(player, Assets.image("image/bee"), 64, 64)

  Arcade.create(player)
  Arcade.set_shape_type(player, ShapeType.circle)
  Arcade.set_radius(player, 32)

}

If we run this, it will look identical to before! That's because there's no gravity or anything on our entity. So how do we know it's working? How do we know the radius matches? We can ask Arcade to debug draw the physics state.

add the highlighted line to create_player

...
Arcade.set_radius(player, 32)
Arcade.set_debug_draw_enabled(world, true)

Gravity is a constant acceleration, so we can use the Arcade.set_acc tool to add a downward acceleration. The value is relative to your world size, and is game specific. For this game, we'll pick -200 as that feels good. You can make it whatever you want!

add the highlighted line to create_player

...
Arcade.set_radius(player, 32)
Arcade.set_acc(player, [0, -200])
Arcade.set_debug_draw_enabled(world, true)

If you run this now, you should see the bee falling off the bottom of the world!

Loading a scene

Our outline includes a scene that has been created for us. This scene includes some background details, and a floor collider which will keep our bee on screen.

A scene is a kind of data based asset, a container for pre-configured entities with their modifiers already attached.

A particular scene can only be loaded once into the same world, but you can load multiple scenes into the same world. This makes them useful as a tool to layer or keep things loaded in the world, and much more. Scenes are typically what you would use for stuff like a Menu, or Level based games.

Scene assets

Scenes are typically created with the luxe editor, but they're simple data inside of a folder. Take a look inside the scene/level.scene/ folder, and look inside any .entity.lx file!

With Scene.create we can load a scene from an asset. We'll use the Asset.scene(id) to grab the asset handle of the scene.

Work In Progress Asset API

The Scene API is available via import "luxe: world/scene" for Scene and is imported already.

Some assets, like the image above, use the older Assets.image(id) API (plural), while the scene and newer assets use Asset.scene(id) API (singular). The reason both exist is because we're moving to the new system and some assets aren't done moving yet.

Just before we create our player, we'll load our level scene into the world.

Add the highlighted line in ready

construct ready() {

  super("ready! %(width) x %(height) @ %(scale)x")

  draw = Draw.create(World.render_set(world))

  Scene.create(world, Asset.scene("scene/level"))

  create_player()

} //ready

With that, we'll see the clouds, some buildings, a gradient and we'll see the floor collider. The bee will bounce off the floor, and we're ready for the next step.

Named input events

In the first tutorial, we used Input.key_state_released to directly query a key. This is great for quick prototypes but doesn't allow multiple keys, gamepads, or mouse inputs easily.

For that we'll need to use named input events. A named input event is what it sounds like, a name assigned to one or more inputs! We have a few of these already defined by our project, if you look inside of outline/inputs.input.lx you'll see this:

jump = {
  keys = ["key_x", "up", "key_w", "space"]
  mouse = ["left"]
  gamepad = [0]
}

If we query this instead of the individual key, any of those inputs will trigger the event. Since these are named events we refer to them by a string value, "jump", but using strings all over our project can lead to code that can be difficult to change.

Instead, what we'll do is make an enum-like class that makes our code easier to use, and gives us code completion and errors if we spell it wrong. The pattern is a static function that returns a string, so we'll make one called In and a method called jump, so we can use In.jump to refer to the event name.

class In {
  static jump { "jump" }
}

class Game is Ready {
...

Implementing jump

Now inside the tick method, we'll add a jump method to make the bee jump. To do that, we'll get the current bee velocity, add some to it, and then set it back. We'll also set the x velocity to 0, because we never want the bee to move horizontally.

jump() {

  var velocity = Arcade.get_vel(player)

  velocity.x = 0
  velocity.y = velocity.y + 150

  Arcade.set_vel(player, velocity)

} //jump

tick(delta: Num) {

  if(Input.event_began(In.jump)) {
    jump()
  }

...

Now when we run the game and press Up, W, X or Space the bee will jump upward.

Player position and speed

The bee jump is a little easy to go off screen, so we'll make a minor change to create_player() to give them a max speed, and we'll also enforce that the bee is always in the same position on screen, about a quarter of the way in.

create_player() {

  ...

  Arcade.set_acc(player, [0, -200])
  Arcade.set_max_speed(player, 150)
  Arcade.set_debug_draw_enabled(world, true)

}

Inside tick, we'll set the player position to world_width / 4 every frame.

tick(delta: Num) {

  Transform.set_pos_x(player, world_width / 4)

  ...

Now when we play, we have a couple jumps before we leave the screen, and our bee is in a nice place for the game.

Create a Prototype instance

Prototypes are similar to a Scene, they are pre-configured entities with their modifiers ready to create.

Prototype vs Scene

Prototypes are not limited to one per world like scenes. You can create an instance as many times as you need.

They can be created dynamically like we will below, and they can be placed inside a scene, and inside of other prototypes. Each instance can have the values from the prototype overridden when placed that way.

Our project includes a pillar ready to go as prototype/pillar.0 (we'll make more with the luxe editor in the next tutorial).

We'll create a create_pillar function, and we'll call it right after creating the player. To create a prototype we use Prototype.create and Asset.prototype similarly. This returns a prototype root entity, which allows us to move the whole instance as a single unit.

  ...
  create_player()
  create_pillar()

} //ready

create_pillar() {

  var pillar = Prototype.create(world, Asset.prototype("prototype/pillar.0"))
  Transform.set_scale(pillar, 0.4, 0.4)
  Transform.set_pos(pillar, world_width / 2, random.int(15, 170))

} //create_pillar

With that, you should see a pillar spawned in the center of the world.

Creating a custom modifier

The next step is to move the pillars across the screen, so the player will have to jump over them.

There's a more detailed guide on custom modifiers here

To do that, we want to make a modifier that will move any pillar that it is attached to, and when the pillar moves off the left of the screen, clean itself up.

Create a folder called system/ in the project

The convention for modifiers is to be in a folder called system/, they are a wren file with a modifier subtype extension. We're gonna make a modifier called pillar and copy paste the code below into it.

Create an empty file called system/pillar.modifier.wren in the project

import "system/pillar.modifier.api" for API, Modifier, APIGet, APISet
import "luxe: world" for Entity, Transform
import "luxe: render" for Render, Geometry
import "luxe.project/asset" for Asset
import "luxe: assets" for Strings
import "luxe: game" for Frame

#block = data
class Data {

}

#api
#display = "Pillar"
#icon = "image/pillar.svg"
#desc = "**A moving pillar**. Moves the pillar horizontally toward the player, then removes itself when offscreen."
class Pillar is API {

}

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

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

  attach(entity: Entity, pillar: Data) {
    Log.print("attached to `%(Strings.get(Entity.get_name(entity)))` `%(entity)`")
  }

  detach(entity: Entity, pillar: Data) {
    Log.print("detached from `%(Strings.get(Entity.get_name(entity)))` `%(entity)`")
  }

  tick(delta: Num) {
    each {|entity: Entity, pillar: Data|

    }
  }
}

Attach the modifier

Now that we have a modifier, we can attach it in the same way that we do for the built in ones. We'll also modify our start position for the pillar, by setting it to world_width + 128 instead.

import "system/pillar.modifier" for Pillar
...
create_pillar() {

  var pillar = Prototype.create(world, Asset.prototype("prototype/pillar.0"))
  Transform.set_scale(pillar, 0.4, 0.4)
  Transform.set_pos(pillar, world_width + 128, random.int(15, 170))
  Pillar.create(pillar)

} //create_pillar

When you run this, you'll also see the line in the log:

[system/pillar.modifier line 29] - attached to prototype/pillar.0 42951770139

You also won't see the pillar! So let's make it move.

Moving the pillars

Inside our pillar.modifier.wren there's a Data class, which was empty at the time.

For our pillar to move, we'll need a speed value and we can store the speed value in this class. The data class is per entity data, and is a little bit special. The fields require a type definition, and often have extra tags to configure how the data works.

Add a speed variable like this with a default value of 100:

class Data {
  var speed: Num = 100
}

Now we can use the tick method to move our pillar and destroy the pillar when it goes off screen.

Systems are one per world

A modifier system sees all entities that is attached to, rather than on an individual entity level. We can see this in the tick method, it has an each method which will hand us each entity and the data for that entity.

Inside the tick method of our modifier, we are handed the entity that we're attached to. This entity is the prototype root of our instance, because that's the entity we attached it to.

When the pillar goes off screen, we'll see this in the log:

[system/pillar.modifier line 34] - detached from prototype/pillar.0 42951770139

The Frame.end {} runs a function at the end of the frame. This is a WIP requirement for this Entity.destroy

tick(delta: Num) {
  each {|entity: Entity, pillar: Data|

    var x_now = Transform.get_pos_x(entity) - pillar.speed * delta
    Transform.set_pos_x(entity, x_now)

    if(x_now < -256) {
      Frame.end { Entity.destroy(entity) }
    }

  } //each
} //tick

More pillars

We probably want more than one pillar to come across the level, so we'll use a tool called World.schedule(world, time, fn). This calls a function every time seconds, but the important part is that it is affected by the world rate.

If we used Frame.schedule(time, fn) it would be global, and not world specific. With World.schedule we can pause by setting the world rate to 0.

  ...

  create_player()

  create_pillar()
  World.schedule(world, 6, 9999) {
    create_pillar()
  }

} //ready

And with that change, we now get a constant stream of pillars to jump over! We have one more important thing to do to finish this tutorial.

Handling collision

Our last step for this game is handling what happens when you hit something.

If you saw the moving pillar video above, the player goes through the walls and keeps jumping forward because of our code to keep it in the same spot.

Instead what we'll do is check the direction of the hit, and if you hit a wall (sideways), pause the game world.

  ...

  handle_collision()

} //ready

handle_collision() {

  Arcade.add_collision_callback(player) {|entity_a, entity_b, state, normal, overlap_dist|
    if(state != CollisionEvent.begin) return

    var dot = Math.dot2D(normal, [0,1,0])

    if(dot.abs < 0.8) {
      World.set_rate(world, 0)
    }

  } //collision callback

} //handle_collision

You can see here we bounce off the top of things, but when we hit the side wall, we stop.

Polishing

The check is a little abrupt, and isn't very fun because it's super precise and you can fail easily.

To make the game a bit more fun, we'll add some squishy behaviour. When we hit a collider, we get the height and check the distance. If the distance is less than 32 (half the radius of our bee), we've just hit the edge of the collider with the bottom of the bee and we can ignore it.

Another tweak, we'll play a bounce animation when we hit a flower. This also uses the Tags modifier, which allows us to tag entities with specific tags and check for them. In this case, our flower entity inside the pillar prototype already has a tag.

handle_collision() {

  Arcade.add_collision_callback(player) {|entity_a, entity_b, state, normal, overlap_dist|
    if(state != CollisionEvent.begin) return

    var dot = Math.dot2D(normal, [0,1,0])

    var scale = Transform.get_scale_world(entity_b)
    var height = Arcade.get_height(entity_b) * scale.y
    var top = Transform.get_pos_y_world(entity_b) + (height * 0.5)
    var dist = (top - (Transform.get_pos_y_world(player) - 32))

    if(dot.abs < 0.8 && dist > 32) {
      World.set_rate(world, 0)
    }

    if(Tags.has_tag(entity_b, "flower")) {
      Anim.play(entity_b, "anim/bounce")
    }

  } //collision callback

} //handle_collision

Reset

One final task is to make it so you can reset the state so you can try again.

We'll add a reset() method, first we reset the player position, and unpause the world. This is called from tick using a simple key check.

reset() {
  Transform.set_pos(player, world_width/4, world_height/2, 0)
  World.set_rate(world, 1)
}

tick(delta: Num) {

  if(Input.key_state_released(Key.key_r)) {
    reset()
  }

  ...

Now, our pillars will still be there, so we'll need to clear them up. We could keep an array of pillars we create, and then clean them up like we did in the draw tutorial? The modifer system we created already knows about all of our pillars though!

We can add a public API to our pillar modifier, e.g Pillar.reset(world). To do this, we'll add a method to the API class in our modifier. This method has access to a method called system_in, which gives us our system to call into.

class Pillar is API {

  static reset(world: World) {
    var system: System = system_in(world)
    system.reset()
  }

}

Now inside of our system, we can add the reset method. This method will simply loop through each pillar, and destroy it.

class System is Modifier {

  ...

  reset() {
    each {|entity: Entity, pillar: Data|
      Frame.end { Entity.destroy(entity) }
    }
  }

And of course, don't forget to call it from our reset method:

reset() {
  Pillar.reset(world)
  Transform.set_pos(player, world_width/4, world_height/2, 0)
  World.set_rate(world, 1)
}

Debug off

One more tweak, now that we know it is working: turn off the debug drawer!

// Arcade.set_debug_draw_enabled(world, true)

Try this

Add score

Add a score variable to the game class, add 1 to it each time a flower is collected.

Add Game Over and a Win condition

Like before, make the experience more complete.

Experiment with values

Try randomizing pillar speeds, pillar schedule timing, bee velocities and more.

Final code

game.wren

import "luxe: world" for World, Entity, Transform, Sprite, Tags, Anim
import "luxe: draw" for Draw, PathStyle
import "luxe: render" for Material
import "luxe: game" for Frame
import "luxe: input" for Input, Key
import "luxe: assets" for Assets, Strings
import "luxe: asset" for Asset
import "luxe: math" for Math
import "luxe: string" for Str
import "luxe: io" for IO
import "random" for Random

import "luxe: world/scene" for Scene
import "luxe: world/prototype" for Prototype
import "arcade: system/arcade.modifier" for Arcade, CollisionEvent, ShapeType
import "system/pillar.modifier" for Pillar

import "outline/ready" for Ready

class In {
  static jump { "jump" }
}

class Game is Ready {

  var random = Random.new()
  var draw: Draw = null

  var player = Entity.none

  construct ready() {

    super("ready! %(width) x %(height) @ %(scale)x")

    draw = Draw.create(World.render_set(world))

    Scene.create(world, Asset.scene("scene/level"))
    create_player()

    create_pillar()
    World.schedule(world, 6, 9999) {
      create_pillar()
    }

    handle_collision()

  } //ready

  handle_collision() {

    Arcade.add_collision_callback(player) {|entity_a, entity_b, state, normal, overlap_dist|
      if(state != CollisionEvent.begin) return

      var dot = Math.dot2D(normal, [0,1,0])

      var scale = Transform.get_scale_world(entity_b)
      var height = Arcade.get_height(entity_b) * scale.y
      var top = Transform.get_pos_y_world(entity_b) + (height * 0.5)
      var dist = (top - (Transform.get_pos_y_world(player) - 32))

      if(dot.abs < 0.8 && dist > 32) {
        World.set_rate(world, 0)
      }

      if(Tags.has_tag(entity_b, "flower")) {
        Anim.play(entity_b, "anim/bounce")
      }

    } //collision callback

  } //handle_collision

  create_player() {

    player = Entity.create(world, "player")
    Transform.create(player, world_width/2, world_height/2)
    Sprite.create(player, Assets.image("image/bee"), 64, 64)

    Arcade.create(player)
    Arcade.set_shape_type(player, ShapeType.circle)
    Arcade.set_radius(player, 32)
    Arcade.set_acc(player, [0, -200])
    Arcade.set_max_speed(player, 150)
    // Arcade.set_debug_draw_enabled(world, true)

  } //create_player

  create_pillar() {

    var pillar = Prototype.create(world, Asset.prototype("prototype/pillar.0"))
    Transform.set_scale(pillar, 0.4, 0.4)
    Transform.set_pos(pillar, world_width + 128, random.int(15, 170))
    Pillar.create(pillar)

  } //create_pillar

  jump() {

    var velocity = Arcade.get_vel(player)

    velocity.x = 0
    velocity.y = velocity.y + 150

    Arcade.set_vel(player, velocity)

  } //jump

  reset() {
    Pillar.reset(world)
    Transform.set_pos(player, world_width/4, world_height/2, 0)
    World.set_rate(world, 1)
  }

  tick(delta: Num) {

    if(Input.key_state_released(Key.key_r)) {
      reset()
    }

    Transform.set_pos_x(player, world_width / 4)

    if(Input.event_began(In.jump)) {
      jump()
    }

    if(Input.key_state_released(Key.escape)) {
      IO.shutdown()
    }

  } //tick

} //Game

system/pillar.modifier.wren

import "system/pillar.modifier.api" for API, Modifier, APIGet, APISet
import "luxe: world" for Entity, Transform
import "luxe: render" for Render, Geometry
import "luxe.project/asset" for Asset
import "luxe: assets" for Strings
import "luxe: game" for Frame

#block = data
class Data {
  var speed: Num = 100
}

#api
#icon = "image/pillar.svg"
#display = "Pillar"
#desc = "**A moving pillar**. Moves the pillar horizontally toward the player, then removes itself when offscreen."
class Pillar is API {

  static reset(world: World) {
    var system: System = system_in(world)
    system.reset()
  }

}

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

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

  attach(entity: Entity, pillar: Data) {
    Log.print("attached to `%(Strings.get(Entity.get_name(entity)))` `%(entity)`")
  }

  detach(entity: Entity, pillar: Data) {
    Log.print("detached from `%(Strings.get(Entity.get_name(entity)))` `%(entity)`")
  }

  reset() {
    each {|entity: Entity, pillar: Data|
      Frame.end { Entity.destroy(entity) }
    }
  }

  tick(delta: Num) {
    each {|entity: Entity, pillar: Data|

      var x_now = Transform.get_pos_x(entity) - pillar.speed * delta
      Transform.set_pos_x(entity, x_now)

      if(x_now < -256) {
        Frame.end { Entity.destroy(entity) }
      }

    } //each
  } //tick

}