Posts mit dem Label XBox Controller werden angezeigt. Alle Posts anzeigen
Posts mit dem Label XBox Controller werden angezeigt. Alle Posts anzeigen

Sonntag, 20. Januar 2019

Nicht ich, sondern der Boden bewegt sich (MonoGame)


Für die Lösung, wie eine Figur auf der Karte Bewegt werden kann, ist davon abhängig, wieviel Leistung mein Ziel System hat. In den meisten Fällen hat man mehr als genügend Leistung, so dass man sich hierbei entscheiden kann. Für relativ schwache System, z.B. wie bei einem Arduino Esplora kann eine Figur auf dem Bildschirm bewegt werden, ohne einem starken flackern. Eine Karte auf dem Bildschirm zu bewegen sieht dagegen wieder schlecht aus, weil der Bildaufbau zu lange dauert. Und das bei einer Auflösung von 128x160 Pixeln.

Nicht die Figur, sondern die Karte bewegt sich
Nun zurück zum Eigentlichen. Die Figur ist immer mittig auf dem Bildschirm und bewegt sich auf einer Karte. Aber Technisch gesehen, wird die Karte bewegt und die Figur hat eine Laufanimation.

Neues MonoGame Projekt
Aktuell wird für dieses Beispiel eine Neue Solution angelegt mit dem Ziel einer UWP Anwendung. Die Zielplattform und Version hat jedoch keinen Effekt für das Beispiel. Der Grund für UWP ist, das die Lauffähigkeit auf PC, Tablet, Raspberry Pi 2&3 und wenn ich mich nicht irre, auch auf der Xbox funktionieren soll. Sollte die Anwendung/Spiel auch auf dem Windows Phone laufen, dann ist eine ältere Minimum Ziel Version von Windows 10 einzustellen.



Ordnung ist das halbe Leben
Als erstes brauchen wir eine Projekt Struktur und legen ein paar Ordner an. Components wird es später drei Komponenten geben, die für die Benutzereingaben, die Karteverarbeitung und das Zeichnen zuständig sind.





















Der Umfang für das Realisieren benötigt einige Zeilen Code, so das eine Aufteilung der Inhalte Notwendig sind. Deshalb kann zunächst in der 'Game1.cs' fast alles weg. Nur der Konstruktor bleibt.

 using Microsoft.Xna.Framework;  
 namespace ExampleWalkingOnMap  
 {  
   public class Game1 : Game  
   {  
     private GraphicsDeviceManager _graphics;  
     public Game1()  
     {  
       this._graphics = new GraphicsDeviceManager(this);  
       this.Content.RootDirectory = "Content";  
       this.Window.Title = "Example Walking on map";  
       this.IsMouseVisible = true;  
     }  
   }  
 } 

Eingabegerät
Für das erfassen von Tasteneingaben, wird die Klasse 'ComponentInputs.cs' und für die Steuerinformation 'InputData.cs' angelegt.








Zunächst muss die 'InputData.cs' bearbeitet werden, um die verarbeiteten Steuerinformationen weiter zutragen.

 using Microsoft.Xna.Framework;  
 namespace ExampleWalkingOnMap.Components.Inputs  
 {  
   public class InputData  
   {  
     public Vector2 Move => new Vector2(this.MoveX, this.MoveY);  
     public float MoveX { get; set; }  
     public float MoveY { get; set; }  
   }  
 } 

Mit der ‚InputData‘ können die Eingaben in ComponentInputs.cs aufgenommen werden. Im Beispiel wird die Tastatur und ein Xbox Controller eingelesen. Damit wäre die Eingabe Komponente fertig.

