Snail Run for Windows Phone






4.97/5 (23投票s)
了解如何在同一应用程序中集成 Silverlight 和 XNA for Windows Phone
目录
- 引言
- 系统要求
- Silverlight 和 XNA:成为朋友
- 重构 MainPage.xaml
- GamePage:让 Silverlight 和 XNA 工作起来
- 被鱿鱼追赶
- 使用 Mappy Win32 创建关卡地图
- Mappy 文件的 XNA 内容管道扩展
- 最终思考
- 历史
引言
到目前为止,使用 Windows Phone 进行游戏开发非常有趣。在这篇文章中,我将介绍“蜗牛快跑!”这款游戏,这是一款传统的迷宫、吃豆人类型的游戏。除了有趣之外,这里真正值得关注的重点是:在同一个解决方案中结合 Silverlight 和 XNA 开发,使用“Mappy”地图创建工具,在 Visual Studio 游戏项目中导入/处理地图,以及如何实现寻路算法,特别是“A* 搜索算法”,为游戏注入活力。
系统要求
要使用本文提供的 Windows Phone 版蜗牛快跑,您必须从 Microsoft 直接下载并安装以下 100% 免费的开发工具:
无论您是熟悉还是初次接触 Silverlight 和 XNA Game Studio 编程,Visual Studio 2010 Express for Windows Phone 都提供了您开始构建 Windows Phone 应用程序所需的一切。
Windows Phone 软件开发工具包 (SDK) 7.1 为您提供了开发 Windows Phone 7.0 和 Windows Phone 7.5 设备上的应用程序和游戏所需的所有工具。
Silverlight 和 XNA:成为朋友
Windows Phone 以为开发者提供两种不同的框架而闻名:Silverlight 和 XNA。虽然 Silverlight 适用于大多数应用程序需求,并且足够使用,因为它具有固有的页面导航、XAML 标记语言的灵活性、内置的转换、故事板和动画、高级数据绑定、矢量渲染、全景和枢轴应用程序,仅举几例,而另一方面,更新/绘图循环、视觉和音频效果以及 XNA Framework 的快速精灵渲染引擎通常是开发者在游戏开发方面的首选。
如果您已经单独使用 XNA Framework 开发了 Windows Phone 游戏,您可能已经遇到并怀念缺乏页面导航、XAML 和绑定以及 Silverlight 开发的其他好处。一些琐碎的应用程序需求,例如渲染简单的列表框,在 XNA 中可能需要大量工作。幸运的是,一旦您决定创建一个新的 Visual Studio 项目,选择一个名为 **Windows Phone Silverlight and XNA Application** 的项目模板,这种架构上的困境就结束了。
这个名为“MyGame”的项目解决方案模板创建了一个包含三个项目的新解决方案:
- MyGame,这是一个 Silverlight 项目,但也包含对 XNA Framework 的引用。这是 Silverlight 和 XNA 一起渲染的地方。
- MyGameLib,这是一个纯 XNA 项目。您可以使用此项目来重用现有的 XNA 代码或将 XNA 代码与 Silverlight 代码分开。
- MyGameLibContent:这是内容管道项目,您可以在其中找到解决方案的资产。
当您运行应用程序时,您会发现它看起来非常像一个普通的 Silverlight 应用程序。这是
唯一显着的例外是中央按钮,它显然会导航到游戏页面。
<!--Create a single button to navigate to the second page which is rendered with the XNA Framework-->
<Button Height="100" Content="Change to game page" Click="Button_Click" />
// Simple button Click event handler to take us to the second page
private void Button_Click(object sender, RoutedEventArgs e)
{
NavigationService.Navigate(new Uri("/GamePage.xaml", UriKind.Relative));
}
至于游戏页面:“GamePage.xaml”使其听起来也像一个常规的 Silverlight 页面,但这并非如此。
<phone:PhoneApplicationPage
x:Class="MyGame.GamePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="480"
shell:SystemTray.IsVisible="False">
<!--No XAML content is required as the page is rendered entirely with the XNA Framework-->
</phone:PhoneApplicationPage>
现在让我们看一下背后的代码类
首先,您会注意到该类中存在一些 XNA 元素:
public partial class GamePage : PhoneApplicationPage
{
ContentManager contentManager;
GameTimer timer;
SpriteBatch spriteBatch;
public GamePage()
{
InitializeComponent();
// Get the content manager from the application
contentManager = (Application.Current as App).Content;
// Create a timer for this page
timer = new GameTimer();
timer.UpdateInterval = TimeSpan.FromTicks(333333);
timer.Update += OnUpdate;
timer.Draw += OnDraw;
}
然后您有
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Set the sharing mode of the graphics device to turn on XNA rendering
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
// TODO: use this.content to load your game content here
// Start the timer
timer.Start();
base.OnNavigatedTo(e);
}
每当您导航离开
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
// Stop the timer
timer.Stop();
// Set the sharing mode of the graphics device to turn off XNA rendering
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(false);
base.OnNavigatedFrom(e);
}
如果您已经是 XNA 开发者,您会注意到
private void OnUpdate(object sender, GameTimerEventArgs e)
{
// TODO: Add your update logic here
}
与
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
}
从上面的代码可以看出,默认的 **Silverlight and XNA** 模板足以节省您一些时间和精力,但不幸的是,它留下了一些重要的部分没有解释,尽管代码中散布着注释。在文章的下一部分,我们将尝试处理这些空白。
重构 MainPage.xaml
第一次化妆是在
这里有趣的点在于泡沫动画的制作。本身就有一个实例
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ctr:SplashScreen x:Name="splashScreen" VerticalAlignment="Center" Grid.ColumnSpan="3" Grid.RowSpan="3"/>
<Image Source="MainPage.png" MouseLeftButtonUp="Image_MouseLeftButtonUp"></Image>
</Grid>
该
private void CreateBubbles()
{
var linearBubbleBrush = new LinearGradientBrush()
{ StartPoint = new Point(1, 0), EndPoint = new Point(0, 1) };
linearBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0xFF, 0x00, 0x20, 0x40), 0.0));
linearBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
var radialBubbleBrush = new RadialGradientBrush() {
Center = new Point(0.25, 0.75), RadiusX = .3, RadiusY = .2, GradientOrigin = new Point(0.35, 0.75) };
radialBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF), 0.0));
radialBubbleBrush.GradientStops.Add(
NewGradient.Stop(Color.FromArgb(0x00, 0xFF, 0xFF, 0xFF), 1.0));
for (var i = 0; i < 10; i++)
{
var diameter = 10 + (i % 4) * 10;
var ms = 2000 + i % 7 * 500;
var ellBubble = new Ellipse()
{
Width = diameter,
Height = diameter,
Stroke = linearBubbleBrush,
Fill = radialBubbleBrush,
StrokeThickness = 3
};
ellBubble.SetValue(Canvas.LeftProperty,
i * (40.0 + 40.0 - diameter / 2));
ellBubble.SetValue(Canvas.TopProperty, 0.0 + 40.0 - diameter / 2);
cnvBubbles.Children.Add(ellBubble);
var leftAnimation = new DoubleAnimation()
{
From = 40.0 * i,
To = 40.0 * i,
Duration = TimeSpan.FromMilliseconds(ms)
};
var topAnimation = new DoubleAnimation()
{
From = 400,
To = 0,
Duration = TimeSpan.FromMilliseconds(ms)
};
var opacityAnimation = new DoubleAnimation()
{
From = 1.0,
To = 0.0,
Duration = TimeSpan.FromMilliseconds(ms)
};
Storyboard.SetTarget(leftAnimation, ellBubble);
Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
Storyboard.SetTarget(topAnimation, ellBubble);
Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
Storyboard.SetTarget(opacityAnimation, ellBubble);
Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity"));
leftAnimation.EasingFunction = new BackEase()
{ Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
topAnimation.EasingFunction = new BackEase()
{ Amplitude = 0.5, EasingMode = EasingMode.EaseOut };
var sb = new Storyboard();
sb.Children.Add(leftAnimation);
sb.Children.Add(topAnimation);
sb.Children.Add(opacityAnimation);
sb.RepeatBehavior = RepeatBehavior.Forever;
bubbles.Add(ellBubble);
storyBoards.Add(sb);
sb.Begin();
}
}
GamePage:让 Silverlight 和 XNA 工作起来
首先,您必须向项目中添加内容。我们拥有的内容类型只有精灵和关卡地图。**精灵 (Sprite)** 是一个常见的概念,不需要进一步解释。我们有蜗牛、珍珠、鱿鱼和海星的图形内容。这些是我们游戏中的角色。**关卡地图 (Level maps)** 是由第三方开源工具 **MapWin** 创建的地图,然后被整合到我们的游戏内容中。
现在,让我们看一下位于 **GamePage.xaml.cs** 的代码背后的类。该类作用域内值得注意的元素是:
已知的
- 实例。
ContentManager ,GameTimer 和SpriteBatch Camera2d 实例。顾名思义,它用于在我们穿越游戏迷宫时进行滚动/缩放。 - UIElementRenderer 是最重要的部分:它将允许我们将 Silverlight 内容渲染到纹理中,从而我们可以将它们与 XNA 生成的图形放在一起。
- GameSettings 用于预定义的遊戲設定,例如速度、屏幕分辨率等。
- ScoreManager 管理,嗯……分数。
- Level 类包含关于游戏关卡的信息。
- 现在我们进入类构造函数。在这里,我们正在为首次使用做准备,因此许多事情都在构造函数中设置。例如,**contentManager** 首先被实例化,然后就可以使用了。**timer** 是 XNA Framework 的一个实例
.
.
.
public partial class GamePage : PhoneApplicationPage
{
ContentManager contentManager;
GameTimer timer;
SpriteBatch spriteBatch;
Camera2d camera;
// For rendering the XAML onto a texture
UIElementRenderer elementRenderer;
GameSettings settings = new GameSettings();
ScoreManager scoreManager = new ScoreManager();
GameStateMachine stateMachine = new GameStateMachine();
List<Level> levels = new List<Level>();
int CurrentLevelNumber = 1;
double minUpdateTimeSpanMs = 20;
double accumulatedUpdateTimeSpanMs = 0;
double minChaseTimeSpanMs = 500;
double accumulatedChaseTimeSpanMs = 0;
Texture2D seaTexture;
public GamePage()
.
.
.
类。这个类允许控制两个重要事件:**Update** 和 **Draw**。这里我们注意到事件是如何绑定到方法的。
public GamePage()
{
InitializeComponent();
// Get the content manager from the application
contentManager = (Application.Current as App).Content;
// Create a timer for this page
timer = new GameTimer();
timer.UpdateInterval = TimeSpan.FromTicks(166667);
timer.Update += OnUpdate;
timer.Draw += OnDraw;
camera = new Camera2d(2, 0, settings.ScreenHeight, settings.ScreenWidth, settings.CameraCenter);
// Use the LayoutUpdate event to know when the page layout
// has completed so that we can create the UIElementRenderer.
LayoutUpdated += new EventHandler(GamePage_LayoutUpdated);
TouchPanel.EnabledGestures = GestureType.Flick | GestureType.Hold | GestureType.Tap;
this.DataContext = scoreManager;
LoadLevels();
}
事件函数完成的。
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Set the sharing mode of the graphics device to turn on XNA rendering
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
// Start the timer
timer.Start();
foreach (var level in levels)
{
level.Initialize();
}
seaTexture = contentManager.Load<Texture2D>(string.Format("{0}/Sea", settings.SpritesRoot));
base.OnNavigatedTo(e);
scoreManager.Level = CurrentLevelNumber;
stateMachine.GameStateChanged += new GameStateMachine.GameStateChangedHandler(stateMachine_GameStateChanged);
stateMachine.ChangeState(GameState.PreparingToStartLevel);
}
该
private void OnUpdate(object sender, GameTimerEventArgs e)
{
UpdateLevel(e);
ChaseSnail(e);
HandleInput();
UpdateCamera();
}
private void UpdateLevel(GameTimerEventArgs e)
{
accumulatedUpdateTimeSpanMs += e.ElapsedTime.TotalMilliseconds;
if (accumulatedUpdateTimeSpanMs > minUpdateTimeSpanMs)
{
accumulatedUpdateTimeSpanMs = 0;
CurrentLevel.Update(e.ElapsedTime);
}
}
ChaseSnail
private void ChaseSnail(GameTimerEventArgs e)
{
if (stateMachine.CurrentSate == GameState.Playing)
{
accumulatedChaseTimeSpanMs += e.ElapsedTime.TotalMilliseconds;
if (accumulatedChaseTimeSpanMs > minChaseTimeSpanMs)
{
accumulatedChaseTimeSpanMs = 0;
CurrentLevel.Squids.ForEach(x =>
{
if (x.ChaseMovementQueue.Count == 0)
{
x.ChaseSnail();
}
});
}
}
}
最后是摄像机更新。通常,摄像机会尝试跟随鱿鱼的移动。但是,当鱿鱼到达迷宫的边缘时,摄像机就会停止移动。因此,摄像机移动的区域是有限的。
private void HandleInput()
{
//Check if touch Gesture is available
if (TouchPanel.IsGestureAvailable)
{
// Read the gesture so that you can handle the gesture type
GestureSample gesture = TouchPanel.ReadGesture();
switch (gesture.GestureType)
{
case GestureType.Flick:
var x = gesture.Delta.X;
var y = gesture.Delta.Y;
CurrentLevel.Flick(x, y);
break;
case GestureType.Tap:
CurrentLevel.Snail.EnqueueMove(Gesture.Tap);
break;
default:
//do something
break;
}
}
}
然后我们有了
private void UpdateCamera()
{
var newCameraPos = CurrentLevel.Snail.Position * settings.TileWidth;
if (newCameraPos.X < camera.ViewportWidth / 4)
{
newCameraPos.X = camera.ViewportWidth / 4;
}
else if (newCameraPos.X > CurrentLevel.MapWidth * settings.TileWidth -
camera.ViewportWidth / (camera.Zoom * 2) + settings.MapTopLeftCorner.X)
{
newCameraPos.X = CurrentLevel.MapWidth * settings.TileWidth -
camera.ViewportWidth / (camera.Zoom * 2) + settings.MapTopLeftCorner.X;
}
if (newCameraPos.Y < camera.ViewportHeight / 4)
{
newCameraPos.Y = camera.ViewportHeight / 4;
}
else if (newCameraPos.Y > CurrentLevel.MapHeight * settings.TileWidth -
camera.ViewportHeight / (camera.Zoom * 2) + settings.MapTopLeftCorner.Y)
{
newCameraPos.Y = CurrentLevel.MapHeight * settings.TileWidth -
camera.ViewportHeight / (camera.Zoom * 2) + settings.MapTopLeftCorner.Y;
}
camera.Pos = newCameraPos;
}
方法。根据游戏状态,它需要不同地绘制。
private void OnDraw(object sender, GameTimerEventArgs e)
{
switch (stateMachine.CurrentSate)
{
case GameState.PreparingToStartLevel:
case GameState.GameOver:
case GameState.LevelCompleted:
case GameState.LevelFailed:
case GameState.Paused:
DrawCentralMessage(e);
break;
case GameState.Playing:
DrawPlaying(e);
break;
}
}
状态时,它必须显示消息。这些消息在 Silverlight XAML 中定义,而在这里是渲染 Silverlight 内容与 XNA 一起的方式:**elementRenderer.Render();** 渲染 Silverlight 页面(即构建所有视觉元素),然后 **spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);** 告诉应用程序在游戏的精灵批次中渲染 Silverlight 内容。
private void DrawCentralMessage(GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.Black);
// Render the Silverlight controls using the UIElementRenderer.
elementRenderer.Render();
this.spriteBatch.Begin();
// Using the texture from the UIElementRenderer,
// draw the Silverlight controls to the screen.
spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);
this.spriteBatch.End();
}
该
private void DrawPlaying(GameTimerEventArgs e)
{
DrawBackground();
DrawMaze(e);
DrawAnimals(e);
DrawSilverlight();
}
然后我们在上面绘制迷宫。
private void DrawBackground()
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.Black);
this.spriteBatch.Begin();
// Using the texture from the UIElementRenderer,
// draw the Silverlight controls to the screen.
spriteBatch.Draw(seaTexture, Vector2.Zero,
new xna.Rectangle(0, 0, settings.ScreenWidth, settings.ScreenHeight),
Color.White);
this.spriteBatch.End();
}
然后渲染动物。
private void DrawMaze(GameTimerEventArgs e)
{
this.spriteBatch.Begin(SpriteSortMode.BackToFront,
BlendState.AlphaBlend, null, null, null, null,
camera.Transformation(spriteBatch.GraphicsDevice));
this.spriteBatch.Draw((float)e.ElapsedTime.TotalSeconds,
CurrentLevel.Map2D, settings.MapTopLeftCorner, Color.White);
this.spriteBatch.End();
}
最后,我们在所有内容之上绘制 Silverlight 纹理。这个 Silverlight 部分只是游戏进行期间出现在屏幕顶部的分数信息。
private void DrawAnimals(GameTimerEventArgs e)
{
this.spriteBatch.Begin(SpriteSortMode.BackToFront,
BlendState.AlphaBlend, null, null, null, null,
camera.Transformation(spriteBatch.GraphicsDevice));
CurrentLevel.Snail.Draw(e.ElapsedTime, spriteBatch,
settings.MapTopLeftCorner, 0);
foreach (var pearl in CurrentLevel.Pearls)
{
pearl.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
}
foreach (var starFish in CurrentLevel.StarFishes)
{
starFish.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
}
foreach (var squid in CurrentLevel.Squids)
{
squid.Draw(e.ElapsedTime, spriteBatch, settings.MapTopLeftCorner, 1);
}
this.spriteBatch.End();
}
您可以在 MSDN 的文章 "How to: Combine Silverlight and the XNA Framework in a Windows Phone Application" 中阅读有用的进一步信息。
private void DrawSilverlight()
{
// Render the Silverlight controls using the UIElementRenderer.
elementRenderer.Render();
this.spriteBatch.Begin();
// Using the texture from the UIElementRenderer,
// draw the Silverlight controls to the screen.
spriteBatch.Draw(elementRenderer.Texture, Vector2.Zero, Color.White);
this.spriteBatch.End();
}
这款游戏使用了经典的寻路算法,称为“A*”(发音为“A Star”)。根据维基百科:
被鱿鱼追赶
当 A* 遍历图时,它会沿着已知成本最低的路径前进,并一路维护一个按顺序排列的备用路径段的优先级队列。如果在任何点,正在遍历的路径段的成本高于遇到的另一个路径段,它就会放弃成本较高的路径段,转而遍历成本较低的路径段。这个过程会一直持续到到达目标。
A* 搜索用于从起始节点找到机器人运动规划问题中的目标节点的路径的说明。空心圆代表开放集中的节点,即尚未探索的节点,实心圆代表闭集中的节点。闭合节点上的颜色指示与起点的距离:越绿,越远。首先可以看到 A* 在直线方向上朝着目标移动,然后在遇到障碍物时,它会探索开放集中的节点。

