关闭

使用 MonoGame* 开发游戏

3161人阅读 评论(0) 收藏 举报
分类:

https://software.intel.com/zh-cn/articles/developing-games-with-monogame

作者: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 函数。 该函数可初始化和运行游戏:

1static void Main()
2{
3    using (var game = new Game1())
4        game.Run();
5}

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)”,设置构建操作。 如果不执行该操作,则会在加载资产时出现错误。

创建两个字段存储足球和足球场的纹理:

1private Texture2D _backgroundTexture;
2private Texture2D _ballTexture;

这些字段可在 LoadContent 方法中加载:

1protected override void LoadContent()
2{
3    // Create a new SpriteBatch, which can be used to draw textures.
4    _spriteBatch = new SpriteBatch(GraphicsDevice);
5
6    // TODO: use this.Content to load your game content here
7    _backgroundTexture = Content.Load<Texture2D>("SoccerField");
8    _ballTexture = Content.Load<Texture2D>("SoccerBall");
9}

请注意,纹理的名称与内容(Content )文件夹中的文件名称相同,但是没有扩展名。

接下来,在 Draw 方法中绘制纹理:

01protected override void Draw(GameTime gameTime)
02{
03    GraphicsDevice.Clear(Color.Green);
04
05    // Set the position for the background   
06    var screenWidth = Window.ClientBounds.Width;
07    var screenHeight = Window.ClientBounds.Height;
08    var rectangle = new Rectangle(0, 0, screenWidth, screenHeight);
09    // Begin a sprite batch   
10    _spriteBatch.Begin();
11    // Draw the background   
12    _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White);
13    // Draw the ball
14    var initialBallPositionX = screenWidth / 2;
15    var ínitialBallPositionY = (int)(screenHeight * 0.8);
16    var ballDimension = (screenWidth > screenHeight) ?
17        (int)(screenWidth * 0.02) :
18        (int)(screenHeight * 0.035);
19    var ballRectangle = new Rectangle(initialBallPositionX, ínitialBallPositionY,
20        ballDimension, ballDimension);
21    _spriteBatch.Draw(_ballTexture, ballRectangle, Color.White);
22    // End the sprite batch   
23    _spriteBatch.End();
24    base.Draw(gameTime);
25}

这种方法是用绿色清屏,然后绘制背景并绘制罚球点的足球。 第一种方法 spriteBatch Draw 可绘制能够调整为窗口尺寸的背景,位置 0,0;第二种方法可绘制罚球点的足球。 它可调整为窗口大小的比例。 此处没有运动,因为位置不改变。 接下来是移动足球。

移动足球

如要移动足球,我们必须重新计算循环中每个迭代的位置,并在新的位置绘制它。 在 Update 方法中执行新位置的计算:

01protected override void Update(GameTime gameTime)
02{
03    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
04        Keyboard.GetState().IsKeyDown(Keys.Escape))
05        Exit();
06
07    // TODO: Add your update logic here
08    _ballPosition -= 3;
09    _ballRectangle.Y = _ballPosition;
10    base.Update(gameTime);
11
12}

足球位置在每个循环中都会通过减去三个像素进行更新。 如果你希望让球移动地更快,则必须减去更多的像素。 变量 _screenWidth_screenHeight_backgroundRectangle_ballRectangle_ballPosition 是私有字段,可在 ResetWindowSize 方法中进行初始化:

01private void ResetWindowSize()
02{
03    _screenWidth = Window.ClientBounds.Width;
04    _screenHeight = Window.ClientBounds.Height;
05    _backgroundRectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
06    _initialBallPosition = new Vector2(_screenWidth / 2.0f, _screenHeight * 0.8f);
07    var ballDimension = (_screenWidth > _screenHeight) ?
08        (int)(_screenWidth * 0.02) :
09        (int)(_screenHeight * 0.035);
10    _ballPosition = (int)_initialBallPosition.Y;
11    _ballRectangle = new Rectangle((int)_initialBallPosition.X, (int)_initialBallPosition.Y,
12        ballDimension, ballDimension);
13}

