When I started the project, there were three main things I established right away.
- It would be a Megami Tensei style dungeon-crawler, featuring SMT demons as enemies. (This eliminated the need for me to make a ton of custom enemy sprites, although I did have to arrange them into a format that worked for AAO.) You would use controls on the bottom screen to navigate.
- To tie the horror theme to the gameplay, there would be a part where a powerful enemy appears on the map outside of battle and chases you around. I decided early on that this would be an eyeball, which I modeled in blender.
- There would be multiple floors and they would be implemented in such a way that the maps (or at least some of them) could be procedurally generated.
As anyone who played it might be able to tell, number 3 got cut and never made it in entirely. It wasn't because it was impossible, but it would have taken a long time and I just didn't have enough time, so I went with manually designed levels, which were much easier to manage.
Once I had these things established, I created a main screen.
This is when I decided on Candy as a currency/score metric for the game. As you can see, the HP was originally going to be signified by Hearts, but in the end, I went with a meter. The meter is just composed of a bunch of the same image of a thin white block lined up next to each other.
I also drew all the walls. Every wall is its own image, and the game logic controls when each wall should be visible (which I'll get into later). All together, not including doors and stairs and such, the walls look like this:
I decided that seeing three tiles away was enough, as more than that would have gotten out of hand pretty quickly. If every wall is its own image, making one more tile back visible would have added at least 9 more images, and the screen would have gotten really crowded:
The map was the first big puzzle I had to figure out. Map data isn't very complicated at the bare bones. All you really need is a grid of information for each tile, so the game knows whether it's a wall, or whether it's a walkable tile. But how in AAO could you store data on a grid of information? Theoretically, you could create a frame for every possible tile you could stand on, viewed from every possible direction, on every floor. But that would have been ridiculous and probably would have sent my frame count into the hextuple digit territory. I had to make it dynamic, as data that could be read in real time by the game. If you're familiar with programming, your first thought might be a 2D array (basically just an organized numbered list of variables). But AAO doesn't have arrays. So I was stuck, I thought, with somehow storing the info I needed in a single variable.
My first thought was to use binary. By storing the map data as a series of 1s and 0s, (1s being walkable tiles and 0s being walls), I could store a large amount of information in a simple number. For example, let's say I have a map that looks like this:
I could remove the linebreaks and store that as this number: 1111100001011110100001111. But here's the problem. AAO can't store numbers that long in its variables. I can't recall the exact limit, but I tested it and found that you could only have so many digits before AAO would start cutting them off or breaking. So that plan didn't work. Then I thought "What if I convert that binary number to decimal?" That same long string of 1s and 0s could be represented as the number 32554255 in decimal. I spent a while trying to create a system of converting numbers from binary to decimal and back in AAO, but then I realized the major flaw with this plan. The decimal number is useless to me if I actually want to get information out of it. Somehow I would need to convert that decimal number back to binary, which would mean I would need that long 25 digit (or 100 digit in the case of real maps in the game) number again. And there was another problem as well. 1s and 0s are all well and good when it comes to walls and floors, but what about other things, like an event tile where I want a cutscene to occur, or a stairway down to another floor, or a door? 1 and 0 weren't enough to convey that information. So I went back to the drawing board. Binary wasn't going to work.
The solution I came up with was crude, but it worked. I needed to store information on a 10x10 grid of tiles for each floor. That's 100 tiles at a time. So I just made 100 variables. map0, map1, map2 ... all the way to map99. Each variable had one of the following values:
Code: Select all
3: Locked Door
4: Stairs Up
5: Stairs Down
A-Z: Various event tiles that do different things depending on which floor they're on.
But this came with its own problem. As a person with eyeballs, I can read the variable names and know which variable corresponds to which tile on the map. But how would the game know that? For example, let's say the player is at position 31, and they're facing east. The tile in front of them should be tile 32. How can I know to grab the value of map32 to check to see if it's a wall or a walkable tile? Here's where we start getting into some advanced AAO wizardry. When you use the "test an expression's value" action in AAO, in the advanced mode, there is a checkbox you can check under "Name of the variable to test". This lets you use variables to figure out the name of the variable you want to look at. If the player's position was stored in a variable called "p_pos", then I could use the following code to get the map variable containing the tile to the right of that player:
The dot in that line causes you to "concatenate" (meaning that you're taking the number you get from p_pos + 1, and appending it to the end of the word 'map' so that the game reads it as 'map32'). This trick was a lifesaver and was used extensively throughout Halloween Hero, but it didn't work all the time. (More on that later.)
Even still, that on its own isn't enough. We've still got another problem. I want the tile in front of the player, but how do I know the tile in front of them is their current position + 1? That only works when they're facing east (and not when they're facing the edge of the map, such as in position 39. 39 + 1 would be 40, which would be a tile on the complete other side of the map). Look at this visual arrangement of map positions to see what I mean:
Code: Select all
So first I had to figure out which way the player was facing, which was easy enough to store in a variable, which I called "p_dir". It could either be N, E, S, or W. Let's say the player is at position 21. If the player is facing North, then the tile in front of them would be 11, which means you just need to subtract 10. Pretty simple. If they're facing East, you add 1, if they're facing west, you subtract 1, and if they're facing south, you add 10. Then I just need to take into account whether the player is facing the edge of the map or not. If they're facing west, and they're in the 0s column, then you don't want to check -1, because then you'd get a tile in the 9s column on the other side of the map. Likewise, if we're in the top row and facing north, we can't subtract 10, or we'd get a number less than 10 that's not even on the map. So taking all of this into account, I came up with the following formula:
Code: Select all
((p_dir = 'N' & (p_pos > 9)) & 'map' . (p_pos - 10)) | ((p_dir = 'E' & (p_pos % 10) < 9) & 'map' . (p_pos + 1)) | ((p_dir = 'S' & (p_pos < 90)) & 'map' . (p_pos + 10)) | ((p_dir = 'W' & (p_pos % 10) > 0) & 'map' . (p_pos - 1)) | alwayszero
This probably looks like absolute gibberish to most people, but it's not too hard to read once you understand one little AAO trick, as well as some programming symbols. This one is thanks to Ferdielance as well as Enthalpy's v6 Variable Guide
This is basically a fancy way of making an "if" statement (for example, "if there are clouds in the sky, it's going to rain, or else it won't rain"), and unfortunately the only way in AAO to do so in one line without using a whole bunch of frames. Here's how it works:
Code: Select all
((clouds are in the sky) & 'Its gonna rain') | 'No rain'
The game hits this line, and it's trying to figure out whether it's going to rain or not. First off we have a "|" symbol separating multiple possible outcomes. This symbol means "OR". Either 'Its gonna rain' OR 'No rain'. But using the "&" (AND) symbol, we've added a condition to 'Its gonna rain' so that outcome will only happen if 'clouds are in the sky'. On the other hand, 'No rain' doesn't have any conditions attached to it. So if there are no clouds in the sky, this statement is going to choose the outcome that doesn't require there to be clouds in the sky, hence, 'No rain'.
Going back to the formula with this in mind, the game is trying to figure out the name of the variable we want it to read, remember? We want the tile in front of the player. So our first outcome is:
((p_dir = 'N' & (p_pos > 9)) & 'map' . (p_pos - 10))
In other words, if the player's direction is North AND, if their position is greater than 9, then return 'map' . (p_pos - 10). If those conditions aren't met, it moves onto the next possible outcome:
((p_dir = 'E' & (p_pos % 10) < 9) & 'map' . (p_pos + 1))
If the player's direction is East, AND if the remainder (that's the % symbol) of their position divided by 10 is less than 9 (which means they're not in a column ending in 9), then return 'map' . (p_pos + 1).
It then goes through all the other possible outcomes and their conditions, until it gets to the final position where if none of these conditions are true (which means we're checking a tile that's outside of the map, which we want to return 0 as a wall, so players can't walk off the map), it returns a variable called "alwayszero" which I set at the beginning of the game to always have a value of 0. We can't just put the value 0 there, because we're looking for a variable name to read.
So with that, we've got a map, and we can read its data and do stuff with it. But there is one wrinkle. Dynamically generating the name of a variable to read only works in a few circumstances. When you're in "Define new variables", "Test an expression's value", or "Evaluate conditions to redirect player." You need that checkbox that lets you use an expression to create the expression that's going to be read. You can't do this, say, in "Reveal/Hide an object in a place". That's going to be a problem, as you'll see in this next part.
Actually Displaying the Map
The core of the game's ability to display things comes from using "objects" in places. You know, like when you add a picture of a piece of evidence to a crime scene. Well, my entire top screen is basically one crime scene, and every single possible object that could be displayed on the screen is an object. Here's what they look like with most of them displaying at once:
There are a total of 36 background objects (representing the walls and 3D parts of the screen), and a total of 763 foreground objects (representing the mini-map, all the possible numbers on the candy counter, as well as the HP bar). The vast majority of those objects are minimap images. Because for every single possible icon that can appear on the map (floors, stairs up, stairs down, doors, eyeballs, and black squares to cover spaces that haven't been seen yet), that's another 100 objects. There's no ability in AAO to move objects, so every single image needs to be there in every single possible position. No way around that, unfortunately.
But if I actually tried to use AAO as it was intended and used "Evaluate conditions to redirect player" frames to go to various frames and hide or show various objects on the screen every time I needed to (so every time the player moves), the game would have been slow as molasses. So the trick I use to try to show or hide objects in as few frames as possible is the one listed above.
First off, I added a background object and foreground object that's just a transparent image called BLANK. Then, for every single possible object that could be displayed, I wrote out the conditions under which they should be displayed in one frame, and the conditions they should be hidden in another frame. For example, here is the formula for drawing the first tile of the minimap, which is the first foreground object I added, so it has an ID of 1, and corresponds to the variable map0.
Code: Select all
((map0 = '1' | map0 = 'A' | map0 = 'B' | map0 = 'C' | map0 = 'D' | map0 = 'E' | map0 = 'F' | map0 = 'G' | map0 = 'H' | map0 = 'I' | map0 = 'J' | map0 = 'K' | map0 = 'L' | map0 = 'M' | map0 = 'N' | map0 = 'O' | map0 = 'P' | map0 = 'Q' | map0 = 'R' | map0 = 'S' | map0 = 'T' | map0 = 'U' | map0 = 'V' | map0 = 'W' | map0 = 'X' | map0 = 'Y' | map0 = 'Z') & 1) | 604
Basically what this code is saying is "If map0 has a value of either "1" or any of A through Z, then we can assume that the tile at position 0 is walkable, and should be displayed on the minimap, so return the ID "1" as the object to display. If not, then it's not walkable, so we don't want it to display, so we'll instead return "604" which is the ID of the "BLANK" image I added earlier. It will reveal the blank image, but the player won't see it, so it won't matter.
Unfortunately for me, "map0" is entered manually here. Because this is an expression that's determining which ID of object to reveal, and not an expression that determines which variable name to read, we can't use the trick from earlier. We have to enter the right variable name manually. So for tiles 2 and 3, I entered this:
Code: Select all
((map1 = '1' | map1 = 'A' | map1 = 'B' | map1 = 'C' | map1 = 'D' | map1 = 'E' | map1 = 'F' | map1 = 'G' | map1 = 'H' | map1 = 'I' | map1 = 'J' | map1 = 'K' | map1 = 'L' | map1 = 'M' | map1 = 'N' | map1 = 'O' | map1 = 'P' | map1 = 'Q' | map1 = 'R' | map1 = 'S' | map1 = 'T' | map1 = 'U' | map1 = 'V' | map1 = 'W' | map1 = 'X' | map1 = 'Y' | map1 = 'Z') & 2) | 604
((map2 = '1' | map2 = 'A' | map2 = 'B' | map2 = 'C' | map2 = 'D' | map2 = 'E' | map2 = 'F' | map2 = 'G' | map2 = 'H' | map2 = 'I' | map2 = 'J' | map2 = 'K' | map2 = 'L' | map2 = 'M' | map2 = 'N' | map2 = 'O' | map2 = 'P' | map2 = 'Q' | map2 = 'R' | map2 = 'S' | map2 = 'T' | map2 = 'U' | map2 = 'V' | map2 = 'W' | map2 = 'X' | map2 = 'Y' | map2 = 'Z') & 3) | 604
And I continued this all the way to tile 100.
Did I enter all of these things manually? Well, I very well might have done so if it weren't a few very severe inconveniences. Number 1, you can't just press tab to move to the next field, you need to use the mouse to click on the field or else your browser will think you're tabbing into other AAO fields from outside the action window. And number 2, it wasn't just the object ID field that needed to be filled out. For every single one, all of these fields had to be filled out:
And in particular, the field that says 'foreground_objects' by default doesn't have single quotes around it, which will
break the game. The alternative would be not using advanced mode and manually using the mouse to select the correct map every single time, which would have been even more of a nightmare. So I needed to go in and add those single quotes manually. I am not nearly
enough of a masochist to do all of that by hand. So I did what any programmer would do, and found a way to automate it.
I downloaded autohotkey and wrote up scripts to automatically enter the fields for me. This ranged from anywhere between 15-minutes to an hour of my computer being unusable as it moved the mouse on its own to click on fields and type up their contents. But believe it or not, displaying the minimap was the easy part. Where it got really tricky was displaying the walls and 3D objects. You'd think the minimap would take longer, with there being over 700 minimap objects and only 35ish wall objects, right?
Well, here's the issue. Because I can't use the trick from earlier to get the exact map variable I need for any given tile, I can't just use the same formula from earlier to determine whether the tile in front of the player is a wall and should be displayed as a wall. Or rather, I could
. but it would have taken way too many frames and been very slow. Instead, I needed to tackle it from a different direction. There are 100 possible map variables to check, but only 1 variable for the player's location. So here's an example of what I came up with:
Code: Select all
(((map0 = '0' | map0 = '2' | map0 = '3' ) & p_dir = 'N' & p_pos = 10) & 1) | (((map0 = '0' | map0 = '2' | map0 = '3' ) & p_dir = 'E' & p_pos = -1) & 1) | (((map0 = '0' | map0 = '2' | map0 = '3' ) & p_dir = 'S' & p_pos = -10) & 1) | (((map0 = '0' | map0 = '2' | map0 = '3' ) & p_dir = 'W' & p_pos = 1) & 1) | 19
This code is checking whether the tile directly in front of the player is a wall or not (if map0 is equal to either 0, 2, or 3, which all represent tiles in which a wall must be displayed, and if the player is standing in position 10 while facing North, and so on), in which case the first background image (id 1) representing a wall directly in front of you will be displayed. If not, then object 19, the id of the "BLANK" background image will be displayed instead.
The more astute of you may have noticed something horrifying, however. This formula says "map0" and specifies specific player positions as part of its conditions. The implications of this are exactly as you might imagine. For every single possible wall image that could be displayed, I wrote up a formula for each and every possible position that wall could be in (that being 100 possible positions), and then checked whether the player was in the correct position to see that wall. We're not talking about 35 formulas. We're talking about 3500.
There was literally no possible way I could ever hope to do this manually. Naturally, I automated it, but even the process of waiting for my computer to enter all of those formulas on its own was arduous and tedious. Each wall image would take hours to completely fill out, and after a certain point, my browser started running out of memory and crashing, causing me to have to start over. (Thankfully I saved every time I completed 100 of them, so it wasn't starting over completely from scratch) This happened at least 10 times, although I wasn't counting. I really
wanted to squeeze all of those formulas into 1 frame, but in the end, it wasn't possible. It was too much for my browser. I had to split it into 2 frames, so technically you might notice the doors and some other objects in the game appear 1 frame later than the rest of the walls. A small sacrifice, but one that needed to be made.
Finally, there was the matter of hiding parts of the minimap that you haven't walked on yet. This was surprisingly easy, although there was one unfortunate limitation. If I wanted to do it the correct way, I would have had to store each tile the player had walked on, on each floor. But in AAO, that would have meant 100 more variables for every single floor. In the end, I was already pushing it with the number of variables as it was, so I decided against that. Instead, I would just reveal the full map depending on whether you've cleared a floor or not, and then reset the map if you left the floor. I mean, you can't expect everything to go perfectly when AAO isn't designed for something like this.
So what I did was have black square objects cover the minimap, and revealed all of them in 1 frame, then hid specific ones based on the player's position whenever they moved. No variables required.