用 MonoGame 2D 创建 UWP 游戏Create a UWP game in MonoGame 2D

适用于 Microsoft Store、用 C# 和 MonoGame 编写的简单 2D UWP 游戏A simple 2D UWP game for the Microsoft Store, written in C# and MonoGame

行走的恐龙的子画面表

简介Introduction

MonoGame 是一款轻型游戏开发框架。MonoGame is a lightweight game development framework. 本教程介绍了用 MonoGame 进行游戏开发的基础知识,包括如何加载内容、绘制子画面、添加动画效果和处理用户输入。This tutorial will teach you the basics of game development in MonoGame, including how to load content, draw sprites, animate them, and handle user input. 此外,还介绍了一些更高级的概念,如碰撞检测和对高分辨率屏幕按比例缩放等。Some more advanced concepts like collision detection and scaling up for high-DPI screens are also discussed. 本教程需要 30-60 分钟。This tutorial takes 30-60 minutes.

必备条件Prerequisites

  • Windows 10 和 Microsoft Visual Studio 2019。Windows 10 and Microsoft Visual Studio 2019. 单击此处了解如何设置 Visual StudioClick here to learn how to get set up with Visual Studio.
  • .NET 桌面开发框架。The .NET desktop development framework. 如果你尚未安装,可以通过重新运行 Visual Studio 安装程序和修改 Visual Studio 2019 安装来获得。If you don't already have this installed, you can get it by re-running the Visual Studio installer and modifying your installation of Visual Studio 2019.
  • C# 或类似面向对象的编程语言的基础知识。Basic knowledge of C# or a similar object-oriented programming language. 单击此处以了解如何开始使用 C#Click here to learn how to get started with C#.
  • 熟悉基本的计算机科学概念如类、方法以及变量,将有所帮助。Familiarity with basic computer science concepts like classes, methods, and variables is a plus.

为什么选择 MonoGame?Why MonoGame?

谈及游戏开发环境时,我们并不缺少选择。There’s no shortage of options when it comes to game development environments. 从功能齐全的引擎(如 Unity)到全面、复杂的多媒体 API(如 DirectX),但这些都让人很难知道从何处入手。From full-featured engines like Unity to comprehensive and complex multimedia APIs like DirectX, it can be hard to know where to start. MonoGame 是一组工具,其复杂度处于游戏引擎与更专业的 API(如 DirectX)之间。MonoGame is a set of tools, with a level of complexity falling somewhere between a game engine and a grittier API like DirectX. 它提供了易于使用的内容管道,以及用于创建在各种平台上运行的轻型游戏所需的所有功能。It provides an easy-to-use content pipeline, and all the functionality required to create lightweight games that run on a wide variety of platforms. 最重要的是,MonoGame 应用是用纯 C# 编写,因此,你可以通过 Microsoft Store 或其他类似的分发平台快速分发。Best of all, MonoGame apps are written in pure C#, and you can distribute them quickly via the Microsoft Store or other similar distribution platforms.

获取代码Get the code

如果你不想跟着教程逐步学习,只想了解 MonoGame 的实际应用,请单击此处以获得完成后的应用If you don’t feel like working through the tutorial step-by-step and just want to see MonoGame in action, click here to get the finished app.

在 Visual Studio 2019 中打开项目,并按 F5 以运行示例。Open the project in Visual Studio 2019, and press F5 to run the sample. 首次进行此操作可能会需要一段时间,因为 Visual Studio 需要提取安装中缺少的所有 NuGet 包。The first time you do this may take a while, as Visual Studio needs to fetch any NuGet packages that are missing from your installation.

如果已完成此操作,则请跳至下一节“设置 MonoGame”,以查看代码的分步操作实例。If you’ve done this, skip the next section about setting up MonoGame to see a step-by-step walkthrough of the code.

注意: 此示例中创建的游戏并不完整(或完全没有乐趣)。Note: The game created in this sample is not meant to be complete (or any fun at all). 其唯一目的在于展示用 MonoGame 进行 2D 游戏开发的所有核心概念。Its only purpose is to demonstrate all the core concepts of 2D development in MonoGame. 可随意使用此代码并进行改进,或者在掌握基础知识之后从头开始。Feel free to use this code and make something much better—or just start from scratch after you’ve mastered the basics!

设置 MonoGame 项目Set up MonoGame project

  1. MonoGame.net 中安装适用于 Visual Studio 的 MonoGame 3.6Install MonoGame 3.6 for Visual Studio from MonoGame.net

  2. 启动 Visual Studio 2019。Start Visual Studio 2019.

  3. 转到“文件”->“新建”->“项目” Go to File -> New -> Project

  4. 在 Visual C# 项目模板下,选择“MonoGame”和“MonoGame Windows 10 通用项目” Under the Visual C# project templates, select MonoGame and MonoGame Windows 10 Universal Project

  5. 将项目命名为“MonoGame2D”并选择“确定”。Name your project “MonoGame2D" and select OK. 创建好项目之后,它可能看上去会像错误百出,但在首次运行此项目并安装完所有缺少的 NuGet 包之后,这种情况就会消失。With the project created, it will probably look like it is full of errors—these should go away after you run the project for the first time, and any missing NuGet packages are installed.

  6. 确保已将“x86”和“本地计算机”设为目标平台,然后按 F5 生成并运行空项目 。Make sure x86 and Local Machine are set as the target platform, and press F5 to build and run the empty project. 如已遵循以上步骤,则你应该会在生成完项目之后看到一个空的蓝色窗口。If you followed the steps above, you should see an empty blue window after the project finishes building.

方法概述Method overview

