- Date
Map Generation Code is out for TfQuest!
Posted by
Aika
Hello Everyone!
New release day! This version is still very much pre-alpha, but I think there’s enough here that it warrants a release.
This is a major devlog concerning a new version with some hands-on stuff, so it’s actually hello to everyone instead of just the people on SubscribeStar! (To all the people supporting financially, thank you very much. I know I say it lots, but it means the world to me.)
Today will be mostly a modding guide for how to set up custom regions. This system will get new features closer to release, but it’s not in a good enough state for me to continue to tie other systems into it. That means it’s on to bigger and better!
What was I working on this week?
This week was a split between getting this release ready and working on what I mentioned last week, which is parsing work! There’s not much exciting to show there quite yet. It’s mostly just backend work right now, so we’ll probably talk about parsing functions in next week’s devlog and focus on the map generation for now.
So, what have I done on the map generation side? Like I mentioned last week, I fixed the literal edge case, added validation, and gave the filler rooms a bit more personality (variance in shape). I also made an (extremely preliminary) polygon helper tool that can assist in getting coordinates for polygon-based map generation. This will be significantly improved in the future, don’t worry. However, it should be enough to make polygons approachable for people who don’t live on a cartesian plane (myself included).
What am I working on next week?
Next week will be more of the same for me. Getting the initial bits of information parsed over from Inform is going well, but there’s lots more work to do there. Consolidating that code is a considerable amount of effort, but we’re making decent headway. On top of that, most of the original code (even for things that are parsed successfully) will be culled significantly for readability and performance. That means a lot of work on this front. I’m hoping for visible results within the next week or two. Anyways, let’s get back to the fun stuff!
Map Generation Documentation
This section will be documentation on how to set up regions. Eventually, there will be a section on a wiki with all of this information. Let’s get into it!
Generation order is as follows:
1. “mapShapes” and “mapHoles” are loaded onto the map
2. Pinned rooms are placed onto their required locations
3. Mandatory rooms are placed randomly into “mapShape” regions
4. Connections are generated between all of the rooms
5. (Currently Unimplemented) Rooms that aren’t mandatory replace filler rooms of the same shape. This will be implemented once the rooms have content in them.
6. Connections are validated
Region Definition File
Regions are defined by files located in “External Files/Regions”. They are JSON files that have all of the world generation properties within them. By tweaking these properties, you can affect how the generation looks. It’s not necessary to have a JSON editor to change these files, but a simple one like Notepad++ will help point out any mistakes if you’re new to JSON.
Currently, “dungeon.json” is the only accessible region. To see the results of your changes, please keep in mind that adding other regions will load them, but they will not be viewable.
Let’s talk a bit about each of the properties within the region definition file:
mapShape
Maps are made up of polygons and circles. These define the basic shape of the map. “mapShapes” are also the only region where rooms are randomly placed during generation. Map Shapes are listed under the “mapShape” property of the region definition file, and you can have as many as you’d like to. Each shape will be loaded and considered equally. Circles are the simplest to use, while polygons are more complex and allow for greater control. You can see an example of this format when you first open the file:
"mapShape": [
[Entry 1],
[Entry 2]
]
To hide/show mapShapes and mapHoles, push 'H' on the keyboard
Circles [“circle”, radius, x, y]
Circles are the simplest map shape, only involving three data sets. You need a radius, x position, and y position to draw a circle. To define a circle in “mapShape”, start by declaring the shape as a circle, and then listing the radius, x, and y positions of the circle.
Example:
[“circle”, 1000, 50, -400]
Polygon [“polygon”, [p1.x, p1.y], [p2.x, p2.y],… [pN.x, pN.y],]
Polygons are more complex than circles because they require a list of points. Generally, using my polygon helper tool that will be covered later is the best way to generate these points, however it can be done manually. To define a polygon, first start by declaring the shape as a polygon, then listing the points in order.
Example (rectangle):
["polygon", [-500, -500], [500, -500], [500, 500], [-500, 500]]
mapHoles
Map Holes are similar to Map Shapes in creation, but they override and ‘cut through’ Map Shapes. A Map Shape with an overlapping hole will not be considered a map shape in the overlapping region. You can find Map Holes in the “mapHoles” property of the region definition. For more information on the formatting of Map Holes, see Map Shapes. The formatting is identical.
To hide/show mapShapes and mapHoles, push 'H' on the keyboard
areaBounds
The “areaBounds” property defines the starting location of the overall rectangle that bounds the Map Shapes. This will not matter in most cases. However, if your room generation happens in Narnia, it can be best to shift these coordinates to match the center of your map generation.
roomDimensions
“roomDimensions.height” and “roomDimensions.width” exist to allow for variable image sizes. Changing this number will change the scaling of the rooms. This feature is currently untested (I don’t have any art to use).
AStarWeights
The “AStarWeights” property is simple, yet incredibly important to the overall room generation. Generation is driven by connecting rooms through an algorithm called ‘AStar’. Each weight is essentially a cost to traverse that type of tile. Two tiles of weight 1 will be considered the same as traversing one tile of cost 2. Using these weights can be incredibly powerful, but very tricky. It is extremely important to remember that weights are relative to each other.
placedRoomWeight
“placedRoomWeight” defines the weighting (or cost) of tiles that have rooms on them. Tiles are updated to this value as rooms are placed on them.
General effect of lowering this weight: more rooms use the same path instead of cutting across empty space
innerGeometryWeight
“innerGeometryWeight” defines the weighting within “mapShapes” where no rooms have been placed.
General effect of lowering this weight: Pathing is more likely inside mapShapes instead of outside.
holesGeometryWeight
“holesGeometryWeight” defines the weighting within “mapHoles” where no rooms have been placed.
General effect of lowering this weight: Pathing is more likely within holes.
outerGeometryWeight
“outerGeometryWeight” defines the weighting outside of both “mapShapes” and “mapHoles” where no rooms have been placed.
General effect of lowering this weight: Pathing is more likely around the perimeter of the defined region.
generationOption
“generationOption” determines how the randomly placed rooms are connected. They can be chained together similarly to the “mapShapes” for more complicated generations with no limit. Unlike mapShapes, their order does matter due to the tiles with ‘placedRoomWeight’ changing after each generation. Here is an example of syntax for multiple generations:
"generationOption": [["spider", 0, -4], [“meandering”]],
More will most definitely be added. For now, there are currently three options:
Meandering [“meandering”]
Meandering is the simplest option. It picks two rooms and paths from one to another, like a person taking a walk and deciding on each destination after the last.
Closest [“closest”, closedLoop]
Closest is similar to meandering, except that it picks the closest untouched room to path to next.
When “closedLoop” is true, the algorithm will connect the first room picked to the last room picked. When “closedLoop” is false, the algorithm will leave the loop open resulting in more of a snake. Planned for future development is the ability to start the generation at a specific spot on the map.
Ex: [“closest”, true]
Or: [“closest”, false]
Spider [“spider”, x, y]
Spider generation attempts to connect every room to a single point through the most efficient path. “x” and “y” determine the location of that point on the map. Use of this pattern can yield impressive results in conjunction with other methods but is viable on its own. Requires significant attention to weightings to achieve different numbers of branches.
Ex: [“spider”, 0, 0]
“___DirectionClosure”
This weighting determines the frequency at which connecting rooms have bends in them. There is a chance equal to “firstDirectionClosure” for an exit to be cut off. If that exit is cut off, there is a chance equal to “secondDirectionClosure” for another exit to be cut off. If that exit is cut off, there is a chance equal to “thirdDirectionClosure” for another exit to be cut off. This does not affect the validation step. Setting these too high can result in wackiness.
Cursory Room Documentation
This will not be all-inclusive; however, it should give enough information on the room documents to create a reasonable facsimile of whatever generation you’d like to.
Room Definition File
Files for each room are located within the ‘External Files/Rooms’ folder. The folders within the Rooms have no bearing and can be arranged in any way for organization.
Rooms are loaded in two parts. First, a defaults file is loaded that has all of the necessary components for a room to function. These files are done by region and can be located in the folder ‘External Files/Room Defaults’. Then, the room file is loaded over top of that, replacing any data that is not identical.
Currently, as we are in the middle of parsing the data from these rooms, the files are extremely messy. I am only going to briefly discuss the variables that are currently relevant to the generation, and I’ll make sure this section gets proper documentation once things have settled a bit more
Directions [up, down, north, east, south, west]
Directions is an array of directions that the room can lead. This is a holdover from Inform and may be replaced in the future.
An example is as follows: [false, false, true, true, false, true], or a 3-way room with no south opening.
Setting each one to true/false will determine whether travel in that direction is possible. Up/down currently do nothing. Setting all of the rooms to false will cause errors.
Pinned [bool]
Setting pinned to true will cause the room to be placed in a specific location. The location of the pinned room will be preserved except in extreme cases where two pinned rooms cannot be connected.
gridPosition {"x": 0, "y": 0},
gridPosition is only important for pinned rooms. Setting gridPosition for pinned rooms places the pinned room at the gridPosition specified.
region
The room’s region will be matched against the region’s region property. If they are the same, the room will be included in that region.
isRoomImageInternal
This Boolean sets a flag for whether the program searches internally or externally for the file. External images will not load properly unless this is set to false.
file_name
The file name of the image. Currently images are loaded from a folder called Res://Assets//dungeon//, so in order to add custom images, you’ll need to create this folder. This will be fixed to be easier in later versions.
NOTE: If isRoomImageInternal is not set properly, the image will not display.
Known Bugs
I’ve managed to fix all of the bugs that I’ve found (related to map generation). Let me know if you find any in this release!
Polygon Generation Tool
To get output from the tool, launch the program from the .console.exe
Not gonna lie, this tool is scuffed but it’s also so incredibly helpful. When I have more dev time, I am planning on a more robust suite of tools for modding. Basically, you left-click to put points down, then right-click to remove points if you don’t like what you did. After you make a change, the current polygon prints out into the console pre-formatted to be slapped in to mapShape or mapHoles. Camera controls are the same as in TfQuest currently (arrow keys and scroll wheel).
Notably, the tool mimics the polygons that are used in Godot, so if the polygon edges cross over each other, the polygon will fail to draw. The white point in the centre is 0, 0 for reference.
Thanks for reading!
Let me know what you think of the new release, and if you’re excited about the new map generation! I’d also love to hear about other ideas for generation options that people have.
See you around on Discord or next week’s devlog!