Irrlicht - from Noob to Pro: 2D-Animationen


Allgemeines Bearbeiten

Da Irrlicht kein animiertes gif-Format lesen kann, müssen wir selber tätig werden um 2D-Animationen darzustellen. Dabei machen wir uns die Idee, die dem gif-Format zu Grunde liegt zu nutze. Wir blenden einfach nacheinander verschiedene Bilder ein. Dabei vergeht zwischen den verschiedenen Bildern eine gewisse Zeitspanne, die so geschickt zu wählen ist, dass die Bilderfolge flüssig aussieht.

Das heißt aber auch, dass wir genauso viele Bilder wie Animationsschritte brauchen. Das beinhaltet viel Arbeitsaufwand und vor allem eine Menge Redundanz. Erinnern wir uns mal an das Kapitel 2D-Ausgaben mit dem Videodriver zurück. Genauer gesagt an folgenden Codeausschnitt:

//Zeichnet ein Bild mit einem Alphakanal mit Clipping und Blendingoptionen
virtual void draw2DImage (const video::ITexture *texture, const core::position2d< s32 > &destPos, const core::rect< s32 > &sourceRect, const core::rect< s32 > *clipRect=0, SColor color=SColor(255, 255, 255, 255), bool useAlphaChannelOfTexture=false) = 0;

Schauen wir uns den dritten Parameter sourceRect genauer an. Bisher wurde nicht näher auf diesen eingegangen, aber genau der wird nun wichtig. Mit sourceRect kann man einen Teil des Bildes angeben, der dargestellt werden soll. Und das gibt uns die Möglichkeit den Arbeitsaufwand und die Redundanz zu minimieren. Anstelle von vielen Bildern brauchen wir nur noch ein einziges, von dem wir gezielt uns des entsprechende Quellrechteck herauspicken und dieses zeichnen lassen. Solche Grafiken nennt man Spritesheets. Nachfolgende Grafik zeigt das Spritesheet eines Bombermans. Der rote Rahmen ist ein Quellrechteck.

 

Um zum Beispiel eine Animation abzuspielen, die zeigt, wie Bomberman auf den Spieler zuläuft, würde man die Quellrechtecke in der obersten Reihe von links nach rechts abspielen. Das wollen wir uns nachfolgend am Code anschauen.

Beispiel Bearbeiten

Dieser Codeauschschnitt zeigt lediglich wie Spriteanimationen mit Irrlicht generell funktionieren. Hier wird nicht erklärt, wie man eine gut zu nutzende Sprite- und Spriteanimationsklasse schreibt.

#include <irrlicht.h>

#pragma comment(lib, "Irrlicht.lib")

using namespace irr;

using namespace scene;
using namespace video;
using namespace gui;
using namespace core;
using namespace io;

Nun erzeugen wir uns eine Referenz auf unser Bild und einen Integer, der uns später sagen wird, in welchem Animationsschritt wir uns befinden.

//Referenz auf das Spritesheet
ITexture* sprite;
//Animationspointer
u32 currentIndex;

Nun initialisieren wir unsere Anwendung und unsere Variablen.

int main(void)
{
	IrrlichtDevice* device = createDevice(EDT_OPENGL, dimension2d<u32>(128,64));

	if(device)
	{
		IVideoDriver* driver = device->getVideoDriver();

		sprite = driver->getTexture("rpgbomberman.png");
		//Unsere Animation beginnt im 1. Frame ganz vorne
		currentIndex = 0;

Nun brauchen wir einen Timer. Irrlicht hat eine eingebaute Timer-Klasse, die man sich über den IrrlichtDevice zurückliefern lassen kann. Wir müssen aller xyz Millisekunden currentIndex um 1 erhöhen. Wir erzeugen also einen Zeitstempel und überprüfen, ob die Differenz zwischen aktueller Zeit und letztem Zeitstempel größer als sagen wir 250 Millisekunden ist.

u32 timeStamp = device->getTimer()->getTime();

		while(device->run())
		{
			driver->beginScene(true, true, SColor(255,255,255,255));
			
				if(device->getTimer()->getTime() - timeStamp > 250)
				{

Falls dem so ist, wird ein neuer Zeitstempel erzeugt und der Animationszähler um eins erhöht. Das darf nicht beliebig geschehen. Wir haben pro Reihe 4 Frames. Somit darf der Animationszähler niemals größer als 3 sein. Falls doch setzen wir ihn auf 0 zurück, woraufhin die Animation endlos läuft. Denkbar wäre auch die Animation abbrechen zulassen.

timeStamp = device->getTimer()->getTime();
					currentIndex ;

					if(currentIndex == 4)
					{
						currentIndex = 0;
					}
				}

Nun kommt das wichtigste: Das Zeichnen des Frames. Im Moment halten wir das ganze recht statisch. Wir wissen die Auflösung unseres Spritesheets, kennen die Größe eines Frames, die obendrien noch alle gleich groß sind, und wissen auch wie viele Frames in einer Reihe sind. Somit können wir schnell die Position eines Frames berechnen.

Breite/Höhe eines Frames = 32/48
Position des Frames x/0 = currentIndex*32/0
				driver->draw2DImage(sprite, position2d<int>(0,0), rect<s32>(currentIndex*32, 0, (currentIndex+1)*32, 48), 0, SColor(255,255,255,255), true);


			driver->endScene();
		}

		device->drop();
	}	

	return 0;
}

Nun wird aller 250 Millisekunden ein neuer Frame eingeblendet und unsere Animation ist perfekt, obwohl der ganze Code sehr statisch ist. Nicht alle Spritesheets werden die gleiche Größe haben oder Frames der gleichen Größe beinhalten. Später werden wir noch sehen, wie wir solche Dinge berücksichtigen können.

Kompletter Code Bearbeiten

#include <irrlicht.h>

#pragma comment(lib, "Irrlicht.lib")

using namespace irr;

using namespace scene;
using namespace video;
using namespace gui;
using namespace core;
using namespace io;

ITexture* sprite;
u32 currentIndex;

int main(void)
{
	IrrlichtDevice* device = createDevice(EDT_OPENGL, dimension2d<u32>(128,64));

	if(device)
	{
		IVideoDriver* driver = device->getVideoDriver();

		sprite = driver->getTexture("rpgbomberman.png");
		currentIndex = 3;

		u32 timeStamp = device->getTimer()->getTime();

		while(device->run())
		{
			driver->beginScene(true, true, SColor(255,255,255,255));
			
				if(device->getTimer()->getTime() - timeStamp > 250)
				{
					timeStamp = device->getTimer()->getTime();
					currentIndex++;

					if(currentIndex == 4)
					{
						currentIndex = 0;
					}
				}

				driver->draw2DImage(sprite, position2d<int>(0,0), rect<s32>(currentIndex*32, 0, (currentIndex+1)*32, 48), 0, SColor(255,255,255,255), true);

			driver->endScene();
		}

		device->drop();
	}	

	return 0;
}