Sprites
Da bin ich wieder. Leider hat es diesmal etwas länger gedauert, ich war abgelenkt von diversen Projekten, die wohl auch in den nächsten Tagen hier gepostet werden.
Zum einen bin ich aktuell dabei für D eine pygame ähnliche Engine Struktur zu schaffen, da mir der ganze funktionale Code von SDL, statt Objekt Orientiertierung, wie ich es eben von pygame und generell von Python gewohnt bin, mächtig auf den Nerv geht.
Zum anderen haben ein paar Kollegen und ich beschlossen, ein Adventure Spiel zu entwickeln, wahrscheinlich in Python.
Aber in diesem Artikel möchte ich erklären, wie man Sprites auf die dazugehörige Karte zeichnet, so wie angekündigt.
Das mag für den einen ganz einfach klingen, ein Sprite ist ja nur eine Spielerfigur, sprich eine Grafik, also laden und an verschiedene Positionen blitten … oder doch nicht?
Weit gefehlt sag ich nur. Ein Sprite ist viel mehr als nur “eine Grafik”.
Zum einen, weil meistens in dieser “einen” Grafik gleich mehrere Ansichten der Figur stecken (links gehend, rechts gehend etc.) andererseits weil diese Grafik auch nocht präzise gehandelt werden sollte, also nicht zu schnell von einem Frame (sprich Bildausschnitt) zum nächsten, sowie kontrollierter Ablauf des Wechsels der einzelnen Frames und Go, Stop sowie Richtungs – Kontroll Funktion.
Und ein solcher Code sollte schon in seiner eigenen Klasse gekapselt werden, allein für die Wiederverwendbarkeit.
Fangen wir also mal mit den Basics an, was brauchen wir?
1. Einen Konstruktor der das Bild mit allen möglichen Ausschnitten (ich gehe im weiteren Verlauf davon aus, dass in der Datei gleich mehrere Bildausschnitte sind) entgegen nimmt. In diesem Fall 4 * 4 Frames, wie hier zusehen
.
Was noch? Einen Destruktor, der das Bild wieder freigibt, eine Möglichkeit um das Sprite zu stoppen bzw. gehen zu lassen, eine update Funktion die berechnet, welcher aktueller Ausschnitt gezeigt wird und eine Render Funktion, die das Bild auf/in das Fenster zeichnet.
Fangen wir klein an, und setzen erstmal alle nötigen Eigenschaften (Klasseninterne Variablen). Dazu erstellen wir eine neue Date und nennen sie Sprite.d und schreiben dort folgendes:
private {
import Derelict.sdl.sdl;
import Derelict.sdl.image;
import std.math;
}
class Sprite {
private {
SDL_Surface * _surface;
short _x, _y;
ubyte _current_frame, _current_dir;
ubyte _frames;
float _update;
ubyte _frame_width, _frame_height;
}
public {
float speed;
byte[2] dir;
bool move;
ubyte[3] colorkey = [255, 0, 255];
}
}
Nun zur Erklärung:
Zu aller erst einmal, importieren wir SDL sowie die SDL Extension “Image” über Derelict, danach dann die Mathematik Lib. aus der D Standard Lib.
Danach deklarieren wir als “private” das Bild (_surface), die Koordinaten (_x, _y), den derzeitigen Bildausschnitt (_current_frame), die derzeitigen “Reihe” oder “Zeile” (1 – 4) des Bildes, die angibt, in welche Richtung (Englisch “direction”) das Sprite gerade “sieht” oder “läuft” (_current_dir), die Behilfs – variable _update zur korrekten Berechnung wann der nächste Frame kommen soll, sowie die totale Anzahl alles Bildausschnitte in einer Reihe (_frames). Dazu kommt noch die Breite und Höhe des jeweiligen frames (_frame_width, _frame_height).
Als “public” deklarieren wir dagegen die Geschwindigkeit (speed), die Richtung für x und y (dir, dir[0] für die X Richtung, dir[1] für Y, wobei ein positiver Wert das Sprite nach Rechts bzw. Unten laufen und ein negativer das Sprite nach Links bzw. Hoch laufen lässt) und zu letzt, ob sich das Sprite überhaupt bewegen soll (move) und welchen Colorkey es enthält (colorkey), in diesem Beispiel wird die Farbe Rosa in RGB Form (255, 0, 255) als default vordefiniert.
Nun zu den Methoden. Da wären zunächst einmal der Konstruktor, welcher den Namen des Bildes engegen nimmt, sowie die Breite und Höhe der Bildausschnitte (meist 16 * 16 oder 32 * 32).
Außerdem berechnet er aus der Breite eines Bildausschnittes und der Breite des gesamten Bildes die Anzahl der Frames (oder Bildausschnitte) pro Zeile und legt den Colorkey für das Bild fest, mittels SDL_MapRGB und SDL_SetColorKey.
this(string filename, ubyte frame_width, ubyte frame_height) {
this._surface = IMG_Load(cast(char*) filename);
this._frames = std.math.round(this._surface.w / tile_width);
this._frame_height = frame_height;
this._frame_width = frame_width;
ubyte r, g, b;
r = this.colorkey[0];
g = this.colorkey[1];
b = this.colorkey[2];
uint color = SDL_MapRGB(this._surface.format, r, g, b);
SDL_SetColorKey(
this._surface,
SDL_SRCCOLORKEY,
color
);
}
Nun fehlt noch der Destruktor, der das Bild wieder freigibt
~this() {
SDL_FreeSurface(this._surface);
}
sowie die get und set Methode für die Position des Sprites.
void setPos(short x, short y) {
this._x = x;
this._y = y;
}
short[2] getPos() {
return [this._x, this._y];
}
Soweit ja noch recht nachvollziehbar und einfach.
Doch der Clou ist ja immer noch die korrekte Bestimmung des frames und der Zeile.
Dazu legen wir eine weitere Methode update an, welche allerdings dazu eine weitere, allerdings private, Methode benötigt: _inc_frame.
private void _inc_frame(ubyte inc) {
this._current_frame += inc;
if (this._current_frame >= this._frames) {
this._current_frame = 0;
}
}
void update() {
if (!this.move) {
if (this._current_frame > 0) {
this._current_frame = 0;
}
return;
}
this._update += this.speed;
while (this._update >= 1) {
this._inc_frame(1);
this._x += this.dir[0];
this._y += this.dir[1];
this._update -= 1;
}
if (this.dir[0] != 0) {
this._current_dir = (this.dir[0] > 0 ? RIGHT : LEFT);
} else if (this.dir[1] != 0) {
this._current_dir = (this.dir[1] > 0 ? DOWN : UP);
} else {
this._current_dir = DOWN;
}
}
Bevor ich erkläre, wozu die Methoden gut sind, ist sicher jemanden aufgefallen, das dort undefinierte Konstanten sind: UP, DOWN, LEFT, RIGHT. Woher stammen die?
Bislang existieren sie noch gar nicht, wir müssen sie uns erst anlegen, möglicherweise zwischen den als private und den als public deklarierten Klasseneigenschaften:
const ubyte UP = 2;
const ubyte DOWN = 1;
const ubyte LEFT = 4;
const ubyte RIGHT = 3;
Wozu mag sich jmd. fragen. Diese Konstanten geben an, welche Zeile des Bildes derzeit aktiv ist. Zeile 1 in diesem Bsp. heißt, das Sprite sieht/läuft runter, Zeile 2 das es Hoch sieht/läuft, Zeile 3 heißt rechts, Zeile 4 links. Zur Kontrolle einfach nochmal das Bild weiter oben angucken
.
Nun aber die Erklärung für die Methoden.
_inc_frame (von Englisch increase, erhöhen) erhöht mit jedem korrekten Durchlauf der while Schelife der update Funktion den Frame um 1, wir springen also zum nächsten Bildausschnitt.
Sofern sich das Bild nicht bewegt, wird die update Methode gar nicht weiter beachtet, sollte der derzeitige frame größer als 0 sein, wird er auf 0 zurck gesetzt, um den Zustand des Stillstandes auch Auszudrücken (wem es noch nicht aufgefallen ist: jeder erster frame pro Reihe repräsentiert das Sprite stillstehend). Wenn nicht, wird die private Variable _update um die derzeitige Geschwindkeit erhöht. Wenn dieser Wert 1 ist oder überschreitet, wird die while Schleife durchlaufen, der Frame erhöhrt, die x und y variablen angepasst (je nach Wert des öffentlichen dir arrays) und die Eigenschaft _update um den Faktor 1 “verkleinert”.
Am Ende wird, je nachdem ob der Spieler in X – oder Y – Richtung geht, die derzeitige Reihe (this._current_dir) der Bildausschnitte “berechnet” (dazu sind die erwähnten Konstanten am Anfang gut) indem geprüft wird, ob das Sprite in X oder Y Richtung geht, je nachdem wird die aktuelle Reihe ausgewählt, Sollte das Sprite noch nicht gesteuert worden sein (zu Anfang des Programms) dann schaut/läuft das Sprite nach unten (der else Teil. mit this._current_dir = DOWN;).
Nun fehlt noch eine Render Methode, um das Sprite auf den Bildschirm zu bekommen, da kommen wir, wie am Anfang schon erwähnt, zur SDL_BlitSurface Funktion.
void render() {
SDL_Rect dst_rect = {
cast(ushort) (this._current_frame * this._frame_width),
cast(ushort) ((this._current_dir - 1) * this._frame_height),
this._frame_width, this._frame_height
};
SDL_Rect pos_rect = {
this._x, this._y,
this._frame_width,
this._frame_height
};
SDL_Surface * display = SDL_GetVideoSurface();
SDL_BlitSurface(this._surface, &dst_rect, display, &pos_rect);
}
Zu Anfang werden zwei SDL Rechteckte erstellt, eines, welches die aktuelle Position des Sprites im Fenster enthält, das andere, welches den aktuellen Ausschnitt/Frame enthält. Dabei wird allerdings noch (dem genauen Beobachter wird es aufgefallen sein) von this._current_dir 1 abgezogen, da this._current_dir von 1 bis 4 geht aber der Computer, wie wir alle wissen, von 0 anfängt zu zählen. Danach holen wir uns per SDL_GetVideoSurface() die Oberfläche des Fensters und “blitten” den fertigen Frame in das Fenster. Fertig.
Zur Kontrolle, der gesamte Code sieht nun so aus:
private {
import Derelict.sdl.sdl;
import Derelict.sdl.image;
import std.math;
}
class Sprite {
private {
SDL_Surface * _surface;
short _x, _y;
ubyte _current_frame, _current_dir;
ubyte _frames;
float _update;
ubyte _frame_width, _frame_height;
}
const ubyte UP = 2;
const ubyte DOWN = 1;
const ubyte LEFT = 4;
const ubyte RIGHT = 3;
public {
float speed;
byte[2] dir;
bool move;
ubyte[3] colorkey = [255, 0, 255];
}
this(string filename, ubyte frame_width, ubyte frame_height) {
this._surface = IMG_Load(cast(char*) filename);
this._frames = cast(ubyte) std.math.round(this._surface.w / frame_width);
this._frame_height = frame_height;
this._frame_width = frame_width;
ubyte r, g, b;
r = this.colorkey[0];
g = this.colorkey[1];
b = this.colorkey[2];
uint color = SDL_MapRGB(this._surface.format, r, g, b);
SDL_SetColorKey(
this._surface,
SDL_SRCCOLORKEY,
color
);
this._update = this.speed = 0.0;
this.move = true;
}
~this() {
SDL_FreeSurface(this._surface);
}
void setPos(short x, short y) {
this._x = x;
this._y = y;
}
short[2] getPos() {
return [this._x, this._y];
}
private void _inc_frame(ubyte inc) {
this._current_frame += inc;
if (this._current_frame >= this._frames) {
this._current_frame = 0;
}
}
void update() {
if (!this.move) {
if (this._current_frame > 0) {
this._current_frame = 0;
}
return;
}
this._update += this.speed;
while (this._update >= 1) {
this._inc_frame(1);
this._x += this.dir[0];
this._y += this.dir[1];
this._update -= 1;
}
if (this.dir[0] != 0) {
this._current_dir = (this.dir[0] > 0 ? RIGHT : LEFT);
} else if (this.dir[1] != 0) {
this._current_dir = (this.dir[1] > 0 ? DOWN : UP);
} else {
this._current_dir = DOWN;
}
}
void render() {
SDL_Rect dst_rect = {
cast(ushort) (this._current_frame * this._frame_width),
cast(ushort) ((this._current_dir - 1) * this._frame_height),
this._frame_width, this._frame_height
};
SDL_Rect pos_rect = {
this._x, this._y,
this._frame_width,
this._frame_height
};
SDL_Surface * display = SDL_GetVideoSurface();
SDL_BlitSurface(this._surface, &dst_rect, display, &pos_rect);
}
}
Knappe 120 Zeilen Code und schon bewegt sich so ein Sprite 1A.
Und um es später benutzen zu können; so einfach wird’s eingebunden:
import Sprite;
void main() {
scope Sprite player = new Sprite("image/player.tga", 16, 16);
player.move = true;
player.setPos(250, 200);
player.speed = 0.1;
SDL_Event event;
bool game_loop = true;
while (game_loop) {
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
game_loop = false;
break;
case SDL_KEYDOWN:
switch (event.key) {
case SDLK_UP:
player.dir = [0, -1];
break;
case SDLK_DOWN:
player.dir = [0, 1];
break;
case SDLK_LEFT:
player.dir = [-1, 0];
break;
case SDLK_RIGHT:
player.dir = [1, 0];
break;
default:
player.dir = [0, 0];
}
break;
}
}
player.update();
player.render();
}
}
Add a comment »2 comments to this article
- Code Arts » Sprites, Nachtrag
- Sprites in Aktion: Ziele anvisieren & Winkelberechnung | Code Arts