Intro
A while back, I participated in a game jam, specifically POSTAL Jam 2, which is basically a contest where developers create games inspired by the infamous POSTAL series. I've been a long time fan of POSTAL 2 and it's one of my favorite games of all time, so when I heard that the developers behind the game were hosting a game jam, I just couldn't resist.
One of the standout entries that actually ended up taking second place was POSTAL: Onslaught, a retro arcade-style shoot-em-up that I made for fun inbetween other projects and responsibilities.
The game features randomly generated levels that are made utilizing a custom little scripting language I wrote so I thought it might be interesting to talk about it!
Why a Custom Scripting Language?
Instead of manually designing a set of pre-made levels and randomly picking between them or something boring like that, I decided to challenge myself. Mostly just because I thought it would be fun and I've never done anything like this before previously.
Might not have been the greatest use of my time, but I definitely appreciated it later on in development.
How Does It Work?
The level generation script is stored in what is essentially just a plain text file with a .level extension, parsed during runtime by my LevelScriptParser class in Unity from the StreamingAssets folder. The scripting language was designed to be pretty simple and straightforward to allow me to iterate pretty quickly.
Variables
Variables can be either a whole number/integer or a floating point value, and they can control everything from the size of the level to random placement positions. For instance, to define randomized dimensions of a level, I can simply do this:
VAR LEVEL_SIZE_X RAND_INT 10 50VAR LEVEL_SIZE_Y RAND_INT 10 50
LEVEL_SIZE_X and LEVEL_SIZE_Y will be random integers between 10 and 50 using the RAND_INT function. These variables can then be used in conditions and loops.
Loops
The scripting language includes FOR loops, which allow iteration for placing multiple entities or filling areas with tiles and whatnot. Here's an example of a loop that places a row of tiles:
FOR X 0 10TILE grass_tile X 0ENDFOR
This loop places a line of grass tiles using the TILE function along the x-axis from (0,0) to (10,0). The flexibility of loops also allows for more advanced stuff, like filling larger areas with a set of rules.
Conditions and Operators
The language supports conditional logic using IF statements, enabling dynamic changes based on various conditions.
This allows you to control the behavior of elements within the level.
Conditional statements can use various operators to define the logic.
Below are the supported key operators:
>- Greater than<- Less than>=- Greater than or equal to<=- Less than or equal to==- Equal to!=- Not equal to
Here’s how you can decide whether to place an obstacle:
IF LEVEL_SIZE_X > 30OBJECT rock_1 5 5ENDIF This condition checks if the level width is greater than 30. If true, it spawns a rock at the coordinates (5,5).
Entity and object randomization
Randomness plays a huge role in procedural generation obviously. Alongside random integers and floats, there are specialized functions with parameters for random placement of entities and objects. These parameters ensure that stuff spawns safely away from other things, maintaining balance while preserving a feeling of unpredictability in its placement. For example:
ENTITY_RANDOM civilian_npc dirt 3 3 8
This line spawns a civilian_npc entity randomly on a dirt tile. The following values indicate the constraints:
-
The entity will not spawn closer than 3 tiles from the level bounds
-
The entity must be at least 3 tiles away from any nearby objects/entities
-
The entity must maintain a minimum distance of 8 tiles from the player
OBJECT_RANDOM level_palm_tree dirt 3 3 3 OUTSIDE_BOUNDS
This line will randomly spawn a level_palm_tree on a dirt tile with certain restrictions:
-
The object will not spawn closer than 3 tiles from the level bounds
-
The object must be at least 3 tiles away from any nearby objects/entities
-
The object must maintain a minimum distance of 3 tiles from the player
-
The presence of the
OUTSIDE_BOUNDSflag means this tree will spawn outside the playable level bounds for cosmetic purposes only
Structures and Tile Fills
Another feature of the language is the ability to fill areas with tiles or place procedural structures. Using the TILE_FILL function, I can easily fill a region of the level:
TILE_FILL dirt 0 0 10 10
This will fill a 10x10 area with dirt tiles.
The STRUCTURE function allows me to place predefined randomized structures:
STRUCTURE player_housing LEVEL_BOUNDS_DIST + 5
This will generate a player housing structure in a random location, ensuring that it is at least 5 tiles away from the boundaries of the level.
Breakdown of the Level Generation Process
1. Setting the Level Dimensions and Boundaries
VAR LEVEL_SIZE_X RAND_INT 64 96VAR LEVEL_SIZE_Y RAND_INT 64 96VAR LEVEL_BOUNDS_DIST 13
This section of the script randomizes the dimensions of the level (between 64 and 96 tiles for both width and height). It also defines a boundary distance of 13 tiles, which will be used later to ensure there is a consistent buffer around the edge of the level.
2. Filling the Area with Dirt
TILE_FILL dirt 0 0 LEVEL_SIZE_X-1 LEVEL_SIZE_Y-1
This function fills the entire level with dirt tiles as the base terrain. Every tile in the defined area is initially filled with dirt to essentially set up the rest of the generation...
3. Creating Cliffs along the Level Edges
FOR X LEVEL_BOUNDS_DIST LEVEL_SIZE_X-LEVEL_BOUNDS_DIST-1TILE dirt_cliff_b X LEVEL_BOUNDS_DIST-1TILE dirt_cliff_t X LEVEL_SIZE_Y-LEVEL_BOUNDS_DIST-1ENDFOR
FOR Y LEVEL_BOUNDS_DIST LEVEL_SIZE_Y-LEVEL_BOUNDS_DIST-1TILE dirt_cliff_l LEVEL_BOUNDS_DIST-1 YTILE dirt_cliff_r LEVEL_SIZE_X-LEVEL_BOUNDS_DIST-1 YENDFOR
These loops place cliff tiles around the edges of the level, forming simple boundaries.
TILE dirt_cliff_tl LEVEL_BOUNDS_DIST-1 LEVEL_SIZE_Y-LEVEL_BOUNDS_DISTTILE dirt_cliff_tr LEVEL_SIZE_X-LEVEL_BOUNDS_DIST LEVEL_SIZE_Y-LEVEL_BOUNDS_DISTTILE dirt_cliff_bl LEVEL_BOUNDS_DIST-1 LEVEL_BOUNDS_DIST-1TILE dirt_cliff_br LEVEL_SIZE_X-LEVEL_BOUNDS_DIST LEVEL_BOUNDS_DIST-1
Corner cliffs are then placed at the four corners of the level to finish off the boundary. These cliffs help to separate the playable area from the rest of the map, making it feel more like an arena.
5. Placing Player Housing Structure
STRUCTURE player_housing LEVEL_BOUNDS_DIST + 56. Placing Entities and Randomizing NPCs
VAR POLICE_COUNT RAND_INT 1 5VAR CIVILIAN_COUNT RAND_INT 4 6VAR PROTESTER_COUNT RAND_INT 1 3FOR INDEX 1 POLICE_COUNTENTITY_RANDOM police_enemy_npc dirt 3 3 8ENDFORFOR INDEX 1 CIVILIAN_COUNTENTITY_RANDOM civilian_npc dirt 3 3 8ENTITY_RANDOM civilian_enemy_npc dirt 3 3 8ENDFORFOR INDEX 1 PROTESTER_COUNTENTITY_RANDOM protester_npc dirt 3 3 8ENDFORThe number of police and civilian NPCs is randomized between 1-5 and 4-6, and protesters between 1-3 respectively. These values are then used in loops to randomly place them throughout the level using set rules.
7. Adding Random Pickups
FOR INDEX 1 3ENTITY_RANDOM weapon_pickup dirt 5 3 8ENDFORFOR INDEX 1 3ENTITY_RANDOM item_pickup dirt 3 3 8ENDFORWeapon and item pickups are scattered across the map randomly. The constraints in the script ensure they do not spawn too close to other entities, like the player, ensuring the level remains balanced.
8. Placing Trees and Other Objects
FOR INDEX 1 5OBJECT_RANDOM level_palm_tree dirt 2 3 3ENDFORFOR INDEX 1 6OBJECT_RANDOM level_cactus dirt 2 3 3ENDFOR...
Objects like trees and rocks are randomly placed to give the level a more varied and natural appearance. The script ensures that they don't spawn too close to the player or other objects/entities.
9. Placing out-of-bounds Objects
FOR INDEX 1 20OBJECT_RANDOM level_palm_tree dirt 3 3 3 OUTSIDE_BOUNDSOBJECT_RANDOM level_cactus dirt 3 3 3 OUTSIDE_BOUNDSOBJECT_RANDOM level_rock1 dirt 3 3 3 OUTSIDE_BOUNDSENDFORCosmetic objects are placed outside the playable area using the OUTSIDE_BOUNDS flag, ensuring that the surroundings outside the playable area appears populated.
10. Placing more Structures
FOR INDEX 1 2STRUCTURE npc_housing_01 LEVEL_BOUNDS_DIST + 3STRUCTURE npc_housing_02 LEVEL_BOUNDS_DIST + 3STRUCTURE npc_housing_03 LEVEL_BOUNDS_DIST + 3ENDFORFinally, a few procedural structures are randomly placed in the level including various NPC housing, ensuring they maintain a certain distance from the boundaries.
Final Thoughts
Creating this custom scripting language was a bit of a challenge, but it became an essential part of delivering decent enough levels that are fun to play.
I hope this dive into the inner workings of the scripting language was interesting. Thanks for reading!