Animierter Bitmap-Hintergrund für 2D Spiele in XNA

Animierter Bitmap-Hintergrund für 2D Spiele in XNA

Für ein Spiel, das den Bildschirm stellenweise „leer“ hat brauchte ich einen Hintergrund.

Um das ganze etwas netter zu gestalten habe ich mir überlegt, den Hintergrund zu animieren, und zwar so, dass es aussieht, als würden Wolken und Nebel im Hintergrund umherschweben. Ein Video sagt mehr als 1000 Worte.

Das läßt sich nun recht einfach anstellen.

Voraussetzungen: Visual Studio 2008 oder 2010, inkl. XNA Game Studio 3.1 oder 4.0. Evtl. noch ein Bildbearbeitungsprogramm, z. B. Photoshop oder Gimp. Grundlegende Kentnisse in XNA Programmierung werden ebenfalls vorausgesetzt.
Ich zeige im Artikel nur die interessanten Ausschnitte aus dem Code, die komplette, lauffähige Version findet sich am Ende des Artikels zum Runterladen.

Überblick

Es geht darum, einen Effekt zu erzeugen, so, als würde sich Nebel im Hintergrund bewegen. Das passiert durch animierte teiltransparente Bitmaps (Verschiebung, Skalierung), die übereinander gezeichnet werden, um einen räumlichen Eindruck zu erzeugen.

Schließlich werden diese in eine eigene Klasse in XNA eingefügt, die mit nur wenigen Funktionsaufrufen in jedes Programm eingebunden werden kann.

Erstellung der Bitmaps

Zunächst einmal brauchen wir passende Bilder, die Wolken und Nebel darstellen sollen. Im Wesentlichen handelt es sich hierbei um (bei mir) einfarbige Bilder, die zusätzlich noch Alpha-Informationen (also Informationen darüber, wie transparent jeder Pixel ist) enthalten.

Wenn wir von Alpha reden, müssen wir aufpassen: ein hoher Alpha Werte bedeutet eine hohe Deckung bzw. kleine Transparenz. Alpha und Transparenz sind also entgegenlaufend, während Alpha und Deckung einander entsprechend (Alpha = 1 bedeutet komplette Deckung).


Für dieses Beispiel habe ich 4 Bilder erzeugt. Ein Bild enthält keine Transparenz und wird nachher das unterste Bild darstellen, die anderen Bilder enthalten viel Transparenz und werden Wolkenschichten über den anderen Schichten darstellen, um so komplexere Bewegungen zu simulieren. Später werden wir die Texturen einfach skalieren und verschieben, wenn wir da nur mit einer Textur arbeiten wird die Szene relativ schnell langweilig. Eine gute Bildgröße ist 512×512 Pixel.

Eine kurze Anleitung, um solche Bilder mit Photoshop zu erstellen, findet sich hier (englisch), die beschriebeneVorgehensweise läßt sich aber auch mit jedem anderen umfangreicheren Grafikprogramm erzeugen (ähnliche Anleitung für Gimp). Der spannende Schritt ist hier, dass aus einem generierten Bild (Wolken) eine Transparenzmaske (Alphachannel) gemacht wird.

Bereitstellen der Bitmaps in Visual Studio

Anschließend können wir diese Bilder in einem neu erzeugten Ordner „images“ dem Projekt hinzufügen.

Da die komplette Logik später in einer separaten Datei Background.cs sein soll, müssen wir dieses nun erzeugen und dem Projekt hinzufügen.

Hernach fügen wir der Background Klasse eine LoadContent Methode hinzu:

1
2
3
4
5
6
7
public void LoadContent(ContentManager content)
{
    background[0] = content.Load(@"images/background-solid");
    background[1] = content.Load(@"images/background-3");
    background[2] = content.Load(@"images/background-2");
    background[3] = content.Load(@"images/background-1");
}

Diese Methode werden wir später aus der Game Klasse heraus aufrufen.

Zeichnen der Bitmaps