创建完项目之后,现在请在“解决方案资源管理器”中打开“Game1.cs”文件 。Now you’ve created the project, open the Game1.cs file from the Solution Explorer. 这是大多数游戏逻辑的存储位置。This is where the bulk of the game logic is going to go. 创建新的 MonoGame 项目时,将在此自动生成许多关键方法。Many crucial methods are automatically generated here when you create a new MonoGame project. 下面我们来快速了解这些内容:Let’s quickly review them:

public Game1() 构造函数。public Game1() The constructor. 在本教程中,我们不会对此方法进行任何更改。We aren’t going to change this method at all for this tutorial.

protected override void Initialize() 我们将在此初始化所使用的任何类变量。protected override void Initialize() Here we initialize any class variables that are used. 游戏开始时将会调用此方法一次。This method is called once at the start of the game.

protected override void LoadContent() 此方法用于在游戏开始之前将内容(如protected override void LoadContent() This method loads content (eg. 纹理、音频、字体)加载到内存。textures, audio, fonts) into memory before the game starts. 和初始化一样,应用启动时也会调用此方法一次。Like Initialize, it’s called once when the app starts.

protected override void UnloadContent() 此方法用于卸载非内容管理器内容。protected override void UnloadContent() This method is used to unload non content-manager content. 我们不会用到此方法。We don’t use this one at all.

protected override void Update(GameTime gameTime) 每个游戏循环周期都会调用此方法一次。protected override void Update(GameTime gameTime) This method is called once for every cycle of the game loop. 我们在此更新游戏中使用的任何对象或变量的状态。Here we update the states of any object or variable used in the game. 这包括对象的位置、速度或颜色等等。This includes things like an object’s position, speed, or color. 这也是处理用户输入的地方。This is also where user input is handled. 简而言之,此方法用于处理游戏逻辑的每个部分(在屏幕上绘制对象除外)。In short, this method handles every part of the game logic except drawing objects on screen.

protected override void Draw(GameTime gameTime) 这是在屏幕上使用“Update”方法提供的位置绘制对象的地方。protected override void Draw(GameTime gameTime) This is where objects are drawn on the screen, using the positions given by the Update method.

绘制子画面Draw a sprite

你已运行新的 MonoGame 项目并看到了美丽的蓝天,让我们再添加一些地面背景。So you’ve run your fresh MonoGame project and found a nice blue sky—let’s add some ground. 在 MonoGame 中,2D 艺术将以“子画面”形式添加到应用中。In MonoGame, 2D art is added to the app in the form of “sprites.” 子画面只是一种作为单一实体操纵的计算机图形。A sprite is just a computer graphic that is manipulated as a single entity. 可以对子画面实施移动、缩放、成形、添加动画效果和组合操作,以在 2D 空间中创建你能想象到的任何效果。Sprites can be moved, scaled, shaped, animated, and combined to create anything you can imagine in the 2D space.

1.下载纹理1. Download a texture

对我们来说,第一个子画面会非常乏味。For our purposes, this first sprite is going to be extremely boring. 单击此处以下载此无特色的绿色矩形Click here to download this featureless green rectangle.

2.为 Content 文件夹添加纹理2. Add the texture to the Content folder

  • 打开“解决方案资源管理器” Open the Solution Explorer
  • 右键单击“Content”文件夹中的“Content.mgcb”并选择“打开方式” 。Right click Content.mgcb in the Content folder and select Open With. 从弹出菜单中选择“Monogame 管道”,并选择“确定” 。From the popup menu select Monogame Pipeline, and select OK.
  • 在新窗口中,右键单击“内容”项并选择“添加”->“现有项” 。In the new window, Right-Click the Content item and select Add -> Existing Item.
  • 在文件浏览器中查找并选择绿色矩形。Locate and select the green rectangle in the file browser.
  • 将此项命名为“grass.png”并选择“添加” 。Name the item “grass.png” and select Add.

3.添加类变量3. Add class variables

若要将此图像加载为子画面纹理,请打开 Game1.cs 并添加以下类变量。To load this image as a sprite texture, open Game1.cs and add the following class variables.

const float SKYRATIO = 2f/3f;
float screenWidth;
float screenHeight;
Texture2D grass;

SKYRATIO 变量用于表示场景中的蓝天与草地比例,在本案例中为 2/3。The SKYRATIO variable tells us how much of the scene we want to be sky versus grass—in this case, two-thirds. screenWidthscreenHeight 用于跟踪应用窗口的大小,而 grass 则是我们存储绿色矩形的地方。screenWidth and screenHeight will keep track of the app window size, while grass is where we’ll store our green rectangle.

4.初始化类变量并设置窗口大小4. Initialize class variables and set window size

screenWidthscreenHeight 变量仍需初始化,因此请将此代码添加到 Initialize 方法:The screenWidth and screenHeight variables still need to be initialized, so add this code to the Initialize method:

ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.FullScreen;

screenHeight = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;
screenWidth = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width;

this.IsMouseVisible = false;

除了获得屏幕的高度和宽度外,我们还可以将应用窗口模式设为“全屏”并隐藏鼠标 。Along with getting the screen’s height and width, we also set the app’s windowing mode to Fullscreen, and make the mouse invisible.

5.加载纹理5. Load the texture

若要将纹理加载到草地变量中,请将以下内容添加到 LoadContent 方法:To load the texture into the grass variable, add the following to the LoadContent method:

grass = Content.Load<Texture2D>("grass");

6.绘制子画面6. Draw the sprite

若要绘制矩形,请将以下行添加到 Draw 方法:To draw the rectangle, add the following lines to the Draw method:

GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.Draw(grass, new Rectangle(0, (int)(screenHeight * SKYRATIO),
  (int)screenWidth, (int)screenHeight), Color.White);
spriteBatch.End();

