RPG Maker gives the user a lot of easy to use controls to create content and string it together. The stringing together part, crucially, is translated to Ruby code behind the scenes. RPG Maker also introduces a lot of flexibility by allowing the developer to modify and write their own Ruby files. Some things remain off limits (e.g. the functions that decrypt and extract information from resources) but most everything else from characters to scenes is open for fun.
Now then, my goals for the random dungeon are pretty simple (I think). I want a dungeon design that is:
- Easily reproducible
- Feel hand crafted
- At least reasonably interesting to move through
- Always has a path to all pre-placed events on the map
Let's tackle the first one of these requirements first. RPG Maker is very much all about using static maps for the games that you can create with it. There is a custom dungeon generator included in RPG Maker VX Ace, but it generates the maps in the editor before compiling which means we can't leverage it to our advantage here. If there was a way to access this feature via scripting it would be a different story, but from what I can tell this is not possible.
The route I've decided to go will be to write a Ruby module which takes input such as a seed number and then generates a dungeon level based on that seed. This allows us to easily reproduce the exact layout of the dungeon floor as long as we don't lose the seed and we don't change the other variables that go into the map generation algorithm.
Inspired by other "mysterious dungeon" games I've decided that under certain circumstances the dungeon layout will change. So long as the player does not rest for the night to recover their HP & MP, they can leave and then go right back into the dungeon and it will be just as they left it. As soon as they rest though, the floor layout changes when a new seed number is picked. This decision unexpectedly helped me out when I realized that I couldn't save a generated layout to the map files (due to them being encrypted), but the engine already accounts for saving some variables for me, so I can just stick the seed in there for each floor and the player can save and load their game with no worries!
The next aspect I want is for it to feel hand crafted. If I wanted to be truly random here, all I'd have to do is pick a random number from 0 to whatever the top tile id number is and shove it into a map's data table. This dungeon would be very unlikely to be playable though. Instead I've decided that it would be good to make the individual pieces of the dungeons myself and then just have my generation algorithm place them as it sees fit. Hopefully this will make the map feel more like someone took the time to craft each little bit by hand, even though I'm not about to do that.
We'll come back to the other two requirements I have next time. For now, we have enough to get something basic working, so let's get to that. I'm going to talk a bit about the "technical" bits on how to do this, so if that's not your cup of tea, feel free to skip down to the screenshots.
Looking at the RGSS3 documentation, we can see that the Game_Map object has an accessor for a table called "data." The table is actually a simple implementation of a three dimensional array, containing exactly four integers for each x,y coordinate. Initially I thought that at least one of those four integers would be for storing which event (if any) was present at that coordinate, but on further research it seems like all four integers are for layers of tiles at that coordinate (which is not unexpected given that past versions of RPG Maker allowed you to access different layers to put tiles on top of each other, but sadly that is not the case with VX Ace).
I've chosen, completely at random, to make my dungeon floors 100x100 tiles in size and all my hand crafted rooms that will be randomly placed are now 10x10 in size. Each map is assigned an id which is directly related to its filename, making it easy to randomly pick a number from 0 to N where N is the number of rooms I've made (this is stored in a variable) and then add the id of the first room (right now it's 22). At this point, it just takes a little bit of scripting know how to whip up a couple of four loops that randomly choose a room and copy the data from it to the dungeon map every 10 tiles.
Before we do that though, I decided that it would be nice to place some things that are static on each dungeon floor, for example, a starting and ending area. The reason for doing this isn't too complex: I want to be able to have something I know will always be there like landmark or a place to teleport the player to when they go down a floor or something. I can also use this to create boss battle rooms or similar events that I might want to keep unique to a floor. With this in mind, before any room is chosen to be copied over to our dungeon floor I check the upper left most tile in each 10x10 set. If that tile has a tile id of 0 (that is, it is blank) then I go ahead but if it has any other tile id, then I assume that the 10x10 set it represents is already filled and I skip it.
After that's written, the next problem is figuring out how to call it on each floor. This is actually pretty easy due to the handy Event system in RPG Maker. On the map for each dungeon floor we'll add an event whose trigger type is Autorun. This means that the event will run automatically when the map is loaded. Using the script command we'll pass the map's data table and seed value into our algorithm and generate the map each time it's loaded, then we use the "Erase Event" command to keep it from firing over and over again.
Here's what it looks like after everything I've described is wired up:
For comparison, here's what it looks like in the editor:
For comparison, here's what it looks like in the editor:
|This is at 1/4 zoom. That's the starting area down in the right and up on the left is the end of the floor.|
It still has some problems though, where room exits don't always match up. Like this:
My next post will address the other two dungeon generation goals and fix problems like the ones above.