📜 ⬆️ ⬇️

Developing (soccer) games using MonoGame

Everyone wants to develop games, and this is not surprising: they are popular and sell well. Who does not want to become famous and rich by making the next Angry Birds or Halo? In reality, however, game development is one of the most difficult tasks in programming, because in the game you need to pick up such a combination of graphics, sound and gameplay so that it captures the user.
To make life easier for game developers, a variety of frameworks are created, not only for C and C ++, but also for C # and even JavaScript. One of these frameworks is Microsoft XNA, which uses Microsoft DirectX technology and allows you to create games for Xbox 360, Windows, and Windows Phone. Microsoft XNA is no longer evolving, but at the same time, the Open Source community offered another option - MonoGame. Let's take a closer look at this framework using the example of a simple football (what is this?) Game.

What is MonoGame?

MonoGame is an open source implementation of the XNA API not only for Windows, but also for Mac OS X, Apple iOS, Google Android, Linux and Windows Phone. This means that you can create games immediately under all these platforms with minimal changes. Ideal for those making plans to capture the world!
You don't even need Windows to develop with MonoGame. You can use MonoDevelop (open source cross-platform IDE for Microsoft .NET languages) or cross-platform IDE Xamarin Studio to work on Linux and Mac.
If you are a Microsoft .NET developer and use Microsoft Visual Studio daily, MonoGame can be installed there. At the time of this writing, the latest stable version of MonoGame was 3.2, which is installed in Visual Studio 2012 and 2013.

Create the first game

To create the first game, in the template menu, select MonoGame Windows Project. Visual Studio will create a new project with all the necessary files and links. If you run the project, you will get something like this:
Boring, isn't it? Nothing, this is just the beginning. You can start developing your game in this project, but there is one nuance. You cannot add any objects (images, sprites, fonts, etc.) without converting them to MonoGame compatible format. To do this, you need something from the following:

So, in Program.cs you have the function Main. It initializes and launches the game.
static void Main() { using (var game = new Game1()) game.Run(); } 

Game1.cs is the core of the game. You have two methods that are called 60 times per second: Update and Draw. With Update you recalculate the data for all elements of the game; Draw, respectively, implies the drawing of these elements. Note that the time for iteration of the cycle is given quite a bit - only 16.7 ms. If there is not enough time to perform the cycle, the program will skip several Draw methods, which, naturally, will be noticeable in the picture.
For example, we will create a football game “score a penalty.” The kick will be controlled by our touch, and the computer “goalkeeper” will try to catch the ball. The computer selects a random location and speed "goalkeeper." Points are considered to be our usual way.

Add content to the game

The first step in creating a game is to add content. Let's start with the field pattern and the ball pattern. Create two PNG patterns: the fields (below) and the ball (on the KDPV).
')


To use these images in the game, they need to compile. If you are using the XNA Game Studio or the Windows Phone 8 SDK, you need to create an XNA content project. Add pictures to this project and assemble it. Then go to the project directory with copy the resulting .xnb files into your project. XNA Content Compiler does not require a new project, objects in it can be compiled as needed.
When .xnb files are ready, add them to the Content folder of your game. Create two fields in which we will store the ball and field textures:
 private Texture2D _backgroundTexture; private Texture2D _ballTexture; 


These fields are loaded in the LoadContent method:
 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"); } 

Now draw the textures in the Draw method:
 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); } 

This method fills the screen with green, and then draws the background and the ball on the penalty point. The first method spriteBatch Draw draws the background, fitted to the size of the window, the second method draws the ball on the penalty point. There is no movement here yet - you need to add it.

Ball movement

To move the ball, you need to recalculate its location in each iteration of the cycle and draw it in a new location. Let's calculate the new position in the Update method:
 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); } 

The position of the ball is updated in each cycle by subtracting 3 pixels. The variables _screenWidth, _screenHeight, _backgroundRectangle, _ballRectangle and _ballPosition are initialized in the ResetWindowSize method:
 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); } 

This method resets all variables depending on the window size. It is called in the Initialize method.
 protected override void Initialize() { // TODO: Add your initialization logic here ResetWindowSize(); Window.ClientSizeChanged += (s, e) => ResetWindowSize(); base.Initialize(); } 

This method is called in two places: at the beginning and every time the window is resized. If you run the program, you will notice that the ball moves straight, but does not stop with the end of the field. You need to move the ball when it flies into the goal with the following code:
 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 _goalLinePosition variable is also initialized in the ResetWindowSize method.
 _goalLinePosition = _screenHeight * 0.05; 

In the Draw method, you also need to remove all calculations:
 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); } 

