Featured Titles
VOXELLENT DEV LOG
VOXELLENT ON TWITTER:
SAMPLE SCRIPT:
I’ll share more info later on my procedural map gen cellular automata and real-time combat scripts for ‘RPG in a Box’, but here’s the current state of my WIP ‘build_map’ script which runs over a prebuilt map of 30×30 temporary tiles on a dummy map. (This is necessary to preserve NPC pathfinding in the finished map.) The dummy map also contains a remote tile to hold a player asset while the map is generating, and a dummy NPC outside the main grid for editing ‘behavior’ settings for the map. I’ve heavily commented the code and left in debugging ‘print’ calls so others can follow the steps, which are:
- Loop through all temp tiles giving them IDs and a random state – ‘floor’ or ‘black’ based on a map density variable
- Loop through all tiles cleaning up shaping based on number of neighbours, with total number of loops defined in a variable to adjust the chunkiness of the floor plan
- Loop through all tiles adding walls and black ‘fill’ objects on empty tiles that are adjacent to walls so backs of walls aren’t visible during game play
- Clean up walkability and interaction between tiles along the way
- Move the player to a valid starting tile and place exit
- Place lootable objects with variations for alcoves, corners, walls and open floor
- Find single-width corridors and place doors
- Place NPC generators which are also destructible in their own script and for which a separate ‘spawn_mob’ script spawns NPCs onto the map after map load
- Update loading animation along the way as the whole process takes around 15 seconds when using detailed 32x32x48 tiles
To follow this script you probably need to be familiar with how cellular automata and arrays work. Any suggestions for improvements are welcome. This is still as work in progress:
// CELLULAR AUTOMATA MAP GEN 18/4/20
// TO DO: Pick random tileset
// TO DO: Fix glitch on nav for tiles at coord 30
// Dim maximum map size
maxSizeX = 20;
maxSizeY = 20;
minSizeX = 20;
minSizeY = 20;
tDepth = -2;
// set random dimensions
columns = random(minSizeX, maxSizeX);
rows = random(minSizeY, maxSizeY);
set_global_property("columns", columns); // Used by other scripts to check all tiles
set_global_property("rows", rows); // Used by other scripts to check all tiles
// map gen vars
openChance = 4; // init grid density (1-10)
overpopLim = 4; // max cell closed neighbours
killNum = 2; // min cell closed neighbours
birthNum = 4; // # neighbours to tile an empty cell
loopNum = 1; // Cellular loops
exitSet = false;
//play_animation(player, "loading1"); // Start loading anim TO DO: Use swap model instead of having this in constant model
//play_music("Battle Cry.ogg"); // TO DO: Stop music not currently possible
wait(0.01); // Refreshes screen
//set_directional_light_enabled(false); // Darken map
//set_ambient_light_enabled(false);
print("Map size:" + columns + ":" + rows); // Debug
// TO DO: Reset map
tCount = 0;
col = 0;
while col < columns do
col += 1;
//print("First do col:" + col); // Debug
row = 0;
while row < rows do
row += 1;
//print("First do row:" + row); // Debug
bufferZone = true;
if col > 2 and col < (columns - 1) then // Buffer zone // TO DO: Check this is still necessary
if row > 2 and row < (rows - 1) then // Buffer zone
bufferZone = false;
end
end;
tempTile = tile[col, row, tDepth]; // Used to wipe or replace temp tiles
//print("At temp tile:" + tempTile); // Debug
if random(0, 10) <= openChance and bufferZone == false then
tName = "tile" + col + ":" + row; // Current tile
print("Swap floor tile:" + tName); // Debug
// Name tile as a floor
tName = "tile" + col + ":" + row;
play_animation(tempTile, "dungeon_floor");
assign_entity_id(tempTile, tName); // Rename temp tile as a wall floor // TO DO: Repeat this process later during cellular
tCount += 1; // Keep track of total tiles
add_to_group(tName, "tGroup1");
else
blackName = "black" + col + ":" + row;
//print("Make black tile: " + blackName); // Debug
// Put temp tile on first frame in first pass as we may be reloading map
// play_animation(tempTile, "black");
assign_entity_id(tempTile, blackName); // Rename temp tile as a wall floor
end
end
end;
// Uncomment following 2 lines to swing camera to watch map load
//lock_camera(); // Debug
//move_camera(coord[0, 15, 200], coord[15, 15, 0]); // Debug
play_animation(player, "loading2"); // Advance loading anim
wait(0.1); // Refreshes screen
//display_message("Click to generate random map... (Approx. " + loadTime + " seconds)"); // Debug
loop = 0; // Full loop through celullar automata
while loop < loopNum do
loop += 1;
//display_message("Generate map " + loop + "/" + loopNum + " (Click to proceed)"); // Debug
// With each tile in map - exclude one row around edge to simplify neighbour checks later
col = 1; // Skip map border
while col < columns do
col += 1;
row = 1;
while row < rows do
row += 1;
tName = "tile" + col + ":" + row; // Current tile, used in multiple places below
tTile = entity[tName];
blackName = "black" + col + ":" + row; // Black tile name for same location
bTile = entity[blackName];
//print("XTile" + col + ":" + row); // Debug
// Count this tile's neighbours
count = 0;
i = -2;
while i < 1 do
i += 1;
//print("i:" + i); // Debug
j = -2;
while j < 1 do
j += 1;
//print("j:" + j); // Debug
// Exclude middle point (i.e.: tile for which neighbours are being checked)
if i == 0 and j == 0 then
// Do nothing
else
checkCol = col + i;
checkRow = row + j;
//display_message("Tile" + checkCol + ":" + checkRow); // Debug
tCheck = "tile" + checkCol + ":" + checkRow;
if group["tGroup" + loop] contains entity[tCheck] then // Check if there's a tile there
//print("Tile " + count + " counted at " + checkCol + ":" + checkRow + " on side " + i + ":" + j); // Debug
count += 1;
end
end
end
end;
//print("Tile count:" + count); // Debug
//print("Time name:" + tName); // Debug
nextLoop = loop + 1;
nextLoop = "tGroup" + nextLoop;
if group["tGroup" + loop] contains tTile then // Check if there's a tile there in the current loop
if count <= killNum then // tile has <= X neighbours, remove
remove_from_group(tTile, "tGroup" + loop);
// Tile is a floor tile, swap to black
//print("Swapping for black tile at " + i + ":" + j); // Debug
play_animation(tTile, "black");
assign_entity_id(tTile, blackName); // Rename tile as a black
tCount -= 1; // Keep track of total tiles
// Replace with black corner
//print("Made black tile: " + blackName); // Debug
else // Otherwise tile has enough neighbours, leave and add to next loop
add_to_group(tTile, nextLoop);
end
elseif count >= birthNum then // Empty tile has >= X neighbours, fill
//print("Swapping floor tile at " + col + ":" + row); // Debug
tempTile = tile[col, row, tDepth];
play_animation(tempTile, "dungeon_floor");
// Flip tile to random direction
dungeonDirs = array[NORTH, EAST, SOUTH, WEST];
tempTile.direction = dungeonDirs[random(0, 3)];
assign_entity_id(tempTile, tName); // Name tile as a black
tTile = entity[tName]; // Refresh tTile
tCount += 1; // Keep track of total tiles
add_to_group(tTile, nextLoop);
end
end;
end;
end;
nextLoop = null; // Clean up nextLoop
// TO DO: Cleanup old loop groups
play_animation(player, "loading3"); // Advance loading anim
wait(0.01); // Refreshes screen
//display_message("Placing walls, map contents and walk paths (Click to proceed)"); // Debug
// Set walkabilities, wall objects and populate map contents
col = 1;
while col < columns do
col += 1;
row = 1;
while row < rows do
row += 1;
//print("Making row walkable:" + row); // Debug
tName = "tile" + col + ":" + row; // current tile, used in multiple places below
tCheck = tile[col, row, tDepth];
nameCheck = tCheck.id;
wallCheck = "Wall" + col + ":" + row;
blackCheck = "black" + col + ":" + row; // Used to check if tile is a black corner
if tName == nameCheck then // Only check neighbours for floor tiles
// TO DO: Might be rechecking other naming formats, e.g.: genTile
wallCount = 0; // Used to count number of walls on a tile
// Check surrounding tiles
i = -2;
while i < 1 do
i += 1;
//display_message("i:" + i); // Debug
j = -2;
while j < 1 do
j += 1;
//display_message("j:" + j); // Debug
checkCol = col + i;
checkRow = row + j;
posFactor = (j * 10) + i; // Used in wall positioning
//display_message("Tile" + checkCol + ":" + checkRow); // Debug
sideCheck = tile[checkCol, checkRow, tDepth]; // Side tile being checked
checkName = sideCheck.id;
wallSideCheck = "Wall" + checkCol + ":" + checkRow; // Used to check if tile is a wall
blackCheck = "black" + checkCol + ":" + checkRow; // Used to check if tile is a black
wallObj = "Obj" + col + ":" + row; // Used to name wall object inside current tile
fillObj = "fill" + checkCol + ":" + checkRow; // Used to name fill object in adjacent tile
if i == 0 and j == 0 then // Exclude middle floor point (i.e.: tile for which neighbours are being checked)
// Do nothing, everything is handled from adjacent tiles and we need to wait for all walls to be done
elseif (i * j) == 0 then // Perpendicular positions only
if checkName == blackCheck and entity[fillObj] == null then // We found an empty void perpendicular to a floor tile
// TO DO: Multiple 'object cannot be placed on the specified tile' errors
print("Add fill object at " + checkCol + ":" + checkRow); // Debug
assign_entity_id(sideCheck, wallSideCheck); // Rename black tile as a wall block
sideCheck = tile[checkCol, checkRow, tDepth]; // Update vars for use in next if
checkName = sideCheck.id;
add_object("obj_wall_fill", coord[checkCol, checkRow, tDepth], fillObj);
// tempTile = tile[col, row, tDepth].id;
replace_navigation(checkName, WALK_AND_INTERACT, INTERACT_ONLY); // Make side tile unwalkable
modify_navigation(tName, checkName, INTERACT_ONLY);
end;
if checkName == wallSideCheck and entity[checkName] != null then // Tile is already wall set in a previous loop
//print("Found wall at " + i + ":" + j); // Debug
//print("Add wall object at " + col + ":" + row); // Debug
// Place wall objs inside floor tile for touch interaction
pickObj = random(1, 2); // Randomise wall objects
if posFactor == 10 then
wallObj = wallObj + "south";
pickObj = "obj_wall_S" + pickObj;
add_object(pickObj, coord[col, row, tDepth], wallObj);
wallCount += 1; // Used to find walls and corners
set_entity_property(wallObj, "wDir", "south"); // Used by break_wall
elseif posFactor == -10 then
wallObj = wallObj + "north";
pickObj = "obj_wall_N" + pickObj;
add_object(pickObj, coord[col, row, tDepth], wallObj);
wallCount += 1; // Used to find walls and corners
set_entity_property(wallObj, "wDir", "north"); // Used by break_wall
elseif posFactor == 1 then
wallObj = wallObj + "east";
pickObj = "obj_wall_E" + pickObj;
add_object(pickObj, coord[col, row, tDepth], wallObj);
wallCount += 1; // Used to find walls and corners
set_entity_property(wallObj, "wDir", "east"); // Used by break_wall
elseif posFactor == -1 then
wallObj = wallObj + "west";
pickObj = "obj_wall_W" + pickObj;
add_object(pickObj, coord[col, row, tDepth], wallObj);
wallCount += 1; // Used to find walls and corners
set_entity_property(wallObj, "wDir", "west"); // Used by break_wall
end; // TO DO: Else other numbers
//display_message(checkName); // Debug
else // This is a perpendicular floor tile
// TO DO: Make non-neighbours non walkable
end;
elseif checkName == blackCheck and checkName != wallSideCheck then // This is a black corner not perpendicular to this floor tile and not already made a wall
// TO DO: Multiple 'object cannot be placed on the specified tile' errors
// DIAGONALS:
print("Diagonal at " + checkCol + ":" + checkRow); // Debug
// TO DO: Further optimise to find corners. Currently creating many which are overwritten
assign_entity_id(sideCheck, wallSideCheck); // Rename temp tile as a wall floor
// Places some walls for tiles not yet wall checked
add_object("obj_wall_fill", coord[checkCol, checkRow, tDepth], fillObj);
sideCheck = tile[checkCol, checkRow, tDepth]; // Update vars
checkName = sideCheck.id;
replace_navigation(sideCheck, WALK_AND_INTERACT, INTERACT_ONLY) // Make side tile unwalkable
end
end
end;
// TO DO: Clean-up if wall count == 4 clear tile and reset to black so no isolated tiles
// All the neighbour checking is done. Populate tile contents and nav settings
// POPULATE TILE CONTENTS
//print("Wall count = " + wallCount); // Debug
//print("Populate tile contents");
objCheck = "Obj" + col + ":" + row;
oCheck = entity[objCheck]; // TO DO: Why is this here?
if entity[objCheck] == null and tCheck.id == tName then // There's nothing here and it's a floor tile
// TO DO: Check for other object and tile types?
// TO DO: Also check if it's a wall to allow objs against walls -> if oCheck.property["wDir"] != null then
// PUT PLAYER:
if player.coord.x == 0 and player.coord.y == -20 then // If player is still at origin
print("Put player at " + col + ":" + row); // Debug
put_player(tile[col, row, tDepth].id);
elseif player.coord.x != col and player.coord.y != row then // Check we're not placing on top of the player
// TO DO: Set frequencies as vars
// ALCOVES:
// Do alcoves first or they clash with door finding
if wallCount == 3 then // We're in an alcove
print("Alcove found");
// TRUNKS:
if random(1, 100) < 20 then // Add trunk
thingName = "Trunk" + col + ":" + row;
add_object("trunk_2", coord[col, row, tDepth], thingName);
// GARGOYLES:
elseif random(1, 100) < 25 then // Add gargoyle
thingName = "Alcove" + col + ":" + row;
add_object("gargoyle_1", coord[col, row, tDepth], thingName);
// CAGES:
elseif random(1, 100) < 33 then // Add gargoyle
thingName = "Alcove" + col + ":" + row;
add_object("cage_1", coord[col, row, tDepth], thingName);
// MANACLES:
elseif random(1, 100) < 50 then // Add gargoyle
thingName = "Alcove" + col + ":" + row;
add_object("manacles_1", coord[col, row, tDepth], thingName);
// BARRELS:
else
thingName = "Barrel" + col + ":" + row;
add_object("barrel_4", coord[col, row, tDepth], thingName);
end;
// TO DO: When alcove is next to door, opening door overrides INTERACT_ONLY
replace_navigation(tile[col, row, tDepth], WALK_AND_INTERACT, INTERACT_ONLY); // Set alcove tile to interact only
// Turn to face opening
if entity[wallObj + "north"] == null then
entity[thingName].direction = NORTH;
elseif entity[wallObj + "south"] == null then
entity[thingName].direction = SOUTH;
elseif entity[wallObj + "east"] == null then
entity[thingName].direction = EAST;
else // West
entity[thingName].direction = WEST;
end;
// TO DO: Other objects than trunks
// DOORS:
elseif entity[wallObj + "south"] != null and entity[wallObj + "north"] != null then
if entity[wallObj + "east"] == null and entity[wallObj + "west"] == null then
print("Add door");
dName = "Door" + col + ":" + row;
pickObj = random(1, 2); // Randomise door objects
pickObj = "door_EW" + pickObj;
// Add North-South door
add_object(pickObj, coord[col, row, tDepth], dName);
set_entity_script(entity[dName], "open_door_2");
// Swap both types of navigation
replace_navigation(tile[col, row, tDepth], WALK_ONLY, INTERACT_ONLY); // Set door tile to interact only
replace_navigation(tile[col, row, tDepth], WALK_AND_INTERACT, INTERACT_ONLY); // Set door tile to interact only
// TO DO: Set entity script here instead of default?
end
elseif entity[wallObj + "east"] != null and entity[wallObj + "west"] != null then
if entity[wallObj + "south"] == null and entity[wallObj + "north"] == null then
print("Add door");
dName = "Door" + col + ":" + row;
pickObj = random(1, 2); // Randomise door objects
pickObj = "door_NS" + pickObj;
// Add East-West door
add_object(pickObj, coord[col, row, tDepth], dName);
set_entity_script(entity[dName], "open_door_2");
// Swap both types of navigation
replace_navigation(tile[col, row, tDepth], WALK_ONLY, INTERACT_ONLY); // Set door tile to interact only
replace_navigation(tile[col, row, tDepth], WALK_AND_INTERACT, INTERACT_ONLY); // Set door tile to interact only
// TO DO: Set entity script here instead of default?
end;
// CORNERS:
// Must comes after doors
elseif wallCount == 2 then // We're in a corner
// CAVE WATER:
if random(1, 100) < 5 then // Add cave water
print("Place cave water at " + col + ":" + row); // Debug
tempName = "Pool" + col + ":" + row;
tempNum = random(1, 3);
objName = "cave_pool_" + tempNum;
add_object(objName, coord[col, row, tDepth], tempName);
// dungeonDirs = array[NORTH, EAST, SOUTH, WEST]; // TO DO: Check where this is also defined much earlier and move to start?
entity[tempName].direction = dungeonDirs[random(0, 3)];
play_animation(tempName, "default"); // Water anim
replace_navigation(tCheck, WALK_AND_INTERACT, INTERACT_ONLY);
end;
// MOB GENERATORS:
elseif random(1, 100) < 5 then // Add generator
print("Place generator at " + col + ":" + row); // Debug
gName = "Gen" + col + ":" + row;
// First set generator base tile
set_entity_model(tCheck, "generator_1");
// Then add generator roof/pillars object
add_object("generator_1", coord[col, row, tDepth], gName);
// TO DO: Add object on roof to spawn new mods?
// TO DO: Replace navigation tempoerarily on mob spawn?
// TO DO: Loop through neighbours fix interaction?
// EXIT HATCH:
elseif random(1, tCount) < (tCount / maxSizeX) and exitSet == false then // Add exit hatch near end of loop
// TO DO: Fix late tile count check
print("Place exit at " + col + ":" + row); // Debug
// TO DO: Remove existing navigation first? (Uses replace below)
set_entity_model(tCheck, "floor_hole");
//display_message ("Place exit"); // Debug
add_object("trapdoor", coord[col, row, tDepth], "exit");
//replace_navigation(tCheck, WALK_AND_INTERACT, INTERACT_ONLY); // TO DO: Allows running over trapdoor
exitSet = true;
// TO DO: Wall 'cave pool' water trickle
// SCONCES:
elseif random(1, 100) < 50 then // Add sconce
// TO DO: One sconce model with direction set
print("Place sconce at " + col + ":" + row); // Debug
sconceName = "Sconce" + col + ":" + row;
if entity[wallObj + "south"] != null then
add_object("wall_torch_1", coord[col, row, tDepth], sconceName);
entity[sconceName].direction = SOUTH;
elseif entity[wallObj + "north"] != null then
add_object("wall_torch_1", coord[col, row, tDepth], sconceName);
entity[sconceName].direction = NORTH;
elseif entity[wallObj + "east"] != null then
add_object("wall_torch_1", coord[col, row, tDepth], sconceName);
entity[sconceName].direction = EAST;
elseif entity[wallObj + "west"] != null then
add_object("wall_torch_1", coord[col, row, tDepth], sconceName);
entity[sconceName].direction = WEST;
end; // TO DO: Else other numbers
// Set tile to damage player
set_entity_script(tile[col, row, tDepth], "torch_burn_1", STOP_ON_TILE);
// TO DO: Check walkability
// BREAKABLE CRATES:
elseif random(1, 100) < 3 then // Add barrel
//display_message ("Place barrel"); // Debug
print("Place crate at " + col + ":" + row); // Debug
crateName = "crate" + col + ":" + row;
add_object("crate_breakable_1", coord[col, row, tDepth], crateName);
replace_navigation(tile[col, row, tDepth], WALK_AND_INTERACT, INTERACT_ONLY);
// dungeonDirs = array[NORTH, EAST, SOUTH, WEST]; // TO DO: Check where this is also defined much earlier and move to start?
entity[crateName].direction = dungeonDirs[random(0, 3)];
end;
end
end;
//print("Tile populated at " + col + ":" + row); // Debug
else // This was a black tile
replace_navigation(tCheck, WALK_AND_INTERACT, NONE);
end
end
end;
play_animation(player, "loading4"); // Advance loading anim
// TO DO: Double-check exit placement found a valid tile in above loop
//reset_camera();
//display_message("Ready to go (Click to proceed)"); // Debug
// TO DO: Give items for testing only
give_item("ITEM_0001", 1);
give_item("ITEM_0002", 1);
give_item("ITEM_0003", 1);
show_toolbar();
play_animation(player, "idle");
player.stat["max_hp"] = 100; // TO DO: For testing only
player.stat["hp"] = 100;
// TO Do: set_global_property("show_player_health", true) when possible
set_ambient_light_color(color[255, 212, 186]);
execute_script("spawn_mobs");
Happy scripting!
Categories: Featured.
Follow responses: RSS 2.0
Both comments and pings are currently closed.
Thanks for sharing that huge script! That shows what the engine is capable of if you go beyond simple interaction scripts. Wowza.