Why Adventure Games Rock - Part 1

4 Comments

Why Adventure Games Rock - Part 1

It's now been six months since I've released my first adventure game. Now that the development effort is over, I've spent some time considering, "What next?", and, even more importantly, "What would make it better?". Ron Gilbert, creator of Monkey Island, famously wrote the article, "Why Adventure Games Suck." It's an excellent read. Believe it or not, this article heavily affected the design of Tick's Tales, and I'm happy for it. However, I spent so much time emphasizing what not to do -- dead ends, death, mind-bending logic. I wish I spent as much time thinking about what to do. And that is the subject of this research.

So I'm writing a series called Why Adventure Games Rock to catalog my research. Unlike Ron Gilbert, I don't have a suite of my own games to reflect upon. Instead, I'll reflect on other developer's games. I'll try to look at 1-2 each week. My goal is to examine adventure games with a critical eye, both classic and modern, and figure out why they work. Adventure games' flaws are now well known. But I'd like to look at what makes a player feel great accomplishment, feel invested in the characters, and a narrative style that complements the adventure genre.

King's Quest V

The year is 1991. A six-year-old me is watching my grandfather play the desert sequence in King's Quest V, pad of paper in hand. This particular desert is made up of screen after screen of monotonous desert, speckled here and there with oasises. Upon reaching one, Graham and the player alike are relieved as the narrator says, "Ahh, life giving water, nectar of the gods. Graham can now feel strength and renewal flowing through him." I was quite enchanted by this artful, albeit deeply flawed, adventure game. Let's see what design elements really worked in this game. We're going to set aside the plethora of problems this game has, especially the infuriating dead ends.

Exploration is the means of progress and the reward for progress

King's Quest V tailors the "explorable area" very well. When the player makes progress in this game, they know it. They're taken to a new land. Progress is made by exploring and solving puzzles, which, in turn, opens new areas to explore.  

Most adventure games follow a similar pattern. However, King's Quest V mostly closes off areas as the player advances. This might seem cheap at first, but King's Quest V uses it to its advantage.

First, it creates a clear chapter boundary, giving the player a definitive sense of accomplishment. The linear flow makes progress undeniable, and it feels measurable. Other adventure games will open new areas in different parts of the game, resulting in an ever-expanding explorable world. In those games, it's hard to gauge "how far have I come?"

Chapter 1 - sierrachest.com

Chapter 1 - sierrachest.com

Chapter 2 - sierrachest.com

Chapter 2 - sierrachest.com

Perhaps even more importantly, closing off areas after a opening new ones gives a sense of relief, because the player knows they won't have to backtrack. Think if you had to walk all the way back to Crispin's house after arriving on Mordack's island. Not fun.

And finally, closing off old sections gives the designer the ability to control pacing. King's Quest V doesn't take as much advantage of it as, say, Kyrandia 2: Hand of Fate, in which each area has just the right amount of puzzles to make the world feel populated. The player doesn't wander through a world that is 95% done. We want a graph that looks like right side: 

The ever-expanding world

The ever-expanding world

Closing old areas when new ones are opening

Closing old areas when new ones are opening

It's worth noting that this design feature is not unique to adventure games. For example, in the Diablo series, progress is also marked by a change in scenery. But it's especially rewarding when each backdrop is a piece of art, as is the case in King's Quest V.

Player's knowledge > Character's knowledge

This is an area in which I've come to disagree with Ron Gilbert. He argues that you shouldn't have to have the player die in order to get information necessary to win. In one sense, I totally get it. If the player has to watch Graham die 20 times in a desert in order to locate an oasis for the 21st Graham, then it's a really unrealistic puzzle that breaks immersion. However, I'm becoming convinced that this is an unhelpful term, and, in fact, a goal not worth pursuing.

