Chapter 9. Going Deeper into the Dungeon with Portals

Up to this point, we have learned quite a bit about creating a game world, and we just started to interact with the game world in the previous chapter. We need to bump it up a notch now by adding to both the editor and the game project the ability to create and use portals. A portal is an opening that connects two worlds or allows someone to cross over from one area into another. In terms of a tiled game world, we need a portal that will teleport the player to another location on the map, as well as specify a position on a different level file entirely. Once the code to teleport to a new location on a map is understood, adding the ability to teleport to a new level is just a matter of loading that new level file with the Level class and then setting the scroll position to the target X,Y position. The level editor already has fields for portals built in to the tile structure, which we will learn to use in this chapter.

Here’s what we’ll cover in this chapter:

  • Updating the level editor

  • Level class modifications

  • Teleporting to another dimension

  • Looking for tile collisions

Updating the Level Class

We need to update the Level class to accommodate the more advanced features of the editor that have gone unused until now. The editor project is available in the chapter’s resource files with the full source code, but we already covered the editor in detail back in Chapter 6, “Creating the Dungeon Editor.” As you may recall, the content of a .level file is plain XML data that can be viewed and edited in a text editor. Among the tile data fields are four generic data fields that store strings, so you can put any data you want there—whole numbers, decimal numbers, names, descriptions, etc. You could use one as a searchable item name field and then add an item to the game world at that tile location, or even something as exotic as a script function name. Here are the data fields for each tile already supported by the level editor:

  • Tile palette number

  • Data 1

  • Data 2

  • Data 3

  • Data 4

  • Collidable

  • Portal

  • Portal X

  • Portal Y

  • Portal file

These fields are sufficient for now, but we can add new ones to meet the design goals for a new game in the future if needed. We need to make a few new changes to the Level class (Level.cs) to handle all of the data fields from a level file. First, the new data fields need to be added to the tilemapStruct structure, and then we need to add a function to return tile data at a specified index in the tilemap so that the player’s character sprite can interact with the level. In other words, we need to find out where the portals are and let the character get teleported! Here is the new Level class with an expanded tilemapStruct and loadTilemap() function. The new items are highlighted in bold.

public class Level
{
    public struct tilemapStruct
    {
        public int tilenum;
        public string data1;
        public string data2;
        public string data3;
        public string data4;
        public bool collidable;
        public bool portal;
        public int portalx;
        public int portaly;
        public string portalfile;
    }

    private Game p_game;
    private Size p_mapSize = new Size(0, 0);
    private Size p_windowSize = new Size(0, 0);
    private int p_tileSize;
    private Bitmap p_bmpTiles;
    private int p_columns;
    private Bitmap p_bmpScrollBuffer;
    private Graphics p_gfxScrollBuffer;
    private tilemapStruct[] p_tilemap;
    private PointF p_scrollPos = new PointF(0, 0);
    private PointF p_subtile = new PointF(0, 0);
    private PointF p_oldScrollPos = new PointF(-1, -1);

    public Level(ref Game game, int width, int height, int tileSize)
    {
        p_game = game;
        p_windowSize = new Size(width, height);
        p_mapSize = new Size(width * tileSize, height * tileSize);
        p_tileSize = tileSize;

        //create scroll buffer
        p_bmpScrollBuffer = new Bitmap(p_mapSize.Width + p_tileSize,
            p_mapSize.Height + p_tileSize);
        p_gfxScrollBuffer = Graphics.FromImage(p_bmpScrollBuffer);
        //create tilemap
        p_tilemap = new tilemapStruct[128 * 128];
    }

    public tilemapStruct getTile(PointF p)
    {
        return getTile((int)(p.Y * 128 + p.X));
    }

        public tilemapStruct getTile(int pixelx, int pixely)
    {
        return getTile(pixely * 128 + pixelx);
    }

    public tilemapStruct getTile(int index)
    {
        return p_tilemap[index];
    }

    //get/set scroll position by whole tile position
    public Point GridPos
    {
        get
        {
            int x = (int)p_scrollPos.X / p_tileSize;
            int y = (int)p_scrollPos.Y / p_tileSize;
            return new Point(x, y);
        }
        set
        {
            float x = value.X * p_tileSize;
            float y = value.Y * p_tileSize;
            p_scrollPos = new PointF(x, y);
        }
    }

    //get/set scroll position by pixel position
    public PointF ScrollPos
    {
        get { return p_scrollPos; }
        set { p_scrollPos = value; }
    }