该方法可根据窗口的尺寸重置所有变量。 它可在 Initialize 方法中调用:

1protected override void Initialize()
2{
3    // TODO: Add your initialization logic here
4    ResetWindowSize();
5    Window.ClientSizeChanged += (s, e) => ResetWindowSize();
6    base.Initialize();
7}

这种方法在两个不同的位置调用:流程的开始以及每次窗口发生改变时。 Initialize 可处理 ClientSizeChanged,因此当窗口尺寸发生改变时,与窗口尺寸相关的变量将进行重新评估,足球将重新摆放至罚球点。

如果运行程序,你将看到足球呈直线移动,直至字段结束时停止。 当足球到达目标时,你可以使用以下代码将足球复位:

01protected override void Update(GameTime gameTime)
02{
03    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
04        Keyboard.GetState().IsKeyDown(Keys.Escape))
05        Exit();
06
07    // TODO: Add your update logic here
08    _ballPosition -= 3;
09    if (_ballPosition < _goalLinePosition)
10        _ballPosition = (int)_initialBallPosition.Y;
11
12    _ballRectangle.Y = _ballPosition;
13    base.Update(gameTime);
14
15}

The _goalLinePosition variable is another field, initialized in the ResetWindowSize method:

1_goalLinePosition = _screenHeight * 0.05;

你必须在 Draw 方法中做出另一个改变:移除所有计算代码。

01protected override void Draw(GameTime gameTime)
02{
03    GraphicsDevice.Clear(Color.Green);
04
05   var rectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
06    // Begin a sprite batch   
07    _spriteBatch.Begin();
08    // Draw the background   
09    _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White);
10    // Draw the ball
11     
12    _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White);
13    // End the sprite batch   
14    _spriteBatch.End();
15    base.Draw(gameTime);
16}

该运动与目标呈垂直角度。 如果你希望足球呈一定的角度移动,则需要创建 _ballPositionX 字段,并增加(向右移动)或减少(向左移动)它。 更好的方法是将 Vector2 用于足球位置,如下:

01protected override void Update(GameTime gameTime)
02{
03    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
04        Keyboard.GetState().IsKeyDown(Keys.Escape))
05        Exit();
06
07    // TODO: Add your update logic here
08    _ballPosition.X -= 0.5f;
09    _ballPosition.Y -= 3;
10    if (_ballPosition.Y < _goalLinePosition)
11        _ballPosition = new Vector2(_initialBallPosition.X,_initialBallPosition.Y);
12    _ballRectangle.X = (int)_ballPosition.X;
13    _ballRectangle.Y = (int)_ballPosition.Y;
14    base.Update(gameTime);
15
16}

如果运行该程序,将会显示足球以一个角度运行(图 5)。 接下来是让球在用户点击它时运动。

图 5. 带有足球移动的游戏

触摸和手势

在该游戏中,足球的运动必须以触摸轻拂开始。 该轻拂操作决定了足球的方向和速度。

在 MonoGame 中,你可以使用 TouchScreen 类获得触摸输入。 你可以使用原始输入数据或 Gestures API。 原始输入数据更灵活,因为你可以按照希望的方式处理所有输入;Gestures API 可将该原始数据转换为过滤的手势,以便只接受你希望接收的手势输入。

虽然 Gestures API 更易于使用,但是有几种情况不能使用这种方法。 例如,如果你希望检测特殊手势,如 X 型手势或多手指手势,则需要使用原始数据。

对于该游戏,我们仅需要轻拂操作,Gestures API 支持该操作,所以我们使用它。 首先需要通过使用 TouchPanel 类指明希望使用的手势。 例如,代码:

1TouchPanel.EnabledGestures = GestureType.Flick | GestureType.FreeDrag;

. . . 仅支持 MonoGame 检测并通知轻拂和拖动操作。 然后,在 Update 方法中,你可以按照如下方式处理手势:

1if (TouchPanel.IsGestureAvailable)
2{
3    // Read the next gesture   
4    GestureSample gesture = TouchPanel.ReadGesture();
5    if (gesture.GestureType == GestureType.Flick)
6    {
7        
8    }
9}

