Professional Documents
Culture Documents
RPG Tutorial 2 Engine
RPG Tutorial 2 Engine
Introduction
In this tutorial, you are going to extract the two-dimensional tile-based rendering and collision system
from the Role-Playing Game (RPG) Sample, and add it to a new XNA Game Studio game. The tutorial
makes the following assumptions about the needs of your game:
1) You are comfortable working with C# and developing your own game.
2) You are interested in re-using the RPG Samples tile-based rendering and collision system (the
tile engine).
3) You will define the map layers in the same manner as the RPG Sample.
4) You still need portals to move between maps.
5) You are not interested in any of the other components of the RPG Sample, including animated
sprites, treasure chests, monsters, and quests. If you are interested in any of these, you can add
them to your new project, as needed.
6) You will tie the tile engine into your own game logic, which may or may not be based on the
Session type in the RPG Sample.
Though this tutorial adds the tile engine to an empty XNA Game Studio project, you can adjust the steps
as appropriate for your own game. You may also feel free to adjust namespace, class, or other names
within the RPG Sample code to match your own conventions. However, be careful to accommodate
those changes as you work through the tutorial.
Base Layer: A numerical index into the tiles in the maps texture, specifying the first sprite
drawn in the space. Typically, this is the ground.
Fringe Layer: A numerical index into the tiles in the maps texture, specifying the second sprite
drawn in the space. Typically, these sprites are trees, buildings, fences anything that is at
ground level, but might be matched up with various ground tiles. The separation between the
base and fringe layers means you can have one tree sprite (fringe layer) that can sit on top of
any kind of ground sprite (base layer).
Object Layer: A numerical index into the tiles in the maps texture, specifying the sprite drawn
in the space after all objects have been drawn. These sprites always appear on top of all other
objects in the same tile characters, chests, and so on. These include treetops and signs.
Collision Layer: An integer of value 0 (false) or nonzero (true), used as a Boolean value
representing whether that tile can be entered by the player.
When these layers are drawn with TileEngine.DrawLayer, the view will be centered on the current
position, if possible. It will constrain the view to a given viewport so that the background color is never
seen.
The map object also contains lists of the objects with which the player may interact. The only relevant
one to this tutorial is the list of portals gateways that move the player from one map object to
another. This data is stored in two lists. The first list contains a list of the Portal objects with their
names, and the map name, portal names, and final destination to which they link. The second list
contains MapEntry<Portal> objects, which represent instances of the portal at a particular tile position
on the map. MapEntry objects also contain data about the direction that the object is facing, but facing
direction does not matter for portal objects.
Data Project: A code library that defines the game types that will be used in the games data
files and loaded by way of the XNA Content Pipeline at run-time. In the RPG Sample, the game
types provide their own Content Pipeline reader types as nested classes within each data type.
Content Pipeline Extension Project: A Content Pipeline extension library that provides content
writers for the game types defined in the data project. The built-in importers and processors for
XML files generally dont need to be replaced. Objects such as portal objects and MapEntry
objects do not have their own separate content files. They are built and loaded as part of
building and loading a map content file, and their readers and writers are invoked by the map
reader and writer. The same is true of inherited types the MapEntry reader and writer calls
into the base types (ContentEntry) reader and writer.
Game Project: The executable XNA Game Studio game project, containing references to the
data and extension projects, as well as game data stored as XML.
A Map type will be defined in the data project, along with any supporting types that are necessary.
Content Pipeline writer types for the Map type (and any supporting types) will be added to the
extension project. Finally, the TileEngine type, and a few supporting types, will be added to the game
project, along with a modified piece of content from the RPG Sample for testing purposes.
Internally, the tile engine is defined statically. A single map is set into the tile engine, and calls to
TileEngine.Update and TileEngine.DrawLayer will use that maps data for movement/collision and
rendering, respectively. The tile engine is not implemented as a XNA GameComponent subclass
because the layers typically are interleaved with sprites from other sources.
4) Open Windows Explorer and navigate to the new projects directory. An alternate method is to
open any code file in the project in Visual Studio, right-click its tab, click Open Containing
Folder, and then navigate up to the root folder of the project.
Note: The tutorial will refer to this window as the new-game explorer window.
You will notice that each of the three projects you just created has its own subdirectory in this
root folder.
Next, set up the project references in the new game project:
1) The game project needs to know about the data types so it can load them at runtime. In
Solution Explorer, right-click the game project node, and click Add Reference. In the Add
Reference window that pops up, click the Projects tab, select your data project, and then click
OK.
2) The pipeline project also needs to know about the data types so it can build them. In Solution
Explorer, right-click the pipeline project node, and click Add Reference. In the Add Reference
window that pops up, click the Projects tab, select your data project, and then click OK.
3) The games content project needs to know about the processor library so it can build the
content. In Solution Explorer, right-click the Content node under game project node, and then
click Add Reference. In the Add Reference window that pops up, click the Projects tab, select
your pipeline project, and then click OK.
Data Project:
o RolePlayingGameData\Map\Map.cs
o RolePlayingGameData\Map\Portal.cs
o RolePlayingGameData\ContentEntry.cs
o RolePlayingGameData\ContentObject.cs
o RolePlayingGameData\Direction.cs
o RolePlayingGameData\MapEntry.cs
Pipeline Project:
o RolePlayingGameProcessors\Map\MapWriter.cs
o RolePlayingGameProcessors\Map\PortalWriter.cs
o RolePlayingGameProcessors\ContentEntryWriter.cs
o RolePlayingGameProcessors\MapEntryWriter.cs
o RolePlayingGameProcessors\RolePlayingGameWriter.cs
Game Project:
o RolePlayingGame\TileEngine\PlayerPosition.cs
RolePlayingGame\TileEngine\TileEngine.cs
You do not need to preserve the same subdirectory structure inside the new project folders. For
example, you may place Map.cs in the same directory as ContentEntry.cs, though both code files must
be within the new data projects directory.
Now that the files are in the correct project directories, add the code files to their respective projects.
You could do this by right-clicking on each project, and clicking Add Existing Item for each code file.
However, since the code files are in the correct locations already, there is a faster way. For each project,
select the root project node, go to the Projects menu, and then click Show All Files. This will show white
rectangles with dashed outlines next to files that are present in the projects directory, but are not yet
included in the project. Select all the code files that you have copied in, right-click them, and then click
Include in Project. You should toggle off Show All Files on each project after you are done. This ensures
you do not accidentally include the bin or obj directories, or any other unintended content. Repeat
this process for each project.
Game Project\Content\Maps:
o RolePlayingGame\Content\Maps\Map001.xml
o RolePlayingGame\Content\Maps\Map002.xml
GameProject\Content\Textures\Maps\NonCombat:
o RolePlayingGame\Content\Textures\Maps\NonCombat\ForestTiles.png
Repeat the process described in the Copying the Code section of this tutorial to include the content
files in the content project. Select the Content node in the game project, go to Project, and click Show
All Files. Select the Maps and Textures subdirectories, right-click them, and then click Include in
Project, which will automatically include all contents of those directories in the project as well.
You only copied a small subset of the code from the RPG Sample, and the code and data files that were
copied particularly the Map type and related types and content reference types and methods that
were left behind. These features may or may not be relevant to your game. This tutorial assumes they
are not relevant. If you want, you can add those features and associated data back in after you
complete this tutorial.
The next several steps will remove these references from the copied code and content. In general, you
will find the compiler to be your friend if it does not know about a certain type of object, then chances
are its referring to a line of code that should be removed. It will even underline each affected line,
making them easy to identify.
underlined these are references to the recently-removed fields. Remove each of these lines, including
the entire for-loop that iterates over the chestEntries list (since that list is gone). You can check that the
lines removed correlate exactly to the fields that were removed earlier, and that all of the fields
remaining are still copied or assigned in the Clone method. If you have done this correctly, the Clone
method should look like this:
public object Clone()
{
Map map = new Map();
map.AssetName = AssetName;
map.baseLayer = BaseLayer.Clone() as int[];
map.collisionLayer = CollisionLayer.Clone() as int[];
map.fringeLayer = FringeLayer.Clone() as int[];
map.mapDimensions = MapDimensions;
map.name = Name;
map.objectLayer = ObjectLayer.Clone() as int[];
map.portals.AddRange(Portals);
map.portalEntries.AddRange(PortalEntries);
map.spawnMapPosition = SpawnMapPosition;
map.texture = Texture;
map.textureName = TextureName;
map.tileSize = TileSize;
map.tilesPerRow = tilesPerRow;
return map;
}
Next, navigate to the Content Type Reader region, and examine the MapReader.Read method. Again, a
number of lines have been underlined by the compiler, signifying errors due to our removed data.
Remove each of the lines, including each loop that loads the missing lists of map objects. As with the
Clone method, you can check that the lines removed correlate exactly to the fields that were removed
earlier, and that all of that fields remaining are still read in. If you have done this correctly, then the
MapReader.Read method should look like this:
/// <summary>
/// Read a Map object from the content pipeline.
/// </summary>
public class MapReader : ContentTypeReader<Map>
{
protected override Map Read(ContentReader input,
Map existingInstance)
{
Map map = existingInstance;
if (map == null)
{
map = new Map();
}
map.AssetName = input.AssetName;
map.Name = input.ReadString();
map.MapDimensions = input.ReadObject<Point>();
map.TileSize = input.ReadObject<Point>();
map.SpawnMapPosition = input.ReadObject<Point>();
map.TextureName = input.ReadString();
map.texture = input.ContentManager.Load<Texture2D>(
System.IO.Path.Combine(@"Textures\Maps\NonCombat",
map.TextureName));
map.tilesPerRow = map.texture.Width / map.TileSize.X;
map.BaseLayer = input.ReadObject<int[]>();
map.FringeLayer = input.ReadObject<int[]>();
map.ObjectLayer = input.ReadObject<int[]>();
map.CollisionLayer = input.ReadObject<int[]>();
map.Portals.AddRange(input.ReadObject<List<Portal>>());
map.PortalEntries.AddRange(
input.ReadObject<List<MapEntry<Portal>>>());
foreach (MapEntry<Portal> portalEntry in map.PortalEntries)
{
portalEntry.Content = map.Portals.Find(
delegate(Portal portal)
{
return (portal.Name == portalEntry.ContentName);
});
}
return map;
}
}
We should be done with the data project, but lets use the compiler to check that for us. In the Solution
Explorer, right-click the data project node, and click Build. The build should be successful and return no
errors.
Next, you need to know what references to now-missing data need to be removed. In the Solution
Explorer, right-click the pipeline project node, and click Build. You should see a number of compiler
errors in the Error List window.
Double-click the first entry. This should take you to the MapWriter.Write method. As with Map.Clone,
remove all the lines that have been underlined.
Note: In some cases, all of the incorrect lines might not be underlined. If you only see an underline
under the currently selected line, double-click each of the entries in the Error List window. This will force
Visual Studio to underline the affected lines.
When you finish, then the second half of MapWriter.Write method (the output.Write calls, after the
data validation) should look like this:
output.Write(value.Name);
output.WriteObject(value.MapDimensions);
output.WriteObject(value.TileSize);
output.WriteObject(value.SpawnMapPosition);
output.Write(value.TextureName);
output.WriteObject(value.BaseLayer);
output.WriteObject(value.FringeLayer);
output.WriteObject(value.ObjectLayer);
output.WriteObject(value.CollisionLayer);
output.WriteObject(value.Portals);
output.WriteObject(value.PortalEntries);
We should be done with the pipeline project, but lets use the compiler to check that for us. In the
Solution Explorer, right-click the pipeline project node, and click Build. The build should be successful
and return no errors.
The first step is to remove references to the Input Manager, which is a complex input abstraction for the
RPG Sample that might cause your game have more complexity or exhibit a different functionality than
you want. The tile engine code that takes user input is well-localized; it is all in
TileEngine.UpdateUserMovement, in the Party region. Your game may not have a party of player
characters, but the functionality is the same. You may wish to rename these methods and regions later.
However, for consistency, the tutorial will refer to their original names.
The new input-handling system will be gamepad-only, and it will use the left thumbstick for moving the
current player position.
First, add a using statement to the top of the function for the XNA Frameworks Input namespace.
While youre here, add an entry for the XNA Frameworks content namespace, which you will need later.
The new using-statement block, found at the beginning of the code file, should look like this:
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using RolePlayingGameData;
#endregion
Then, replace each of the InputManager calls with equivalent comparisons to the gamePadState
variable. For example, this is the old code:
if (InputManager.IsActionPressed(InputManager.Action.MoveCharacterUp))
GameTime gameTime)
{
Vector2 desiredMovement = Vector2.Zero;
GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);
// accumulate the desired direction from user input
if (gamePadState.ThumbSticks.Left.Y > 0f)
{
if (CanPartyLeaderMoveUp())
{
desiredMovement.Y -= partyLeaderMovementSpeed;
}
}
if (gamePadState.ThumbSticks.Left.Y < 0f)
{
if (CanPartyLeaderMoveDown())
{
desiredMovement.Y += partyLeaderMovementSpeed;
}
}
if (gamePadState.ThumbSticks.Left.X < 0f)
{
if (CanPartyLeaderMoveLeft())
{
desiredMovement.X -= partyLeaderMovementSpeed;
}
}
if (gamePadState.ThumbSticks.Left.X > 0f)
{
if (CanPartyLeaderMoveRight())
{
desiredMovement.X += partyLeaderMovementSpeed;
}
}
if (desiredMovement == Vector2.Zero)
{
return Vector2.Zero;
}
return desiredMovement;
}
You need to make several other changes to the tile engine, but lets use the compiler again to find those
problems. In the Solution Explorer, right-click the game project node, and click Build. You should see
several compiler errors in the Error List window.
Double-click the first entry in the Error List window, and you should be taken to this line:
// adjust the map origin so that the party is at the center of the viewport
mapOriginPosition += viewportCenter - (partyLeaderPosition.ScreenPosition +
Session.Party.Players[0].MapSprite.SourceOffset);
The error text states that Session does not exist, but the real error is that we are no longer interested in
a SourceOffset. This term is used by the tile engine to ensure that the camera is pointing at the center
of the player sprite, not its feet. Remove that term and the operators that depend on it. The corrected
code should look like this:
// adjust the map origin so that the party is at the center of the viewport
mapOriginPosition += viewportCenter - partyLeaderPosition.ScreenPosition;
The next entry in the Error List window takes you to this line:
mapOriginPosition.Y += MathHelper.Max(
(viewport.Y + viewport.Height - Hud.HudHeight) (mapOriginPosition.Y + map.MapDimensions.Y * map.TileSize.Y), 0f);
As with the last one, the error text states that the Hud type does not exist, but the real error is that we
are no longer interested in that term. In the RPG Sample, the bottom of the screen is taken up by a
heads-up display with information about the party. Thats not part of the new game, and the map
display will take up the whole screen. You need to remove that term. The corrected code should look
like this:
mapOriginPosition.Y += MathHelper.Max(
(viewport.Y + viewport.Height) (mapOriginPosition.Y + map.MapDimensions.Y * map.TileSize.Y), 0f);
The third and final entry in the Error List window should take you to this line:
// check for anything that might be in the tile
if (Session.EncounterTile(mapPosition))
This line refers to a very important function in the RPG Sample, Session.EncounterTile, which is defined
in Session\Session.cs in the RolePlayingGame project. This function checks a given tile for anything with
which the user can interact, and it responds appropriately. You need that functionality for portals.
However, you do not want to bring in all of Session.cs, so you will need to add the functionality to the
TileEngine class.
You will need a ContentManager object to load the map objects. The static TileEngine does not have
any way to retrieve one from the new games Game object (the RPG Sample used the Session type to
make a ContentManager object available statically). At the top of the TileEngine class, add a public
static ContentManager field to the TileEngine, which the Game1 object will eventually fill:
static class TileEngine
{
public static ContentManager ContentManager = null;
In your game, you may eventually make the content manager available to the tile engine in another
manner.
Return to the TileEngine.MoveIntoTile method. Above the if statement, add a line to search for
portals in the portal entry list, using the given position:
// search for portals in the new tile
Then, change the if statement to check whether portalEntry is null, adding code to handle the null
case:
// search for portals in the new tile
MapEntry<Portal> portalEntry = map.PortalEntries.Find(
delegate(MapEntry<Portal> entry)
{
return (entry.MapPosition == mapPosition);
});
// if there is a portal, then move through it
if ((portalEntry != null) && (portalEntry.Content != null))
{
return false;
}
Next, we need to make sure that the map name in the portalEntry object is a valid content path. If you
have followed the directions to this point, then the map XML files are in the Content\Maps subdirectory
inside your game project. Add code within the new if statement to make sure that the content name is
correct:
// search for portals in the new tile
MapEntry<Portal> portalEntry = map.PortalEntries.Find(
delegate(MapEntry<Portal> entry)
{
return (entry.MapPosition == mapPosition);
});
// if there is a portal, then move through it
if ((portalEntry != null) && (portalEntry.Content != null))
{
// make sure the content name is valid
string mapContentName =
portalEntry.Content.DestinationMapContentName;
if (!mapContentName.StartsWith(@"Maps\"))
{
mapContentName = System.IO.Path.Combine(@"Maps", mapContentName);
}
return false;
}
Finally, add calls to load the new map and to TileEngine.SetMap, passing in the new map and the new
portal, if any:
// search for portals in the new tile
MapEntry<Portal> portalEntry = map.PortalEntries.Find(
delegate(MapEntry<Portal> entry)
{
return (entry.MapPosition == mapPosition);
});
// if there is a portal, then move through it
if ((portalEntry != null) && (portalEntry.Content != null))
{
// make sure the content name is valid
string mapContentName =
portalEntry.Content.DestinationMapContentName;
if (!mapContentName.StartsWith(@"Maps\"))
{
mapContentName = System.IO.Path.Combine(@"Maps", mapContentName);
}
// load the new map
Map newMap = ContentManager.Load<Map>(mapContentName);
SetMap(newMap,
newMap.FindPortal(portalEntry.Content.DestinationMapPortalName));
return false;
}
We should be done with the pipeline project, but lets use the compiler to check that for us. In the
Solution Explorer, right-click the pipeline project node, and click Build. The build should be successful
and return no errors.
Hooking it Up
Congratulations! You have modified the RPG Sample code, and it builds all of the types and content
without any errors. Press F5 to start running your game.
Unfortunately, the game only renders the default CornflowerBlue background color. This is because
there is nothing in the Game implementation that makes use of the Map or TileEngine types!
Open Game1.cs within the game project. First, add using lines to the top of the file for the
namespaces used by the tile engine (RolePlayingGame) and the data types (RolePlayingGameData).
using RolePlaying;
using RolePlayingGameData;
If you choose to rename the namespaces as you added the files to match your own project, then you
might not need to do this.
Next, add a line of code to fill the ContentManager object in the TileEngine type:
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
// configure the content manager for the tile engine
TileEngine.ContentManager = Content;
}
Finally, the tile engine needs to be drawn. Add calls to begin and end the SpriteBatch object, and draw
the layers of the tile engine. You need to leave a spot for any additional object rendering you might
choose to add later. That spot would fit between the base and fringe drawing and the object drawing.
Thus, you will need two DrawLayer calls:
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
//
// draw the tile engine
//
// draw the base and fringe layers
TileEngine.DrawLayers(spriteBatch, true, true, false);
// TODO: draw anything that goes on the map
That should be all of the modifications necessary. Press F5 to run the game.
spriteBatch.End();
base.Draw(gameTime);
}
Now you can easily see that the current position is blocked by the collision layer, when the tile engine
scrolls, and when you reach the bottom of Map001 and move to Map002. Be careful when moving to
the southern end of the bridge on Map002. Your game will probably attempt to load Map003, which
you have not brought over.
Conclusion
We structured this tutorial to port the most functionality with the fewest steps. If your game would
benefit from AnimatingSprites, or any of the other functionality in the game, then you can port more
code over. Also, there are many useful constants and other pieces of functionality to explore in the tile
engine. Examine the code and make whatever modifications your game needs.
The final step is to implement your own game, or to adapt these steps to add the tile engine to your
existing project. The RPG Sample provides you with a tile engine that gives you a strong baseline for
implementing your own two-dimensional game.