Chapter 15. Deep Places of the World

Be on your guard. There are older and fouler things than Orcs in the deep places of the world.

—Gandalf, The Fellowship of the Ring, J.R.R. Tolkien

This final chapter covers several important concepts that will give the dungeon crawler engine some much-needed polish. My goal is to make it as easy as possible for you to create your own RPG, by giving just enough information in the example to show how things work, but without going so far into the gameplay that it’s difficult to understand how the sample game works. I wouldn’t call this a complete game by any means; it is an RPG engine. The gameplay is up to you! Among the topics covered are a line-of-sight (LOS) object visibility algorithm; a lantern that lights up the area near the player; character generation; and Lua script support. The final example includes these features and more, including loading and saving the game and rudimentary monster A.I. Along the way, many small but significant improvements have been made to the classes (especially the Game class) to accommodate these new requirements of the engine. All of the editors are in the runtime folder for the game in this chapter’s resource files (www.courseptr.com/downloads) for easy access, so if you want to make changes to the game, just fire up the editors and start editing. You have all the tools you need to build your own game, and we will just go over a few new ones in this final chapter.

Here is a quick summary of what we’ll cover:

  • Line of sight

  • Torch light radius

  • Scroller optimizations

  • Lua script language

  • Finishing touches

Going Deeper

Everything is starting to really take shape in the dungeon crawler engine. Now we can add treasure and items and monsters in code or via the level editor data. The monsters aren’t too bright yet, but they just need some A.I. code to make them move and attack which will be handled in the last chapter. There’s one big issue that I want to address because it’s a stable of this genre—line of sight.

A really complex game engine would hide everything that isn’t directly in the player’s line of sight. We could create a node search system to determine whether an object or monster is visible to the player, and then hide them if they are behind a wall or around a corner. But, I was thinking about a simpler way to handle line of sight. Well, simple is a relative term; what I think of as simple, you might have a hard time understanding, and vice versa! How do you tell when something is in view? Well, the only practical way to handle that is to use the collidable property, because we have no other way of identifying walls or obstacles. Collidable could be used for a small statue or water fountain or something solid that you can see past but not walk through, so there are potential problems with collidable, but in general—and for our game—collidable is only really used for wall tiles that are impassible.

Line of Sight (Ray Casting)

Our code already checks to see when an object should be drawn when it is in the current scrolling viewport. But, an item or monster is still drawn even if one or more walls separate them from the player sprite! Wouldn’t it be really great if objects only came into view when nothing is obstructing your line of sight? That would make a big difference in the gameplay, add to replay value, and quite simply, make the game more scary!

How do you calculate line of sight? Good question! There’s an age-old algorithm invented long ago by a computer scientist named Bresenham, who figured out a very fast way to draw lines on a computer screen. Prior to Bresenham, programmers used trigonometry to draw lines, but Bresenham lines use if statements and counters instead, making it much faster. (See Figure 15.1.) Treat the player’s position as the starting point, and the target object’s position as the end point, and calculate the points on a line connecting them using whole tiles for each pixel. There will only be a few steps in the “line” to calculate. At each step, we check to see whether the tile is collidable, and if any tile is, then we know the object is not visible to the player.

Drawing a line using tiles with the Bresenham algorithm.

Figure 15.1. Drawing a line using tiles with the Bresenham algorithm.

This technique is more popularly called ray casting.

Another approach is possible. We could simulate firing a bullet from the player to the monster in a straight line, and actually move it along several pixels at a time. As the theoretical bullet is moving along, we figure out which tile it is over at each step, look at the collidable property of that tile, and deal with that result in the same way. This avoids the Bresenham algorithm, replacing it with just a simple direction calculation, and by starting at the center of a tile, that step could jump one full tile’s worth of pixels (32) at a time. The problem with this second solution, although simpler, is that the startup is math heavy—you have to calculate the angle from the player to the monster using sine and cosine, which comes out as X and Y velocity values.

Note

For a detailed description of the Bresenham line algorithm, including historical details, take a look at this Wikipedia article: http://en.wikipedia.org/wiki/Bresenham’s_line_algorithm.

