作者:Bruno Sonnino
Download article as PDF
全球各地的开发人员都希望开发游戏。 为什么不呢? 游戏是计算机历史上销量最高的产品之一,游戏业务带来的财富不断吸引着开发人员的加入。 作为开发人员,我当然希望成为下一个开发愤怒的小鸟* 或光晕*的开发人员。
但是,事实上,游戏开发是软件开发最困难的领域之一。 你不得不牢记那些从来不会使用的三角函数、几何和物理类。 除此之外,你的游戏必须以吸引用户沉浸其中的方式来组合声音、视频和故事情节。 然后,你需要再编写一行代码!
为了简化难度,开发游戏使用的框架不仅要能够使用 C 和 C++,还要能够使用 C# 或 JavaScript*(是的,你可以使用 HTML5 和 JavaScript 开发适用于您的浏览器的三维游戏)。
其中一个框架是 Microsoft XNA*,该框架基于 Microsoft DirectX* 技术,支持为 Xbox 360*、Windows* 和 Windows Phone* 创建游戏。 微软已经初步淘汰了 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 开发的一款跨平台 IDE — Xamarin Studio*。 借助这些 IDE,你可以使用 C# 在 Linux 或 Mac 上进行开发。
如果你是一位 Microsoft .NET 开发人员,并且日常使用的工具是 Microsoft Visual Studio*,你可以像我一样将 MonoGame 安装到 Visual Studio 中并且用它来创建游戏。 在撰写本文时,MonoGame 的最新稳定版本是 3.2 版。该版本可在 Visual Studio 2012 和 2013 中运行,并支持创建支持触摸功能的 DirectX 桌面游戏。
MonoGame 安装在 Visual Studio 中随附了许多新模板,你可从中选择来创建游戏,如图 1 所示。
图 1.全新 MonoGame* 模板
现在,如要创建第一个游戏,请点击 MonoGame Windows Project,然后选择一个名称。 Visual Studio 可创建一个包括所有所需文件和参考的新项目。 如果运行该项目,则应如图 2 所示。
图 2.在 MonoGame* 模板中创建的游戏
很无聊,是吗? 只有一个蓝色屏幕;但是,构建任何游戏都要从它开始。 按 Esc,则可关闭窗口。
现在,你可以使用目前拥有的项目开始编写游戏,但是有一个问题: 如要添加任何资产(图像、子图、声音或字体),你需要将其编写为与 MonoGame 兼容的格式。 对于这一点,你需要以下选项之一:
- 安装 XNA 游戏 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,但是在 Visual Studio 2012 中安装 Windows Phone 8 SDK 更好。 你可以用它创建项目来编译资产。
XNA 内容编译器
如果不希望安装 SDK 来编译资产,则可使用 XNA 内容编译器(详见“了解更多信息”中的链接),该编译器是一款开源程序,能够将资产编译至 MonoGame 中可使用的 .xnb 文件。
创建第一个游戏
使用 MonoGame 模板创建的上一个游戏可作为所有游戏的起点。 你可以使用相同的流程创建所有游戏。 Program.cs 中包括 Main 函数。 该函数可初始化和运行游戏:
static void Main()
{
using (var game = new Game1())
game.Run();
}
Game1.cs
是游戏的核心。 有两种方法需要在一个循环中每秒钟调用 60 次: 更新和绘制。 在更新中,为游戏中的所有元素重新计算数据;在绘制中,绘制这些元素。 请注意,这是一个紧凑的循环。 你只有 1/60 秒,也就是 16.7 毫秒来计算和绘制数据。 如果你超出该事件,程序就会跳过一些绘制循环,游戏中就会出现图形故障。
近来,台式电脑上的游戏输入方式是键盘和鼠标。 除非用户购买了外部硬件,如驱动轮和操纵杆,否则我们只能假定没有其他的输入方法。 随着新硬件的推出,如超极本™ 设备、 2 合 1 超极本和一体机, 输入选项发生了变化。 你可以使用触摸输入和传感器,为用户提供更加沉浸式、逼真的游戏体验。
对于第一款游戏,我们将创建足球点球赛。 用户使用触摸的方式来“射门”,计算机守门员接球。 球的方向和速度由用户的敲击动作来决定。 计算机守门员将会随机选择一个方向和速度接球。 射门成功得一分。 反之,守门员的一分。
向游戏添加内容
游戏中的第一步是添加内容。 通过添加背景场地和足球开始。 如要执行该操作,则需要创建两个 .png 文件:一个文件用于足球场(图 3),另一个用于足球(图 4)。
图 3.足球场
图 4.足球
如要在游戏中使用这些文件,你需要对其进行编译。 如果正在使用 XNA Game Studio 或 Windows Phone 8 SDK,则需要创建一个 XNA 内容项目。 该项目不需要在同一个解决方案中。 你只需要用它来编译资产。 将图像添加至该项目并对其进行构建。 然后,访问项目目标目录,并将生成的 .xnb 文件复制至你的项目。
我更喜欢使用 XNA 内容编译器,它不需要新项目且支持按需编译资产。 仅需打开程序,将文件添加至列表,选择输出目录,并点击“编译(Compile)”。 .xnb 文件便可添加至该项目。
内容文件
.xnb 文件可用时,将其添加至游戏的 “内容( Content)” 文件夹下。 你必须为每个文件,包括“内容(Content)”、“复制至输入目录(Copy to Output Directory)”以及“如果较新则复制(Copy if Newer)”,设置构建操作。 如果不执行该操作,则会在加载资产时出现错误。
创建两个字段存储足球和足球场的纹理:
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);
}
The _goalLinePosition
variable is another field, initialized in the ResetWindowSize
method:
_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)。 接下来是让球在用户点击它时运动。
图 5.带有足球移动的游戏
触摸和手势
在该游戏中,足球的运动必须以触摸轻拂开始。 该轻拂操作决定了足球的方向和速度。
在 MonoGame 中,你可以使用 TouchScreen
类获得触摸输入。 你可以使用原始输入数据或 Gestures API。 原始输入数据更灵活,因为你可以按照希望的方式处理所有输入;Gestures API 可将该原始数据转换为过滤的手势,以便只接受你希望接收的手势输入。
虽然 Gestures API 更易于使用,但是有几种情况不能使用这种方法。 例如,如果你希望检测特殊手势,如 X 型手势或多手指手势,则需要使用原始数据。
对于该游戏,我们仅需要轻拂操作,Gestures API 支持该操作,所以我们使用它。 首先需要通过使用 TouchPanel 类
指明希望使用的手势。 例如,代码:
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
获取并处理它。
使用触摸对运动执行 Initiate 操作
首先,使用 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 field
:
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();
}
添加守门员
游戏现在可以运行了,但是它还需要一个制造难度的元素:你必须添加一个守门员,在用户踢出足球后一直运动。 守门员是 XNA 内容编译器编译的 .png 文件(图 6)。 我们必须将该编译文件添加至 Content 文件夹,为 Content 设置构建操作,并将“复制至输出目录 (Copy to Output Directory)”设置为“如果较新则复制(Copy if Newer)”。
图 6.守门员
守门员在 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)结束。 现在,我们开始更新游戏分数。
添加分数记录(Scorekeeping)
如要向游戏中添加游戏记录,我们必须添加一个新资产:spritefont,其字体可用于游戏。 spritefont是描述字体的 .xml 文件,包括字体家族及其尺寸和重量及其他属性。 在游戏中,你可以按照以下方式使用 spritefont:
<?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,并将“复制至输出目录(Copy to Output Directory)”设置为“如果较新则复制(Copy if Newer)”。 字体可在 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);
}
打开球场灯光
作为最后一个触摸设计,该款游戏可在室内光线较暗时打开球场灯光。 全新超极本和 2 合 1 设备通常具备一个光线传感器,你可以用它来确定室内光线的程度并更改背景的绘制方式。
对于台式机应用,我们可以使用面向 Microsoft .NET Framework 的 Windows API Code Pack,它是一款支持访问 Windows 7 及更高版本操作系统特性的库。 但是,在该游戏中,我们采用了另一种方式:WinRT Sensor API。 这些 API 虽然面向 Windows 8 而编写,但是同样适用于台式机应用,且不经任何更改即可使用。 借助它们,你无需更改任何代码即可将应用移植到 Windows 8。
英特尔® 开发人员专区(IDZ)包括一篇如何在台式机应用中使用 WinRT API 的文章(详见“了解更多信息”部分)。 基于该信息,你必须在 Solution Explorer 中选择该项目,右击它,然后点击 Unload Project。 然后,再次右击该项目,并点击 Edit project。 在第一个 PropertyGroup
中添加 TargetPlatFormVersion
标签:
<PropertyGroup><Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
…
<FileAlignment>512</FileAlignmen><TargetPlatformVersion>8.0</TargetPlatformVersion></PropertyGroup>
再次右击项目,然后点击Reload Project。 Visual Studio 将重新加载该项目。 当向项目中添加新标签时,将能够在 Reference Manager 中看到 Windows标签,如图 7 所示。
图 7.Reference Manager 中的 Windows* 标签
向项目中添加 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
方法可返回一个非空变量,以便用来检查灯光变化。 通过编写 ReadingChanged
事件来执行该操作,如下:
LightSensor light = LightSensor.GetDefault();
if (light != null)
{
light.ReportInterval = 0;
light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10;
}
如果读取的值小于 10,则变量 _lightsOn
为真,你可以用它以不同的方式来绘制背景。 如果你看到 spriteBatch
的 Draw
方法,将会发现第三个参数为颜色。 到目前为止,你只使用过白色。 该颜色用于为位图着色。 如果你使用白色,则位图中的颜色将保持不变;如果你使用黑色,则位图将会全部变为黑色。 你可以使用任何颜色为位图着色。 你可以使用颜色来打开灯光,当灯光关闭时使用绿色,开启时使用白色。 在 Draw
方法中,更改背景的绘制:
_spriteBatch.Draw(_backgroundTexture, rectangle, _lightsOn ? Color.White : Color.Green);
现在,当你运行程序时,当灯光关闭时你将会看到深绿色背景,当灯光开启时将会看到浅绿色背景(图 8)。
图 8.完整游戏
现在你拥有了一款完整的游戏。 但是,它尚且未完成,它还需要大量改进(命中时的动画,守门员接住球或球击中球门柱时的反弹画面),但是我把它作为家庭作业留给你。 最后一步是将游戏移植到 Windows 8。
将游戏移植至 Windows 8。
将 MonoGame 游戏移植至其他平台非常简单。 你只需要在 MonoGame Windows Store Project类型的解决方案中创建一个新项目,然后删除 Game1.cs
文件并将 Windows Desktop 应用 Content
文件夹中的四个 .xnb 文件添加至新项目的 Content 文件夹。 你无需向源文件中添加新文件,只需添加链接。 在 Solution Explorer 中,右击 Content 文件夹
,点击 “添加/现有文件(Add/Existing Files)”,在 Desktop 项目中选择四个 .xnb 文件,点击“添加(Add)”按钮旁边的下箭头,并选择“添加为链接(Add as link)”。 Visual Studio 可添加四个链接。
然后,将 Game1.cs
文件从以前的项目添加至新项目。 重复对 .xnb 文件所执行的流程:右击项目,点击“添加/现有文件(Add/Existing Files)”,从其他项目文件夹中选择 Game1.cs 文件,点击“添加(Add)”按钮旁边的下箭头,然后点击“添加为链接(Add as link)”。 最后需要改动的地方是 Program.cs
,你需要对 Game1
类的命名空间进行更改,因为你现在使用的是台式机项目中的 Game1
类。
完成 — 你创建了一款适用于 Windows 8 的游戏!
结论
游戏开发本身是一项困难重重的任务。 你需要记住三角、几何和物理类,并运用这些概念来开发游戏(如果教授者在教授这些课题时使用的是游戏,会不会很棒?)
MonoGame 让该任务更简单。 你无需处理 DirectX,可以使用 C# 来开发游戏,并且能够完全访问硬件。 你可以在游戏中使用触摸、声音和传感器。 此外,你还可以开发一款游戏,对其进行较小的修改并将其移植至 Windows 8、Windows Phone、Mac OS X、iOS 或 Android。 当你希望开发多平台游戏时,这是一个巨大的优势。
了解更多信息
关于作者
Bruno Sonnino 是巴西的微软最有价值专家(MVP)。他是一位开发人员、咨询师兼作家,曾编写过五本有关 Delphi 的书籍,并由 Pearson Education Brazil 以葡萄牙语出版,此外,他还在巴西和美国的杂志和网站上发表过多篇文章。