Windows 8 游戏(SkyWar)为 Intel App Up 使用 MonoGame Framework 的旅程






4.91/5 (10投票s)
本文将详细介绍我为 CodeProject 的 Intel AppUp 比赛开发 SkyWar 游戏的经历。
介绍
本文将详细介绍我为 CodeProject 的 Intel AppUp 比赛开发 SkyWar 游戏的经历。您可以下载文章中附带的完整游戏源代码。该游戏目前正在微软商店进行验证阶段,您很快就能在商店中看到适用于 Windows Phone 7.5 Mango 和 Windows Phone 8 的相同游戏。该游戏可免费下载。
背景
我偶然发现了 Intel 应用创新大赛,并决定参加此次比赛。比赛要求创建一个能展示 Intel Ultrabook 最佳特性的 Windows 8 应用。我思考了许多想法,最终决定创建一个 Windows 8 游戏。
Ultrabook 配备了触摸、传感器、GPS 和 Smart Connect 等功能。因此,我构思了一个充分利用触摸和传感器,并具有良好用户体验的游戏。我对 XNA 框架略知一二。我认为使用 XNA 可以创建 Windows 8 游戏应用。但 Windows 8 不再支持 XNA,而 Windows Phone 8.0 SDK 支持。因此,我开始寻找替代方案,并找到了 Bob 发布的这篇优秀的文章。我发现使用 Mono Game 框架,我们可以轻松创建 Windows 8 游戏应用,甚至可以免费将游戏移植到 Android 和 iPhone。最棒的是,它与 XNA 完全
相同。
游戏故事线
在编写游戏之前,我们应该清楚我们要做什么。游戏的目的是什么,游戏将如何进行,对玩家的挑战是什么,结束的标准是什么等等。
最终,我构思了一个名为 Sky War 的太空射击游戏,其灵感来源于老式的街机游戏。
这款游戏允许玩家在一个星空中控制一架太空战斗机。
玩家将在银河系中面对不同的敌人。在每个关卡中,玩家将面对一组敌人,击败他们后,将面对关卡的 Boss 或龙形敌人。
玩家开始时将拥有 3 条初始生命线和 100% 的生命值。但后来为了击败如此危险的敌人,玩家将获得游戏中令人惊叹的生命线。
玩家玩得越多,游戏就会变得越复杂,出现更难的龙形或 Boss 敌人。
我们应该有更详细的故事线,例如纹理做什么,有多少对象等等。在这篇文章中,我将只简要介绍故事。
设置环境
在开始为 Windows 8 构建游戏之前,了解 Mono Game Framework 以及如何设置游戏创建环境会很有帮助。
MonoGame 为希望将代码移植到非微软平台的 XNA 开发者提供了跨平台的 XNA Framework 实现。您可以阅读关于 Mono Game 的 Mono Game 概述。让我快速跳到环境设置。
如何设置 Mono Game 环境?
有两种设置环境的方法,我将解释一种简单的方法。
1) 安装 Visual Studio 2010。在其上安装服务包 SP1(这是安装 Windows Mobile SDK 7.1 所必需的)。
2) 安装 Windows Mobile SDK 7.1,它将自动在您的计算机上安装 Xna Game Studio 4.0。(您可能想知道为什么我们安装了 VS 2010,因为 VS 2012 及以后版本没有 XNA 模板,并且 MonoGame 框架没有内容管道项目,而内容管道项目是在 XNA 中预编译图形和音频以供运行时使用的步骤)。
3) 安装 Visual Studio 2012。
4) 安装 Mono Game 安装程序,您可以从此处下载。
这将为您设置好环境,并在 Visual Studio 2010 和 2012 中创建 MonoGame 模板。
第二种方法您可以在此处阅读。现在我们已经准备好为 Windows 8 创建我们的第一个 Mono Game 了。
开始吧
在创建任何游戏之前,我们应该准备好以下这些东西:
1) 游戏故事线(游戏详细描述、关卡规划等)
2) 纹理(我们在游戏中使用的任何图像、声音等)
3) 游戏状态管理
上面我提供了简要的游戏故事,但对于游戏,我们应该有一个详细的故事线。后来我花了大量时间从不同的免费网站收集适合游戏的纹理。我在 Photoshop 中根据我的需求修改了它们。
如果您是初学者,对 XNA 完全不了解,请参考本文底部的链接和书籍。
准备好所有这些之后,我们就可以开始编码了。
让我们打开 Visual Studio 2012 并选择 Mono Game 项目模板。将您的项目命名为 Sky War。在这里我们应该注意到,我们没有内容项目。处理此问题的最佳方法是,在您的游戏中创建一个 Content 文件夹。然后打开 Visual Studio 2010,将所有您想在游戏中使用的纹理添加到其中,构建项目,然后将其添加到 Sky War 游戏的内容文件夹中。现在将内容文件夹中所有纹理的“如果较新则复制”设置为 true。这就是我们处理 Mono Game 框架中没有内容项目的方式。
现在我们可以开始为游戏的每个对象编写类了。下面附加的截图将为您提供游戏的大致概念,我们将开发出什么——
查看图片后,我们可以识别出以下游戏对象——
滚动背景(用于创建移动的背景对象)、飞船(玩家在银河系中驾驶的飞船)、子弹(玩家和外星人都会使用)、外星人(敌方飞船)、外星人组件(屏幕上可见的外星人数量及重新初始化)、外星人 Boss(关卡 Boss)、动画精灵(用于纹理动画)、音频库(用于播放音频)、外星人类型(游戏中有不同类型、不同能力的外星人)、关卡数据(关卡信息)、强化道具(玩家当前拥有的强化道具),以及初始化一切的主游戏屏幕。我将逐一讨论最难的代码部分,其余代码相对容易理解。您可以下载附加的源代码。
微软在其网站上提供了大量的游戏状态管理教程和代码,您可以在这里找到。因此,我们无需在此讨论。我快速点进去下载了代码。我对其进行了修改,使其同时支持触摸和键盘。您可以在文章中找到附加的类。我们将重点关注对象。
滚动背景(移动背景)
我开始编写代码,创建一个移动的背景。当您运行游戏并仔细观察时,背景有两个图像层叠在一起,第二个图像在第一个图像上方不断移动。这给用户一种玩家正在银河系中移动的感觉。当用户触摸油门按钮或按下键盘上的向上键时,就会发生这种情况。下面的图片会让您对这一点有一个很好的了解。
这里我们有两个图像,背景图像和与之平行的图像,它将覆盖在背景图像之上。我使用了下面的代码来创建移动背景。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
public class Background
{
Texture2D t2dBackground, t2dParallax;
GameData _gameData = null;
int iViewportWidth = 800;
int iViewportHeight = 600;
int iBackgroundWidth = 1600;
int iBackgroundHeight = 1080;
int iParallaxWidth = 1600;
int iParallaxHeight = 1080;
int iBackgroundOffset;
int iParallaxOffset;
public int BackgroundOffset
{
get { return iBackgroundOffset; }
set
{
iBackgroundOffset = value;
if (iBackgroundOffset < 0)
{
iBackgroundOffset += iBackgroundHeight;
}
if (iBackgroundOffset > iBackgroundHeight)
{
iBackgroundOffset -= iBackgroundHeight;
}
}
}
public int ParallaxOffset
{
get { return iParallaxOffset; }
set
{
iParallaxOffset = value;
if (iParallaxOffset < 0)
{
iParallaxOffset += iParallaxHeight;
}
if (iParallaxOffset > iParallaxHeight)
{
iParallaxOffset -= iParallaxHeight;
}
}
}
bool drawParallax = true;
public bool DrawParallax
{
get { return drawParallax; }
set { drawParallax = value; }
}
public Background(ContentManager content, string sBackground, string sParallax)
{
_gameData = GameData.GetGameDataInstance();
iViewportHeight = _gameData.GameScreenHeight;
iViewportWidth = _gameData.GameScreenWidth;
t2dBackground = content.Load<Texture2D>(sBackground);
_gameData.BackgroundWidth = iBackgroundWidth = t2dBackground.Width;
_gameData.BackgroundHeight = iBackgroundHeight = t2dBackground.Height;
t2dParallax = content.Load<Texture2D>(sParallax);
iParallaxWidth = t2dParallax.Width;
iParallaxHeight = t2dParallax.Height;
}
public Background(ContentManager content, string sBackground)
{
_gameData = GameData.GetGameDataInstance();
iViewportHeight = _gameData.GameScreenHeight;
iViewportWidth = _gameData.GameScreenWidth;
t2dBackground = content.Load<Texture2D>(sBackground);
_gameData.BackgroundWidth = iBackgroundWidth = t2dBackground.Width;
_gameData.BackgroundHeight = iBackgroundHeight = t2dBackground.Height;
t2dParallax = t2dBackground;
iParallaxWidth = t2dParallax.Width;
iParallaxHeight = t2dParallax.Height;
drawParallax = false;
}
public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(t2dBackground, new Rectangle(0, -1 *
iBackgroundOffset, iViewportWidth, iBackgroundHeight), Color.White);
if (iBackgroundOffset > iBackgroundHeight - iViewportHeight)
{
spriteBatch.Draw(t2dBackground, new Rectangle(0, (-1 * iBackgroundOffset) +
iBackgroundHeight, iViewportWidth, iBackgroundHeight), Color.White);
}
if (drawParallax)
{
spriteBatch.Draw(t2dParallax, new Rectangle(0, -1 * iParallaxOffset,
iViewportWidth, iParallaxHeight), Color.SlateGray);
if (iParallaxOffset > iParallaxHeight - iViewportHeight)
{
spriteBatch.Draw(t2dParallax, new Rectangle(0, (-1 * iParallaxOffset) +
iParallaxHeight, iViewportWidth, iParallaxHeight), Color.SlateGray);
}
}
}
}
}
这里,iViewportWidth
和 iViewportHeight
是游戏宽度和高度。iBackgroundWidth
和 iBackgroundHeight
是主背景的宽度和高度。iParallaxWidth
和 iParallaxHeight
是覆盖在其上的图像的高度。
除了变量之外,我还有 BackgroundOffset
和 ParallalxOffset
属性,可以从类外部设置它们。您会注意到上面的两个属性会检查我们是否已经滚出了每张纹理的任一端,并在必要时进行环绕。如果我们不这样做,背景图像的滚动就会超出屏幕。
当使用此构造函数创建 Background 类的实例时,我们将传入一个内容管理器并使用它来加载两张纹理。我们的构造函数将 iBackgroundWidth
、iBackgroundHeight
、iParallaxWidth
和 iParallaxHeight
设置为从我们加载的纹理中获取的相应值。
我们的 Draw 方法接收一个 SpriteBatch
对象供其使用,并且我们假设它位于 SpriteBatch.Begin
和 SpriteBatch.End
调用块内。
我们的第一个语句绘制背景图像,并根据背景偏移量进行偏移。
spriteBatch.Draw(t2dBackground, new Rectangle(0, -1 * iBackgroundOffset,
iViewportWidth, iBackgroundHeight), Color.White);
当我们创建目标矩形时,我们将左侧位置设置为“-1 * iBackgroundOffset
”,这会将我们的图像向左移动 iBackgroundOffset
像素。
这效果很好,除了当我们滚到绘制偏移图像无法完全填满整个显示区域时。如果我们不考虑这一点,我们最终会得到一个部分填充的背景,然后是 XNA 的蓝色窗口。这时下一条语句就派上用场了。
// If the right edge of the background panel will end within the bounds of
// the display, draw a second copy of the background at that location.
if (iBackgroundOffset > iBackgroundHeight - iViewportHeight)
{
spriteBatch.Draw(t2dBackground, new Rectangle(0, (-1 * iBackgroundOffset) +
iBackgroundHeight, iViewportWidth, iBackgroundHeight), Color.White);
}
首先,我们检查是否需要绘制图像的第二个副本。如果需要,我们重复上面的绘制调用,只是我们通过将背景图像的高度加到调用中来修改第二个目标矩形的位置。这将使第二个图像副本精确地对齐在第一个副本结束的位置。我们永远不需要绘制超过两个这样的图像来填满屏幕,因为背景图像的高度大于屏幕的高度。我们绘制函数的其余部分与视差星空叠加层执行完全相同的过程,但在检查是否应该绘制它之后。它使用与背景相同的偏移和第二个副本绘制逻辑。接下来,我们需要通过运行其构造函数来实际初始化背景。在我们游戏的 LoadContent
方法中,添加以下代码:
background = new Background(Content,@"Textures\PrimaryBackground",@"Textures\ParallaxStars");
这里,我们调用 Background 对象的构造函数,并将我们的 Content Manager
对象以及我们将要使用的两张纹理的资源名称传递给它。
spriteBatch.Begin
和 spriteBatch.End
块内的绘制代码之前添加以下行:
background.Draw(spriteBatch);
玩家(飞船)
我面临的一大挑战是为各种 Windows 8 设备(触摸屏和非触摸屏设备)提供游戏支持。Ultrabook 拥有出色的触摸驱动程序,可以优雅地与 Windows 8 配合使用,用户甚至无需更改任何代码。下面是玩家类的代码。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class SpaceShip
{
GameData _gameData;
AnimatedSprite asSprite;
Texture2D t2dSpaceShip;
SoundEffectInstance spaceShipDead;
bool sActive;
public bool IsSpaceShipActive { get { return sActive; } private set { sActive = value; } }
int iX = 604;
int iY = 260;
int iFacing = 1;
bool bThrusting = false;
int iScrollRate = 0;
int iShipAccelerationRate = 1;
int iShipVerticalMoveRate = 3;
float fSpeedChangeCount = 0.0f;
float fSpeedChangeDelay = 0.1f;
float fVerticalChangeCount = 0.0f;
float fVerticalChangeDelay = 0.01f;
TimeSpan _deathDelay, _deathCountDelay;
float[] fFireRateDelay = new float[3] { 0.15f, 0.1f, 0.05f };
float fSuperBombDelayTimer = 2f;
int iMaxSuperBombs = 5;
int iMaxWeaponLevel = 1;
int iShipMaxFireRate = 2;
int iMaxAccelerationModifier = 5;
int iSuperBombs = 0;
int iWeaponLevel = 0;
int iWeaponFireRate = 0;
int iAccelerationModifier = 1;
public int X
{
get { return iX; }
set { iX = value; }
}
public int Y
{
get { return iY; }
set { iY = value; }
}
public int Facing
{
get { return iFacing; }
set { iFacing = value; }
}
public bool Thrusting
{
get { return bThrusting; }
set { bThrusting = value; }
}
public int ScrollRate
{
get { return iScrollRate; }
set { iScrollRate = value; }
}
public int AccelerationRate
{
get { return iShipAccelerationRate; }
set { iShipAccelerationRate = value; }
}
public int VerticalMovementRate
{
get { return iShipVerticalMoveRate; }
set { iShipVerticalMoveRate = value; }
}
public float SpeedChangeCount
{
get { return fSpeedChangeCount; }
set { fSpeedChangeCount = value; }
}
public float SpeedChangeDelay
{
get { return fSpeedChangeDelay; }
set { fSpeedChangeDelay = value; }
}
public float VerticalChangeCount
{
get { return fVerticalChangeCount; }
set { fVerticalChangeCount = value; }
}
public float VerticalChangeDelay
{
get { return fVerticalChangeDelay; }
set { fVerticalChangeDelay = value; }
}
public int ShipWidth { get; private set; }
public int ShipHeight { get; private set; }
public Rectangle BoundingBox
{
get { return new Rectangle(iX, iY, _gameData.SpaceShipWidth, _gameData.SpaceShipHeight); }
}
public int SuperBombs
{
get { return iSuperBombs; }
set
{
iSuperBombs = (int)MathHelper.Clamp(value,
0, iMaxSuperBombs);
}
}
public int FireRate
{
get { return iWeaponFireRate; }
set
{
iWeaponFireRate = (int)MathHelper.Clamp(value,
0, iShipMaxFireRate);
}
}
public float FireDelay
{
get { return fFireRateDelay[iWeaponFireRate]; }
}
public int WeaponLevel
{
get { return iWeaponLevel; }
set
{
iWeaponLevel = (int)MathHelper.Clamp(value,
0, iMaxWeaponLevel);
}
}
public float SuperBombDelay
{
get { return fSuperBombDelayTimer; }
}
public int AccelerationBonus
{
get { return iAccelerationModifier; }
set
{
iAccelerationModifier = (int)MathHelper.Clamp(value,
1, iMaxAccelerationModifier);
}
}
public SpaceShip(ContentManager content, string texture)
{
_gameData = GameData.GetGameDataInstance();
t2dSpaceShip = content.Load<texture2d>(texture);
asSprite = new AnimatedSprite(t2dSpaceShip, 0, 0, t2dSpaceShip.Width, t2dSpaceShip.Height, 1);
ShipWidth = t2dSpaceShip.Width;
ShipHeight = t2dSpaceShip.Height;
iX = _gameData.GameScreenWidth / 2 - t2dSpaceShip.Width / 2;
iY = _gameData.GameScreenHeight / 2 + _gameData.GameScreenHeight / 4 + _gameData.GameScreenHeight / 6;
asSprite.IsAnimating = false;
sActive = true;
spaceShipDead = _gameData.AudioLibarary.BossDead;
}
public void ResetSpaceShip()
{
iX = _gameData.GameScreenWidth / 2 - t2dSpaceShip.Width / 2;
iY = _gameData.GameScreenHeight / 2 + _gameData.GameScreenHeight / 4 + _gameData.GameScreenHeight / 6;
iAccelerationModifier = 1;
iWeaponFireRate = 0;
iWeaponLevel = 0;
iSuperBombs = 0;//(int)MathHelper.Max(1, iSuperBombs);
iScrollRate = 0;
}
public void PlayerHit(int power = 1)
{
_gameData.PlayerHealth -= 20;
if (_gameData.PlayerHealth == 0)
{
spaceShipDead.Play();
_gameData.PlayerHealth = 0;
sActive = false;
_gameData.PlayerLives--;
if (_gameData.PlayerLives == 0)
_gameData.IsGameOver = true;
else
{
_deathDelay = TimeSpan.FromSeconds(3);
_deathCountDelay = TimeSpan.Zero;
}
}
else if (_gameData.PlayerHealth < 0)
_gameData.PlayerHealth = 0;
}
public void Update(GameTime gametime)
{
if ((!sActive) && _deathDelay > TimeSpan.Zero)
{
if ((_deathCountDelay += gametime.ElapsedGameTime) > _deathDelay)
{
sActive = true;
_gameData.PlayerHealth = 100;
ResetSpaceShip();
_deathDelay = TimeSpan.Zero;
}
}
}
public void Draw(SpriteBatch sb)
{
if (sActive)
asSprite.Draw(sb, iX, iY, false);
}
}
}
AnimatedSprite
当然将用于容纳上面的图像。我们将使用我们的动画精灵类的 bAnimating
功能来防止它自动动画,以便我们可以控制显示哪个帧的飞船“动画”。
iX
和 iY
确定飞船在屏幕上的位置。在本教程系列我们将开发的游戏中,飞船(在水平方向上)将永远不会离开屏幕中心,但会在垂直方向上自由移动。
iFacing
确定玩家当前面向的方向。0=右,1=左。
bThrusting
在玩家积极向某个方向移动时(而不是在该方向上滑行)设置为 true。
接下来的几个变量都与我们的飞船/屏幕的移动有关。我们将在稍后将飞船添加到屏幕并处理移动时进一步详细介绍。
iScrollRate
确定飞船实际移动的速度和方向(这与 Facing 无关,因为有可能朝一个方向移动但面向另一个方向)。正值表示向右移动,负值表示向左移动。数字的大小决定了屏幕每帧滚动的像素数。
iShipAccelerationRate
设置 iScrollRate
可以改变的速度。值为 1 意味着每次速度变化时,它都会变化 1。
当玩家按下游戏手柄/键盘上的向上或向下键时,iShipVerticalMovementRate
是飞船垂直移动的像素数。
fSpeedChangeCount
和 fSpeedChangeDelay
确定 iShipAccelerationRate
可以应用的速率。fSpeedChangeCount
累积自上次速度变化以来的时间。当它大于 fSpeedChangeDelay
时,允许速度变化,并且如果发生变化,它将被重置为 0。
BoundingBox
属性仅返回一个基于我们飞船位置和大小的新矩形。我们将为游戏中的其他对象添加类似的属性,其中一些比这个更复杂,以考虑对象在“世界地图”中的位置。
fBoardUpdateDelay
和 fBoardUpdateInterval
将用于控制屏幕整体滚动的速度(如果我们仅依赖 Update 调用,可能会出现速度不一致的情况)。
类的构造函数和 Draw 方法非常简单;我们在构造函数中传入纹理,在 Draw 方法中传入 SpriteBatch。
我们现在可以将其集成到我们的游戏中。通过传递内容管理器和纹理路径来创建玩家对象的方法如下。
spaceShipObj = new SpaceShip(content, "images/plane");
现在我们需要根据传感器更新玩家飞船(当用户倾斜 Ultrabook 或使用键盘时,玩家的计划应该移动)。因此,我们将在游戏的 update 方法中编写更新代码。
spaceShipObj.Update(gameTime);
float gameElapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (spaceShipObj.IsSpaceShipActive)
{
float movement = 0.0f;
if (System.Math.Abs(acceloVelocity.X) > accelThreshold)
{
movement = acceloVelocity.X * acceleoSpeed;
}
//update spaceship postision
spaceShipObj.X += (int)movement;
spaceShipObj.Update(gameTime);
if (spaceShipObj.X <= iPlayAreaLeft)
spaceShipObj.X = iPlayAreaLeft;
if (spaceShipObj.X + spaceShipObj.ShipWidth >= iPlayAreaRight)
spaceShipObj.X = iPlayAreaRight - spaceShipObj.ShipWidth;
spaceShipObj.SpeedChangeCount += gameElapsedTime;
spaceShipObj.VerticalChangeCount += gameElapsedTime;
//check if gas or fire is touched or not
TouchCollection touchCollection = TouchPanel.GetState();
foreach (TouchLocation tl in touchCollection)
{
if ((tl.State == TouchLocationState.Pressed) || (tl.State == TouchLocationState.Moved))
{
//check gas
if (GasTouchBound.Intersects(new Rectangle((int)tl.Position.X, (int)tl.Position.Y, 80, 80)))
{
_touchGasFlag = true;
if (spaceShipObj.SpeedChangeCount > spaceShipObj.SpeedChangeDelay)
CheckVerticalMovementKeys(Keyboard.GetState(), GamePad.GetState(PlayerIndex.One));
}
else
UpdateShipSpeed();
//check fire
if (FireTouchBound.Intersects(new Rectangle((int)tl.Position.X, (int)tl.Position.Y, 80, 80)))
{
CheckOtherKeys();
}
}
}
if (touchCollection.Count == 0)
{
UpdateShipSpeed();
}
//give keyboard support
KeyboardState keyboardState = Keyboard.GetState();
if (spaceShipObj.SpeedChangeCount > spaceShipObj.SpeedChangeDelay)
{
if (keyboardState.IsKeyDown(Keys.Up))
CheckVerticalMovementKeys(Keyboard.GetState(), GamePad.GetState(PlayerIndex.One));
else
UpdateShipSpeed();
}
if (spaceShipObj.VerticalChangeCount > spaceShipObj.VerticalChangeDelay)
{
CheckHorizontalMovementKeys(keyboardState, GamePad.GetState(PlayerIndex.One));
}
if (keyboardState.IsKeyDown(Keys.Space))
CheckOtherKeys();
}
Update 方法的支持方法
protected void CheckVerticalMovementKeys(KeyboardState ksKeys, GamePadState gsPad)
{
bool bResetTimer = false;
spaceShipObj.Thrusting = false;
if (spaceShipObj.ScrollRate > -iMaxHorizontalSpeed)
{
spaceShipObj.ScrollRate -= spaceShipObj.AccelerationRate;
if (spaceShipObj.ScrollRate < -iMaxHorizontalSpeed)
spaceShipObj.ScrollRate = -iMaxHorizontalSpeed;
bResetTimer = true;
}
spaceShipObj.Thrusting = true;
spaceShipObj.Facing = 1;
if (bResetTimer)
spaceShipObj.SpeedChangeCount = 0.0f;
}
protected void UpdateShipSpeed()
{
if (_touchGasFlag)
{
spaceShipObj.ScrollRate = spaceShipObj.ScrollRate + 1;
if (spaceShipObj.ScrollRate >= iMinHorizontalSpeen)
spaceShipObj.ScrollRate = iMinHorizontalSpeen;
}
}
protected void CheckHorizontalMovementKeys(KeyboardState ksKeys, GamePadState gsPad)
{
if (ksKeys.IsKeyDown(Keys.Left))
{
spaceShipObj.X -= 5;
if (spaceShipObj.X < iPlayAreaLeft)
spaceShipObj.X = iPlayAreaLeft;
}
if (ksKeys.IsKeyDown(Keys.Right))
{
spaceShipObj.X += 5;
if (spaceShipObj.X + spaceShipObj.ShipWidth > iPlayAreaRight)
spaceShipObj.X = iPlayAreaRight - spaceShipObj.ShipWidth;
}
}
上面的代码用于在玩家触摸、倾斜 Ultrabook 或按下导航箭头键时更新玩家飞船。TouchCollection 对象将为您提供用户触摸的所有位置。我们可以对其进行循环,并快速识别它是否接触到左侧或右侧的按钮,并采取相应的行动。如果 TouchCollection
计数为零且按下了键盘键,我们则再次移动玩家。在这两种情况下,我们都将更新玩家的速度,并将其向前移动到世界地图中。
精灵、外星人和外星人组件
外星人是坏人,它们会试图阻止玩家继续前进。外星人可以发射子弹。我们可以使用同一个类创建外星人和 Boss(龙),或者我们可以遵循面向对象编程的原则,创建一个包含外星人所有通用属性的类,然后继承该类并创建两个不同的类——Alien
和 BossAlien
。这就是我在游戏中做的那样。我创建了一个通用的 Sprite 类,它可以被任何对象复用。它包含了大多数通用属性。下面是 Sprite 类的代码。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class Sprite
{
public Texture2D Texture { get; private set; }
public Vector2 Position;
public Vector2 Velocity;
public Vector2 Origin;
public bool Active = true;
public float Scale = 1;
public float Rotation;
public float ZLayer;
public Color Color = Color.White;
public int TotalFrames { get; private set; }
public TimeSpan AnimationInterval;
public bool OneShotAnimation;
public bool DeactivateOnAnimationOver = true;
private Rectangle[] _rects;
private int _currentFrame;
private TimeSpan _animElapsed;
public int FrameWidth { get { return _rects == null ? Texture.Width : _rects[0].Width; } }
public int FrameHeight { get { return _rects == null ? Texture.Height : _rects[0].Height; } }
public Sprite(Texture2D texture, Rectangle? firstRect = null,
int frames = 1, bool horizontal = true, int space = 0)
{
Texture = texture;
TotalFrames = frames;
if (firstRect != null)
{
_rects = new Rectangle[frames];
Rectangle first = (Rectangle)firstRect;
for (int i = 0; i < frames; i++)
_rects[i] = new Rectangle(first.Left + (horizontal ? (first.Width + space) * i : 0),
first.Top + (horizontal ? 0 : (first.Height + space) * i), first.Width, first.Height);
}
else
_rects = new Rectangle[] { new Rectangle(0, 0, texture.Width, texture.Height) };
}
public virtual void Update(GameTime gameTime)
{
if (Active)
{
if (TotalFrames > 1 && (_animElapsed += gameTime.ElapsedGameTime) > AnimationInterval)
{
if (++_currentFrame == TotalFrames)
{
_currentFrame = 0;
if (OneShotAnimation)
{
_currentFrame = TotalFrames - 1;
if (DeactivateOnAnimationOver)
Active = false;
}
}
_animElapsed -= AnimationInterval;
}
Position += Velocity;
}
}
public virtual void Draw(GameTime gameTime, SpriteBatch batch)
{
if (Active)
{
batch.Draw(Texture, Position, _rects == null ? null : (Rectangle?)_rects[_currentFrame],
Color, Rotation, Origin, Scale, SpriteEffects.None, ZLayer);
}
}
public int ActualWidth
{
get { return (int)(FrameWidth * Scale); }
}
public int ActualHeight
{
get { return (int)(FrameHeight * Scale); }
}
public Rectangle BoundingRect
{
get
{
return new Rectangle(
(int)(Position.X - Origin.X * Scale), (int)(Position.Y - Origin.Y * Scale),
(int)(_rects[0].Width * Scale), (int)(_rects[0].Height * Scale));
}
}
public virtual bool Collide(Sprite other)
{
return Active && other.Active && BoundingRect.Intersects(other.BoundingRect);
}
public virtual bool Collide(Rectangle other)
{
return Active && BoundingRect.Intersects(other);
}
}
}
这将检查精灵与其他精灵或矩形的碰撞。Rectangle
类将有一个内置方法 called intersects
,它会返回对象是否相交,返回 true 或 false。我们要做的第一件事是创建一个自定义的 Alien
类,它继承自 Sprite
类并添加一些特定的外星人属性。这将使我们在管理所有外星人时更容易。添加一个名为 Alien
的新类,并从 Sprite
派生。请看下面的代码——
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class Alien : Sprite
{
public readonly bool IsBoss;
public readonly AlienType Type;
GameData _gameData = null;
public int HitPoints;
SoundEffectInstance alienDead;
public Alien(AlienType type, bool isBoss)
: base(type.Texture, type.FirstFrame, type.Frames, type.IsHorizontal, type.Space)
{
IsBoss = isBoss;
Type = type;
Origin.X = type.FirstFrame.Value.Width / 2;
AnimationInterval = type.AnimationRate;
_gameData = GameData.GetGameDataInstance();
HitPoints = type.Bullets;
alienDead = _gameData.AudioLibarary.AlienDead;
}
public override void Update(GameTime gameTime)
{
if (Active && !_isDead)
{
if (Position.X < 0 || Position.X > _gameData.GameScreenWidth)
Velocity.X = -Velocity.X;
if (Position.Y > _gameData.GameScreenHeight + 40)
{
Active = false;
if (OutOfBounds != null)
OutOfBounds(this);
}
}
else if (_isDead)
{
if (Color.R > 4)
Color.R -= 4;
else
{
Active = false;
}
return;
}
base.Update(gameTime);
}
public event Action<Alien> OutOfBounds;
public event Action<Alien> Killed;
private bool _isDead;
public bool IsDead { get { return _isDead; } }
public virtual void Hit(int power = 1)
{
if ((HitPoints -= power) <= 0)
{
alienDead.Play();
// alien killed, start death sequence
_isDead = true;
Color = new Color(255, 0, 0);
if (Killed != null)
Killed(this);
}
}
}
}
代码很简单,因为我们只需要绘制和更新外星人。如果它在屏幕上,它就是活动的,否则我们将它设置为非活动。我们将更新它的速度和可能的加速度来更新它。但是工作还没有完成。现在我们将创建一个 Alien
引用数组在 AlienComponent
类中。它将管理屏幕上的一组外星人。在这种情况下,由于每个外星人可能看起来都不同,我们可能需要根据需要动态创建外星人。另一种可能的方法是允许在精灵初始化后更改精灵的纹理。目前,精灵的纹理是通过构造函数传递的,并且是只读的。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SkyWar
{
class AliensComponent
{
GameplayScreen _gameScreen;
GameData _gameData;
BossAlien bossAlien = null;
Alien[] _aliens = new Alien[40];
int _currentAlienIndex;
Random _rnd = new Random();
TimeSpan _elapsedTime;
int _currentLiveAliens, _totalKilledAliens;
private TimeSpan _delayCount, _delay;
public TimeSpan Delay
{
get { return _delay; }
set
{
_delay = value;
_delayCount = TimeSpan.Zero;
}
}
internal IEnumerable<Alien> Aliens { get { return _aliens; } }
public BossAlien BossAlien { get { return bossAlien; } }
// alien bullets
Sprite[] _bullets = new Sprite[40];
//SoundEffectInstance[] _bulletSoundInst = new SoundEffectInstance[24];
//SoundEffect _bulletSound;
Texture2D _texture;
int _totalBullets, _currentBullet;
public AliensComponent(GameplayScreen gpScreen, ContentManager content)
{
_gameScreen = gpScreen;
_gameData = GameData.GetGameDataInstance();
_texture = content.Load<Texture2D>("images/laser1");
}
public void Update(GameTime gameTime)
{
// get current level data
LevelData data = _gameScreen.GetCurrentLevelData();
if (data == null)
{
_gameData.IsGameCompleted = true;
return;
}
foreach (var bullet in _bullets)
{
if (bullet != null && bullet.Active)
{
bullet.Update(gameTime);
if (bullet.Position.Y > _gameData.GameScreenHeight + 30)
{
bullet.Active = false;
_totalBullets--;
}
else if (bullet.Collide(_gameScreen.SpaceShipComponent.BoundingBox))
{
_gameScreen.SpaceShipComponent.PlayerHit();
bullet.Active = false;
_totalBullets--;
}
}
}
if (Delay > TimeSpan.Zero)
{
if ((_delayCount += gameTime.ElapsedGameTime) >= Delay)
Delay = TimeSpan.Zero;
}
if (_delay == TimeSpan.Zero && (_elapsedTime +=
gameTime.ElapsedGameTime) > data.AlienGenerationTime)
{
_elapsedTime = TimeSpan.Zero;
var alien = CreateAlien(data);
if (alien != null)
{
_currentLiveAliens++;
while (_aliens[_currentAlienIndex] != null && _aliens[_currentAlienIndex].Active)
_currentAlienIndex = (_currentAlienIndex + 1) % _aliens.Length;
_aliens[_currentAlienIndex] = alien;
}
}
foreach (var alien in _aliens)
if (alien != null && alien.Active)
{
alien.Update(gameTime);
if (!alien.IsDead && _gameScreen.SpaceShipComponent.IsSpaceShipActive &&
alien.Collide(_gameScreen.SpaceShipComponent.BoundingBox))
{
alien.Hit();
_gameScreen.SpaceShipComponent.PlayerHit();
if (_totalKilledAliens < data.TotalAliensToFinish)
_totalKilledAliens = 0;
}
if (_delay == TimeSpan.Zero && _totalBullets <
data.MaxAlienBullets && _rnd.Next(100) < data.FireChance)
CreateBullet(alien);
}
if (bossAlien != null && bossAlien.Active && (!bossAlien.IsDead))
{
bossAlien.Update(gameTime);
if (!bossAlien.IsDead && _gameScreen.SpaceShipComponent.IsSpaceShipActive &&
bossAlien.Collide(_gameScreen.SpaceShipComponent.BoundingBox))
{
bossAlien.Hit();
_gameScreen.SpaceShipComponent.PlayerHit();
}
if (_delay == TimeSpan.Zero && _totalBullets <
data.MaxAlienBullets && _rnd.Next(100) < data.FireChance)
CreateBullet(bossAlien);
}
}
private Alien CreateAlien(LevelData data)
{
if (_currentLiveAliens > data.MaxActiveAliens) return null;
if (_totalKilledAliens == data.TotalAliensToFinish)
{
_totalKilledAliens++;
bossAlien = CreateBoss(data);
bossAlien.Killed += delegate
{
_currentLiveAliens--;
_gameScreen.InitLevel(_gameScreen.Level + 1);
_totalKilledAliens = 0;
};
return null;
}
int chance = 0;
int value = _rnd.Next(100);
foreach (var sd in data.SelectionData)
{
if (value < sd.Chance + chance)
{
Alien alien = new Alien(sd.Alien, false);
alien.Position = new Vector2((float)(_rnd.NextDouble() * _gameData.GameScreenWidth), -50);
alien.Velocity = new Vector2((float)(_rnd.NextDouble() * alien.Type.MaxXSpeed * 2 -
alien.Type.MaxXSpeed), (float)(_rnd.NextDouble() * alien.Type.MaxYSpeed + 2));
alien.Scale = 0.7f;
_aliens[_currentAlienIndex] = alien;
alien.OutOfBounds += a =>
{
_currentLiveAliens--;
};
alien.Killed += a =>
{
_currentLiveAliens--;
_totalKilledAliens++;
};
return alien;
}
chance += sd.Chance;
}
return null;
}
private BossAlien CreateBoss(LevelData data)
{
BossAlien alien = new BossAlien(data.Boss);
alien.Position = new Vector2((float)(_rnd.NextDouble() * _gameData.GameScreenWidth), 10);
alien.Velocity = new Vector2((float)(_rnd.NextDouble() *
alien.Type.MaxXSpeed * 4 - alien.Type.MaxXSpeed), 0.0f);
alien.Scale = 2.3f;
return alien;
}
private void CreateBullet(Sprite alien)
{
while (_bullets[_currentBullet] != null && _bullets[_currentBullet].Active)
_currentBullet = (_currentBullet + 1) % _bullets.Length;
Sprite bullet = _bullets[_currentBullet];
if (bullet == null)
{
bullet = new Sprite(_texture, new Rectangle(0, 0, 16, 46));
bullet.Scale = 0.5f;
bullet.ZLayer = .5f;
//bullet.Color = Color.White;
_bullets[_currentBullet] = bullet;
//_bulletSoundInst[_currentBullet] = _bulletSound.CreateInstance();
//_bulletSoundInst[_currentBullet].Volume = .7f;
}
bullet.Position = alien.Position + new Vector2(-2, alien.ActualHeight / 2);
bullet.Velocity.Y = alien.Velocity.Y > 0 ? 1.8f * alienVelocity.Y : _rnd.Next(10) + 3;
bullet.Active = true;
//_bulletSoundInst[_currentBullet].Play();
_totalBullets++;
}
public void Draw(GameTime gameTime, SpriteBatch spbatch)
{
foreach (var alien in _aliens)
if (alien != null && alien.Active)
alien.Draw(gameTime, spbatch);
foreach (var bullet in _bullets)
if (bullet != null)
bullet.Draw(gameTime, spbatch);
if (bossAlien != null && bossAlien.Active && (!bossAlien.IsDead))
bossAlien.Draw(gameTime, spbatch);
}
}
}
该类非常简单,我们有一个 Alien[] _aliens = new Alien[40]
在类中,我们将动态创建外星人并在屏幕上移动它们。此类还负责 _currentLiveAliens
和 _totalKilledAliens
以更新分数和关卡。还有一个用于遍历外星人数组的属性。
internal IEnumerable<Alien> Aliens { get { return _aliens; } }
它的 Update 方法最重要,它负责所有操作。它等待外星人生成周期,并生成新的外星人。如果外星人被杀死,它将更新计数和分数。如果杀死的外星人总数达到关卡数,它将加载新关卡。
现在让我们讨论游戏关卡规划、得分、需要杀死多少外星人才能继续前进、外星人能发射多少子弹、外星人生成时间等。
游戏关卡和难度控制
我试图使游戏尽可能动态化。我尝试使用 xml 文件来控制游戏难度、需要杀死的总外星人数量、外星人能发射的总子弹数量、屏幕上活动的外星人总数等。因此,明天如果我们觉得游戏太简单或太难,我们只需要修改 xml,而游戏的其余部分将保持不变。
下面的 xml 用于配置游戏中的不同外星人。正如您清楚看到的,它指定了外星人的名称、它将为玩家提供多少分数、最大 x 和 y 轴移动速度、纹理路径、纹理中有多少帧以及击杀该外星人需要多少子弹。
<?xml version="1.0" encoding="utf-8" ?>
<AlienTypes>
<AlienType Name="alien1" Score="10" MaxXSpeed="2" MaxYSpeed="3"
Texture="Images/alien1" Frames="1" Space="1" FirstFrame="0,0,50,50" Bullets="1"
AnimationRate="500" />
<AlienType Name="alien2" Score="15" MaxXSpeed="3" MaxYSpeed="4"
Texture="Images/alien2" Frames="1" Space="1" FirstFrame="0,0,42,67" Bullets="1"
AnimationRate="400" />
<AlienType Name="alien3" Score="20" MaxXSpeed="4" MaxYSpeed="5"
Texture="Images/alien3" Frames="1" Space="1" FirstFrame="0,0,41,64" Bullets="1"
AnimationRate="400" />
<AlienType Name="alien4" Score="25" MaxXSpeed="4" MaxYSpeed="4"
Texture="Images/alien4" Frames="1" Space="1" FirstFrame="0,0,40,52" Bullets="1"
AnimationRate="400" />
</AlienTypes>
同样,我们可以使用关卡 xml 来控制关卡。您可以看到它清楚地指定了有多少关卡,每个关卡屏幕上应该有多少活跃外星人,玩家需要杀死多少外星人才能进入下一关,关卡末尾会出现哪种龙形或 Boss 敌人,新的外星人生成时间是什么,玩家需要击中多少子弹才能杀死 Boss。
<?xml version="1.0" encoding="utf-8" ?>
<Levels>
<Level Number="1" MaxActiveAliens="12" TotalAliensToFinish="10"
Boss="boss1" AlienGenerationTime="1200"
ChangeDirChance="2" FireChance="7"
MaxAlienBullets="7">
<AlienTypes>
<AlienType Name="alien1" Chance="25" />
<AlienType Name="alien2" Chance="20" />
<AlienType Name="alien3" Chance="20" />
<AlienType Name="alien4" Chance="15" />
<AlienType Name="alien5" Chance="25" />
</AlienTypes>
</Level>
<Level Number="2" MaxActiveAliens="16" TotalAliensToFinish="14"
Boss="boss2" AlienGenerationTime="1000"
ChangeDirChance="2" FireChance="14"
MaxAlienBullets="10">
<AlienTypes>
<AlienType Name="alien1" Chance="20" />
<AlienType Name="alien2" Chance="20" />
<AlienType Name="alien3" Chance="20" />
<AlienType Name="alien4" Chance="15" />
<AlienType Name="alien5" Chance="25" />
<AlienType Name="alien6" Chance="25" />
</AlienTypes>
</Level>
</Levels>
我上传了完整的源代码,但无法上传纹理,因为它的尺寸远远超过了 Code Project 的限制。当 Game Studio 将纹理转换为 .xnb 文件时,其尺寸会大大增加。
您可以在这里找到我以前的 App Innovation 文章。
希望这能为您提供完整的游戏指导。尽情游戏。
看点
我赢得了第一轮比赛,并在收到一台 Ultrabook 后,及时创建了一个应用程序并上传到 Microsoft 应用商店。但它一直卡在验证阶段,至今仍未完成(因为我生病了,所以无法继续完成)。
我从 Intel AppUp 比赛中学到了很多。我第一次尝试了新东西(MonoGame)。
参考文献
在我结束本文之前,我想为帮助我学习 MonoGame 和 XNA 的文章添加一些参考资料。书籍:XNA 3.0 Game Programming from Novice to Professional, Xna Game Programming Recepies
链接