ProfilProfil
 Registrieren
 Login
Bild der WocheBild der Woche

(von Backslider)
Kommentare (0)
****

Weitere
User onlineBenutzer online
Gäste online: 14
Mitglieder online: Keine
Registrierte Mitglieder: 2116
Neustes Mitglied: onkel_keks

Zerstörbares 2D-Terrain

Autor: SniperED007 (Übersetzung: SteveKr)

Inhaltsverzeichnis

  1. Level erstellen
  2. Auf ans Programmieren
  3. Deformations-Sprite bewegen
  4. Level zerstören
  5. Ende

Dies ist eine Übersetzung des englischen Artikels Deformable Levels von Ziggyware.com
This article is a translation of the English article Deformable Levels from Ziggyware.com

Eine russische Übersetzung findet sich hier: Разрушаемый уровень в 2D

^ Level erstellen


Zunächst legen wir ein neues XNA-Projekt an. Anschließend erstellen wir das Level für unser Spiel. In diesem Artikel verwenden wir dazu Paint.Net, aber natürlich kann man auch jedes andere Grafikprogramm benutzen. Die Größe des Bildes legen wir auf 800x600 fest, da das auch die Größe ist, die ein mit XNA erstelltes Fenster standardmäßig besitzt.

Fangen wir nun also an das Level zu erstellen. Dazu selektieren wir den Farbpinsel, deaktivieren das Anti-Aliasing und zeichnen nun die Umrisse der Landschaft (vorzugsweise in Schwarz).
Jetzt sollten wir etwas ähnliches wie das hier sehen:


Bild 1: Levelumriss


Nun markieren wir den Bereich oberhalb der schwarzen Linie mit dem Zauberstab und drücken die Entf-Taste. Der obere Bereich sollte nun in Grau-Weiße-Karierte-Boxen geändert sein, die den Alpha-Layer repräsentieren.

Anschließend wählen wir die gewünschte Farbe für das Level aus und füllen den unteren Bereich mittels Farbeimer.


Bild 2: Level


Damit sind wir soweit fertig und können das Bild als PNG-Datei mit dem Namen "level.png" abspeichern.

Als nächstes kümmern wir uns nun um Deformationsbild. Das Bild soll 128x128 Pixel groß sein und besteht aus einem Kreis. Dazu verwenden wir das Ellipse-Werkzeug mit der gleichen Pinselbreite wie beim Landschaftsumriss und deaktivieren erneut Anti-Aliasing.


Bild 3: Level


Das Bild speichern wir wieder als PNG-Bild und nennen es "deform.png".

Zu guter Letzt erstellen wir noch einen Hintergrundhimmel. Das Bild dazu hat, wie das Level, die Maße 800x600.
Nun wählen wir als Primärfarbe ein passendes Blau und belassen die Sekundärfarbe auf Weiß. Im Effekte-Menü klicken wir dann auf "Effekte|Rendern|Wolken".


Bild 4: Himmel


Abspeichern können wir das Bild diesmal als JPG, da wir keinen Alpha-Layer benötigen.

^ Auf ans Programmieren


Nach der grafischen Vorarbeit können wir nun endlich mit dem Programmieren beginnen. Dazu fügen wir zunächst die drei Bilder, die wir erstellt haben, in den Content-Ordner unseres Projekts hinzu.
Anschließend öffnen wir die Game1.cs-Datei und fügen unter der folgenden Zeile...
SpriteBatch spriteBatch;

die Deklarationen für Himmels-, Level- und Deformations-Textur hinzu:
private Texture2D textureSky;
private Texture2D textureLevel;
private Texture2D textureDeform;

In der LoadContent-Methode ersetzen wir den // TODO-Kommentar durch die Laderoutinen für unsere Texturen:
textureSky = Content.Load<Texture2D>("sky");
textureLevel = Content.Load<Texture2D>("level");
textureDeform = Content.Load<Texture2D>("deform");

In der Draw-Methode ersetzen wir abermals den // TODO-Kommentar:
spriteBatch.Begin();

spriteBatch.Draw(textureSky, new Vector2(0, 0), Color.White);
spriteBatch.Draw(textureLevel, new Vector2(0, 0), Color.White);
spriteBatch.Draw(textureDeform, new Vector2(100, 100), Color.White);

spriteBatch.End();

Sehr schön. Wir können nun unser Spiel ausführen und unser Level, welches ungefähr so ausschauen sollte, sehen:


Bild 5: Level


Nicht schlecht für nur 11 geschriebene Zeilen Code!

^ Deformations-Sprite bewegen


Um das Deformations-Sprite zu bewegen werden wir die Maus nutzen. Wenn wir derzeit das Spiel starten, merken wir jedoch, dass der Mauszeiger über dem Spielfenster nicht sichtbar ist. Das wollen wir nun ändern.
Dazu ersetzen wir in der Initialize-Methode das // TODO-Kommentar mit:
this.IsMouseVisible = true;

