I’m going to take a little detour from my series on how to make a game in Frogatto to discuss in detail an important topic to anybody who wants to develop using Frogatto: how event handling works.
Frogatto’s game objects receive ‘events’ frequently. They receive a create event in the first frame they run, a collide_side event when the object runs into a wall, a collide_feet event when landing on the ground, and a jumped_on event when another object lands on top of the object. They even receive a process event that is triggered every frame. There are around 30 built-in events, and more types of events can be added.
Events are the big chance you, as a game developer, have to customize an object’s behavior. When an event occurs on an object, you get to specify any actions the object should make in response.
Frogatto’s event handlers are written in Frogatto Formula Language (FFL). FFL is very different from many other computing languages, such as Python or Lua or Javascript in that FFL is a pure functional language. What this means is that FFL’s entire purpose is to run some calculations and return the results of these calculations. An FFL formula has no ability to directly change the game state.
Huh? How does this work then? What use is writing some code if it can’t actually change anything? Here is how it works: when an event occurs, the game engine evaluates the FFL formula — during this evaluation the game state is completely frozen — and the formula returns one or more command objects. After the FFL has finished running and returned its results, the game engine steps through these commands that FFL returned and executes them.
This might seem like a distinction without a difference, but it’s very important. Note the difference between when the game engine evaluates FFL (without allowing modification of the game) and then, once the FFL is done, executes the commands that FFL gave it. This has the huge benefit of making it so an FFL formula runs in an environment where nothing changes, where everything can be viewed as a mathematical model of the current game state — where one doesn’t have to keep track of the possibility of variables changing during the evaluation of a formula.
Anyhow, enough theoretical talk! Let’s get down to a concrete example. We’ll start with a simple one. Suppose we have an object which steadily moves upwards by 2 pixels every frame, here is how we could make it work:
on_process: "set(y, y-2)"
Every frame, the process event gets fired. The FFL code, set(y, y-2)
is evaluated. What is this ‘set’ thing? It’s a built-in function that returns a set command. If the object’s y position was 100 before the event was triggered, the FFL will evaluate to a set command with the information encoded to set the value of ‘y’ to 98.
After the FFL returns this command and has finished, the game engine will execute the command. Executing the command will result in setting the object’s y value to 98, shifting it up 2 pixels. Note that the process of evaluating the formula is part of FFL. Executing the command takes place within the game engine, and there is no FFL involved.
The set function is the most often used function out of an extensive suite of functions FFL has available for generating commands. There are commands for playing sound and music, for spawning new objects, for different graphical effects, for debugging, and so forth.
Note that it is important to understand the difference between an FFL function that returns a value that is useful in FFL, and an FFL function that returns a command. For instance, FFL includes the sin function, which returns a value that is useful in intermediate calculations but doesn’t make much sense to return to the game engine. The spawn function gives us a command which, when executed by the game engine, spawns a new object and is exactly the kind of thing we might return to the game engine.
FFL is a full-fledged programming language, and we can include complex logic in it. Let’s say we wanted to make the object reverse direction every two seconds (i.e. every 100 frames). We could write the event like this:
on_process: "if((cycle/100)%2 == 0, set(y, y-2), set(y, y+2))"
Note how, in FFL, if is just a function. It looks at its first argument, and if its first argument is true, it’ll evaluate to its second argument (the ‘then’ clause) otherwise it’ll evaluate to its third argument (the ‘else’ clause).
I want to dive much deeper into how FFL works, and what can be done with it, but that will wait until a later post. Right now, I want to talk more about the event handling mechanism Frogatto uses.
Like I said, an event handler is evaluated and then after the FFL has returned its results, those results are executed by the game engine. This has some interesting implications. Firstly, FFL can return multiple commands in a list, like this:
on_process: "[set(y, y-2), if(y < 0, set(x, x+2))]"
This will return commands to decrement y by 2, and if y is less than 0, increment x by 2. Note though that since the entire formula is evaluated before any execution occurs, the value for y throughout the formula is the value at the start of the event. This means that if y is 1 before the event occurs, the if condition will fail, because the set(y, y-2) will not be executed until after the event returns.
Consider this code:
on_process: "[set(y, 5), debug(y)]"
debug evaluates to a command that outputs its arguments, in the game window and the console. Very useful for debugging. Note though that it will output the value of y as it was before the event was fired. If you wanted it to output 5, you might do something like this:
on_process: "[set(y, new_value), debug(new_value)] where new_value = 5"
Now consider this code,
on_process: "[set(y, y-100), set(y, y-5), set(y, y-2)]"
Pretty silly code, right? But what does it do? Say y was 100 before the event was called. We will return three set commands, the first will be encoded to set y to 0, the second to set y to 95, and the third to set y to 98. The game engine executes commands in a list in order, so the last command will set y to 98, and the first two commands will in effect do nothing.
One special function is the fire_event function. It returns a command that explicitly fires an event. For instance,
on_process: "[set(x, x-2), set(y, y-2), fire_event(self, 'output_state')]",
on_output_state: "debug(x, y)"
When the process event occurs, commands will be returned to decrement x and y, and then a command to fire an 'output_state' event. These commands will be executed in sequence. x and y will be set, and then output_state will be fired, causing on_output_state to be evaluated. Finally, the debug command returned by on_output_state will be executed, causing the updated x, y to be output.
Note that fire_event is very useful in some situations, but should be used judiciously. Its main purpose is NOT to allow chaining of events like this, but to allow us to fire_events on other objects (e.g. if we hit another object we might want to fire an event on them to know they've been hit).
Hopefully this gives us a nice overview of how the Frogatto event system works. Hopefully, soon, I'll write more about all the features FFL has to offer. In the meantime, you might like to check out our FFL FAQ.
…always useful infos Sirp!!
Thanks a lot …
[…] Frogatto Formula Language (FFL) is the language used to customize Frogatto and control its behavior. I’ve talked about how Frogatto’s event handling system works before, right here. […]
[…] developed for one of our other projects, Frogatto. We have some articles on how FFL works, here and here so I won’t get into the nitty gritty here. However for most things in Argentum Age someone […]
[…] developed for one of our other projects, Frogatto. We have some articles on how FFL works, here and here so I won’t get into the nitty gritty here. However for most things in Argentum Age someone […]