C#使用 MonoGame* 开发游戏

发表于2018-05-31
评论0 5.9k浏览
全球各地的开发人员都希望开发游戏。为什么不呢?游戏是计算机历史上销量最高的产品之一,游戏业务带来的财富不断吸引着开发人员的加入。作为开发人员,我当然希望成为下一个开发愤怒的小鸟*或光晕*的开发人员。

但是,事实上,游戏开发是软件开发最困难的领域之一。你不得不牢记那些从来不会使用的三角函数、几何和物理类。除此之外,你的游戏必须以吸引用户沉浸其中的方式来组合声音、视频和故事情节。然后,你需要再编写一行代码!

为了简化难度,开发游戏使用的框架不仅要能够使用C和C++,还要能够使用C#或JavaScript*(是的,你可以使用HTML5和JavaScript开发适用于您的浏览器的三维游戏)。

其中一个框架是MicrosoftXNA*,该框架基于MicrosoftDirectX*技术,支持为Xbox360*、Windows*和WindowsPhone*创建游戏。微软已经初步淘汰了XNA,但是与此同时,开源社区加入了一位新成员:MonoGame*。

MonoGame是什么?

MonoGame是XNA应用编程接口(API)的开源实施方式。它不仅能够实施面向Windows的XNAAPI,还能够实施面向Mac*OSX*、AppleiOS*、GoogleAndroid*、Linux*和WindowsPhone的XNAAPI。这意味着,你只需进行较少的改动即可为所有平台开发游戏。这种特性非常棒:你可以使用能够轻松移植至所有主要台式机、平板电脑和智能手机平台的C#来创建游戏。该框架能够帮助开发人员开发出一款享誉全球的游戏。

在Windows上安装MonoGame

甚至,你不需要使用Windows便可使用MonoGame进行开发。你可以使用MonoDevelop*(面向Microsoft.NET语言的开源跨平台集成开发环境[IDE])或Xamarin开发的一款跨平台IDE—XamarinStudio*。借助这些IDE,你可以使用C#在Linux或Mac上进行开发。

如果你是一位Microsoft.NET开发人员,并且日常使用的工具是MicrosoftVisualStudio*,你可以像我一样将MonoGame安装到VisualStudio中并且用它来创建游戏。在撰写本文时,MonoGame的最新稳定版本是3.2版。该版本可在VisualStudio2012和2013中运行,并支持创建支持触摸功能的DirectX桌面游戏。

MonoGame安装在VisualStudio中随附了许多新模板,你可从中选择来创建游戏,如图1所示。
                                                          图1.全新MonoGame*模板

现在,如要创建第一个游戏,请点击MonoGameWindowsProject,然后选择一个名称。VisualStudio可创建一个包括所有所需文件和参考的新项目。如果运行该项目,则应如图2所示。
                                            图2.在MonoGame*模板中创建的游戏

很无聊,是吗?只有一个蓝色屏幕;但是,构建任何游戏都要从它开始。按Esc,则可关闭窗口。

现在,你可以使用目前拥有的项目开始编写游戏,但是有一个问题:如要添加任何资产(图像、子图、声音或字体),你需要将其编写为与MonoGame兼容的格式。对于这一点,你需要以下选项之一:
  • 安装XNA游戏Studio4.0
  • 安装WindowsPhone8软件开发套件(SDK)
  • 使用外部程序,如XNA内容编译器

XNAGameStudio

XNAGameStudio可提供为Windows和Xbox360创建XNA游戏所需的一切组件。此外,它还包括内容编译器,可将资产编译至.xnb文件,然后编译MonoGame项目所需的一切文件。目前,仅可在VisualStudio2010中安装编译器。如果你不希望仅出于该原因来安装VisualStudio2010,则可在VisualStudio2012中安装XNAGameStudio(详见本文“了解更多信息”部分的链接)。

WindowsPhone8SDK

你可以在VisualStudio2012中直接安装XNAGameStudio,但是在VisualStudio2012中安装WindowsPhone8SDK更好。你可以用它创建项目来编译资产。

XNA内容编译器

如果不希望安装SDK来编译资产,则可使用XNA内容编译器(详见“了解更多信息”中的链接),该编译器是一款开源程序,能够将资产编译至MonoGame中可使用的.xnb文件。

创建第一个游戏

使用MonoGame模板创建的上一个游戏可作为所有游戏的起点。你可以使用相同的流程创建所有游戏。Program.cs中包括Main函数。该函数可初始化和运行游戏:
static void Main()
{
    using (var game = new Game1())
        game.Run();
}

Game1.cs是游戏的核心。有两种方法需要在一个循环中每秒钟调用60次:更新和绘制。在更新中,为游戏中的所有元素重新计算数据;在绘制中,绘制这些元素。请注意,这是一个紧凑的循环。你只有1/60秒,也就是16.7毫秒来计算和绘制数据。如果你超出该事件,程序就会跳过一些绘制循环,游戏中就会出现图形故障。

近来,台式电脑上的游戏输入方式是键盘和鼠标。除非用户购买了外部硬件,如驱动轮和操纵杆,否则我们只能假定没有其他的输入方法。随着新硬件的推出,如超极本™设备、2合1超极本和一体机,输入选项发生了变化。你可以使用触摸输入和传感器,为用户提供更加沉浸式、逼真的游戏体验。

对于第一款游戏,我们将创建足球点球赛。用户使用触摸的方式来“射门”,计算机守门员接球。球的方向和速度由用户的敲击动作来决定。计算机守门员将会随机选择一个方向和速度接球。射门成功得一分。反之,守门员的一分。

向游戏添加内容

游戏中的第一步是添加内容。通过添加背景场地和足球开始。如要执行该操作,则需要创建两个.png文件:一个文件用于足球场(图3),另一个用于足球(图4)。
                                                               图3.足球场

                                                                    图4.足球

如要在游戏中使用这些文件,你需要对其进行编译。如果正在使用XNAGameStudio或WindowsPhone8SDK,则需要创建一个XNA内容项目。该项目不需要在同一个解决方案中。你只需要用它来编译资产。将图像添加至该项目并对其进行构建。然后,访问项目目标目录,并将生成的.xnb文件复制至你的项目。

我更喜欢使用XNA内容编译器,它不需要新项目且支持按需编译资产。仅需打开程序,将文件添加至列表,选择输出目录,并点击“编译(Compile)”。.xnb文件便可添加至该项目。

内容文件

.xnb文件可用时,将其添加至游戏的“内容(Content)”文件夹下。你必须为每个文件,包括“内容(Content)”、“复制至输入目录(CopytoOutputDirectory)”以及“如果较新则复制(CopyifNewer)”,设置构建操作。如果不执行该操作,则会在加载资产时出现错误。

创建两个字段存储足球和足球场的纹理:
private Texture2D _backgroundTexture;
private Texture2D _ballTexture;

这些字段可在LoadContent方法中加载:
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    _spriteBatch = new SpriteBatch(GraphicsDevice);
    // TODO: use this.Content to load your game content here
    _backgroundTexture = Content.Load<Texture2D>("SoccerField");
    _ballTexture = Content.Load<Texture2D>("SoccerBall");
}

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

接下来,在Draw方法中绘制纹理:
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Green);
    // Set the position for the background    
    var screenWidth = Window.ClientBounds.Width;
    var screenHeight = Window.ClientBounds.Height;
    var rectangle = new Rectangle(0, 0, screenWidth, screenHeight);
    // Begin a sprite batch    
    _spriteBatch.Begin();
    // Draw the background    
    _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White);
    // Draw the ball
    var initialBallPositionX = screenWidth / 2;
    var ínitialBallPositionY = (int)(screenHeight * 0.8);
    var ballDimension = (screenWidth > screenHeight) ? 
        (int)(screenWidth * 0.02) :
        (int)(screenHeight * 0.035);
    var ballRectangle = new Rectangle(initialBallPositionX, ínitialBallPositionY,
        ballDimension, ballDimension);
    _spriteBatch.Draw(_ballTexture, ballRectangle, Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

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

移动足球

如要移动足球,我们必须重新计算循环中每个迭代的位置,并在新的位置绘制它。在Update方法中执行新位置的计算:
protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();
    // TODO: Add your update logic here
    _ballPosition -= 3;
    _ballRectangle.Y = _ballPosition;
    base.Update(gameTime);
}

足球位置在每个循环中都会通过减去三个像素进行更新。如果你希望让球移动地更快,则必须减去更多的像素。变量_screenWidth、_screenHeight、_backgroundRectangle、_ballRectangle和_ballPosition是私有字段,可在ResetWindowSize方法中进行初始化:
private void ResetWindowSize()
{
    _screenWidth = Window.ClientBounds.Width;
    _screenHeight = Window.ClientBounds.Height;
    _backgroundRectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
    _initialBallPosition = new Vector2(_screenWidth / 2.0f, _screenHeight * 0.8f);
    var ballDimension = (_screenWidth > _screenHeight) ?
        (int)(_screenWidth * 0.02) :
        (int)(_screenHeight * 0.035);
    _ballPosition = (int)_initialBallPosition.Y;
    _ballRectangle = new Rectangle((int)_initialBallPosition.X, (int)_initialBallPosition.Y,
        ballDimension, ballDimension);
}

该方法可根据窗口的尺寸重置所有变量。它可在Initialize方法中调用:
protected override void Initialize()
{
    // TODO: Add your initialization logic here
    ResetWindowSize();
    Window.ClientSizeChanged += (s, e) => ResetWindowSize();
    base.Initialize();
}

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

如果运行程序,你将看到足球呈直线移动,直至字段结束时停止。当足球到达目标时,你可以使用以下代码将足球复位:
protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();
    // TODO: Add your update logic here
    _ballPosition -= 3;
    if (_ballPosition < _goalLinePosition)
        _ballPosition = (int)_initialBallPosition.Y;
    _ballRectangle.Y = _ballPosition;
    base.Update(gameTime);
}

The_goalLinePositionvariableisanotherfield,initializedintheResetWindowSizemethod:
_goalLinePosition=_screenHeight*0.05;

你必须在Draw方法中做出另一个改变:移除所有计算代码。
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Green);
   var rectangle = new Rectangle(0, 0, _screenWidth, _screenHeight);
    // Begin a sprite batch    
    _spriteBatch.Begin();
    // Draw the background    
    _spriteBatch.Draw(_backgroundTexture, rectangle, Color.White);
    // Draw the ball
    _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

该运动与目标呈垂直角度。如果你希望足球呈一定的角度移动,则需要创建_ballPositionX字段,并增加(向右移动)或减少(向左移动)它。更好的方法是将Vector2用于足球位置,如下:
protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();
    // TODO: Add your update logic here
    _ballPosition.X -= 0.5f;
    _ballPosition.Y -= 3;
    if (_ballPosition.Y < _goalLinePosition)
        _ballPosition = new Vector2(_initialBallPosition.X,_initialBallPosition.Y);
    _ballRectangle.X = (int)_ballPosition.X;
    _ballRectangle.Y = (int)_ballPosition.Y;
    base.Update(gameTime);
}

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

触摸和手势

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

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

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

对于该游戏,我们仅需要轻拂操作,GesturesAPI支持该操作,所以我们使用它。首先需要通过使用TouchPanel类指明希望使用的手势。例如,代码:
TouchPanel.EnabledGestures=GestureType.Flick|GestureType.FreeDrag;

...仅支持MonoGame检测并通知轻拂和拖动操作。然后,在Update方法中,你可以按照如下方式处理手势:
if (TouchPanel.IsGestureAvailable)
{
    // Read the next gesture    
    GestureSample gesture = TouchPanel.ReadGesture();
    if (gesture.GestureType == GestureType.Flick)
    {
        …
    }
}

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

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

首先,使用Initialize方法在游戏中启用轻拂手势:
protected override void Initialize()
{
    // TODO: Add your initialization logic here
    ResetWindowSize();
    Window.ClientSizeChanged += (s, e) => ResetWindowSize();
    TouchPanel.EnabledGestures = GestureType.Flick;
    base.Initialize();
}

此时,足球在游戏运行时将会一直运动。使用私有字段_isBallMoving可在足球移动时通知游戏。在Update方法中,当程序检测轻拂操作时,你将_isBallMoving设置为True,则足球将开始运动。当足球到达球门线时,将_isBallMoving设置为False并重置足球的位置:
protected override void Update(GameTime gameTime)
{
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed ||
        Keyboard.GetState().IsKeyDown(Keys.Escape))
        Exit();
    // TODO: Add your update logic here
    if (!_isBallMoving && TouchPanel.IsGestureAvailable)
    {
        // Read the next gesture    
        GestureSample gesture = TouchPanel.ReadGesture();
        if (gesture.GestureType == GestureType.Flick)
        {
            _isBallMoving = true;
            _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f;
        }
    }
    if (_isBallMoving)
    {
        _ballPosition += _ballVelocity;
        // reached goal line
        if (_ballPosition.Y < _goalLinePosition)
        {
            _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
            _isBallMoving = false;
            while (TouchPanel.IsGestureAvailable)
                TouchPanel.ReadGesture();
        }
        _ballRectangle.X = (int) _ballPosition.X;
        _ballRectangle.Y = (int) _ballPosition.Y;
    }
    base.Update(gameTime);
}

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

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

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

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

解决这一问题的方法是使用原始输入,获取触摸点,然后了解其是否在足球附近。以下代码能够决定触摸输入是否可以触发足球。如果可以,手势将设置_isBallHitfield:
TouchCollectiontouches=TouchPanel.GetState();
TouchCollection touches = TouchPanel.GetState();
if (touches.Count > 0 && touches[0].State == TouchLocationState.Pressed)
{
    var touchPoint = new Point((int)touches[0].Position.X, (int)touches[0].Position.Y);
    var hitRectangle = new Rectangle((int)_ballPositionX, (int)_ballPositionY, _ballTexture.Width,
        _ballTexture.Height);
    hitRectangle.Inflate(20,20);
    _isBallHit = hitRectangle.Contains(touchPoint);
}

然后,运动仅在_isBallHit字段为True时开始:
if (TouchPanel.IsGestureAvailable && _isBallHit)

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

Update方法有一个参数:gameTime。如果在移动开始时存储了gameTime值,则可知道足球移动的实际时间,并可在超时后重置游戏:
if (gesture.GestureType == GestureType.Flick)
{
    _isBallMoving = true;
    _isBallHit = false;
    _startMovement = gameTime.TotalGameTime;
    _ballVelocity = gesture.Delta*(float) TargetElapsedTime.TotalSeconds/5.0f;
}
...
var timeInMovement = (gameTime.TotalGameTime - _startMovement).TotalSeconds;
// reached goal line or timeout
if (_ballPosition.Y <' _goalLinePosition || timeInMovement > 5.0)
{
    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
    _isBallMoving = false;
    _isBallHit = false;
    while (TouchPanel.IsGestureAvailable)
        TouchPanel.ReadGesture();
}

添加守门员

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

守门员在LoadContent方法中加载:
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    _spriteBatch = new SpriteBatch(GraphicsDevice);
    // TODO: use this.Content to load your game content here
    _backgroundTexture = Content.Load<Texture2D>("SoccerField");
    _ballTexture = Content.Load<Texture2D>("SoccerBall");
    _goalkeeperTexture = Content.Load<Texture2D>("Goalkeeper");
}

然后,我们必须在Draw方法中绘制它:
protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Green);
    // Begin a sprite batch    
    _spriteBatch.Begin();
    // Draw the background    
    _spriteBatch.Draw(_backgroundTexture, _backgroundRectangle, Color.White);
    // Draw the ball
    _spriteBatch.Draw(_ballTexture, _ballRectangle, Color.White);
    // Draw the goalkeeper
    _spriteBatch.Draw(_goalkeeperTexture, _goalkeeperRectangle, Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

_goalkeeperRectangle在窗口中可提供一个矩形的守门员。它可在Update方法中更改:
protected override void Update(GameTime gameTime)
{
    …
   _ballRectangle.X = (int) _ballPosition.X;
   _ballRectangle.Y = (int) _ballPosition.Y;
   _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY,
                    _goalKeeperWidth, _goalKeeperHeight);
   base.Update(gameTime);
}

_goalkeeperPositionY、_goalKeeperWidth和_goalKeeperHeight字段可在ResetWindowSize方法中更新:
private void ResetWindowSize()
{
    …
    _goalkeeperPositionY = (int) (_screenHeight*0.12);
    _goalKeeperWidth = (int)(_screenWidth * 0.05);
    _goalKeeperHeight = (int)(_screenWidth * 0.005);
}

守门员最初位于屏幕中央的球门线顶端附近。
_goalkeeperPositionX=(_screenWidth-_goalKeeperWidth)/2;

守门员将会在足球开始移动时开始移动。它将会不停地以谐运动的方式从一端移动至另一端。该正弦曲线可描述该运动:X=A*sin(at+δ)

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

该系数将会在用户通过轻拂踢出足球时进行计算:
if (gesture.GestureType == GestureType.Flick)
{
    _isBallMoving = true;
    _isBallHit = false;
    _startMovement = gameTime.TotalGameTime;
    _ballVelocity = gesture.Delta * (float)TargetElapsedTime.TotalSeconds / 5.0f;
    var rnd = new Random();
    _aCoef = rnd.NextDouble() * 0.005;
    _deltaCoef = rnd.NextDouble() * Math.PI / 2;
}

系数a是守门员的速度,0和0.005之间的数字代表0和0.3像素/秒之间的速度(1/60秒内最大像素为0.005)。delta系数是必须是介于0和pi/2之间的数字。足球移动时,你可以更新守门员的位置:
if (_isBallMoving)
{
    _ballPositionX += _ballVelocity.X;
    _ballPositionY += _ballVelocity.Y;
    _goalkeeperPositionX = (int)((_screenWidth * 0.11) *
                      Math.Sin(_aCoef * gameTime.TotalGameTime.TotalMilliseconds + 
                      _deltaCoef) + (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11);
    …
}

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

命中测试

如果希望了解守门员是否能够接住球,你需要知道球的矩形是否与守门员的矩形相交。我们可以按照以下代码计算两个矩形后,在Update方法中执行该操作:
_ballRectangle.X = (int)_ballPosition.X;
_ballRectangle.Y = (int)_ballPosition.Y;
_goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY,
    _goalKeeperWidth, _goalKeeperHeight);
if (_goalkeeperRectangle.Intersects(_ballRectangle))
{
    ResetGame();
}

ResetGame仅可重构代码,将游戏重置为初始状态:
private void ResetGame()
{
    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
    _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2;
    _isBallMoving = false;
    _isBallHit = false;
    while (TouchPanel.IsGestureAvailable)
        TouchPanel.ReadGesture();
}

借助该简单代码,游戏便可知道守门员是否能够接住球。现在,我们需要知道足球是否能够命中。当足球超过球门线时,执行以下代码。
var isTimeout = timeInMovement > 5.0;
if (_ballPosition.Y < _goalLinePosition || isTimeout)
{
    bool isGoal = !isTimeout &&
        (_ballPosition.X > _screenWidth * 0.375) &&
        (_ballPosition.X < _screenWidth * 0.623);
    ResetGame();
}

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

添加分数记录(Scorekeeping)

如要向游戏中添加游戏记录,我们必须添加一个新资产:spritefont,其字体可用于游戏。spritefont是描述字体的.xml文件,包括字体家族及其尺寸和重量及其他属性。在游戏中,你可以按照以下方式使用spritefont:
<?xml version="1.0" encoding="utf-8"?>
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
  <Asset Type="Graphics:FontDescription">
    <FontName>Segoe UI</FontName>
    <Size>24</Size>
    <Spacing>0</Spacing>
    <UseKerning>false</UseKerning>
    <Style>Regular</Style>
    <CharacterRegions>
      <CharacterRegion>
        <Start> </Star>
        <End></End>
      </CharacterRegion>
    </CharacterRegions>
  </Asset>
</XnaContent>

你可以使用XNA内容编译器来编译该.xml文件,并将生成的.xnb文件添加至项目的Content文件夹;将其构建操作设置至Content,并将“复制至输出目录(CopytoOutputDirectory)”设置为“如果较新则复制(CopyifNewer)”。字体可在LoadContent方法中加载:
_soccerFont=Content.Load<SpriteFont>("SoccerFont");

在ResetWindowSize中,重置得分情况:
var scoreSize = _soccerFont.MeasureString(_scoreText);
_scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);

如要保持记录,需要声明两个变量:_userScore和_computerScore。命中时,_userScore变量增加,未命中、超时或守门员接住球时,_computerScore增加:
if (_ballPosition.Y < _goalLinePosition || isTimeout)
{
    bool isGoal = !isTimeout &&
                  (_ballPosition.X > _screenWidth * 0.375) &&
                  (_ballPosition.X < _screenWidth * 0.623);
    if (isGoal)
        _userScore++;
    else
        _computerScore++;
    ResetGame();
}
…
if (_goalkeeperRectangle.Intersects(_ballRectangle))
{
    _computerScore++;
    ResetGame();
}

ResetGame可重新创建得分文本,并设置其情况:
private void ResetGame()
{
    _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y);
    _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2;
    _isBallMoving = false;
    _isBallHit = false;
    _scoreText = string.Format("{0} x {1}", _userScore, _computerScore);
    var scoreSize = _soccerFont.MeasureString(_scoreText);
    _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0);
    while (TouchPanel.IsGestureAvailable)
        TouchPanel.ReadGesture();
}

_soccerFont.MeasureString可使用选中字体测量字符串,你可以使用该测量方式来计算得分情况。得分可在Draw方法中进行绘制:
protected override void Draw(GameTime gameTime)
{
…
    // Draw the score
    _spriteBatch.DrawString(_soccerFont, _scoreText, 
         new Vector2(_scorePosition, _screenHeight * 0.9f), Color.White);
    // End the sprite batch    
    _spriteBatch.End();
    base.Draw(gameTime);
}

打开球场灯光

作为最后一个触摸设计,该款游戏可在室内光线较暗时打开球场灯光。全新超极本和2合1设备通常具备一个光线传感器,你可以用它来确定室内光线的程度并更改背景的绘制方式。

对于台式机应用,我们可以使用面向Microsoft.NETFramework的WindowsAPICodePack,它是一款支持访问Windows7及更高版本操作系统特性的库。但是,在该游戏中,我们采用了另一种方式:WinRTSensorAPI。这些API虽然面向Windows8而编写,但是同样适用于台式机应用,且不经任何更改即可使用。借助它们,你无需更改任何代码即可将应用移植到Windows8。

英特尔®开发人员专区(IDZ)包括一篇如何在台式机应用中使用WinRTAPI的文章(详见“了解更多信息”部分)。基于该信息,你必须在SolutionExplorer中选择该项目,右击它,然后点击UnloadProject。然后,再次右击该项目,并点击Editproject。在第一个PropertyGroup中添加TargetPlatFormVersion标签:
<PropertyGroup>
  <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
…
  <FileAlignment>512</FileAlignmen>
  <TargetPlatformVersion>8.0</TargetPlatformVersion>
</PropertyGroup>

再次右击项目,然后点击ReloadProject。VisualStudio将重新加载该项目。当向项目中添加新标签时,将能够在ReferenceManager中看到Windows标签,如图7所示。
                                       图7.ReferenceManager中的Windows*标签

向项目中添加Windows参考。此外,你还需要添加System.Runtime.WindowsRuntime.dll参考。如在汇编程序列表中看不到,则可浏览.NetAssemblies文件夹。在我的设备上,路径为C:\ProgramFiles(x86)\ReferenceAssemblies\Microsoft\Framework\.NETCore\v4.5。

现在,你可以开始编写代码来检测灯光传感器:
LightSensor light = LightSensor.GetDefault();
if (light != null)
{

如果有灯光传感器,GetDefault方法可返回一个非空变量,以便用来检查灯光变化。通过编写ReadingChanged事件来执行该操作,如下:
ightSensor light = LightSensor.GetDefault();
if (light != null)
{
    light.ReportInterval = 0;
    light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10;
}

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

现在,当你运行程序时,当灯光关闭时你将会看到深绿色背景,当灯光开启时将会看到浅绿色背景(图8)。
                                                                图8.完整游戏

现在你拥有了一款完整的游戏。但是,它尚且未完成,它还需要大量改进(命中时的动画,守门员接住球或球击中球门柱时的反弹画面),但是我把它作为家庭作业留给你。最后一步是将游戏移植到Windows8。

将游戏移植至Windows8

将MonoGame游戏移植至其他平台非常简单。你只需要在MonoGameWindowsStoreProject类型的解决方案中创建一个新项目,然后删除Game1.cs文件并将WindowsDesktop应用Content文件夹中的四个.xnb文件添加至新项目的Content文件夹。你无需向源文件中添加新文件,只需添加链接。在SolutionExplorer中,右击Content文件夹,点击“添加/现有文件(Add/ExistingFiles)”,在Desktop项目中选择四个.xnb文件,点击“添加(Add)”按钮旁边的下箭头,并选择“添加为链接(Addaslink)”。VisualStudio可添加四个链接。

然后,将Game1.cs文件从以前的项目添加至新项目。重复对.xnb文件所执行的流程:右击项目,点击“添加/现有文件(Add/ExistingFiles)”,从其他项目文件夹中选择Game1.cs文件,点击“添加(Add)”按钮旁边的下箭头,然后点击“添加为链接(Addaslink)”。最后需要改动的地方是Program.cs,你需要对Game1类的命名空间进行更改,因为你现在使用的是台式机项目中的Game1类。

完成—你创建了一款适用于Windows8的游戏!

结论

游戏开发本身是一项困难重重的任务。你需要记住三角、几何和物理类,并运用这些概念来开发游戏(如果教授者在教授这些课题时使用的是游戏,会不会很棒?)

MonoGame让该任务更简单。你无需处理DirectX,可以使用C#来开发游戏,并且能够完全访问硬件。你可以在游戏中使用触摸、声音和传感器。此外,你还可以开发一款游戏,对其进行较小的修改并将其移植至Windows8、WindowsPhone、MacOSX、iOS或Android。当你希望开发多平台游戏时,这是一个巨大的优势。
来自:https://blog.csdn.net/y13156556538/article/details/64152066/

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引