Splitscreen mit MonoGame


Spiele machen mehr Spaß, wenn man sie zu zweit spielen kann. Mit MonoGame lässt sich ein Splitscreen mit geringen Aufwand umsetzen. Hierfür verwende ich von mir bereits angelegten Beispiel Code "ExampleMoveOnMap3d" aus dem Blogpost "3d Karte Bewegen".

Eingabegeräte
Für die Steuerung werden als Beispiel Tastatur Eingaben und Xbox Controller aufgeteilt. Daher wird nun vom Typen InputData eine zweite Property angelegt und entsprechend benannt. Die Eingabe muss beim jeden Aufruf immer zurück gesetzt werden, deshalb kommt ein Reset, sowie eine Normalize Methode und halten damit unsere Übersicht in ComponentInput Klasse.

   public class InputData  
   {  
     public Vector2 Move => new Vector2(this.MoveX, this.MoveY);  
     public float MoveX { get; set; }  
     public float MoveY { get; set; }  
     public void Reset()  
     {  
       this.MoveX = 0;  
       this.MoveY = 0;  
     }  
     public void Normalize()  
     {  
       this.MoveX = Math.Min(1, Math.Max(-1, this.MoveX));  
       this.MoveY = Math.Min(1, Math.Max(-1, this.MoveY));  
     }  
   } 

Bei mehreren Gamepads, könnte man die Eingabeerfassung auch in der Schleife abfragen, aber bleiben wir zunächst beim Hartverdrahten.

public class ComponentInputs : GameComponent  
   {  
     public InputData Inputs1 { get; private set; } = new InputData();  
     public InputData Inputs2 { get; private set; } = new InputData();  
     public ComponentInputs(Game game) : base(game)  
     {  
     }  
     public override void Update(GameTime gameTime)  
     {  
       this.Inputs1.Reset();  
       this.Inputs2.Reset();  
       KeyboardState stateKeyboard = Keyboard.GetState();  
       this.Inputs1.MoveX += stateKeyboard.IsKeyDown(Keys.A) ? 1 : 0;  
       this.Inputs1.MoveX -= stateKeyboard.IsKeyDown(Keys.D) ? 1 : 0;  
       this.Inputs1.MoveY += stateKeyboard.IsKeyDown(Keys.W) ? 1 : 0;  
       this.Inputs1.MoveY -= stateKeyboard.IsKeyDown(Keys.S) ? 1 : 0;  
       GamePadState stateGamePad = GamePad.GetState(PlayerIndex.One);  
       this.Inputs2.MoveX += stateGamePad.ThumbSticks.Left.X;  
       this.Inputs2.MoveY += stateGamePad.ThumbSticks.Left.Y;  
       // invert   
       this.Inputs1.MoveX *= -1;  
       // Normalize  
       this.Inputs1.Normalize();  
       this.Inputs2.Normalize();  
     }  
   }  

Die Kartenbewegung
Die Karte muss nun zweimal geladen werden, damit beide Spieler eine Ansicht über ihre Karten Position erhalten. Eine "IF" Abfrage fragt die aktuelle Eingabe ab.
Die im Titelbild zu sehenden Fahrzeuge, habe ich hier nicht mit reingenommen, da der Inhalt ähnlich des Types PlateGrassTile ist. Allerdings ist es wiederum auf meinem Githab Repository zu finden.

public class ComponentMap : GameComponent  
   {  
     private const float _speed = 0.3f;  
     private readonly ComponentInputs _componentInputs;  
     private readonly int _player;  
     private PlateGrassTile[] _groundTiles = new PlateGrassTile[9];  
     public ComponentMap(Game game, ComponentInputs componentInputs, int player) : base(game)  
     {  
       this._componentInputs = componentInputs;  
       this._player = player;  
     }  
     public override void Initialize()  
     {  
       var grass = this.Game.Content.Load("Plate_Grass_01");  
       int index = 0;  
       for (int iY = 0; iY < 3; iY++)  
       {  
         for (int iX = 0; iX < 3; iX++)  
         {  
           this._groundTiles[index] = new PlateGrassTile(this.GetPosition(iX, iY),  
                                   0.5f,   
                                   grass);  
           index++;  
         }  
       }  
     }  
     public override void Update(GameTime gameTime)  
     {  
       Vector3 positon = new Vector3();  
       if (this._player == 1)  
       {  
         positon = new Vector3(this._componentInputs.Inputs1.MoveX, this._componentInputs.Inputs1.MoveY, 0) * _speed;  
       }  
       else  
       {  
         positon = new Vector3(this._componentInputs.Inputs2.MoveX, this._componentInputs.Inputs2.MoveY, 0) * _speed;  
       }  
       foreach (var item in this._groundTiles)  
       {  
         item.Position += positon;  
       }  
     }  
     public void DrawContent(Matrix view, Matrix projection)  
     {  
       foreach (var item in this._groundTiles)  
       {  
         item.Draw(view, projection);  
       }  
     }  
     private Vector3 GetPosition(int x, int y)  
     {  
       float mapLength = 3f;  
       float distance = .02f;  
       float centerMap = (mapLength + distance)   
                 * (float)Math.Sqrt(this._groundTiles.Length) / 2; ;  
       return new Vector3((y * (mapLength + distance)) - centerMap,  
                 (x * (mapLength + distance)) - centerMap,   
                 0);  
     }  
   }  