using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Input;  
 using System;  
 namespace ExampleWalkingOnMap.Components.Inputs  
 {  
   public class ComponentInputs : GameComponent  
   {  
     public InputData Inputs { get; private set; } = new InputData();  
     public ComponentInputs(Game game) : base(game) {  }  
     public override void Update(GameTime gameTime)  
     {  
       this.Inputs.MoveX = 0;  
       this.Inputs.MoveY = 0;  
       KeyboardState stateKeyboard = Keyboard.GetState();  
       this.Inputs.MoveX += stateKeyboard.IsKeyDown(Keys.A) ? 1 : 0;  
       this.Inputs.MoveX -= stateKeyboard.IsKeyDown(Keys.D) ? 1 : 0;  
       this.Inputs.MoveY += stateKeyboard.IsKeyDown(Keys.W) ? 1 : 0;  
       this.Inputs.MoveY -= stateKeyboard.IsKeyDown(Keys.S) ? 1 : 0;  
       GamePadState stateGamePad = GamePad.GetState(PlayerIndex.One);  
       this.Inputs.MoveX += stateGamePad.ThumbSticks.Left.X * -1;  
       this.Inputs.MoveY += stateGamePad.ThumbSticks.Left.Y;  
       // Normalize  
       this.Inputs.MoveX = Math.Min(1, Math.Max(-1, this.Inputs.MoveX));  
       this.Inputs.MoveY = Math.Min(1, Math.Max(-1, this.Inputs.MoveY));  
     }  
   }  
 }

Die Karte
Die Realisierung der Karte wird in Kacheln unterteilt und ist auch relativ leicht um zusetzten. Jede Kachel hat eine eigene Position und eine Textur, womit schon mal das nächste Objekt definiert ist.





















Für die Textur mit verschiedenen Untergründen wird eine Bilddatei zusammen gefasst, die die Unterschiedlichen Bodentexturen beinhaltet. Jedes Objekt kennt dann nur den Ausschnitt aus der Bilddatei. Für das Beispiel verwende ich die Texturen von https://kenney.nl/assets.











Im Projekt wurde bereits mit der Vorlage, der Ordner 'Content' angelegt. Hier wird nun die Textur abgelegt.






Nun muss die Bilddatei noch mit dem Pipline Tool verarbeitet werden, indem ihr doppelkick auf 'Content.mgcb' ausführt. Im Ausschnitt Projekt wird die Bilddatei hinzugefügt.
















Wirkt leider etwas doppelt, aber dies ist notwendig, damit die Textur später im Programmcode geladen werden kann.

















Anschließend auf den Button 'Build' oder F6 drücken und fertig ist dieser Teil und könnt das Tool  wieder schließen.















Nun zurück zum Code für die Kachel Information. Wie bereits erwähnt, kennt die Kachel nur die Position der Textur, die später über die Methode Draw hinein gereicht wird. Mit dem weiteren Parameter 'offset' wird bestimmt, wo im Fenster gerendert wird.

using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Graphics;  
 namespace ExampleWalkingOnMap.Components.Map  
 {  
   public class GroundTile  
   {  
     private Rectangle _texturePosition;  
     public GroundTile(int x, int y, int width, int height)  
     {  
       this._texturePosition = new Rectangle(x, y, width, height);  
     }  
     public void Draw(Texture2D textureMapTiles, SpriteBatch spriteBatch, Vector2 offset)  
     {  
       spriteBatch.Draw(textureMapTiles,  
                offset,  
                this._texturePosition,  
                Color.White,  
                0f,  
                new Vector2(0, 0),  
                new Vector2(1, 1),  
                SpriteEffects.None,  
                0f);  
     }  
   }  
 } 

Die Karte soll aus 10x10 Kacheln bestehen und wird in einer Hilfsklasse erstellt.






Mit dem zwei Dimensionalen Integer Array, lässt sich visuell schnell eine Karte dieser Größe  zusammenstellen, die wiederum noch übersichtlich ist. Zumindest im ersten Abschnitt der Methode. Anschließend wird hier in das Ziel Array gemappt mit den eigentlichen Ziel Objekt.

