Posted on February 18, 2019
In this post, I’ll go over some of how verb handling works in Cascadia Quest (as opposed to my older game Snail Trek).
Given that the bulk of the game consists of responses to actions on items, I need to make sure that the process of writing those handlers is as simple and non-bug prone as possible.
There is more to a verb handler than just the code that responds to it:
- There also needs to be some metadata about the verb on that object – mainly, does it require the player to approach the object or not? ‘look’ might not, but ‘get’ usually will (but not always – it depends on the object).
- We would also ideally like to know if the verb can be used “nounlessly” with the object. For instance, if there is a door and a button in front of the player, ‘open’ can be used nounlessly with the door, but not the button (but you could still type ‘open button’ and expect some kind of fallback response).
- Some verbs that are normally treated as synonymous need to be treated separately on certain objects. ‘look’ vs ‘look in’ is one example. There are more that will be listed later.
The made-up language I’m using did not have a good way to declare verb metadata, so I was left with implementing other methods on an object that would return the necessary information about a verb (e.g. does it require approaching an object). It was much too easy for this to get out of sync with the actual verb handling code.
Additionally, the verb handling code was a basically a series of switch/if/cond statements. Writing logic like that, while straightforward, tends to lead to bugs. A more data-driven approach would be better.
Here’s an example of the old way, from the funny hat dispenser in Snail Trek 2:
(instance hatDispenser (method (remapToCoreVerb verb) ; Normally vLookInside is mapped to vLook. I need to declare that it shouldn't be for this object (if (IsOneOf verb vLookInside) (return verb) ) (return vNone) ) (method (isApproachVerb verb) ; Need to remember to put all approach verbs here (return (IsOneOf verb vUse vPut vLookInside)) ) (method (doVerb theVerb item) (switch theVerb (vKill (Print "If you break the machine, there will never be anymore funny hats.") ) (vGet (Print "Don't get greedy.") ) (vLook (Print "It's a small machine mounted in the wall that says \"Funny Hat Dispenser\".") (Print "It has a small coin slot.") ) (vLookInside (Print "You peek inside the coin slot, but don't see anything.") ) (else ; Logic getting a little complicated here (cond ((or (and (IsOneOf theVerb vUse) (== item nNothing)) (and (IsOneOf theVerb vPut vUse) (== item coin)) ) (InsertCoin) ) (else ; Need to be sure to forward onto the default handlers (super doVerb: theVerb item &rest) ) ) ) ) ) )
Given that I have control over the language, it seemed to make sense to modify the language to support my needs – basically bubbling up the game logic requirements into the language syntax. The less friction between intent and implementation the better.
I didn’t do anything complex like change the compiled language structure to add metadata. Instead, I just made the compiler take the new syntax and convert it behind the scenes to a series of methods that works the old way.
Here’s the same object above, converted to the new way:
(instance hatDispenser ; all these verbs will automatically be tagged as 'make player approach' (nearVerbs (vLookInside (Print "You peek inside the coin slot, but don't see anything.") ) (vUse (InsertACoin) ) (vPut, vUse -> coin (InsertACoin) ) ) ; these work from a distance, but will be tagged for nounless support (farVerbs (vKill (Print "If you break the machine, there will never be anymore funny hats.") ) (vGet (Print "Don't get greedy.") ) (vLook (Print "It's a small machine mounted in the wall that says \"Funny Hat Dispenser\".") (Print "It has a small coin slot.") ) ) )
In addition to being more terse, all the metadata for the verbs is inferred by inspecting the code. The compiler can generate the appropriate code behind the scenes that will inform the framework about all this at runtime.
A small side note: by default, any verb that is handled is considered as a candidate for a nounless verb (meaning you can omit the noun if you’re standing in front of the object). This can still cause issues in some cases: say you have two objects in front of you, and you can only actually ‘get’ one of them. But I wish to have a (perhaps funny) response for trying to ‘get’ the other one. I have not yet implemented any syntactical solution for this (I can still override the appropriate method on the object to solve this though).
Some more details
In each verb clause in a nearVerbs or farVerbs section, I can declare a number of verbs and items. For instance:
(vPut, vUse -> nMushroom, plant
This will handle trying to put the mushroom or plant in this object, or using the mushroom or plant with this object.
The items supplied can be inventory items or other objects in the room. If no items are supplied, then it’s a handler for when the player just uses that verb with the object (nounlessly or noun), with no other objects mentioned:
Note that in the old way, in this scenario it was very easy to forget to explicitly check that no item was supplied to the verb handler. So ‘open elevator with crowbar’ would be treated the same as ‘open elevator’. Sometimes that is desirable, but when that is the default behavior some issues can arise – especially with nounless matches. For instance, consider the case where there are a box and an elevator in front of the player. They type ‘open elevator’, but the box is closer to the player, and we do a nounless match with the box (which supports ‘open’). The result is that the box’s verb handler will be called with vOpen and elevator. Yes, I do nounless matching even when a noun is supplied. This is how you can walk up to Christian and type ‘ask about Shelley’ (turns into ‘ask Christian about Shelley’), or how you can stand in front of an engine and type ‘put fuel’ (turns into ‘put fuel in engine’).
In the case where we really do need to handle any item that is supplied, we can do this:
(vPut -> *
Then there is an implicit ‘item’ variable we can reference to identify which item was said (if we care).
The verbs in Snail Trek and Cascadia Quest work on multiple levels.
First, there is a mapping from the typed words into conceptual verbs. This is handled by a grammar parser and isn’t always a 1-to-1 mapping. For instance, all the following typed phrases will be mapped to vEnter: ‘enter’, ‘get in’, ‘go down’. Or, vTurnOn can be ‘turn on’, but also ‘switch on’ or ‘start’. This minimizes the complexity of the code I need to write, and ensures consistency when performing actions on different objects. I’m constantly changing these as I develop the game and need to distinguish what were formerly the same verb. Currently in Cascadia Quest there are about 440 recognized grammar verbs, and these are mapped (often along with prepositions) to nearly 100 different conceptual verbs that I write code against (stick that in your point-and-click UI!).
It’s often useful to distinguish conceptual verbs in some cases but not others. For instance, looking at a bed might result in a different response than looking under the bed. So there are multiple look verbs: vLook, vLookUnder, vLookInside, vLookBehind, vSearch. By default, these will be mapped to the first one, vLook. So a cupboard, say, can just have a vLook handler, and ‘look in cupboard’ will just result in the cupboard’s vLook response. But if a nearVerbs or farVerbs explicitly handles one of the others, then we will automatically make a distinction.
In the old way, I had to write additional code to tell the framework not to automatically remap these conceptual verbs to the more core set of conceptual verbs. You can see that in the example posted earlier in this post:
(instance hatDispenser (method (remapToCoreVerb verb) (if (IsOneOf verb vLookInside) (return verb) ) (return vNone) ) )
Other examples where this remapping currently happens are: vKill (vCut, vHit). Or vJump (vJumpOff, vJumpOn, vJumpOver).
The next post will go into more detail on some of the subtler verb handling issues.
Posted on May 20, 2018
For Adventure Jam 2018, I created Snowspirit. If you need a bit of help getting through it, here are a few hints. Highlight/select each bullet point to get ever more “spoilery” hints about a problem.
What am I supposed to do? I’ve run out of things to do
- Try to make it snow again.
- The tablets are the key.
- One is a recipe.
- The others hint how to get ingredients.
- These ingredients aren’t in the real world.
- These ingredients aren’t in the real world.
How do I escape reality?
- What happened when you first woke up from your dream? Something fell off your stalactite.
- Find other things to put there
- Go to sleep!
How do I wake up from a dream?
- Just step into the light!
How can I bring things back from dreams?
- You can’t
- But… important items you pick up while dreaming might have real world effects or manifestations…
What am I supposed to do in the forest?
- That’s easy one. Just get some water!
- You might need to revisit it later though.
How do I get to the third (probably?) dream?
- You have what you need. What do you have? What haven’t you used? What’s it made of?
- Just need to transform it a bit.
I’m in space and I’m stuck
- Pay attention to the light
- Notice that items in the dreamworld can’t be brought back to the real world, but they do transfer between dreams
I need a flower!
- If you don’t even know where to look, see the “third dream hint” above
- Everything in that room is important… so read the descriptions for everything.
- Where is that light coming from?
- Maybe from a previous dream?
- There’s a little water. What’s the water doing? What if there were a lot?
- You might need to do stuff in another dream with something from yet another dream.
I did everything and it’s barely snowing….
- Put all the ingredients in the pot? That would be a good start.
- What’s on that fourth triangular tablet? Hmm, you can only see a bit of it…
- At least it tells you how to get to a dream…
Posted on October 25, 2017
I found myself wanting to have stairs and walkways that appear on top of other walkable areas. I had avoided this up to this point because of issues with pathfinding.
While the A* algorithm I use for the actual path finding isn’t limited by two dimensions (it works with any node graph), the game world and base polygons I work with are 2d, and the library I’m using to combine and clip the polygons is 2d. You can find more information on this here.
Consider the following room with a upper level walkway that appears in front of the stairs:
The player can be on the stairs or on the walkway. In the room script I need to track where they are based on where they entered the room and how they have crossed through the area near the top of the stairs. It’s reasonably straightforward to do this and ensure that the player character has the correct depth value (so he/she appears in front of behind the walkway).
If I want to describe the walkway areas for pathfinding, however, I end up with something like this:
If this were true 3d, this would probably work fine. However, in 2d a point where the polygon overlaps itself doesn’t really make sense. Is it on the walkway or on the stairs? Related, my clipping library turns this polygon into this:
Now it’s no longer ambiguous. However, we can freely walk between the stairs and the walkway without going through the upper stair landing.
I could possibly selectively place blockers depending if I detect the player is on the stairs or walkway. But that’s a lot of manual work, and I want a slightly more rigorous solution I can use in many rooms.
My solution is to split the room into multiple pathfinding zones. Visually, this looks like:
Note that these have a small overlapping section near the top of the stairs. This is where we’ll transition between the two zones.
Along with this, I added the following functionality:
- Rooms no longer have a single set of polygons that define their boundaries. Instead, they have a set for each pathfinding “zone”
- Each actor (moving sprite) has a pathfinding zone number associated with it
- I introduced a “pathfinding triggers” concept – these define small regions of the room that actors can pass through that will take them into a new pathfinding zone (this is how the actor’s pathfinding zone number is kept up-to-date). In the example we’re using, these would lie on the edges of the small transition area between the zones.
In terms of movement, the room’s polygon boundaries are used for two functions:
- To answer the question “can I be here?” for an actor. This is used when moving the player with the keyboard (or controller). In this case, I can simply choose the polygon for the pathfinding zone the actor is in, and make the query.
- To be able to find a path from point A to point B. This is used for “click to move”, for moving NPCs from place to place, and also (importantly) when typing in a command that causes you to interact physically with an object (the game needs to move the player to the object).
For the second case, we have the problem that the target pathfinding zone might be different from the one we’re currently in.
To solve this, I extended the pathfinding logic like so:
- If the target position is in the same zone as the start, do things just as before.
- If the target position is in a different zone (B) than the start zone (A), find a transition point T between A and B. Then find a path from A to T, and then T to B. Combine the paths and use this as the result.
The transition point should be inside the overlapping areas of the two zones. To avoid unnatural paths, the transition area should be as small as possible.
So the manual work to support multiple pathfinding zones in a room is:
- Define the sets polygons for each zone (of course) – they must overlap a bit.
- Create triggers on each side of the overlapping transitions between zones – these could be anything (manual coordinate checks, polygons, or control colors). Currently I’m just using control colors, which is a bitmask built into the background. Associated with the triggers in code is the pathfinding zone number they assign to any actor passing through them.
- A transition point for transitions between any two zones.
I should in theory be able to support any number of zones per room, but currently I just support two. With a little bit of extra work, the pathfinding algorithm mentioned in case (2) above could be extended to n zones (in case you need to transition to an intermediate zone separate from the start and end zones).
Note also that “click to move” is still ambiguous, because all you have is a 2d point, and you are making assumptions about which zone it’s in. In practice this isn’t an issue though. It’s completely reasonable just to make it use the zone that the actor is currently in.
Posted on October 9, 2017
The color palette I’ve chosen for Cascade Quest consists of the combinations of the sixteen EGA colors (to simulate the palette used by those old games, which often dithered two colors together). Since color A combined with B is the same as B combined with A, there are only 136 actual colors instead of 256. Not only that, but several combinations result in the same color, so there are really only 126 distinct colors.
This fixed palette poses a number of creative issues when it comes to choosing colors. I’ve already gone into some detail as to how I support blending two colors, but in this post I’ll talk about choosing colors from an artistic perspective.
Choosing colors by specifically specifying the two source EGA colors also isn’t very easy, since the resulting color the combinations would make isn’t always obvious.
Some of the questions I need to answer when choosing a palette are:
- What other colors are similar to this one, but lighter or darker? (or less saturated, etc…)
- What other colors are extremely similar to this one?
- I need a dark area, what good color choices do I have?
Variations around a certain hue
For example, for a certain area I wanted to have an orange and greyscale palette. The greyscale colors are fairly well known, but I wanted a quick way to figure out my options for orange colors.
In the fast palette lookup post, I mentioned a quick tool I made to let me visualize my set of colors. I extended it to encompass different color spaces, and let me click on a color to get its palette index.
In addition to RGB, I have:
- The most useful one, what I call “perceptual HSL”, where luminance is actually the perceived brightness. The human eye responds to green light the most, and blue light the least. Thus (0, 0.2, 0) is perceived to be much brighter than (0, 0, 0.2). More details can be found here.
Here’s a screenshot showing the colors I’ve used for orange objects in the “orange and greyscale” areas:
The dark colors are pretty limited, and my color visualizers tell me that I should probably focus on blue or purple colors if I want variety in dark colors.
If I want to place some somewhat hidden messages in the game – like a color that’s only slightly different from another – this let’s me easily see that.
I have the ability to remap palette indices, so a secret message could be made very visible with a special light in game of course (that “remaps” the surrounding area). But it’s kind of a nice touch that when it’s “invisible”, it’s still actually just slightly visible (or completely invisible if I choose base color combinations that result in identical colors – Void Quest used this somewhere, for a hidden thing at night…).
Re-evaluating color choices
I can also look at palette choices I’ve made. I use five main colors for the “summit” scenes in the first act of the game.
I can visualize those five colors in my tool:
You’ll note that they are all completely identical hues, which maybe isn’t the best choice. Shadows should generally be a little bluer, and highlights a little more yellow. So I might be able to find a better option (although it looks like my choices are a little limited in this region of the color wheel).
As I become more conscious about palette choice, hopefully this tool will help me out in making good decisions.
Posted on September 12, 2017
Cascade Quest was shown to the public for the second time at Seattle Indies Expo (a PAX West side convention) on September 3rd.
Lots of space at SIX this year – they moved it to a new venue, closer to PAX and with air conditioning. Since I haven’t yet commissioned a proper poster for Cascade Quest, I decided to spruce up the booth with green stuff (since the start of the game takes place in a forest): green tablecloth, fake plants, green LED lights. And I purchased a couple of plushies based on characters in the game.
I also had buttons to hand out to folks who played the game or signed up to the email list.
Changes from last demo
I’m still just demoing the first act of the game (and I doubt I’ll change that before release). I had reworked the text input system quite a bit since I last demoed it, and I thought it worked much better. I also changed a few puzzles, redid a few screens and added a couple more.
I also expanded the analytics I recorded slightly – it’s no longer just the text input, but also autosaves (upon entering each screen), screenshots, and the player’s position on screen for each piece of text input. This was in the hopes of being able to diagnose difficult-to-reproduce bugs (every bug I found was easy to reproduce though).
I also added a third demo machine since I had space on two sides of my table and I knew there were going to be more people at Seattle Indies Expo than at the retro game expo back in June. This was a good idea, since all three machines were occupied most of the time.
- Players entered 2395 rooms throughout the day
- Players typed 7067 phrases in to the game
- 5 players made it all the way to the end of the demo
- About 70 people sat down and tried the game
- About 60 players made it out of the ranger station (where the first puzzle is)
That was a lot of data to process, and I ended up spending about a week afterwards making changes to the game to address issues or frustration that players encountered.
Some of the things people typed:
- Talk sweetly to printer (this was due to a bug in the way I handled putting paper into the photocopier in the first room – this frustrated a ton of people)
- Love the printer (related to above)
- Smash desk
- Flip desk (someone’s getting mad – perhaps related to above)
- Get some jordans so you can get hops
- git gud
I added a bunch of in-game hints to explain how the typing system worked, and that alleviated much of the frustration that new-to-text-parser adventure gamers had.
I’ve also added auto-correct, so this eliminated a bunch of frustration people had with typos.
One of the significant changes was to make your text input more context sensitive – it will infer what you’re talking about depending on what you’re near. So ‘get‘ will get the object in front of you. ‘give mushroom‘ will give your mushroom to the person in front of you. It worked a bit like this previously, but I expanded the scope significantly since the last demo.
In the original Sierra adventures, ‘look’ gave a general description of the room. With the contextual stuff, this now describes the object in front of you (or the room, if none). This seems like it was a lot more natural for people. I used my typing hint system to train people to type ‘look around’ to get a description of the room. Seemed to work well, so I think I’ll keep it.
I also added an in-game hint system, but very few people found it.
Some new changes
This time, there are a lot fewer big changes I needed to make in response to feedback. So that’s good.
I’m trying to formalize the conversation system a bit more, to make it seem like the characters are a little more alive. You’ll be able to ask them about topics and such (e.g. ‘ask about paper’) – this already worked in a limited fashion, but I’m making a bit more data-driven so it’s easy to add different responses to queries on various topics.
I may also add a way to identify the objects in front of you, or show hotspots. This might be part of some “casual mode” for more beginner players.
I need to make changes to the in-game hint system I implemented, because very few people found it. However, I felt it was maybe even a bit unnecessary, since I added a lot of signposting for the puzzle solutions (making the game a little less difficult).
I added a secret death scene that I think only one person found. It made me very happy when they did, though.
I talked to lots of people who were excited about adventure games!
Posted on June 29, 2017
At the Seattle Retro Games Expo on June 17-18, I demoed Cascade Quest to the public for the first time. It was a great experience, and a great way to get feedback from a subset of the gaming population.
I didn’t have time to commission a nice poster for the booth, so I just ended up using a large logo mounted to cardboard. The booth looks fairly cobbled together, but it was about as professional as other nearby booths, so I didn’t feel too bad. I also spent a day or two coming up with a demo mode of the game that plays through about 4 minutes of gameplay in various screens. This was probably better for attracting attention than any poster would have been. It shows lots of the text input, which I think was important.
I had two laptops set up (a good idea, since the power kept being cut the first day), one with a TV attached to it. I placed newsletter signup sheets on either side, and also had a signup form directly in the game (if you make it out of the ranger station, at which point you’re probably reasonably invested?). The in-game form ended up having slightly more signups.
How did folks react?
Even though this was a retro game expo, it is mainly focused on the 90s and early 2000s, and heavily focused on Nintendo and Sega. So I was a bit worried that it wouldn’t be the right demographic for Cascade Quest. That may have been true, but I still had a fairly steady stream of people playing the game both days. Most people who played (maybe two thirds) had played the old Sierra games before and were immediately intrigued by the similarity. For some, it was a completely new experience.
I would say the demographics for people who played my game tended towards older. They may not have been the majority of people attending the expo, but there were enough of them that it kept me pretty busy. I also had several small children (under 10? I’m not good at guessing children’s ages) play the game and enjoy it. Some were young enough that they didn’t really understand what was going on (nor were they able to progress very far), but still seemed to enjoy typing things in and seeing the responses. I did have one young girl play the demo to completion (solving most of the puzzles herself) over the course of about two hours.
The good news is that I got overwhelmingly positive feedback about the game. However, I think most folks aren’t likely to give any negative feedback (critical feedback seemed to only come from other game developers). If someone doesn’t like it, they just play for a few minutes and leave (this happened a few times). But the vast majority of people played for a while and said they really enjoyed it.
Some folks were very excited, and told me I had really captured the nostalgic feel of the old Sierra games – but without the design problems they suffered from. This was really nice to hear. Many people didn’t even realize that the graphics weren’t actually EGA – they just looked that way (also my intention). And I got plenty of compliments on the graphics, which is nice for this programmer to hear.
I learned not to judge people’s enjoyment of the game by watching their expressions. Some had constant looks of confusion, anger, or frustration – but in the end said they really enjoyed it. Of course, it was nice to have people sit down and instantly start giggling at the responses the game was giving to their actions.
One person suggested (only half-jokingly) I include an object early in the game that – if you fail to pick it up – prevents you from finishing the game (of course, you don’t realize this until the end). While I am explicitly designing away those issues, this highlights the importance of keeping a good amount of nostalgia in there to appeal to older players. In that same vein, one person asked “can you die in the game?”, and was really happy to hear that deaths are indeed possible.
The only analytics I had in the demo were tracking the players’ typed commands, and tracking whether or not the game reverted to a default response (thereby indicating it wasn’t understood). For future expos I want to be able to replay the players’ movements more completely so I can better understand what they’re trying to do.
My one regret is that I didn’t spend more time watching players play the game. I would walk behind them every once in a while to make sure they weren’t running into bugs, but in general I tried not to bother them. As a result, I don’t feel I got a good enough picture of what issues people ran into.
It was painful at time watching people struggle with the puzzles. However, I began to realize that they should be struggling. In the end, those that made it to the end of the demo (or close to it) took about the time I had expected… roughly two hours. I want people to struggle, but I also want them to feel satisfied when they solve a puzzle (and say to themselves “ah, that’s so obvious, why didn’t I think of that?”). That’s a hard thing to gauge at a broad public demo like this though.
For some reason, players had a much harder time navigating the puzzles on Sunday than on Saturday. I’m not sure if this was just a random thing, or whether there was a difference in demographics (perhaps more hard-code players visited on the first day?)
People ran into a number of bugs with the demo. This told me that my object interaction architecture (which I rewrote several months ago) isn’t robust enough.
Many people praised the text parser autosuggest, but didn’t realize they could use it to actually complete words (they thought it was simply a reference for which words would be accepted). I probably need to tweak the UI a bit to make it behave more like a modern browser search box, rather than a programmer command prompt.
One thing that definitely stood out was that people expected “look” to describe the object in front of the player. Instead, a simple “look” is wired up to give the room description (like Sierra did), and you have to name the object to get a description of it. I do have the ability for many verbs (like “take” or “open”) to operate nounless, based on what’s in front of the player. It seems “look” needs to work that way too. I originally resisted this, because I didn’t want players to have to type “look around” to get a general description of a room. But the evidence was so strong that “look” needs to refer to a specific object that I will have to revisit this.
Some people thought the parser was more intelligent than it was, e.g. “tell my boss that I don’t know where to find paper”.
Other people thought it was dumber than it was. They would type “give copies boss” instead of “give copies to boss”. The problem with this is that the parser interprets the former as “give the boss to the copies” – that is, boss is the direct object, and copies is the indirect object (and we fail to get a valid response). As a result, I will probably lessen the restriction on the grammar and ignore the difference between direct object and indirect object for most scenarios (I struggle to see a situation in which both would be accepted and perform different but valid things).
Appealing to a broad demographic
I would really like to find a way to make the game appeal to a broader demographic than just those who enjoy the nostalgia. I had some success with this, but I think it’s still not accessible enough to modern gamers.
Further data to corroborate this is the ratings that Void Quest has been receiving. A lot of people really like it, but some people really hate it (a relatively high number of 1-star reviews). Unfortunately people who hate it tend not to leave comments on what they don’t like or what turned them off. So I’m basically left guessing. I have to assume that either:
- the text parser turned them off and they didn’t realize that the game they were downloading wasn’t a point-and-click
- the puzzles were too hard
I did have some people at Seattle Retro Game Expo be suspicious of the text parser at first, but then gradually get into the game once they got the hang of it. So part of it may be convincing people to keep playing long enough.
I’m currently in the process of refactoring some of Cascade Quest’s object interaction code to make it more robust and predictable. I’ll also integrate some of the puzzle feedback I got, and then begin work again on the other parts of the game.
Posted on June 27, 2017
A number of people are stuck on my Adventure Jam 2017 entry, Void Quest. The puzzles are pretty hard, especially if you haven’t played these kinds of games before. I tried to signpost the puzzles well, but I may have failed at times. Or maybe you don’t have tons of free time to walk around and figure things out! So without further ado, here are some hints. Highlight/select each bullet point to get ever more “spoilery ” hints about a problem.
How do I get the story started!?
- How do I get the story started!? Take that todo list on your wall!
How do I change from day to night?
- Go to bed (assuming you don’t have anything to do, and aren’t stressed out by anything)
I want a listening device!
- You were gonna get one via mail order, but you procrastinated. Look around your cabin.
I want something to see in the hole!
- Well, your lantern might help a bit. But…
- You really want something better. You were so distraught after the incident with fido and the tree, that you failed to notice something on that darned tree. Easier to see in daylight!
- I wonder who put that there? Maybe they had a way to view pictures too.
How do I carefully lower things down the hole to explore it?
- There’s another thing that has stuff lowered into it. Parts of it are broken, could be useful.
- It’s the crank on the well! And it fits over the hole.
- You’ll also need something under your front porch.
- It’s a spool of fishing line! Look at the crank and the spool in your inventory.
I need a stamp!
- Check your mailbox.
- Postcard. And it didn’t get postmarked!
- Remove it from the postcard. Carefully…
- With some heat.
- By heating your iron on the stove.
I need some cash!
- Did you find a note from your neighbor?
- The note blew into the stump you tried to pull.
- Who stole the cash? Where is this squeaky thief now?
- The thief has a little hole on the top right of his screen. It’s in there.
How do I get gas for the truck?
- Make sure you have performed your other tasks first.
- Have you lowered both a mic and camera? Did something happen afterwards?
- One of those things that “happened” might be the tractor’s gas tank. It landed nearby.
- Might be easier to find at night. What was attracted to the colors on the tractor?
- THE ANSWER:
- The fireflies show where it is in your front yard at night.
How do I get the truck keys?
- They’re on the same screen as the truck.
- At night is a good time to see them with your lantern.
- They’re in the well. Oh, but they’re just out of reach.
- They float (wooden keychain). Change the water level in the well?
- You’ve got a container that can carry liquids, don’t you?
Some other questions:
What’s that little mouse for?
- It’s leading you to something. Might make more sense if you found your neighbor’s note. Which blew onto your property…
- into your stump.
What are those black shapes I see sometimes at night?
- I dunno… it’s a mystery?
That pile of rocks? Any purpose?
- It’s just fun to throw things in the hole I guess.
Is there any purpose to the fireflies?
- Yup. They subtley hint at something which is important later in the game.
What’s the deal with the hole? Who made it? What’s in there?
- Haha, nice try. What’s in there? Well, (most of) your tractor, for starters. Maybe you’ll have to play Cascade Quest to find out more.
Posted on May 23, 2017
Cascade Quest uses a fixed palette that is based of the 16 default EGA colors. The 16-color Sierra games used dithering to give the appearance of more than 16 colors. Given the relatively blurry monitors and TVs of the day, this produced a more convincing effect than it does on modern crisp LCD monitors. As a result, Cascade Quest uses the “undithered” versions of these dithered colors. That is, each base color blended with every other base color. This results in the palette on the left below. Note that due to duplicates, the result is 136 unique colors (16 + 15 + 14 + …. + 1), not 256.
Using any limited-color palette (with arbitrary colors like VGA, or fixed colors like mine) presents some pretty significant limitations. The most obvious is that blending two colors together becomes much more difficult. We need to take the two RGB values, combine them, and then map this color back to the closest matching color in our palette.
The brute force method for palette lookup is to calculate the euclidean distance between the reference color and each other entry in the palette. The closest one wins.
This is brute force approach is fine to do in the case of a simple remap of each palette index to another – for instance, Cascade Quest does this to support automatically darkening (for sprites in shadow) or converting sprites and backgrounds to a certain lighting setup. These are fixed “remaps” that only need to be calculated once at startup.
However, it is much too slow to do on a per-pixel basis. We need per-pixel blending to support high quality scaling and pseudo alpha-blending. Below is an example of a mushroom. The original image is on the left. In the middle is what happens when you scale it to 73% of the original size using nearest neighbor sampling. This is what Sierra’s SCI1+ engines used for scaling, since it’s quick and you are only ever dealing with existing colors in your palette (SCI0 did not support scaling). On the right is a version that is scaled using bilinear filtering, with the resulting colors remapped to the palette.
You could argue that the middle mushroom is more true to the retro aesthetic (but the right-hand one is clearly a more accurate representation of the mushroom). However, look what happens when you have an image with high frequency details:
All the bars have gone missing in the center image, since the nearest neighbor sampling at that zoom level ended up sampling only the white pixels. On the right is the result using bilinear filtering and remapping to the palette. When the scaling level changes smoothly in game, the problems with nearest neighbor sampling become even more distracting.
How can we more quickly map an arbitrary RGB value to a palette index? We could generate a lookup table. However, to do this accurately, we’d need an entry for every possible color: more than 16 million of them! For optimum speed, whatever lookup table we use needs to be small enough to fit well within the processor’s L1 data cache so that memory access doesn’t become a bottleneck. A 16 megabyte array doesn’t fit the bill.
We could of course just make the grid coarser. Instead of 256 values for each RGB component, we use quantize to 16 or 10 or whatever. Once we do this however, we start to significant amounts of incorrect results.
We can perhaps produce more intelligent quantization. If we look at all our palette colors, the combinations are such that there end up only being 10 unique values for each RGB component (in fact, the same ones for each component). This is a property of the source colors we are using, and wouldn’t be true for an arbitrary palette of course.
In the above image, I’ve drawn in grey lines halfway between the 10 possible component values. We can define buckets between each of the grey lines (a total of 10 buckets). Those buckets have the property that any component value that falls within them will have the contained value (black line) as the closest color component. So we end up with two lookup tables. One (of size 256) maps a single RGB component to a bucket. And the other (of size bucketCount ^ 3, or 1000 in this case) maps the three buckets (R, G, B) to an actual palette index. Of course, we also have to do a one time pass to calculate the closest palette color for each bucket.
So, this would actually work perfectly if our palette contained every combination of those 10 discrete values (10 * 10 * 10 = 1000 palette entries) – but it of course doesn’t. As a result, it falls apart quite spectacularly. We can precalculate the nearest palette index for a particular (R, G, B) bucket, but the actual nearest color to any arbitrary (R, G, B) value in that bucket might be different (again, this wouldn’t be an issue if our palette had every combination of our 10 discrete component values).
I thought it would be useful to visualize my palette’s color distribution, so I put together a quick Unity scene that shows the RGB color space and where each palette value is within it.
Looking at a 2d cross section also helps. Here’s looking along the blue axis (thus we see the red and green distribution:
Getting it done
Once I realized that a perfect solution to this problem is probably not possible, I set out to do the best I could. To get more concrete results, I came up with a random test corpus. 1000 randomly chosen colors. They aren’t completely random though. Instead, they are random blends of two randomly chosen palette colors. This results in a color corpus that is closer to what might be actually used in the game for blending operations.
For this corpus, the bucket method described above resulted in 349 of the 1000 colors being incorrectly matched. Pretty bad – in fact, barely better than using an even distribution. I tried doubling the resolution of the buckets. That improved the wrong matches to 215 out of 1000. But at the expense of a bucket lookup table of 8000 entries (20 * 20 * 20).
I then tried some even distributions. Everything from 10 evenly distributed buckets to 20. While generally using more buckets reduced the number of mismatches, the relationship wasn’t monotonically increasing. Fifteen buckets proved to be good bang for the buck (et) – 234 mismatches, which ended up being fewer than with 16 or 17 buckets.
Still sure that a specific bucket distribution could produce better results, I threw some computational power at the problem. I tried lots of slight variations for the boundaries on each bucket. This ended up in finding a bucket distribution that results in only 149 mismatches out of the 1000 color corpus. Not too shabby.
So the approach I’m using (for now), maps each RGB component to a bucket index. Then the bucket index is used to index in the main lookup array (15 * 15 * 15 = 3375 entries).
Where is this used
I use this quick lookup for alpha blending and bilinear filtering during scaling. However, I use the slower accurate version for the global color remapping, since this is only done once at startup and only needs to evaluate 136 unique palette colors.
I timed the drawing performance of the above outhouse alpha blending cycle. The frame draw times for the outhouse were:
- Alpha blended, using the slow (correct) palette lookup: 13.3ms
- Alpha blended using the quick lookup: 0.3ms (44 times faster)
- No blending: 0.03ms.
I also tried seeing if the size of the lookup table made a difference. That is, 10 buckets instead of 15. It did not (though I’m sure increasing the number of buckets would start to slow things down).
A note on gamma-correctness
I’ve been ignoring this throughout this post, but it’s an important bit to touch upon. The base EGA colors are in gamma-corrected space. That is, those values are exactly what is displayed on the monitor. To properly combine them (to get our 136 color palette) so that they look just like the dithered colors, we need to convert them to linear, take the average of the two, and then convert back to gamma space. If we don’t do this, things will look significantly different.
Now, this should also be done when blending. I don’t do this (I just directly blend colors in gamma space) for a few reasons though:
- It would be slower, since we need to do the conversion to linear and back for every pixel
- The resulting colors will already be wrong anyway, since we’re mapping to our fixed palette. So the “perfection” won’t be noticeable.
- Older graphics hardware has gotten away with not doing this for texture filtering (although in general modern graphics hardware does this properly).
Posted on May 22, 2017
Over the two weeks from May 5 to May 19, I worked on an entry for Adventure Jam 2017. Leary of taking two weeks off from working on Cascade Quest, I decided I could use the time to:
- Explore the backstory for one of the characters in Cascade Quest, and maybe unblock some creative hurdles
- Use this opportunity to get some feedback on the input system and style of interactions in Cascade Quest
- Flesh out some of the workflow issues and weak points I have with my engine.
To this effect, I used the same engine (built on top of Unity) that I use for CQ. Over the course of the jam, I made a bunch of improvements (or maybe let’s call them changes) to the engine to support new features.
I decided to do pretty much all the coding and puzzles first, and the art last. That turned out to be a good way (for me at least). I find art to be more predictable in terms of the time taken. This does, however, mean that for the first 10 days of the jam the game didn’t really feel like a game. Everything was so ugly to look at that it takes a lot of imagination to envision what the end the product will be. Luckily for a short project like this, motivation doesn’t really wane anyway though.
I took a lot of shortcuts in order to get the amount of content in that I wanted. A few that I can recall:
- The player can climb over a fence as a quick way to another room. Instead of animating this, I just wipe to black and wipe to the new screen.
- There’s a pickup truck whose engine needs to be started. To avoid having to draw door animations, I placed it so the driver side door was behind the truck as seen from the viewer (and I think I claimed it was missing, to avoid having to deal with “open/close” interactions).
- There’s a scene where you bury something. Again, instead of animating this, I fade to black and just do sound effects.
- A number of objects you have to pick up have no visible onscreen representation. They are hidden in holes, in vehicles, and so on.
- I use a very constrained view of things, so that, for example, you never see your full cabin from the outside.
Issues with the parser and object interactions
I plan to do a post about this specific to Cascade Quest soon, but some of the suspicions I had about flaws in the way I’m doing things became more real.
I am trying to come up with a clean and easy way to map text parser input to verb-noun-noun. A while ago I changed from this kind of code:
(if (Said 'take/paper') ; give the paper to the player, etc... )
to a more data-driven approach, where “features” are present in the current room, and have a noun associated with them, and then a “doVerb” method that handles various actions on them. Then there is a more generic piece of code that inspects them and figures out the right feature whose doVerb should be called. This more like how a point-and-click game would be structured.
It gets complicated of course, because “use gas can on truck” should map to “use-can-truck”. But so should “fill truck” (if you have the gas can). So features need a way to cleanly customize the way they map text input to verb-noun-noun. And it gets even more complicated when you have multiple objects with the same name. And even more complicated when you have inventory items that also have in-room representations.
This topic is too large for this port-mortem, but I’ll make a separate more detailed post about it.
Improvements to the engine
I made a number of improvements:
- I now support per-pixel palette remapping. This is how I manage to get the glow around the player when they hold the lantern at night. The source sprites and background are all in normal color – but then the global palette is switched to a dark blue one for the nighttime, and then the glow sprite uses a pinkish palette just where it is
- Related to the above, sprites can disable drawing to the visual screen, and simply draw to the “per pixel palette index” screen to cause the palette to change in an area.
- I support post-processing effects (really I should just move everything to the GPU, but…). Originally I was planning to use this to make it rain, but I didn’t end up putting that in. The only place it’s used is for the thing that comes out of the hole during the first nighttime.
Here I’ll just go over the backgrounds in the game, with my own comments on what could be improved (if you’re any kind of artist, I would love feedback in the comments).
A problem with many of my backgrounds in this game is the blankness of the ground. I need to have more texture and more variety (granted, this is the background without the sprites). I’m also not quite satisfied with the fence, it perhaps need to have higher contrast.
I didn’t quite know what to put in the background behind the bushes. Originally it was a cliff, but I ended up changing it to abstract trees, because I thought I could make them look scarier at night.
I do like the moss on the roof.
Next up is the room to the south of this:
I like the framing I have here. The orange leaves and the tree silhouettes. I used jagged sharp edges for the silhouettes to try to give them a creepier look (especially at night!). I drew this one second, and started to be more intentional about this.
I’m not happy with the tire tracks, but my limited palette was perhaps hurting me here. And again, the blank ground.
The burned house, I dunno. I needed to distinguish the wood planks, and that’s the darkest grey I have, so that’s really all I could use.
Next up is the front of your cabin:
Again, I used the orange leaves and menacing silhouettes to frame the scene. I needed some variety for the background – so not just bushes, but cliffs (they were also helpful for the story). I’m reasonably happy with the cliffs. I referenced Pedro Medeiros’ wonderful pixel art tutorials for these. Probably the moss in the cracks could have been done better.
Again, the blank plain grass and the relatively featureless tire tracks.
One thing I’m not happy about is the cabin. I think placing it straight on was not a good idea (although it made the door art easier). It would look more interesting if it were slightly off angle.
Finally, the cabin interior. I did this with less than 24 hours left in the jam. At first I wasn’t quite feeling it, then I got into a nice zone where I was really enjoying the art.
I was a little un-enthused when it looked like this to start with:
This was probably an hour’s worth of work, at least. Then I spent just a few minutes adding some “defects” to the walls and floors to break up the straight lines. Basically just drawing some black lines:
Suddenly, so much better! After doing this, I did a bit of the same to the cabin exteriors in the other backgrounds.
And with all the props:
My goal was to get a playtestable build ready by Wednesday (the 12th day of the jam), but that didn’t happen until Thursday. I managed to get two people to play (in total) about 2.5 hours of the game. This was absolutely essential and totally worth the distractions. I only wish I had gotten more people to play it. Even a simple parser-based game really needs a variety of people to try it to understand how different people will word things differently. As a result, the game is still fairly unpolished when it comes to accepting input. This kind of sucks, as one thing I want Cascade Quest to be good at is avoiding any “guess the word” issues.
I decided to build a WebGL version, since that gets more people playing the game. There were a few issues however. One, which I’ve encountered before, is that Unity puts two backspaces characters into the Input.inputString every time the backspace key is pressed and released. This messed up the text input, obviously.
Posted on May 10, 2017
In the old days, Sierra was brutal when it came to save games. Death was around every corner, and it was up to the player to remember to manually save progress. Although Cascade Quest still has a good amount of death, that kind of unforgiving behavior isn’t really acceptable these days. On top of that, save games must survive patching – another complexity that wasn’t really an issue back in the day.
Cascade Quest allows saving your game at almost any point. When this is done, the modifiable state of the world is serialized and saved to disk. This includes all global variables, all script variables, and all objects and their properties.
Changes to the game code can easily invalidate save games. Objects may be added or deleted, variables may be added or removed, and so on. Trying to reload an old save game with a changed codebase would create all sorts of problems.
Luckily, the way the game engine is set up, there is a limited amount of information that gets passed from room to room. When a new room is entered, all stateful scripts are unloaded, and then reloaded on demand. So the only state that gets transferred is the set of global variables. The new room is responsible for instantiating all objects that are relevant based on the global state.
And even among the global state, there is only a portion of that that is relevant persisted state – basically a set of global flags and numbers (that indicate if certain events have happened in the game, for instances).
Upon entering a room, this global state can be saved off somewhere. Then when the player saves a game, this data is saved alongside the rest of the save game data. If, when loading a save game, we see that it was for a different version of the game, we can instead load the global state that defined the game when the player entered the current room. The downside is that this puts the player back to when they entered the room (some UI will be needed to explain this, no doubt).
This is also a trivial way to implement autosave. Upon changing rooms, we save off the above-mentioned global state as the autosave. This does mean we need to be careful sometimes: there are cases when a player’s death is certain when they enter a room. In those cases, logic is needed at room startup to detect this and disable the autosave. These cases are rare, however.
Using that limited set of global state doesn’t completely absolve us of thinking about codebase changes affecting save games of course. Patches to the game can’t ever remove any global state – we can only add new flags or variables. So care must still be taken to have the new code correctly interpret the old code’s state. But a set of flags and variables is much easier to manage than an entire heap of instantiated objects and other state.