    public bool loadTilemap(string filename)
    {
        try
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(filename);
            XmlNodeList nodelist = doc.GetElementsByTagName("tiles");
            foreach (XmlNode node in nodelist)
            {
                XmlElement element = (XmlElement)node;
                int index = 0;
                tilemapStruct ts;
                string data;

                //read data fields from xml
                data = element.GetElementsByTagName("tile")[0].
                    InnerText;
                index = Convert.ToInt32(data);
                data = element.GetElementsByTagName("value")[0].
                    InnerText;
                ts.tilenum = Convert.ToInt32(data);
                data = element.GetElementsByTagName("data1")[0].
                    InnerText;
                ts.data1 = Convert.ToString(data);
                data = element.GetElementsByTagName("data2")[0].
                    InnerText;
                ts.data2 = Convert.ToString(data);
                data = element.GetElementsByTagName("data3")[0].
                    InnerText;
                ts.data3 = Convert.ToString(data);
                data = element.GetElementsByTagName("data4")[0].
                    InnerText;
                ts.data4 = Convert.ToString(data);
                data = element.GetElementsByTagName("collidable")[0].
                        InnerText;
                ts.collidable = Convert.ToBoolean(data);
                data = element.GetElementsByTagName("portal")[0].
                        InnerText;
                ts.portal = Convert.ToBoolean(data);
                data = element.GetElementsByTagName("portalx")[0].
                        InnerText;
                ts.portalx = Convert.ToInt32(data);
                data = element.GetElementsByTagName("portaly")[0].
                        InnerText;
                ts.portaly = Convert.ToInt32(data);
                data = element.GetElementsByTagName("portalfile")[0].
                        InnerText;
                ts.portalfile = Convert.ToString(data);

                //store data in tilemap
                p_tilemap[index] = ts;
            }
        }
        catch (Exception es)
        {
            MessageBox.Show(es.Message);
            return false;
        }
        return true;
    }

    public bool loadPalette(string filename, int columns)
    {
        p_columns = columns;
        try
        {
            p_bmpTiles = new Bitmap(filename);
        }
        catch (Exception ex)
        {
        return false;
        }
        return true;
    }

    public void Update()
    {
       //fill the scroll buffer only when moving
       if (p_scrollPos != p_oldScrollPos)
       {
           p_oldScrollPos = p_scrollPos;
           //validate X range
           if (p_scrollPos.X < 0) p_scrollPos.X = 0;
           if (p_scrollPos.X > (127 - p_windowSize.Width) * p_tileSize)
               p_scrollPos.X = (127 - p_windowSize.Width) * p_tileSize;

           //validate Y range
           if (p_scrollPos.Y < 0) p_scrollPos.Y = 0;
           if (p_scrollPos.Y > (127 - p_windowSize.Height) * p_tileSize)
               p_scrollPos.Y = (127 - p_windowSize.Height) * p_tileSize;

           //calculate sub-tile size
           p_subtile.X = p_scrollPos.X % p_tileSize;
           p_subtile.Y = p_scrollPos.Y % p_tileSize;

           //fill scroll buffer with tiles
           int tilenum, sx, sy;
           for (int x = 0; x < p_windowSize.Width + 1; x++)
                for (int y = 0; y < p_windowSize.Height + 1; y++)
                {
                     sx = (int)p_scrollPos.X / p_tileSize + x;
                     sy = (int)p_scrollPos.Y / p_tileSize + y;
                     tilenum = p_tilemap[sy * 128 + sx].tilenum;
                     drawTileNumber(x, y, tilenum);
                }
       }
   }

   public void drawTileNumber(int x, int y, int tile)
   {
       int sx = (tile % p_columns) * (p_tileSize + 1);
       int sy = (tile / p_columns) * (p_tileSize + 1);
       Rectangle src = new Rectangle(sx, sy, p_tileSize, p_tileSize);
       int dx = x * p_tileSize;
       int dy = y * p_tileSize;
       p_gfxScrollBuffer.DrawImage(p_bmpTiles, dx, dy, src,
           GraphicsUnit.Pixel);
   }

   public void Draw(Rectangle rect)
   {
       Draw(rect.X, rect.Y, rect.Width, rect.Height);
   }

   public void Draw(int width, int height)
   {
       Draw(0, 0, width, height);
   }
   public void Draw(int x, int y, int width, int height)
   {
       Rectangle source = new Rectangle((int)p_subtile.X,
           (int)p_subtile.Y, width, height);
       p_game.Device.DrawImage(p_bmpScrollBuffer, x, y, source,
           GraphicsUnit.Pixel);
   }
}