using System;  
 namespace ExampleWalkingOnMap.Components.Map  
 {  
   internal class MapHelper  
   {  
     internal static GroundTile[,] CreateMap()  
     {  
       int[][] map = new int[10][];  
       map[0] = new int[10] { 0, 0, 0, 0, 0, 0, 0, 0, 2, 2 };  
       map[1] = new int[10] { 0, 0, 0, 0, 0, 0, 0, 0, 2, 2 };  
       map[2] = new int[10] { 1, 1, 1, 1, 1, 1, 1, 0, 2, 2 };  
       map[3] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[4] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[5] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[6] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 2, 2 };  
       map[7] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 };  
       map[8] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 3, 3 };  
       map[9] = new int[10] { 0, 0, 0, 0, 0, 0, 1, 0, 3, 0 };  
       GroundTile[,] groundTiles = new GroundTile[10, 10];  
       for (int iY = 0; iY < 10; iY++)  
       {  
         for (int iX = 0; iX < 10; iX++)  
         {  
           groundTiles[iY, iX] = GetMappingGroundTile(map[iY][iX]);  
         }  
       }  
       return groundTiles;  
     }  
     private static GroundTile GetMappingGroundTile(int groundTile)  
     {  
       switch (groundTile)  
       {  
         case 0:  
           return new GroundTile(0, 0, 64, 64);  
         case 1:  
           return new GroundTile(64, 0, 64, 64);  
         case 2:  
           return new GroundTile(0, 64, 64, 64);  
         case 3:  
           return new GroundTile(64, 64, 64, 64);  
       }  
       throw new ArgumentException("groundTile can be only 0, 1, 2 and 3");  
     }  
   }  
 }  

Für die Verwaltung der Karte wird die Klasse 'ComponentMap.cs' erstellt. Dort werden die Karteninhalte gesteuert und gehalten.






Der Umfang der Klasse ist hier größer als bei den anderen, auch wenn dessen Ausführung sehr klein gehalten ist.

using ExampleWalkingOnMap.Components.Inputs;  
 using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Graphics;  
 namespace ExampleWalkingOnMap.Components.Map  
 {  
   public class ComponentMap : GameComponent  
   {  
     private readonly ComponentInputs _inputs;  
     private GroundTile[,] _mapTiles;  
     private Texture2D _textureMapTiles;  
     private int _mapTileWidth, _mapTileHeight;  
     private Vector2 _screenOffset, _movePosition;  
     public Vector2 Position => new Vector2(this._movePosition.X + this._screenOffset.X,  
                         this._movePosition.Y + this._screenOffset.Y);  
     private int _mapWidth, _mapHeight;  
     public ComponentMap(Game game, ComponentInputs inputs) : base(game) => this._inputs = inputs;  
     internal void SetScreenOffset(float screenWidth, float screenHeight)  
     {  
       var centerScreenWidth = screenWidth / 2;  
       var centerScreenHeight = screenHeight / 2;  
       this._screenOffset = new Vector2(centerScreenWidth, centerScreenHeight);  
       this._movePosition = new Vector2(0, 0);  
     }  
     public override void Initialize()  
     {  
       using (var stream = TitleContainer.OpenStream("Content/MapTiles.png"))  
       {  
         this._textureMapTiles = Texture2D.FromStream(this.Game.GraphicsDevice, stream);  
       }  
       this._mapTiles = MapHelper.CreateMap();  
       this._mapTileWidth = this._textureMapTiles.Width / 2;  
       this._mapTileHeight = this._textureMapTiles.Height / 2;  
       this._mapWidth = this._mapTileWidth * this._mapTiles.GetLength(0);  
       this._mapHeight = this._mapTileHeight * this._mapTiles.GetLength(1);  
     }  
     public override void Update(GameTime gameTime)  
     {  
       this._movePosition += this._inputs.Inputs.Move * 2f;  
     }  
     public void DrawMapTiles(SpriteBatch spriteBatch)  
     {  
       for (int iY = 0; iY < this._mapTiles.GetLength(0); iY++)  
       {  
         for (int iX = 0; iX < this._mapTiles.GetLength(1); iX++)  
         {  
           Vector2 textureOffset = new Vector2(iX * this._mapTileWidth, iY * this._mapTileHeight);  
           Vector2 offset = this.Position + textureOffset;  
           this._mapTiles[iY, iX].Draw(this._textureMapTiles, spriteBatch, offset);  
         }  
       }  
     }  
   }  
 }