For a working example, any kind of object will do, so I’ve stripped out everything but the essentials, leaving in just enough of the game so that treasure items are visible as the player walks around, but they can’t be picked up. Here is a function, objectIsVisibleLOS(), that treats the tilemap as pixels while calculating a Bresenham line between the player and any other tile location.

The objectIsVisibleLOS() function is self-contained, calculating the player’s tile position automatically, and only requires one parameter—the relative location on the screen (in pixel coordinates). Since objects beyond the viewport will be culled anyway, only objects in the viewport need to be tested for line of sight. The Line of Sight demo project demonstrates the technique, as shown in Figure 15.2.

private bool objectIsVisibleLOS(PointF target)
{
    //get player's tile position
    Point p0 = new Point((int)game.Hero.FootPos.X-8,
        (int)game.Hero.FootPos.Y);
    Point line0 = p0;

    //get target tile position
    Point p1 = new Point((int)target.X, (int)target.Y);
    Point line1 = p1;

    //begin calculating line
    bool steep = Math.Abs(p1.Y - p0.Y) > Math.Abs(p1.X - p0.X);
    if (steep)
    {
        //swap points due to steep slope
        Point tmpPoint = new Point(p0.X,p0.Y);
        p0 = new Point(tmpPoint.Y, tmpPoint.X);

        tmpPoint = p1;
        p1 = new Point(tmpPoint.Y, tmpPoint.X);
    }

    int deltaX = (int)Math.Abs(p1.X - p0.X);
    int deltaY = (int)Math.Abs(p1.Y - p0.Y);
    int error = 0;
    int deltaError = deltaY;
    int yStep = 0, xStep = 0;
    int x = p0.X, y = p0.Y;

    if (p0.Y < p1.Y) yStep = 4;
    else yStep = -4;

    if (p0.X < p1.X) xStep = 4;
    else xStep = -4;

    int tmpX = 0, tmpY = 0;

    while (x != p1.X)
    {
        x += xStep;
        error += deltaError;

        //move one along on the Y axis
        if ((2*error) > deltaX)
        {
             y += yStep;
             error -= deltaX;
        }

        //flip the coords if steep
        if (steep)
        {
            tmpX = y;
            tmpY = x;
    }
    else
    {
            tmpX = x;
            tmpY = y;
    }

    //make sure coords are legal
    if (tmpX >= 0 & tmpX < 1280 & tmpY >= 0 & tmpY < 1280 )
    {
        //is this a collidable tile?
        Level.tilemapStruct ts = game.World.getTile(tmpX/32, tmpY/32);
        if (ts.collidable) return false;
        else
        {
            //draw this step of path toward target
            game.Device.DrawRectangle(Pens.Azure, tmpX + 14,
                tmpY + 14, 4, 4);
        }
    }
    else

          //not legal coords
          return false;
    }

    return true;
}
Line of sight (LOS) ray casting is used to hide objects that should not be visible to the player.

Figure 15.2. Line of sight (LOS) ray casting is used to hide objects that should not be visible to the player.

Figure 15.3 shows another view of the demo with the player at the center of the four walled-off rooms, making it impossible to see what’s inside (there’s an item in each room). Note that the line-of-sight rays terminate when they reach a collidable tile, while those without obstruction continue to the target item. This is not a 100 percent foolproof algorithm. There are some cases where an object will be visible when it shouldn’t be, because the algorithm is fast and sometimes the ray’s points fall in between tiles. One improvement would be to use the player’s lantern radius and the ray casting line-of-sight algorithm to hide objects that are either blocked or outside the player’s viewing range. This is the technique I recommend using, with both rather than just line of sight alone.

The items placed inside the walled rooms are invisible due to LOS.

Figure 15.3. The items placed inside the walled rooms are invisible due to LOS.

Torch Light Radius

Having line-of-sight culling is a huge improvement over just drawing everything in the viewport, as far as improving the game’s realism. That, combined with another new technique in drawing the level—torch light radius—will make the player truly feel as if he is walking through a dark, dank, cold dungeon. Since line of sight causes only objects within view to be drawn, we can take advantage of that with this additional effect to make the player’s experience perfect for this genre. It goes without saying that dungeons don’t come with halogen lights on the ceiling—a real underground tunnel and room system would be pitch black, and the player would have to be carrying a torch—there’s just no way around that. I’m not a big fan of micro-management, so I just give the player a permanent torch, but some game designers are cruel enough to cause the torch to burn out!

