I said I would stop! But there’s no stopping me.
When I invited Amandine Bru over to hang out around Ludum Dare and at least see what the theme was, I promised her nothing was going to happen. It was going to be a relaxed evening with no stress of any kind. Now we’re one week later and BAM, I have yet another newborn game to take care of. These things grow so fast…
If you don’t know, game jams are these crazy events where you have a given amount of time to create a completely new video game from scratch (well, almost). Ludum Dare in particular has two variants, the compo (48h, strict rules, solo work only, zero re-use allowed) and the funnier version which I always go for, the jam (72h, lax rules, group work is fine, re-use whatever!)
I’m going to quickly go over three points that I feel have been important to me during that last gamedev jam:
- Not twine
- Yes 3D
- Woopsie deadline
I don’t think there’s anything wrong with Twine. Really, I don’t. It’s a great tool and it gave a voice to many. In fact, if you follow me on Twitter you’ve probably heard me praise it, amidst many rants. But I tried using it, repeatedly (see Neverjam for example), and it doesn’t appeal to me all that much.
See, Twine lacks constraints. For instance, nobody forces you to write anything more interesting than a novel randomly divided in passages. And then well, does it even have to be a game at all?
So I didn’t really want to make a Twine game for Ludum Dare. But I was in the mood for something narrative anyway. What did we do? The only thing I know how to do, obviously, I made a fresh piece of architecture: a space-time interactive story engine.
Here’s how it works: there are two text files that determine the entire content of the game, in YAML format. One, map.yml, contains a list of places like this:
home: name: Kate and Stanley's Home pos: [0, 0] type: house paths: - left crank-path-1 - right home-to-mayor-1 - down huntress-path-1 crank-path-1: # you get the idea...
Places names appear in the upper-left corner when you navigate, the position is
given in spherical coordinates, the type determines the 3D model shown at
this position, and
paths is a list of places you can get to by hitting the
arrow keys or clicking on the arrow indicators in the game.
Movement is the first phase of each turn - you have a certain amount of movement points which you spend by moving from place to place. At any time, you can choose to spend the night.
Let’s say we spend the night at the place with the identifier
home, that we
just defined above: in this case, the game looks up a page named
is defined in the
story.yml file, which looks like this:
at-home: lines: - Tired, Kate decides to go home for the night. - The fireplace is cold and the place is cold all around. - Home sweet home.
Pages are like Twine passages, kind of. They can have a list of lines, that’ll be displayed (the crux of the story) and then many many ways to conditionally advance the story. By default, the node above is equivalent to
at-home: lines: ["Tired...", "The fire...", "Home..."] if: true then: night else: night
Now, we can write an actual condition in that
if block. For example, checking
for the presence of an item.
at-home: lines: # etc. if: has crystal then: win-message else: no-win-today win-message: # lines & stuff here no-win-today: # same
Using if, then, and else, you can make pages keep jumping from one to another until
eventually it reaches the special page
night, which fades to black and back, advancing
to the next day. In which case you go back to the movement phase, having spent one day.
has is only one possibility — to check the inventory — another kind of
is, to compare a stat with a number or a number range. For example,
if something is supposed to happen only between days 3 and 6, one could write:
at-somewhere: if: is day 3..6 then: something-special # implicit else: night
At this point we already have most of the tools we need. We just need a way to pick up
pickup: item, or
pickup: [item1, item2, item3], a way to drop items
drop: [item1, item2, item3] and a way to increase/decrease stats:
inc: strength, and
Note that our example above tests the
day stat — it’s a built-in stat, initialized by
the game to 0 and incremented automatically on each night spent. But for example, a page
inc: day explicitly to simulate “passing out”. Similarly, max movements points
are a stat as well. And you can have any number of named custom stats — anything undefined
is equal to 0, and can be incremented/decremented at will.
By chaining if pages, we can have complex condition chains that can already
lead to quite an interesting story. But we were on a deadline, and we often had 3 or 4
different conditions to test - enter the
at-somewhere-else: switch: "is day ..7": week-one-event "is day ..14": week-two-event "has dagger": maybe-win "is day 31..": dagger-hint then: nothing-happens
Each condition of the switch is tested and, if true, the specified page is flipped to.
If all else fails, the
then clause is used.
Now, a switch is really convenient, but it has a big issue: it tends to repeat pages too much, since if the condition is true it will always flip to the given page, and the conditions will always be tested in the same order.
The solution to that was… a
pool! A list of options, one of which would be picked
at random, removed from the pool, and flipped to. For instance:
at-mayor: pool: - then: mayor-there - then: mayor-absent then: mayor-too-much
And as with the switch, once the pool is exhausted, it goes back to the
clause. To make things more interesting, pool options can appear conditionally
at-mayor: pool: - then: mayor-there - then: mayor-absent - then: mayor-pissed only-if: has threatened-mayor then: mayor-too-much
Oh and obviously we need a way for the player to make choices! That’s the
clause, and it’s written almost exactly like a pool, except it needs a name for
the buttons. In fact, that’s how the main menu was implemented in the jam version!
menu: choices: - name: New game then: at-home1 - name: Skip introduction then: at-home4 - name: Switch Fullscreen then: fullscreen only-if: has desktop - name: Quit to desktop then: exit only-if: has desktop
desktop is a custom item you automatically pick up at the start of the
game if.. you’re playing a desktop build of the game. Now you’re starting to
There are more built-in pages — for example, when nothing interesting is
happening somewhere we didn’t want players to waste a night, so instead of
night we’d flip to
locked and it would switch back to the
movement phase (restoring 1 movement point if the player was out of moves, to
avoid being stuck).
And there are more clauses too! Like
teleport, used only once in the game,
but which wasn’t that hard to add in. At no point during development was I
entirely sure the structure of the game engine was right, but everything turned
out for the best. Maybe old coding chimps end up having some kind of sixth
sense for what to avoid and what compromises to make?
And that’s about it. I usually like to iterate on things like that, but for once I sat down and wrote a spec for most of the story engine, then sat down harder and implemented it all in one go. Meanwhile, Nim had collapsed on the couch from having written off too many potatoes, and I left her a note:
Sure enough, when I woke up, she was halfway through implementing the story. Who says waterfall development never works?
It had been a while since I last did anything tri-dimensional. So long in fact that most of my entourage forgot I even did such things in the first place.
3D is.. hard, inherently. Instead of working with a familiar 2D plane in which there’s only one rotation axis, you’re left stranded in a world where if you get only one camera value wrong, it’s nothing but darkness.
I used Blender to model the world and the various props, but already at the start it was a struggle. How do you UV-map a sphere and then draw paths of consistent widths on it? If you’ve done a bit of cartography, you’ll know that any projection has deformations. As a result, the world map texture (painted in Gimp), ended up looking like this:
See those cones? Those are the north and south poles of the world. While the texture is badly deformed (and that curve was the result of a long period of trial and error) it looks completely normal when mapped on the sphere. Thank God, Blender is able to quick-reload textures (Alt-R) and the Gimp can export PNGs with almost no compression (exporting a 2048x2048 PNG image is actually kinda slow).
But that wasn’t all. Placing houses and stops was hard as well. See, when dealing with rotations in 3D you have a few options:
- Euler angles
- 3x3 matrices
- Curling up in a ball of sadness and crying yourself back to sleep
Euler angles have a well-known flaw, Gimbal lock - I won’t enter into the details here but basically, it has something to do with the fact that 3 values to determine rotation is redundant for some angles - and when you reach a certain point changing a value no longer rotates the object (in our case, the world).
Matrices are no fun to write by hand (no, seriously, 9 numbers, ugh) and are actually more redundant than Euler angles (but used internally by 3D engines because they’re very useful to apply transformation to a vector).
And finally, Quaternions I probably could’ve used in some way, especially for interpolating between points on a path, but I really didn’t have the courage to learn that much in the middle of an already-complex-looking jam.
The constraint I had was the following: for each point on the map, we should have at most 4 different directions, left right up down, no funky angles or whatever, and the camera angle should always be the same, no matter what path you took to reach that point.
Upholding these assumptions was surprisingly difficult, and I did spend perhaps too many hours tweaking the camera movement (especially that nice little effect when you spend the night somewhere where it rotates and shows an angled shot of the place), but I don’t regret it: I may not have ended up with the perfect solution, but it made the game playable. Which, during a game jam where you’re not making an FPS or a platformer, is very much an achievement.
Accessibility was an important point to me - navigation was made possible using the arrow keys or the mouse. Advancing dialogue was made possible by clicking or pressing ‘Enter’ or ‘Space’. Dialogues could be skipped, etc. I spent a lot of time making sure the only confusion stemmed from the story and not the game engine.
Alas, my disappointment was great when I found several comments talking about “very disorienting controls”: I understood only later that it was meant as a positive thing, but given the gigantic amount of time I spent on all that, I became angry and flustered.
Worse still, when I read comments about “the 3D part not adding much to the game”, and how “a pure text game would have been better”. Such is the life of a Ludum Dare participant: spending 72 hours in overdrive and coming back to heavy-handed comments about how you dun goofed, son.
Well, I have no regrets. I wanted to play around with 3D stuff, and I did. Going back to Java and Eclipse-land was almost pleasant, and libgdx had everything I needed, plus it allowed us to have a Web version and a Desktop version rather easily.
Even after babbling on how I can’t do art (boo hoo) I really wanted to contribute to the story. In fact, the initial idea for the story is courtesy of my crazy, sleep-deprived brain. But as it turns out, I ran out of time.
Running out of time is really routine in a game jam. No matter how much you cut the scope down, no matter how much you plan ahead, no matter how hard you work and how much coffee you drink and how many freaking games you’ve already made, there’s always this “oh shit” point about 3 hours before the deadline.
I did spend two hours rewriting most of the introductory text, but being the perfectionist I am, I could have spent hundreds more just going over the story, adding options (as I’m sure Nim would have too), changing some lines slightly…
I usually compose the soundrack for my games (either alone or in collaboration with bigsylvain) but given the circumstances, I decided to spend my time implementing a music mixer (to seamlessly switch between ‘ambient’ and ‘melodic’ music contextually) and pick out a fitting music from ccMixter rather than trying to record it myself.
There was something quite unique about this game. It was my first collaboration with Nim, and her first Ludum Dare. It had been a while since I left my baby programming language ooc at peace instead of trying to do unspeakable things to it.
But mostly what was unique is that the story system was, at some point, “enough”. With every other game I have made, the team and I kept having ideas that were great in theory but a real pain to implement because of all the engine changes and testing that would be required.
With Super Duper Spatio-Temporal Storytelling Engine 6000 though? We had plenty of ideas just before the deadline and it would have taken us only 10 minutes to add them in. And it would’ve been fun and just like we had imagined. Text is a powerful thing, my friends.
Thanks for reading this far! You can find Nim’s own post-mortem on her blog.