It’s a Data-Driven Game World

In the previous chapter, we learned how to create a list of tree sprites and draw them in the game world, so that when the player moves around the trees come into view within the scrolling viewport. That works well when you want to scatter random environment items like bushes, houses, coffee shops, software stores, and, well, anything you want. We can also use the level editor to position objects at a specific location, which is more useful than using randomness, especially when you need to count on a certain thing being at a certain location. For instance, you might have a quest that has the player find a certain landmark where nearby a treasure is buried.

The data fields are numbered 1 to 4, and can contain any type of data—numbers or strings. If we used these fields to position an item, we could use them like so:

Data 1: Item number

Data 2: Position x

Data 3: Position y

Data 4: Script function

The item number would be out of the game item database, or it could be the number or name of a sprite. The x,y position of the item is next in Data 2 and 3. The fourth one is a bit interesting. What is a script function? This goes a bit beyond the scope of this book, but if we wanted to really make this level editor and game engine interesting, we could add Lua script support to the game. Lua is an interpreted programming language—meaning, Lua source code is not compiled; it is simply stored in a text file and treated as text, and yet the Lua interpreter will run our script code at runtime. The ramifications for scripting are enormous. Imagine being able to edit a script without restarting the game. Yes, that’s possible: edit the script, save it, then load and execute the script with the Lua interpreter. Like I said, this gets a bit complicated, but it adds a tremendous amount of design freedom to the game, which is otherwise bound by its data and engine. We will add Lua scripting support to the game in the next section.

Now we need to look at some code to make this all work. Among other things, we have some work to do in the loadTilemap() function now because of the new fields.

Creating a Portal

A portal is a doorway to another dimension. Or, in the case of our editor here, a new x,y location on the map. Or on another map file! Let’s start with a series of portals on a single map and then look at how to portal to another world. Take a look at the data for the highlighted tile in Figure 9.1. The Portal flag is checked, while the x and y fields are set to coordinates (101,16). The examples here are shown using an above-ground representation of the game world instead of a dungeon level, for the sake of clarity—the dungeon levels are both smaller and harder to move around in.

Creating a portal using the portal data fields.

Figure 9.1. Creating a portal using the portal data fields.

The location (101,16) is on the right side of the map, shown in Figure 9.2. What we want to do is have the game jump to that location when our character walks into the portal tile. Nearby, the target location is another portal tile.

The target location of the first portal on the map is (103,16).

Figure 9.2. The target location of the first portal on the map is (103,16).

In case you are wondering why the two portals aren’t linked directly together, that is something you can play with if you want, but if you point one portal to a tile that contains another portal, then your character will teleport twice. Unless you want that kind of behavior, don’t link portal squares directly—have one drop off the player nearby but not directly on another portal. Or, go ahead and do it and see for yourself what happens! In our example, you must press Space to trigger a portal, but if you use automatic teleporting then the player could be teleported repeatedly, possibly even getting stuck in a portal loop.

Teleporting to Another Dimension

The first thing we need to do to get portals working is to isolate the portion of the character sprite that is actually on the “ground,” so to speak. By default, the player sprite (which is called hero in our code) is positioned on the screen in the upper-left corner. Since the sprite is 96x96 pixels in size, there’s a lot of area taken up by the sprite that is much larger than the 32x32 tiles. If we use the upper-left corner, then the player will be interacting with tiles on the ground from a position above and to the left of his or her head! That definitely won’t work. So, we need to adjust the position used to determine what tile the player is walking on—we need to isolate the player’s feet. Figure 9.3 shows the collision boxes for the player sprite. The blue box represents the entire character’s collision box, while the small red box (and red dot) represent the walking collision box.

Isolating the player sprite’s “foot” contact with the ground.

Figure 9.3. Isolating the player sprite’s “foot” contact with the ground.

The small red collision box, and the red dot at its center, is what we actually want to use as a center point to determine which tile the sprite is “walking on.” Thus, when the player walks onto a portal tile, it will accurately look as if the sprite’s feet touched the tile before the teleport occurred. The Portal demo program looks at that coordinate as a position relative to the scroll position and then retrieves the data for the tile at that location. Figure 9.4 shows information about the portal tile the player is standing on—note the message in the upper-left corner of the window.