Als nächstes deklarieren wir eine Vector2-Variable für die Mauskoordinaten und eine Variable für den aktuellen Mauszustand. Folgende Zeilen gehören direkt unter die Deklarationen der Texturen:
private Vector2 mousePosition;
private MouseState currentMouseState;

Jetzt müssen wir nur noch die beiden Variablen kontinuierlich aktualisieren. Um das zu erledigen erstellen wir eine neue Methode UpdateMouse:
protected void UpdateMouse()
{
  currentMouseState = Mouse.GetState();

  // Das liefert uns die Mausposition relativ zur linken oberen Ecke unseres Fensters
  mousePosition = new Vector2(currentMouseState.X, currentMouseState.Y);
}

In der Update-Methode ersetzen wir das // TODO-Kommentar durch einen Aufruf für unsere eben erstellte UpdateMouse-Methode:
UpdateMouse();

Schließlich verändern wir noch den Draw-Aufruf für unser textureDeform-Textur dahingehend, dass die Mausposition Beachtung findet:
spriteBatch.Draw(textureDeform, mousePosition, Color.White);

Führen wir unser Spiel doch mal mit F5 aus und testen, was wir soeben implementiert haben. Smile

^ Level zerstören


Um das Level zerstören zu können, benötigen wir die Textur-Daten. Diese kopieren wir dazu in einen Array, in dem dadurch die Farbwerte für jeden Pixel stecken. Diese Werte werden wir verändern, sobald der Spieler mit dem Deformationsbild das Terrain anklickt.
Für die Zerstörung setzen wir den Level-Array im betroffenden Bereich dem Deformations-Sprite-Array gleich und aktualisieren danach die Level-Textur mit den neuen Werten aus dem Array.

Als Erstes laden wir den Array für die Pixeldaten des Deformationsbildes. Dazu fügen wir unter der Deklaration der currentMouseState-Variable folgende Zeile hinzu:
private Color[] pixelDeformData;

In der LoadContent-Methode fügen wir folgendes nach dem Laden der textureDeform hinzu:
pixelDeformData = new Color[textureDeform.Width * textureDeform.Height];
// Aarry mit den Pixeldaten füllen
textureDeform.GetData<Color>(pixelDeformData, 0, textureDeform.Width * textureDeform.Height);

Da wir das Level endlos oft zerstören können und jedes mal den Pixel-Array laden müssen, packen wir das Pixel-Array-Laden für das Level in eine extra Methode, die wir immer aufrufen werden, sobald die linke Maustaste gedrückt wurde:
protected void DeformLevel()
{
  // Array für die Pixeldaten deklarieren
  Color[] pixelLevelData = new Color[textureLevel.Width * textureLevel.Height];

  // Array füllen
  textureLevel.GetData<Color>(pixelLevelData, 0, textureLevel.Width * textureLevel.Height);

  for (int x = 0; x < textureDeform.Width; x++)
  {
    for (int y = 0; y < textureDeform.Height; y++)
    {
      pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
            * textureLevel.Width] = pixelDeformData[x + y * textureDeform.Width];
    }
  }

  // Textur mit den obigen Änderungen aktualisieren
  textureLevel.SetData<Color>(pixelLevelData);
}

Bevor wir unsere Arbeit ausprobieren können, müssen wir zunächst dafür sorgen, dass die Methode beim Klicken der linken Maustaste aufgerufen wird. Dazu müssten wir eigentlich nur prüfen, ob die Bedingung currentMouseState.LeftButton == ButtonState.Pressed zutrifft. Das Problem bei der Sache ist, dass dadurch die DeformLevel-Methode in jedem Frame aufgerufen würde, in dem die Maustaste nach unten gedrückt wird. Wir wollen allerdings, dass das nur einmal pro Klick geschieht.
Um das realisieren zu können müssen wir dafür sorgen, dass sich die UpdateMouse-Methode den vorherigen Mauszustand behält. Damit können wir anschließend prüfen, ob die Maustaste zuvor bereits gedrückt wurde, oder ob dies erst im aktuellen Frame der Fall ist, was einem Klicken entsprechen würde.

So schaut die überarbeitete Methode mitsamt Aufruf der DeformLevel-Methode aus:
protected void UpdateMouse()
{
  MouseState previousMouseState = currentMouseState;

  currentMouseState = Mouse.GetState();

  // Das liefert uns die Mausposition relativ zur linken oberen Ecke unseres Fensters
  mousePosition = new Vector2(currentMouseState.X, currentMouseState.Y);

  if (previousMouseState.LeftButton == ButtonState.Pressed &&
          currentMouseState.LeftButton == ButtonState.Released)
  {
    DeformLevel();
  }
}

Und damit können wir das ganze endlich ausprobieren.

Hinweis: Wichtig ist, dass wir zunächst nur in gültige Bereiche klicken, also nicht an die Ecken des Fensters, da wir uns dort sonst außerhalb des Arrays befinden.

So sollte es ungefähr ausschauen, wenn wir auf das Terrain klicken:


Bild 6: Himmel


Alles was wir bisher getan haben ist, die Level-Textur im betroffenen Bereich mit einer Kopie der Deformations-Textur zu belegen. Im nächsten Schritt wollen wir nun auch die Pixelfarben im Deformations-Array überprüfen, um bei transparenten Pixeln das Terrain in ruhe zu lassen.
Die gleiche Abfrage können wir auch für den Level-Array durchführen. Das wird verhindern, dass das Deformations-Sprite dort gezeichnet wird, wo das Terrain bereits nicht mehr vorhanden und schon der Himmel zu sehen ist.

Um den Code für das Setzen der Pixeldaten platzieren wir aus diesem Grund eine zusätzliche If-Abfrage:
// Prüfen, ob der Pixel an der aktuellen Koordinate der Deformations- bzw. Leveltextur nicht transparent ist
if (pixelDeformData[x + y * textureDeform.Width] != Color.TransparentWhite
          && pixelLevelData[((int)mousePosition.X + x) +
          ((int)mousePosition.Y + y) * textureLevel.Width] != Color.TransparentWhite)
  {

    pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
         * textureLevel.Width] = pixelDeformData[x + y * textureDeform.Width];
  }

Wenn wir nun das Spiel starten und herumklicken sollten wir folgendes erhalten, was auch schon eher dem entspricht, was wir am Ende erreichen wollen:


Bild 7: Himmel


Im letzten Schritt ändern wir bei jedem weißen Pixel aus dem Deformations-Array den des Levels in transparent. Und um die Methode auch etwas robuster zu machen prüfen wir zusätzlich noch, ob sich die Zerstörung des Levels zu nah an den Textur-Kanten befindet.

Hier nun die endgültige Methode:
protected void DeformLevel()
{
  / Array für die Pixeldaten deklarieren
  Color[] pixelLevelData = new Color[textureLevel.Width * textureLevel.Height];

  // Array füllen
  textureLevel.GetData<Color>(pixelLevelData, 0, textureLevel.Width * textureLevel.Height);

  for (int x = 0; x < textureDeform.Width; x++)
  {
    for (int y = 0; y < textureDeform.Height; y++)
    {
      // Prüfen ob wir uns ausserhalb des Arrays befinden
      if (((mousePosition.X + x) < (textureLevel.Width)) &&
            ((mousePosition.Y + y) < (textureLevel.Height)))
      {
        if ((mousePosition.X + x) >= 0 && (mousePosition.Y + y) >= 0)
       {
          // Prüfen, ob der Pixel an der aktuellen Koordinate der Deformations- bzw. Leveltextur nicht transparent ist
          if (pixelDeformData[x + y * textureDeform.Width] != Color.TransparentWhite
                && pixelLevelData[((int)mousePosition.X + x) +
                ((int)mousePosition.Y + y) * textureLevel.Width] != Color.TransparentWhite)
         {
            // Prüfen ob Deformations-Pixel weiß ist
            if (pixelDeformData[x + y * textureDeform.Width] == Color.White)
            {
               // Wenn er weiß ist auf transparent setzen
               pixelLevelData[((int)mousePosition.X + x) + ((int)mousePosition.Y + y)
                    * textureLevel.Width] = Color.TransparentWhite;
            }
            else
            {
              // Wenn nicht, dann einfach Level-Pixel auf Deformations-Pixel setzen
             pixelLevelData[((int)mousePosition.X + x) +
                            ((int)mousePosition.Y + y) * textureLevel.Width] =
                                pixelDeformData[x + y * textureDeform.Width];
            }
          }
        }
      }
    }
  }

  // Textur mit den obigen Änderungen aktualisieren
  textureLevel.SetData<Color>(pixelLevelData);
}

Damit sollte es nun so ausschauen. Erwähnenswert ist nebenbei, das um den zerstörten Bereich immer noch ein schwarzer Rand vorhanden ist.


Bild 8: Endergebnis



^ Ende


Nach dem gleichen Prinzip können wir auch zusätzliche Elemente zum Level hinzufügen. Zum Beispiel Körper toter Spieler oder einen Grabstein an der Stelle, an der der Spieler gestorben ist.

Hier gibt es den gesamten Quellcode des Projekts zum Herunterladen.

Für pixelgenaue Kollisionsabfrage gibt es 2 Möglichkeiten. Wir könnten die Farbe jedes einzelnen Pixels der Leveltextur auf seine Transparenz hin übeprüfen, was allerdings recht langsam wäre. Oder wir erstellen zu Beginn einen extra Boolean-Kollions-Array, in dem alle transparenten Pixel durch false und alle Terrain-Pixel durch true repräsentiert werden. Diesen Array können wir in der DeformLevel-Methode an der Stelle, an der wir auch die Level-Pixel auf Alpha setzen, aktualisieren.

Infos

Name: Zerstörbares 2D-Terrain
Autor: SniperED007 (Übersetzung: SteveKr)
Kommentare: Thread