This issue has everything to do with the user experience, and nothing to do with breaking immersion. Here's my rationale:

  1. In nearly every other game genre, death is a thing. Strategy, RPG, platformers, heck, even card games! We don't consider it bad design there.
  2. Death in early adventure games resulted in costly consequences, and burdensome preparations. Hopefully you saved recentely. And hopefully you saved before it was too late. It's best to create a unique save file every 10 seconds, just in case. Not to mention it'll take several button presses [boring] to restore.
  3. Having consequences to making the wrong decision results in a sense of tension. 
  4. It's just as easy to break immersion by removing death altogether. Imagine if Mordack just stood there waiting for the player to try inventory item after inventory item to defeat him. That would take them out of the game pretty quick. 
Take your time. No rush.

Take your time. No rush.

In total, player knowledge and character knowledge are not equal. Heavily relying on a player to die or gain information that the character couldn't know may be a sign of bad design, but it isn't always. In fact, sometimes it's used to very good effect. For example:

  • The desert puzzle is rewarding. Yes, the player has to make map or memorize oasis locations -- many people may not consider this fun. But the relief of seeing an oasis is a product of the consequence of the many attempts without finding one. 
  • Many scenes have compelling tension: sneaking past the sleeping bandit, choosing which treasure to take from the treasure room, the blue creature, and the wizard showdown with Mordack. 
  • The genie bottle. Now, admittedly, this puzzle sucks, but not because of the death. The player gets a genie's bottle that, when opened, traps Graham in the bottle and then, game over. If the player gives it to the wicked witch (no rationale), she'll open it, and herself becomes trapped. By no means am I defending this puzzle's logic, however, the player learns of its effect by first dying. When the player gives the bottle to the witch, they know what would befall her, but, presumably, Graham doesn't. And that's great. It gives the player the feeling of outsmarting the witch. To be sure, this same effect could be accomplished without the prerequisite death. However, if dying allows an opportunity to give the player a moment of triumph, I'm all for it.

In the end, death requires tact, and if a scenario can be better written without death, perhaps that's the way to go. Of critical importance: if a game has death, the save/restore UX needs to be seamless. No hunting for save files, no requisite saves every 5 seconds. Player died? Get a funny message and they're back at it 3 seconds later.

Music / SFX

The early 90's was still the wild west in game design. They didn't have rules and guidelines to the extent we do now. I've noticed that modern AAA games have mastered the art of background music. The themes may be memorable, but their goal is to subtly complement the experience the player is having. 

Not so with Sierra, and King's Quest in particular. I would describe the music as "melody-forward". The tunes are memorable. They grab the player's attention. They can hardly be called background music.  This is a very good thing for a more lighthearted adventure. Since the adventure game experience is so much simpler than, say, strategy games, there's room for more forward music. 

Additionally, the timed/orchestrated cutscenes makes for a rewarding experience. The music plays part of the storytelling, emphasizing certain points, even using music for sound effects, much like old cartoons would use the orchestra for their sound effects. This, stylistically, works well for the high spirited adventure. Would it work as well for sci-fi? Probably not.

King's Quest VI

King's Quest VI is rather different from King's Quest V. The pacing is different and the puzzle styles are completely different. Let's focus on some of those differences.

The contraption is a plan, not a contraption.

So adventure games are notorious for requiring the player to build contraptions. There's the infamous make-a-disguise puzzle in Gabriel Knight 3. In King's Quest VII, the player sends the moon back up using a rubber chicken. They're obscure, and often mind-bending. How is King's Quest VI different?

Puzzles are combined to create a master plan.