首先,确定是否有可用手势。 如果有,则可以调用 ReadGesture 获取并处理它。

使用触摸对运动执行 Initiate 操作

首先,使用 Initialize 方法在游戏中启用轻拂手势:

1protected override void Initialize()
2{
3    // TODO: Add your initialization logic here
4    ResetWindowSize();
5    Window.ClientSizeChanged += (s, e) => ResetWindowSize();
6    TouchPanel.EnabledGestures = GestureType.Flick;
7    base.Initialize();
8}

此时,足球在游戏运行时将会一直运动。 使用私有字段 _isBallMoving 可在足球移动时通知游戏。 在 Update 方法中,当程序检测轻拂操作时,你将 _isBallMoving 设置为 True,则足球将开始运动。 当足球到达球门线时,将 _isBallMoving 设置为 False 并重置足球的位置:

01protected override void Update(GameTime gameTime)
02{
03    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
04        Keyboard.GetState().IsKeyDown(Keys.Escape))
05        Exit();
06
07    // TODO: Add your update logic here
08    if (!_isBallMoving && TouchPanel.IsGestureAvailable)
09    {
10        // Read the next gesture   
11        GestureSample gesture = TouchPanel.ReadGesture();
12        if (gesture.GestureType == GestureType.Flick)
13        {
14            _isBallMoving = true;
15            _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f;
16        }
17    }
18    if (_isBallMoving)
19    {
20        _ballPosition += _ballVelocity;
21        // reached goal line
22        if (_ballPosition.Y < _goalLinePosition)
23        {
24            _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
25            _isBallMoving = false;
26            while (TouchPanel.IsGestureAvailable)
27                TouchPanel.ReadGesture();
28        }
29        _ballRectangle.X = (int) _ballPosition.X;
30        _ballRectangle.Y = (int) _ballPosition.Y;
31    }
32    base.Update(gameTime);
33
34}

不再保持足球增量:程序使用 _ballVelocity 字段从 x 和 y 方向上设置足球速度。 Gesture.Delta 可返回上一次更新之后的运动变量。 如要计算轻拂操作的速度,请将该矢量与 TargetElapsedTime 属性相乘。

如果足球正在移动,_ballPosition 矢量将按照速度(每帧的像素数)增加直至足球到达球门线。 以下代码:

1_isBallMoving = false;
2while (TouchPanel.IsGestureAvailable)
3    TouchPanel.ReadGesture();

. . .可以执行两个操作:它可以让足球停止,也可以移除输入队列的所有手势。 如果你不执行该操作,则用户能够在足球移动时进行轻拂操作,这将会使足球在停止之后再次移动。

当运行该游戏时,你可以轻拂足球,它能够以你轻拂的速度和方向进行移动。 但是,此处有一个问题。 代码无法检测到轻拂操作出现的位置。 你可以轻拂屏幕的任何位置(不仅是足球内部),然后足球将开始移动。 你可以使用 gesture.Position 检测轻拂的姿势,但是该属性将会一直返回 0,0,因此便无法使用该方法。

解决这一问题的方法是使用原始输入,获取触摸点,然后了解其是否在足球附近。 以下代码能够决定触摸输入是否可以触发足球。 如果可以,手势将设置 _isBallHit field

TouchCollection touches = TouchPanel.GetState();

01TouchCollection touches = TouchPanel.GetState();
02
03if (touches.Count > 0 && touches[0].State == TouchLocationState.Pressed)
04{
05    var touchPoint = new Point((int)touches[0].Position.X, (int)touches[0].Position.Y);
06    var hitRectangle = new Rectangle((int)_ballPositionX, (int)_ballPositionY, _ballTexture.Width,
07        _ballTexture.Height);
08    hitRectangle.Inflate(20,20);
09    _isBallHit = hitRectangle.Contains(touchPoint);
10}

然后,运动仅在 _isBallHit 字段为 True 时开始:

1if (TouchPanel.IsGestureAvailable && _isBallHit)