在这里,我们使用 spriteBatch.Draw 方法来将给定的纹理置于矩形对象的边界内。Here we use the spriteBatch.Draw method to place the given texture within the borders of a Rectangle object. 矩形由通过左上角和右下角的 x 和 y 坐标定义。A Rectangle is defined by the x and y coordinates of its top left and bottom right corner. 使用先前定义的 screenWidthscreenHeightSKYRATIO 变量,我们可以在屏幕下方 1/3 处绘制绿色矩形纹理。Using the screenWidth, screenHeight, and SKYRATIO variables we defined earlier, we draw the green rectangle texture across the bottom one-third of the screen. 如果运行程序,则你现在应该可以看到先前的蓝色背景已被绿色矩形部分覆盖。If you run the program now you should see the blue background from before, partially covered by the green rectangle.

绿色矩形

为高分辨率屏幕进行缩放Scale to high DPI screens

如果在高像素监视器上运行 Visual Studio,如 Surface Pro 或 Surface Studio 上的监视器,则你会发现以上步骤中的绿色矩形不会完全覆盖屏幕下方的 1/3。If you’re running Visual Studio on a high pixel-density monitor, like those found on a Surface Pro or Surface Studio, you may find that the green rectangle from the steps above doesn’t quite cover the bottom third of the screen. 它可能会悬浮在屏幕的左下角上。It’s probably floating above the bottom-left corner of the screen. 若要修复此情况并为所有设备提供统一的游戏体验,我们需要创建一种能够根据屏幕的像素缩放某些值的方法。To fix this and unify the experience of our game across all devices, we will need to create a method that scales certain values relative to the screen’s pixel density:

public float ScaleToHighDPI(float f)
{
  DisplayInformation d = DisplayInformation.GetForCurrentView();
  f *= (float)d.RawPixelsPerViewPixel;
  return f;
}

接下来,将 Initialize 方法中的 screenHeightscreenWidth 初始化值替换为以下值:Next replace the initializations of screenHeight and screenWidth in the Initialize method with this:

screenHeight = ScaleToHighDPI((float)ApplicationView.GetForCurrentView().VisibleBounds.Height);
screenWidth = ScaleToHighDPI((float)ApplicationView.GetForCurrentView().VisibleBounds.Width);

如果使用的是高分辨率屏幕并尝试马上运行应用,则你应该会看到绿色矩形已按预期覆盖住屏幕下方的 1/3。If you’re using a high DPI screen and try to run the app now, you should see the green rectangle covering the bottom third of the screen as intended.

生成 SpriteClassBuild the SpriteClass

在开始为子画面添加动画效果之前,我们将制作一个名为“SpriteClass”的新类,使我们能够降低子画面操纵的表层复杂性。Before we start animating sprites, we’re going to make a new class called “SpriteClass,” which will let us reduce the surface-level complexity of sprite manipulation.

1.创建新类1. Create a new class

在“解决方案资源管理器”中,右键单击“MonoGame2D (通用 Windows)”并选择“添加”->“类” 。In the Solution Explorer, right-click MonoGame2D (Universal Windows) and select Add -> Class. 将该类命名为“SpriteClass.cs”,然后选择“添加” 。Name the class “SpriteClass.cs” then select Add.

2.添加类变量2. Add class variables

将此类添加到刚创建的类中:Add this code to the class you just created:

public Texture2D texture
{
  get;
}

public float x
{
  get;
  set;
}

public float y
{
  get;
  set;
}

public float angle
{
  get;
  set;
}

public float dX
{
  get;
  set;
}

public float dY
{
  get;
  set;
}

public float dA
{
  get;
  set;
}

public float scale
{
  get;
  set;
}

在这里,我们将设置用于绘制子画面并为其添加动画效果所需的类变量。Here we set up the class variables we need to draw and animate a sprite. xy 变量表示子画面当前在屏幕上的位置,而 angle 变量则表示子画面当前的角度(0 表示垂直,90 表示顺时针倾斜 90 度)。The x and y variables represent the sprite’s current position on the plane, while the angle variable is the sprite’s current angle in degrees (0 being upright, 90 being tilted 90 degrees clockwise). 请注意,对于此类,xy 表示子画面中心的坐标(默认原点为左上角)。It’s important to note that, for this class, x and y represent the coordinates of the center of the sprite, (the default origin is the top-left corner). 这使得旋转子画面更加容易,因为无论原点位于何处,子画面均可旋转,并且围绕中心旋转还能给我们提供统一的旋转运动。This is makes rotating sprites easier, as they will rotate around whatever origin they are given, and rotating around the center gives us a uniform spinning motion.

随后,我们将设置 dXdYdA,它们分别表示 xyangle 变量的每秒变化率。After this, we have dX, dY, and dA, which are the per-second rates of change for the x, y, and angle variables respectively.

3.创建构造函数3. Create a constructor

创建 SpriteClass 实例时,我们通过 Game1.cs 为图形设备提供构造函数、纹理相对于项目文件夹的路径以及纹理相对于其原始尺寸的缩放比例。When creating an instance of SpriteClass, we provide the constructor with the graphics device from Game1.cs, the path to the texture relative to the project folder, and the desired scale of the texture relative to its original size. 开始游戏后,我们需要设置“Update”方法中的其他类变量。We’ll set the rest of the class variables after we start the game, in the update method.

public SpriteClass (GraphicsDevice graphicsDevice, string textureName, float scale)
{
  this.scale = scale;
  if (texture == null)
  {
    using (var stream = TitleContainer.OpenStream(textureName))
    {
      texture = Texture2D.FromStream(graphicsDevice, stream);
    }
  }
}

4.更新和绘制4. Update and Draw

我们还需将几个方法添加到 SpriteClass 声明中:There are still a couple of methods we need to add to the SpriteClass declaration:

public void Update (float elapsedTime)
{
  this.x += this.dX * elapsedTime;
  this.y += this.dY * elapsedTime;
  this.angle += this.dA * elapsedTime;
}