In der Update Methode wird die Bewegung mit 2 Multipliziert. Wenn die Bewegung zu langsam erscheint, dann kann hier der Wert erhöht werden. Wie bereits angemerkt, zeigt dieser Teil das Nötigste, um eine Karte anzulegen und für das Rendern vorzubereiten.

Rendern
Am Ende muss die Karte auf dem Bildschirm gerendert werden. Dazu wird noch eine weitere Komponente angelegt mit dem Namen 'ComponentRender.cs'.




Damit die Karte auf die Start Position in die Mitte zentriert wird, wird die Größe des Fensters eingelesen und dessen Werte für den Offset der Karte zugewiesen.

using ExampleWalkingOnMap.Components.Map;  
 using Microsoft.Xna.Framework;  
 using Microsoft.Xna.Framework.Graphics;  
 using Windows.UI.ViewManagement;  
 namespace ExampleWalkingOnMap.Components.Render  
 {  
   public class ComponentRender : DrawableGameComponent  
   {  
     private readonly ComponentMap _componentMap;  
     private SpriteBatch _spriteBatch;  
     public ComponentRender(Game game, ComponentMap componentMap) : base(game)  
     {  
       this._componentMap = componentMap;  
     }  
     public override void Initialize()  
     {  
       this._spriteBatch = new SpriteBatch(this.GraphicsDevice);  
       var screenWidth = (float)ApplicationView.GetForCurrentView().VisibleBounds.Width;  
       var screenHeight = (float)ApplicationView.GetForCurrentView().VisibleBounds.Height;  
       this._componentMap.SetScreenOffset(screenWidth, screenHeight);  
     }  
     public override void Draw(GameTime gameTime)  
     {  
       this.GraphicsDevice.Clear(Color.DarkBlue);  
       this._spriteBatch.Begin();  
       this._componentMap.DrawMapTiles(this._spriteBatch);  
       this._spriteBatch.End();  
     }  
   }  
 }  

Zurück zum Start
Am Ende müssen die Komponenten in die GameComponentCollection hinzugefügt werden, damit die Inhalte ausgeführt werden. Dazu geht es wieder in die 'Game1.cs' Code Datei. Hier werden in den Member die Komponenten eingetragen und im Konstruktor wird die 'UpdateOrder' zugewiesen und schließlich in die Collection aufgenommen.

 using ExampleWalkingOnMap.Components.Inputs;  
 using ExampleWalkingOnMap.Components.Map;  
 using ExampleWalkingOnMap.Components.Render;  
 using Microsoft.Xna.Framework;  
 namespace ExampleWalkingOnMap  
 {  
   public class Game1 : Game  
   {  
     private GraphicsDeviceManager _graphics;  
     private ComponentInputs _componentInputs;  
     private ComponentMap _componentMap;  
     private ComponentRender _componentRender;  
     public Game1()  
     {  
       this._graphics = new GraphicsDeviceManager(this);  
       this.Content.RootDirectory = "Content";  
       this.Window.Title = "Example Walking on map";  
       this.IsMouseVisible = true;  
       this._componentInputs = new ComponentInputs(this);  
       this._componentInputs.UpdateOrder = 1;  
       this.Components.Add(this._componentInputs);  
       this._componentMap = new ComponentMap(this, this._componentInputs);  
       this._componentMap.UpdateOrder = 2;  
       this.Components.Add(this._componentMap);  
       this._componentRender = new ComponentRender(this, this._componentMap);  
       this._componentRender.UpdateOrder = 3;  
       this.Components.Add(this._componentRender);  
     }  
   }  
 }  

Wenn ich hier im Blog-Post keinen Code ausgelassen habe, dann sollte sich die Anwendung starten lassen und die Karte mit den Tasten AWSD sich bewegen lassen. Oder auch mit dem Xbox Controller.