如果运行游戏,你将仅可在轻拂操作启动足球时移动它。 但是,此处仍然存在一个问题:如果点击球的速度太慢或以其无法击中球门线的位置点击,则游戏将会结束,因为足球不会返回起始点。 必须为足球移动设置一个超时。 当到达超时时,游戏便会将足球复位。

Update 方法有一个参数: gameTime。 如果在移动开始时存储了 gameTime 值,则可知道足球移动的实际时间,并可在超时后重置游戏:

01if (gesture.GestureType == GestureType.Flick)
02{
03    _isBallMoving = true;
04    _isBallHit = false;
05    _startMovement = gameTime.TotalGameTime;
06    _ballVelocity = gesture.Delta*(float) TargetElapsedTime.TotalSeconds/5.0f;
07}
08
09...
10
11var timeInMovement = (gameTime.TotalGameTime - _startMovement).TotalSeconds;
12// reached goal line or timeout
13if (_ballPosition.Y <' _goalLinePosition || timeInMovement > 5.0)
14{
15    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
16    _isBallMoving = false;
17    _isBallHit = false;
18    while (TouchPanel.IsGestureAvailable)
19        TouchPanel.ReadGesture();
20}

添加守门员

游戏现在可以运行了,但是它还需要一个制造难度的元素:你必须添加一个守门员,在用户踢出足球后一直运动。 守门员是 XNA 内容编译器编译的 .png 文件(图 6)。 我们必须将该编译文件添加至 Content 文件夹,为 Content 设置构建操作,并将“复制至输出目录 (Copy to Output Directory)”设置为“如果较新则复制(Copy if Newer)”。

图 6. 守门员

守门员在 LoadContent 方法中加载:

01protected override void LoadContent()
02{
03    // Create a new SpriteBatch, which can be used to draw textures.
04    _spriteBatch = new SpriteBatch(GraphicsDevice);
05
06    // TODO: use this.Content to load your game content here
07    _backgroundTexture = Content.Load<Texture2D>("SoccerField");
08    _ballTexture = Content.Load<Texture2D>("SoccerBall");
09    _goalkeeperTexture = Content.Load<Texture2D>("Goalkeeper");
10}

然后,我们必须在 Draw 方法中绘制它:

01protected override void Draw(GameTime gameTime)
02{
03
04    GraphicsDevice.Clear(Color.Green);
05    
06    // Begin a sprite batch   
07    _spriteBatch.Begin();
08    // Draw the background   
09    _spriteBatch.Draw(_backgroundTexture, _backgroundRectangle, Color.White);
10    // Draw the ball
11    _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White);
12    // Draw the goalkeeper
13    _spriteBatch.Draw(_goalkeeperTexture, _goalkeeperRectangle, Color.White);
14    // End the sprite batch   
15    _spriteBatch.End();
16    base.Draw(gameTime);
17}

_goalkeeperRectangle 在窗口中可提供一个矩形的守门员。 它可在 Update 方法中更改:

01protected override void Update(GameTime gameTime)
02{
03    
04
05   _ballRectangle.X = (int) _ballPosition.X;
06   _ballRectangle.Y = (int) _ballPosition.Y;
07   _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY,
08                    _goalKeeperWidth, _goalKeeperHeight);
09   base.Update(gameTime);
10}

_goalkeeperPositionY、_goalKeeperWidth_goalKeeperHeight 字段可在 ResetWindowSize 方法中更新:

1private void ResetWindowSize()
2{
3    
4    _goalkeeperPositionY = (int) (_screenHeight*0.12);
5    _goalKeeperWidth = (int)(_screenWidth * 0.05);
6    _goalKeeperHeight = (int)(_screenWidth * 0.005);
7}

守门员最初位于屏幕中央的球门线顶端附近。

1_goalkeeperPositionX = (_screenWidth - _goalKeeperWidth)/2;

守门员将会在足球开始移动时开始移动。 它将会不停地以谐运动的方式从一端移动至另一端。 该正弦曲线可描述该运动:

X = A * sin(at + δ)

其中,A 是运动幅度(目标宽度),t 是运动时间, aδ 是随机系数(这将会使运动具备一定的随机性,因此用户将无法预测守门员的速度和方向)。