The movement of the ball is perpendicular to the goal line. If you want to move the ball at an angle, create the _ballPositionX variable and increase it (to move to the right) or decrease (to move to the left). Another best option is to use Vector2 to position the ball:
 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); } 

If you run the program now, you will see that the ball flies at an angle. The next task is to attach finger control.


We implement management

In our game, control is exercised with a finger. The movement of the finger sets the direction and force of impact.
In MonoGame, sensory data is obtained using the TouchScreen class. You can use raw data or Gestures API. Raw data provides more flexibility because you have access to all of the information, the Gestures API transforms raw data into gestures, and you can filter only those that you need.
In our game, we need only a click and, since the Gestures API supports this movement, we will use it. First of all, let’s indicate with what gesture we will use:
 TouchPanel.EnabledGestures = GestureType.Flick | GestureType.FreeDrag; 

Only clicks and dragging will be handled. Next, in the Update method, we will process gestures:
 if (TouchPanel.IsGestureAvailable) { // Read the next gesture GestureSample gesture = TouchPanel.ReadGesture(); if (gesture.GestureType == GestureType.Flick) { … } } 

Enable click in the Initialize method:
 protected override void Initialize() { // TODO: Add your initialization logic here ResetWindowSize(); Window.ClientSizeChanged += (s, e) => ResetWindowSize(); TouchPanel.EnabledGestures = GestureType.Flick; base.Initialize(); } 

Until now, the ball rolled all the time while the game was being played. Use the _isBallMoving variable to tell the game when the ball is moving. In the Update method, when a click is detected, set _isBallMoving to True and the motion will begin. When the ball recounts the goal line, set _isBallMoving to False and return the ball to its original position:
 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); } 

The speed of the ball is no longer constant; the program uses the _ballVelocity variable to set the speed along the x and y axes. Gesture.Delta returns movement change since the last update. To calculate your click speed, multiply this vector by TargetElapsedTime.
If the ball moves, the _ballPosition vector changes based on the speed (in pixels per frame) until the ball reaches the goal line. The following code stops the ball and removes all gestures from the input queue:
 _isBallMoving = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); 

Now if you start the ball, it will fly in the direction of the click and with its speed. However, there is one problem: the program does not look where the click occurred on the screen. You can click anywhere and the ball will start moving. The solution is to use raw data, get a touch point and see if it is near the ball. If yes, the gesture sets the _isBallHit variable:
 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); } 

Then the movement starts only if _isBallHit is True:
 if (TouchPanel.IsGestureAvailable && _isBallHit) 

There is another problem. If you hit the ball too slowly or in the wrong direction, the game will end, as the ball will not cross the goal line and will not return to its original position. It is necessary to set the limit time of the ball. When the timeout is reached, the game starts again:
 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(); } 

Adding a goalkeeper

Our game works - we will now make it more difficult by adding a goalkeeper who will move after we hit the ball. The goalkeeper is a picture in PNG format, we first compile it.

The goalkeeper is loaded in the LoadContent method:
 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 it in the Draw method.
 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 is the goalkeeper's rectangle in the window. It is changed in the Update method:
 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); } 

The _goalkeeperPositionY, _goalKeeperWidth and _goalKeeperHeight fields variables are updated in the ResetWindowSize method:
 private void ResetWindowSize() { … _goalkeeperPositionY = (int) (_screenHeight*0.12); _goalKeeperWidth = (int)(_screenWidth * 0.05); _goalKeeperHeight = (int)(_screenWidth * 0.005); } 

The initial position of the goalkeeper is in the center of the window in front of the goal line:
 _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth)/2; 

The goalkeeper starts moving with the ball. He performs harmonic oscillations from side to side:
X = A * sin (at + δ),
Where A is the oscillation amplitude (goal width), t is the oscillation period, and δ is a random variable so that the player cannot predict the goalkeeper’s movement.
Odds are calculated at the moment of hitting the ball:
 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; } 

The coefficient a is the goalkeeper’s speed, a number between 0 and 0.005, representing a speed between 0 and 0.3 pixels per second. The delta factor is a number between 0 and π / 2. When the ball moves, the position of the goalkeeper is updated:
 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); … } 