The key to making the dungeon level look dark everywhere except near the player is by using a radius value, highlighting all tiles within a certain range around the player. All tiles in that range are drawn normally, while all other tiles are drawn with a level of darkness (perhaps 50 percent white). GDI+ has a feature that we could use to draw some of the tiles darker that are beyond the light radius around the player. By using ImageAttribute, it is possible to set the gamma level to increase or decrease the lighting of an image. In testing, however, this proved to be too slow for the game, causing performance to drop significantly (because the actual pixels of the image were being manipulated). Instead, we’ll just have to manipulate the artwork. If you are intrigued by the ImageAttribute approach—don’t be. In practice, anything you can do easily with artwork is a far better solution than trying to do it in code. Try to keep your code as straightforward as possible, without all kinds of conditions and options, and put some requirements on the artwork instead for better results.

The Torch Light demo is shown in Figure 15.4. The first and most obvious problem with this is the square-shape of the lit area (where it really should be round in shape). The second problem with this demo is that areas beyond a wall appear to be lit even though the lantern should not be penetrating the wall. It would seem we need to combine the lantern code with the ray-casting line-of-sight code for object visibility as well as for lighting. The great thing about this problem, strange as that may sound, is that we have all the code we need to make any such changes we want to make—to both object visibility and to the lighting around the player.

Lighting up a small area around the player to simulate a lantern.

Figure 15.4. Lighting up a small area around the player to simulate a lantern.

How do you want to tackle it? Are these issues important for your own game design goals? I can only give you the tools, but you must make the game! These examples are meant to teach concepts, not to create a fun gameplay experience. So, use these concepts and the code I’m sharing with you in creative ways while creating your own dungeon crawler!

The dark tile set is just a copy of the normal palette.bmp image with the brightness turned down and saved as a new file, palette_dark.bmp. Some changes must be made to the Level class to make this work. Another Bitmap variable is needed to handle the dark tiles:

private Bitmap p_bmpTilesDark;

The loadPalette() function is modified to require both palette filenames:

public bool loadPalette(string lightTiles, string darkTiles, int columns)
{
    p_columns = columns;
    try {
        p_bmpTiles = new Bitmap(lightTiles);
        p_bmpTilesDark = new Bitmap(darkTiles);
        fillScrollBuffer();
    }
    catch (Exception) { return false; }
    return true;
}

The function fillScrollBuffer() draws the tiles representing the current scroll buffer. This function is called only when the player moves one full tile width or height (32 pixels), at which point the buffer must be re-filled with tiles at the new scroll position. This function, in turn, calls on a helper called drawTile-Number(), to actually do the physical drawing of the tile image. This function has not changed since the Level class was originally created.

private void fillScrollBuffer()
{
    Point currentTile = new Point((int)p_scrollPos.X / p_tileSize,
        (int)p_scrollPos.Y / p_tileSize);

    for (int tx = 0; tx < p_windowSize.Width + 1; tx++)
    {
        for (int ty = 0; ty < p_windowSize.Height + 1; ty++)
        {
            int rx = currentTile.X   + tx;
            int ry = currentTile.Y + ty;
            int tilenum = p_tilemap[ry * 128 + rx].tilenum;

            Point playerTile = p_game.Hero.GetCurrentTilePos();
            if ((Math.Abs(rx - (playerTile.X + currentTile.X)) <= 3) &&
                (Math.Abs(ry - (playerTile.Y + currentTile.Y)) <= 3))

            {
                //draw tile using light tileset
                drawTileNumber(ref p_bmpTiles, tx, ty, tilenum);
            }
            else
            {
                //draw tile using dark tileset
                drawTileNumber(ref p_bmpTilesDark, tx, ty, tilenum);
            }
        }
    }
}

Even more sophisticated forms of lighting can be adopted, such as using an adaptation of the line-of-sight code developed recently to cause the light range to stop when it hits a wall. If you are going for an even more realistic look for your own game, you might try this. In simple experiments I found it fairly easy to look for collidable tiles while drawing the lighter tiles. Creative coding could produce interesting special effects like a flashlight-style light or a lamp light that swings back and forth as the player walks.