该系数将会在用户通过轻拂踢出足球时进行计算:

01if (gesture.GestureType == GestureType.Flick)
02{
03    _isBallMoving = true;
04    _isBallHit = false;
05    _startMovement = gameTime.TotalGameTime;
06    _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f;
07    var rnd = new Random();
08    _aCoef = rnd.NextDouble() * 0.005;
09    _deltaCoef = rnd.NextDouble() * Math.PI / 2;
10}

系数 a 是守门员的速度,0 和 0.005 之间的数字代表 0 和 0.3 像素/秒之间的速度(1/60 秒内最大像素为 0.005)。 delta 系数是必须是介于 0 和 pi/2 之间的数字。 足球移动时,你可以更新守门员的位置:

1if (_isBallMoving)
2{
3    _ballPositionX += _ballVelocity.X;
4    _ballPositionY += _ballVelocity.Y;
5    _goalkeeperPositionX = (int)((_screenWidth * 0.11) *
6                      Math.Sin(_aCoef * gameTime.TotalGameTime.TotalMilliseconds +
7                      _deltaCoef) + (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11);
8    
9}

运动的幅度是 _screenWidth * 0.11(目标尺寸)。 将(_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11 添加至结果,以便守门员移动至目标前方。 现在,开始构建让守门员接住球。

命中测试

如果希望了解守门员是否能够接住球,你需要知道球的矩形是否与守门员的矩形相交。 我们可以按照以下代码计算两个矩形后,在 Update 方法中执行该操作:

1_ballRectangle.X = (int)_ballPosition.X;
2_ballRectangle.Y = (int)_ballPosition.Y;
3_goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY,
4    _goalKeeperWidth, _goalKeeperHeight);
5if (_goalkeeperRectangle.Intersects(_ballRectangle))
6{
7    ResetGame();
8}

ResetGame 仅可重构代码,将游戏重置为初始状态:

1private void ResetGame()
2{
3    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
4    _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2;
5    _isBallMoving = false;
6    _isBallHit = false;
7    while (TouchPanel.IsGestureAvailable)
8        TouchPanel.ReadGesture();
9}

借助该简单代码,游戏便可知道守门员是否能够接住球。 现在,我们需要知道足球是否能够命中。 当足球超过球门线时,执行以下代码。

1var isTimeout = timeInMovement > 5.0;
2if (_ballPosition.Y < _goalLinePosition || isTimeout)
3{
4    bool isGoal = !isTimeout &&
5        (_ballPosition.X > _screenWidth * 0.375) &&
6        (_ballPosition.X < _screenWidth * 0.623);
7    ResetGame();
8}

足球必须完全在目标中,因此,其位置必须在第一个球门柱之后(_screenWidth * 0.375)开始,并在第二个球门柱之前(_screenWidth * 0.625 − _screenWidth * 0.02)结束。 现在,我们开始更新游戏分数。

添加分数记录(Scorekeeping)

如要向游戏中添加游戏记录,我们必须添加一个新资产:spritefont,其字体可用于游戏。 spritefont 是描述字体的 .xml 文件,包括字体家族及其尺寸和重量及其他属性。 在游戏中,你可以按照以下方式使用 spritefont:

01<?xml version="1.0" encoding="utf-8"?>
02<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
03  <Asset Type="Graphics:FontDescription">
04    <FontName>Segoe UI</FontName>
05    <Size>24</Size>
06    <Spacing>0</Spacing>
07    <UseKerning>false</UseKerning>
08    <Style>Regular</Style>
09    <CharacterRegions>
10      <CharacterRegion>
11        <Start> </Star>
12        <End></End>
13      </CharacterRegion>
14    </CharacterRegions>
15  </Asset>
16</XnaContent>
你可以使用 XNA 内容编译器来编译该 .xml 文件,并将生成的 .xnb 文件添加至项目的 Content 文件夹;将其构建操作设置至 Content,并将“复制至输出目录(Copy to Output Directory)” 设置为“如果较新则复制(Copy if Newer)”。 字体可在 LoadContent 方法中加载:
1_soccerFont = Content.Load<SpriteFont>("SoccerFont");

ResetWindowSize 中,重置得分情况:

1var scoreSize = _soccerFont.MeasureString(_scoreText);
2_scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);