Von der 'ComponentMap' wird nun eine zweite Instance angelegt in der Game1.cs. Die 'UpdateOrder' muss dann entsprechend angepasst werden für den ComponentRender und im Konstruktor Parameter wird die zweite Instance eingereicht.

public class Game1 : Game  
   {  
     private readonly GraphicsDeviceManager _graphics;  
     private readonly ComponentInputs _componentInputs;  
     private readonly ComponentMap _componentMapPlayer1;  
     private readonly ComponentMap _componentMapPlayer2;  
     private readonly ComponentRender _componentRender;  
     public Game1()  
     {  
       this._graphics = new GraphicsDeviceManager(this);  
       this.Content.RootDirectory = "Content";  
       this.Window.Title = "Move on map 3d";  
       this.IsMouseVisible = true;  
       this._componentInputs = new ComponentInputs(this);  
       this._componentInputs.UpdateOrder = 1;  
       this.Components.Add(this._componentInputs);  
       this._componentMapPlayer1 = new ComponentMap(this, this._componentInputs, 1);  
       this._componentMapPlayer1.UpdateOrder = 2;  
       this.Components.Add(this._componentMapPlayer1);  
       this._componentMapPlayer2 = new ComponentMap(this, this._componentInputs, 2);  
       this._componentMapPlayer2.UpdateOrder = 3;  
       this.Components.Add(this._componentMapPlayer2);  
       this._componentRender = new ComponentRender(this, this._componentMapPlayer1, this._componentMapPlayer2);  
       this._componentRender.UpdateOrder = 4;  
       this.Components.Add(this._componentRender);  
     }  
   }  


Bildausgabe aufteilen
Nun kann auch schon der Render Bereich angepasst werden. Zunächst wird eine Anpassung an der 'CameraView' Klasse vorgenommen. In der Methode 'Initialize()' muss die Projektion auf die Hälfte reduziert werden. Für die Rechte Seite muss der zu anzufangende Viewport auf die Mitte des Bildschirms verschoben werden. In 'Draw()' muss dann der Eingerichtete ViewPort für den Renderbereich zugewiesen werden.

public class CameraView  
   {  
     private readonly Game _game;  
     private readonly ComponentMap _componentContent;  
     public Vector3 Position { get; set; }  
     public Matrix View { get; private set; }  
     public Matrix Projection { get; private set; }  
     private bool _isRightView = false;  
     private Viewport _viewPort;  
     public CameraView(Game game, ComponentMap componentMap, bool isRightView)  
     {  
       this._game = game;  
       this._componentContent = componentMap;  
       this._isRightView = isRightView;  
     }  
     public void Initialize()  
     {  
       this._viewPort = this._game.GraphicsDevice.Viewport;  
       this._viewPort.Width = this._viewPort.Width / 2;  
       if (this._isRightView)  
       {  
         this._viewPort.X = this._viewPort.Width;  
       }  
       var aspectRatio = this.GetAspectRatio();  
       var position = new Vector3(-1f, 5f, 5f);  
       var target = new Vector3(0, 0, 0);  
       var farPlaneDistance = 10000;  
       this.View = Matrix.CreateLookAt(position, target, Vector3.Backward);  
       this.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,  
                                     aspectRatio,  
                                     1,  
                                     farPlaneDistance);  
     }  
     public void Draw()  
     {  
       this._game.GraphicsDevice.Viewport = this._viewPort;  
       this._componentContent.DrawContent(this.View, this.Projection);  
     }  
     private float GetAspectRatio()  
     {  
       var w = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width / 2;  
       var h = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;  
       return w / h;  
     }  
   }

Aus einer 'CameraView' werden nun zwei. Beide haben nun den zusätzlichen Parameter mit der sich dann der Splitinhalt von Links oder Rechts einstellen lässt. Das Initialisieren der 'CamereView', muss dann über die zu überschreibende Methode 'Initialize()' abgearbeitet werden von der geerbten Klasse 'DrawableGameComponente'.

public class ComponentRender : DrawableGameComponent  
   {  
     private readonly CameraView _cameraViewLeft;  
     private readonly CameraView _cameraViewRight;  
     public ComponentRender(Game game, ComponentMap componentContent, ComponentMap componentContent2) : base(game)  
     {  
       this._cameraViewLeft = new CameraView(game, componentContent, false);   
       this._cameraViewRight = new CameraView(game, componentContent2, true);  
     }  
     public override void Initialize()  
     {  
       this._cameraViewLeft.Initialize();  
       this._cameraViewRight.Initialize();  
     }  
     public override void Draw(GameTime gameTime)  
     {  
       this.GraphicsDevice.Clear(Color.CornflowerBlue);  
       this._cameraViewLeft.Draw();  
       this._cameraViewRight.Draw();  
     }  
   }  

Nachwort
Wie zu sehen ist, ist die Umsetzung für Split Screen nicht sonderlich kompliziert und ist auch noch in wenigen Schritten machbar. Mit ein wenig mehr Aufwand kann das OpenClosePrinzip angewendet werden, womit dann anschließend eine Umsetzung eines vier Spieler Modus erleichtert.


Links ist die Tastatur, Rechts wird vom GamePad gesteuert.

Kommentare

Beliebte Posts aus diesem Blog

Arduino Control (Teil 5) - PWM Signal einlesen

RC Fahrtenregler für Lego Kettenfahrzeug

Angular auf dem Raspberry Pi