Karamazovapy's levelgen tutorial
Sometimes levels get stale. It's not because they're poorly designed, it's just that gameplay is limited by the level's elements. Once those elements have been explored, things start to feel repetitive. Level generation addresses this problem by altering some or all of the level's elements each time it loads. Until significant work is done to quantify what makes a level successful, fully generated levels will be limited to mazes and other purely logic-based designs. However, levelgen files can be used to manipulate predetermined elements within a more traditional design.
This tutorial assumes you know nothing about coding or programming languages. It is designed to teach you how to integrate levelgen files into your level design as quickly and easily as possible. There are many different ways to code the following examples. If you're interested, download the project files and let's get started!
Note: The following text and project files can be downloaded here.
Open Project1.level in the level editor. Go to LEVEL PARAMETERS and look at the entry next to Levelgen Script. Notice that it says Project1.levelgen. Return to the editor and test the level. Try reloading it a few times in-game using /restart. You'll notice that the item in the center alternates between a barrier block and a repair item. Now that you see what happens when the levelgen file runs, open Project1.levelgen and take a look at the code itself.
local CenterSquare = math.random(2) if(CenterSquare == 2) then levelgen:addLevelLine("BarrierMaker 200 -0.4 0 0.4 0") else levelgen:addLevelLine("RepairItem 0 0 20") end
Here's what each part does:
- local - tells the script we're about to use and name a variable
- CenterSquare - this is the variable name we've chosen. notice that it describes what it's attached to, namely what appears in the center square. it's important to know that variables are case sensitive, which means that CenterSquare is different from centersquare.
- = - one equal sign assigns a value to a variable
- math.random(2) - when there's a single number in the parens, math.random picks an integer between 1 and the specified value. This math.random picks between 1 and 2.
- if() - if tests whether something is true or not. here, it's testing whether the random number picked by random.math and stored in CenterSquare is 2.
- == - two equal signs next to one another test whether the thing on the left is the same as the thing on the right
- then - tells the code what to do if the "if" test is true
- levelgen:addLevelLine("") - this is the only levelgen-specific code we're going to use. everything else here is just Lua stuff. this command temporarily adds a line of level code to the .level file being loaded. make sure you always include the closing ") after your line of level code. leaving this out will cause your levelgen file to crash, which means the levelgen won't produce anything when you run it.
- BarrierMaker... - this is just normal level code. you can copy and paste it from a .level file.
- else - tells the code what to do if the "if" test comes up false. here, it comes into play if the random number picked by math.random and stored to CenterSquare is 1 instead of 2.
- end - tells the code that this block is all done. here, it puts an end to the "if" test and its possible outcomes.
Take another look at the block of code. In English, it says "About half the time, put a block in the middle of the level. If you don't do that, put a repair item there." Try changing the code to make a testitem appear instead of the repairitem. Try making a different barrier appear. You can copy and paste this block of code into any levelgen file and change it in any way you want. If all this makes sense, you're ready for the next section!
Open Project2.level in the level editor. Notice that the levelgen file in the level parameters is Project2.levelgen. Project1 was a little boring, so let's make Project2 a little more interesting. Test the level and see what happens. If you /restart enough times, you notice that 1-4 resource items appear near the center. Open Project2.levelgen in a text editor to see how it works.
After the block from Project1, you'll see our new code. This time, we're working with the variable ResourceNumber, and math.random will now choose a number from 1-4. The first if() checks to see if the number is 4. If it is, four resource items are created. If it's not, the code uses elseif() to test if the number is 3.
- elseif() - runs after another if or elseif and works the same way as a regular "if" statement. Like the if, elseif needs to be followed by a "then".
Adding several lines of level code as a group is as easy as giving each one its own addLevelLine. This code has a test with four different outcomes. What if you wanted to add a fifth outcome with five resource items? What if you wanted to create 1-3 resource items? Try modifying the code to test your ideas. When you feel comfortable with the elseif statement and outcomes that add several lines of code, you're ready for Project3.
The tutorial level is developing nicely, but it's awfully empty. Open Project3_grid.level in the editor to see one way of filling it out. What if we want each grid block to work as a unit within the levelgen file? We could copy and paste the BarrierMaker lines from the Project3_grid.level file. Try opening Project3_grid.level in a text editor. Do you see the problem? You can kind of see where each grid block starts and ends, but it's not very clear. While you can probably figure it out for this level, more complicated designs would be a lot more work. Open Project3_grid_work.level in the editor to see one way around this.
We know all the barriers will have a width of 1 in the end, so for now there's no harm in giving each group a different width. By going in order, we know that BarrierMaker 5 is the Y-Axis, Top grid block. BarrierMaker 10 is the Right, Top grid block. BarrierMaker 15 is the Left, X-Axis block. By saving the level with different barrier width groups, we can easily arrange the different blocks within the text editor for copying and pasting. Once the grid blocks are separated, it's important to assign a variable name to each one. If you make a mistake, want to change a block, or want to add elements later, it's important that you can distinguish each area on sight. Next you can use a quick Find&Replace to change each instance of "BarrierMaker 15" to "BarrierMaker 1", etc, before copying and pasting the blocks into the levelgen file.
With everything already separated, it should be easy to create math.random and if/then code for each grid block. But from a level design perspective, how random do we want the grid blocks to be? The level would be more fun if there was usually a bit of space in the corners for fighting and usually a divider along the edges. Why don't we have each corner stay open about 80% of the time and have each divider appear about 80% of the time? To make this happen, we want each corner block to appear about 1 in 5 times and each edge block to NOT get added about 1 in 5 times. Take a look at Project3.levelgen to see how math.random(5) is used to achieve these results. If you don't want anything to happen in the event of a certain outcome, it's fine to leave a then or else statement blank. Notice again how each block of levelgen code uses a descriptive variable so we know exactly what grid area is in play.
In this project, each of our grid blocks used lines of the same width. If that isn't the case in your level, here's another way to extract the code you're interested in:
- save a backup of the "complete" level i.e. Project3.backup.level
- in the editor, delete all the parts of the level you're not interested in
- save. you have a backup saved under a different name. don't screw this up.
- open the altered level in a text editor
- copy and paste the level code you want to deal with in the levelgen
- copy and paste the complete level from your backup file
- save the copy and pasted original and hit ctrl+shift+L in the editor to refresh the restored level
- repeat steps 2-7 for each section of variable code
You can see why changing barrier widths for uniform sections is a much faster process. How could you group the grid blocks in the levelgen code to only create symmetrical levels? How could you group all the edges together and all the corners together? What if you wanted the edge blocks to appear 90% of the time instead of 80%? What if you wanted the corners empty two thirds of the time? When you feel like you understand grouping sections of level code and adjusting probability, you're ready to move on to Project4.
Try opening and playing Project3.level in the editor. What do you notice when the edge blocks disappear? The level feels a bit empty. Why don't we change that? Open Project4.level in the editor and test it. You might need to /restart a few times to see just what's changed. Now, when the top or bottom edge grid blocks are missing and the center is present, a turret appears. When the left or right edge grid blocks are missing and the center is present, a forcefield projector appears. Open Project4.levelgen and scroll to the bottom to see how it works.
- and - links two conditionals. Only activates "then" if both conditions are true.
Now that you're looking at the end of Project4.levelgen, you can see why it's important to give variables descriptive names. Also, look back to confirm what happens when CenterSquare == 2 or YTop == 5. As stated earlier, == tests to see if two things are equal. Other useful tests include ~=, <, >, <=, >=. If you can't guess how they work by looking at them, try using them to figure it out. [hint] We used and to link the edge blocks being missing and the center square being present because from a level design standpoint, the forcefield projectors would be the most useful and the turrets would be the least annoying in this configuration. How would you create a special forcefieldprojector for the rare instances where an entire row or column disappears? What about something special in the center when all the corner blocks are missing? If you've got a handle on conditionals and the "and", you're ready for Project5.
Project4 made some progress in making the tutorial level more complete, but it didn't address its biggest flaw. The level feels especially empty when an edge block AND the center block are both missing. How could we use what we already know to enhance the level in those situations? What if we wanted to insert a testitem or asteroid in place of an edge block when both it and the center square are absent? We could just add another series of and statements, but this is a tutorial. Let's make it slightly more interesting. Open up Project5.levelgen to see another way of making it work.
Since the test for both the center square and an edge block being missing only needs to run if the test we added in Project4 fails, we can insert another if/then block into the old if/then statements. This is called nesting. It's a good habit to indent nested code to make it easier to read. We could've nested the original CenterSquare/YTop set of if/then statements inside the original YTop/YBottom/LeftX/RightX if/then blocks, but nesting can get confusing pretty quickly. At the same time, nesting may speed up your code by only running tests when they're needed. Another advantage to nesting code is that it visually links related tests. Here, the nested section all has to do with adding items when edge blocks are missing. But why do we need a second set of if/then statements in the first place?
Why can't we just use this code?
if(CenterSquare == 2) and (RightX == 5) then levelgen:addLevelLine("ForceFieldProjector 0 1.9 0 0") else levelgen:addLevelLine("TestItem 1.2 0") end
Try modifying the code and testing it to see the result. How could you nest the added code from Project4 into the main block from Project3? Once that code was nested, how could you achieve the same results of Project5 without adding any additional if/then statements? There are many ways to attack the problems presented in this tutorial. In it, we've addressed a number of basic coding elements that should allow you to introduce chance-related coding to your own designs.
- when using levelgen scripts to create adjacent barriers, overlapping the edges will give them a cleaner rendered look
- it can be helpful to create one master level with all possible design elements included, before breaking it up into levelgen scripts. check out ProjectOverload.level to see what this might look like.
- make periodic backups of both your level and your levelgen script. If you modify one and things stop working, check the newer version against the backup to help pinpoint the error.
- if you test your level and none of the levelgen material appears, there may just be a typo somewhere in your code. It could be as simple as a missing ) or "end" statement. It could also be an extra space or quotation mark where one doesn't belong.
- copying and pasting code can reduce typos
- include lots of comments (start with --)
- use ctrl-r to see a dynamic preview of the levelgen script in the editor
- using a special Lua editor with contextual highlighting can help you spot errors where the colors don't look right. Posting code on the forums should work in the same way.
- this tutorial outlines some ways you can use math.random in your levelgens, but you have the whole Lua library at your fingertips! Click here for the 5.1 Reference Manual