使用 MonoGame 开发游戏





5.00/5 (4投票s)
MonoGame 是 XNA 应用程序编程接口 (API) 的开源实现。
Intel® Developer Zone 提供用于跨平台应用程序开发、平台和技术信息、代码示例以及同行专业知识的工具和操作指南,以帮助开发人员创新并取得成功。加入我们的 物联网、Android、Intel® RealSense™ 技术 和 Windows 社区,下载工具、访问开发套件、与志同道合的开发人员分享想法,并参与黑客马拉松、比赛、路演和本地活动。
相关文章
面向第四代英特尔®酷睿™处理器的英特尔®处理器显卡开发人员指南
触摸和传感器
如何创建可用的触摸UI
如何调整触摸控件
引言
全世界的开发人员都想开发游戏。为什么不呢?游戏是计算机史上最畅销的产品之一,游戏业务中涉及的巨额财富不断吸引着开发者投身其中。作为一名开发者,我肯定希望成为开发下一款《愤怒的小鸟》* 或《光环》* 的人。
然而,在实践中,游戏开发是软件开发中最困难的领域之一。你必须记住那些你认为永远用不到的三角学、几何学和物理学课程。除此之外,你的游戏必须将声音、视频和故事情节以一种使用户想要一玩再玩的方式结合起来。而这一切都发生在你写下第一行代码之前!
为了使事情变得更容易,已经有一些框架可用于使用 C 和 C++,甚至 C# 或 JavaScript*(是的,你可以使用 HTML5 和 JavaScript 为你的浏览器开发三维游戏)。
这些框架之一是 Microsoft XNA*,它建立在 Microsoft DirectX* 技术之上,允许你为 Xbox 360*、Windows* 和 Windows Phone* 创建游戏。Microsoft 已经逐步淘汰了 XNA,但与此同时,开源社区推出了一款新产品:MonoGame*。
什么是 MonoGame?
MonoGame 是 XNA 应用程序编程接口 (API) 的开源实现。它不仅实现了 Windows 上的 XNA API,还实现了 Mac* OS X*、Apple iOS*、Google Android*、Linux* 和 Windows Phone 上的 XNA API。这意味着你只需进行一些小的更改,就可以为所有这些平台开发游戏。这是一个很棒的功能:你可以使用 C# 创建游戏,并轻松地将其移植到所有主要的桌面、平板电脑和智能手机平台。对于那些想要用他们的游戏征服世界的人来说,这是一个很好的起点。
在 Windows 上安装 MonoGame
你甚至不需要 Windows 就可以使用 MonoGame 进行开发。你可以使用 MonoDevelop*(一款开源的、跨平台的、适用于 Microsoft .NET 语言的集成开发环境 [IDE])或 Xamarin Studio*(一款由 Xamarin 开发的跨平台 IDE)。使用这些 IDE,你可以在 Linux 或 Mac 上使用 C# 进行开发。
如果你是一名 Microsoft .NET 开发者,并且像我一样每天都使用 Microsoft Visual Studio*,那么你可以将 MonoGame 安装到 Visual Studio 中,并用它来创建你的游戏。在撰写本文时,MonoGame 的最新稳定版本是 3.2 版。此版本在 Visual Studio 2012 和 2013 中运行,并允许你创建 DirectX 桌面游戏,如果你想在游戏中支持触摸,这将是必需的。
MonoGame 的安装在 Visual Studio 中带来了许多新模板供你选择创建游戏,如图 1 所示。
现在,要创建你的第一个游戏,请单击 **MonoGame Windows Project**,然后选择一个名称。Visual Studio 会创建一个包含所有必要文件和引用的新项目。如果你运行此项目,你会看到如图 2 所示的内容。
无聊,不是吗?只是一块蓝屏,但这却是你构建的任何游戏的起点。按 Esc 键,窗口将关闭。
你现在就可以使用已有的项目编写你的游戏了,但有一个问题。在将任何资源(图像、精灵、声音或字体)编译成 MonoGame 兼容的格式之前,你无法添加它们。为此,你需要以下选项之一:
- 安装 XNA Game Studio 4.0
- 安装 Windows Phone 8 软件开发工具包 (SDK)
- 使用外部程序,如 XNA 内容编译器
XNA Game Studio
XNA Game Studio 拥有创建 Windows 和 Xbox 360 的 XNA 游戏所需的一切。它还有一个内容编译器,可以将资源编译成 .xnb 文件,然后为你的 MonoGame 项目编译所有文件。目前,你只能在 Visual Studio 2010 中安装编译器。如果你不想仅仅为此目的安装 Visual Studio 2010,你可以在 Visual Studio 2012 中安装 XNA Game Studio(参见本文“更多信息”部分的链接)。
Windows Phone 8 SDK
你无法直接在 Visual Studio 2012 中安装 XNA Game Studio,但 Windows Phone 8 SDK 可以很好地安装在 Visual Studio 2012 中。你可以使用它来创建一个项目来编译你的资源。
XNA 内容编译器
如果你不想安装 SDK 来编译你的资源,可以使用 XNA 内容编译器(参见“更多信息”部分的链接),这是一个开源程序,可以将你的资源编译成 .xnb 文件,这些文件可以在 MonoGame 中使用。
创建你的第一个游戏
前面使用 MonoGame 模板创建的游戏是所有游戏的基础。你将使用相同的过程来创建所有游戏。在 Program.cs 中,你有 Main 函数。该函数初始化并运行游戏。
static void Main() { using (var game = new Game1()) game.Run(); }
Game1.cs
是游戏的核心。在那里,有两个方法以每秒 60 次的频率循环调用:Update 和 Draw。在 Update 中,你重新计算游戏中所有元素的**数据**;在 Draw 中,你绘制这些元素。请注意,这是一个紧密的循环。你只有 1/60 秒,即 16.7 毫秒的时间来计算和绘制**数据**。如果你花费的时间超过这个时间,程序可能会跳过一些 Draw 周期,你将在游戏中看到图形**故障**。
直到最近,桌面计算机游戏的输入设备一直是键盘和鼠标。除非用户购买了额外的硬件,例如赛车方向盘或操纵杆,否则你无法假定存在其他输入方法。随着现在提供的新硬件,如 Ultrabook™ 设备、Ultrabook 2 合 1 和一体机 PC,你的选择发生了变化。你可以使用触摸输入和传感器,为用户提供更具沉浸感和逼真的游戏体验。
对于这款第一个游戏,我们将创建一个点球大战足球游戏。用户将使用触摸“踢”球,电脑守门员将尝试接住球。球的方向和速度将由用户的滑动决定。电脑守门员将选择一个随机的边和速度来接球。每进一球得一分。否则,守门员得分。
向游戏中添加内容
游戏的第一步是添加内容。首先添加背景场地和球。为此,创建两个 .png 文件:一个用于足球场地(图 3),另一个用于球(图 4)。
要使用这些文件,你必须编译它们。如果你使用的是 XNA Game Studio 或 Windows Phone 8 SDK,你必须创建一个 XNA 内容项目。该项目不必在同一个解决方案中。你只用它来编译资源。将图像添加到此项目,然后生成它。然后,转到项目目标目录,并将生成的 .xnb 文件复制到你的项目中。
我更喜欢使用 XNA 内容编译器,它不需要新项目,并且允许你在需要时编译资源。只需打开程序,将文件添加到列表中,选择输出目录,然后单击编译。.xnb 文件就可以添加到项目中了。
内容文件
当 .xnb 文件可用时,将它们添加到游戏的 Content 文件夹中。你必须将每个文件的生成操作设置为 **Content**,并将 **复制到输出目录** 设置为 **如果较新则复制**。如果这样做,尝试加载资源时会收到错误。
创建两个字段来存储球和场地的纹理
private Texture2D _backgroundTexture; private Texture2D _ballTexture;
这些纹理在 LoadContent 方法中加载
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. _spriteBatch = new SpriteBatch(GraphicsDevice); // TODO: use this.Content to load your game content here _backgroundTexture = Content.Load<Texture2D>("SoccerField"); _ballTexture = Content.Load<Texture2D>("SoccerBall"); }
请注意,纹理的名称与 Content 文件夹中的文件名相同,但没有扩展名。
接下来,在 Draw
方法中绘制纹理
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Green); // Set the position for the background var screenWidth = Window.ClientBounds.Width; var screenHeight = Window.ClientBounds.Height; var rectangle = new Rectangle(0, 0, screenWidth, screenHeight); // Begin a sprite batch _spriteBatch.Begin(); // Draw the background _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White); // Draw the ball var initialBallPositionX = screenWidth / 2; var ínitialBallPositionY = (int)(screenHeight * 0.8); var ballDimension = (screenWidth > screenHeight) ? (int)(screenWidth * 0.02) : (int)(screenHeight * 0.035); var ballRectangle = new Rectangle(initialBallPositionX, ínitialBallPositionY, ballDimension, ballDimension); _spriteBatch.Draw(_ballTexture, ballRectangle, Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
此方法以绿色清除屏幕,然后绘制背景和球在点球点上的位置。第一个 spriteBatch Draw
方法将背景绘制成窗口大小的尺寸 - 位置 0,0;第二个方法在点球点绘制球。它被缩放到与窗口大小成比例。这里没有移动,因为位置没有改变。下一步是移动球。
移动球
要移动球,你必须在循环的每次迭代中重新计算其位置,并在新位置绘制它。在 Update
方法中执行新位置的计算
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here _ballPosition -= 3; _ballRectangle.Y = _ballPosition; base.Update(gameTime); }
球的位置在每次循环中通过减去三个像素来更新。如果你想让球移动得更快,你必须减去更多的像素。变量 _screenWidth
、_screenHeight
、_backgroundRectangle
、_ballRectangle
和 _ballPosition
是私有字段,在 ResetWindowSize
方法中初始化
private void ResetWindowSize() { _screenWidth = Window.ClientBounds.Width; _screenHeight = Window.ClientBounds.Height; _backgroundRectangle = new Rectangle(0, 0, _screenWidth, _screenHeight); _initialBallPosition = new Vector2(_screenWidth / 2.0f, _screenHeight * 0.8f); var ballDimension = (_screenWidth > _screenHeight) ? (int)(_screenWidth * 0.02) : (int)(_screenHeight * 0.035); _ballPosition = (int)_initialBallPosition.Y; _ballRectangle = new Rectangle((int)_initialBallPosition.X, (int)_initialBallPosition.Y, ballDimension, ballDimension); }
此方法重置所有依赖于窗口大小的变量。它在 Initialize
方法中被调用
protected override void Initialize() { // TODO: Add your initialization logic here ResetWindowSize(); Window.ClientSizeChanged += (s, e) => ResetWindowSize(); base.Initialize(); }
此方法在两个不同的地方被调用:在过程的开始和每次窗口大小改变时。Initialize
处理 ClientSizeChanged
,因此当窗口大小改变时,依赖于窗口大小的变量会被重新评估,并且球会被重新定位到点球位置。
如果你运行程序,你会看到球沿着直线移动,但当它到达场地尽头时不会停止。你可以在球到达球门时使用以下代码重新定位球
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here _ballPosition -= 3; if (_ballPosition < _goalLinePosition) _ballPosition = (int)_initialBallPosition.Y; _ballRectangle.Y = _ballPosition; base.Update(gameTime); }
_goalLinePosition
变量是另一个字段,在 ResetWindowSize
方法中初始化
_goalLinePosition = _screenHeight * 0.05;
你必须在 Draw
方法中做出另一个更改:删除所有计算代码。
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Green); var rectangle = new Rectangle(0, 0, _screenWidth, _screenHeight); // Begin a sprite batch _spriteBatch.Begin(); // Draw the background _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White); // Draw the ball _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
移动是垂直于球门的。如果你想让球以角度移动,创建一个 _ballPositionX
字段并递增它(向右移动)或递减它(向左移动)。更好的方法是使用 Vector2
作为球的位置,如下所示
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here _ballPosition.X -= 0.5f; _ballPosition.Y -= 3; if (_ballPosition.Y < _goalLinePosition) _ballPosition = new Vector2(_initialBallPosition.X,_initialBallPosition.Y); _ballRectangle.X = (int)_ballPosition.X; _ballRectangle.Y = (int)_ballPosition.Y; base.Update(gameTime); }
如果你运行程序,它将显示球以角度移动(图 5)。下一步是让球在用户滑动时移动。
触摸和手势
在此游戏中,球的**运动**必须从触摸滑动开始。这种滑动决定了球的方向和速度。
在 MonoGame 中,你可以使用 TouchScreen
类获取触摸输入。你可以使用原始输入数据或手势 API。原始输入数据更灵活,因为你可以按自己的方式处理所有输入数据,而手势 API 会将这些原始数据转换为过滤后的手势,这样你只会收到你想要的手势的输入。
虽然手势 API 更易于使用,但在某些情况下它无法使用。例如,如果你想检测特殊手势,如 X 形或多指手势,你将需要使用原始数据。
对于此游戏,我们只需要滑动,并且手势 API 支持这一点,因此我们将使用它。第一件事是使用 TouchPanel class
指示你想要的手势。例如,代码
TouchPanel.EnabledGestures = GestureType.Flick | GestureType.FreeDrag;
. . . 使 MonoGame 只检测并通知你滑动和拖动。然后,在 Update
方法中,你可以按如下方式处理手势
if (TouchPanel.IsGestureAvailable) { // Read the next gesture GestureSample gesture = TouchPanel.ReadGesture(); if (gesture.GestureType == GestureType.Flick) { … } }
首先,确定是否有任何手势可用。如果有,你可以调用 ReadGesture
来获取并处理它。
通过触摸启动**运动**
首先,在游戏中使用 Initialize 方法启用滑动**手势**
protected override void Initialize() { // TODO: Add your initialization logic here ResetWindowSize(); Window.ClientSizeChanged += (s, e) => ResetWindowSize(); TouchPanel.EnabledGestures = GestureType.Flick; base.Initialize(); }
到目前为止,球在游戏运行时一直**移动**。使用一个私有字段 _isBallMoving
来告诉游戏球何时在**移动**。在 Update 方法中,当程序检测到滑动时,你将 _isBallMoving
设置为 True,**运动**就**开始**了。当球到达球门线时,将 _isBallMoving
设置为 False 并重置球的位置
protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) Exit(); // TODO: Add your update logic here if (!_isBallMoving && TouchPanel.IsGestureAvailable) { // Read the next gesture GestureSample gesture = TouchPanel.ReadGesture(); if (gesture.GestureType == GestureType.Flick) { _isBallMoving = true; _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f; } } if (_isBallMoving) { _ballPosition += _ballVelocity; // reached goal line if (_ballPosition.Y < _goalLinePosition) { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _isBallMoving = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); } _ballRectangle.X = (int) _ballPosition.X; _ballRectangle.Y = (int) _ballPosition.Y; } base.Update(gameTime); }
球的增量不再是固定的:程序使用 _ballVelocity
字段来设置球在 x 和 y 方向上的速度。Gesture.Delta
返回自上次更新以来的**运动**变化。要计算滑动的速度,将此向量乘以 TargetElapsedTime
属性。
如果球在**运动**,_ballPosition
向量会**增加**(以像素/帧为单位),直到球到达球门线。以下代码
_isBallMoving = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture();
. . . 做了两件事:它停止了球并清除了输入队列中的所有**手势**。如果不这样做,用户可以在球**移动**时滑动,使其在球停止后再次**移动**。
当你运行游戏时,你可以滑动球,它会朝着你滑动的方向以滑动速度**移动**。然而,这里有一个**问题**。代码没有检测到滑动发生在何处。你可以在屏幕上的任何位置滑动(不只是在球上),球都会**开始****移动**。你可以使用 gesture.Position
来检测滑动的位置,但该属性始终返回 0,0,所以你不应该使用它。
解决方案是使用原始输入,获取触摸点,然后查看它是否靠近球。以下代码确定触摸输入是否击中了球。如果击中,则**手势**设置 _isBallHit
字段
TouchCollection touches = TouchPanel.GetState();
TouchCollection touches = TouchPanel.GetState(); if (touches.Count > 0 && touches[0].State == TouchLocationState.Pressed) { var touchPoint = new Point((int)touches[0].Position.X, (int)touches[0].Position.Y); var hitRectangle = new Rectangle((int)_ballPositionX, (int)_ballPositionY, _ballTexture.Width, _ballTexture.Height); hitRectangle.Inflate(20,20); _isBallHit = hitRectangle.Contains(touchPoint); }
然后,只有当 _isBallHit
字段为 True 时,**运动**才会**开始**
if (TouchPanel.IsGestureAvailable && _isBallHit)
如果你运行游戏,只有当滑动从球开始时,你才能**移动**球。这里仍然有一个**问题**:如果你滑动得太慢,或者在任何不会击中球门的**方向**上,游戏就会**结束**,因为球永远不会回到**起始**位置。你必须为球的**运动**设置一个**超时**。当达到**超时**时,游戏会重新定位球。
Update 方法有一个参数:gameTime
。当你**开始****运动**时,如果你存储 gameTime
值,你就可以知道球实际**运动**的时间,并在**超时**后重置游戏。
if (gesture.GestureType == GestureType.Flick) { _isBallMoving = true; _isBallHit = false; _startMovement = gameTime.TotalGameTime; _ballVelocity = gesture.Delta*(float) TargetElapsedTime.TotalSeconds/5.0f; } ... var timeInMovement = (gameTime.TotalGameTime - _startMovement).TotalSeconds; // reached goal line or timeout if (_ballPosition.Y <' _goalLinePosition || timeInMovement > 5.0) { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _isBallMoving = false; _isBallHit = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); }
添加守门员
游戏现在可以运行了,但它需要一个难度元素:你必须添加一个守门员,在用户踢球后,他会继续**移动**。守门员是一个 .png 文件,由 XNA 内容编译器编译(图 6)。你必须将此编译后的文件添加到 Content 文件夹,将其生成操作设置为 Content,并将复制到输出目录设置为复制(如果较新)。
守门员在 LoadContent
方法中加载
protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. _spriteBatch = new SpriteBatch(GraphicsDevice); // TODO: use this.Content to load your game content here _backgroundTexture = Content.Load<Texture2D>("SoccerField"); _ballTexture = Content.Load<Texture2D>("SoccerBall"); _goalkeeperTexture = Content.Load<Texture2D>("Goalkeeper"); }
然后,你必须在 Draw
方法中绘制它
protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.Green); // Begin a sprite batch _spriteBatch.Begin(); // Draw the background _spriteBatch.Draw(_backgroundTexture, _backgroundRectangle, Color.White); // Draw the ball _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White); // Draw the goalkeeper _spriteBatch.Draw(_goalkeeperTexture, _goalkeeperRectangle, Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
_goalkeeperRectangle
拥有守门员在窗口中的矩形。它在 Update 方法中被更改
protected override void Update(GameTime gameTime) { … _ballRectangle.X = (int) _ballPosition.X; _ballRectangle.Y = (int) _ballPosition.Y; _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY, _goalKeeperWidth, _goalKeeperHeight); base.Update(gameTime); }
_goalkeeperPositionY
、_goalKeeperWidth
和 _goalKeeperHeight
字段在 ResetWindowSize
方法中更新
private void ResetWindowSize() { … _goalkeeperPositionY = (int) (_screenHeight*0.12); _goalKeeperWidth = (int)(_screenWidth * 0.05); _goalKeeperHeight = (int)(_screenWidth * 0.005); }
初始守门员位置在屏幕中央,靠近球门线顶部
_goalkeeperPositionX = (_screenWidth - _goalKeeperWidth)/2;
守门员将在球开始**运动**时开始**移动**。它将以谐波**运动**从一侧**移动**到另一侧。这个正弦曲线描述了它的**运动**
X = A * sin(at + δ)
其中 *A* 是**运动**幅度(球门宽度),*t* 是**运动**时间,*a* 和 *δ* 是随机**系数**(这将使**运动**在某种程度上是随机的,因此用户无法预测守门员的速度和**方向**)。
当用户通过滑动踢球时,**系数**将被计算出来
if (gesture.GestureType == GestureType.Flick) { _isBallMoving = true; _isBallHit = false; _startMovement = gameTime.TotalGameTime; _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f; var rnd = new Random(); _aCoef = rnd.NextDouble() * 0.005; _deltaCoef = rnd.NextDouble() * Math.PI / 2; }
*a* **系数**是守门员的速度,一个介于 0 和 0.005 之间的数字,表示速度在 0 到 0.3 像素/秒之间(在 1/60 秒内最大为 0.005 像素)。Delta **系数**是一个介于 0 和 pi/2 之间的数字。当球在**运动**时,你将更新守门员的位置
if (_isBallMoving) { _ballPositionX += _ballVelocity.X; _ballPositionY += _ballVelocity.Y; _goalkeeperPositionX = (int)((_screenWidth * 0.11) * Math.Sin(_aCoef * gameTime.TotalGameTime.TotalMilliseconds + _deltaCoef) + (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11); … }
**运动**的幅度是 _screenWidth
* 0.11(球门的大小)。将 _screenWidth
* 0.75 / 2.0 + _screenWidth
* 0.11 加到结果中,以便守门员在球门前**移动**。现在,是时候让守门员接住球了。
命中检测
如果你想知道守门员是否接住了球,你必须知道球的矩形是否与守门员的矩形相交。你可以在 Update
方法中这样做,在计算完两个矩形之后
_ballRectangle.X = (int)_ballPosition.X; _ballRectangle.Y = (int)_ballPosition.Y; _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY, _goalKeeperWidth, _goalKeeperHeight); if (_goalkeeperRectangle.Intersects(_ballRectangle)) { ResetGame(); }
ResetGame 只是对代码的重构,将游戏重置到初始状态
private void ResetGame() { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2; _isBallMoving = false; _isBallHit = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); }
通过这段简单的代码,游戏就能知道守门员是否接住了球。现在,你必须知道球是否击中了球门。当你**移动**球**越过**球门线时,你就会知道这一点。
var isTimeout = timeInMovement > 5.0; if (_ballPosition.Y < _goalLinePosition || isTimeout) { bool isGoal = !isTimeout && (_ballPosition.X > _screenWidth * 0.375) && (_ballPosition.X < _screenWidth * 0.623); ResetGame(); }
球必须完全进入球门,所以它的位置必须从第一个球门柱(_screenWidth
* 0.375)之后开始,并在第二个球门柱(_screenWidth
* 0.625 - _screenWidth
* 0.02)之前结束。现在是更新游戏分数的时候了。
添加记分
要向游戏中添加记分,你必须添加一个新资源:一个带有游戏中字体**的** spritefont。spritefont 是一个描述字体的 .xml 文件——字体系列、大小和粗细,以及一些其他属性。在游戏中,你可以使用这样的 spritefont
<pre class="brush: xml"> <?xml version="1.0" encoding="utf-8"?> <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics"> <Asset Type="Graphics:FontDescription"> <FontName>Segoe UI</FontName> <Size>24</Size> <Spacing>0</Spacing> <UseKerning>false</UseKerning> <Style>Regular</Style> <CharacterRegions> <CharacterRegion> <Start> </Star> <End></End> </CharacterRegion> </CharacterRegions> </Asset> </XnaContent>
你必须使用 XNA 内容编译器编译此 .xml 文件,并将生成的 .xnb 文件添加到项目的 Content 文件夹中;将其生成操作设置为 **Content**,并将复制到输出目录设置为 **如果较新则复制**。字体在 LoadContent
方法中加载
_soccerFont = Content.Load<SpriteFont>("SoccerFont");
在 ResetWindowSize
中,重置分数的**位置**
var scoreSize = _soccerFont.MeasureString(_scoreText); _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);
要记分,请声明两个变量:_userScore
和 _computerScore
。当进球发生时,_userScore
变量**增加**,而当球出界、**超时**或守门员接住球时,_computerScore
**增加**
if (_ballPosition.Y < _goalLinePosition || isTimeout) { bool isGoal = !isTimeout && (_ballPosition.X > _screenWidth * 0.375) && (_ballPosition.X < _screenWidth * 0.623); if (isGoal) _userScore++; else _computerScore++; ResetGame(); } … if (_goalkeeperRectangle.Intersects(_ballRectangle)) { _computerScore++; ResetGame(); }
ResetGame
重新创建分数文本并设置其**位置**
private void ResetGame() { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2; _isBallMoving = false; _isBallHit = false; _scoreText = string.Format("{0} x {1}", _userScore, _computerScore); var scoreSize = _soccerFont.MeasureString(_scoreText); _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0); while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); }
_soccerFont.MeasureString
使用所选字体测量字符串,你将使用此测量来计算分数**位置**。分数将在 Draw 方法中绘制
protected override void Draw(GameTime gameTime) { … // Draw the score _spriteBatch.DrawString(_soccerFont, _scoreText, new Vector2(_scorePosition, _screenHeight * 0.9f), Color.White); // End the sprite batch _spriteBatch.End(); base.Draw(gameTime); }
打开体育场灯
作为最后的润色,当房间里的光线水平变暗时,游戏会打开体育场灯。新的 Ultrabook 和 2 合 1 设备通常配有光线传感器,你可以利用它来确定房间里的光线**量**,并更改背景的绘制**方式**。
对于桌面应用程序,你可以使用 Microsoft .NET Framework 的 Windows API Code Pack,这是一个可以访问 Windows 7 及更高版本操作系统功能的库。但是,对于这款游戏,让我们选择另一条**路径**:WinRT Sensor API。虽然是为 Windows 8 编写的,但这些 API 也可用于桌面应用程序,并且可以**无需****任何**更改就**使用**。使用它们,你可以将应用程序移植到 Windows 8,而**无需****更改****一行**代码。
Intel® Developer Zone (IDZ) 有一篇关于如何在桌面应用程序中使用 WinRT API 的文章(参见“更多信息”部分)。基于这些信息,你必须在解决方案资源管理器中选择项目,右键单击它,然后单击 **卸载项目**。然后,再次右键单击项目,然后单击 **编辑项目**。在第一个 PropertyGroup
中,添加一个 TargetPlatFormVersion
标签
<PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> … <FileAlignment>512</FileAlignmen> <TargetPlatformVersion>8.0</TargetPlatformVersion> </PropertyGroup>
再次右键单击项目,然后单击 **重新加载项目**。Visual Studio 重新加载项目。当你向项目添加新引用时,你将在引用管理器中看到 **Windows** 选项卡,如图 7 所示。
将 Windows 引用添加到项目。你还需要添加 System.Runtime.WindowsRuntime.dll
引用。如果在程序集列表中找不到它,你可以浏览到 .Net Assemblies
文件夹。在我的机器上,路径是 C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5
。
现在,你可以编写代码来检测光线传感器
LightSensor light = LightSensor.GetDefault(); if (light != null) {
如果存在光线传感器,GetDefault
方法将返回一个非 null 变量,你可以用它来检测光线**变化**。通过连接 ReadingChanged
事件来做到这一点,如下所示
LightSensor light = LightSensor.GetDefault(); if (light != null) { light.ReportInterval = 0; light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10; }
如果读数小于 10,则变量 _lightsOn
为 True,你可以用它以不同的**方式**绘制背景。如果你查看 spriteBatch
的 Draw
方法,你会看到第三个参数是颜色。到目前为止,你只使用了白色。此颜色用于**着色**位图。如果使用白色,位图中的颜色将**保持**不变;如果使用黑色,位图将**全**黑。任何其他颜色都会**着色**位图。你可以使用颜色来打开灯,当灯熄灭时使用绿色,当灯亮起时使用白色。在 Draw
方法中,更改背景的绘制**方式**
_spriteBatch.Draw(_backgroundTexture, rectangle, _lightsOn ? Color.White : Color.Green);
现在,当你运行程序时,你会看到灯熄灭时是深绿色背景,灯亮起时是浅绿色背景(图 8)。
你现在已经完成了一个完整的游戏。它**远**未**完成**——它仍然需要大量的**打磨**(进球时的动画、守门员接球或球击中门柱时的球**反弹**)——但我将其留给你作为**家庭作业**。最后一步是将游戏移植到 Windows 8。
将游戏移植到 Windows 8
将 MonoGame 游戏移植到其他平台很简单。你只需要在解决方案中创建一个新项目,类型为 **MonoGame Windows Store Project**,然后删除 Game1.cs
文件,并将 Windows Desktop 应用程序 Content 文件夹中的四个 .xnb 文件添加到新项目的 Content 文件夹中。你将**不会**添加文件的**新副本**,而是添加对**原始**文件的**链接**。在解决方案资源管理器中,右键单击 Content folder
,单击 **添加/现有文件**,选择 Desktop 项目中的四个 .xnb 文件,单击 **添加** 按钮旁边的下拉箭头,然后选择 **添加为链接**。Visual Studio 将添加这四个链接。
然后,将 Game1.cs
文件从旧项目添加到新项目中。重复你对 .xnb 文件进行的操作:右键单击项目,单击 **添加/现有文件**,从其他项目文件夹中选择 Game1.cs 文件,单击 **添加** 按钮旁边的下拉箭头,然后单击 **添加为链接**。最后的更改在 Program.cs
中,你必须更改 Game1
类的命名空间,因为你使用的是来自桌面项目的 Game1
类。
就这样——你已经为 Windows 8 创建了一个游戏!
结论
开发游戏本身就是一项艰巨的任务。你将不得不**回忆**你的几何学、三角学和物理学课程,并将所有这些**概念**应用于游戏开发(如果老师们在教授这些科目时使用游戏,那不是很好吗?)。
MonoGame 使这项任务变得**容易**了一些。你**无需**处理 DirectX,你可以使用 C# 开发你的游戏,并且你可以完全访问硬件。触摸、声音和传感器都可以用于你的游戏。此外,你可以开发一个游戏并将其移植到 Windows 8、Windows Phone、Mac OS X、iOS 或 Android,只需进行**微小**的**更改**。当你想要开发**多平台**游戏时,这**绝对**是一大**优势**。
更多信息
- 访问 MonoDevelop 网站 http://monodevelop.com
- 访问 Xamarin 网站获取 Xamarin Studio http://xamarin.com
- 从 http://monoGame.net 下载 MonoGame
- 在 http://www.microsoft.com/en-us/download/details.aspx?id=23714 下载 XNA Game Studio
- 了解如何在 Windows 8 上使用 Visual Studio 2012 安装 XNA,请参阅 http://blogs.msdn.com/b/uk_faculty_connection/archive/2013/11/12/installing-xna-on-windows-8-with-visual-studio-2012.aspx
- 从 http://www.microsoft.com/en-us/download/details.aspx?id=35471 下载 Windows Phone 8 SDK
- 从 http://xnacontentcompiler.codeplex.com 获取 XNA 内容编译器
- 从 http://msdn.microsoft.com/en-us/library/bb447759.aspx 获取 spritefont XML 模式
- Microsoft .NET Framework 的 Windows API Code Pack 可从 http://archive.msdn.microsoft.com/WindowsAPICodePack 获取。
- 请参阅“从桌面应用程序中使用 Windows 8* WinRT API”,网址为 http://software.intel.com/en-us/articles/using-winrt-apis-from-desktop-applications。