Nachwort
Mit ein wenig mehr Code, kann man dann einige Unreinheiten beheben, wie z.B. die Übergänge von Kachel zu Kachel sehen nicht immer sauber aus oder das der Untergrund Einfluss auf die Laufgeschwindigkeit hat. Und Überhaupt fehlt die Laufende Figur in der Mitte.

Die Anwendung läuft soweit auch auf einem Raspberry Pi mit Windows 10 IoT. Jedoch sollte man keine hohen Performance erwarten, da hier keine DirectX Unterstützung vorhanden ist. Im Folgenden Video zeige ich die Anwendung  auf einem Bildschirm mit einer Eingestellten Auflösung von 640x350 Pixeln (Nativ sind jedoch 480x320). In HD, also 1920x1080p, sind weniger als fünf Bilder pro Sekunde zu erwarten. Zudem ist der Boot Vorgang relativ lang und bis die MonoGame App gestartet ist. Da vergehen schon einige Minuten.
Der Aufbau und das Prinzip funktioniert auch mit Windows Forms, WPF und XAML. Auch wenn diese UI Technologien nicht dafür gedacht sind, kann man sehen zumindest sehen wie Leistungsfähig die anderen sind.

Die fertige Solution habe ich wie immer auf dem GitHub geladen. Zusätzlich sind die meisten Inhalte kommentiert.




Dienstag, 10. April 2018

Gamepad per UWP


Hätte ich ein wenig mehr Recherchiert, dann hätte ich gleich eine UWP Anwendung geschrieben. Mit Visual Studio kann man schon seit Windows 8.1 UWP Anwendungen schreiben, die für PC, Tablet und Windows Phone funktionieren. Klar, Windows Phone ist vielleicht nicht mehr interessant, aber man kann das Smartphone immer noch zum Basteln verwenden.

Suche nach bestehenden Lösungen
Vor einigen Wochen suchte ich eine Lösung, wie man andere GameController einlesen kann und stieß auf einen anderen Blog Namens "Sander van de Velde", der über die Klasse Gamepad in UWP Anwendung schrieb. Kurz gelesen und gleich probiert und siehe da, es geht. Man kann sogar mit dem Xbox Controller die eigene Anwendung Teilweise sogar Bedienen. Egal ob auf PC, Tablet oder Raspberry Pi. 


Für den Einstieg
Als Beispiel wird ein Punkt auf der Anwendung bewegt. Der Aufbau zeigt, welche Grundlegende Programmcodezeilen Notwendig sind, um die Eingabe vom Xbox Controller zu nutzen.

Gamepad erkennen
Wenn der Xbox Controller mit dem Ziel Gerät verbunden wird, dann wird das GamepadAdded Event gesendet. Das gleiche gilt auch wenn der Controller getrennt wird (GamepadRemoved). Hinweis: MainViewModel habe ich nicht aufgeführt (siehe ggf. Github).

 // ist eigentlich bei einem so kleinen Beispiel nicht Notwendig ein ViewModel zu verwenden 
 private MainViewModel _viewModel = new MainViewModel();  
 // Ermöglicht eine Schleife Asynchron zum Haupteil der Anwendung auszuführen.  
 private Task _run;  
 // Hauptklasse für das einlesen des Xbox Controllers.  
 private Gamepad _gamePad;  
 // Wird nur für den zufälligen Farbwechsel verwendet.  
 private Random _random = new Random();  
 public MainPage()  
 {  
   this.InitializeComponent();  
   this.DataContext = this._viewModel;  
   Gamepad.GamepadAdded += this.Gamepad_GamepadAdded;  
   Gamepad.GamepadRemoved += this.Gamepad_GamepadRemoved;  
   this.MovingPoint.Fill = new SolidColorBrush(Colors.Black);  
 }  
 // Wenn der Xbox Controller getrennt wird.  
 private void Gamepad_GamepadRemoved(object sender, Gamepad e) =&gt; this._gamePad = null;  
 // Wird ausgeführt, wenn ein Xbox Controller verbunden wurde.  
 private void Gamepad_GamepadAdded(object sender, Gamepad e)  
 {  
   this._gamePad = e;  
   // … Ab hier kann der Controller ausgelesen werden.  
 }  

