Role-Playing Game 简易教程 – 重用 Tile Engine
本文翻译自RPG_Tutorial_2_Engine.doc 文章所提RPG示例可在此下载
这篇教程会告诉你如何在Role-Playing Game (RPG)示例中提取二维贴图及碰撞系统,并将它添加到一个XNA Game Studio创建的全新游戏里。此教程有以下假设:
1) 你将可以使用C#语言轻易写出你自己的游戏。
2) 你对重用这个二维贴图及碰撞系统(或称"Tile Engine")有那么点兴趣。
3) 你会像示例程序那样给地图分层。
4) 你仍然需要"传送门"(Portal)的概念来完成地图之间的切换。
5) 你不需要示例程序除地图之外的其它组分。例如动态的游戏元素,宝箱,怪物,还有任务。当然,如果你的确对这些感兴趣的话,你也可以将这些加到你的项目里,只是此教程不作描述。
6) 你也许会用自己的游戏逻辑来约束Tile Engine,就像RPG示例中的Session部分。
尽管这篇教程是将Tile Engine加入到一个空的Xna Game Studio创建的项目中,不过你仍然可以为了你的游戏适当的调整步骤。你的确可以改变示例中的namespace,class以及一些其它的命名来满足你自已的约定。但你仍要小心翼翼地来调整这些改变。
Tile Engine 的工作原理
Tile Engine是一个代码级的系统,它可以在一张符合规范的二维地图上完成渲染和碰撞检测操作。你可以在解决方案下的RolePlayingGame项目里发现为此目的服务的运行时代码,它们在TileEngine\TileEngine.cs文件里。既然你在同一时间只能运行一个Tile Engine,并且它不拥有任何外部对象的生命周期,所以整个类型被静态指定为TileEngine类型 。
Tile Engine追踪游戏世界里的一点。这一点通常用来做会摄像机的中心,确定可见区域,并完成碰撞检测。它在 TileEngine\PlayerPosition.cs文件中指定为PlayerPosition类型。PlayerPosition有三部分:一个整型数的贴图位置,用来指定“玩家”所在贴图的地图坐标。一个浮点数偏移量,用来表明玩家相对贴图中心点的确切位置。一个方向的枚举值,根据玩家上一次的运行来确定玩家当前面向的方向。
在解决方案下的RolePlayingGameData项目下的Maps\Map.cs指定了Map类型。它包含一个给定的地图的全套数据。而最重要的数据则是四个二维整型数组:
· Base Layer: 一组数字索引,用来确定贴图块绘制地图纹理的那一部分。标明空间中绘制的第一个贴图。通常,这是地面。
· Fringe Layer: 一组数字索引,用来确定贴图块绘制地图纹理的那一部分。标明空间中绘制的第二个贴图。通常情况下,这些贴图一般都是树,建筑物,栅栏等地面上的东西。但可能会配合不同的Base Layer贴图。base和fringe层的分离意味着你可以有一棵对(Fringe Layer)站在任何一种地面贴图(Base Layer)之上。
· Object Layer: 一组数字索引,用来确定贴图块绘制地图纹理的那一部分。标明空间中所有贴图绘完之后的贴图。这些贴图通常显示在所有物体的顶层——角色,宝箱等等。树梢和指示牌也在这一层。
· Collision Layer: 用零(flase)或非零数字(true)作为一个布尔值用来表明此块贴图是否可让玩家进入。
当所有的这些层被TileEngine.DrawLayer绘制时,视图将围绕当前位置,如果可能的话,这将迫使窗口显示当前视野,以至于背景色将永远看不见。
地图对象也包含了所有与玩家交互物体的所有信息。此教程只有一个相关的物体,“传送门”——玩家从一个地图对象移动到另一个地图对象的关口。此数据存储在两个列表里。第一个是传送门列表,此表保存有传送门名称,地图名称,以及传送后的终点信息等。第二个则是MapEntry<Portal>对象的列表,该对象代表了一个传送门实例,特别是贴图在地图上位置。MapEntry对象还包含物体当前面向的数据,但面向对传送门来说并无关系。
Tile Engine的代码结构
Tile Engine像其它大多数XNA Game Studio创建的游戏一样分为三部分。
· Data Project: 一个代码库,这个代码库定义了所有在游戏运行时使用XNA内容管道加载游戏数据的游戏类型。在RPG示例中,对第一个数据类型,游戏类型都使用嵌套类提供了自己的内容管道的读者类型。
· Content Pipeline Extension Project: 一个内容管道的扩展库,提供Data Project定义游戏类型中的写者类型。系统提供的Xml文件的Importers和processors一般不需要更换。像传送门和MapEntry对象并没有他们独立的内容文件。他们作为地图的内容文件生成和加载,而且他们的读和写也是由地图的读写操作来完成。与继承的情况相同——MapEntry的读写调用了基类(ContentEntry)的读写。
· Game Project: 可执行的XNA Game Studio游戏项目,包括了对Data项目和Extension项目的引用,同时将数据存储为XML文件。
Map类型及其它被支持的类型将会定义在Data项目中,Map类型(及其它被支持的类型)的内容管道写者类型将被添加到Extension项目中。最后,TileEngine类型,及少数被支持的类型连同为测试而修改RPG示例的内容会添加到Game项目中。
本质上讲,Tile Engine是静态定义的。一张地图加载到Tile Engine里,便会调用TileEngine.Update和TileEngine.DrawLayer分别用来完成移动/碰撞检测和渲染的工作。Tile Engine并没有作为XNA GameComponent的子类实现,因为各层通常是其它来源的贴图的交错。
创建项目
首先,请先确定你已经在你的电脑上存在一个RPG示例程序。
1) 如果你没有,请先下载并安装在你电脑上。
2) 请在Visual Studio创建一个新的"Role Playing Game – Windows"项目。
3) 打开Windows资源管理器并浏览到RPG项目的目录下。交替在Visual Studio的项目中打开所有的代码文件,右击标签,点击 在Windows资源管理器中打开文件夹,然后浏览至项目的根目录。
注: 本教程将此窗口称为"RPG explorer window"
你会注意到这个文件夹下有三个文件夹——RolePlayingGame, RolePlayingGameProcessors, 还有RolePlayingGameData——与RPG示例解决方案中的三个项目对应。
现在,让我们来创建一个新的Windows解决方案来重用Tile Engine:
1) 请在XNA Game Studio中创建一个新的"Windows Game"项目,你可以随便命名,但请务必勾选"为解决方案创建目录"。
注: 本教程将这个原始项目称为"game project"
2) 在解决方案资源管理器,右击解决方案节点(窗口的根节点),在"添加"选项处悬停,点击"新建项目"。选择"Windows Game Library",起一个不同于game project的名字。
注: 本教程将此项目称为"data project"
在项目中删除Class1.cs。右击data project根结点并点击"添加引用"。在刚弹出的"添加引用"窗口中,点击".NET"标签,向下滚动并从列表中选择"System.Xml",点击 确定。
3) 在解决方案资源管理器,右击解决方案节点(窗口的根节点),在"添加"选项处悬停,点击"新建项目"。选择"Content Pipeline Extension Library",起一个不同于game project和data project的名字。在项目中删除ContentProcessor1.cs。
注: 本教程将此项目称为"pipeline project"
4) 打开Windows资源管理器并浏览到新项目的目录下。交替在Visual Studio的项目中打开所有的代码文件,右击标签,点击 在Windows资源管理器中打开文件夹,然后浏览至项目的根目录。
注: 本教程将此项目称为"new-game explorer window"
你会注意到你所创建的每一个项目都会在这个根文件夹下有自己的子文件夹。
下一步,在新的游戏项目中设置引用:
1) "game project"需要知道所有的数据类型,以便在运行时加载他们。在解决方案资源管理器下,右击"game project"节点,点击"添加引用"。在刚弹出的"添加引用"窗口中,点击"项目"标签,选择"data project",然后点击确定。
2) "pipeline project"同样需要知道所有的数据类型,以便生成他们。在解决方案资源管理器下,右击"pipeline project"节点,点击"添加引用"。在刚弹出的"添加引用"窗口中,点击"项目"标签,选择"data project",然后点击确定。
3) 游戏的"Content Project"需要知道Processor库以便生成内容。在解决方案资源管理器下,右击"Content"节点,点击"添加引用"。在刚弹出的"添加引用"窗口中,点击"项目"标签,选择"pipeline project",然后点击确定。
复制代码
你需要从RPG示例项目中复制Tile Engine的代码及相关的部件到你的新游戏项目中。从"RPG explorer window"特定的文件夹复制下列文件到"new-game explorer window"相应的文件夹下 :
· 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
你不必在项目中保持相同的目录结构,例如,你可以将Map.cs与ContentEntry.cs放在同一文件夹下,但两个文件都必须放在"data project"的目录下的文件夹里。现在,这些文件都已经放在正确的项目目录下了,还需添加所有的代码文件到相应的项目中去。你可以通过右击每个项目,为每一个代码文件添加现有项来完成。然后,既然代码文件早已在正确的位置上了,这便有了一种更为简洁的方法。在每一个项目中,选择根项目节点,在菜单栏的"项目"里点击"显示所有文件"。这样将会用虚线轮廓的白色矩形标明那些在目录下的但不在项目里的文件。选择所有你要复制进去的文件,右击,然后点击"包括在项目中"。在你完成此步工作后,强烈建议你取消"显示所有文件"。这将确保你不会不小心将"bin"或者"obj"文件夹包含进来,或者是其他的任何不期望发生的事情。对每一个项目重复此步骤。
复制测试数据
你的游戏也许会有自己的地图及纹理,但是如果使用示例中的地图和纹理用来测试会简单许多。
在"new-game explorer window",进入"game project"的子目录,再进入"Content"子目录。创建一个名为"Maps"的文件夹,在同一目录(Content)再创建一个名为"Textures"的目录。进入"Textures"子目录,创建子文件夹"Maps"。进入"Maps"目录并创建一个名为"NonCombat"文件夹。在这篇教程结束后,你可以通过编辑Map.cs中的MapReader.LoadContent方法轻松改变地图纹理所在文件夹。
从"RPG explorer window"特定的文件夹复制下列文件到"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
重复本教程中的"复制代码"部分中描述的过程,将文件包括在Content项目中。选择"game project"里面的"Content"节点,在菜单栏的"项目"里点击"显示所有文件"。选择"Maps"和"Textures"子目录,右击,然后点击"包括在项目中",会自动在项目中包含这些目录中的所有内容。
检查你所做的一切
你已经创建了一个带有数据项目和管道项目的新游戏,并且你也已经从RPG示例程序中复制了Tile Engine的源码及其它一些用于测试的内容。
你只是复制了RPG示例中的一个很小的子集而已,对于已经复制的代码及数据文件,特别是地图类型和一些相关的类型和内容,对于它们来说,一些相关的引用类型及方法都被省略了。这些功能可能会可能不会与你的游戏有关。本教程假定它们是不相关的。如果你愿意,你可以在教程结束后再添加这些特性和相关数据到你的游戏里。
接下来的几个步骤,将会从从复制的代码和内容移除这些引用。一般而言,你会发现编译器将会成为你同舟共济的朋友。如果编译器无法正确识别一个特定类型的对象,那这就意味着它是应该删掉的代码行。编译器会用下划线标识出错的代码行,使它们易于辨认。
修改 Data Types
首先,先让我们删掉MapEntry.cs中那些不属于新游戏的代码。MapEntry.cs中的"Graphics Data"分块(region C#里分块预处理命令)包含了有关AnimatingSprite对象的定义,此对象会复制一份角色精灵保存成独立的动画并渲染,我们的新游戏并不需要AnimatingSprite类,你需要删掉整个"Graphics Data"分块。
下一步,我们将移除Map.cs中那些可用但不属于我们新游戏的数据。打开Map.cs文件,并检查其内容,你会注意到大多数的数据是通过一个私有字段及一个公共属性,这通常用来实现读和写的访问。当你要移除一个给定的字段时,你需要连同访问该字段的公共属性一并移除,这些属性应该直接声明在该字段下方。
新游戏并不实现PRG示例中的战斗引擎,导航至"Graphics Data"分块,并移除"combatTextureName"和"combatTexture"字段和属性。
新游戏并不实现RPG示例中的音频管理器及音乐系统。你需要移除整个"Music"分块。
在新游戏中实现的对象只有一个,传送门。打开"Map Contents"分块,并移除除portalEntries字段,与之关联的PortalEntries属性,及FindPortal方法之外的所有代码。被移除的字段和属性是MapEntry泛型中的宝箱,固定战斗,非玩家角色,旅馆,商店及随机遇敌等。传送门的数据应该在"Map Contents"分块开始的部分就被实现了,所以移除该分块的其它部分应该是十分容易的。
现在Map及MapEntry中就只有你感兴趣的数据了。接下来,你必须编辑其余的方法来移除任何对已丢失字段及属性的引用。最简单的方法是让编译器来识别这些错误。在解决方案资源管理器,右击"Data project"节点,然后点击"生成"操作,你应该能看到错误列表窗口中看到许多编译错误。
双击错误列表中的第一个错误。这会将光标移至Map.Clone方法开始的地方。检查这个方法,你会发现许多行已经用下划线标出,这些都是对刚移除字段的引用。移除所有的这些行,包括整个在chestEntries列表(此表已移除)中迭代的For循环。你需要检查一下是否所有移除的行都与先前移除的字段相关,其余字段都还被Clone方法复制或分配。如果你正确的按上述步骤执行了,那么Clone方法应该是这个样子:
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;
}
下一步,导航至"Content Type Reader"分块,并检查MapReader.Read方法。同样,因为我们移除的数据,许多行已经被编译器用下划线标出。移除所有的这些行,包括每个用以加载已丢失地图对象的循环。就是Clone方法那样,你需要检查一下是否所有移除的行都与先前移除的字段相关,以确其它的字段仍然可以被读入。如果你正确的按上述步骤执行了,那么MapReader.Read方法应该是这个样子:
/// <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;
}
}
我们已经完成了对"data project"的修改,但还是让我们用编译器来检测一下吧。在解决方案资源管理器中,右击"data project"节点,点击"生成"。生成操作应该被成功执行并无错误返回。
修改 Pipeline Types
接下来,你必须清理内容管道中有关数据对象的写类型,移除任何对已丢失字段及属性的引用。首先,你需要改变内容管道中有关待读类型的名字。
打开RolePlayingGameWriter.cs. 这个wirter是RPG示例中所有wirter的基类。它的责任是在运行时实现reader及类型名称。这些名字将会让内容管道在加载给定的.XNB二进制文件确定它是那一种类型。这些名字是全修饰的,这意味着它们将包含一个可被发现的反射的名字,你新建的"data project"可能并不叫RolePlayingGameDataWindows或者RolePlayingGameDataXbox。如果是这种情况的话,你需要修改有关"data project"的代码。在这个特殊的文件里按下Ctrl-H用你"data project"的名字替 换 RolePlayingGameDataWindows 。
下一步,你需要将所有对丢失字段的引用移除。在解决方案资源管理器,右击"pipeline project"节点,然后点击"生成"操作,你应该能看到错误列表窗口中看到许多编译错误。
双击错误列表中的第一个错误。这会将光标移至MapWriter.Write方法。像对待Map.Clone方法那样,移除所有出现下划线的行。
注: 在某些情况下,并不是所有不正确的行都会出现下划线。如果你只在当前错误的行看见了下划线,就双击错误列表里每一个错误,这将强制Visual Studio对这些受影响的行画上下划线。
当你完成时,MapWirter.Wirte方法的后半部分(数据验证之后的output.Wirte调用)将会是这个样子:
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);
我们已经完成了对"pipeline project"的修改,但还是让我们用编译器来检测一下吧。在解决方案资源管理器中,右击"pipeline project"节点,点击"生成"。生成操作应该被成功执行并无错误返回。
修改 Map Content
你已经在代码里移除了某些功能,接下来就是在XML中移除引用了这些功能的标签。
在XML文件中移除下列标签,不论是有结束标签还是内容标签,一并移除:<CombatTextureName>, <MusicCueName>, <CombatMusicCueName>, <ChestEntries>, <FixedCombatEntries>, <RandomCombat>, <QuestNpcEntries>, <PlayerNpcEntries>, <InnEntries>, 及<StoreEntries>. Maps\Map001.xml和Maps\Map002.xml均如此修改。
既然地图类型,及其它相关类型,还有内容都已经清理完毕,现在是时候回到"game project"修复Tile Engine本身了。
修改 Tile Engine
为了清理Tile Engine,你仍然需要像数据和管道项目那样,移除那些并不适合新游戏的代码,在解决方案资源管理器里导航至"game project",并打开TileEngine.cs。
第一步是移除对"Input Manager"的引用,它在RPG示例中完成了对输入的高度抽象,这将会导致你的游戏有更高的复杂性或者表现出你并不想要的其它功能。Tile Engine对所有的用户输入都是在本机完成的,它全部包含在TileEngine.UpdateUserMovement里,在"Party"分块中,你的游戏可能没有玩家角色,但功能是相同的。你不妨稍后重命名这些方法和分块。但为了一致性,本教程将参考其原来的名称。
新的输入处理系统将会仅支持手柄输入,通过使用左摇杆来移动当前玩家的位置。
首先,在XNA Framework输入命名空间的上方添加using语句。你还需要在这里为"Content"的命名空间添加一个入口,这将在稍后用到。在代码刚开始的地方,你会发现新的using语句块,应该是这个样子:
#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
开始修改TileEngine.UpdateUserMovement,你需要在这个函数开始的时候添加一个新的调用来读取手柄的最近状态 :
private static Vector2 UpdateUserMovement(
GameTime gameTime)
{
Vector2 desiredMovement = Vector2 .Zero;
GamePadState gamePadState = GamePad .GetState( PlayerIndex .One);
然后,用gamePadState变量的比较来取代每一个InputManager调用。例如,这是之前的代码:
if (InputManager.IsActionPressed(InputManager.Action.MoveCharacterUp))
这是之后的代码:
if (gamePadState.ThumbSticks.Left.Y > 0f)
完成后的TileEngine.UpdateUserMovement会是这个样子:
/// <summary>
/// Update the user-controlled movement of the party.
/// </summary>
/// <returns> The controlled movement for this update. </returns>
private static Vector2 UpdateUserMovement(
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;
}
你仍然需要对Tile Engine做出一些改变。但我们还是再一次让编译器来帮我们解决问题吧。在解决方案资源管理器中,右击"game project"节点,点击"生成"。生成操作应该被成功执行并无错误返回。
双击错误列表中的第一个错误。光标将会出现在这一行:
// 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);
错误提示是当前上下文中不存在名称"Session",但事实是我们并不对SourceOffset有任何兴趣。这东西在Tile Engine里面被用来确保相机指向玩家角色的中心,而不是指向玩家的脚。移除这个东西及其它相关的操作。正确的代码会是这个样子:
// adjust the map origin so that the party is at the center of the viewport
mapOriginPosition += viewportCenter - partyLeaderPosition.ScreenPosition;
错误列表中的下一个错误,会将光标定位在这一行:
mapOriginPosition.Y += MathHelper .Max(
(viewport.Y + viewport.Height - Hud.HudHeight) -
(mapOriginPosition.Y + map.MapDimensions.Y * map.TileSize.Y), 0f);
与一个错误类似,错误提示是当前上下文中不存在名称"Hub",但事实是我们并不需要它了。在RPG示例程序中,屏幕的底部会显示主角队伍的信息,但这并不是新游戏的一部分,我们需要让地图占据整个窗口。你需要移除"Hub",正确的代码会是这个样子:
mapOriginPosition.Y += MathHelper .Max(
(viewport.Y + viewport.Height) -
(mapOriginPosition.Y + map.MapDimensions.Y * map.TileSize.Y), 0f);
错误列表中的第三个错误,也是最后一个,会将光标定位在这一行:
// check for anything that might be in the tile
if ( Session .EncounterTile(mapPosition))
这一行引用了RPG示例中一个非常重要的功能——Session.EncounterTile,它定义在RolePlayingGame项目下的Session\Session.cs文件里。它的功能是检测一个给定贴图与用户的的所有交互,并实时响应。传送门需要这个功能。但是,你并不需要全部Session.cs里面的内容,你只需要将用到的那部分功能添加到TileEngine类里面即可。
你会需要一个ContentManager对象来加载地图对象,新游戏里的静态的TileEngine类没有任何方法来加载游戏对象(RPG示例中使用Session来提供一个可用的静态ContentManager对象)。在TileEngine类的顶部,添加一个public static ContentManager字段,Game1对象将对其初始化 :
static class TileEngine
{
public static ContentManager ContentManager = null ;
在你的游戏里,你最终可能会以另一种方式来实现TileEngine里面的ContentManager 。
让我们回到TileEngine.MoveIntoTile方法,在下面这个"if"语句之上,添加在传送门列表寻找传送门的代码。它们的位置会是这样:
// 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))
然后,修改"if"语句来检测portalEntry是否为空,并添加代码来处理为空的情形:
// 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 ;
}
下一步,我们需要确保portalEntry对象的地图名是有效的Content路径,如果你按之前的方法来了,那么地图XML文件就已经在"game project"的Content\Maps子目录里了。添加新的"if"语句以确保Content的名字是正确的:
// 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 ;
}
最后,如果在新地图中有传送门的话,则添加一个加载新地图的TileEngine.SetMap调用 :
// 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 ;
}
我们已经完成了"game project"的修改,但还是让我们用编译器来检测一下吧。在解决方案资源管理器中,右击"game project"节点,点击"生成"。生成操作应该被成功执行并无错误返回。
统一
祝贺你!你已经完全修改了RPG示例当中的代码。而且所生成的types和Content都没有任何错误了,按F5运行你的游戏吧!
不幸的是,游戏只是渲染出了默认的CornflowerBlue背景色。这是因为游戏从来都没有使用地图也没有实现任何Tile Engine相关的东西。
打开"game project"里面的Game1.cs。首先便是要在文件的顶部使用"using"来添加Tile Engine(RolePlayingGame) 和 data types(RolePlayingGameData)的命名空间。
using RolePlaying;
using RolePlayingGameData;
如果因为你项目的需要,你之前改变过这些命名空间,那你可能就需要改变using之后的RolePlaying或RolePlayingGameData。
接下来,让我添加一行代码来初始化TileEngine里的ContentManager对象:
public Game1()
{
graphics = new GraphicsDeviceManager ( this );
Content.RootDirectory = "Content" ;
// configure the content manager for the tile engine
TileEngine .ContentManager = Content;
}
你在Game1.LoadContent里有两件事可做:
1) 加载刚开始的测试地图,并设置Tile Engine。第一张地图直接生成,所以,你需要传递"null"参数,它会被Map中的spawnposition用到。
2) 通过Tile Engine提供的静态Viewport属性设置当前视口。
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch (GraphicsDevice);
// set the viewport for the tile engine
TileEngine .Viewport = graphics.GraphicsDevice.Viewport;
// load the initial map and set it into the tile engine
TileEngine .SetMap(Content.Load< Map >( @"Maps\\Map001" ), null );
}
Tile Engine同样需要更新,所以要在Game1.Update中调用TileEngine.Update:
protected override void Update( GameTime gameTime)
{
// Allows the game to exit
if ( GamePad .GetState( PlayerIndex .One).Buttons.Back == ButtonState .Pressed)
this .Exit();
// update the tile engine
TileEngine .Update(gameTime);
base .Update(gameTime);
}
最后,就只剩在窗口画出Tile Engine的渲染了。在SpriteBatch对象的Begin和End方法之间添加Tile Engine画层方法的调用。你需要留下一层来渲染之后可能用到的东西,这一层将介于base层和fringe层之间。因此,你将调用两次DrawLayer方法:
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);
}
这样,就已经完成了所有必需的修改。按F5运行你的游戏吧!
小结
我们写了这篇教程,就当是你用来使用RPG示例中大部分功能的方法吧。如果你的游戏会用 到 AnimatingSprites,或者是其它的功能,你可以将更多的代码置入其中。Tile Engine中还有许多十分有用的常量及功能等你挖掘。分析代码,让所有你游戏所需要的都增加进去吧。
最后就是实现你自己的游戏,或者是熟悉这些步骤,将Tile Engine添加到你已有的项目中去。RPG示例程序提供给你的Tile Engine将给予你2D游戏制作中强力的支持。
附:
本文翻译自RPG_Tutorial_2_Engine.doc 文章所提RPG示例可在此下载
XNA教程在国内教程少的可怜 之前学习时十分感激上海第八中学物理组的翻译 于是在大致实现了教程步骤之后 决意翻译此文 希望对后来学习XNA的有所帮助~ 不过由于俺自身英语水平有限 如有不懂 请直接参阅原文。
我是用XNA4.0实现 许多步骤并非完全一致 但大致无异 原文有 “Let’s Take This Tile Engine for a Spin!” 部分 因XNA 4.0中的GraphicsDevice.Clear并没有相关Rectangle参数的重载,无法通过编译,未有实践 故未有翻译。