public void Draw (SpriteBatch spriteBatch)
{
  Vector2 spritePosition = new Vector2(this.x, this.y);
  spriteBatch.Draw(texture, spritePosition, null, Color.White, this.angle, new Vector2(texture.Width/2, texture.Height/2), new Vector2(scale, scale), SpriteEffects.None, 0f);
}

Game1.cs 中的 Update 方法调用 Update SpriteClass 方法,后者用于根据其各自的变化率更新其子画面的 xyangle 值。The Update SpriteClass method is called in the Update method of Game1.cs, and is used to update the sprites x, y, and angle values based on their respective rates of change.

Game1.cs 中的 Draw 方法调用 Draw 方法,后者用于在游戏窗口中绘制子画面。The Draw method is called in the Draw method of Game1.cs, and is used to draw the sprite in the game window.

用户输入和动画User input and animation

现在,我们已经生成了 SpriteClass,接下来我们将用其创建两个新的游戏对象:第一个是玩家可通过箭头键和空格键控制的头像。Now we have the SpriteClass built, we’ll use it to create two new game objects, The first is an avatar that the player can control with the arrow keys and the space bar. 第二个是玩家必须避让的对象The second is an object that the player must avoid.

1.获取纹理1. Get the textures

对于玩家的头像,我们将使用 Microsoft 独有的忍者神猫,它骑在它信赖的霸王龙身上。For the player’s avatar we’re going to use Microsoft’s very own ninja cat, riding on his trusty t-rex. 单击此处以下载此图像Click here to download the image.

现在就来谈谈玩家需要避让的障碍物。Now for the obstacle that the player needs to avoid. 忍者神猫和肉食恐龙都最讨厌什么呢?What do ninja-cats and carnivorous dinosaurs both hate more than anything? 吃蔬菜!Eating their veggies! 单击此处以下载此图像Click here to download the image.

如同先前的绿色矩形一样,通过 MonoGame 管道将这些图像添加到 Content.mgcb,然后将其分别命名为“ninja-cat-dino.png”和“broccoli.png”。Just as before with the green rectangle, add these images to Content.mgcb via the MonoGame Pipeline, naming them “ninja-cat-dino.png” and “broccoli.png” respectively.

2.添加类变量2. Add class variables

将以下代码添加到 Game1.cs 中的类变量列表中:Add the following code to the list of class variables in Game1.cs:

SpriteClass dino;
SpriteClass broccoli;

bool spaceDown;
bool gameStarted;

float broccoliSpeedMultiplier;
float gravitySpeed;
float dinoSpeedX;
float dinoJumpY;
float score;

Random random;

dinobroccoli 是我们的 SpriteClass 变量。dino and broccoli are our SpriteClass variables. dino 将控制玩家头像,而 broccoli 则控制花椰菜障碍物。dino will hold the player avatar, while broccoli holds the broccoli obstacle.

spaceDown 用于跟踪空格键是被按住还是被按下后再松开。spaceDown keeps track of whether the spacebar is being held down as opposed to pressed and released.

gameStarted 用于告诉我们用户是否已第一次开始游戏。gameStarted tells us whether the user has started the game for the first time.

broccoliSpeedMultiplier 用于确定花椰菜障碍物在屏幕内的移动速度。broccoliSpeedMultiplier determines how fast the broccoli obstacle moves across the screen.

gravitySpeed 用于确定玩家头像在跳跃后加速下降的速度。gravitySpeed determines how fast the player avatar accelerates downward after a jump.

dinoSpeedXdinoJumpY 用于确定玩家头像移动和跳跃的速度。dinoSpeedX and dinoJumpY determine how fast the player avatar moves and jumps. 分数用于跟踪玩家已成功躲避的障碍物数量。score tracks how many obstacles the player has successfully dodged.

最后,random 用于为花椰菜障碍物的行为添加一些随机性。Finally, random will be used to add some randomness to the behavior of the broccoli obstacle.

3.初始化变量3. Initialize variables

接下来,我们需要初始化这些变量。Next we need to initialize these variables. 将以下代码添加到“Initialize”方法中:Add the following code to the Initialize method:

broccoliSpeedMultiplier = 0.5f;
spaceDown = false;
gameStarted = false;
score = 0;
random = new Random();
dinoSpeedX = ScaleToHighDPI(1000f);
dinoJumpY = ScaleToHighDPI(-1200f);
gravitySpeed = ScaleToHighDPI(30f);

请注意,对于高分辨率设备,需要缩放最后三个变量,因为它们是用于指定像素变化率的。Note that the last three variables need to be scaled for high DPI devices, because they specify a rate of change in pixels.

4.生成 SpriteClasses4. Construct SpriteClasses

我们将在 LoadContent 方法中生成 SpriteClass 对象。We will construct SpriteClass objects in the LoadContent method. 将此代码添加到已有对象中:Add this code to what you already have there:

dino = new SpriteClass(GraphicsDevice, "Content/ninja-cat-dino.png", ScaleToHighDPI(1f));
broccoli = new SpriteClass(GraphicsDevice, "Content/broccoli.png", ScaleToHighDPI(0.2f));

花椰菜在游戏中的显示大小比我们期望的大得多,因此,我们需要将其缩小至其原始尺寸的 1/5。The broccoli image is quite a lot larger than we want it to appear in the game, so we’ll scale it down to 0.2 times its original size.

5.设置障碍物行为5. Program obstacle behaviour

我们希望花椰菜在屏幕以外的地方繁殖,并朝玩家头像的方向生长,从而使玩家需要躲避它。We want the broccoli to spawn somewhere offscreen, and head in the direction of the player’s avatar, so they need to dodge it. 要实现此效果,请将此方法添加到 Game1.cs 类:To accomplish this, add this method to the Game1.cs class:

