Links

Marcus on wikipedia
Marcus on Facebook
Marcus on Smashwords
Marcus on NOOK Books
Marcus on National Library

VOXELLENT DEV LOG

posted by Marcus, May 21, 2020 @ 6:12 am
Voxellent Main Menu
Voxellent Main Menu

VOXELLENT ON TWITTER:

‘Skelly’ Idle/Walk/Attack Cycles
‘Owlbeast’ Idle/Walk/Attack Cycles
‘Xbow Monk’ Idle/Walk/Attack Cycles
‘Adventurer’ Idle/Walk/Attack 1/Attack 2 Cycles
‘Gobby’ Idle/Walk/Attack/Hit/Death Cycles
Alchemy Table
Store
Mob Combat
Randomised Dungeons

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:

  1. Loop through all temp tiles giving them IDs and a random state – ‘floor’ or ‘black’ based on a map density variable
  2. 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
  3. 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
  4. Clean up walkability and interaction between tiles along the way
  5. Move the player to a valid starting tile and place exit
  6. Place lootable objects with variations for alcoves, corners, walls and open floor
  7. Find single-width corridors and place doors
  8. 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
  9. 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!

One Response to “VOXELLENT DEV LOG”

  1. Christian says:

    Thanks for sharing that huge script! That shows what the engine is capable of if you go beyond simple interaction scripts. Wowza.