如要保持记录,需要声明两个变量: _userScore_computerScore。 命中时,_userScore 变量增加,未命中、超时或守门员接住球时,_computerScore 增加:

01if (_ballPosition.Y < _goalLinePosition || isTimeout)
02{
03    bool isGoal = !isTimeout &&
04                  (_ballPosition.X > _screenWidth * 0.375) &&
05                  (_ballPosition.X < _screenWidth * 0.623);
06    if (isGoal)
07        _userScore++;
08    else
09        _computerScore++;
10    ResetGame();
11}
12
13if (_goalkeeperRectangle.Intersects(_ballRectangle))
14{
15    _computerScore++;
16    ResetGame();
17}

ResetGame 可重新创建得分文本,并设置其情况:

01private void ResetGame()
02{
03    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
04    _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2;
05    _isBallMoving = false;
06    _isBallHit = false;
07    _scoreText = string.Format("{0} x {1}", _userScore, _computerScore);
08    var scoreSize = _soccerFont.MeasureString(_scoreText);
09    _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);
10    while (TouchPanel.IsGestureAvailable)
11        TouchPanel.ReadGesture();
12}

_soccerFont.MeasureString 可使用选中字体测量字符串,你可以使用该测量方式来计算得分情况。 得分可在 Draw 方法中进行绘制:

01protected override void Draw(GameTime gameTime)
02{
03
04    // Draw the score
05    _spriteBatch.DrawString(_soccerFont, _scoreText,
06         new Vector2(_scorePosition, _screenHeight * 0.9f), Color.White);
07    // End the sprite batch   
08    _spriteBatch.End();
09    base.Draw(gameTime);
10}

打开球场灯光

作为最后一个触摸设计,该款游戏可在室内光线较暗时打开球场灯光。 全新超极本和 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 标签:

1<PropertyGroup>
2  <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
3
4  <FileAlignment>512</FileAlignmen>
5  <TargetPlatformVersion>8.0</TargetPlatformVersion>
6</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

现在,你可以开始编写代码来检测灯光传感器:

1LightSensor light = LightSensor.GetDefault();
2if (light != null)
3{

如果有灯光传感器,GetDefault 方法可返回一个非空变量,以便用来检查灯光变化。 通过编写 ReadingChanged 事件来执行该操作,如下:

1LightSensor light = LightSensor.GetDefault();
2if (light != null)
3{
4    light.ReportInterval = 0;
5    light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10;
6}

如果读取的值小于 10,则变量 _lightsOn 为真,你可以用它以不同的方式来绘制背景。 如果你看到 spriteBatchDraw 方法,将会发现第三个参数为颜色。 到目前为止,你只使用过白色。 该颜色用于为位图着色。 如果你使用白色,则位图中的颜色将保持不变;如果你使用黑色,则位图将会全部变为黑色。 你可以使用任何颜色为位图着色。 你可以使用颜色来打开灯光,当灯光关闭时使用绿色,开启时使用白色。 在 Draw 方法中,更改背景的绘制:

1_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 以葡萄牙语出版,此外,他还在巴西和美国的杂志和网站上发表过多篇文章。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    风色年代——不只是技术!

    有问题?欢迎到公众号下留言^ ^
    个人资料
    • 访问:1576454次
    • 积分:16254
    • 等级:
    • 排名:第674名
    • 原创:44篇
    • 转载:570篇
    • 译文:4篇
    • 评论:206条
    公告
    本博收藏大部分文章为转载,并在文章开头给出了原创作者及原文出处,如有再转,敬请保留相关信息,这是大家对原创作者劳动成果的自觉尊重!!
    早期遗留的部分文章未能及时给出相关原创信息,在此谨致歉意,后续会尽全力予以纠正。如为您带来不便,请于本博下留言,谢谢配合。