Scroller Optimizations

The scroller engine now built into the Level class is not optimized. In our efforts to get a scrolling level to move and draw, no concern was given to performance. But, there is a built-in capability to optimize the scroller which will result in at least a 10x frame rate increase. Presently, the fillScrollBuffer() function is called any time the player moves—even a pixel. This is highly inefficient. But, again, the important thing is to get a game to work first, then worry about performance later. Now is that time! By adding the gamma light modifications to the tile renderer, there is an added strain on the engine to maintain a steady frame rate. By making a few modifications to the scroller, based on the scroll buffer image, the game loop will run so much faster!

The first thing we might do to speed up the game is to take a look again at the doUpdate() and doScrolling() functions. Level.Update() contains user input code, so we need the scroller to continue as it has for consistent player movement. But, the problem is Level.Update() also contains engine-level scroll buffer code. We could detach the scroll buffer code from the player movement code so it can be run in a faster part of the engine (outside of the time-limited user input cycle). We must be careful with changes like this, though, because the game’s parts are now highly interconnected; detaching any one piece or changing its behavior might affect other systems.

Another optimization that might be made is to the scroll buffer code. As you may recall, the scroll buffer is one full tile (32 pixels) larger around the edges than the screen. The scroll buffer is then shifted in whatever direction the player is moving until it has moved 32 pixels, at which point the scroll buffer must be re-filled. In theory! As a matter of fact, that isn’t happening at all—the scroll buffer is being refilled every step the player makes! Instant 10–20x performance boost here.

Open up the Level.Update( ) function, and down near the bottom there is a block of code beginning with this:

//fill the scroll buffer only when moving
if (p_scrollPos != p_oldScrollPos || p_game.Hero.Position != p_oldPlayerPos)

At the bottom of that code block is a call to fillScrollBuffer(). This is where the optimization will be made! Can you figure it out? In fairness, the game works as is; this is an important but not essential modification. If you need help with it, come by my forum to chat about it with others—maybe you will be the first person to figure it out? (www.jharbour.com/forum).

Tip

As a reward, I will give away a free book to the first person who posts a solution to this optimization!

Lua Script Language

Scripting is a subject that might seem to belong back in Part II, “Building the Dungeon,” since it is a core engine-level feature. But, until all of the classes were built for the dungeon crawler engine, there really was nothing we could do with a script language—which must necessarily be based on existing features within a game’s source code. A script language can be used to create almost an entire game, if the engine behind it supports gameplay functions, but until now we have not had enough of a working example to make use of scripting. The script language of choice here is Lua, which will be briefly introduced along with code to tie Lua scripts into the game engine.

When you combine the versatility that a data-driven game engine like this one affords, along with a custom level editor, you already have a great combination for making a great game. But when you add script support to the mix, things get even more interesting! We have progressed to the point in both the game and the editors where, sure, we could get by with the excellent tools and code already in hand, but I want to raise the cool factor even higher with the addition of scripting support.

Now, let me disclaim something first: Yes, scripting is cool and adds incredible power to a game project, but it requires a lot of extra effort to make it work effectively.

The cool factor is that we can call C# functions from within a Lua script file! Likewise, we can call Lua functions from our C# code—interpreted Lua functions! But what about all of the global variables in a Lua source code file? The variables are automatically handled by the Lua engine when a script file is loaded. I’m not going to delve into a full-blown tutorial on the Lua language, because I just don’t have time or space. Instead, we’re just going to use it and you’ll see how useful a scripting language is by watching it put to use.

Hint

There is one really big drawback to Lua: once you have “seen the light,” you may never go back to writing a game purely with a compiled language like Basic or C# again! Lua is so compelling that you’ll wonder how in the world you ever got anything done before you discovered it!

Installing Lua

The key to adding Lua support to our C# code is an open-source project called LuaInterface, hosted at the LuaForge website: http://luaforge.net/projects/luainterface/. The sources for LuaInterface are housed in a Google Code Subversion (SVN) repository at http://code.google.com/p/luainterface, with support for Visual C# 2008. I have included a project with this chapter that has the pre-compiled version of LuaInterface ready to use.

Definition

