Professional Documents
Culture Documents
RPG Tutorial 2 Engine
RPG Tutorial 2 Engine
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.
Note: The tutorial will refer to this project as the data project.
Delete Class1.cs from the project. Right-click the data project root node and
click Add Reference. In the new Add Reference window that pops up,
click the .NET tab, scroll down and select System.Xml from the list, and
click OK.
3) In Solution Explorer, right-click the Solution node (the root node in the
window), hover over the Add option, and click New Project. Select Content
Pipeline Extension Library , and give it a different name from the game
and data projects. Delete ContentProcessor1.cs from the project.
Note: The tutorial will refer to this project as the pipeline project.
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, rightclick 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
o 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.
Copy the following files from the specified folders in the RPG explorer window into
the new-game explorer window:
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.
The new game does not implement the RPG Samples combat engine. Navigate to
the Graphics Data region, and remove the combatTextureName and
combatTexture fields, and the CombatTextureName and CombatTexture
properties.
The new game does not implement the audio manager or music system from the
RPG Sample. You need to remove the entire Music region.
The only map objects that are implemented in the new game are portals. Open the
Map Contents region and remove everything except the portalEntries field, the
associated PortalEntries property, and the FindPortal method. The removed
fields and properties are MapEntry lists for chests, fixed-combats, non-player
characters, inns, and stores, and the RandomCombat field and property. The
portal data should be implemented first in the Map Contents region, so it should
be easy to remove everything else inside that region.
The Map and MapEntry types now only define the data in which you are
interested. Now you have to edit the remaining methods to remove any references
to the now-missing fields and properties. The easiest way to identify these is to let
the compiler do the work. In the Solution Explorer, right-click the data project node,
and click Build. You should see a number of compiler errors in the Error List
window.
Double-click the first listing in the Error List window. This will take you to the
beginning of the Map.Clone method. Examine the method, and you will see a
number of lines that have segments 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.
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.
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))
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))
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
MapEntry<Portal> portalEntry = map.PortalEntries.Find(
delegate(MapEntry<Portal> entry)
{
return (entry.MapPosition == mapPosition);
});
// check for anything that might be in the tile
if (Session.EncounterTile(mapPosition))
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);
}
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
// draw the object layer
TileEngine.DrawLayers(spriteBatch, false, false, true);
spriteBatch.End();
base.Draw(gameTime);
}
That should be all of the modifications necessary. Press F5 to run the game.
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.