Group: Member
Posts: 5
Type: None
RM Skill: Undisclosed
Random Dungeon Generator v1.0 Author: timm1980 (aka tkirch)
Introduction
This is my first script that I'm posting here, so bear with me, and please forgive my relative newness to the process.
Using this requires not only a script, but one important event, and a few special maps. See the example project for all the details.
Basically, the idea is, you make a script call when you're about to enter a dungeon, and the dungeon will be randomly generated based upon parameters you specify: width, height, depth, and seed. seed is what determines the effects of the random number generator. So a 5x5x3 dungeon with seed 0 will always be the same, but a 5x5x3 dungeon with any other seed value will be very different.
Give it a try and let me know what you think!
Features
Two different types of random dungeons can be created. Zelda-style: * A "maze" of individual rooms, forming a three dimensional structure * Each room is it's own mini-map, a part of the greater dungeon * Events, and other elements can be placed on cells to only appear in the "far point" (furthest distance from entrance) or in random cells (by using the RDZ_SPECIAL_VALUE variable to control whether or not the event is shown) * 16 Different cells are used, for each possible combination of paths top/bottom/left/right * 0 = no paths, 1 = bottom, 2 = left, 3 = bottom/left, 4 = top, 5 = top/bottom, 6 = bottom/left, 7 = top/bottom/left, 8 = right, 9 = right/bottom, 10 = right/left, 11 = right/left/bottom, 12 = right/top, 13 = right/top/bottom, 14 = right/top/left, 15 = right/left/top/bottom * Each of the cells must be the same size, but any size can be used (and these can be edited right within RPG Maker VX!) * Also note that the X coordinate of the top/bottom entry point from each cell must be the same across all cells, ditto for the Y coordinate of the left/right entry point * Because only a single cell is loaded at once as the game map, this doesn't use a lot of system resources... thus the overall dungeon implied internally can effectively be quite large (in fact, the script imposes no particular limit!) * Do note, though, that larger dungeons will take more time to generate... to be precise, the generation time is roughly linear with the number of cells in the dungeon, e.g. length * width * depth
Flat-map format, which is currently limited to a single floor, but the entire dungeon exists on a single flat map
* The single-floor dungeon is still constructed in the same maze-like manner, but it is all assembled into a single map * Cells (which are all rectangular blocks of tiles, 5x5 in my example project) are all cut from a single Map created in RMVX
Screenshots
(Coming soon!)
How to Use
I HIGHLY recommend looking at the demo, because the nature of the maps and events that must be precreated for this to work well are more than can be adequately described in text. (Despite my mini novel above)
# For a Zelda-style dungeon, these are the locations (x,y) on each cell # to where the player should be teleported when entering from the: # Bottom, Left, Top, Right, Upstairs, Downstairs RDZ_POS_XY = [[8, 11], [1, 7], [8, 1], [15, 7], [8, 7], [8, 7]]
# These are the 16 Map IDs to use to refer to each of the 16 possible # cells to display (0...15) Each map should have events on it (which # should be controlled by switches) for stairs up and down RDZ_MAP_ID = [18, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
# Describes which three variables are used to store the "entrance position" # (position on the world map or other map where the player is just before # entering the random dungeon) Before entering the dungeon, the in-game # event should store MapID, PlayerX, PlayerY to these three variables RDZ_ENTRANCE_POS = [1, 2, 3]
# Describes which three variables are used to store the "next position" # where the player will teleport to. The events already setup on the # example map cells do this already, but these three variables must be # specifically reserved for this purpose. They are MapID, X, Y, like above. RDZ_NEXT_POS = [4, 5, 6]
# The script reports back (in variables, which can be accessed by events) # the direction of the last move, and the current X,Y,Z location of the # player within the random dungeon # Special value is a value 0...65535 assigned to the cell randomly at # creation time (useful for placing random events only to appear occasionally) RDZ_LAST_MOVE = 7 RDZ_CURRENT_Z = 8 RDZ_CURRENT_Y = 9 RDZ_CURRENT_X = 10 RDZ_SPECIAL_VALUE = 11
# Designate two switches (which should be used by the stairs events # mentioned above) that are used by the script to report back which set(s) # of stairs should be enabled on each cell RDZ_STAIR_SWITCH = [1, 2]
# Designate two switches (which can be optionally used by any special # events) that are used by the script to report back whether the current # cell is the "far point" of the dungeon (further distance from entrance of # any cell), and whether or not it has a special designation RDZ_FAR_POINT = 3
# For a flat-floor style random dungeon, these three variables indicate # the MapID, X, Y to which the event should transfer the player after # calling the appropriate scripts to create the random dungeon RDF_NEXT_POS = [4, 5, 6]
# The event ID of the "exit dungeon" event on a flat-map style # dungeon. This is needed so that the script can place the event # appropriately after assembling the map RDF_EXIT_EVENT_ID = 4 RDF_STAIRS_UP_EVENT_ID = [1, 6, 7] RDF_STAIRS_DOWN_EVENT_ID = [2, 8, 9] RDF_SPECIAL_EVENT_ID = 3
# The default MAP_ID to be used as the source of tiling when # assembling a floor-style random dungeon RDF_DEFAULT_TILE_MAP_ID = 2
# (x,y) location (relative to upper left of a cell of tiles) # where the stairs and any chests/random-event should be placed RDF_STAIRS_LOCATION = [3, 2] RDF_CHEST_LOCATION = [1, 2]
# A fraction representing the proportion of upper layer (Tileset B...E) # tiles to copy; higher number = less copied (1/x) RDF_EMBELLISHMENT_COPY = 7
### End of modification Section
# The following are overrides/modifications to the built-in default # classes, to allow us to do some of what is needed with setting up # maps to hold a randomly generated dungeon.
class Spriteset_Map def set_map_data(data) @tilemap.map_data = data end end
class Scene_Map def set_map_data(data) @spriteset.set_map_data(data) end end
class Game_Event def set_map_id(map_id) @map_id = map_id if @event.name.include?("$rand") end end
class Game_Interpreter alias tkirch_rd_command_123 command_123 def command_123 if @original_event_id > 0 key = [$game_map.random_map_id, @original_event_id, @params[0]] $game_self_switches[key] = (@params[1] == 0) end $game_map.need_refresh = true return true end
alias tkirch_rd_command_111 command_111 def command_111 result = false case @params[0] when 0 # Switch result = ($game_switches[@params[1]] == (@params[2] == 0)) when 1 # Variable value1 = $game_variables[@params[1]] if @params[2] == 0 value2 = @params[3] else value2 = $game_variables[@params[3]] end case @params[4] when 0 # value1 is equal to value2 result = (value1 == value2) when 1 # value1 is greater than or equal to value2 result = (value1 >= value2) when 2 # value1 is less than or equal to value2 result = (value1 <= value2) when 3 # value1 is greater than value2 result = (value1 > value2) when 4 # value1 is less than value2 result = (value1 < value2) when 5 # value1 is not equal to value2 result = (value1 != value2) end when 2 # Self switch if @original_event_id > 0 key = [$game_map.random_map_id, @original_event_id, @params[1]] if @params[2] == 0 result = ($game_self_switches[key] == true) else result = ($game_self_switches[key] != true) end end when 3 # Timer if $game_system.timer_working sec = $game_system.timer / Graphics.frame_rate if @params[2] == 0 result = (sec >= @params[1]) else result = (sec <= @params[1]) end end when 4 # Actor actor = $game_actors[@params[1]] if actor != nil case @params[2] when 0 # in party result = ($game_party.members.include?(actor)) when 1 # name result = (actor.name == @params[3]) when 2 # skill result = (actor.skill_learn?($data_skills[@params[3]])) when 3 # weapon result = (actor.weapons.include?($data_weapons[@params[3]])) when 4 # armor result = (actor.armors.include?($data_armors[@params[3]])) when 5 # state result = (actor.state?(@params[3])) end end when 5 # Enemy enemy = $game_troop.members[@params[1]] if enemy != nil case @params[2] when 0 # appear result = (enemy.exist?) when 1 # state result = (enemy.state?(@params[3])) end end when 6 # Character character = get_character(@params[1]) if character != nil result = (character.direction == @params[2]) end when 7 # Gold if @params[2] == 0 result = ($game_party.gold >= @params[1]) else result = ($game_party.gold <= @params[1]) end when 8 # Item result = $game_party.has_item?($data_items[@params[1]]) when 9 # Weapon result = $game_party.has_item?($data_weapons[@params[1]], @params[2]) when 10 # Armor result = $game_party.has_item?($data_armors[@params[1]], @params[2]) when 11 # Button result = Input.press?(@params[1]) when 12 # Script result = eval(@params[1]) when 13 # Vehicle result = ($game_player.vehicle_type == @params[1]) end @branch[@indent] = result # Store determination results in hash if @branch[@indent] == true @branch.delete(@indent) return true end return command_skip end
end
class Scene_File alias tkirch_rd_write_save_data write_save_data def write_save_data(file) tkirch_rd_write_save_data(file) if $game_random_dungeon != nil Marshal.dump($game_random_dungeon, file) end end
alias tkirch_rd_read_save_data read_save_data def read_save_data(file) tkirch_rd_read_save_data(file) begin $game_random_dungeon = Marshal.load(file) rescue
end end
end
class Game_Map def set_random_id(id) @random_map_id = id end
def random_map_id if @random_map_id == nil @random_map_id = @map_id end return @random_map_id end
alias tkirch_rd_setup setup def setup(map_id) @random_map_id = map_id tkirch_rd_setup(map_id) end
def set_map_data(width, height, data) @map.width = width @map.height = height @map.data = data end
def setup_rdz_events(random_map_id) for i in @events.keys @events[i].set_map_id(random_map_id) end end
for event in @map.events.values if event.name.include?("$rand") and empty_positions.size > 0 data_key = empty_positions.keys.sort[0] location = empty_positions[data_key] xx = location[0] yy = location[1] @events[event.id] = Game_Event.new(key_map_id, event) @events[event.id].moveto(xx * tile_width + RDF_CHEST_LOCATION[0], yy * tile_height + RDF_CHEST_LOCATION[1]) @events[event.id].refresh empty_positions.delete(data_key) end end end
def generateFromVariables $game_random_dungeon = RDF.new($game_variables[14], $game_variables[14], $game_variables[15], $game_variables[13], 2) end end
class Scene_Title alias tkirch_rd_create_game_objects create_game_objects def create_game_objects tkirch_rd_create_game_objects $game_random_dungeon = nil end end
# The base class for creating a random dungeon layout # Basically, it's a 3-dimensional array [z][y][x] with a single # integer describing each "cell" of the dungeon # In a Zelda-style dungeon, each cell is a screen # In a flat-style dungeon, each cell is a rectangular area of tiles # in the larger created map (in the example created, each cell is 5x5 tiles # but they can be basically any size) # # The random number is 24 bits in length. The lower 8 bits describe the # nature of the cell (see in code below), the upper 16 bits are a random # value that can be used to determine random, but fixed display properties # (intended for things like cosmetic decorations, etc, not yet implemented) class Random_Dungeon_Base attr_accessor :width attr_accessor :height attr_accessor :depth attr_accessor :cells
# These constants describe the bitmask used by the lower 8 bits of the # cell description value; they indicate if there's an opening to the bottom, # left, top, right; if there's a stairway up or down, and a "special" value, # indicating that the cell is the furthest one from the entrance to the # dungeon BT = 1 LT = 2 TP = 4 RT = 8 UP = 16 DW = 32 SP = 64 FN = 128
# Initializer, just sets up the array, etc def initialize(width, height, depth) @width = width @height = height @depth = depth @cells = Array.new(@depth) { Array.new(@height) { Array.new(@width, 0) } } end
# Make the random dungeon # seed is the value to determine "which" random dungeon to generate # cutProb = probability that a cell will have an exit in any given direction # joinProb = probability that a cell will have an exit that reconnects to an # already-explored portion of the dungeon # sProb = probability of a cell having an up or down staircase # sJoinProb = probability a cell has a staircase that reconnects to an # already-explored portion of the dungeon (e.g. redundant staircase) # maxS = max number of staircases descending from any single floor # Leaving out an in-depth discussion of the internals of how this method # works, it will suffice to say that it basically executes a breath-first # search, starting from the entrance, and continuing until no new paths # are (randomly selected to be) created def make(seed = 0, cutProb = 0.5, joinProb = 0.2, sProb = 0.15, sJoinProb = 0.05, maxS = 3) oldseed = srand(seed) queue = [] startZ = 0 startY = @height - 1 startX = @width / 2 @cells[startZ][startY][startX] = BT queue << [startX, startY, startZ, 0] queue << [startX, startY, startZ, 0] queue << [startX, startY, startZ, 0] farP = [startX, startY, startZ] farS = 1 stCount = Array.new(@depth, 0) while queue.length > 0 curPos = queue.delete_at(0) curX = curPos[0] curY = curPos[1] curZ = curPos[2] curS = curPos[3] if curS > farS farP = [curX, curY, curZ] farS = curS end if curY < @height - 1 modify = false if @cells[curZ][curY + 1][curX] > 0 modify = true if rand() < joinProb else if rand() < cutProb modify = true queue << [curX, curY + 1, curZ, curS + 1] end end if modify @cells[curZ][curY][curX] |= BT @cells[curZ][curY + 1][curX] |= TP end end if curX > 0 modify = false if @cells[curZ][curY][curX - 1] > 0 modify = true if rand() < joinProb else if rand() < cutProb modify = true queue << [curX - 1, curY, curZ, curS + 1] end end if modify @cells[curZ][curY][curX] |= LT @cells[curZ][curY][curX - 1] |= RT end end if curY > 0 modify = false if @cells[curZ][curY - 1][curX] > 0 modify = true if rand() < joinProb else if rand() < cutProb modify = true queue << [curX, curY - 1, curZ, curS + 1] end end if modify @cells[curZ][curY][curX] |= TP @cells[curZ][curY - 1][curX] |= BT end end if curX < @width - 1 modify = false if @cells[curZ][curY][curX + 1] > 0 modify = true if rand() < joinProb else if rand() < cutProb modify = true queue << [curX + 1, curY, curZ, curS + 1] end end if modify @cells[curZ][curY][curX] |= RT @cells[curZ][curY][curX + 1] |= LT end end if curZ > 0 && stCount[curZ - 1] < maxS && (@cells[curZ][curY][curX] & DW == 0) modify = false if @cells[curZ - 1][curY][curX] > 0 modify = true if rand() < sJoinProb else if rand() < sProb modify = true queue << [curX, curY, curZ - 1, curS + 1] end end if modify @cells[curZ][curY][curX] |= UP @cells[curZ - 1][curY][curX] |= DW stCount[curZ - 1] = stCount[curZ - 1] + 1 end end if curZ < @depth - 1 && stCount[curZ] < maxS && (@cells[curZ][curY][curX] & UP == 0) modify = false if @cells[curZ + 1][curY][curX] > 0 modify = true if rand() < sJoinProb else if rand() < sProb modify = true queue << [curX, curY, curZ + 1, curS + 1] end end if modify @cells[curZ][curY][curX] |= DW @cells[curZ + 1][curY][curX] |= UP stCount[curZ] = stCount[curZ] + 1 end end end @cells[farP[2]][farP[1]][farP[0]] |= SP @cells.each { |level| level.each { |row| row.each_index { |i| row[i] |= (256 * rand(65536)) } } } srand(oldseed) end
end
# This is the instance of the class that creates a Zelda-style random dungeon # Initialize with RDZ.new(height, width, depth, seed) class RDZ < Random_Dungeon_Base
# The next six methods are already called by the example-tiles # The "tell" the random dungeon that we want to go in a certain # direction, which sets up the variables that tell the game # where to next move def goRight @curX += 1 @lastMove = 1 setVars end def goLeft @curX -= 1 @lastMove = 3 setVars end def goTop @curY -= 1 @lastMove = 0 setVars end def goBottom @curY += 1 @lastMove = 2 setVars end def goUp @curZ -= 1 @lastMove = 4 setVars end def goDown @curZ += 1 @lastMove = 5 setVars end
# Set some game variables based upon the "state" of (our position in) the random dungeon def setVars if curY == @height $game_variables[RDZ_NEXT_POS[0]] = $game_variables[RDZ_ENTRANCE_POS[0]] $game_variables[RDZ_NEXT_POS[1]] = $game_variables[RDZ_ENTRANCE_POS[1]] $game_variables[RDZ_NEXT_POS[2]] = $game_variables[RDZ_ENTRANCE_POS[2]] return end cell = @cells[curZ][curY][curX] $game_variables[RDZ_NEXT_POS[0]] = RDZ_MAP_ID[cell & 15] $game_variables[RDZ_NEXT_POS[1]] = RDZ_POS_XY[@lastMove][0] $game_variables[RDZ_NEXT_POS[2]] = RDZ_POS_XY[@lastMove][1] $game_switches[RDZ_STAIR_SWITCH[0]] = false $game_switches[RDZ_STAIR_SWITCH[1]] = false $game_switches[RDZ_STAIR_SWITCH[0]] = true if (cell & UP) == UP $game_switches[RDZ_STAIR_SWITCH[1]] = true if (cell & DW) == DW $game_switches[RDZ_FAR_POINT] = true if (cell & SP) == SP $game_variables[RDZ_LAST_MOVE] = @lastMove $game_variables[RDZ_CURRENT_Z] = @curZ $game_variables[RDZ_CURRENT_Y] = @curY $game_variables[RDZ_CURRENT_X] = @curX $game_variables[RDZ_SPECIAL_VALUE] = cell / 256 end
def fillRow(z, y) @cells[z][y].each_index { |x| copyTile(x, y, z, @cells[z][y][x] & 15, @cells[z][y][x] / 256) } end
def copyTile(x, y, z, tileNum, rval) tilex = (tileNum % 4) * (@tile_width + 1) tiley = (tileNum / 4) * (@tile_height + 1) mapx = x * @tile_width mapy = y * @tile_height a = 16807 r = rval % 65536 c = 13 m = 2147483647 for xx in 0...@tile_width for yy in 0...@tile_height @map_data[z][mapx + xx, mapy + yy, 0] = @tiling_map.data[tilex + xx, tiley + yy, 0] #@map_data[z][mapx + xx, mapy + yy, 1] = @tiling_map.data[tilex + xx, tiley + yy, 1] @map_data[z][mapx + xx, mapy + yy, 2] = @tiling_map.data[tilex + xx, tiley + yy, 2] if r % RDF_EMBELLISHMENT_COPY == 0 r = (a * r + c) % m end end end
end
Credit and Thanks
None of this script comes from anyone else; the idea for it was loosely borrowed from both Dragon Quest 9, and the original Legend of Zelda, however.
Authors Notes
There is no arbitrary limit on how large of a dungeon can be created; larger dungeons will be slower to create and load, and may approach the limits of what RMVX can handle well, however.
Currently, I know this won't work right if you save a game while inside of a random dungeon.
Likely Future Features to Include
* Ability to Save/Load while in a random-dungeon (either Zelda or flat style) * Flat-style dungeons will multiple floors * Flat-style dungeons will randomly placed treasure chests or other events * Flat-style dungeons automatically copy only some random layer two map features (I'm thinking for embellishments like moss, skeletons, etc, so that each cell of the finished map looks a bit different, despite being created from only 16 cells) * Any other ideas that are suggested that seem feasible and worthwhile?
This post has been edited by timm1980: Jul 20 2011, 06:45 PM
I like this, but I see some issues that I must address
There is the error on loading a game saved in a random dungeon, like you said.
The main issue however, is that it is easy to get lost in the second one - the one with transfers. This is because it makes it almost impossible to traverse. Unless I'm missing something.
I was however, looking for something like this to create a LOZ-esque lost woods map. And Very nice job man.
__________________________
CURRENT WORKS DEMON BLADE - RPGVXA SHINIGAMI - ~12 Pages - 3 chapters complete, 1 in progress. --------------------------------- OTHER WORKS INCANTA-corrupted. INCANTA REDUX - RPGVX - On Hold --------------------------------- LITERARY WORKS
Longer Works ANGEL OF DEATH - Short Story ~ 4 Pages. SHINIGAMI - ~12 Pages - 3 chapters complete, 1 in progress. DEMON BLADE - Book/Novel?- 34 pages - On Hold. FERAL - Short Story, Length: 6 pages], 2nd person Narrative. -R3 Writing Competition #2 - First-Place DARKSPAWN - Book - 3 Pages On Hold RELGEA CHRONICLE - Book - 118 pages DRAKENGHOUL - book - 36 handwritten pages
Poems I KNOW - Poem - 30 Lines
Song Lyrics End of Days - Song- 44 Lines Kids Killing Kids - Song - 94 Lines
-If you want this sig in another language, move to a country that speaks it.-
-Lv 13 Thread Killer
My R3 Writing Corner
My Wordpress
Relgea Chronicle
X-M-O Story Quoting.
QUOTE (X-M-O @ Jun 19 2012, 02:45 AM)
QUOTE (Pharonix)
so what's going on in this thread?
QUOTE (kayden997)
Redd's back and the first thing he does is...
QUOTE (Pharonix)
pick my mom up in 15 ...*sigh*
QUOTE (kayden997)
You know it's legit!
QUOTE (Pharonix)
Then again, I wrecked the last one...........
QUOTE (Tsutanai)
Oh ok you can have a pink frosted sprinkled doughnut
Group: Member
Posts: 5
Type: None
RM Skill: Undisclosed
QUOTE (Pharonix @ Jul 19 2011, 06:08 PM)
I like this, but I see some issues that I must address
There is the error on loading a game saved in a random dungeon, like you said.
The main issue however, is that it is easy to get lost in the second one - the one with transfers. This is because it makes it almost impossible to traverse. Unless I'm missing something.
I was however, looking for something like this to create a LOZ-esque lost woods map. And Very nice job man.
WIthin the next day or two, I should be ready to post an updated version, which fixes the save issue, allows multiple floors in a flat-style dungeon (the one without traversing), and several other improvements.
The zelda-style maps, with transfers from room to room, I agree, are very difficult to traverse. I wonder if adding in an auto-mapping feature of some kind might be worth looking into.. hmmm...
That said, I personally prefer the flat style, as it's more like what I expect in this style of RPG. If there's interest in the zelda-style, however, I'm happy to look into doing that as well.
Thanks for looking at it!
This post has been edited by timm1980: Jul 19 2011, 09:07 PM
Group: Member
Posts: 5
Type: None
RM Skill: Undisclosed
QUOTE (Helios @ Jul 23 2011, 07:39 AM)
You could allow users to create multiple version of the same map junction, and the game randomly choose one of them when creating maps.
Placing events (especially fixed ones like chest/rock/switch etc) randomly will also help.
The way the latest version works, if you create an event with $rand in the name, it will randomly pick a cell in which to place it. Any Tileset B-E tiles are randomly used (everything from Tileset A is used in the final map)
I definitely like the suggestion about multiple different junctions, and choosing them randomly. Wouldn't be too hard to implement, and would add some nice variety.
Group: Member
Posts: 54
Type: None
RM Skill: Undisclosed
Some random thoughts while trying out the demo::
QUOTE
# The event ID of the "exit dungeon" event on a flat-map style # dungeon. This is needed so that the script can place the event # appropriately after assembling the map RDF_EXIT_EVENT_ID = 4 RDF_STAIRS_UP_EVENT_ID = [1, 6, 7] RDF_STAIRS_DOWN_EVENT_ID = [2, 8, 9] RDF_SPECIAL_EVENT_ID = 3
Better use something else, like key words in comment or event name as flag... event ID is inconvenient.
QUOTE
# (x,y) location (relative to upper left of a cell of tiles) # where the stairs and any chests/random-event should be placed RDF_STAIRS_LOCATION = [3, 2] RDF_CHEST_LOCATION = [1, 2]
So every event's position in the cell is fixed? and so do the max number of event, which is always one? Not so random IMHO...
This post has been edited by Helios: Aug 14 2011, 07:49 PM