Lua is the Portuguese word for “Moon.” The official spelling is LUA, with all caps, but I prefer to spell it without all caps because that leads the reader to assume it’s an acronym rather than a word.

Installing LuaInterface

After compiling the LuaInterface project, you’ll get a file called LuaInterface.dll which contains the .NET assembly for the project. You will also need the Lua runtime library file, lua51.dll. Copy LuaInterface.dll and lua51.dll to any project that needs Lua support and you’ll be all set. (Note also that these files must always be distributed with your game.) Whether you compiled it yourself or just copied it from the chapter resource files, create a new C# project. Then, open the Project menu and select Add Reference. Locate the LuaInterface.dll file and select it, as shown in Figure 15.5.

Adding the LuaInterface.dll file to the project.

Figure 15.5. Adding the LuaInterface.dll file to the project.

Nothing will seem to change in the project. To verify that the component has been added, open Project, Properties, and bring up the References tab, where you should see the component among the others available to your project. See Figure 15.6.

List of referenced components in this project.

Figure 15.6. List of referenced components in this project.

Testing LuaInterface

Here is our first short example program that loads a Lua script file. The form for this program has a TextBox control, which is used as a simple console for printing out text from both the Lua script and our Basic code. Figure 15.7 shows the result of the program.

public partial class Form1 : Form
{
    private TextBox textBox1;
    public Lua lua;

    public Form1()
    {

    InitializeComponent();
}

private void Form1_Load(object sender, EventArgs e)
{
    this.Text = "Lua Script Demo";
    textBox1 = new TextBox();
    textBox1.Dock = DockStyle.Fill;
    textBox1.Multiline = true;
    textBox1.Font = new Font("System", 14, FontStyle.Regular);
    this.Controls.Add(textBox1);

    //create lua object
    lua = new Lua();

    //link a C# function to Lua
    lua.RegisterFunction("DoPrint", this, this.GetType().
           GetMethod("DoPrint"));

    //load lua script file
    lua.DoFile("script.lua");

    //get globals from lua
    string name = lua.GetString("name");
    double age = lua.GetNumber("age");

        DoPrint("name = " + name);
        DoPrint("age = " + age.ToString());

    }

    //this function is visible to Lua script
    public void DoPrint(string text)
    {
        textBox1.Text += text + "
";
    }
}
We now have Lua script language support for our game.

Figure 15.7. We now have Lua script language support for our game.

Hint

The LuaInterface.dll requires the .NET Framework 2.0, not the later versions such as 3.5. If you are using Visual C# 2010, it will default to the later version of the .NET Framework. To get LuaInterface to work with your Visual C# 2010 project, you may need to switch to .NET Framework 2.0 (which is done via Project Properties). You may also need to manually set the target from “Any CPU” to “x86” to get the Lua library to work with Visual C# 2010.

First, the TextBox control is created and added to the form with the Multiline property set to true so the control acts like a console rather than an entry field.

Next, the LuaInterface.Lua object is created. That object, called lua, is then used to register a C# function called DoPrint() (note that it must be declared with public scope in order for Lua to see it!). Next, lua.DoFile() is called to load the script code in the script.lua file. This file must be located in the .inDebug folder where the executable file is created at compile time. So, we can think of a script file like any game asset file, equivalent to a bitmap file or an audio file.

When DoFile() is called, that not only opens the script file, it also executes the code. This is one of the two ways to open a script file. The second way is to use LoadFile() instead, which simply loads the script into memory, registers the functions and globals, but does not start executing statements yet.

After the script has been loaded and run, then we can tap into the lua object to retrieve globals from the lua object, as well as call functions in the script code. In this example, we just grab two globals (name and age) and print out their values. This demonstrates that Lua can see our Basic function and call it, and that we can tap into the globals, which is the most important thing!

Here is the script.lua file for this project:

-This is my first Lua Script!
-create some globals
name = "Rich Golden"
age = 24
-call a function in the program
DoPrint( "Welcome to " .. _VERSION )

The last line in the script.lua file calls DoPrint(), which is not a Lua function; it’s a function in our C# program! As soon as a function is registered with Lua using RegisterFunction(), it becomes visible to the script code.

Hint

Do you see that odd-looking pair of dots in the last line of the script file? The double dot is how you combine strings in Lua (also called concatenation).