public void SpawnBroccoli()
{
  int direction = random.Next(1, 5);
  switch (direction)
  {
    case 1:
      broccoli.x = -100;
      broccoli.y = random.Next(0, (int)screenHeight);
      break;
    case 2:
      broccoli.y = -100;
      broccoli.x = random.Next(0, (int)screenWidth);
      break;
    case 3:
      broccoli.x = screenWidth + 100;
      broccoli.y = random.Next(0, (int)screenHeight);
      break;
    case 4:
      broccoli.y = screenHeight + 100;
      broccoli.x = random.Next(0, (int)screenWidth);
      break;
  }

  if (score % 5 == 0) broccoliSpeedMultiplier += 0.2f;

  broccoli.dX = (dino.x - broccoli.x) * broccoliSpeedMultiplier;
  broccoli.dY = (dino.y - broccoli.y) * broccoliSpeedMultiplier;
  broccoli.dA = 7f;
}

此方法的第一个部分用于通过两个随机数字来确定花椰菜对象在屏幕以外开始繁殖的点。The first part of the of the method determines what off screen point the broccoli object will spawn from, using two random numbers.

第二部分用于确定花椰菜的生长速度,这由当前的分数决定。The second part determines how fast the broccoli will travel, which is determined by the current score. 玩家每成功躲避 5 个花椰菜,其生长速度就会变得更快。It will get faster for every five broccoli the player successfully dodges.

第三部分用于设置花椰菜子画面的移动方向。The third part sets the direction of the broccoli sprite’s motion. 花椰菜繁殖时,它会朝玩家头像(恐龙)的方向生长。It heads in the direction of the player avatar (dino) when the broccoli is spawned. 此外,我们还将 dA 值设为 7f,使得花椰菜在追逐玩家时在空中旋转。We also give it a dA value of 7f, which will cause the broccoli to spin through the air as it chases the player.

6.设置游戏开始状态6. Program game starting state

在处理键盘输入之前,我们需要一个方法,以便为我们创建的两个对象设置初始游戏状态。Before we can move on to handling keyboard input, we need a method that sets the initial game state of the two objects we’ve created. 除了在应用运行时就立即开始游戏外,我们希望用户通过按空格键来手动开始游戏。Rather than the game starting as soon as the app runs, we want the user to start it manually, by pressing the spacebar. 添加以下代码,以设置动画对象的初始状态并重置分数:Add the following code, which sets the initial state of the animated objects, and resets the score:

public void StartGame()
{
  dino.x = screenWidth / 2;
  dino.y = screenHeight * SKYRATIO;
  broccoliSpeedMultiplier = 0.5f;
  SpawnBroccoli();  
  score = 0;
}

7.处理键盘输入7. Handle keyboard input

接下来,我们需要添加一个新方法,以通过键盘处理用户输入。Next we need a new method to handle user input via the keyboard. 将此方法添加到 Game1.csAdd this method to Game1.cs:

void KeyboardHandler()
{
  KeyboardState state = Keyboard.GetState();

  // Quit the game if Escape is pressed.
  if (state.IsKeyDown(Keys.Escape))
  {
    Exit();
  }

  // Start the game if Space is pressed.
  if (!gameStarted)
  {
    if (state.IsKeyDown(Keys.Space))
    {
      StartGame();
      gameStarted = true;
      spaceDown = true;
      gameOver = false;
    }
    return;
  }            
  // Jump if Space is pressed
  if (state.IsKeyDown(Keys.Space) || state.IsKeyDown(Keys.Up))
  {
    // Jump if the Space is pressed but not held and the dino is on the floor
    if (!spaceDown && dino.y >= screenHeight * SKYRATIO - 1) dino.dY = dinoJumpY;

    spaceDown = true;
  }
  else spaceDown = false;

  // Handle left and right
  if (state.IsKeyDown(Keys.Left)) dino.dX = dinoSpeedX * -1;

  else if (state.IsKeyDown(Keys.Right)) dino.dX = dinoSpeedX;
  else dino.dX = 0;
}

首先,我们需要使用四个 IF 语句:Above we have a series of four if-statements:

如果按下 Escape 键,则第一个将退出游戏。The first quits the game if the Escape key is pressed.

如果在游戏尚未开始时按下空格键,则第二个将会开始游戏。The second starts the game if the Space key is pressed, and the game is not already started.

如果按下空格键,则第三个将会通过更改其 dY 属性来使恐龙头像跳跃。The third makes the dino avatar jump if Space is pressed, by changing its dY property. 请注意,如果不在“地面”上(dino.y = screenHeight * SKYRATIO),则玩家无法跳跃,此外,如果是长按空格键而不是只按一次的话,玩家也无法跳跃。Note that the player cannot jump unless they are on the “ground” (dino.y = screenHeight * SKYRATIO), and will also not jump if the space key is being held down rather than pressed once. 游戏开始后,这将使恐龙无法跳跃,通过按下相同的按键即可开始游戏。This stops the dino from jumping as soon as the game is started, piggybacking on the same keypress that starts the game.

最后,最后一个条件从句用于检查是否按下向左或向右方向箭头,如果是,则相应地更改恐龙的 dX 属性。Finally, the last if/else clause checks if the left or right directional arrows are being pressed, and if so changes the dino’s dX property accordingly.

挑战: 你能否使上面的键盘处理方法能够处理 WASD 输入方案以及箭头键?Challenge: can you make the keyboard handling method above work with the WASD input scheme as well as the arrow keys?

8.为 Update 方法添加逻辑8. Add logic to the Update method

接下来,我们需要将所有这些部分的逻辑添加到 Game1.csUpdate 方法中:Next we need to add logic for all of these parts to the Update method in Game1.cs:

float elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
KeyboardHandler(); // Handle keyboard input
// Update animated SpriteClass objects based on their current rates of change
dino.Update(elapsedTime);
broccoli.Update(elapsedTime);