Detecting when the player walks on a portal tile.

Figure 9.4. Detecting when the player walks on a portal tile.

In the game, it’s up to you how the portals will work. You can make them automatically teleport the player just by merely walking on the tile, or you can require the player to take some action—perhaps using an item to trigger the portal. In our program, the Space key is the trigger. When the portal is engaged, the player is teleported to the target coordinate (101,16), as shown in Figure 9.5.

The portal has sent the player across the map!

Figure 9.5. The portal has sent the player across the map!

Trick

Getting tired of the same old ground tiles in every example? Replace them! You are encouraged to use a different set of ground tiles or add new ones to this collection. I am only using these same tiles for consistency. You may replace the tiles in the level editor and in the game. The only requirement is that your tile palette image be oriented like the one presented in the book and that the tiles remain at 32x32 pixels in size. Otherwise, some coding changes will be needed.

Looking for Tile Collisions

The Portal demo program also looks for the Collidable property in tiles and reports on the screen when a collidable tile is identified. Figure 9.6 shows the message that is printed when the player walks over a collidable tile. Although the sprite doesn’t respond to collidable tiles yet in this example, we can use this information to enable collision response in the next major revision to the game.

Detecting collidable tiles.

Figure 9.6. Detecting collidable tiles.

Hint

This quick example is not quite polished yet, so expect to see some jittery sprites and timing problems. The point is to get these features to work first, and then make them work great afterward!

Secret Doors

With the portal system working, we can now use it to create secret doors to appear to walk through walls! A portal is just a tile property that defines a target location for the player to be moved to. If you set the target for a portal to a tile just one or two spaces away, then it can appear as if the player is hopping over an obstacle. You could use this technique to get past solid objects or walls, which would be even more effective if a trigger object or key is required.

Portal Demo Program

Here is the source code for the Portal demo program.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
using RPG;

namespace Portal_Project
{
    public partial class Form1 : Form
    {
       public struct keyStates
       {
            public bool up, down, left, right;
        }

        Game game;
        Level level;
        keyStates keyState;
        bool gameover = false;
        Sprite hero;
        int heroDir = 0;
        bool portalFlag = false;
        Point portalTarget;
        int drawLast = 0;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Text = "Portal Demo";

            //create game object
            Form form = (Form)this;
            game = new Game(ref form, 800, 600);

            //create tilemap
            level = new Level(ref game, 25, 19, 32);
            level.loadTilemap("portals.level");
            level.loadPalette("palette.bmp", 5);

            //load hero
            hero = new Sprite(ref game);
            hero.Image = game.LoadBitmap("hero_sword_walk.png");
            hero.Columns = 9;
            hero.TotalFrames = 9 * 8;
            hero.Size = new Size(96, 96);
            hero.Position = new Point(400 - 48, 300 - 48);
            hero.AnimateWrapMode = Sprite.AnimateWrap.WRAP;
            hero.AnimationRate = 20;

            while (!gameover)
            {
                doUpdate();
            }
            Application.Exit();
        }