Sharing Tile Data with Lua

Now that we have a Lua linkage in our project, we should give Lua access to the game engine. I want to be able to scroll the game world to any location with a function call, as well as read the data for any tile on the tilemap, including the tile under the player’s current position, for obvious reasons. Once those things are done, then it will be possible to add Lua functions to the tilemap via the level editor. At that point, the game engine becomes less of a factor for gameplay code. Any variable in the engine can be sent to the Lua code as a global, and vice versa! This level of cooperation along with the runtime interpretation of Lua script makes it an extremely valuable addition to the game.

Hint

If you get the unusual runtime error shown below, that usually means the program could not find the lua51.dll file which is a dependent of LuaInterface.dll. Be sure both dll files are located in the .inDebug folder of your project.

An unhandled exception of type ‘System.InvalidOperationException’ occurred in Lua Script Demo.exe. Additional information: An error occurred creating the form. See Exception.InnerException for details. The error is: Could not load file or assembly ‘lua51, Version=0.0.0.0, Culture=neutral, PublicKeyToken =1e1fb15b02227b8a’ or one of its dependencies. The system cannot find the file specified.

Incorporating Script Into the Engine

The great thing about the source code for the dungeon crawler is that all of the “big pieces” are in classes that are initialized in Form1_Load(). Furthermore, all of the real work is being done in the doUpdate() function, which calls doScrolling (), doMonsters(), doHero(), etc. This is a very clean way to build a game, because it is easy to add new things to it, and we can see easily what’s happening in this game at a glance without wading through hundreds of lines of code muddying up these main functions. The way this code is structured also makes it possible to script much of it with Lua!

In order to give the script something to work on, we have to call a function in the script file. When a script is opened, if there is no function, then it is simply parsed and no more processing takes place. You can continue to use the script global variables but no work takes place in the script unless you tell it to do something from the C# game code. In our Form1_Load() source code, we have a function called doUpdate() that does timing, calls all of the update functions, displays info on the screen, etc.—in other words, this is our workhorse function, the core loop of the game. We’re going to plug the Lua update into the game loop along with the other “do” functions.

Hint

The script.lua file must be in the inDebug folder just like bitmap files and other assets.

All of the components of the game can be selectively loaded with script functions, and almost total control is given to the script to override what happens in the default C# code (in Form1_Load() and doUpdate()). Let’s briefly review the properties available. Note that most are all read-only properties. Making changes to them does not affect the game, as they are just intended to supply information to the script. The exception is ScrollX and ScrollY, which are both sent to the script and back to C#, so you can change the player’s current location in the world with these properties. Open the final project to see these script functions in action.

  • WindowTitle

  • ScrollX

  • ScrollY

  • PortalFlag

  • CollidableFlag

  • Health

  • HP

  • QuestNumber

  • QuestSummary

  • QuestCompleteFlag

  • MessageResult

Here are the functions available to our Lua script, which are tied in to the C# code.

  • LoadLevel()

  • LoadItems()

  • LoadQuests()

  • LoadHero()

  • DropGold()

  • AddCharacter()

  • Write()

  • Message()

Finishing Touches

Unfortunately, we are out of room and time to go into any more detail on the finished game! You will have to open the completed Dungeon Crawler game in the chapter resources to see the final version of the game! Here are features currently in the game that we did not have time to cover (because this is not a 500-page book):

  1. Monster A.I.

  2. Loading and saving the game

  3. Loading level files using portals.

  4. Creating player characters (Figure 15.8).

    Rolling the stats for a new character.

    Figure 15.8. Rolling the stats for a new character.

  5. Combining line of sight and light radius for both monsters and items.

  6. Shifting most of the startup code to Lua.

At any rate, you now have enough tools to create your own RPG! Of course, no game is ever truly finished, it’s just “good enough” to play, so maybe you will do something fun with this RPG engine?

Level Up!

You are now a level 15 game programmer who is ready to take your skills and abilities to build your own dungeon crawler RPG! In the words of Yoda, “No more training do you require. Already know you that which you need.” But one task remains before your training is complete—you must build your own game. I hope you have enjoyed the journey of learning how to build a custom role-playing game. It has been a blast to work on this game! Happy exploring!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.40.36