// Accelerate the dino downward each frame to simulate gravity.
dino.dY += gravitySpeed;

// Set game floor so the player does not fall through it
if (dino.y > screenHeight * SKYRATIO)
{
  dino.dY = 0;
  dino.y = screenHeight * SKYRATIO;
}

// Set game edges to prevent the player from moving offscreen
if (dino.x > screenWidth - dino.texture.Width/2)
{
  dino.x = screenWidth - dino.texture.Width/2;
  dino.dX = 0;
}
if (dino.x < 0 + dino.texture.Width/2)
{
  dino.x = 0 + dino.texture.Width/2;
  dino.dX = 0;
}

// If the broccoli goes offscreen, spawn a new one and iterate the score
if (broccoli.y > screenHeight+100 || broccoli.y < -100 || broccoli.x > screenWidth+100 || broccoli.x < -100)
{
  SpawnBroccoli();
  score++;
}

9.绘制 SpriteClass 对象9. Draw SpriteClass objects

最后,在最后调用 spriteBatch.Draw后,请将以下代码添加到 Game1.csDraw 方法中:Finally, add the following code to the Draw method of Game1.cs, just after the last call of spriteBatch.Draw:

broccoli.Draw(spriteBatch);
dino.Draw(spriteBatch);

在 MonoGame 中,对 spriteBatch.Draw 的新调用将会覆盖任何先前的调用。In MonoGame, new calls of spriteBatch.Draw will draw over any prior calls. 这意味着,花椰菜和恐龙子画面将覆盖现有的草地子画面,因此,无论其位置如何,他们都不会再隐藏在草地后面。This means that both the broccoli and the dino sprite will be drawn over the existing grass sprite, so they can never be hidden behind it regardless of their position.

现在尝试运行游戏,并通过箭头键和空格键移动恐龙。Try running the game now, and moving around the dino with the arrow keys and the spacebar. 如果遵照以上步骤,则你应该能够让你的头像在游戏窗口内移动,且花椰菜的生长速度将会不断加快。If you followed the steps above, you should be able to make your avatar move within the game window, and the broccoli should spawn at an ever-increasing speed.

玩家头像和障碍物

用 SpriteFont 呈现文本Render text with SpriteFont

使用以上代码,我们可以在场景后跟踪玩家的分数,但我们实际上并未告诉玩家分数。Using the code above, we keep track of the player’s score behind the scenes, but we don’t actually tell the player what it is. 此外,在应用启动时,我们还会提供一个相当不直观的介绍 - 玩家将会看到一个蓝色和绿色窗口,但是没法知道需要按空格键才能开始滚动。We also have a fairly unintuitive introduction when the app starts up—the player sees a blue and green window, but has no way of knowing they need to press Space to get things rolling.

若要修复这两个问题,我们将使用名为 SpriteFont 的新型 MonoGame 对象。To fix both these problems, we’re going to use a new kind of MonoGame object called SpriteFonts.

1.创建 SpriteFont 描述文件1. Create SpriteFont description files

在“解决方案资源管理器”中找到“Content”文件夹 。In the Solution Explorer find the Content folder. 在此文件夹中,右键单击“Content.mgcb”文件并选择“打开方式” 。In this folder, Right-Click the Content.mgcb file and select Open With. 从弹出菜单中,选择“MonoGame 管道”,然后按“确定” 。From the popup menu select MonoGame Pipeline, then press OK. 在新窗口中,右键单击“内容”项并选择“添加”->“新建项” 。In the new window, Right-Click the Content item and select Add -> New Item. 选择“SpriteFont 说明”,将其命名为“分数”并按“确定” 。Select SpriteFont Description, name it “Score” and press OK. 然后,通过相同的步骤添加另一个名为“GameState”的 SpriteFont 描述。Then, add another SpriteFont description named “GameState” using the same procedure.

2.编辑描述2. Edit descriptions

右键单击“MonoGame 管道”中的“Content”文件夹,然后选择“打开文件位置” 。Right click the Content folder in the MonoGame Pipeline and select Open File Location. 你应该会看到一个文件夹和刚刚创建的 SpriteFont 描述文件,以及已添加到“Content”文件夹的所有图像。You should see a folder with the SpriteFont description files that you just created, as well as any images you’ve added to the Content folder so far. 现在,你可以关闭并保存 MonoGame 管道窗口。You can now close and save the MonoGame Pipeline window. 通过“文件资源管理器”,在文本编辑器(Visual Studio、NotePad++、Atom 等)中打开两个描述文件 。From the File Explorer open both description files in a text editor (Visual Studio, NotePad++, Atom, etc).

每个描述包含多个用于描述 SpriteFont 的值。Each description contains a number of values that describe the SpriteFont. 我们将进行一些更改:We're going to make a few changes:

Score.spritefont 中,将 值从 12 改为 36。In Score.spritefont, change the value from 12 to 36.

GameState.spritefont 中,将 值从 12 改为 72,并将 值从 Arial 改为 Agency。In GameState.spritefont, change the value from 12 to 72, and the value from Arial to Agency. Agency 是 Windows 10 计算机标配随带的另一种字体,将为我们的介绍屏幕添加别样的风格。Agency is another font that comes standard with Windows 10 machines, and will add some flair to our intro screen.

3.加载 SpriteFont3. Load SpriteFonts

返回 Visual Studio,我们将先为介绍初始屏幕添加新的纹理。Back in Visual Studio, we’re first going to add a new texture for the intro splash screen. 单击此处以下载此图像Click here to download the image.

如同先前一样,右键单击“内容”并选择“添加”->“现有项”为项目添加纹理 。As before, add the texture to the project by right-clicking the Content and selecting Add -> Existing Item. 将此新项命名为“start-splash.png”。Name the new item “start-splash.png”.

