More layouts

Published July 26, 2008
Advertisement
After the last journal post, I got to thinking about how I tend to not like the layouts that accretion algorithm generates, especially with large numbers of boxes. There tends to develop a very tendril-like, tentacled appearance to the layouts, as later boxes append themselves to protrusions, thus increasing the protrusions. This can tend to make the structure grow unpredictably in certain directions. A technique I had an idea for, but didn't get around to until this morning, is to start with a collection of non-intersecting boxes and sort of collapse them inward to a central point, until they all intersect. It seemed like it would be a good technique for compact dungeon layouts, so I whipped up the following Lua script to test it out:

function box_intersect(box1, box2)  local ax1,ay1,ax2,ay2=box1[1],box1[2],box1[1]+box1[3],box1[2]+box1[4]  local bx1,by1,bx2,by2=box2[1],box2[2],box2[1]+box2[3],box2[2]+box2[4]    -- Disallow single-corner intersections  if bx1==ax2 and by1==ay2 then return false end  if bx2==ax1 and by1==ay2 then return false end  if bx2==ax1 and by2==ay1 then return false end  if bx1==ax2 and by2==ay1 then return false end      if bx1<=ax2 and bx2>=ax1 and by1<=ay2 and by2>=ay1 then return true  else return false endendfunction group_intersect(group1, group2)  local i,j    for i=1,#group1.boxlist,1 do    local b1=group1.boxlist    for j=1,#group2.boxlist,1 do      local b2=group2.boxlist[j]            if box_intersect(b1, b2)==true then return true end    end  end    return falseendfunction calc_bounding_box(group)  local i  minx,miny=100000, 100000  maxx,maxy=-100000, -100000    for i=1,#group.boxlist,1 do    local box=group.boxlist    if box[1]1] end    if box[2]2] end    if box[1]+box[3]>maxx then maxx=box[1]+box[3] end    if box[2]+box[4]>maxy then maxy=box[2]+box[4] end  end    group.minx,group.miny,group.maxx,group.maxy=minx,miny,maxx,maxy  group.cx=minx+((maxx-minx)/2)  group.cy=miny+((maxy-miny)/2)endfunction merge_group(group1, group2)  local i  for i=1,#group2.boxlist,1 do    table.insert(group1.boxlist, group2.boxlist)  end  calc_bounding_box(group1)endfunction translate_group(group, xinc, yinc)  local i    for i=1,#group.boxlist,1 do    local box=group.boxlist    box[1]=box[1]+xinc    box[2]=box[2]+yinc  end    calc_bounding_box(group)endfunction calc_grouplist_bounding_box(grouplist)  -- Calculate bounding box and local center of a list of groups  local i  local minx,miny,maxx,maxy=100000,100000,-100000,-100000    for i=1,#grouplist.groups,1 do    local group=grouplist.groups    if group.minx    if group.maxx>maxx then maxx=group.maxx end    if group.miny    if group.maxy>maxy then maxy=group.maxy end  end    grouplist.minx, grouplist.miny, grouplist.maxx, grouplist.maxy=minx,miny,maxx,maxy  grouplist.cx=minx+((maxx-minx)/2)  grouplist.cy=minx+((maxy-miny)/2)endfunction intersect_groups(grouplist, group)  -- Test all groups in grouplist for intersection with group, and merge, then repop grouplist  local holdlist={}    for i=1,#grouplist.groups,1 do    local thisgroup=table.remove(grouplist.groups)    if group_intersect(group, thisgroup) then      -- Groups intersect, merge into group      merge_group(group, thisgroup)    else      table.insert(holdlist, thisgroup)    end  end    -- Intersected groups should now be in group, repopulate grouplist with the holdlist  for i=1,#holdlist,1 do    table.insert(grouplist.groups, holdlist)  end  table.insert(grouplist.groups, 1, group)  calc_grouplist_bounding_box(grouplist)endfunction move_group(grouplist, group, dir)  -- Move the given group 1 unit in the given direction, test for intersections with other groups, and merge  local xinc, yinc=0,0    if dir==1 then    xinc,yinc=0,-1  elseif dir==2 then    xinc,yinc=0,1  elseif dir==3 then    xinc,yinc=-1,0  else    xinc,yinc=1,0  end    translate_group(group, xinc, yinc)  intersect_groups(grouplist, group)endfunction move_one_group(grouplist)  local group=table.remove(grouplist.groups)  local cx,cy=group.cx,group.cy      local dir  if math.abs(cx)>math.abs(cy) then    -- Horizontal direction    if cx<0 then dir=4    else dir=3    end  else    if cy<0 then dir=2    else dir=1    end  end      move_group(grouplist, group, dir)end    function move_all_groups(grouplist)  local i  local holdlist={}    while #grouplist.groups > 1 do    move_one_group(grouplist)  endendfunction new_group(box)  -- Create a group from a box  local group={}  group.boxlist={}  table.insert(group.boxlist, box)  calc_bounding_box(group)    return groupendfunction new_grouplist()  -- Create a new grouplist  local grouplist={}  grouplist.groups={}  grouplist.minx,grouplist.maxx,grouplist.miny,grouplist.maxy=0,0,0,0  grouplist.cx,grouplist.cy=0,0  return grouplistendfunction add_group(grouplist, group)  table.insert(grouplist.groups, group)  calc_grouplist_bounding_box(grouplist)endfunction swap_elements(whichlist, a, b)  local temp=whichlist[a]  whichlist[a]=whichlist  whichlist=tempendfunction shuffle_list(whichlist)  for i=1,#whichlist,1 do    local a=randRange(1,#whichlist)    swap_elements(whichlist, i, a)  endendfunction seed_boxes(grouplist, numx, numy, minwidth, maxwidth, minheight, maxheight, startx, starty)  -- Seed a grouplist with groups.  -- Each group is a single box. Boxes are placed in rows and columns, spaced by maxwidth/maxheight  local holdlist={}  local x,y  for x=0,numx-1,1 do    for y=0,numy-1,1 do      local bx,by = startx+x*(maxwidth+3), starty+y*(maxheight+3)      local bw,bh=randRange(minwidth,maxwidth), randRange(minheight, maxheight)      local group=new_group({bx,by,bw,bh})      table.insert(holdlist, group)    end  end    shuffle_list(holdlist)  for i=1,#holdlist,1 do    add_group(grouplist, holdlist)  end      end    function draw_box(box, buffer, tx, ty, v)  local x1,y1,x2,y2=box[1]+tx,box[2]+ty,box[1]+box[3]+tx, box[2]+box[4]+ty    for x=x1,x2,1 do    buffer:set(x,y1,1.0)    buffer:set(x,y2,1.0)  end  for y=y1,y2,1 do    buffer:set(x1,y,1.0)    buffer:set(x2,y,1.0)  end    for x=x1+1, x2-1, 1 do    for y=y1+1, y2-1, 1 do      buffer:set(x,y,v)    end  end  endfunction draw_box_list(boxtable, buffer)  local tx=-boxtable.minx  local ty=-boxtable.miny    local w=(boxtable.maxx-boxtable.minx)+1  local h=(boxtable.maxy-boxtable.miny)+2  buffer:init(w,h)    for i=1,#boxtable.boxlist,1 do    local v=i    while v>20 do v=v-20 end    v=v/20    draw_box(boxtable.boxlist, buffer, tx, ty,v)  endendfunction draw_group(group, tx, ty, buffer)  for i=1,#group.boxlist,1 do    draw_box(group.boxlist, buffer, tx, ty, 0.5)  endendfunction draw_grouplist(grouplist, buffer)  local tx=-grouplist.minx  local ty=-grouplist.miny    local w=(grouplist.maxx-grouplist.minx)+2  local h=(grouplist.maxy-grouplist.miny)+2  buffer:init(w,h)    for i=1,#grouplist.groups,1 do    draw_group(grouplist.groups, tx, ty, buffer)  end  buffer:set(tx,ty,0.6)end