Da wir jetzt bereits alles soweit fertig haben wäre ein schöner nächster Schritt, bereits etwas auf dem Bildschirm sichtbar zu haben.
Wir verwenden folgenden Code (wobei background und currect Rectangles sein müssen, und bgcurcol eine Color ist):

1
2
3
4
5
6
7
8
9
public void Draw(SpriteBatch spriteBatch)
{
    spriteBatch.Draw(background[0], currect[0], bgcurcol);
    for (int index = 1; index < background.Length; ++index)
        spriteBatch.Draw(background[index], currect[index],
            // create white color with alpha so that foremost image is most transparent, decreasing
            // towards background (becoming more opaque)
            new Color((int)Color.White.R, (int)Color.White.G, (int)Color.White.B, (int)((background.Length - index) * (150 / background.Length) + 100)));
}

So sieht denn auch der Konstruktor aus (für XNA 4):

1
2
3
4
5
6
7
8
9
10
11
public Background()
{
    for (int index = 0; index < intervalTime.Length; ++index)
        intervalTime[index] = 0.25f; // time before first change
 
    bgcurcol = new Color();
    bgoldcol = new Color(Color.Green.R, Color.Green.G,Color.Green.B, 255);
    bgnewcol = new Color(Color.White.R, Color.White.G, Color.White.B, 255);
    bginterval = 5f;
    bgpercent = 0;
}

In XNA 3.1 sieht der Code etwas übersichtlicher aus, Color hat hier noch einen anderen Konstruktor:

1
2
bgoldcol = new Color(Color.Green, 255);
bgnewcol = new Color(Color.White, 255);

Vorbereitung der Animation

Bevor wir nun zur Bewegung der Bitmaps sowie deren Skalierung kommen, die XNA-typisch in der Update-Methode landen wird, müssen wir uns ein paar Gedanken dazu machen, wie genau wir diese Veränderungen erzeugen können.

Und wieder.. benutzen wir eine möglichst einfache Lösung. Jede Veränderung besteht aus einem Startzustand s, einem Endzustand e, sowie einer Dauer d. Damit sind alle Rahmenbedingungen beschrieben.
Für die Anzeige spannend ist nun der aktuelle Zustand. Aus der seit dem Start s‘ abgelaufenen Zeit ergibt sich dieser durch eine Interpolation von Start- und Endzustand.
Die seit dem Start s‘ vergangene Zeit relativ zur Dauer d ist also gleich der Gewichtung von Startzustand s und Endzustand e im aktuellen Zustand. Sobald der Endzustand e erreicht ist (die Dauer d wurde überschritten) wird der Endzustand der neue Startzustand, die vergangene Zeit wird auf 0 gesetzt, sowie eine neue Dauer gewählt – die Dauer für die nächste Transformation.

Grau ist alle Theorie..

Animation in der Update Methode

1
2
3
4
5
6
7
8
9
10
public void Update(GameTime gameTime)
{
    for (int i = 0; i < intervalPercent.Length; ++i)
    {
        intervalPercent[i] += (float)gameTime.ElapsedGameTime.TotalSeconds / intervalTime[i];
        if (intervalPercent[i] > 1)
            startMorph(i);
        currect[i] = Lerp(oldrect[i], newrect[i], intervalPercent[i]);
    }
}

Hier wird zunächst in Zeilen 3 – 9 über alle Objekte der aktuelle Fortschritt berechnet, und zwar als Wert zwischen 0 und 1 (Zeile 5). Sobald 1 überschritten wird, wir ein neuer Zyklus gestartet (neues Morph-Ziel gesetzt). Schließlich wird in Zeile 8 das aktuelle Rechteck (das Position sowie Größe jeder Bitmap darstellt) als Wert zwischen oldrect und newrect berechnet. Die Lerp Funktion, die hier auf Rechtecken arbeitet basiert auf der Vector2.Lerp Funktion.
Im Nachhinein sehe ich hier auch, dass sich die verschiedenen Felder gut zu einer „Morph“-Klasse hätten zusammenfügen lassen. Sollte der Code an dieser Stelle weiter wachsen wäre das eine gute Möglichkeit für ein erstes Refactoring.



