Blog
Action!
This is what will undoubtedly be the last making-of of the year before the release of the playable demo of the Superfluous video game scheduled for the end of the year.
I already discussed, in order:
- algorithms (location & moving)
- graphics (character with characteristics)
- music (musical themes)
We will tackle the last big piece that is necessary for the game: writing and storing “levels” (which we should rather call “rooms” since the game is separated into different rooms connected by doors and corridors).
Separation of engine and content
A small technical point first: the levels are not “hard-coded”, that is to say that in the source code of the game (C++ language), you will never find a room description, dialogues, puzzles, path to pictures, etc. The levels are stored in text files which are read and interpreted by the program at runtime. In practice, this means that I don't need to recompile the game and provide different software (the EXE file on Windows, APK on Android, etc.) when I add levels.
A classic technical choice is to code the levels in an extension scripting language like the Lua language. It was my first attempt, before I realized it was an unnecessarily complex solution for my needs: all the possible actions will be fixed by my engine, I only need a language of description and not of a script language. Basically, I turned to a technical choice with less flexibility but easier to set up.
The levels consist of a list of elements, with a certain number of features. An “object” element describes for example the image to be loaded, its XY coordinates on the screen, as well as a Z coordinate for the depth, its different states (for example, a door can have a closed state and an open state, which the person playing will be able to modify), etc. Then, it is possible to interact with most of these objects and trigger a series of actions according to the chosen verb (open, move, take, etc.).
We also have an element for each sound, each music, each animation, each character, for the dialogues, for the level changes, etc. The list goes on, but once the general principle is implemented, it's easy to expand to other items.
Which format?
Several data exchange format options quickly emerged:
I quickly eliminated the “homemade format” option: although it is tempting (because by definition, I'd do what I want there), it involves the creation of a parser (the algorithm that reads the different elements of the file without crashing), which is far from trivial if you want to do something sufficiently robust. Needless to say that would mean adding a load of code for something not really fun to do.
I started by using XML, but I quickly found it too wordy, cluttered with special characters, and probably much more complicated than necessary for my use: I intend to write the levels “by hand” and the XML has a rather painful syntax for that.
So I looked at JSON and YAML and quickly adopted YAML because it was still the most pleasant to type by hand (less cluttered) and to read: – to caricature, I had the impression that JSON was YAML but polluted with braces everywhere – almost everything is represented by strings in JSON, while YAML accepts different types (and leaves the choice to surround its strings with quotes or not, which can be useful to distinguish different types of strings visually). It does not change much from a parser point of view, but it allows to have a much more readable syntax highlighting in YAML.
Visual comparison of a piece of my level in the three languages (XML, JSON, and YAML which was my choice)
In terms of code, I rely on the libyaml (C library) to parse the files, to which I added my small custom layer to make the link with my own code. There was indeed a higher level C ++ library, but it seemed to me less universally installed and supported than _ libyaml _ (which has been around for quite a while and pre-installed almost everywhere). Knowing that I intended, from the start, to support a certain number of fairly heterogeneous operating systems, I preferred to use the most common library.
Example
Here is a concrete example (which is also part of the real first level of the game), where there is a bottle that you can take and use to water a plant. I know, it's a bit of a spoiler, but pretty harmless since watering the plant has no use in solving the puzzles (and taking the bottle was spoiled from the trailer (French only, sorry about that)).
We therefore have two objects, a plant object, whose name displayed on
the screen is “foliage plant”, with its coordinates. The “view”
property represents the place where the character must position
himself if he must interact with the object. “box_collision
” is a
technical detail that allows you to choose whether the cursor must be
right on it to click it or simply on a bounding box (this makes the
selection of small objects less tedious, as is the case for the
bottle). Then, we have the different states with the associated “skin”
(the PNG image to load). And finally, the actions that can be
performed on it. If an action does not exist (for example, if I try to
make “talk to” on the bottle), then a default action will be triggered
(typically, a comment like “no” or “I cannot do that”).
So, if I try to look at the plant, Sophie will turn to the plant
(“look []
”) and say “It's a yucca …”. A more complicated case will
be to use the bottle on the plant: Sophie will first go to the right
place (the “view
” of the plant) with the “goto []
” function. Then,
there are somewhat special instructions: “system [wait]
” specifies
that you have to wait for all the previous instructions to be carried
out before moving on to the next one (basically, you wait until Sophie
has reached the plant to do the rest). Then, “system [lock]
” and
“system [unlock]
” allow you to lock and unlock the interface:
between these two instructions, the player no longer has control over
the game to be sure that the action is not interrupted (yes, that
means that as long as Sophie has not reached the plant, you can click
elsewhere to cancel the action and do something else). Finally, the
“play []
” instruction lets you choose an animation, in this case the
character's “action” animation (which just consists of holding out
your hand) for 1 second.
There are many other possible instructions, for playing sounds,
changing the state of an object (the “set [state]
” above), changing
coordinates, and so on. Then, the corresponding mechanisms must be
implemented on the engine side and called at the right time. By
combining all of this, a dynamic environment for the character to
interact with is easily created.
So of course, when the levels have lots of objects and possible interactions, the YAML files quickly become large: for example, the 2 levels that will be provided with the playable demo at the end of the year respectively include 654 and 1172 lines.
Ultimately, it is not impossible that I will work on a level editor to avoid having to write them entirely by hand. That would make things easier in particular for the placement of objects: for the moment, I place them in Inkscape and I copy the coordinates by hand.
Here we go …
This was the last making-of of the year, and probably the last of its kind. After the release of the playable demo, I may do other articles, but more technical: for example, how to manage “assets” (game data, images, YAML files, etc.) and their distribution , how to properly manage compilation on heterogeneous platforms, how to port on Android, on Emscripten, cross-compile from Gnunux to Windows, etc. (spoiler: yes, I did all of that). In short, all these additional but important, complicated things that are not explained to you in programming lessons. See you soon for the playable demo!