(来源:维基百科)
上述算法是我们问题的完美解决方案。幸运的是,我从 Code Project 借鉴了 Sacha Barber 的一个精彩贡献(涉及查找伦敦地铁任何两个站点之间的最佳路径),并制作了自己的版本。我只需要改变概念:Sacha 的文章涉及一个人试图在当前站点和目标站点之间找到最佳路径,同时尊重这些站点之间的地理连接。另一方面,在 Snail Quest 中,人由鱿鱼代表。目标站点是蜗牛所在的单元格,站点是迷宫内的空单元格,并且代替站点之间的连接,我们现在有了空单元格之间的连接,这些就是迷宫中的“走廊”。
public List<MovementType> DoSearch(Vector2 squidPosition, Vector2 snailPosition)
{
pathsSolutionsFound = new List<List<Vector2>>();
pathsAgenda = new List<List<Vector2>>();
List<Vector2> pathStart = new List<Vector2>();
pathStart.Add(squidPosition);
pathsAgenda.Add(pathStart);
while (pathsAgenda.Count() > 0 && pathsAgenda.Count() < 100)
{
List<Vector2> currPath = pathsAgenda[0];
pathsAgenda.RemoveAt(0);
if (currPath.Count(
x => x.Equals(snailPosition)) > 0)
{
pathsSolutionsFound.Add(currPath);
break;
}
else
{
Vector2 currPosition = currPath.Last();
List<Vector2> successorPositions =
GetSuccessorsForPosition(currPosition);
foreach (var successorPosition in successorPositions)
{
if (!currPath.Contains(successorPosition) &&
pathsSolutionsFound.Count(x => x.Contains(successorPosition)) == 0)
{
List<Vector2> newPath = new List<Vector2>();
foreach (var station in currPath)
newPath.Add(station);
newPath.Add(successorPosition);
pathsAgenda.Add(newPath);
}
}
}
}
//Finally, get the best Path, this should be the 1st one found due
//to the heuristic evaluation performed by the search
if (pathsSolutionsFound.Count() > 0)
{
var solutionPath = pathsSolutionsFound[0];
var movementList = new List<MovementType>();
var Vector2 = solutionPath[0];
for (var i = 1; i < solutionPath.Count(); i++)
{
var movement = MovementType.None;
if (solutionPath[i].X > Vector2.X)
movement = MovementType.Right;
if (solutionPath[i].X < Vector2.X)
movement = MovementType.Left;
if (solutionPath[i].Y > Vector2.Y)
movement = MovementType.Bottom;
if (solutionPath[i].Y < Vector2.Y)
movement = MovementType.Top;
movementList.Add(movement);
Vector2 = solutionPath[i];
}
return movementList;
}
return null;
}
使用 Mappy Win32 创建关卡地图
正如我在开发这款游戏初期所发现的,创建具有重复块的场景是一项非常无聊的任务。幸运的是,有一个非常有趣的地图工具供游戏开发者使用:Mappy Win32。它允许使用一小组常用图形块开发大型游戏场景。所有老派游戏(想想马里奥兄弟、吃豆人、索尼克等)都是使用有限数量的块(或通常称为“图块”)制作的。
您可以在此网站免费下载 Mappy 工具。
让我们看看如何使用 Mappy 创建关卡场景:首先,打开文件菜单,然后选择 **新建地图 (New Map)**。
然后我们将地图配置为使用 25 x 15 个图块,每个图块 32 x 32 像素。
下一个重要步骤是导入 **图块条 (Tile Strip)**,这是一组 32 x 32 的块,包含我们地图使用的有限图块。
最后,我们使用之前导入的块自由绘制我们的地图。
最后一步是保存文件。我们将其命名为 **Level1.FMP** 并保存在 **\SnailRun\SnailRun\SnailRunLibContent\Maps\** 文件夹中。请注意,该文件现在是我们游戏内容的一部分。
但是等等,这个 .FMP 扩展名是 Mappy 应用程序专有的。XNA 如何导入/处理这种文件?答案是,**它不能**。它根本不知道 .FMP 扩展名。这就是为什么我们必须自己动手。幸运的是,又有一些聪明人已经为我们完成了艰巨的工作。
Mappy 文件的 XNA 内容管道扩展
尽管 XNA Framework 可以处理各种文件,但不幸的是,它可以读取和处理的文件数量有限。这就是为什么,如果涉及到一种新类型的文件,我们就需要自己实现一个内容管道扩展。幸运的是,我在 Codeplex 上找到了 XNA 内容管道扩展到 Mappy 地图 (.FMP),由巴西游戏开发者 **Luciano José** 为 Windows 和 XBox 平台开发。
“**项目描述** 这个 XNA 库到 Mappy 地图有助于 XNA 开发者将 Mappy 工具中制作的 Tile Map 与您的 XNA 项目(Windows 和 Xbox 360)集成。
该库允许您只需将您的存档 (.FMP) 拖放到 XNA 项目(Windows 和 Xbox 360)的内容项目中。
使用 Mappy 工具(http://www.tilemap.co.uk/mappy.php)创建您的图块地图,以便与此 XNA 库集成。
在 SharpGames(巴西 XNA 社区)上查看我关于该库的文章(葡萄牙语)。
http://www.sharpgames.net/Artigos/Artigo/tabid/58/selectmoduleid/376/ArticleID/1585/reftab/54/Default.aspx
访问巴西 XNA 社区:http://www.sharpgames.net/
它的工作原理是读取 .FMP 文件并读取其中的底层结构/对象。这样我们就可以有效地“读取”地图,并利用数据来寻找路线、测试与障碍物的碰撞等。
不幸的是,正如我所说,**XNA 内容管道扩展 for Mappy** 仅针对 Windows 和 XBox 平台。但是经过一些时间,我设法将其移植到 Windows Phone 平台并使其正常工作。然后我以发布模式对其进行了编译,并将其作为引用包含在项目中。
在将 **Level1.FMP** 添加到 Content 项目后,您仍然需要配置 **内容处理器属性**。
最终,一切都工作正常,每个人都高兴。
最终思考
就是这样!我希望您喜欢这款游戏和这里介绍的概念。如果您有任何问题、投诉或想法,请在下方留言。
历史
- 2012-02-29:初始版本。