        private void Form1_KeyUp(object sender, KeyEventArgs e)
        {
           switch (e.KeyCode)
           {
               case Keys.Escape : gameover = true; break;
               case Keys.Up:
               case Keys.W : keyState.up = false; break;
               case Keys.Down:
               case Keys.S : keyState.down = false; break;
               case Keys.Left:
               case Keys.A : keyState.left = false; break;
               case Keys.Right:
               case Keys.D : keyState.right = false; break;
               case Keys.Space:
                   if (portalFlag) level.GridPos = portalTarget;
                   break;
           }
        }

        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
           switch (e.KeyCode)
           {
               case Keys.Up:
               case Keys.W: keyState.up = true; break;
               case Keys.Down:
               case Keys.S: keyState.down = true; break;
               case Keys.Left:
               case Keys.A: keyState.left = true; break;
               case Keys.Right:
               case Keys.D: keyState.right = true; break;
           }
        }

        private void doUpdate()
        {
           //move the tilemap scroll position
           int steps = 8;
           PointF pos = level.ScrollPos;

           //up key movement
           if (keyState.up)
           {
               if (hero.Y > 300 - 48) hero.Y -= steps;
               else
               {
                   pos.Y -= steps;
                   if (pos.Y <= 0) hero.Y -= steps;
               }

           }
           //down key movement
           else if (keyState.down)
           {
               if (hero.Y < 300 - 48)
                   hero.Y += steps;
               else
               {
                   pos.Y += steps;
                   if (pos.Y >= (127 - 19) * 32) hero.Y += steps;
               }
           }

           //left key movement
           if (keyState.left)
           {
               if (hero.X > 400 - 48) hero.X -= steps;
               else
               {
                   pos.X -= steps;
                   if (pos.X <= 0) hero.X -= steps;
               }
           }
           //right key movement
           else if (keyState.right)
           {
               if (hero.X < 400 - 48) hero.X += steps;
               else
               {
                   pos.X += steps;
                   if (pos.X >= (127 - 25) * 32) hero.X += steps;
               }
           }

           //update scroller position
           level.ScrollPos = pos;
           level.Update();

           //limit player sprite to the screen boundary
           if (hero.X < -32) hero.X = -32;
           else if (hero.X > 800 - 65) hero.X = 800 - 65;
           if (hero.Y < -48) hero.Y = -48;
           else if (hero.Y > 600 - 81) hero.Y = 600 - 81;

           //orient the player in the right direction
           if (keyState.up && keyState.right) heroDir = 1;
           else if (keyState.right && keyState.down) heroDir = 3;
           else if (keyState.down && keyState.left) heroDir = 5;
           else if (keyState.left && keyState.up) heroDir = 7;
           else if (keyState.up) heroDir = 0;
           else if (keyState.right) heroDir = 2;
           else if (keyState.down) heroDir = 4;
           else if (keyState.left) heroDir = 6;
           else heroDir = -1;

           //get the untimed core frame rate
           int frameRate = game.FrameRate();

           //drawing code should be limited to 60 fps
           int ticks = Environment.TickCount;
           if (ticks > drawLast + 16)
           {
               drawLast = ticks;

               //draw the tilemap
               level.Draw(0, 0, 800, 600);

               //draw the hero
               int startFrame = heroDir * 9;
               int endFrame = startFrame + 8;
               if (heroDir > -1) hero.Animate(startFrame, endFrame);
               hero.Draw();

               //print da stats
               game.Print(700, 0, frameRate.ToString());
               int y = 0;
               game.Print(0, y, "Scroll " + level.ScrollPos.ToString());
               y += 20;
               game.Print(0, y, "Player " + hero.Position.ToString());
               y += 20;

               Point feet = HeroFeet();
           int tilex = (int)(level.ScrollPos.X + feet.X) / 32;
           int tiley = (int)(level.ScrollPos.Y + feet.Y) / 32;
           Level.tilemapStruct ts = level.getTile(tilex, tiley);
           game.Print(0, y, "Tile " + tilex.ToString() + "," +
               tiley.ToString() + " = " + ts.tilenum.ToString());
           y += 20;
           if (ts.collidable)
           {
               game.Print(0, y, "Collidable");
               y += 20;
           }
           if (ts.portal)
           {
               game.Print(0, y, "Portal to " + ts.portalx.ToString() +
                   "," + ts.portaly.ToString());
               portalFlag = true;
               portalTarget = new Point(ts.portalx - feet.X / 32,
                   ts.portaly - feet.Y / 32);
               y += 20;
           }
           else
               portalFlag = false;

           //highlight collision areas around player
           game.Device.DrawRectangle(Pens.Blue, hero.Bounds);
           game.Device.DrawRectangle(Pens.Red, feet.X + 16 - 1,
               feet.Y + 16 - 1, 2, 2);
           game.Device.DrawRectangle(Pens.Red, feet.X, feet.Y, 32, 32);

           //refresh window
           game.Update();
           Application.DoEvents();
       }
       else
       {
           //throttle the cpu
           Thread.Sleep(1);
       }
   }
        //return bottom center position of hero sprite
        //where feet are touching ground
        private Point HeroFeet()
        {
           return new Point((int)(hero.X + 32), (int)(hero.Y + 32 + 16));
        }
    }
}

Level Up!

This chapter saw some dramatic improvements to both the Level class and the Dungeon Crawler game engine code, with the addition of code to detect collidable tiles, and code to make portals active, allowing us to teleport the player to a new location. Although the level editor provides the "portalfile" field to enable teleporting to a position in a different level file, we will reserve that feature for later. Believe it or not, we now have a game world that is suitable as an environment for the Dungeon Crawler game! That means we can shift focus from the game world and level editing over to a new subject—people and monsters!

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

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