The amplitude of movement is _screenWidth * 0.11 (gate width). Add (_screenWidth * 0.75) / 2.0 + _screenWidth * 0.11 to the result so that the goalkeeper moves in front of the goal.

Checking whether the ball is caught and adding an account

To check whether the ball is caught or not, you need to see if the rectangles of the goalkeeper and the ball intersect. We do this in the Update method after calculating the positions:
 _ballRectangle.X = (int)_ballPosition.X; _ballRectangle.Y = (int)_ballPosition.Y; _goalkeeperRectangle = new Rectangle(_goalkeeperPositionX, _goalkeeperPositionY, _goalKeeperWidth, _goalKeeperHeight); if (_goalkeeperRectangle.Intersects(_ballRectangle)) { ResetGame(); } 

ResetGame returns the game to its original state:
 private void ResetGame() { _ballPosition = new Vector2(_initialBallPosition.X, _initialBallPosition.Y); _goalkeeperPositionX = (_screenWidth - _goalKeeperWidth) / 2; _isBallMoving = false; _isBallHit = false; while (TouchPanel.IsGestureAvailable) TouchPanel.ReadGesture(); } 

Now you need to check whether the ball hit the goal. Do this when the ball crosses the line:
 var isTimeout = timeInMovement > 5.0; if (_ballPosition.Y < _goalLinePosition || isTimeout) { bool isGoal = !isTimeout && (_ballPosition.X > _screenWidth * 0.375) && (_ballPosition.X < _screenWidth * 0.623); ResetGame(); } 

To add account management, you need to add a new object to the game - a font with which numbers will be written. A font is an XML file that describes the font: type, size, style, etc. In the game we will use this font:
 <?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> 

You must compile this font and add the resulting XNB file to the Content folder of your project:
 _soccerFont = Content.Load<SpriteFont>("SoccerFont"); 

In ResetWindowSize, reset the account position:
 var scoreSize = _soccerFont.MeasureString(_scoreText); _scorePosition = (int)((_screenWidth - scoreSize.X) / 2.0); 

To store the score, we define two variables: _userScore and _computerScore. _userScore increases when a player scores a goal, _computerScore - when a player misses, the time limit expires or the goalkeeper catches the ball.
 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 recreates the text of the account and sets its position:
 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 measures the length of the counting string, this value is used to calculate the position. The score is drawn in the Draw method:
 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); } 

Stadium lights

As a final touch, we’ll add lights to the game to turn on the stadium lights when it’s dark inside. Let's use for this the light sensor, which is now in many ultrabooks and transformers. To enable the sensor, you can use the Windows API Code Pack for the Microsoft .NET Framework, but we will go another way: using the WinRT Sensor API. Although this API is written for Windows 8, it can also be used in desktop applications.
Select your project in Solution Explorer, right-click and select Unload Project. Then press the right button again - Edit project. In the first PropertyGroup, add the TargetPlatFormVersion tag:
 <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> … <FileAlignment>512</FileAlignmen> <TargetPlatformVersion>8.0</TargetPlatformVersion> </PropertyGroup> 

Press the right button again and select Reload Project. Visual Studio will reload the project. When you add a new link to the project, you will see the Windows tab in the Reference Manager, as shown in the figure.

Add a link to Windows in the project. It will also require adding a reference to System.Runtime.WindowsRuntime.dll.
Now let's write the detection code of the light sensor:
 LightSensor light = LightSensor.GetDefault(); if (light != null) { 

If the sensor is present, you will get a nonzero value that you can use to determine the lightness:
 LightSensor light = LightSensor.GetDefault(); if (light != null) { light.ReportInterval = 0; light.ReadingChanged += (s,e) => _lightsOn = e.Reading.IlluminanceInLux < 10; } 

If the reading is less than 10, the _lightsOn variable is True and the background will be drawn somewhat differently. If you look at spriteBatch in the Draw method, you will see that its third parameter is color. Previously, we always used white, but the background color does not change. If you use any other color, the background color will change. Choose green when the lights are off and white when on. Let's make changes to the Draw method:
 _spriteBatch.Draw(_backgroundTexture, rectangle, _lightsOn ? Color.White : Color.Green); 

Now our field will be dark green when the lights are off and light green when turned on.

The development of our game is over. Of course, it is necessary to finish something in it: to come up with an animation when the ball is hammered, to add a ball bounce from the bar and so on. Let it be your homework.
Original article on Intel Developer Zone

Source: https://habr.com/ru/post/227667/


All Articles