Live tutorial
The last week was spent adding the Wren language to program toy. Unlike lua, Wren is class-based, while sharing many of the advantages that make lua such a great scripting language for games. It's thrilling to finally have this feature because it opens up a world of possibilities for game programming learning material, directly consumable on the web.
This tutorial will teach you the fundamentals of setting up a simple yet fully working game using toy. Thanks to how portable toy is, you will do all this right here, on this web page :)
You are free to write directly in the script editor, or to use the paste
button of each snippet which will insert the code at the current cursor position inside the editor. Using the browser copy function with Ctrl+V
will also put it in the editor clipboard, so you can then paste it.
This tutorial aims to familiarize yourself with toy user API, as well as demonstrate the potential for live interactive learning material, and how toy can help achieve that. It uses Wren, but all the functions and classes used here are just a 1:1 mapping of the C++ API. Please keep in mind this is also a proof of concept, which will be further refined and perfected in future iterations.
This tutorial is divided into four successive parts: if you don't want to actually complete it, but just want to see the example code in action, you can preview each part by clearing the text editor, clicking on the paste
button below, and press reload
in the editor panel.
- part 1 - draw a viewport, control the camera and render randomly colored cubes
- part 2 - define an entity type, and create a world filled with randomly colored entities
- part 3 - add physics to our entities, and create a terrain object
- part 4 - define an agent type whose movement can be controlled, and add a controller to command it
Creating the App
The code on the right is your starting point: it setups the minimal toy game, a running app, with a black screen :)
The GameModule
interface provides five main hooks for writing your game logic:
init()
is called when initializing the gamestart()
is called when the game session is startedpump()
is called on each framescene()
is called everytime a scene is createdpaint()
is called each frame when a scene is painted
Creating a viewer
The first thing we will do is creating a Viewer
in the UI hierarchy of our app. The ui::scene_viewer
function declares a special kind of Viewer
that contains its own Scene
. Call it in the body of the pump()
function:
var viewer = Ui.scene_viewer(ui)
Note: In Wren, everything is an object, and every function is a method, so this is actually a static method on the Ui
class. In C++, it's a regular function.
Starting to draw
Next, we want to draw some geometry to the viewport. First, we need to initialize the scene graph using Scene::begin()
.
var graph = viewer.scene.begin()
Press reload
. You created an empty black viewport. Just to be sure there is a viewer, you can try this:
viewer.viewport.clear_colour = Colour.Red
Drawing a cube
Now that our scene graph is initialized, we can start actually drawing geometry. Add these to the pump()
function after creating the viewer:
- declare a node in the graph, which holds the transform, by calling
gfx::node()
, passing the parent, an object (null), and a position - declare a shape as a child of this node, by calling
gfx::shape()
passing the parent, the shape, and a symbol holding the shape colours
var node = Gfx.node(graph)
Gfx.shape(node, Cube.new(), Symbol.new(Colour.White))
Press reload
. Now you have a white cube in the middle of the viewport.
You can try passing a different Colour
, or a different Cube
created with a size argument of type Vec3
:
Colour.new(0.3, 0.8, 0.1)
Cube.new(Vec3.new(1, 2, 0.5))
Controlling the camera
The simplest way is to call ui::orbit_controller
, which takes a Viewer
and controls its camera. It's the simplest controller: it handles input to make the camera orbit around a fixed center point, when pressing Middle Mouse Button
.
var orbit = Ui.orbit_controller(viewer)
Drawing more cubes
Let's create more cubes, in a loop, by passing different colors and positions to thenode()
and draw()
functions.
for (i in 0...50) {
var colour = Colour.hsl(i / 50, 1, 0.5)
var node = Gfx.node(graph, null, Vec3.new(i * 4 - 100, 0, 0))
Gfx.draw(node, Cube.new(Vec3.new(1)), Symbol.new(colour))
}
Note: the Symbol
object controls how the shape is rendered. Its two first parameters are the solid/plain colour, and the wireframe colour (if any).
Make it random
Wren provides a built-in Random
number generator, on which you call the float()
or int()
functions to generate numbers in the given range. First, create it.
var rand = Random.new()
Then, inside the loop, let's create random position
, extents
and colour
for each of our cubes.
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var extents = Vec3.new(rand.float(1, 5))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
You have to also modify the calls to the node()
and draw()
functions to use the attributes we created:
var node = Gfx.node(graph, null, position)
Gfx.draw(node, Cube.new(extents), Symbol.new(colour))
First part recap
By now, you should have a code similar to the following:
import "random" for Random
import "toy" for ScriptClass, Vec2, Vec3, Complex, Colour, Cube, Sphere, Quad, Symbol, Ui, Gfx, BackgroundMode, DefaultWorld, Entity, Movable, Solid, CollisionShape, GameMode, OrbitMode
foreign class MyGame {
static new(module) { __constructor.call(MyGame, module) }
static bind() { __constructor = VirtualConstructor.ref("GameModuleBind") }
init(app, game) { start(app, game) }
start(app, game) {}
pump(app, game, ui) {
var viewer = Ui.scene_viewer(ui)
var orbit = Ui.orbit_controller(viewer)
var graph = viewer.scene.begin()
var rand = Random.new()
for (i in 0...50) {
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var extents = Vec3.new(rand.float(1, 5))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
var node = Gfx.node(graph, null, position)
Gfx.draw(node, Cube.new(extents), Symbol.new(colour))
}
}
scene(app, scene) {}
paint(app, scene, graph) {}
}
MyGame.bind()
var game = MyGame.new(module)
Defining an Entity
Until now, we were directly declaring pure graphical shapes. To design a game, it's convenient to define some Entity
types.
class Body {
construct new(id, parent, position, shape, colour) {
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, position)
_shape = shape
_colour = colour
_complex.setup([_entity])
}
static bind() { __cls = ScriptClass.new("Body", [Entity.type]) }
}
Let's add the above definition to the top of our script: we define an Entity with only one component: an Entity
, which holds the 3d transform, and a list of children. This is enough to formalize the simplest spatial objects.
We also need to bind this new class, somewhere near the end of the script:
Body.bind()
Accessing fields
In Wren, fields cannot be accessed directly, they need to be exposed through getters. We will need to access these three fields to render our entities, so add this code to the Body
class:
entity { _entity }
shape { _shape }
colour { _colour }
Setting up our globals
Instead of directly declaring pure graphical shapes, we will now render proper Entities. To do this, we need: a World
to contain the entities, a Scene
to render them, and a list of entities. We will declare them as global variables: add these declarations anywhere in the script, outside of any class:
var GWorld = null
var GScene = null
var GBodies = []
Note: in Wren, global variables need to start with an uppercase letter to be accessible from inside class methods.
Creating the World
We will create all these inside the start()
function. First, let's create the game world:
GWorld = DefaultWorld.new("Example")
game.world = GWorld.world
Create the entities
We can now create an array of Bodies
parented to the World
we just created, and use the same random logic we used to draw cubes in the first part of the tutorial, only now we pass the attributes position
, shape
and colour
to the Body
constructor:
var rand = Random.new()
for (i in 0...50) {
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
var shape = Cube.new(Vec3.new(rand.float(1, 5)))
GBodies.add(Body.new(0, GWorld.world.origin, position, shape, colour))
}
Creating a Scene
Finally we need to add a scene to the game, which we will render in our viewport. Add this to the end of the start()
handler:
GScene = app.add_scene()
Because the scene is now managed by the application, instead of creating a scene_viewer()
, which contains its own Scene
, we create a simple viewer()
and pass it the scene to render. Let's replace the code inside pump()
with this:
var viewer = Ui.viewer(ui, GScene.scene)
var orbit = Ui.orbit_controller(viewer, 0, -0.37, 100.0)
Rendering the Scene
The rendering of this scene will now be handled by the application: it will call the paint()
handler. Let's add this very simple rendering code inside paint()
:
for (body in GBodies) {
var self = Gfx.node(graph, body, body.entity.position, body.entity.rotation)
Gfx.shape(self, body.shape, Symbol.new(body.colour))
}
Visual enhancements
We can add a radiance environment and a sun light at the beginning of our paint()
handler:
Gfx.radiance(graph, "radiance/tiber_1_1k.hdr", BackgroundMode.Radiance)
Gfx.sun_light(graph, 0, 0.37)
Physically based materials
We can also render our cubes with a physically based material instead of the default unshaded one. To do this, we call Gfx::pbr_material
, which expects the name of the material, and the colour. We will slightly modify the rendering loop and make it like this:
for (body in GBodies) {
var node = Gfx.node(graph, body, body.entity.position, body.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "body %(body.entity.id)", body.colour)
Gfx.shape(node, body.shape, Symbol.new(Colour.White), 0, material)
}
We are using Wren's builtin %()
operator, which is used to add a formatted value inside of a string literal, to create a material identifier unique to our body.
Result
Finally, pressing reload
should yield the expected scene filled with your randomly created entities of various size, shapes and colours. If you didn't manage, don't worry: the full solution is on the next page.
This looks very similar to what we did in the first part, right ? Why go to all this trouble for the same result ? In the first part, we only had pure immediately rendered graphical shapes. Having proper game Entities instead, means we can apply all sorts of game logic to these objects, like add physics to them. The next part will show you how to do that.
Second part recap
By now, you should have a code similar to the following:
import "random" for Random
import "toy" for ScriptClass, Vec2, Vec3, Complex, Colour, Cube, Sphere, Quad, Symbol, Ui, Gfx, BackgroundMode, DefaultWorld, Entity, Movable, Solid, CollisionShape, GameMode, OrbitMode
class Body {
construct new(id, parent, position, shape, colour) {
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, position)
_shape = shape
_colour = colour
_complex.setup([_entity])
}
entity { _entity }
shape { _shape }
colour { _colour }
static bind() { __cls = ScriptClass.new("Body", [Entity.type]) }
}
var GWorld = null
var GScene = null
var GBodies = []
foreign class MyGame {
static new(module) { __constructor.call(MyGame, module) }
static bind() { __constructor = VirtualConstructor.ref("GameModuleBind") }
init(app, game) { start(app, game) }
start(app, game) {
GWorld = DefaultWorld.new("Example")
game.world = GWorld.world
var rand = Random.new()
for (i in 0...50) {
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
var size = Vec3.new(rand.float(1, 5))
GBodies.add(Body.new(0, GWorld.world.origin, position, Cube.new(size), colour))
}
GScene = app.add_scene()
}
pump(app, game, ui) {
var viewer = Ui.viewer(ui, GScene.scene)
var orbit = Ui.orbit_controller(viewer, 0, -0.37, 100.0)
}
scene(app, scene) {}
paint(app, scene, graph) {
Gfx.radiance(graph, "radiance/tiber_1_1k.hdr", BackgroundMode.Radiance)
Gfx.sun_light(graph, 0, 0.37)
for (body in GBodies) {
var node = Gfx.node(graph, body, body.entity.position, body.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "body %(body.entity.id)", body.colour)
Gfx.shape(node, body.shape, Symbol.new(Colour.White), 0, material)
}
}
}
Body.bind()
MyGame.bind()
var game = MyGame.new(module)
Adding physics
Let's add one more component to our Body
class. The Solid
component adds rigid body physics to an Entity
. It expects a CollisionShape
, which we construct from a Shape
, a flag to specify whether it's static
, and its mass
.
class Body {
construct new(id, parent, position, shape, colour) {
// add these lines
_movable = Movable.new(_entity)
_solid = Solid.new(_entity, CollisionShape.new(shape), false, 1.0)
// ...
// modify this line: we need to add that new components to the Entity
_complex.setup([_entity, _movable, _solid])
}
// ...
// add a getter for the solid component, we will need it later
solid { _solid }
// modify this line: we need to register all the components in our Entity class
static bind() { __cls = ScriptClass.new("Body", [Entity.type, Movable.type, Solid.type]) }
}
Press reload
. Your bodies will now fall downwards!
Defining a Terrain class
We don't want our objects to fall indefinitely. Let's add a new Terrain
Entity class.
class Terrain {
construct new(id, parent, size) {
_quad = Quad.new(Vec3.new(size,0,size), Vec3.new(size,0,-size), Vec3.new(-size,0,-size), Vec3.new(-size,0,size))
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, Vec3.new(0, -10, 0))
_solid = Solid.new(_entity, CollisionShape.new(_quad), true, 0.0)
_complex.setup([_entity, _solid])
}
quad { _quad }
entity { _entity }
static bind() { __cls = ScriptClass.new("Terrain", [Entity.type, Solid.type]) }
}
Two main differences with our Body
class:
- we create a
Quad
shape, using thesize
given as a parameter to our constructor - we pass
true
to thestatic
parameter of theSolid
, and amass
of0.0
- we don't need a
Movable
component this time
Bind the Terrain class
Like the other classes, we need to bind our Terrain
class to register its type.
Terrain.bind()
Creating the Terrain object
Let's declare a GTerrain
global variable next to our other globals, to hold the terrain object.
var GTerrain = null
Let's create the terrain object, with a size
of 100
, in the body of start()
handler:
GTerrain = Terrain.new(0, GWorld.world.origin, 100)
Press reload
. The objects will now fall downwards but be stopped by the invisible Terrain
object! We need to render it too.
Rendering the Terrain object
Finally, we need to add some rendering logic: again, like for the Bodies
, we simply render its shape, with a predefined grey pbr material:
var terrain = Gfx.node(graph, GTerrain, GTerrain.entity.position, GTerrain.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "ground", Colour.new(0.3, 1))
Gfx.shape(terrain, GTerrain.quad, Symbol.new(Colour.White), 0, material)
Alternate cubes and spheres
Now that we have physics enabled, it will be more fun with different types of shapes. Let's modify the creation loop slightly, to alternate between Cubes
and Spheres
:
for (i in 0...100) {
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
var shape = i % 2 == 0 ? Cube.new(Vec3.new(rand.float(1, 5))) : Sphere.new(rand.float(1, 5))
GBodies.add(Body.new(0, GWorld.world.origin, position, shape, colour))
}
Third part recap
By now, you should have a code similar to the following:
import "random" for Random
import "toy" for ScriptClass, Vec2, Vec3, Complex, Colour, Cube, Sphere, Quad, Symbol, Ui, Gfx, BackgroundMode, DefaultWorld, Entity, Movable, Solid, CollisionShape, GameMode, OrbitMode
class Body {
construct new(id, parent, position, shape, colour) {
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, position)
_movable = Movable.new(_entity)
_solid = Solid.new(_entity, CollisionShape.new(shape), false, 1.0)
_shape = shape
_colour = colour
_complex.setup([_entity, _movable, _solid])
}
entity { _entity }
solid { _solid }
shape { _shape }
colour { _colour }
static bind() { __cls = ScriptClass.new("Body", [Entity.type, Movable.type, Solid.type]) }
}
class Terrain {
construct new(id, parent, size) {
_quad = Quad.new(Vec3.new(size,0,size), Vec3.new(size,0,-size), Vec3.new(-size,0,-size), Vec3.new(-size,0,size))
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, Vec3.new(0, -10, 0))
_solid = Solid.new(_entity, CollisionShape.new(_quad), true, 0.0)
_complex.setup([_entity, _solid])
}
quad { _quad }
entity { _entity }
static bind() { __cls = ScriptClass.new("Terrain", [Entity.type, Solid.type]) }
}
var GWorld = null
var GScene = null
var GTerrain = null
var GBodies = []
foreign class MyGame {
static new(module) { __constructor.call(MyGame, module) }
static bind() { __constructor = VirtualConstructor.ref("GameModuleBind") }
init(app, game) { start(app, game) }
start(app, game) {
GWorld = DefaultWorld.new("World")
game.world = GWorld.world
GTerrain = Terrain.new(0, GWorld.world.origin, 100)
var rand = Random.new()
for (i in 0...100) {
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
var shape = i % 2 == 0 ? Cube.new(Vec3.new(rand.float(1, 5))) : Sphere.new(rand.float(1, 5))
GBodies.add(Body.new(0, GWorld.world.origin, position, shape, colour))
}
GScene = app.add_scene()
}
pump(app, game, ui) {
var viewer = Ui.viewer(ui, GScene.scene)
var orbit = Ui.orbit_controller(viewer, 0, -0.37, 100.0)
}
scene(app, scene) {}
paint(app, scene, graph) {
Gfx.radiance(graph, "radiance/tiber_1_1k.hdr", BackgroundMode.Radiance)
Gfx.sun_light(graph, 0, 0.37)
var terrain = Gfx.node(graph, GTerrain, GTerrain.entity.position, GTerrain.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "ground", Colour.new(0.3, 1))
Gfx.shape(terrain, GTerrain.quad, Symbol.new(Colour.White), 0, material)
for (body in GBodies) {
var node = Gfx.node(graph, body, body.entity.position, body.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "body %(body.entity.id)", body.colour)
Gfx.shape(node, body.shape, Symbol.new(Colour.White), 0, material)
}
}
}
Body.bind()
Terrain.bind()
MyGame.bind()
var game = MyGame.new(module)
Defining an Agent class
To allow the user to control an object, let's add one more class: an Agent
, which is a special kind of Body
, with additional logic so that its movement can be controlled.
class Agent is Body {
construct new(id, parent, position, shape, colour) {
super(id, parent, position, shape, colour)
_aiming = true
_angles = Vec2.new(0)
_force = Vec3.new(0)
_torque = Vec3.new(0)
}
aiming { _aiming }
angles { _angles }
force { _force }
torque { _torque }
}
The force
and torque
will represent the translation and rotation forces applied to our agent, relative to itself. The aiming
and angles
fields will be used by the camera controller, to determine in which direction we are looking.
Adding the movement logic
Each frame, we will use the previously defined fields to modify the velocity of the actual simulated physics object. Let's add an update()
function that we will call each frame to do the following:
- calculate the
force
in absolute coordinates - apply the absolute
force
, together with thetorque
, to the physicalSolid
object - set the
angular factor
to0
to maintain our object upright in all situations
class Agent is Body {
// ...
update() {
var velocity = this.solid.impl.linear_velocity()
var force = Mud.rotate(this.entity.rotation, _force)
this.solid.impl.set_linear_velocity(Vec3.new(force.x, velocity.y - 1, force.z))
this.solid.impl.set_angular_velocity(_torque)
this.solid.impl.set_angular_factor(Vec3.new(0))
}
}
Note: We are combining the force with the vertical component of the velocity, minus one, to simulate gravity.
Create the player Agent
Create another global variable GAgent
near the other ones to hold our player agent.
GAgent = null
Then add this code to create it, in the body of the start()
handler:
GAgent = Agent.new(0, GWorld.world.origin, Vec3.new(0), Cube.new(), Colour.White)
By just appending it to our list of bodies, we ensure it will be rendered with the other bodies:
GBodies.add(GAgent)
Handling the input/control logic
To correctly handle input to control our object, we have to add three things inside the pump()
handler right after creating the viewer:
- call
hybrid_controller
withThirdPerson
mode: it handles the mouseAgent
aiming angle for us - call
velocity_controller
, to map the arrow keys to update theforce
andtorque
accordingly
Ui.hybrid_controller(viewer, OrbitMode.ThirdPerson, GAgent.entity, GAgent.aiming, GAgent.angles)
Ui.velocity_controller(viewer, GAgent.force, GAgent.torque, 20)
Finally we need to call the update()
function we implemented on our Agent
class, to apply the forces to the physical object.
GAgent.update()
Fourth part recap
By now, you should have a code similar to the following:
import "random" for Random
import "toy" for ScriptClass, Complex, Vec2, Vec3, Colour, Cube, Sphere, Quad, Symbol, Ui, Gfx, BackgroundMode, DefaultWorld, Entity, Movable, Solid, CollisionShape, GameMode, OrbitMode
class Body {
construct new(id, parent, position, shape, colour) {
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, position)
_movable = Movable.new(_entity)
_solid = Solid.new(_entity, CollisionShape.new(shape), false, 1.0)
_shape = shape
_colour = colour
_complex.setup([_entity, _movable, _solid])
}
entity { _entity }
solid { _solid }
shape { _shape }
colour { _colour }
static bind() { __cls = ScriptClass.new("Body", [Entity.type, Movable.type, Solid.type]) }
}
class Agent is Body {
construct new(id, parent, position, shape, colour) {
super(id, parent, position, shape, colour)
_aiming = true
_angles = Vec2.new(0)
_force = Vec3.new(0)
_torque = Vec3.new(0)
}
update() {
var velocity = this.solid.impl.linear_velocity()
var force = Mud.rotate(this.entity.rotation, _force)
this.solid.impl.set_linear_velocity(Vec3.new(force.x, velocity.y - 1, force.z))
this.solid.impl.set_angular_velocity(_torque)
this.solid.impl.set_angular_factor(Vec3.new(0))
}
aiming { _aiming }
angles { _angles }
force { _force }
torque { _torque }
}
class Terrain {
construct new(id, parent, size) {
_quad = Quad.new(Vec3.new(size,0,size), Vec3.new(size,0,-size), Vec3.new(-size,0,-size), Vec3.new(-size,0,size))
_complex = Complex.new(id, __cls.type)
_entity = Entity.new(id, _complex, parent, Vec3.new(0, -10, 0))
_solid = Solid.new(_entity, CollisionShape.new(_quad), true, 0.0)
_complex.setup([_entity, _solid])
}
quad { _quad }
entity { _entity }
static bind() { __cls = ScriptClass.new("Terrain", [Entity.type, Solid.type]) }
}
var GWorld = null
var GScene = null
var GTerrain = null
var GBodies = []
var GAgent = null
foreign class MyGame {
static new(module) { __constructor.call(MyGame, module) }
static bind() { __constructor = VirtualConstructor.ref("GameModuleBind") }
init(app, game) { start(app, game) }
start(app, game) {
GWorld = DefaultWorld.new("World")
game.world = GWorld.world
GTerrain = Terrain.new(0, GWorld.world.origin, 100)
GAgent = Agent.new(0, GWorld.world.origin, Vec3.new(0), Cube.new(), Colour.White)
GBodies.add(GAgent)
var rand = Random.new()
for (i in 0...100) {
var position = Vec3.new(rand.float(-50, 50), rand.float(0, 20), rand.float(-50, 50))
var colour = Colour.hsl(rand.float(0, 1), 1, 0.5)
var shape = i % 2 == 0 ? Cube.new(Vec3.new(rand.float(1, 5))) : Sphere.new(rand.float(1, 5))
GBodies.add(Body.new(0, GWorld.world.origin, position, shape, colour))
}
GScene = app.add_scene()
}
pump(app, game, ui) {
var viewer = Ui.viewer(ui, GScene.scene)
Ui.hybrid_controller(viewer, OrbitMode.ThirdPerson, GAgent.entity, GAgent.aiming, GAgent.angles)
Ui.velocity_controller(viewer, GAgent.force, GAgent.torque, 20)
GAgent.update()
}
scene(app, scene) {}
paint(app, scene, graph) {
Gfx.radiance(graph, "radiance/tiber_1_1k.hdr", BackgroundMode.Radiance)
Gfx.sun_light(graph, 0, 0.37)
var terrain = Gfx.node(graph, GTerrain, GTerrain.entity.position, GTerrain.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "ground", Colour.new(0.3, 1))
Gfx.shape(terrain, GTerrain.quad, Symbol.new(Colour.White), 0, material)
for (body in GBodies) {
var node = Gfx.node(graph, body, body.entity.position, body.entity.rotation)
var material = Gfx.pbr_material(app.gfx, "body %(body.entity.id)", body.colour)
Gfx.shape(node, body.shape, Symbol.new(Colour.White), 0, material)
}
}
}
Body.bind()
Terrain.bind()
MyGame.bind()
var game = MyGame.new(module)