The script is executed in the Accidental Noise Library Lua interpreter, then groups of boxes can be made and collapsed:

gl=new_grouplist()-- Seed boxes-- Parameters are: grouplist, number_of_boxes_wide, number_of_boxes_high, minwidth, maxwidth, minheight, maxheight, starting_x, starting_yseed_boxes(gl, 10, 10 10, 60, 10, 60, -300, -300)-- Collapse all the boxes inwardmove_all_groups(gl)b=CArray2Dd()draw_grouplist(gl,b)saveBufferToTGA(b,"grouplist.tga")


The code functions by splitting the collection of boxes into groups. Initially when the grouplist is seeded with boxes, each box is a separate group. The function move_all_groups iteratively selects a group to move, and translates it toward the center along either the X or the Y axis, depending on whether it is further from the center along the X axis or the Y axis. It moves the group 1 unit at a time, then tests for intersection with all other groups in the list. Any groups it intersects with are removed from the main list and merged into the current group. The intersection test ignores intersection cases where only 1 corner of each box intersects, to prevent degenerate intersections that would not result in a shared edge between two boxes. This sort of layout depends on boxes sharing edges, so that doorways or passages between box sections may be generated later in the process.

The algorithm continues selecting groups and moving them inward toward (0,0) until there is only 1 group left, at which point it exits and the layout is done.

Here is a sample seeded grouplist:


and here is the same group collapsed inward until intersection:



As can be seen, there isn't as much tendency for the layout to develop 'tentacles'; the layout tends to be more compact, and fit more into the shape of the original seeded array.

Of course the algorithm as presented could use some tuning, but there are again a few ways to tweak it's behavior. The initial shape of the starting array has an effect on the shape of the final version. Here is a layout that begins wide:



and collapsed:



Once you have the layout, you can analyze the intersections to produce a graph of edge info, showing which boxes connect with which other boxes, where their shared edges are, etc... Then types for each section can be assigned, and the final sections with connection and type data can be passed to the next level of refinement for room generation.
Previous Entry Layouts
Next Entry Back to the islands
0 likes 1 comments

Comments

Jotaf
This is cool :)

Procedural level generation is one of those topics that never loses its interest. Anyone can whip up a simple dungeon, but interesting levels are much more difficult to produce. Not because you need complex algorithms, but because you need genuinely creative algorithms. This is one of those cases ;)

I suggest you drop this description in the RGRD group at http://groups.google.com/group/rec.games.roguelike.development/topics , the folks there would be very interested. Despite the crudeness of the ASCII displays used by most roguelikes, there is a strong emphasis on dungeon generation and discussions on this topic are common.
July 27, 2008 10:05 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement