C#使用 MonoGame* 开发游戏
发表于2018-05-31
全球各地的开发人员都希望开发游戏。为什么不呢?游戏是计算机历史上销量最高的产品之一,游戏业务带来的财富不断吸引着开发人员的加入。作为开发人员,我当然希望成为下一个开发愤怒的小鸟*或光晕*的开发人员。
但是,事实上,游戏开发是软件开发最困难的领域之一。你不得不牢记那些从来不会使用的三角函数、几何和物理类。除此之外,你的游戏必须以吸引用户沉浸其中的方式来组合声音、视频和故事情节。然后,你需要再编写一行代码!
为了简化难度,开发游戏使用的框架不仅要能够使用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/