Lineare Interpolation funktioniert nach dem Dreisatz. Im Falle eines Rechtecks wird nun jeder Eckpunkt einzeln zwischen alter und neuer Position linear interpoliert. Dadurch ergibt sich über das gesamte Rechteck eine lineare Interpolation.

Im letzten Codeschnipsel haben wir die Funktion startMorph benutzt und praktisch auch bereits erklärt.

1
2
3
4
5
6
7
private void startMorph(int index)
{
    intervalTime[index] = RandomInRange(4, 8);
    intervalPercent[index] = 0f;
    oldrect[index] = newrect[index];
    newrect[index] = GetNewRect(oldrect[index]);
}

Hier wird eine neue Intervall(gesamt)zeit zufällig gewählt, das aktuelle Intervall auf 0% gesetzt, das neue Ausgangsrechteck ist das alte Endrechteck, und es wird ein neues Endrechteck gewählt.
Immer wieder nett ist die Funktion, um eine Zufallszahl (float oder double) in einem Intervall zu erhalten.

1
2
3
4
private float RandomInRange(float low, float high)
{
    return low + (float)random.NextDouble() * (high - low);
}

Ein neues Endrechteck

Interessanter ist die Funktion, die ein neues Rechteck erzeugt. Hierbei wird ein Rechteck um einen beliebigen Punkt innerhalb des Rechtecks größer oder kleiner skaliert sowie verschoben. Wichtig ist hierbei die anschließende Prüfung ob das Ergebnis immer noch den aktuellen Bildschirmausschnitt überdeckt, also größer ist.

1
2
3
4
5
6
7
8
9
10
private Rectangle GetNewRect(Rectangle rectangle)
{
    Rectangle rect;
    do {
        Vector2 scaleAround = new Vector2(RandomInRange(0, 1), RandomInRange(0, 1));
        float scale = RandomInRange(0.75f, 1.25f);
        rect = Move(Scale(rectangle, scaleAround, scale), new Vector2(RandomInRange(-100, 100), RandomInRange(-100, 100)));
    } while (!CheckRectangleLarger(rect, drawingAreaRectangle));
    return rect;
}

Abschließende Worte

Damit sind wir mehr oder minder durch mit den interessanten Punkten. Selbstverständlich müssen noch die hier beschriebenen Variablen erzeugt werden, Werte müssen gesetzt werden (z. B. die Fenstergröße, um zu wissen, welchen Bereich die Bitmaps überdecken müssen), und die passenden Funktionen, insbesondere Update und Draw, müssen aus der Game Klasse heraus aufgerufen werden. Da möchte ich jedoch auf den vollständigen Source Code verweisen bzw. nur noch kurz den Code aus der Game Klasse zeigen, der die Fenstergröße übergibt (nur am Start, sollte die Größe während der Laufzeit geändert werden muß dieser Code erneut aufgerufen werden – und dann muß man aufpassen, dass die Bitmaps auf die neue Größe wachsen können).

1
2
3
4
5
6
7
8
protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);
 
    background.LoadContent(Content);
    background.SetViewSize(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
}

Im Source Code findet sich noch eine Funktion zum Farb-Morphen, ähnlich dem Größen-Morphen. Diese hat jedoch keinen Einfluß auf die Darstellung. (Während der Entwicklung hatte sie mal einen Einfluß.. der ist aber beim Rumspielen mit verschiedenen Optionen verschwunden und jetzt.. ist er weg und ich kann ihn nicht mehr finden.)
Wer sich den Code anschaut wird feststellen, dass ich zu viele jener Clean Code Bücher gelesen habe und daran arbeite, Funktionen möglichst einfach und aussagekräftig benamt zu erzeugen, so dass sich Code.. wie ein gutes Buch liest. 😉 Dabei sollten Kommentare kaum noch notwendig sein.

Es gibt zwei Versionen für XNA 3.1 sowie für XNA 4.0, die sich nur in Kleinigkeiten unterscheiden, die Syntax für den Spritebatch.Begin Befehl hat sich leicht geändert.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.