Gamepad einlesen und Eingabe Werte verwenden
Das Auslesen gibt ein Typ von GamepadReading zurück. Dieser enthält alle Eingaben, die zu diesen Zeitpunkt des Auslesen getätigt wurden.
Die UI und das Auslesen befinden sich in zwei Ebenen und laufen somit Asynchrone zu einander. Damit die umgerechneten Werten an die UI Elemente zugewiesen werden können, wird der Dispatcher verwendet.

 // Liest die den Xbox Controller aus. Für den Vorgang wird eine Schleife verwendet.  
 private async void ReadingGamepad()  
 {  
   while (true)  
   {  
     if(this._gamePad == null) { break; }  
     // Aktuelle Eingabe erfassen.  
     GamepadReading gp = this._gamePad.GetCurrentReading();  
     // UI und der Task laufen in verschiedenen Ebenen.   
     // Daher muss über den Dispatcher die Eingabe Daten   
     // und die Daten aus dem ViewModel Bindung Async behandelt werden.  
     await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>  
     {  
       // Linken Stick einlesen  
       var left = gp.LeftThumbstickX;  
       var top = gp.LeftThumbstickY * -1;  
       // Abstand zum Rand  
       var distanceToBorder = 30;  
       // Eingabe zu einer Position auf der Anwendung umrechnen.  
       var halfLeft = (this.ActualWidth - (distanceToBorder * 2)) / 2d;  
       left = halfLeft + (left * halfLeft);  
       var halfTop = (this.ActualHeight - distanceToBorder * 2) / 2d;  
       top = halfTop + (top * halfTop);  
       // verhindern, dass der Punkt den Rand nicht erreicht.  
       if (left < distanceToBorder) { left = distanceToBorder; }  
       if (left > this.ActualWidth) { left = this.ActualWidth; }  
       if (top < distanceToBorder) { top = distanceToBorder; }  
       if (top > this.ActualHeight - distanceToBorder) { top = this.ActualHeight - distanceToBorder; }  
       // Position als Text Ausgeben mit bis zu zwei Stellen hinter dem Komma.  
       this._viewModel.TextPosition = $"LEFT: {left:N2}, TOP: {top:N2}";  
       // leider kein Binding, daher die simple zuweisung der Ziel Position und Farbe  
       this.MovingPoint.Margin = new Thickness(left, top, 0, 0);  
       // Farbe wechseln  
       if (gp.Buttons == GamepadButtons.A)  
       {  
         // verkürzte die schreibweise  
         byte f() => (byte)this._random.Next(0, 255);  
         this.MovingPoint.Fill = new SolidColorBrush(Color.FromArgb(255, f(), f(), f()));  
       }  
     });  
     // 2 Millisekunden interval  
     await Task.Delay(2);  
   }  
 }  

Fazit
Das war alles. Und dazu war keine zusätzliche GameEngine Notwendig, um die Eingabedaten vom Xbox Controller zu lesen. Auf dem PC und Tablet funktioniert dies auch Kabellos. Nur mit dem Raspberry Pi 2 und 3 konnte ich die Verbindung nur mit einem USB Micro herstellen. Auf Windows Phone (Lumia 930) ließ sich die Anwendung starten, aber es konnte keine Bluetooth Verbindung hergestellt werden.

Mittwoch, 17. Januar 2018

Zeichnen auf dem TFT mit dem XBox Controller

 
Im ersten Beispiel wurde beschrieben, wie man die Daten von einem Xbox Controller zu dem Arduino sendet und dabei einen Servo ansteuerte. Nachteil an der Code Konstellation war, dass nur ein Byte Wert übermittelt wurde und auch sehr instabil lief. Mit ein paar Anpassungen lässt sich dies einfach beheben. (siehe: Servo mit dem Xbox Controller steuern)

Hinweis
Das folgende Beispiel ist vom Aufbau relativ simple gehalten, so dass bei der Ausgabe Kompromisse eingegangen werden, wie z.B. die Daten Qualität über die Serielle Schnittstelle.

Mehr Daten Senden
Das MonoGame Projekt muss nur in der Update Methode angepasst werden. Zum einen wird geprüft, ob noch Bytes geschrieben werden. Dann können die Eingabewerte vom Xbox Controller eingelesen werden und über die Serielle Verbindung versendet werden.

 protected override void Update(GameTime gameTime)  
 {  
   GamePadState state = GamePad.GetState(PlayerIndex.One);  
   if (state.Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))  
   {  
     this.Exit();  
   }  
   if (this._serialPort.BytesToWrite == 0)  
   {  
     byte stickValueX = (byte)((state.ThumbSticks.Left.X + 1) * 90);  
     byte stickValueY = (byte)((state.ThumbSticks.Left.Y + 1) * 90);  
     byte buttonValuePaint = (byte)(state.Buttons.A == ButtonState.Pressed ? 10 : 0);  
     byte buttonValueReset = (byte)(state.Buttons.B == ButtonState.Pressed ? 10 : 0);  
     this._serialPort.Write(new byte[] { stickValueX, stickValueY, buttonValuePaint, buttonValueReset }, 0, 4);  
     this._serialPort.WriteLine(string.Empty);  
   }  
   Task.Delay(5);  
   base.Update(gameTime);  
 }  

Mehr Daten Empfangen
Die Lösung ist simple. Um mehrere Bytes einzulesen, müssen diese über einen Schleifen Vorgang vom Serial gelesen werden. Die Byte Werte werden dann zunächst in eine lokales Byte Array zugewiesen. Anschließend kann noch geprüft werden ob alle Bytes gelesen wurden. Die ersten zwei Werte sollten zumindest da sein, ansonsten kann der zu zeichnende Punkt (eigentlich ein Kreuz) nicht bewegt werden.

 bool ReadReceivedBytes() {  
  byte received[4];  
  int index = 0;  
  while(Serial.available() &gt; 0) {  
   if(index < 5) {  
    received[index] = Serial.read();  
   }  
   index++;  
  }  
  if(index < 4) {  
   return false;  
  }  
  mReceivedStickX = received[0];  
  mReceivedStickY = received[1];  
  mReceivedButtonA = received[2];  
  mReceivedButtonB = received[3];  
  return true;  
 }  

Ausgabe und Fehler
Die Ausgabe erfolgt diesmal nicht an einem Servo, sondern an einem TFT Display mit einer Auflösung von 160x128 Pixeln, der auch für den Arduino Esplora ebenfalls verwendet wird.
Die Übertragung ist nicht immer Fehlerfrei, womit für genauere Ergebnisse eine Fehlerbehandlung notwendig wird. Aber das wird in einem anderen Blog Post beschrieben. :)



Samstag, 18. November 2017

Servo mit dem Xbox Controller steuern


Das ansteuern eines Servos über einen Xbox One Controller ist simpel umzusetzen. Für dieses Beispiel wird folgendes verwendet:

  • MonoGame
  • Xbox Controller
  • Arduino UNO oder vergleichbar
  • Servo
  • Motor Shield
  • Externe Batterie

Nach der Installation von MonoGame sind in Visual Studio mehre Vorlagen verfügbar. Benötigt wird das Template 'MonoGame Windows Project', das im folgenden Bild als erstes in der Liste erscheint.


