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