接下来,将以下类变量添加到 Game1.csNext, add the following class variables to Game1.cs:

Texture2D startGameSplash;
SpriteFont scoreFont;
SpriteFont stateFont;

然后将这些行添加到 LoadContent 方法:Then add these lines to the LoadContent method:

startGameSplash = Content.Load<Texture2D>("start-splash");
scoreFont = Content.Load<SpriteFont>("Score");
stateFont = Content.Load<SpriteFont>("GameState");

4.绘制分数4. Draw the score

转到 Game1.csDraw 方法,并将以下代码添加到 spriteBatch.End(); 前面Go to the Draw method of Game1.cs and add the following code just before spriteBatch.End();

spriteBatch.DrawString(scoreFont, score.ToString(),
new Vector2(screenWidth - 100, 50), Color.Black);

以上代码使用我们创建的子画面描述(Arial 36 号)在屏幕右上角附近绘制玩家的当前分数。The code above uses the sprite description we created (Arial Size 36) to draw the player’s current score near the top right corner of the screen.

5.绘制水平居中的文本5. Draw horizontally centered text

制作游戏时,你经常需要绘制水平居中或垂直居中的文本。When making a game, you will often want to draw text that is centered, either horizontally or vertically. 若要将介绍文本水平居中,请将此代码添加到 spriteBatch.End(); 之前的 Draw 方法中。To horizontally center the introductory text, add this code to the Draw method just before spriteBatch.End();

if (!gameStarted)
{
  // Fill the screen with black before the game starts
  spriteBatch.Draw(startGameSplash, new Rectangle(0, 0,
  (int)screenWidth, (int)screenHeight), Color.White);

  String title = "VEGGIE JUMP";
  String pressSpace = "Press Space to start";

  // Measure the size of text in the given font
  Vector2 titleSize = stateFont.MeasureString(title);
  Vector2 pressSpaceSize = stateFont.MeasureString(pressSpace);

  // Draw the text horizontally centered
  spriteBatch.DrawString(stateFont, title,
  new Vector2(screenWidth / 2 - titleSize.X / 2, screenHeight / 3),
  Color.ForestGreen);
  spriteBatch.DrawString(stateFont, pressSpace,
  new Vector2(screenWidth / 2 - pressSpaceSize.X / 2,
  screenHeight / 2), Color.White);
  }

首先,我们需要创建两个字符串,分别代表我们想要绘制的文本的每一条线。First we create two Strings, one for each line of text we want to draw. 接下来,我们需要使用 SpriteFont.MeasureString(String) 方法测量每行的打印宽度和高度。Next, we measure the width and height of each line when printed, using the SpriteFont.MeasureString(String) method. 这将为我们提供如 Vector2 对象一样的尺寸,其中 X 属性包含其宽度,Y 属性包含其高度。This gives us the size as a Vector2 object, with the X property containing its width, and Y its height.

最后,我们绘制每条线。Finally, we draw each line. 若要将文本水平居中,我们需要使其位置矢量的 X 值等于 screenWidth / 2 - textSize.X / 2To center the text horizontally, we make the X value of its position vector equal to screenWidth / 2 - textSize.X / 2.

挑战: 你应该对以上步骤做出哪些调整才能对文本进行垂直及水平居中?Challenge: how would you change the procedure above to center the text vertically as well as horizontally?

尝试运行游戏。Try running the game. 你看到介绍初始屏幕了吗?Do you see the intro splash screen? 花椰菜每繁殖一次,分数是否就递增了呢?Does the score count up each time the broccoli respawns?

介绍初始屏幕

碰撞检测Collision detection

现在,我们已经让花椰菜围绕在你周围,并且花椰菜繁殖一次分数就会递增,但实际上没有办法输掉游戏。So we have a broccoli that follows you around, and we have a score that ticks up each time a new one spawns—but as it is there is no way to actually lose this game. 我们需要使用一种方法来知道恐龙和花椰菜子画面是否碰撞,以宣布游戏结束。We need a way to know if the dino and broccoli sprites collide, and if when they do, to declare the game over.

1.获取纹理1. Get the textures

我们所需的最后一张图像是“游戏结束”的图像。The last image we need, is one for “game over”. 单击此处以下载此图像Click here to download the image.

与前面的绿色矩形忍者神猫和花椰菜图像一样,通过 MonoGame 管道将此图像添加到 Content.mgcb,然后将其命名为“game-over.png”。Just as before with the green rectangle, ninja-cat and broccoli images, add this image to Content.mgcb via the MonoGame Pipeline, naming it “game-over.png”.

2.矩形碰撞2. Rectangular collision

在游戏中检测到碰撞时,对象往往会被简化,以降低所涉及的数学知识的复杂性。When detecting collisions in a game, objects are often simplified to reduce the complexity of the math involved. 我们会将玩家头像和花椰菜障碍视作矩形,以检测两者之间是否存在碰撞。We are going to treat both the player avatar and broccoli obstacle as rectangles for the purpose of detecting collision between them.

打开 SpriteClass.cs 并添加新的类变量:Open SpriteClass.cs and add a new class variable:

const float HITBOXSCALE = .5f;

此值表示碰撞检测的“宽松”程度(对玩家来说)。This value will represent how “forgiving” the collision detection is for the player. 如果值为 .5f,则恐龙可能与花椰菜碰撞的矩形边沿(常称为“有效命中体”)为全尺寸纹理的一半。With a value of .5f, the edges of the rectangle in which the dino can collide with the broccoli—often call the “hitbox”—will be half of the full size of the texture. 这会导致出现少数以下实例:两个纹理的角发生碰撞,但图像的所有部分实际上并未发生任何接触。This will result in few instances where the corners of the two textures collide, without any parts of the images actually appearing to touch. 可以根据你自己的喜好随意调整此值。Feel free to tweak this value to your personal taste.