Programmcode mit MonoGame
Sobald das Projekt angelegt würde, könnt ihr die Game1.cs Datei öffnen. Für die Verbindung zum Arduino wird die Klasse SerialPort verwendet. Dazu sollte vorher bekannt sein, welcher COM Port bei euch der Arduino verwendet. Die Baudrate von 115200 ist die maximale Geschwindigkeit, die zuverlässig funktioniert. Die restlichen Parameter sind die Default Werte der Seriellen Verbindung zum Arduino (siehe Programmcode).
Die überschriebenen Methoden 'Initialization()', 'LoadContent()' und 'Draw()' werden für das Beispiel nicht verwendet und können entfernt werden.
Mit der Update Methode wird der Xbox Controller eingelesen. Der Input Wert vom Stick kommt dann aus dem GamePadState unter '.ThumbSticks.Left.X'. Der Float Wert geht von -1 bis +1 und wird deshalb zunächst um plus eins dass Offset verschoben. Anschießend wird der Float Wert in ein Byte Wert transformiert, dass dann anschließend an den Serial Port geschrieben wird.
Damit die Anwendung nicht zu viele Bytes versendet, wird am Ende 50 Millisekunden gewartet. Ansonsten läuft der Buffer im Arduino über.

GraphicsDeviceManager _graphics;  
 private SerialPort _serialPort;  
 public Game1()  
 {  
   _graphics = new GraphicsDeviceManager(this);  
   // Verwendete COM Port muss ggf. angepasst werden.  
   this._serialPort = new SerialPort("COM5", 115200, Parity.None, 8, StopBits.One);  
   this._serialPort.Open();  
 }  
 protected override void UnloadContent()  
 {  
   this._serialPort.DiscardOutBuffer();  
   this._serialPort.Close();  
 }  
 protected override void Update(GameTime gameTime)  
 {  
   GamePadState state = GamePad.GetState(PlayerIndex.One);  
   byte stickValue = (byte)((state.ThumbSticks.Left.X + 1) * 90);  
   this._serialPort.Write(new byte[] { stickValue }, 0, 1);  
   Task.Delay(50).Wait();  
 }  

Arduino als Empfänger
Der Arduino muss nun die empfangenden Bytes vom PC entgegen nehmen und daraus ein PWM Signal erzeugen das zur Steuerung des Servos verwendet wird. Weiters habe ich im Programmcode beschrieben.

 #include <Servo.h>  
 Servo servo;  
 int servoPin = 9;  
 int receivedValue;  
 void setup() {  
  // Muss die selbe Baudrate haben,   
  // wie in der MonoGame Anwendung.  
  Serial.begin(115200);  
  // die meisten Servos arbeiten mit einem   
  // HIGH Signal von 1ms bis 2ms.  
  servo.attach(servoPin, 1000, 2000);  
 }  
 void loop() {  
  if( Serial.available() > 0){  
   receivedValue = Serial.read();  
   // Den eingelesenen Wert Abschneiden,  
   // falls der maximale Stellwert überschritten wird.  
   if(receivedValue > 180) {  
    receivedValue = 180;  
   }  
   else if(receivedValue < 0) {  
    receivedValue = 0;  
   }  
   // alle weiteren eingegangenen bytes werden verworfen  
   Serial.flush();  
  }  
  // Aktuellen Einstellwert festlegen.  
  servo.write(receivedValue);  
  delay(20);  
 }  

Was nun
Im Grunde ist die Ausführung simpel und das Schreiben dieses Post hat da deutlich mehr Zeit benötig. :D
Aber was kann man nun mit dem Wissen anstellen? Nun, man kann stattdessen einer  Kabelgebundenen Ausführung, auf eine Bluetooth Funkverbindung wechseln. Damit ließe sich dann ein RC Model ansteuern. Mag etwas Umständlich sein, die Fernsteuerung gegen etwas eigenes gebasteltes zu ersetzen, aber darin liegt der Spaß.

Zur Vollständigkeit wieder ein Link zu dem Github Repository und dem gesamten Beispiel Programmcode.

Nachtrag
Leider funktioniert dieses einfache Beispiel nur in dieser Konstellation. Sobald versucht wird, zwei Bytes zu versenden, treten Probleme auf mit der Seriellen Verbindung.
Wie man das behebt, beschreibe ich mit dem Post "Zeichnen auf dem TFT mit dem XBox Controller"





Ameisen Simulation und andere Dinge

Wer lange sich schon mit C# beschäftig, hat sicherlich schon mal was von AntMe gelesen oder gehört. Diese Idee hatte ich aufgegriffen und mi...