Jump to content

Let's Make a Game - Procedual Content Creation (part 03)

Flexman

4,583 views

blog-0020693001336928269.jpgAbout: 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.

 

Part 01

Part 02

 

 

 

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...

blogentry-52-0-81331000-1337095915_thumb.jpg

 

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.

 

Info: CELL is a term I use to describe a container in virtual space.

 

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.

 

blogentry-52-0-25371600-1337098798_thumb.jpg

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.



12 Comments


Recommended Comments

This series of tutorial is more or less designed for my as I'm working on my game CELL's ;) Good work.

Share this comment


Link to comment

Nice of you to say so Roland. Of course I only use the word cell as reference to an area, a virtual space of n size.

 

If you make the patches small and room height to something like 8 patches then and add vertex noise you get some very lumpy cave like walls, there's plenty of room to experiment.

Share this comment


Link to comment

Thanks for this great set of tutorials I am finding them very enjoyable. I recently did a series of game assets for a project using only procedural textures it’s a really exciting way to work.

Share this comment


Link to comment

I should finish this tutorial sometime :) I remember I had to break off to finish my terrain editing book. It might be more interesting to continue this with Leadwerks 3 though. The Delaney function might come into it's own for this.

Share this comment


Link to comment

Nice article. Any chance for a 4th part? Or at least some short comment with a hint about how to merge rooms and connect them with corridors.

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 3
      Previously, we saw how the new renderer can combine multiple cameras and even multiple worlds in a single render to combine 3D and 2D graphics. During the process of implementing Z-sorting for multiple layers of transparency, I found that Vulkan does in fact respect rasterization order. That is, objects are in fact drawn in the same order you provide draw calls to a command buffer.
      Furthermore, individual primitives (polygons) are also rendered in the order they are stored in the indice buffer:
      Now if you were making a 2D game with 1000 zombie sprites onscreen you would undoubtedly want to use 3D-in-2D rendering with an orthographic camera. Batching and depth discard would give you much faster performance when the number of objects goes up. However, the 2D aspect of most games is relatively simple, with only a dozen or so 2D sprites making up the user interface. Given that 2D graphics are not normally going to be much of a bottleneck, and that the biggest performance savings we have achieved was in making text a static object, I decided to rework the 2D rendering system into something that was a little simpler to use.
      Sprites are no longer a 3D entity, but are a new type of pure 2D object. They act in a similar way as entities with position, rotation, and scale commands, but they only use 2D coordinates:
      //Create a sprite auto sprite = CreateSprite(world,100,100); //Make blue sprite->SetColor(0,0,1); //Position in upper-left corner of screen sprite->SetPosition(10,10) Sprites have a handle you can set. By default this is in the upper-left corner of the sprite, but you can change it to recenter them. Sprites can also be rotated around the Z axis:
      //Center the handle sprite->SetHandle(0.5,0.5); //Rotation around center sprite->SetRotation(45); SVG vector images are great for 2D drawing and GUIs because they can scale for different display resolutions. We support these as well, with an optional scale value the image can be rasterized at.
      auto sprite = LoadSprite(world, "tiger.svg", 0, 2.0);
      Text is now just another type of sprite:
      auto text = CreateSprite(world, font, L"Hello, how are you today?\nI am fine.", 72, TEXT_LEFT); These sprites are all displayed within the same world as the 3D rendering, so unlike what I previously wrote about...
      You do not have to create extra cameras or worlds just to draw 2D graphics. (If you are doing something advanced then the multi-camera method I previously described is a good option, but you have to have very demanding needs for it to make a difference.) Regular old screen coordinates you are used to will be used (coordinate [0,0] is top-left). By default sprites will be drawn in the order they are created. However, I definitely see a need for additional control here and I am open to ideas. Should there be a sprite order value, a MoveToFront() method, or a system of different layers? I'm not sure yet.
      I'm also not sure how per-camera sprites will be controlled. At this time sprites are stored in a per-world list, but we will want some 2D elements to only appear on some cameras. I am not sure yet how this will be controlled.
      I am going to try to get an update out soon with these features so you can try them out yourself.
    • By Josh in Josh's Dev Blog 11
      Previously I described how multiple cameras can be combined in the new renderer to create an unlimited depth buffer. That discussion lead into multi-world rendering and 2D drawing. Surprisingly, there is a lot of overlap in these features, and it makes sense to solve all of it at one time.
      Old 2D rendering systems are designed around the idea of storing a hierarchy of state changes. The renderer would crawl through the hierarchy and perform commands as it went along, rendering all 2D elements in the order they should appear. It made sense for the design of the first graphics cards, but this style of rendering is really inefficient on modern graphics hardware. Today's hardware works best with batches of objects, using the depth buffer to handle which object appears on top. We don't sort 3D objects back-to-front because it would be monstrously inefficient, so why should 2D graphics be any different?
      We can get much better results if we use the same fast rendering techniques we use for 3D graphics and apply it to 2D shapes. After all, the only difference between 3D and 2D rendering is the shape of the camera projection matrix. For this reason, Turbo Engine will use 2D-in-3D rendering for all 2D drawing. You can render a pure 2D scene by setting the camera projection mode to orthographic, or you can create a second orthographic camera and render it on top of your 3D scene. This has two big implications:
      Performance will be incredibly fast. I predict 100,000 uniquely textured sprites will render pretty much instantaneously. In fact anyone making a 2D PC game who is having trouble with performance will be interested in using Turbo Engine. Advanced 3D effects will be possible that we aren't used to seeing in 2D. For example, lighting works with 2D rendering with no problems, as you can see below. Mixing of 3D and 2D elements will be possible to make inventory systems and other UI items. Particles and other objects can be incorporated into the 2D display.
      The big difference you will need to adjust to is there are no 2D drawing commands. Instead you have persistent objects that use the same system as the 3D rendering.
      Sprites
      The primary 2D element you will work with is the Sprite entity, which works the same as the 3D sprites in Leadwerks 4. Instead of drawing rectangles in the order you want them to appear, you will use the Z position of each entity and let the depth buffer take care of the rest, just like we do with 3D rendering. I also am adding support for animation frames and other features, and these can be used with 2D or 3D rendering.

      Rotation and scaling of sprites is of course trivial. You could even use effects like distance fog! Add a vector joint to each entity to lock the Z axis in the same direction and Newton will transform into a nice 2D physics system.
      Camera Setup
      By default, with a zoom value of 1.0 an orthographic camera maps so that one meter in the world equals one screen pixel. We can position the camera so that world coordinates match screen coordinates, as shown in the image below.
      auto camera = CreateCamera(world); camera->SetProjectionMode(PROJECTION_ORTHOGRAPHIC); camera->SetRange(-1,1); iVec2 screensize = framebuffer->GetSize(); camera->SetPosition(screensize.x * 0.5, -screensize.y * 0.5); Note that unlike screen coordinates in Leadwerks 4, world coordinates point up in the positive direction.

      We can create a sprite and reset its center point to the upper left hand corner of the square like so:
      auto sprite = CreateSprite(world); sprite->mesh->Translate(0.5,-0.5,0); sprite->mesh->Finalize(); sprite->UpdateBounds(); And then we can position the sprite in the upper left-hand corner of the screen and scale it:
      sprite->SetColor(1,0,0); sprite->SetScale(200,50); sprite->SetPosition(10,-10,0);
      This would result in an image something like this, with precise alignment of screen pixels:

      Here's an idea: Remember the opening sequence in Super Metroid on SNES, when the entire world starts tilting back and forth? You could easily do that just by rotating the camera a bit.
      Displaying Text
      Instead of drawing text with a command, you will create a text model. This is a series of rectangles of the correct size with their texture coordinates set to display a letter for each rectangle. You can include a line return character in the text, and it will create a block of multiple lines of text in one object. (I may add support for text made out of polygons at a later time, but it's not a priority right now.)
      shared_ptr<Model> CreateText(shared_ptr<World> world, shared_ptr<Font> font, const std::wstring& text, const int size) The resulting model will have a material with the rasterized text applied to it, shown below with alpha blending disabled so you can see the mesh background. Texture coordinates are used to select each letter, so the font only has to be rasterized once for each size it is used at:

      Every piece of text you display needs to have a model created for it. If you are displaying the framerate or something else that changes frequently, then it makes sense to create a cache of models you use so your game isn't constantly creating new objects. If you wanted, you could modify the vertex colors of a text model to highlight a single word.

      And of course all kinds of spatial transformations are easily achieved.

      Because the text is just a single textured mesh, it will render very fast. This is a big improvement over the DrawText() command in Leadwerks 4, which performs one draw call for each character.
      The font loading command no longer accepts a size. You load the font once and a new image will be rasterized for each text size the engine requests internally:
      auto font = LoadFont("arial.ttf"); auto text = CreateText(foreground, font, "Hello, how are you today?", 18); Combining 2D and 3D
      By using two separate worlds we can control which items the 3D camera draws and which item 2D camera draws: (The foreground camera will be rendered on top of the perspective camera, since it is created after it.) We need to use a second camera so that 2D elements are rendered in a second pass with a fresh new depth buffer.
      //Create main world and camera auto world = CreateWorld(); auto camera = CreateCamera(world); auto scene = LoadScene(world,"start.map"); //Create world for 2D rendering auto foreground = CreateWorld() auto fgcam = CreateCamera(foreground); fgcam->SetProjection(PROJECTION_ORTHOGRAPHIC); fgcam->SetClearMode(CLEAR_DEPTH); fgcam->SetRange(-1,1); auto UI = LoadScene(foreground,"UI.map"); //Combine rendering world->Combine(foreground); while (true) { world->Update(); world->Render(framebuffer); } Overall, this will take more work to set up and get started with than the simple 2D drawing in Leadwerks 4, but the performance and additional control you get are well worth it. This whole approach makes so much sense to me, and I think it will lead to some really cool possibilities.
      As I have explained elsewhere, performance has replaced ease of use as my primary design goal. I like the results I get with this approach because I feel the design decisions are less subjective.
    • By Josh in Josh's Dev Blog 6
      I'm putting together ideas for a racing game template to add to Leadwerks. We already support vehicles. The challenge is to put together that looks and feels slick and professional, like a real game people want to play. The finished demo will be submitted to Greenlight, GameJolt, IndieDB, itch.io, etc.
       
      Gameplay
      First, I wanted to think about what style of racing I want this to be. I don't want street racing because it's kind of boring, and the level design is more involved. I don't want spintires-style technical offroading because it's too specialized. I want some fun medium-paced 4x4 racing like in
      , but modern. 


       
      This single-player game will pit you against seven computer-controlled components. You win by coming in the top three places. A time-trial option will allow you to compare your scores to other players via Steam leaderboards.
       
      The HUD will display a speedometer, your place in the race, current lap, and total and current lap time.
       
      Cars will be 4x4 trucks, identical except with a different texture.
       
      The player can turn headlights on and off, honk their horn, and drive. The transmission will always be automatic.
       
      Pressing the C key will alternate between views, including 3rd person, 3rd person further away, first-person (in-car), and a free third person camera that doesn't rotate with the car.
       
      Checkpoints will be placed throughout the level, with a sound when you pass through.
       
      After the race is complete, a replay will be performed from data recorded during the race, and scores will be shown on the screen.
       
      Environment
      I want the environment to be scrubby arid desert with big dramatic crags in the background.
       

       
      Roads will be painted on with a dirt texture, and decals will be used to add tire tracks sporadically. Decals will fade out at a fairly close distance, as I plan on having lots of them in the map.
       
      The game will allow you to set the time of day and weather. I have not decided if the weather and time of day will change as the race progresses. Time of day includes night, morning, afternoon, and evening.
       
      Weather can also be set, with options for sunny, rainy, and snowy. Snow will use a post-processing effect to add snow on all upwards-facing surfaces. Tire grip will be reduced in snowy and rainy conditions.
       
      The vehicles will throw up a cloud of dirt, mud, water, or other material, based on the primary texture of the terrain where they are contacting. Dirt, water, raindrops, ice, snow, and other effects will hit the camera and remain for a moment before fading.
       
      Screen-space reflection will be showcased heavily on the vehicle bodies.
       
      One song will play for the menu and one for the race. The song will sound something like this at 0:44 because it sounds modern:


       
      Or maybe this:

       
      Scope Limits
      The game is single-player only.
      I'm not going to bother with changes to the terrain or vehicles leaving tread marks.
      There will be no arms visible when the camera is inside the car.
      The environment will be static. There will be no destruction of the environment, and no moving objects or physically interactive items except for the cars.
      I am not going to implement an overhead map.
      I am not going to implement vehicle damage.
      Other than finishing the game GUI, I do not want to implement any new features in Leadwerks to complete this.
      The game will not attempt to be realistic or follow any real-world racing events.
      The race will not portray an audience or people standing around.
      No weapons.

×
×
  • Create New...