If you haven't had a chance to try the game, I'd suggest you get familiar with it first by clicking the link below. It's free. Please enjoy, and we'd love your feedback!
Going into the build, we knew the map UI and navigation mechanics were going to be the most difficult. We also spent the most time developing them. This is one of those cases where we knew the level of difficulty, realized we didn't have a clear path forward, and didn't just jump right into coding a solution. We really needed to draw out, by hand on a white-board, the exact way we were going to do this.
Map & Navigation Overview
Navigation is actually pretty complicated. Think about it. Navigation could use buttons, or mouse movement, or the keyboard, or all three; and what occurs will change depending on the location, direction, and interactions of the player. After many dead-end conversations about how to build the navigation system, we decided to actually draw out our dream UI, and determine how to build from there.
As we stated in Part 1, we weren't trying to advance the dungeon crawler genre here, so we took a lot inspiration from old-school dungeon crawler games. We were intrigued by Nintendo's Legend of Zelda and the PC game Mordor: The Depths of Dejenol. Both games used a grid system.
Zelda was much more advanced than Mordor. It allowed for movement within a particular grid map cell while holding a third-person perspective. It also allowed for several sub-locations within a particular grid map cell. So each grid map location had it's own grid map! For instance, you could be in the mountains of Hyrule (the world for the Legend of Zelda), blow up a part of the mountain and find a hidden location. Pretty cool! As much as we loved the idea, 100 hours of development was not going to cut it for something so complex. But we loved the grid map, and we loved the shop concept (more on that later).
Mordor was built with a first-person perspective but was much more primitive (surprisingly for a first-person game). It showed a grid map with an arrow in the direction you were facing and displayed an image of the direction you were looking in. So, if you turned right, it would turn you right on the grid map and display a different picture. Much more basic, but still engaging.
We decided on the Mordor navigational approach and map design. We were going to have custom maps, of many different sizes and shapes, with directional input. If you have played our game then you know this is NOT how our game works...
The old turn-based dungeon crawlers relied on a keyboard. An easy-to-play game would require using keys to turn the player and a single key (Up key) to move the player in the direction they wanted to go. This meant that we needed a way to track not only the cell the player occupied but the direction they faced on that cell. For a 100 cell map (what we have), that is 400 different possibilities. Not terrible, but graphically this was going be a TON of work, because we wanted to have good artwork of the locations within the game. Mordor's navigation was easy to do in this way because, frankly, it was graphically boring. There was just a dungeon and so the environment within the dungeon didn't change much. That meant that the graphics of each location and each direction of each location could be heavily recycled. But we wanted to take the player to a city! Halfway into architecting the map and navigational solution, we realized the hurdles and graphical assets that would be required to make it engaging so we went back to the white-board for round two...
We thought Mordor's approach to navigation was close to what we wanted but we needed to simplify. Why not take out the direction? A player could still explore the world but we could remove the first-person perspective. This seemed reasonable, and it removed a significant amount of complexity while allowing us to still create compelling artwork. Now each cell had one graphical asset only. That's it! We took a hard look at how the more recent throwback game A Dark Room used a grid map for navigation and drew some more inspiration from their very simple map.
Ok. So, what about the size of the map? At first, we went big. Really big. The original map was 25 x 25 for a total of 625 cell locations. Keep in mind, at this point, we were still contemplating 12-13 levels for our game. That would have come out to about 8,125 cell locations. LMFAO. We reduced the size of the map, not due to the difficulty of developing the graphical assets for such a large game world, but because we were struggled to construct a way to navigate the grid. I'll give an example.
Take a look at the grid below. If we are on cell location 60 and we triggered a move to the right, then we would want cell location 60 to no longer be highlighted. We would want cell location 61 to be highlighted instead. We always needed a reference point for the cell and so created an attribute just for the players location. Easy enough. If the player triggered a move to the right, that attribute would have 1 added to it. If the player triggered a move to the left, that attribute have -1 added to it. If they triggered a move up, the attribute would have -26 added to it, and finally if they triggered a move down, the attribute would have 26 added to it. This is a little bit wonky, as you can see, but it does the job.
The real struggle came when dealing with the borders of the map. How were we going to stop a player from going off the map? For instance, when a player is on cell location 8, and they trigger a move up, the result will be -18 which is no bueno. There is no cell -18... We could have limited it by any number between 0-25. Ok, that's doable. But what about if the player is on cell location 441 and triggers a move to the right? Or, for that matter, any cell location on the right border. Starting to see our problem here? There was no clear, mathematically simple, path to numerically check the cell and place limiters on player navigation.
Our clever solution, and we are quite proud of it, was to restrict the grid map to a 10 x 10, starting on 0. Let's take a look at the map below for reference.
In our new, simpler, grid map, cell location 25 is highlighted. If we go right, we add 1. If we go left, we add -1. Up we add -10 and down we add 10. Definitely simpler, but the kicker is at the borders. On the northern border we just look at the number of digits. If the cell location is a single digit number, then the player cannot move up. On the eastern border, if the cell location ends with a 9, they can't move right. On the western border, if the cell location ends with 0, they can't move left. Finally, on the southern border, if the cell location starts with a 9, they can't move down. Eureka!
We built an HTML table, spaced everything evenly, and threw an 'x' into each cell with an aura:if tag that only rendered the 'x' if the aura:attribute for the cell location was a match. We styled the table and we moved on.
This was the first big break on the navigation build and it gave us the confidence that we could build this thing. At this point, we had a single level with 100 cell locations. Respectable. Doable. High-fives all around.
We created a Custom Metadata Type called Level with a Number field. Then we created a Custom Metadata Type called Cell and we added the following fields to it:
Why? Well, we had a base map for each location but we weren't going to hard code location data into the page. We wanted the data to pull dynamically depending on the level the player was on. We also wanted anyone to be able to create their own maps.
The fields above were all picklist fields with two options 'w' (for wall) and 'o' (for open). We then created 100 (named 0-99) Cell__mdt records that were all related to the single Level__mdt record for Level 1. We then used the fields to place the walls on the map. The image below is from our final map, for reference. You'll notice the black borders on some of the cell locations. These were the borders we set in those fields. So, for cell location 12 (from the top left: 1 down, 2 right), the Cell__mdt fields would all be set to 'o' except the Up__c field, which would be set to 'w'. Cell location 2 would all be set to Up__c, Down__c, and Left__c to 'w'; and Right__c to 'o'. Setting each cell location shared wall was important as our navigation code would only factor in the current player cell location, not those cell locations surrounding them.
Before we show you the navigation code, it is worth pointing out that we went back and forth about how to trigger movement on the map. We ultimately decided on using the arrow keys on a keyboard. Looking back, we probably should have built this for mobile. Mobile first, anyone? I say that because we are currently refactoring the code and UI because of an outpouring of people that said we needed this available on mobile. And, well, mobile doesn't have a keyboard with arrows. At least not one that's usable for something like this. So, the code we will show you is for a keyboard only. The mobile version is coming, but that will be in a much later blog post once we have it thoroughly debugged and UAT tested.
Here is the navigation code. Walk through below.
At the start of the method, we set a listener, window.addEventListener(). All this does is listen for keyboard events (user pressing keys). The next two blocks of code check that the player can actually move. If so, then the actionCriteriaMet variable is true.
We then loop through the cells to find the cell location record that matches the currentLocation. This represents whatever is the player's current location. Now we have access to any cell restrictions in the Custom Metadata Type record. We then set the up, down, left, and right variables to the corresponding cell location fields. Limitations set. From here, we fire code depending on the event.code (key pressed). As described above, we set the limitation of the map borders but we also set the limitation of the cell location here. For instance, when the player keys left, the code looks at whether or not the left variable is equal to 'o'. If it is, the location can update to the current location plus -1. Feel free to read through the code for more details.
Navigation can be super complex. We did good by spending a solid amount of time upfront developing a solution and, because of that, were able to get navigation to under 100 lines of code.
Resource Management & Health Management
We wanted our game to require the player to develop a survival strategy, not just button mash to kill enemies. So, we came up with the idea of death by starvation. Super novel, right *he said sarcastically*? Basically, as the player moves through the map, their Food and Water resources will dwindle. Food and Water are character attributes that track, well, your food and water. Once there is no more Food, Water resources dwindle faster. Once Food and Water resources are depleted, Health begins to dwindle. This is how it would happen in real life, kind of, so that's what we went with.
Each location varied in terms of how a player might interact with it, and each location could have unique characteristics. So, we came up with the following order of events that would run in our code for every move:
Player moves to a new location.
Player location updates are made
Resources are updated
Health is updated
Check if Shop Location, Tavern, Secret Location, or Enemy Location
Run Location Logic
To deal with handling this order of events, we created a new function called startCellInteraction. I won't add the full function code here. What follows is the code which handles resource and health impacts from player movement. This occurs right after the player location updates.
This is pretty straightforward code. First, we get the players current stats for Food, Water, and Health. Then, we check their current values and make changes to the values of those stats based on the checks. Finally, we set the new values in the component.
Something to note here: We should have used variables for the values by which we reduce the stats. We'll be updating this code in a different version to allow for scalable difficulty levels, but you get the idea.
As you can see, every time the player moves Food drops by 1 and Water drops by 0.5. Once Food is depleted, Water drops by a total of 1. Once Water is depleted, Health drops by 1. This is one of the hardest parts of the game to overcome, and yet it was one of the simplest parts of the game to build.
We wanted each cell location to display a few different things.
Location Image. Our simplified approach meant that we only needed 1 graphical asset per cell location. Yay!
All three of these needed to display dynamically and update with each movement of the player. Here is an example of what it looks like in the UI at the time of this post:
Here is the relevant component code:
The first thing you will notice is we have divided this into two columns. The first column holds the cell location image and the second column holds the cell location name followed by the cell location description. Let's break these apart.
First the image, which we get from the location_static_resource attribute.
The HTML is pretty basic here. We are passing in a string to the <img> tag and setting the src. However, we have to dynamically update this attribute with every player move. We do that by getting and setting the value in the startCellInteraction function we worked with earlier. So, let's look at a different section of that function now.
This is a lot to digest, and you don't need to really understand it all. The key takeaways are that we create a locationStaticResource string variable, we loop through the cell location records and find the one that matches the current player location. Then, we set the locationStaticResource variable to the Static_Resource__c field on the cell__mdt record. This is a new field we added to the cell Custom Metadata Type. It is just a string field. Once we have the field set to the Static_Resource__c value, we concatenate the string with '$Resource.' (standard code for calling a static resource), create the url variable and set it to the location_static_resource attribute.
The second column is even more simple. It uses an <aura:if> tag to determine which cell location matches the current cell location and displays the text for two different fields. That's it.
Shops, Taverns, and Secret Locations
Okay, home stretch here. We have several special locations within the grid map. They consist of Shops where a player can buy health, energy, and energy weapons; Taverns where a player can replenish their food and water; and, finally, Secret Locations (identified as Storage Rooms) where a player can collect items that give them permanent upgrades.
We simplified the interactions within the locations by making it so no enemies would ever be allowed to occupy them. Enemies can still guard cell locations but they will occupy other cell locations in front of or around special cell locations.
How these locations work is pretty straightforward, so we'll jump right into the code. This all exists within the startCellInteraction function.
I'm not showing the component.get of the type variable. Nothing interesting there. We just get the Type__c field on the cell__mdt and set it to the type variable. Another custom field! It's a picklist that can be set to Shop, Tavern, Storage Room, or Staircase.
Taverns are crazy simple. If a player lands on a Tavern, we get their Food and Water and set them to their Food Capacity and Water Capacity. Essentially, we refill all of their supplies.
If a player lands on a Shop, we turn a modal on. We do this by using an aura:attribute which switches from false to true, which in turn switches an aura:if tag from false to true and displays the content. We won't go into the Shop code (purchasing) here.
If a player lands on a Storage Room (special location), then any number of things can occur. The code is more lengthy but definitely not that complicated. The first thing you should know is we created another Custom Metadata Type called Item. It has all of the weapons, health and energy restoration items, and special items. In the code, we grab the list of items and then determine which item type it is. The special items only increase capacity of user stats, so we need to find the specific stat that it increases. Once we have that we just update the stat. In each case, this is the capacity field for a stat. For example, the character__c.Energy__c field has a corresponding character__c.Energy_Capacity__c field. One thing you may notice is that energy is the only stat where we have a 'Item_Found_XXXX__c' field being set. That is because the game isn't finished yet, and the only item available to find in the current demo is an item that increases the players energy capacity.
Finally, we have the type Staircase, which is the entrance to the next dungeon level. We'll get into that more during a different blog post, but as you can see, it is fairly simple. When the player lands on a cell location of type Staircase, a modal pops up in which the player will interact.
Cell Location Cleared
After a player has been to a cell location and lived to tell the tale, the cell's Cleared__c field is set to true. This is not cell__mdt! It is cell__c. Yes, we have a bit of a duplicate object thingy going on here. If you need more context as to why we did it this way, check out the Data Model section of How we created a game using only Salesforce: Part 1 - An Overview. The summary: We used the Custom Metadata Type (cell__mdt) to set anything about the map that was static and unchanging, and we used the Custom Object (cell__c) to allow the player to interact with the map and have that interaction persist, but only for that one player.
We have a number of checks later in the code that are contingent on the cell__c.Cleared__c field being set to false. One of the things you'll notice if you play the game is that once you defeat an enemy in a cell location, you never encounter an enemy in that location again. We considered creating a re-spawning mechanism but, again, we ran out of time. We'll discuss that in our blog about battling.
That's it for now! Next week we'll discuss the following topic:
Start Menu & Character Creation
Attributes & Their Advantages
Strength (Attack Adders)
Endurance (Health Adders)
Intelligence (Learning Adders)
Charisma (Cheaper Prices)
Saving & Loading a Game
To get started with Cloud Pacific, go here to complete a simple form. A Cloud Pacific Account Manager will collaborate with you to clarify your needs and goals, and customize a service package and roadmap that will help your business grow and thrive on Salesforce. Reach out to us here!