使用 MonoGame 框架为 Intel App Up 开发 Windows 8 游戏 (SkyWar) 之旅





5.00/5 (2投票s)
为 Intel App Up 比赛开发 Windows 8 游戏 SkyWar 之旅
引言
本文介绍了我为CodeProject的Intel AppUp竞赛开发SkyWar游戏的经历。您可以下载文章附带的完整游戏源代码。该游戏目前处于微软商店的验证阶段,很快您将可以在Windows Phone 7.5 Mango和Windows Phone 8商店中找到这款免费下载的游戏。
背景
我偶然发现了Intel App Innovation contest,并决定参加这个比赛。比赛要求创建一个展示Intel Ultrabook最佳功能的Windows 8应用。我构思了很多想法,最终决定创建一个Windows 8游戏。
Ultrabook具备触控、传感器、GPS和Smart Connect等功能。所以我想到创建一个能充分利用触控和传感器并提供良好用户体验的游戏。我对XNA框架略知一二。我原以为可以使用XNA创建Windows 8游戏应用。但Windows 8已不再支持XNA,不过Windows Phone 8.0 SDK支持。于是我开始寻找替代方案,并找到了bob的这篇很棒的文章。我发现使用MonoGame框架,我们可以轻松地为Windows 8创建游戏应用,甚至可以免费移植游戏到Android和iPhone。最棒的是,它和XNA几乎一模一样。
游戏剧情
在编写游戏之前,我们应该清楚自己要做什么。游戏的目的是什么,游戏如何进行,玩家将面临什么挑战,游戏的结束标准是什么等等。
最终,我构思了一个名为Sky War的太空射击游戏,其灵感来源于老式街机游戏。
这款游戏允许玩家控制一艘太空飞船在星空中飞行。
玩家将在星系中面对不同的敌人。在每个关卡中,玩家将面对一组敌人,击败他们后,将面对该关卡的Boss或巨龙敌人。
玩家开始时将有3条生命线和100%的生命值。但之后要击败如此危险的敌人,玩家将获得游戏中的惊人生命线。
玩家玩得越多,游戏就会变得越复杂,面临的巨龙或Boss敌人也会越困难。
我们应该有更详细的剧情,纹理会做什么,会有多少个对象等等。对于本文,我将只做简要概述。
设置环境
在我们开始为Windows 8构建游戏之前,了解一点MonoGame框架以及如何设置游戏开发环境会很有帮助。
MonoGame为希望将代码移植到非微软平台的XNA开发者提供了跨平台XNA框架实现。您可以进一步阅读Mono Game概述。让我快速进入环境设置。
如何设置MonoGame环境?
有两种设置环境的方法,我将解释最简单的方法。
- 安装Visual Studio 2010。在其上安装Service Pack SP1(这是安装Windows Mobile SDK 7.1所必需的)。
- 安装Windows Mobile SDK 7.1,它将自动在您的机器上安装XNA Game Studio 4.0。(您可能在想为什么安装VS 2010,因为VS 2012及更高版本没有XNA模板,而且MonoGame框架没有内容管道项目,这是XNA中在运行时准备图形和音频的预编译器步骤)。
- 安装Visual Studio 2012。
- 安装MonoGame安装程序,您可以从此处下载。
这将为您设置好环境,并在Visual Studio 2010和2012中创建MonoGame模板。
第二种方法您可以在此处阅读。现在我们已经准备好为Windows 8创建我们的第一个MonoGame了。
开始吧
在创建任何游戏之前,我们应该准备好以下几项:
- 游戏剧情(详细的游戏描述,关卡规划等)
- 纹理(我们将在游戏中使用到的任何图像、声音等)
- 游戏状态管理
上面提供了简要的游戏剧情,但对于游戏,我们应该有一个详细的剧情。之后我花了大量时间从不同的免费网站收集适合游戏的纹理。我用Photoshop按需修改了它们。
如果您是初学者,对XNA一无所知?请参考本文底部的链接和书籍推荐。
一切准备就绪后,我们开始编码。
让我们打开Visual Studio 2012并选择MonoGame项目模板。为您的项目命名为Sky War。这里需要注意的是,我们没有内容项目。处理此问题的最佳方法是在您的游戏中创建一个Content文件夹。现在打开Visual Studio 2010,将所有您想要在游戏中使用的纹理添加到其中,构建项目,然后添加到Sky War游戏中的Content文件夹。然后将Content文件夹中所有纹理的“复制到输出目录”设置为“如果更新则复制”。这就是我们处理MonoGame框架中没有内容项目的方式。
现在我们可以开始为游戏中的每个对象编写类了。下面附带的截图将为您提供游戏的一个简要概念,我们将开发什么——
查看图片后,我们可以识别出游戏中的以下对象——
滚动背景(用于创建移动的背景对象)、飞船(玩家在星系中驾驶的飞船)、子弹(玩家和外星人都会重用)、外星人(敌方飞船)、外星人组件(屏幕上可见的外星人数量及重新生成)、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
对象以及我们将使用的两个纹理的资源名称。
Draw
方法,并在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的调用,可能会导致速度不一致)。
类的Constructor
和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(巨龙)创建在同一个类中,或者我们可以遵循OOP原则,创建一个包含外星人所有通用属性的类,然后继承它并创建两个不同的类——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
类有一个内置方法interest,如果对象相交则返回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);
}
}
}
}
代码很简单,我们只会绘制和更新外星人,如果它在屏幕上,它就是活动的,否则我们将active设置为false。我们将更新它的可能速度和速度来更新它。但工作尚未完成。现在我们将在AlienComponent
类中创建一个Alien引用的数组。它将管理屏幕上的外星人集合。在这种情况下,由于每个外星人可能看起来都不同,我们可能需要根据需要动态创建外星人。另一种可能的方法是允许在精灵初始化后更改精灵的纹理。目前,精灵的纹理是通过构造函数传递的,并且是只读的,因为。
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 * alien.Velocity.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>
同样,为了控制关卡,我们有level 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
链接