Let's Make a Game - Procedual Content Creation (part 03)
About: This is a short series for the Leadwerks community on the process of creating a simple game using procedural content.
This week we implement the map creation process discussed in part 02 then add the mesh generation and a controller to fly around our level. Then we'll have the first iteration of our procedurally generated map.
Starting with the Map class.
Map:Create Function...first iteration, no corridors.
Simple nested for-loop to generate a room for each cell. Each room is stored in the table Map.Room[] with dimensions, a counter for reference and a cell offset in world units.
For our example a cell is a virtual 40x40 space, our example map is made of 4 x 4 cells.
We call the function like this...
Map:Create (407, 4 , 4 , 40 )
And our function definition is...
function Map:Create( seed , xsize , ysize , cellscale , roomscale ) self.xsize = xsize self.ysize = ysize self.seed = seed self.roomscale = roomscale math.randomseed( seed ) if roomscale == nil then roomscale = 1.0 end Map.roomscale = roomscale if cellscale == nil then cellscale = 40.0 end Map.cellscale = cellscale Map.roomheight = 2.0 Print("Creating map, dimensions " .. xsize .. " x " .. ysize) -- table to store our rooms Map.Room = {} -- room counter rcount = 1 for x=1,xsize do for y=1,ysize do if math.random() > 0.2 then self.Room[rcount] = {} local r = self.Room[rcount] r.roomID = rcount r.cellx = x r.celly = y r.x = math.random(cellscale) r.y = math.random(cellscale) r.width = math.ceil( math.random(3 , cellscale ) ) r.length = math.ceil( math.random(3 , cellscale ) ) r.info = string.format("id %.2d cellx %.2d celly %.2d x:%.3d y:%.3d w:%d h:%d", r.roomID , r.cellx , r.celly , r.x , r.y , r.width , r.length ) print(r.info) rcount = rcount+1 end end end self:MakeGeometry() end
In the above code to reduce rooms arranged in a solid 'grid' there's a random chance that a cell skips room creation. The cell offset ( r.x and r.y ) serves to add more irregularity to the layout.
Map.roomscale and Map.cellscale: bigger number = bigger space. One scales distance between rooms, the other scales the room mesh.
After generating the geometry from such an arrangement we get this...
I've added a head-up display to show an overhead map and annotations to the screenshot to show how the distribution of rooms work. So far so good.
Perhaps worth mentioning the CELLS are ordered top to bottom then left to right. That gives you some idea that the random offset works to radically shift rooms around to avoid being too attached to the grid arrangement.
Before we generate any geometry for our rooms we have all the data we need to display a map (like the one above). Debug overlays or HUDs are handy during early development and can be migrated to a finished game HUD later. All we need right now is some way to check room volumes and positioning, some info and the players position. Later we can use a second camera to draw a top-down view if required.
Another feature we want in our HUD is some flag to draw it, a position to move it about the screen and a scale so we can fit the map to the whole screen or just squeeze it into a corner. The following code does all this. X and Y is typically used as a screen-coordinate position to draw an element and s is the size in pixels to fit the map into.
If you need to re-size text as well then you need to start rendering these to a buffer but we'll avoid the extra complexity. This is often made easier with OpenGL commands but they are not exposed to Leadwerks LUA scripts (as of 2.5).
Drawing the HUD and overhead map
function DrawHUD() if App.showhud ~= true then return end SetBlend(2) SetColorf(0.1,0.3,0.1,0.3) DrawText("ROOM DATA",22,22) local c = string.format("RoomCount %d", #Map.Room) DrawText(c,22,60) local x = 40 local y = 80 for n,r in pairs(Map.Room) do DrawText( r.info , x , y ) y = y + 16 end x = 400 y = 80 s = 512 sx = s / (Map.cellscale * Map.xsize) sy = s / (Map.cellscale * Map.ysize) DrawText("OVERHEAD MAP",x,y-20) --DrawLine(x,y,x+s,y) DrawLine(x+s,y,x+s,y+s) DrawLine(x+s,y+s,x,y+s) DrawLine(x,y+s,x,y) for n=0,Map.xsize do DrawLine(x + (n*Map.cellscale*sx) , y , x + (n*Map.cellscale*sx) , y + s) end for n=0,Map.ysize do DrawLine(x , y + (n*Map.cellscale*sy) , x + s , y + (n*Map.cellscale*sy)) end -- sorry for the fiddly math here -- its needed to match the map scale with the HUD display scale for n,r in pairs(Map.Room) do local roomx = x + (( r.x + (r.cellx-1)*Map.cellscale) *sx ) local roomy = y + (( r.y + (r.celly-1)*Map.cellscale) *sy ) DrawRect( roomx , roomy , r.width * sx, r.length * sy ) DrawText(string.format("%.2d",r.roomID), roomx + ((r.width*sx)*0.5)-8 , roomy + ((r.length*sx)*0.5)-9 ) end DrawText(string.format("view co-ords x:%d y:%d", camera.position.x , camera.position.z) , 40, GraphicsHeight() - 20 ) -- player marker cross in yellow SetColorf(0.3,0.3,0.1,0.3) DrawText("X", x + (camera.position.x * sx)-4 , y - (camera.position.z * sy) -8 ) SetBlend(0) end
The map size is adjusted by changing "s = 512" to however many pixels across. Objects are scaled and drawn accordingly. Once we start merging rooms this will need some alteration, the overhead camera might be a viable alternative and one we can have some fun with.
randomly flagged tile data, looks like a "BallBlazer" level.
Tiles and Geometry
Rather than write up how it works you can look through the code and tinker with it. The next and penultimate part will cover generating corridors and using "tile" data to merge overlapping rooms and add props like doors. You'll see already there are random light sources assigned to each room, random assortments of props such as columns, crates, particles etc. can be done in a similar fashion.
The Code for this article
I've attached the full code which creates the room data and geometry (sans no corridors and intelligent tile merging - we'll deal with that in part 04).
Don't forget to execute this script using ScriptEditor.exe (or run it with ENGINE.EXE) you will need to make sure the path at the top of START.LUA (MediaDir) points to your Leadwerks SDK location and the default scripts are present (as "required" at the top of the script).
You'll find a few bits of code commented out and older functions I used to create meshes. I've left them in for curiosity (learning and laughter).
Until part 04, have a good weekend.
- 4
12 Comments
Recommended Comments