Game Builder: Intro to Scripting (API v2)

While it's perfectly possible to make an entire game in Game Builder without writing a single line of code, writing code will allow you to create much richer and more interactive experiences that wouldn't be possible just with built-in objects and cards.

Anyway, enough about the benefits of code :) If you are reading this, it's because you are already interested in writing some code and you know that coding is fun, so let's get right to it.

Fundamentals

Any game in Game Builder is made up of two fundamental things: terrain and actors. Terrain means the ground and any blocks, ramps, corners, etc, that you have placed with the Construct tool. Everything else is an actor. For example, in the scene below, can you tell what's terrain and what's an actor?

Answer: there are 5 actors in the scene:

The ground and the metal platform in the center are terrain.

Let's add some code!

Let's start simple. Create a new game and put just a slime on the ground. Not a very exciting game (yet!), I know, but let's start simple.

You can see that the slime moves around. That's a behavior that comes with it by default because wandering around aimlessly sounds like something a slime would do, but for our purposes let's remove it.

Select the Logic tool and click on the slime. Then look for its Enemy AI panel and remove the Wander Around cards.

You should now have a slime that's standing still.

Now comes the fun part. Use the Logic tool again on the slime, but this time click the Code tab at the top.

And we get... a Javascript editor!

What you are doing now is you're creating a new card that we will later use on the slime (but you could use it on anything else).

Cards are what give behaviors to actors. Each card corresponds to a code module like the one you're seeing right now. Yep, cards are just a bunch of Javascript. There is no magic, only code!

First, rename your new card by clicking on the card title field (it's the text field highlighted in red the image above). Name it something interesting, say, MY CARD.

Ok, now delete all the sample code you see in the editor and replace it by this:

  export function onTick() {
    log("Tick...");
  }
  

Now click the Save button above the coding area in order to apply your changes.

What happens now?

You have just created a card. You have not yet added that card to the slime. This is what we will do next.

Adding the card to the slime

Now go back to the Logic tab (make sure the slime is selected).

In the Custom panel, click the dotted rectangle to add a new card. Then drag your fancy new My Card card from the list into the slot:

Don't let your hopes up too much yet, this example isn't going to do anything, but it will show you an important feature.

When you drop the card, your slime is now printing "Tick..." to the console every time the onTick() function is called.

Where is this console, you ask? Just press the ~ key to view it. On a US keyboard, that's the key at the top-left of the keyboard below the ESC key, normally just to the left of the 1 key. You should now see a little console appear at the top of the screen, filled with "Tick..." messages. Close it by pressing ~ again.

You now know how to print logs. It may not sound exciting, but it will be useful later when you're debugging something and need to print stuff out!

Ok, now let's change that code to do something more interesting than printing a log. Ready? Replace it with this:

  export function onTick() {
    spin(2);
  }
  

Now click the Run button above the coding area in order to apply your changes to your slime.

What do you get? Yes, a spinning slime:

How does this happen? We are calling the spin API function, which spins an actor with the given rotational speed. In this case we set it to 2 radians/second. Prefer to use degrees/second? No worries, just use the handy degToRad function.

  export function onTick() {
    // Spin at 45 degrees/second.
    spin(degToRad(45));
  }
  

Ok, enough spinning for now. Let's talk about moving around. This slime is going places.

Movement

Now try replacing the code by this:

  export function onTick() {
    moveForward(1);
  }
  

What do you get? If your answer was runaway slime, you are right! The slime just moves forward infinitely:

The moveForward API call moves the actor forward with the given speed. In this case we required a speed of 1 meter/second.

So what exactly does forward mean? Well, it's the direction the slime is currently looking.

To be more precise, we need to do a quick intro to coordinate systems (also called spaces).

Brief intro to spaces

Briefly stated, a space or a coordinate system is a way to interpret a set of (x, y, z), coordinates. By convention, the X value means "to the right", Y means "up" and Z means "forward". With relation to what? Well, that's exactly what we have to always clearly state! If I tell you to "go two steps to the right", your immediate question will be "my right or your right?" (ok, maybe it would be the second question, just after "why are you giving me orders?").

A space is just a formalization of this. A space is just a notion of which way is right/up/forward and with relation to what:

The names we normally give to these arrows are:

Look at the slime in this scene. The three arrows on it represent the slime's actor space. That's what the slime thinks of as "forward" (blue arrow), "right" (red arrow) and "up" (green arrow). Same thing with the lion: that's the lion's actor space. That's what it thinks of as forward/right/up. It's different than the slime's notion. It's a different space.

The third space or coordinate system in the picture (the one in the center) is what we call world space. It's a global coordinate system that everyone agrees on. When something is given in world coordinates that's the space that it's represented on. So if I say "go two steps in the world forward direction", everyone in the scene would agree about in which direction to go. World space is the same for everyone.

When we are talking about world space, instead of saying "forward", "backward", "right" and "left", we tend to say north, south east and west instead, because that makes it more clear that we are talking about world space. So if I say "everyone, move two steps north", everyone would go in the same direction regardless of where they are facing.

Note: if you have used other game engines and graphics programs before, you may have noticed that some of them like to think of Z as pointing "backward" instead of "forward". We use forward because we think it's more intuitive this way.

Moving in actor space

We already saw moveForward() in the last section. Now we can be more precise about it: moveForward() moves forward in actor space, so the actor will go in the direction that it's looking at.

Just like we have moveForward(), we also have moveBackward(), moveRight(), moveLeft(), moveUp() and moveDown(). Those are all in actor space, so relative to where the actor is looking.

Let's combine spin() with moveForward now for an interesting effect:

  // My script

  export function onTick() {
    spin(1);
    moveForward(1);
  }
  

What do you get now? Yes, a slime that runs around in circles:

Why does this happen? Think about what happens on every tick: the slime turns a little bit, then takes a tiny step forward. The fact that it's turning means that "forward" is different every time, so the net effect is that it runs around in a circle. Neat, right?

Moving in world space

All of those move in actor space. Now let's see how we'd move in world space. Suppose for example that you wanted to move the slime north, no matter where it's facing. In this case you can use the moveGlobal function, which moves an actor with a world-space velocity:

  export function onTick() {
    // Move north.
    moveGlobal(vec3(0,0,1));
  }
  

Note that the slime is moving towards the world's north direction regardless of where it's looking. That's why it moves sideways.

Interactions

What's a fundamental thing that is missing in the examples we've done so far? Hint: it's pretty fundamental in games.

Answer: player interaction! So far, the player has done nothing but watch the slimes move, turn, etc. Let's change that now. For example, let's make the slime change color when it collides with something.

For that, we need to introduce a fundamental concept: messages.

A message is just a packet of information that gets sent from the engine to an actor, or from an actor to another actor. The message tells the actor that something happened, and gives the actor a chance to react to it. The code that reacts to a message is called the message handler function.

You have been using messages already! Guess what: Tick is a message, so when you write your onTick function, you are in fact handling the Tick message. The tick message gets delivered to every actor on every tick.

Another message that the engine delivers to actors automatically is the Collision message, which indicates that another actor has collided with it. Let's try to write a handler for that message and see what happens.

Go back to your slime and edit My Card. Replace the code with this:

  export function onCollision() {
    // Make it blue.
    setTint(0, 0, 1);
  }
  

We set things up so that when the slime hits another actor, it will set its color to blue.

Try it out. When you bump against the slime, it turns blue.

Great, right?

Gate: first try

Let's use our new skills to make a gate that opens when you bump into it. Place a gate between some terrain blocks like this:

Now, use the Logic tool on the gate and remove its built-in IF-THEN panel, because it will interfere with our example. We just want a plain gate!

Now let's try to naively implement a script that opens the gate when you bump into it. Make a new card (or reuse the card you already had on the slime) and put it on the gate. Write this code on the card:

  export function onCollision() {
    // Open the gate by moving the the left a bit
    moveLeft(1);
  }
  

It says "if I collide with something, then move left". Sounds right, doesn't it? Well... almost. Look at what happens:

As you can see, the gate moves left with a speed of 1 meter/second only while the player is colliding with it. If you stop colliding with the gate, it stops.

This happens because the onCollision message gets delivered every frame only while the gate is colliding. After it stops colliding, it is no longer delivered, so your moveLeft call doesn't execute, hence the gate stops moving.

How could we implement a gate that opens smoothly all the way, without needing the player to continually bump against it?

Well, for that, the gate needs to remember that it's currently in the "opening" state. For that, we need memory!

Memory

Memory means a set of values that an actor or a card remembers. You can store variables in memory so you can access them later. It also gets included in the save file, so whatever values you store are persisted if you save the game and load it again.

There are two levels of memory:

Let's use card memory to create a smoother gate. When the player bumps into it, we will store a variable in card memory called triggered (it could be called anything else, it's just a variable!), and it will represent the fact that the gate has been triggered by the user.

Then, instead of opening the gate only while it's being collided against, we will use the onTick function to slowly open it if the triggered variable is set to true.

To access the card's memory, simply use the card object:

  export function onTick() {
    if (card.triggered) {
      moveLeft(2);
    }
  }

  export function onCollision() {
    card.triggered = true;
  }
  

Much better, right? What is happening in this code is:

  1. onTick gets called every frame. But we don't do anything because card.triggered hasn't been set yet. So the gate stays closed.
  2. The player bumps into the gate.
  3. onCollision now gets called on the gate.
  4. We set card.triggered to true.
  5. onTick continues to get called every frame, but now that card.triggered is set, we call moveLeft(2) That function moves the actor to its left with a speed of 2 meters/second.

Resetting the game

You may have noticed something weird in the previous example. If you reset the game (press Ctrl+R), the gate will immediately open again as if you had triggered it. Why? This happens because memory is not reset automatically.

To ensure that things return to their initial state, you have to implement onInit to reset any memory that you use to a known initial state.

So here is our corrected code:

  // onInit is called every time the game is reset:
  export function onInit() {
    card.triggered = false;
  }
 
  export function onTick() {
    if (card.triggered) {
      moveLeft(2);
    }
  }

  export function onCollision() {
    card.triggered = true;
  }
  

This ensures that when the game is reset, the variable triggered starts with the value of false.

There is still something weird... the gate moves infinitely to the left! Why? There is no stopping condition. Once triggered is set to true, it's never reset.

What we want is to reset it after, say, 1 second. To do this, we can send ourselves a "delayed message" to remind ourselves to reset it:

  export function onInit() {
    card.triggered = false;
  }

  export function onTick() {
    if (card.triggered) {
      moveLeft(2);
    }
  }

  export function onCollision() {
    card.triggered = true;
    // Remind ourselves to stop in 1 second.
    sendToSelfDelayed(1, "StopIt");
  }

  export function onStopIt() {
    card.triggered = false;
  }
  

Now the gate stops after 1 second of motion.

Properties

When you're designing cards, it's a good strategy to make them as reusable as possible. Don't over-optimize a card for that one particular actor. Say, for instance, you need to make a slime that turns blue for 5 seconds when the player hits it. If the "blue" and "5 seconds" are hard-coded into the card's code, what happens when you need a slime that turns red for 4 seconds instead?

That's what properties are for.

This card, for example, could declare two properties: the color to use, and the duration, and then implement its logic in terms of these properties:

  export const PROPS = [
    propColor("Color"),
    propNumber("Duration", 3)
  ];

  export function onInit() {
    // Start green.
    setTint(0, 1, 0);
  }

  export function onCollision() {
    // Bump! Change to the requested color.
    setTintColor(props.Color);
    // Remind ourselves to change back to green.
    sendToSelfDelayed(props.Duration, "RestoreColor");
  }

  export function onRestoreColor() {
    setTint(0, 1, 0);
  }
  

Users of the card (you!) will now be able to assign its properties:

The more flexible you make your cards, the more you will be able to reuse them in different actors and contexts!

Sending messages to other actors

Now let's do a more complicated example: a button that launches a rocketship.

There are 2 actors in this example: the rocketship and the button. Hitting the button will cause it to send a "Launch" message to the rocketship. The rocketship, upon receiving that message, will start to go up slowly.

So let's build the scene first:

Nothing fancy. Ok, now let's start with the code for the ship:

  // Ship script
  export function onTick() {
    // If rocket was launched, move up at 2 units/second.
    if (card.launched) {
      moveUp(2);
    }
  }

  // This gets called when we receive the "Launch" message
  // (sent by the button).
  export function onLaunch() {
    card.launched = true;
  }

  export function onInit() {
    card.launched = false;
  }
  

Notice how the rocket is programmed to just sit there doing nothing until the card.launched variable is set to true, at which point on every tick it will call moveY to move up at 2 units/second.

When the rocket receives the Launch message (which the button will send, as we will show next), then it sets the card.launched variable to true, causing the motion to begin.

So here is what we write for the button:

  // Declare the "rocket" property of this script, of type Actor:
  export const PROPS = [
    propActor("rocket")
  ];

  // This gets called when the player collides with the button.
  export function onCollision() {
    // Tell rocket to launch.
    send(props.rocket, "Launch");
  }
  

There is a funny bit we haven't seen before. This thing:

  export const PROPS = [
    propActor("rocket")
  ];
  

This indicates that this script has an actor property that can be set. This is similar to other properties we saw before (propNumber and propColor), but the value of this property is a reference to another actor.

To assign it, go to the card on the button, and click on the field. You can then select the rocket to set the value of that field, and from that point on you can refer to the rocket from your card as props.rocket.

This is used in onCollision, in which we use the send function to send a message to the rocket. The message is simply Launch, which causes the rocket's onLaunch function to be called, which itself causes the rocket to start going up.

Here is the result:

What's Next

Now that you know the fundamental of how scripting works, you can take a look at the API reference for a full list of functions supported by the API. Happy coding!