接下来,为 SpriteClass.cs 添加新的方法:Next, add a new method to SpriteClass.cs:

public bool RectangleCollision(SpriteClass otherSprite)
{
  if (this.x + this.texture.Width * this.scale * HITBOXSCALE / 2 < otherSprite.x - otherSprite.texture.Width * otherSprite.scale / 2) return false;
  if (this.y + this.texture.Height * this.scale * HITBOXSCALE / 2 < otherSprite.y - otherSprite.texture.Height * otherSprite.scale / 2) return false;
  if (this.x - this.texture.Width * this.scale * HITBOXSCALE / 2 > otherSprite.x + otherSprite.texture.Width * otherSprite.scale / 2) return false;
  if (this.y - this.texture.Height * this.scale * HITBOXSCALE / 2 > otherSprite.y + otherSprite.texture.Height * otherSprite.scale / 2) return false;
  return true;
}

此方法用于检测两个矩形对象是否发生了碰撞。This method detects if two rectangular objects have collided. 此算法通过测试来确定矩形的任一面(或多个面)之间是否存在间隙。The algorithm works by testing to see if there is a gap between any of the sides of the rectangles. 如果存在间隙,则没有发生碰撞;如果没有间隙,则必定发生了碰撞。If there is any gap, there is no collision—if no gap exists, there must be a collision.

3.加载新纹理3. Load new textures

然后,打开 Game1.cs 并添加两个新的类变量,一个用于存储游戏结束子画面纹理,另一个布尔类变量用于跟踪游戏状态:Then, open Game1.cs and add two new class variables, one to store the game over sprite texture, and a Boolean to track the game’s state:

Texture2D gameOverTexture;
bool gameOver;

然后,初始化 Initialize 方法中的 gameOverThen, initialize gameOver in the Initialize method:

gameOver = false;

最后,将纹理加载到 LoadContent 方法的 gameOverTexture 中:Finally, load the texture into gameOverTexture in the LoadContent method:

gameOverTexture = Content.Load<Texture2D>("game-over");

4.实现“游戏结束”逻辑4. Implement “game over” logic

调用 KeyboardHandler 方法之后,将此代码添加到 Update 方法:Add this code to the Update method, just after the KeyboardHandler method is called:

if (gameOver)
{
  dino.dX = 0;
  dino.dY = 0;
  broccoli.dX = 0;
  broccoli.dY = 0;
  broccoli.dA = 0;
}

这将导致在游戏结束时所有运动均停止,因此,恐龙和花椰菜子画面将冻结在其当前位置。This will cause all motion to stop when the game has ended, freezing the dino and broccoli sprites in their current positions.

接下来,在 Update 方法末尾,在 base.Update(gameTime) 前面添加此行:Next, at the end of the Update method, just before base.Update(gameTime), add this line:

if (dino.RectangleCollision(broccoli)) gameOver = true;

这将调用我们在 SpriteClass 中创建的 RectangleCollision 方法,如果返回 True,则会将游戏标记为结束。This calls the RectangleCollision method we created in SpriteClass, and flags the game as over if it returns true.

5.添加用于重置游戏的用户输入5. Add user input for resetting the game

将此代码添加到 KeyboardHandler 方法,使用户能够在按 Enter 时重置游戏:Add this code to the KeyboardHandler method, to allow the user to reset the game if they press Enter:

if (gameOver && state.IsKeyDown(Keys.Enter))
{
  StartGame();
  gameOver = false;
}

6.绘制游戏结束初始屏幕和文本6. Draw game over splash and text

最后,在首次调用 spriteBatch.Draw 后(此调用应用于绘制草地纹理),将此代码添加至“Draw”方法。Finally, add this code to the Draw method, just after the first call of spriteBatch.Draw (this should be the call that draws the grass texture).

if (gameOver)
{
  // Draw game over texture
  spriteBatch.Draw(gameOverTexture, new Vector2(screenWidth / 2 - gameOverTexture.Width / 2, screenHeight / 4 - gameOverTexture.Width / 2), Color.White);

  String pressEnter = "Press Enter to restart!";

  // Measure the size of text in the given font
  Vector2 pressEnterSize = stateFont.MeasureString(pressEnter);

  // Draw the text horizontally centered
  spriteBatch.DrawString(stateFont, pressEnter, new Vector2(screenWidth / 2 - pressEnterSize.X / 2, screenHeight - 200), Color.White);
}

在这里,我们将使用与前面相同的方法来绘制水平居中的文本(重复使用我们在介绍初始屏幕中使用过的字体),以及将 gameOverTexture 置于窗口的上半部中间。Here we use the same method as before to draw text horizontally centered (reusing the font we used for the intro splash), as well as centering gameOverTexture in the top half of the window.

已完成!And we’re done! 尝试再次运行游戏。Try running the game again. 如果已遵循以上步骤,则在恐龙与花椰菜发生碰撞时,游戏应该就会结束,并且系统会提示玩家按 Enter 键重新开始游戏。If you followed the steps above, the game should now end when the dino collides with the broccoli, and the player should be prompted to restart the game by pressing the Enter key.

游戏结束

发布到 Microsoft StorePublish to the Microsoft Store

此游戏是作为一款 UWP 应用创建的,因此,我们可以将此项目发布到 Microsoft Store。Because we built this game as a UWP app, it is possible to publish this project to the Microsoft Store. 此流程包含几个步骤。There are a few steps to the process.

必须以 Windows 开发人员的身份注册You must be registered as a Windows Developer.

必须使用应用提交清单You must use the app submission checklist.

必须将此应用提交以进行认证The app must be submitted for certification.

有关更多详细信息,请参阅发布 UWP 应用For more details, see Publishing your UWP app.