Think about it. Early in the game, the player; meets Jollo. He's the royal clown, and Alexander befriend him. Later, the player concocts a plan with him -- to swap lamps with the evil, genie-possessing royal vizier, and to convince said genie that Alex is dead. That's not a slingshot, it's not a trap, it's a clever plan. It requires the player to solve puzzles in the same way that you might for a contraption, but it feels more grand. Additionally, it makes Alexander only part of the puzzle. It requires the help of at least 7 people to defeat Alhazred (Alexander, Jollo, Cassima, Saladin, Shamir, and Cassima's parents). That feels rather grand.

 

 

Few "fetch" quests

King's Quest VI has very few fetch/trade puzzles. Compare it to King's Quest V. The player essentially have a series of unrelated trades. For example, you need a hammer that you get from a cobbler by giving him a pair of shoes that you get in exchange from some emeralds that you could only acquire using an amulet that you got from a gypsy in exchange for a gold coin that you got with a staff that you steal.

The interactions are the same -- e.g., use ring on guard, give lamp to Jollo -- but the effect in the game is very different. 

Let's take a look at an example.

Each gnome has a single sense

Each gnome has a single sense

  • These five gnomes are the guardians of the Isle of Wonder. They act as a sort of locked door to this island. The player must first satisfy them in order to pass. How do you satisfy them? Each gnome has a single sense, and the player must use inventory items to convince them that Alexander is not a human, one at a time. This could've easily been written as a 5 item fetch quest, each gnome wanting a single item to appease them. But that would be boring. Notice that this puzzle isn't immersive. It doesn't try to be realistic. It's a quirky puzzle applied to quirky, fictional, unrealistic characters, and it works.

The scenarios like this go on and on. Very few puzzles are explicitly spelled out as a fetch quest. E.g.,

  • Send a message to princess Cassima through a nightengale
  • Gather incriminating evidence
  • Save yourself from burning to death
  • Make the lord of the dead shed a tear.

The more outlandish the idea, the more compelling the storytelling.

Tell the player the answer as part of the puzzle

In Tick's Tales, I made it a point to always tell the player the solution before the player solves it. The player only has to pay attention, and figure out how. King's Quest VI doesn't do this always, but it has a couple of subtle tricks to accomplish the same effect.

Old lamps for new...

Old lamps for new...

  • Musical themes are reprised in related situations, giving the player a clue to the solution. For example, Beauty's theme is almost the same as Beast's theme. By the time the player meets Beast, they've probably heard Beauty's theme several times. Also, the dangling participle has a similar melody to the bookworm. These are signposts to the solution, and, ideally, the player doesn't even notice.
  • Sometimes it's said outright. Jollo says, "Maybe you could convince him (the genie) that you died!"
  • Checkhov's gun principle  says:  "Remove everything that has no relevance to the story. If you say in the first chapter that there is a rifle hanging on the wall, in the second or third chapter it absolutely must go off. If it's not going to be fired, it shouldn't be hanging there." A similar rule applies to adventure games, and King's Quest VI directly manipulates the order the player is exposed to certain characters, so that they definitely know a solution before they see it. For example, the first time the player makes their way into town, they see a peddler selling new lamps in exchange for old lamps. Later, when they acquire an old lamp, they think, "Oh, I can exchange it for a new lamp!" but the peddler has since left. This makes them ponder what the purpose of the peddler is. Later on, it "clicks" when they learn they need a replica of the genie's lamp.

In summary

Not all of these principles applies to every single game. But these principles do work for King's Quest genre, and I think they're worth considering. Tick's Tales follows some of the principles, and others it doesn't. For future games, I'll think about crafting the narrative and puzzles considering these principles.

If there are specific games you'd like me to study in this fashion, please write a comment! Stay tuned for more.

4 Comments

Making a tale tick: How to make Tick walk to where I click

2 Comments

Making a tale tick: How to make Tick walk to where I click

I often think of classic adventure games as their own thing -- their slow-pace, narrative-oriented nature seem to have more in common with a graphic novel than a video game. Yet some things are ubiquitous, regardless of the genre. For example, real time strategy games, first person shooters, roguelikes, adventure games -- heck, nearly any game, will have some sort of path finding. Today, we're going to look at how I did it in Clojure for Tick's Tales.

The problem: We want to keep our protagonist in a walkable area. As cool and hero-worthy as Tick might be, he still can't walk through walls, buildings, or even grass. Also, we don't want to make Tick look dumb while trying to complete the simple task of walking through a door. If you've ever played any of the SCI0 Sierra games, you might remember infuriating problems like this:

There's a lot of clicking in this gif.

There's a lot of clicking in this gif.

How to get Tick to where I click

There are several approaches to pathfinding. For Tick's Tales, I opted to make a black and white image that would represent the passable and unpassable areas.

We'll load this image as a giant data structure in the game. I scale the image down to half size (160x120). Next, convert each pixel from this image into a vector of vectors, representing what's passable, and what's not.

By way of simple example, let's say that data structure looks like this:

(def c-map [[1 1 1 0 1 1]
            [1 1 1 0 1 1]
            [1 1 1 0 1 1]
            [1 1 1 1 1 1]])

This vector of vectors will serve as a graph for A* pathfinding. There's a wealth of information on this algorithm. If you're unfamiliar with it, or need to brush up on it, I  highly recommend you start here

Now let's say Tick is standing at [0 0], and the user clicked at [0 5] on this map. We can see this visually:

Red represents Tick's starting position, and green represents his destination.

Red represents Tick's starting position, and green represents his destination.

We want to create a function that will take a map like this, the start position, the end position, and returns the path to that position. That is:

In code, it might look something like this.

(defn path-find [collision-map start-pos end-pos]
  ;; Should return something like this
  ;; [[0 0] [1 1] [2 2] [3 3] [2 4] [1 5] [0 5]]
  )

Some nomenclature

Throughout this post, I'll be using a couple of terms that might be useful to know.

  • Position: a point on the grid, represented as a tuple of [y x] coordinates. (Yes, that may seem backwards. This done for easy get-in mechanics.)
  • Neighbors: All of the adjacent positions to a position.
  • Path:   A sequence of positions, e.g., [[0 0] [1 1] [2 2]]
  • Node: For our purposes, a node will be a tuple of a position and a path. That is, a node represents a position, and the sequence of positions from the start position to that position. For example [[2 2], [[0 0] [1 1]]. 
  • Frontier: A set of nodes to check next
  • Visited: A set of positions that we've already visited, to prevent backtracking.
  • Heuristic: An approximate distance, used to figure out which nodes are the best candidates of being the right path.

Note: Node might be a departure from what comes to mind if you're already familiar with pathfinding. A* and breath-first-search are graph traversal algorithms, but with our grid-based approach, we aren't really storing our map as a graph with nodes and edges. As such, node is a fitting term for "A position and the breadcrumbs of how we got to that position." 

Some precursors

Before we can dive into finding the path between the two points, we'll need to write a function that gives us the neighbors from a given positions. 

; each represents adjacent positions, northwest, north, northeast, etc.
(def neighbor-deltas [[-1 -1] [0 -1] [1 -1]
                      [-1  0]        [1  0]
                      [-1  1] [0  1] [1  1]])
                      
                      
;; get each point that is passable that's adjacent
(defn neighbors [collision-map [y x]]
  (let [candidates (map #(mapv + %1 %2)
                        (repeat [y x])
                        neighbor-candidates)]
    (remove
     #(= 0 (get-in collision-map % 0))
     candidates)))
     
(neighbors c-map [2 2])
;; ([1 1] [2 1] [3 1] [1 2] [3 2])

We build up a list of candidates, which is just a sequence of all of the neighbors. Then we filter out any impassable candidates from that list.

Breadth first search

Let's start with the easiest implementation to get Tick to where we want him to go, a breadth first search, which we'll build upon later. It's probably easiest to start visually.

Orange represents the frontier. 

Orange represents the frontier. 

In the above animation, we start at the starting position, check its neighbors, then each of its neighbors' neighbors, and so on, until we find the destination. Along the way, we'll keep breadcrumbs of how we got to the current path. Let's take a look at an implementation in Clojure.

(defn breadth-first-path-find [collision-map start-pos end-pos]
  (loop [frontier [[start-pos [start-pos]]]
         visited (set [start-pos])]
    (let [[[current-pos current-path] & frontier] frontier
          unvisited-neighbors (->> current-pos
                              (neighbors collision-map)
                              (remove visited))]
      (cond (nil? current-pos)
            []
            (= current-pos end-pos)
            (conj current-path current-pos)
            :else
            (recur (into (vec frontier)
                         (mapv #(vector % (conj current-path %)) unvisited-neighbors))
                   (into visited [current-pos]))))))
                   

Let's break it down into the small bits.

(defn breadth-first-path-find [collision-map start-pos end-pos]
  (loop [frontier [[start-pos [start-pos]]]
         visited (set [start-pos])]
    ...))

Frontier is a sequence of nodes, each with a position, and the path leading up to that position. In the beginning, we only have one, the start position. But as we recurse through this loop, it'll continually add nodes to the frontier. 

We also keep track of visited, containing each of the positions we've already checked. This set is only used to ensure we don't visit the same node twice. Otherwise, we'd end up in an endless loop.

(defn breadth-first-path-find [collision-map start-pos end-pos]
  (loop [frontier [[start-pos [start-pos]]]
         visited (set [start-pos])]
    (let [[[current-pos current-path] & frontier] frontier
          unvisited-neighbors (->> current-pos
                                (neighbors collision-map)
                                (remove visited))]
      ...)))

Each iteration of the loop, we're going to check one node to see if we've reached the destination. [current-pos current-path] is a destructured representation of that node. For a few examples of destructuring, see here. One common destructuring form is [[current & more] some-sequence]. It's a shorthand that assigns the first thing in some-sequence to current, and the remaining items more. We leverage that pattern here. We pull off the first node from frontier, and rebind frontier to the remaining nodes. We further destructure the node by getting the current-position, and the current-path.

Now consider again the body of the loop.

(defn breadth-first-path-find [collision-map start-pos end-pos]
  (loop [frontier [[start-pos [start-pos]]]
         visited (set [start-pos])]
    (let [[[current-pos current-path] & frontier] frontier
          unvisited-neighbors (->> current-pos
                              (neighbors collision-map)
                              (remove visited))]
      (cond (nil? current-pos)
            []
            (= current-pos end-pos)
            (conj current-path current-pos)
            :else
            (recur (into (vec frontier)
                         (mapv #(vector % (conj current-path %)) unvisited-neighbors))
                   (into visited [current-pos]))))))
                   

Each loop, we check to see if the current node is nil. This happens when we have hit every possible position there is, and still haven't found the end-pos. In this situation, there is no path to destination, so we return an empty path.

If we find the end-pos, we add it as our last step in the path, and we're done.

Otherwise, we continue looping, adding all unvisited neighbors to the frontier. When we do, we add the current position to the path. We also add the current position to the visited set, to ensure we don't backtrack and get stuck in an endless loop.

All done... or are we?

Breadth-first search is just too slow. Even in our very simple example, every single position is checked. But in Tick's Tales, the map is 160x120, or 19,200 positions to check, each with breadcrumbs detailing how it got there. Since we short circuit if we find the end position, we won't always check all 19,200, but that's still going to be crazy slow.

Let's take a look at another example on the same map, and hopefully it'll illustrate how we can improve our solution.

 

Tick starts looking to the left, and to the right. Shouldn't he check out the right first? We'll use A* to do just that.

A* search

There's really only one difference between the breadth first search and the A* search. An A* search tries to find the best neighbors to search first, rather than searching all directions equally.

Which are the best neighbors? The best neighbors are a combination of two things:

  1. Proximity to the destination. This may seem self explanatory; we check the nodes that are closest to the destination first.
  2. The distance traveled so far. This might seem less intuitive. The basic idea is that just going in the path of the destination might lead to a dead end. If it does, Tick would walk into the wall then start walking the perimeter until he finds a way around it. He'll still make it to the destination, but it won't be a very direct path.  By factoring in the total distance traveled, we consider other options, trying to go around the obstacle in the first place.
(defn heuristic [[x1 y1] [x2 y2]]
  ;; approximate the distance to the destination, using	manhattan distance.
  (+ (Math/abs (- x2 x1))
     (Math/abs (- y2 y1))))
     
(defn a*-path-find [collision-map start-pos end-pos]
  (loop [frontier (p/priority-map [start-pos []] 0)
         visited (set [start-pos])]
    (let [[[current-pos current-path] c] (peek frontier)
          frontier (pop frontier)
          unvisited-neighbors (->> current-pos
                                   (neighbors collision-map)
                                   (remove visited))]
      (cond (nil? current-pos)
            []
            (= current-pos end-pos)
            (conj current-path current-pos)
            :else
            (recur
             (reduce (ƒ [frontier neighbor]
                       (let [neighbor-path (conj current-path current-pos)]
                         (assoc frontier [neighbor neighbor-path]
                                (+ (count neighbor-path)
                                   (heuristic neighbor end-pos)))))
                     frontier
                     unvisited-neighbors)
             (into visited [current-pos]))))))

I've highlighted the differences from the breadth-first search.

  • We added a heuristic function. This is used to give an approximate distance the destination, using manhattan distance. If we weight based off this value, Tick will avoid going to the left in the example above.
  • Our frontier vector has been replaced with a priority-map, which is part map, part list. Instead of conjing on to it, we assoc. This allows us to specify the node, and the priority of the node. When reading from the priority-map (using peek and pop), it will be sorted by the priority. Here's a little example of how this works:
(peek (assoc (p/priority-map) :a 500 :b 1 :c 501))
;; [:b 1]
;; Notice b comes first, because keys are popped of in order of the value.
  • Jump down to the reduce, the meat of what's changed. Instead of just adding all of the unvisited neighbors to the end of the frontier, we have to assoc each one, specifying its priority. We use reduce to do this. To prioritize, we include both the distance traveled so far (the number of steps in the path) and the heuristic (approximate distance).

There you have it! We can test our pathfinding now:

(a*-path-find c-map [0 0] [0 5])
;;[[0 0] [1 1] [2 1] [3 2] [4 3] [3 4] [2 5] [1 5] [0 5]]

Lessons Learned

Even though this solution is relatively simple (heck, it's barely 30 lines of code!), there is a lot I'll do differently next time around. It's difficult to get into all the nitty-gritty in a blog post, but I'll share a little bit of what I learned along the way.

  • The code in this post resembles what I started out with for Tick's Tales, but not what I ended up with. I quickly found that any implementation using immutable datastructures and  a pixel-perfect grid based approach (like above) is still too slow. In the end, I reverted to using java.util.PriorityQueue in the end.
  • The paths sometimes come out really jerky. I had to implement some smoothing after calculating the path.

My conclusion from this is that a grid-based approach is great for real time strategy games, but I'd probably switch to a graph based on polygons (like navmesh) if I started over. The advantages of this are threefold:

  • The total number of nodes would be drastically smaller from the 160x120 maps shown here. It'd go from 19,200 to around 50 or so in most cases.
  • The paths would automatically be smooth, because the steps in the paths would be straight lines from point to point.
  • As a result of the small graph size, I'd be able to keep an immutable solution.

That's all for now. Hopefully this was helpful!

2 Comments

Making a tale tick: The making of Tick's Tales - Part 1

3 Comments

Making a tale tick: The making of Tick's Tales - Part 1

What makes a tale tick?

This is the first in a series of blog posts describing the programming, art, story, and music direction behind Tick's Tales: Up All Knight. My goal is to show some of the methodology behind the game, both for those interested in making adventure games of their own, and for those who are just curious about Tick's Tales. If there's something you're interested in hearing about, please make sure to post a comment, and I'll share what I can! If you're interested in the game, please do follow @tickstales on twitter!

I'm going to start by sharing some details about Tick's Tales' backend, as it's probably the most unique aspect amongst other adventure games.

This story has Clojure

Tick's tales is built in Clojure. It's a very atypical programming language choice for a game. As far as I know, there haven't been any sizeable games built in Clojure. So why choose it?

  • The REPL (Read Evaluate Print Loop) is very powerful, and allows me to rapidly change the game while it's running.
  • Using play-clj and libgdx means that I can target Windows, OS X, Linux, Android, and iOS easily.
  • A lot of new creative opportunities open up if I create my own engine (vs. visionaire, ags)
  • Clojure's persistent data structures could prove incredibly flexible for building out entities in a stateless way
  • I could leverage core.async to easily build a scripting language/api. (covered in a future article)

The REPL

When I started, I underestimated just how much the REPL sped development up. This dramatic speed-up came in two forms. The first is that I could make changes to the game while it's running. And secondly, I didn't have to create any editors or tool.

A quick feedback loop

Using the REPL, I can change the game while it's running. When Tick's Tales starts up in development mode, I can change any file, and see its changes immediately. A typical example might be tweaking a script. Perhaps I'm trying to fine-tune the timing in the dialogue. I can make the code changes, Cmd+Tab over to the game, and see it while it's running. If I'm not happy, I can just rinse and repeat. A picture's worth 1000 words, so watch this example of changing a font color on the fly.

 

It also means that I have access to game state while it's running. I can inspect any entity as the game is running, and even change it, without making any code changes. 

 

Editor-less

Many adventure game engines will include an editor that lets you define animations, walkable areas, write scripts, place objects and characters, etc. To be sure, these are very helpful tools, and make it easy to quickly put the game together. But they also take a lot of development effort to maintain, and most of the time, probably more than the game engine itself. With the quick feedback loop that I had above, I never found the need to do this. It was a simple enough task to tweak the game as it was running. 

There was another added benefit to this. When you use an editor, essentially the game is serialized to some data format. It essentially draws the boundaries of what your game can do. Without one, it made it much more rapid to try out new ideas. 

For example, there's a water fountain in the game. Originally, when you walked into that scene, the water fountain would play at a constant volume. Based on a recommendation from a friend, it would be better to have the fountain's volume be based on Tick's proximity to it. This was trivial to try, and it ended up making it into the game. This was so easy, in part, because I didn't have an editor. Instead of having to define a way to describe this behavior with data, I was able to replace a constant (the volume of the sound) with a function that returns the volume of the sound.  

Creative power

By rolling my own engine, I tried some things that are atypical to the adventure genre. The best example is probably the camera. Most of the time, adventure games either have a fixed background, or a panning background with some parallax. I tried something different with Tick's Tales. I wanted to make it feel like there was someone behind the camera. I wanted to have a little camera shake and zooming. 

 

This was relatively easy to do (thanks libgdx!), and I think it does make the game feel more alive, even though it is low-res.

Persistent data structures

Most games these days either use Object-oriented programming or an Entity-component system for modeling the world. While you can do either in Clojure, I wanted to stick to simple data structures for modeling most things. Most everything you see in Tick's tales are simple maps, vectors, and sets. If you're familiar with json, this will look vaguely familiar.

; the clouds on the title screen
{ :texture (get-texture "title/clouds.png" )
:x 0 :y 0
:scale-x 4 :scale-y 4
:origin-x 0 :origin-y 0
:z 2
  :arbitrary-data [1 2 3]}

This might seem relatively simplistic, but what it meant is that I could add behavior and data to objects very easily. There's an Alan Perlis adage that goes, "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures." I believe this to be true. For Tick's Tales it means that there is a consistent and predictable way to change all of the objects in the game. Clojure's library provides a huge tool set for manipulating data structures, and those can be used for literally everything in the game:

; change some state about an entity
(assoc-in entities [:room :entities :damsel :saved?] true)

; shift player over 20 pixels
(update-in entities [:room :entities :ego :x] + 20)

; or, shift any entity to any position
(update-in entities [:room :entities target] assoc :x 100 :y 50)

; calculate the total distance of a walk path
(reduce + 0 (map :distance (:path ego)))

I can't overstate how much boilerplate code I didn't have to write because I was able to use persistent data structures, and the many, consistent, operations to manipulate them.

Persistent data structures do have disadvantages, though. For one, because they are immutable, each change does require some memory allocation. In a game the size of Tick's Tales, it's pretty much negligible on the desktop. However, on mobile, garbage collection is notably slower and causes noticeable jitter and FPS, and I'm going to have to find optimization opportunities before it's ready for those platforms. 

Additionally, I spent quite some time optimizing pathfinding, as Clojure's persistent data structures and arithmetic were too slow by default.

In conclusion

I'm really happy I wrote Tick's Tales in Clojure. I'd do it again in a heartbeat. Feel free to post any questions, and I'll try to answer them in the next post.

Until next time,
Bryce

3 Comments