介绍
这个指南将指引我们一步步的通过使用Game Studio Express Beta2来创建一个简单的基于贴片(tile)的引擎。这个引擎与那些以前的旧型RPG游戏很相似。但是稍微比那些强一点(包括美工能力,因为我的贴片(tile)实在太糟糕了!)你可以使用GSE(game studio express,以后简称GSE)很快捷的装配一个比那些好看了多的贴片。
我要创建的引擎是一个基于正方形-贴片(square-tile)的系统。我打算在不久之后再推出一个制作基于等尺寸贴片(isometric tile)的引擎的教程。
概念
在这个指南当中,我们将要制作一个简单的基于贴片的引擎。要开始工作,首先我们需要:
l 一个用来显示贴片的地图网格
l 一个“贴片集”(“Tile Set”),就是一个正方图形,可以绘制在地图上来表示玩家的位置。
创建工程
打开Game Studio Express Beta然后创建一个类型为”Windows Game(XNA)”的新工程。别忘了为你的工程起一个名字。尽管在指南I中我依然使用Game1.cs这个文件名,但是我建议大家不要使用默认的”Game1.cs”这个文件名。基本的”Windows Game(XNA)”模版包含了一个Graphics对象和一个代表你的游戏的叫做Game1.cs的类。Game1.cs中的代码包含如下五个重要的方法:
l Initialize – 在游戏启动的时候执行。在这个函数里你可以在你的游戏启动的时候注册你的游戏组件和其他的东西。
l LoadGraphicsContent – 也是在游戏启动的时候执行,但是在游戏丢失显示焦点以后,需要重新加载非自动管理素材(content)时,还会再次执行。
l UnloadGraphicsContent – 在游戏退出时调用他来释放素材(content)。
l Update – 这个函数在一个连续循环中不断执行,你可以在这里添加你的游戏逻辑。而且在这里你可以接收玩家的输入信息,更新游戏中物体的位置,等等。
l Draw – 最后,Draw方法负责来渲染当前游戏的场景状态到屏幕。系统会尽其所能的不断执行这个函数(翻译的不大好,其实原句的意思就是系统会尽可能多的调用这个函数来获得更好的显示效果,即帧频更高)。
创建我们的资源
在创建我们的引擎之前,我们需要一些用于显示的贴片。我简单的缩放了一些从The TextureBin 网站找到的纹理图来制作了一些贴片。那些拥有绘画才能的天才们可以自己绘制更漂亮的贴片集。创建的贴片每个与每个之间必须连接的很平滑,这也是相当重要的。
好了,下面就是我为这个指南快速“创建”的贴片
草(grass) 路(Road) 石头(Rock) 水(water)
你可以下载并保存这些图片到你的机器上供自己使用。在这个引擎例子种,我设置贴片的大小为48×48像素。所有贴片集中的贴片(本引擎中)都必须是这个尺寸。
向工程中添加资源
为了让我们的游戏能够访问这些资源,我们需要把他们添加到工程中去。因为我们还没有对这些精灵(sprites)设置透明,我们可以将他们仍旧保存为.JPG文件。DirectX Texture Tool能够用来创建带有alpha通道的纹理。我们将在后续的课程讲到如何在地图中添加一个图形来表示玩家的时候,讲解如何使用DierctX Texture Tool。
对于Beta 2版本,我们将使用”素材管道”来管理我们的图形素材。素材管道能让我们的资源变成一种既能在windows上也能在Xbox上使用的格式。
在解决方案资源管理器中用鼠标右键点击你的工程的名字,然后选择”添加->新建文件夹”。将文件夹命名为”Content”。鼠标右键点击Content文件夹,然后再一次选择”添加->新建文件夹”。这次我们给新建的文件夹起名叫”Textures”。
现在,使用Windows Explorer(Windows资源管理器)复制你的图形资源到Textures文件夹下。当你做好这一切以后,Conternt/Textures文件夹下应该有4个后缀名为.JPG文件。
回到Visual C#编译器,然后右击Texures文件夹然后选择”添加->现有项”,在出现的对话框中,选中这些贴片文件(你可能需要改变文件类型为images以后才可以看到他们译注:使用images可能也不能看到,我在我的机器上发现images里没有支持jpg文件格式,所以应该选择”all files”即”所有文件”类型才能看到)然后点击确定。素材管道将给我们每一个资源分配一个资源名。(在解决方案资源管理器下面的属性窗口中可以看到)首先,我们要把我们的图形设置为”始终复制”,这样他们就能够在程序运行时可以被访问到。素材管道知道如何来操作这些素材。
声明变量
首先第一件我们需要做的事就是声明一些我们将在游戏中表示我们游戏中物体的变量。既然这样,我们需要一个地图,绘制贴片的”精灵”,还有一些在Draw和Update中操作游戏状态的控制变量。
在解决方案资源管理器中右键单击Game1.cs然后选择查看代码。找到构造函数(Public Game1())然后将下面的代码复制到构造函数结束大括号的后面(译注:即在构造函数的两个大括号外面,千万不要放到构造函数内部了)(这些变量在所有的方法之外,所以他们可以被类中的所有方法访问)
// An array of "Texture2D" objects to hold our Tile Set
Texture2D[] t2dTiles = new Texture2D[4];
const int iMapWidth = 20;
const int iMapHeight = 20;
// Our simple integer-array based map
int[,] iMap = new int[iMapHeight,iMapWidth] {
{ 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
{ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};
// Variable we will need for Keyboard Input
KeyboardState ksKeyboardState;
//Map coordinates for upper left corner
int iMapX = 0;
int iMapY = 0;
// How far from the Upper Left corner of the display do we want our map to start
int iMapDisplayOffsetX = 30;
int iMapDisplayOffsetY = 30;
// How many tiles should we display at a time
int iMapDisplayWidth = 6;
int iMapDisplayHeight = 6;
// The size of an individual tile in pixels
int iTileWidth = 48;
int iTileHeight = 48;
// How rapidly do we want the map to scroll?
float fKeyPressCheckDelay = 0.25f ;
float fTotalElapsedTime=0;
//this is the object that will draw the sprites
SpriteBatch spriteBatch;
这里的变量有不少,我来一步一步的讲解:
// An array of "Texture2D" objects to hold our Tile Set
Texture2D[] t2dTiles = new Texture2D[4];
这里声明了一个名字为t2dTiles的“Texture2D”类型的对象的数组,我们在引擎中定义了4个文件。这个对象数组为精灵保存位图数据,这个精灵将用于贴片集。
const int iMapWidth = 20;
const int iMapHeight = 20;
// Our simple integer-array based map
int[,] iMap = new int[iMapHeight,iMapWidth] {
{ 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
{ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};
这是个非常简化的游戏地图的表示。我们定义了这个地图的尺寸为20乘20个贴片(通过iMapWidth和iMapHeight常量),然后简单的声明了一个表示地图数组。一个更加真实的系统应该是从外部读取地图数据的,或者是一个使用地图编辑器制作的固定的资源。不过对于这个指南来说,这已经足够了。数组的元素数目即地图中贴片的数目。
// Variable we will need for Keyboard Input
KeyboardState ksKeyboardState;
当我们在后续教程中允许玩家按下键盘上的某键来进行”移动”的时候,我们会需要这个变量的。
//Map coordinates for upper left corner
int iMapX = 0;
int iMapY = 0;
// How far from the Upper Left corner of the display do we want our map to start
int iMapDisplayOffsetX = 30;
int iMapDisplayOffsetY = 30;
// How many tiles should we display at a time
int iMapDisplayWidth = 6;
int iMapDisplayHeight = 6;
// The size of an individual tile in pixels
int iTileWidth = 48;
int iTileHeight = 48;
这里应该没有仔细解释的必要了吧。这些变量将在Draw方法中用来确定如何复制精灵来显示。
// How rapidly do we want the map to scroll?
float fKeyPressCheckDelay = 0.25f ;
float fTotalElapsedTime=0;
这些变量用于控制地图要多快的响应用户的输入。我们不需要一点点的漫步,当玩家按下一个键然后地图应该快速向玩家指定的那个方向移动,应该和Update函数的调用一样快。
//this is the object that will draw the sprites
SpriteBatch spriteBatch;
最后我们需要一个在Draw方法内使用的SpriteBatch对象。SpriteBatch负责复制精灵变量中的贴片到显示缓存中。
加载资源
我们工程的默认框架中为我们创建了”LoadGraphicsContent”方法,而且会在游戏启动时自动调用。要加载我们的素材,使用下面的代码:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
// TODO: Load any ResourceManagementMode.Automatic content
t2dTiles[0] = content.Load<Texture2D>(@"content/textures/tilemap_tut_grass");
t2dTiles[1] = content.Load<Texture2D>(@"content/textures/tilemap_tut_water");
t2dTiles[2] = content.Load<Texture2D>(@"content/textures/tilemap_tut_road");
t2dTiles[3] = content.Load<Texture2D>(@"content/textures/tilemap_tut_rock");
spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
}
// TODO: Load any ResourceManagementMode.Manual content
}
如果loadAllContent为真,我们就应该加载所有(表示我们是在游戏启动的第一次调用这个函数)。如果不为真,那么就只需要加载我们手动控制的资源。我们将管理这些资源的工作交给XNA来作,所以我们只需要调用他们一次就够了。
注意我们没有在纹理文件的后面加上扩展名。这是因为素材管道自动分配了一个名字给每个资源,默认是这个文件的去掉扩展名的文件名,由于素材管道的作用实际上我们这里并不是直接操作的文件。
绘制地图
如果你现在编译并执行你的工程,你将只会得到一个默认的蓝色窗口,因为我们还没有对两个”游戏循环”函数做任何修改。将Draw方法修改为:
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
// Draw the map
for (int y = 0; y < iMapDisplayHeight; y++)
{
for (int x = 0; x < iMapDisplayWidth; x++)
{
spriteBatch.Draw(t2dTiles[iMap[y + iMapY, x + iMapX]],
new Rectangle((x * iTileWidth) + iMapDisplayOffsetX,
y * iTileHeight + iMapDisplayOffsetY, iTileWidth, iTileHeight),
Color.White);
}
}
spriteBatch.End();
base.Draw(gameTime);
}
这里是我们地图引擎的核心,所以我们将一行一行的解释。所有我们的绘制工作都是在BeginScene()和EndScene()函数的中间进行的。
spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
通知spriteBact对象,我们将要开始绘制精灵。使用”SpriteBlendMode.AlphaBlend”可以保证spriteBatch在绘制时将带有alpha通道(透明)。我们在地图最初的显示时并不是用alpha通道,等到我们讲到对象(也就是,玩家)在地图上的遮盖效果时就会有用处了。
// Draw the map
for (int y = 0; y < iMapDisplayHeight; y++)
{
for (int x = 0; x < iMapDisplayWidth; x++)
{
spriteBatch.Draw(t2dTiles[iMap[y + iMapY, x + iMapX]],
new Rectangle((x * iTileWidth) + iMapDisplayOffsetX,
y * iTileHeight + iMapDisplayOffsetY, iTileWidth, iTileHeight),
Color.White);
}
}
在这里我们实际上绘制了所有组成这个地图的精灵。我们设置了两个for循环,一个用于Y轴,一个用于X轴,iMapDisplayHeight和iMapDisplayWidth变量决定了总共要绘制的贴片的数目。
我们调用spriteBatch.Draw方法来实际绘制一个精灵到显示缓存中。SpriteBatch.Draw函数的第一个参数是我们要绘制的精灵。我们通过iMap数组找到要绘制的精灵是哪一个。我们将从这个数组中得到一个0到3之间的数字,这正好对应与我们储存在贴片集中的贴片。
第二个参数是一个也矩形对象,这个矩形对象用于表示我们要在显示缓存的哪里显示。我们使用了循环计数值,贴片的宽度,还有DisplayOffset变量来创建这个矩形。
最后,第三个参数为绘制精灵时的色调(tinting),我们并不是用色调,所以指定Color.White值就可以了。
spriteBatch.End();
在所有工作完成以后,我们结束这段批处理。
base.Draw(gameTime);
}
这里只是简单的调用基类的Draw方法。运行这个工程,你可以看到一个地图,这个地图将显示在窗口的左上角的(30,30)坐标处(单位是像素) ,地图的高和宽为6个贴片。
处理玩家的输入
现在我们可以绘制地图了,我们应该允许玩家按下方向键来移动。为了实现这个,我们需要修改Update方法来监测键盘的输入。
将Update方法替换为:
protected override void Update(GameTime gameTime)
{
// Allows the default game to exit on Xbox 360 and Windows
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
fTotalElapsedTime += elapsed;
ksKeyboardState = Keyboard.GetState();
if (ksKeyboardState.IsKeyDown(Keys.Escape))
{
this.Exit();
}
// See if enough time has elapsed since we last moved on the map.
if (fTotalElapsedTime >= fKeyPressCheckDelay)
{
if (ksKeyboardState.IsKeyDown(Keys.Up))
{
iMapY--;
fTotalElapsedTime = 0.0f ;
}
if (ksKeyboardState.IsKeyDown(Keys.Down))
{
iMapY++;
fTotalElapsedTime = 0.0f ;
}
if (ksKeyboardState.IsKeyDown(Keys.Left))
{
iMapX--;
fTotalElapsedTime = 0.0f ;
}
if (ksKeyboardState.IsKeyDown(Keys.Right))
{
iMapX++;
fTotalElapsedTime = 0.0f ;
}
if (iMapX < 0) { iMapX = 0; }
if (iMapX > iMapWidth - iMapDisplayWidth) { iMapX = iMapWidth - iMapDisplayWidth; }
if (iMapY < 0) { iMapY = 0; }
if (iMapY > iMapHeight - iMapDisplayHeight) { iMapY = iMapHeight - iMapDisplayHeight; }
}
base.Update(gameTime);
}
还是和以前一样,这里有很多代码,我们一步一步的讲解:
// Allows the default game to exit on Xbox 360 and Windows
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
这一部分是默认的模版自动生成的。如果玩家按下xbox 360控制器上的”Back”键,游戏就会退出。
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
fTotalElapsedTime += elapsed;
这里允许我们累计程序已经运行的时间,并将时间保存在一个名为elapsed的浮点变量中,我们可以使用这个变量来决定地图移动的速度。
ksKeyboardState = Keyboard.GetState();
这里,我们获得当前的键盘状态信息,并将这个信息保存在一个变量中,这个变量将用来检测玩家按下的键。
if (ksKeyboardState.IsKeyDown(Keys.Escape))
{
this.Exit();
}
为了方便,如果Escape被按下,那么就退出程序。很明显你不希望在一个真实的程序中这样做(因为玩家可能会不小心按倒)。你可以弹出一个退出确认对话框来避免这种状况。
// See if enough time has elapsed since we last moved on the map.
if (fTotalElapsedTime >= fKeyPressCheckDelay)
{
在我们检查玩家是否按下了移动键之前,我们需要首先检查一下是否已经满足了最小地图滚动间隔时间(minimum map scroll time)(我将默认设置为0.25秒)。如果我们没有加入这行代码的话,地图将会滚动的非常快。
if (ksKeyboardState.IsKeyDown(Keys.Up))
{
iMapY--;
fTotalElapsedTime = 0.0f ;
}
if (ksKeyboardState.IsKeyDown(Keys.Down))
{
iMapY++;
fTotalElapsedTime = 0.0f ;
}
if (ksKeyboardState.IsKeyDown(Keys.Left))
{
iMapX--;
fTotalElapsedTime = 0.0f ;
}
if (ksKeyboardState.IsKeyDown(Keys.Right))
{
iMapX++;
fTotalElapsedTime = 0.0f ;
}
如果任意一个方向键被按下了,那么就通过更新X/Y坐标来让”地图”向那个方向移动。
if (iMapX < 0) { iMapX = 0; }
if (iMapX > iMapWidth - iMapDisplayWidth) { iMapX = iMapWidth - iMapDisplayWidth; }
if (iMapY < 0) { iMapY = 0; }
if (iMapY > iMapHeight - iMapDisplayHeight) { iMapY = iMapHeight - iMapDisplayHeight; }
最后,我们检测是否X和Y的坐标已经超出了地图,如果是的话则将坐标维持在边界。
总结
到现在为止,你应该已经有了一个基于贴片的地图了。
然而,还有很多的事情我们还没有做,这个简单的系统还可以继续增强其功能。在接下来的指南里我们将可能实现以下的功能:
l 为玩家增加一个”avatar”。这应该是一个通过alpha混合绘制在地图中央的精灵动画。
l 通过记录独立的贴片的偏移来使地图移动的更平滑。当移动键被按下时调整少量的绘制位置,而不是以一个贴片为移动量。注意你可能希望调整fKeyPressCheckDelay来改变你移动速度慢了48倍的事实,
l 在地图的四周制作一个界面,也许会用角色的状态等等。
l 供选择的,设置iMapDisplayOffsetX和iMapDisplayOffSetY为0并且设置iMapDisplayWidth为14,iMapDisplayHeight为10,这样地图将成为一个640乘480的窗口。
当然,你也可以期望使用这个实际的系统来制作一个游戏。
译者czhou的联系方式:caoxiao.youxiang@gmail.com