Dieser Thread bezieht sich auf das Programm "Tutorial Game", dass du im Showroom
herunterladen kannst, und als Ergänzung und Weiterführung zu den Haupttutorials
gedacht ist, die man in der Entwicklungsumgebung über [F1] aufrufen kann.
Zunächst folgen ein paar Tipps, danach der erste Teil des Tutorials.
Hinweis :
Das Programm enthält kaum spielerische Elemente. Ausser dem kleinen Dorf (drei
Bildschirme gross) gibt es nichts weiter zu entdecken (die anderen Gebiete werden
generisch erzeugt). Die Beispielaufgabe liegt darin, die "Zauberkugel" in deinen
Besitz zu bringen. Die Steuerung ist der Datei "LiesMich.txt" zu entnehmen.
Projektstruktur/Programmierstil ________________________________________________________
Gewöhne dir gleich zu Beginn eine logische und übersichtliche Programmstruktur an.
Sobald deine Projekte komplexer werden, ist es wichtig, den Überblick zu behalten.
- Einrücken:
Rücke den Inhalt von Schleifen immer deutlich sichtbar ein und benutze Leerzeichen
(z.B. nach Kommata):
Falsch:
WHILE TRUE
FOR x=0 TO 100
PRINT x,0,0
SHOWSCREEN
NEXT
WEND
Richtig:
WHILE TRUE
FOR x = 0 TO 100
PRINT x, 0, 0
SHOWSCREEN
NEXT
WEND
- Kommentieren:
Kommentiere Befehlsblöcke ausreichend in allgemeinverständlicher Sprache.
Das erleichtert deutlich die Analyse deines Quellcodes.
Beispiel:
// write mouse state into variables
MOUSESTATE mx, my, mbl, mbr // x coord, y coord, left button, right button
mxspeed = MOUSEAXIS(0) // x axis (x speed)
myspeed = MOUSEAXIS(1) // y axis (y speed)
mwheel = MOUSEAXIS(2) // wheel (1 = up / -1 = down)
mbm = MOUSEAXIS(5) // middle button
- Mehrere Quelldateien:
Ordne Funktionsgruppen in unterschiedlichen Quelldateien an, wenn die Sprungliste
zu lang wird. Siehe dazu:
Help:glbasic -> GLBasic intern -> Mehrere Quelldateien
- Sprungmarken:
Die Verwendung von Sprungmarken und Lesezeichen erleichtern das Navigieren
im Quellcode (Beispiel: siehe Code unten).
- Templates:
Benutze ein vorgefertigtes Programmgerüst, damit du dich schneller auf das
Wesentliche konzentrieren kannst.
Beispiel:
// --------------------------------- //
// Project: ExampleTemplate
// --------------------------------- //
//_____________________________________________________________________________ DECLARATIONS
//............................................................................... CONs
Constants:
//............................................................................... VARs
Variables:
// mouse state variables
GLOBAL mx, my // mouse coordinates
GLOBAL mbl, mbm, mbr // mouse button left, middle, right
GLOBAL mxspeed, myspeed, mwheel // mouse x speed, y speed, mouse wheel
// main directory
GLOBAL MainDir$
//............................................................................... DIMs
DIMs:
//.............................................................................. TYPEs
TYPes:
//______________________________________________________________________________________ INI
// main initialization
GOSUB subINI
//________________________________________________________________________________ MAIN LOOP
MainLoop:
WHILE TRUE
// read mouse state
GOSUB subReadMouse
//...................................................................... MAIN CODE >>>
//...................................................................... <<< MAIN CODE
// draw mouse pointer
DRAWSPRITE 1, mx-15, my-15
// draw buffer on screen
SHOWSCREEN
WEND
//_________________________________________________________________________ SUBs / FUNCTIONs
// ------------------------------------------------------------- //
// -=# SUBINI #=-
// .............................................................
// main initialization
// ------------------------------------------------------------- //
SUB subINI:
//.................................................. LOAD GFX
GFX:
// get main/set GFX directory
MainDir$ = GETCURRENTDIR$()
SETCURRENTDIR(MainDir$ + "Media/GFX")
// load standard font-gfx : index 0
LOADFONT "smalfont.png", 0 // standard font
// set font index
SETFONT 0 // standard font
// mouse pointer
LOADSPRITE "MPointer.png", 1
//.................................................. LOAD SFX
SFX:
SETCURRENTDIR(MainDir$ + "Media/SFX")
//................................................ LOAD MUSIC
Music:
SETCURRENTDIR(MainDir$ + "Media/Music")
// reset to main directory
SETCURRENTDIR(MainDir$)
ENDSUB // SUBINI
// ------------------------------------------------------------- //
// -=# SUBREADMOUSE #=-
// .............................................................
// reads various mouse states
// ------------------------------------------------------------- //
SUB subReadMouse:
// write mouse state into variables
MOUSESTATE mx, my, mbl, mbr // x coord, y coord, left button, right button
mxspeed = MOUSEAXIS(0) // x axis (x speed)
myspeed = MOUSEAXIS(1) // y axis (y speed)
mwheel = MOUSEAXIS(2) // wheel (1 = up / -1 = down)
mbm = MOUSEAXIS(5) // middle button
ENDSUB // SUBREADMOUSE
- Konventionen:
Markiere unterschiedliche Datentypen um Verwechslungen zu vermeiden.
Ich benutze z.B. folgende Konvention:
Lokale Variabeln : lName
DIMs : dName[]
TYPEn : tName
Konstanten : cNAME
Nützliche Funktionen ___________________________________________________________________
Eine eigene Funktionssammlung wird wichtig, wenn man komplexere Projekte in
Angriff nimmt. Es wird empfohlen, zuerst mit simpleren Funktionen zu beginnen,
um damit die komplizierteren zu realisieren.
Interface:
Eine Sammlung an Interfacefunktionen ist im Showroom ("GUIde") verfügbar.
Trigonometrie:
Trigonometrische Funktionen werden in Spielen häufig verwendet, um z.B.
realistische Bewegungen darzustellen.
Wenn jemand bessere Funktionen, als die unten aufgeführten hat - bitte posten.
- fnMoveX / fnMoveY:
Hiermit kann ein Objekt auf der x/y Achse bewegt werden.
// ------------------------------------------------------------- //
// -=# FNMOVEX #=-
// ------------------------------------------------------------- //
FUNCTION fnMoveX: lAngle, lx, lSpeed
DEC lAngle, 180
lx = lx + COS(lAngle) * lSpeed
RETURN lx
ENDFUNCTION // FNMOVEX
// ------------------------------------------------------------- //
// -=# FNMOVEY #=-
// ------------------------------------------------------------- //
FUNCTION fnMoveY: lAngle, ly, lSpeed
DEC lAngle, 180
ly = ly + SIN(lAngle) * lSpeed
RETURN ly
ENDFUNCTION // FNMOVEY
Nutzung:
Object_x = fnMoveX(Object_Angle, Object_x, Object_Speed)
Object_y = fnMoveY(Object_Angle, Object_y, Object_Speed)
- fnGetAngle
Ermittelt den Winkel zwischen zwei Punkten (Objekten).
// ------------------------------------------------------------- //
// -=# FNGETANGLE #=-
// ................................................................
// computes angle between two points
// ------------------------------------------------------------- //
FUNCTION fnGetAngle: lx1, ly1, lx2, ly2
LOCAL lAngle
// ??? angle-computing-code
lAngle = MOD((ATAN(ly1-ly2,lx1-lx2)+360),360)
RETURN lAngle
ENDFUNCTION // FNGETANGLE
Nutzung:
Winkel = fnGetAngle(Object_1_x, Object_1_y, Object_2_x, Object_2_y)
- fnGetDistance:
Ermittelt die Entfernung zwischen zwei Punkten.
// ------------------------------------------------------------- //
// -=# FNGETDISTANCE #=-
// ------------------------------------------------------------- //
FUNCTION fnGetDistance: lx1, ly1, lx2, ly2
LOCAL lx, ly, lReturn
lx = lx1 - lx2
ly = ly1 - ly2
// ??? distance computing code
lReturn = SQR( ABS(lx * lx) + ABS(ly * ly) )
RETURN lReturn
ENDFUNCTION // FNGETDISTANCE
Nutzung:
Entfernung = fnGetDistance(Object_1_x, Object_1_y, Object_2_x, Object_2_y)
Tools __________________________________________________________________________________
Die richtige Kombination von Tools (also Werkzeugen, Hilfsprogrammen) ist auschlaggebend
für die Qualität deines Projekts. Nimm dir also Zeit und teste verschiedene Tools
ausreichend.
Hier ist eine Liste mit diversen nützlichen Programmen:
http://www.glbasic.com/forum/index.php?topic=7959.0
Tutorialspiel __________________________________________________________________________
Beginnen wir nun mit dem eigentlichen Tutorial:
Wenn dir das befolgen jedes Arbeitsschrittes zu mühsam ist, kannst du dich darauf
beschränken, nur die Erklärungen durchzulesen. Allerdings vergrössert sich der
Lerneffekt durch Praxis.
Lade dir "Tutorial Game" aus dem Showroom herunter (um die Ressourcen zu kopieren).
Starte ein neues Projekt und nenne es "Tutorial Game".
Kopiere das "Example Template" weiter oben (Sektion: Projektstruktur/Programmierstil)
und füge es zusammen mit allen trigonometrischen Funktionen der Sektion "Nützliche
Funktionen" in das Projekt ein.
Erstelle im "Media" Ordner (befindet sich im "app" Ordner des Projekts) einen
Neuen mit dem Namen "GFX". Dort werden sämtliche Grafikdateien abgelegt.
Kopiere folgende Grafiken und füge sie in diesen Ordner ein:
"Character.png", "smalfont.png", "MPointer.png"
Für dieses Beispiel nutzen wir eine Auflösung von 640 mal 480 Pixeln (Bildschirmpunkten)
und eine FPS von 30. FPS = "Frames Per Second" (Bilder pro Sekunde).
Eine FPS von unter 30 ist nicht zu empfehlen, da das Programm dann evtl. nicht
mehr flüssig läuft.
Springe zu "subINI:" und gebe darunter folgenden Code ein:
// set resolution / set to full screen
SETSCREEN 640, 480, 1
// set frames per second to 30
LIMITFPS 30
Laden wir nun die Charaktergrafik. Tippe folgenden Code am besten unter dem, der den
Mauszeiger lädt (damit die Chronologie der Indizes gewahrt bleibt):
// load player character animation
LOADANIM "Character.png", 2, 26, 16
1. Den Charakter steuern
Wir wollen zunächst den Spieler-Charakter über die WASD-Tasten steuern. Und zwar nicht
nur nach Links/Rechts/Oben/Unten, sondern auch diagonal, wenn zwei Tasten gleichzeitig
gedrückt werden. Dazu brauchen wir Variabeln, die die Koordinaten speichern. Weitere
brauchen wir um den Index der aktuellen Animationsgrafik zu speichern sowie die
aktuelle Geschwindigkeit.
Fassen wir also alle charakterbezogenen Daten in einer TYPE-Struktur zusammen, die
wir später erweitern werden:
Springe zu "TYPes:" und gebe folgenden Code ein:
// main character data
TYPE TTchar
x // x coordinate
y // y coordinate
ani // animation index
speed // movement speed in pixel per frame
AniChron[8] // for animation chronology
ENDTYPE
GLOBAL tChar AS TTchar // create instance
DIMDATA tChar.AniChron[], 1, 2, 1, 0, 3, 4, 3, 0 // set animation chronology
In tChar.AniChron[] ist der Bewegungsablauf der Animationsgrafik gespeichert, da ein
Zyklus mehrmals auf dieselben Bilder zugreift. Wenn du dir "Character.png" anschaust,
siehst du dass es jeweils 5 Bilder für einen Durchlauf gibt, der 8 Schritte umfasst
(die Sterbesequenz ist eine Ausnahme). Wenn der Spieler also nach Links läuft, werden
die Werte in AniChron[] als Index für DRAWANIM verwendet, wenn er nach Rechts
läuft wird alles +5 nach Oben +10 usw. genommen.
Nun zum Code, der die Tasten abliest und die Koordinaten verändert.
Wir könnten diesen zwischen
//...................................................................... MAIN CODE >>>
//...................................................................... <<< MAIN CODE
in der Hauptschleife (Sprungmarke: "MainLoop:") platzieren, aber gerade dort ist die
Übersichtlichkeit sehr wichtig. Also wird der Code in eine Funktion (SUB geht auch)
ausgelagert.
Erstelle eine neue Funktion und nenne sie "fnComputeChar", lösche das Textfeld unter
"Parameter". Ich markiere Funktionen mit "fn", damit es zu keinen Verwechslungen
mit Variabeln kommt.
Füge folgenden Code ein:
LOCAL lMoveDir // stores movement-"bits"
LOCAL lAngle // movement angle
STATIC lStoreDir = 15 // saves animation if character stands
STATIC lMoveCnt // current index of animation cycle
// check arrow-keys / set movement-"bits"
IF KEY(30) // [A] Left/ bit: 0001
INC lMoveDir, 1
ELSE
IF KEY(32) // [D] Right / bit: 0010
INC lMoveDir, 10
ENDIF
ENDIF
IF KEY(17) // [W] Up / bit: 0100
INC lMoveDir, 100
ELSE
IF KEY(31) // [S] Down / bit: 1000
INC lMoveDir, 1000
ENDIF
ENDIF
lMoveDir speichert die aktuelle Bewegungsrichtung anhand der Tasten die gedrückt werden.
Jede der WASD Tasten ist mit einem Bitmuster belegt.
[A] (Links) = 0001 / [D] (Rechts) = 0010 / [W] (Oben) = 0100 / (Unten) = 1000
Wenn z.B. [W] und [D] gleichzeitig gedrückt werden, lautet das Bitmuster 0110
(0010 + 0100), dies entspricht also einer Bewegungsrichtung nach Rechts-Oben.
Ergänze die Funktion um den folgenden Code, der dies auswertet:
// translate movement-"bits" into movement direction
// set true 45 degree movement-angle
// set animation frame
SELECT lMoveDir
CASE 1 // west
lMoveDir = 0 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 360 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 10 // east
lMoveDir = 5 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 180 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 100 // north
lMoveDir = 10 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 90 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 1000 // south
lMoveDir = 15 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 270 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 101 // north-west
lMoveDir = 0 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 45 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 110 // north-east
lMoveDir = 5 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 135 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 1010 // south-east
lMoveDir = 5 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 225 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 1001 // south-west
lMoveDir = 0 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 315 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
DEFAULT
lMoveCnt = 0 // reset counter
lAngle = -1 // sets movement angle
// set animation frame
tChar.ani = lStoreDir
ENDSELECT
// move character
IF lAngle <> -1
tChar.x = fnMoveX(lAngle, tChar.x, tChar.speed)
tChar.y = fnMoveY(lAngle, tChar.y, tChar.speed)
ENDIF
SELECT prüft das Bitmuster und speichert in lMoveDir den jeweils ersten Animationsindex
einer Bewegungsrichtung.
lStoreDir speichert den Animationsindex ohne Inkrementor damit der Charakter nicht
mitten in einem Animationszyklus stehen bleibt wenn die Tasten losgelassen werden,
sondern das jeweils erste Bild eines Durchlaufs verwendet.
Dann wird der Bewegungswinkel des Sprites festgelegt.
Ich habe Spiele gesehen, die diagonale Bewegungen ohne trigonometrische Funktionen
darstellen. Wenn die Geschwindigkeit eines Sprites also 3 beträgt und eine Bewegung
nach unten eine Koordinatenverschiebung um +3 (Pixel) auf der Y-Achse und eine
Bewegung nach Rechts +3 auf der X-Achse bedeutet, ist eine Bewegung nach Rechts Unten
mit einer Verschiebung von X + 3 und Y + 3 falsch. Das Sprite bewegt sich so diagonal
schneller als nach Links/Rechts/Oben/Unten.
Also werden die Funktionen fnMoveX()/fnMoveY() verwendet, um die richtigen Koordinaten
beim aktuellen Bewegungswinkel zu berechnen.
In tChar.ani wird der Index des darzustellenden Bildes der Animation gespeichert.
Dabei wird zum ersten Bild eines Animationszyklus (gespeichert in lMoveDir) der
Inkrementor (lMoveCnt - dazu gleich mehr) addiert, was den Index der aktuellen
Animation errechnet.
Nach dem SELECT-Befehl werden dann die x/y Koordinaten des Spielers verändert,
sofern er sich bewegt (Stillstand wird mit lAngle = -1 markiert).
lMoveCnt ist ein Inkrementor, mit dem der aktuelle Index der Animation ermittelt wird.
Füge folgenden Code in die Funktion ein:
// increment/reset animation cycle
INC lMoveCnt, 0.4
IF lMoveCnt > 7; lMoveCnt = 0; ENDIF // reset animation
Dieser Inkrementor wird, wie oben erklärt, mit lMoveDir addiert.
Mit INC wird die Geschwindigkeit der Animationsabläufe festgelegt.
Initialisieren wir nun die Charaktervariabeln. Springe dazu nach "subINI:":
// initialize character values
tChar.x = 307; tChar.y = 232
tChar.speed = 3
Jetzt muss nur noch folgender Code in die Hauptschleife eingefügt werden:
fnComputeChar()
DRAWANIM 2, tChar.ani, tChar.x, tChar.y
Probiere das Programm nun aus. Falls es nicht funktionieren sollte, hier nochmal
der gesamte Quellcode (exklusive aller Funktionen die noch nicht benötigt werden):
// --------------------------------- //
// Project: Tutorial Game
// --------------------------------- //
//_____________________________________________________________________________ DECLARATIONS
//............................................................................... CONs
Constants:
//............................................................................... VARs
Variables:
// mouse state variables
GLOBAL mx, my // mouse coordinates
GLOBAL mbl, mbm, mbr // mouse button left, middle, right
GLOBAL mxspeed, myspeed, mwheel // mouse x speed, y speed, mouse wheel
// main directory
GLOBAL MainDir$
//............................................................................... DIMs
DIMs:
//.............................................................................. TYPEs
TYPes:
// main character data
TYPE TTchar
x // x coordinate
y // y coordinate
ani // animation index
speed // movement speed in pixel per frame
AniChron[8] // for animation chronology
ENDTYPE
GLOBAL tChar AS TTchar // create instance
DIMDATA tChar.AniChron[], 1, 2, 1, 0, 3, 4, 3, 0 // set animation chronology
//______________________________________________________________________________________ INI
// main initialization
GOSUB subINI
//________________________________________________________________________________ MAIN LOOP
MainLoop:
WHILE TRUE
// read mouse state
GOSUB subReadMouse
//...................................................................... MAIN CODE >>>
fnComputeChar()
DRAWANIM 2, tChar.ani, tChar.x, tChar.y
//...................................................................... <<< MAIN CODE
// draw mouse pointer
DRAWSPRITE 1, mx-15, my-15
// draw buffer on screen
SHOWSCREEN
WEND
//_________________________________________________________________________ SUBs / FUNCTIONs
// ------------------------------------------------------------- //
// -=# SUBINI #=-
// .............................................................
// main initialization
// ------------------------------------------------------------- //
SUB subINI:
// set resolution / set to full screen
SETSCREEN 640, 480, 1
// set frames per second to 30
LIMITFPS 30
// initialize character values
tChar.x = 307; tChar.y = 232
tChar.speed = 3
//.................................................. LOAD GFX
GFX:
// get main/set GFX directory
MainDir$ = GETCURRENTDIR$()
SETCURRENTDIR(MainDir$ + "Media/GFX")
// load standard font-gfx : index 0
LOADFONT "smalfont.png", 0 // standard font
// set font index
SETFONT 0 // standard font
// mouse pointer
LOADSPRITE "MPointer.png", 1
// load player character animation
LOADANIM "Character.png", 2, 26, 16
//.................................................. LOAD SFX
SFX:
SETCURRENTDIR(MainDir$ + "Media/SFX")
//................................................ LOAD MUSIC
Music:
SETCURRENTDIR(MainDir$ + "Media/Music")
// reset to main directory
SETCURRENTDIR(MainDir$)
ENDSUB // SUBINI
// ------------------------------------------------------------- //
// -=# SUBREADMOUSE #=-
// .............................................................
// reads various mouse states
// ------------------------------------------------------------- //
SUB subReadMouse:
// write mouse state into variables
MOUSESTATE mx, my, mbl, mbr // x coord, y coord, left button, right button
mxspeed = MOUSEAXIS(0) // x axis (x speed)
myspeed = MOUSEAXIS(1) // y axis (y speed)
mwheel = MOUSEAXIS(2) // wheel (1 = up / -1 = down)
mbm = MOUSEAXIS(5) // middle button
ENDSUB // SUBREADMOUSE
// ------------------------------------------------------------- //
// --- FNCOMPUTECHAR ---
// ------------------------------------------------------------- //
FUNCTION fnComputeChar:
LOCAL lMoveDir // stores movement-"bits"
LOCAL lAngle // movement angle
STATIC lStoreDir = 15 // saves animation if character stands
STATIC lMoveCnt // current index of animation cycle
// check arrow-keys / set movement-"bits"
IF KEY(30) // [A] Left/ bit: 0001
INC lMoveDir, 1
ELSE
IF KEY(32) // [D] Right / bit: 0010
INC lMoveDir, 10
ENDIF
ENDIF
IF KEY(17) // [W] Up / bit: 0100
INC lMoveDir, 100
ELSE
IF KEY(31) // [S] Down / bit: 1000
INC lMoveDir, 1000
ENDIF
ENDIF
// translate movement-"bits" into movement direction
// set true 45 degree movement-angle
// set animation frame
SELECT lMoveDir
CASE 1 // west
lMoveDir = 0 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 360 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 10 // east
lMoveDir = 5 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 180 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 100 // north
lMoveDir = 10 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 90 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 1000 // south
lMoveDir = 15 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 270 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 101 // north-west
lMoveDir = 0 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 45 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 110 // north-east
lMoveDir = 5 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 135 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 1010 // south-east
lMoveDir = 5 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 225 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
CASE 1001 // south-west
lMoveDir = 0 // set animation frame
lStoreDir = lMoveDir // saves animation frame
lAngle = 315 // sets movement angle
// set animation frame with incrementor
tChar.ani = lMoveDir + tChar.AniChron[lMoveCnt]
DEFAULT
lMoveCnt = 0 // reset counter
lAngle = -1 // sets movement angle
// set animation frame
tChar.ani = lStoreDir
ENDSELECT
// move character
IF lAngle <> -1
tChar.x = fnMoveX(lAngle, tChar.x, tChar.speed)
tChar.y = fnMoveY(lAngle, tChar.y, tChar.speed)
ENDIF
// increment/reset animation cycle
INC lMoveCnt, 0.4
IF lMoveCnt > 7; lMoveCnt = 0; ENDIF // reset animation
ENDFUNCTION // FNCOMPUTECHAR
// ------------------------------------------------------------- //
// -=# FNMOVEX #=-
// ------------------------------------------------------------- //
FUNCTION fnMoveX: lAngle, lx, lSpeed
DEC lAngle, 180
lx = lx + COS(lAngle) * lSpeed
RETURN lx
ENDFUNCTION // FNMOVEX
// ------------------------------------------------------------- //
// -=# FNMOVEY #=-
// ------------------------------------------------------------- //
FUNCTION fnMoveY: lAngle, ly, lSpeed
DEC lAngle, 180
ly = ly + SIN(lAngle) * lSpeed
RETURN ly
ENDFUNCTION // FNMOVEY
2. Eine tilebasierte Spielwelt erstellen
Es gibt viele Möglichkeiten eine Spielwelt darzustellen. Für dieses Tutorial benutzen wir
eine Engine, die Tiles (Quadratische Bilder) von 16 * 16 Pixeln Grösse von Links nach
Rechts und von Oben nach Unten anordnet (also jeweils 40 in einer Reihe und 30 in einer
Spalte). Dabei werden zuerst die Bodentiles gezeichnet, danach die Hindernisse und dann
das, was sich über dem Charakter befindet. Aus Gründen der Einfachheit verzichten wir auf
Scrolling und laden den nächsten Level wenn der Spieler den Bildschirmrand erreicht.
Kopiere folgende Grafiken in den "GFX" Ordner:
"Tiles.png", "Selector.png"
Springe nach "GFX:" und lade die Bilder:
// load tiles
LOADANIM "Tiles.png", 3, 16, 16
// load selector
LOADSPRITE "Selector.png", 4
Erstelle eine Type-Struktur, die sämtliche Leveldaten enthält:
// tile type for map
TYPE TTtile
ground[2]
obstacle
overlay
ENDTYPE
GLOBAL tMap[] AS TTtile
REDIM tMap[40][30]
"TTtile" entspricht einem einzigen Tile in "tMap[40][30]".
Die Variabeln in dieser Struktur enthalten die Indizes der Bilder von "Tiles.png".
"ground[2]" (Boden) wird zuerst gezeichnet. Die zwei Felder sollen vehindern, dass
der Level zu gleichförmig aussieht. So kann man z.B. zuerst Grass, Sand, Fliesen etc.
platzieren und danach dekorative Elemente darüber legen.
"obstacle" (Hindernis) speichert den Index der Grafik, die der Spieler nicht betreten
kann. Wenn obstacle = 0 ist, ist kein Hindernis vorhanden.
Die Tiles in "overlay" werden über die Anderen gezeichnet.
Programmieren wir nun eine Engine, die die Werte aus "tMap[][]" ausliest und
auf den Bildschirm zeichnet. Aus Gründen der Übersichtlichkeit lagern wir diese in
eine separate Quelldatei aus. Siehe dazu:
Help:glbasic -> GLBasic intern -> Mehrere Quelldateien
Erstelle eine neue Quelldatei mit dem Namen "TileEngine".
Trage folgende Funktion dort ein:
// ------------------------------------------------------------- //
// --- FNDRAWTILES ---
// ------------------------------------------------------------- //
FUNCTION fnDrawTiles:
LOCAL lx, ly
// display tiles
FOR ly = 0 TO 29
FOR lx = 0 TO 39
// draw ground tiles
DRAWANIM 3, tMap[lx][ly].ground[0], lx * 16, ly * 16
DRAWANIM 3, tMap[lx][ly].ground[1], lx * 16, ly * 16
// draw obstacle tiles
DRAWANIM 3, tMap[lx][ly].obstacle, lx * 16, ly * 16
NEXT
NEXT
ENDFUNCTION // FNDRAWTILES
"fnDrawTiles" zeichnet erstmal nur die "ground" und "obstacle" Tiles. Zu den anderen
Funktionen kommen wir später.
"lx" steht für die aktuelle x-Koordinate, "ly" für die aktuelle y-Koordinate der zu
zeichnenden Grafik. Dabei werden diese mit 16 multipliziert, da die Tiles jeweils
16 Pixel hoch/breit sind.
Trage die Funktion in die Hauptschleife ein (über "fnComputeChar()").
Da die "tMap[][]" ausschliesslich mit dem Wert 0 initialisiert wurde, wird lediglich
das erste Tile von "Tiles.png" dargestellt welches komplett transparent ist.
Wir könnten die Werte für die Karte jetzt über die IDE eintragen aber das würde
zu viel Arbeit machen. Wir schreiben stattdessen einen Editor, mit dem der Level
sehr viel komfortabler gestaltet werden kann. Für diesen deklarieren wir folgende
Datentypen:
CONSTANT cNUM_TILES = 76 // number of tiles
und
// choosen tile (editor)
GLOBAL eTile
// keypress-delay in frames
GLOBAL KeyDelay
"cNUM_TILES" enthält die Anzahl an Tiles die in "Tiles.png" genutzt werden. Wenn du
das Programm um Weitere ergänzen willst, muss diese Zahl geändert werden.
In "eTile" wird der Index des Tiles gespeichert das platziert werden soll.
"KeyDelay" ist ein Zähler, der das Registrieren bestimmter Key-Codes verhindert,
solange er nicht den Wert 0 hat. Somit soll z.B. vermieden werden dass verschiedene
Modi zu schnell umgeschaltet werden.
Trage folgenden Code in die Hauptschleife ein:
// decrease key delay
DEC KeyDelay, 1
IF KeyDelay < 0; KeyDelay = 0; ENDIF
Da der Editor viel Code enthalten wird, lagern wir ihn aus:
Erstelle eine neue Quelldatei und nenne sie "Editor".
Erstelle eine neue Funktion und nenne sie "fnEditor".
Und so soll er funktionieren:
Mit [TAB] wird der Editor aufgerufen/verlassen.
Linksklick platziert das ausgewählte Tile, Rechtklick löscht eines von der Karte.
Mit dem mittleren Mausbutton kann man ein Tile von der Karte kopieren.
Solange die [SPACE] Taste gedrückt wird, kann man per Linksklick eines aus der Liste
wählen und mit Rechtsklick die gesamte Karte damit füllen.
Ein Druck auf [ZB ENTER] (ZB = Ziffernblock) bewirkt dass Grass- und Pflasterstein-
tiles durch ein zufälliges der gleichen Art ersetzt werden. Beim Grass werden zusätzlich
Zierelemente und Hindernisse in die Karte eingefügt. So kann man mit
[SPACE] + Linksklick und [ZB ENTER] schneller Levels erstellen.
Mit [ZB1] wird tMap.ground[] zur Bearbeitung ausgewählt. Um Tiles in tMap.ground[1]
zu platzieren: [LINKS SHIFT] + Linksklick), mit [ZB4] tMap.obstacle und tMap.overlay
mit [ZB7]. Mit den Tasten daneben kann man die Layer (Ebenen) sichtbar/unsichtbar
machen: [ZB2] = ground; [ZB5 ]= obstacle; [ZB8] = overlay.
[F5] speichert die Map. Mit den Pfeiltasten kann man durch die verschiedenen Karten
navigieren. [ZB-] löscht die Karte.
Deklariere nun folgende Variabeln in "fnEditor":
STATIC lLayer // 0 = ground; 1 = obstacle; 2 = overlay
STATIC lGround = 1, lObstacle = 1, lOverlay = 1
LOCAL lx, ly
LOCAL lMapX, lMapY
LOCAL lLoop = TRUE
"lLayer" zeigt den aktuell bearbeiteten Layer an. Mit "lGround", "lObstacle" und
"lOverlay" werden die Layer sichtbar/unsichtbar gemacht (0:unsichtbar 1:sichtbar).
"lx" und "ly" sind Zähler für FOR. In "lMapX" und "lMapY" stehen die Koordinaten
des Tiles in "tMap[][]" das gerade bearbeitet wird (wird anhand der Position des
Mauszeigers errechnet).
Schreibe folgenden Code darunter:
KeyDelay = 7
REPEAT
// read mouse state
GOSUB subReadMouse
// compute map x/y tile
lMapX = mx / 16; lMapY = my / 16
"KeyDelay" wird auf 7 gesetzt und in jedem Frame dekrementiert damit ein zu schneller
Wechsel zwischen den Modi (Spiel/Editor) unterbunden wird.
Dann werden die Werte der Maus gelesen und die Koordinaten des aktuell bearbeiteten
Tiles ermittelt (mx und my sind die aktuellen Mauskoordinaten).
Nun färben wir den Bildschirm in der transparenten Farbe (damit man sieht wo noch kein
Tile platziert wurde) und zeichnen den Level auf den Bildschirm:
// draw background color
DRAWRECT 0, 0, 640, 480, RGB(255, 0, 128)
// display tiles
FOR ly = 0 TO 29
FOR lx = 0 TO 39
IF lGround
// draw ground tiles
DRAWANIM 3, tMap[lx][ly].ground[0], lx * 16, ly * 16
DRAWANIM 3, tMap[lx][ly].ground[1], lx * 16, ly * 16
ENDIF
IF lObstacle
// draw obstacle tiles
DRAWANIM 3, tMap[lx][ly].obstacle, lx * 16, ly * 16
ENDIF
IF lOverlay
// draw overlay tiles
DRAWANIM 3, tMap[lx][ly].overlay, lx * 16, ly * 16
ENDIF
NEXT
NEXT
Dann wird ermittelt ob das ausgewählte Tile oder der Selektor (wenn der rechte
Mausbutton gedrückt wird) gezeichnet werden soll (mbr = mouse button right).
In "eTile" ist der Index des ausgewählten Tiles gespeichert:
// draw selected tile / selector
IF mbr = FALSE
DRAWANIM 3, eTile, INTEGER(mx) / 16 * 16, INTEGER(my) / 16 * 16
ELSE
DRAWSPRITE 4, INTEGER(mx) / 16 * 16, INTEGER(my) / 16 * 16
ENDIF
Mit INTEGER(mx) / 16 * 16, INTEGER(my) / 16 * 16 bewirken wir dass das Tile/der Selektor
sich in 16er Schritten bewegt.
Hiermit zeichnen wir den Mauszeiger und blenden ein paar Informationen ein:
// draw mouse pointer
DRAWSPRITE 1, mx-15, my-15
// display layer information
ALPHAMODE -0.7
SELECT lLayer
CASE 0; PRINT "Ground Mode", 0, 0
CASE 1; PRINT "Obstacle Mode", 0, 0
CASE 2; PRINT "Overlay Mode", 0, 0
ENDSELECT
// display coordinates
PRINT SectorX +"/"+ SectorY, 0, 16
ALPHAMODE 0
Schreiben wir nun den Code, der für das platzieren/löschen/kopieren des Tiles
verantwortlich ist:
// set tile
IF mbl // left mouse button
SELECT lLayer
CASE 0 // ground
IF KEY(42) // [LEFT SHIFT]
tMap[lMapX][lMapY].ground[1] = eTile
ELSE
tMap[lMapX][lMapY].ground[0] = eTile
ENDIF
CASE 1 // obstacle
tMap[lMapX][lMapY].obstacle = eTile
CASE 2 // overlay
tMap[lMapX][lMapY].overlay = eTile
ENDSELECT
ENDIF
// delete tile
IF mbr // right mouse button
SELECT lLayer
CASE 0 // ground
IF KEY(42) // [LEFT SHIFT]
tMap[lMapX][lMapY].ground[1] = 0
ELSE
tMap[lMapX][lMapY].ground[0] = 0
ENDIF
CASE 1 // obstacle
tMap[lMapX][lMapY].obstacle = 0
CASE 2 // overlay
tMap[lMapX][lMapY].overlay = 0
ENDSELECT
ENDIF
// pick tile
IF mbm // middle mouse button
SELECT lLayer
CASE 0 // ground
IF KEY(42) // [LEFT SHIFT]
eTile = tMap[lMapX][lMapY].ground[1]
ELSE
eTile = tMap[lMapX][lMapY].ground[0]
ENDIF
CASE 1 // obstacle
eTile = tMap[lMapX][lMapY].obstacle
CASE 2 // overlay
eTile = tMap[lMapX][lMapY].overlay
ENDSELECT
ENDIF
Erst wird über "lLayer" geprüft, auf welche Ebene sich die Aktion bezieht.
Dann wird der neue Wert in die Karte geschrieben bzw. herausgelesen.
Mit folgendem Code wird "KeyDelay" dekrementiert und ermittelt, welche Layer sichtbar
sein sollen:
// decrease key delay
DEC KeyDelay, 1
IF KeyDelay < 0; KeyDelay = 0; ENDIF
// make layers visible/invisible
IF KEY(72) AND KeyDelay = 0 // [ZB8]
lOverlay = 1 - lOverlay
KeyDelay = 7
ENDIF
IF KEY(76) AND KeyDelay = 0 // [ZB5]
lObstacle = 1 - lObstacle
KeyDelay = 7
ENDIF
IF KEY(80) AND KeyDelay = 0 // [ZB2]
lGround = 1 - lGround
KeyDelay = 7
ENDIF
lGround = 1 - lGround bewirkt das der Wert von lGround zwischen 0 (False) und
1 (True) hin und herwechselt.
Dieser Code lässt den User die zu bearbeitende Ebene einstellen:
// select layer
// overlay
IF KEY(71) // [ZB7]
lLayer = 2
ENDIF
// obstacle
IF KEY(75) // [ZB4]
lLayer = 1
ENDIF
// ground
IF KEY(79) // [ZB1]
lLayer = 0
ENDIF
Zum Schluss fügen wir Code ein, der den Editor beendet, SHOWSCREEN aufruft und die
Schleife schliesst:
// quit editor mode
IF KEY(15) AND KeyDelay = 0 // [TAB]
lLoop = FALSE
KeyDelay = 7
ENDIF
SHOWSCREEN
UNTIL lLoop = FALSE
Hiermit wird der Editor von der Hauptschleife aus aufgerufen:
// change to editor mode
IF KEY(15) AND KeyDelay = 0 // [TAB]
fnEditor()
ENDIF
Schreiben wir nun eine Funktion, die uns ein Tile selektieren lässt:
// ------------------------------------------------------------- //
// --- FNSELECTTILE ---
// .............................................................
// tile selection mode
// ------------------------------------------------------------- //
FUNCTION fnSelectTile:
LOCAL lx, ly, lcnt, lTileIndex
WHILE KEY(57) // [SPACE]
// read mouse state
GOSUB subReadMouse
// draw background color
DRAWRECT 0, 0, 640, 480, RGB(255, 0, 128)
// reset counter
lcnt = 0
// display tiles
FOR ly = 0 TO 29
FOR lx = 0 TO 39
DRAWANIM 3, lcnt, lx * 16, ly * 16
INC lcnt, 1
IF lcnt = cNUM_TILES; BREAK; ENDIF
NEXT
IF lcnt = cNUM_TILES; BREAK; ENDIF
NEXT
// draw selector
DRAWSPRITE 4, INTEGER(mx) / 16 * 16, INTEGER(my) / 16 * 16
// tile index
lTileIndex = (INTEGER(my) / 16 * 40) + mx / 16
// select tile
IF mbl
eTile = lTileIndex
IF eTile > cNUM_TILES; eTile = 0; ENDIF
ENDIF
// fill map with chosen tile
IF mbr
FOR ly = 0 TO 29
FOR lx = 0 TO 39
tMap[lx][ly].ground[0] = lTileIndex
NEXT
NEXT
ENDIF
// draw mouse pointer
DRAWSPRITE 1, mx-15, my-15
SHOWSCREEN
WEND
ENDFUNCTION // FNSELECTTILE
Zuerst werden die Tiles der Reihe nach auf den Bildschirm gezeichnet. Die Schleife wird
unterbrochen, wenn die Maximalanzahl erreicht ist (IF lcnt = cNUM_TILES; BREAK; ENDIF).
Wenn der linke Mausbutton gedrückt wird, wird der aktuelle Index (lTileIndex) in
"eTile" gespeichert, beim rechten Mausbutton wird die Karte mit diesem Tile gefüllt
(Layer: tMap.ground[0]).
Jetzt muss die Funktion nur noch in fnEditor() aufgerufen werden:
// call selection mode
IF KEY(57) // [SPACE]
fnSelectTile()
ENDIF
Um nicht alle Tilevariationen, Zierelemente und Hindernisse selbst setzen zu müssen,
fügen wir diesen Code in unser Projekt ein:
// ------------------------------------------------------------- //
// --- FNRND ---
// ------------------------------------------------------------- //
FUNCTION fnRnd:
LOCAL lx, ly
FOR ly = 0 TO 29
FOR lx = 0 TO 39
SELECT tMap[lx][ly].ground[0]
CASE 2 TO 5 // grass
tMap[lx][ly].ground[0] = RND(3) + 2
IF RND(24) = 0
tMap[lx][ly].ground[1] = RND(19) + 10
IF tMap[lx][ly].ground[1] = 22
tMap[lx][ly].ground[1] = 0
ENDIF
ELSE
tMap[lx][ly].ground[1] = 0
ENDIF
IF RND(32) = 0 AND lx > 0 AND lx < 39 AND ly > 0 AND ly < 29
tMap[lx][ly].obstacle = RND(4) + 67
ELSE
tMap[lx][ly].obstacle = 0
ENDIF
CASE 6 TO 8 // bricks
tMap[lx][ly].ground[0] = RND(2) + 6
ENDSELECT
NEXT
NEXT
ENDFUNCTION // FNRND
Schreibe folgenden Code in "fnEditor":
// random tiles
IF KEY(156) // [ZB ENTER]
fnRnd()
ENDIF
Diese Funktion löscht die gesamte Karte:
// ------------------------------------------------------------- //
// --- FNCLEARMAP ---
// ------------------------------------------------------------- //
FUNCTION fnClearMap:
LOCAL lx, ly
FOR ly = 0 TO 29
FOR lx = 0 TO 39
tMap[lx][ly].ground[0] = 0
tMap[lx][ly].ground[1] = 0
tMap[lx][ly].obstacle = 0
tMap[lx][ly].overlay = 0
NEXT
NEXT
ENDFUNCTION // FNCLEARMAP
Füge folgenden Code in "fnEditor" ein:
IF KEY(74) // [ZB-]
fnClearMap()
ENDIF
Bevor wir Funktionen zum Speichern/Laden der Karten schreiben, folgt erstmal eine
Erläuterung zur Funktionsweise des Levelsystems:
Jede Map ist ein Sektor. Die Variabeln "SectorX" und "SektorY" speichern deine globalen
Koordinaten. Wenn der Charakter den aktuellen Level verlässt, wird der neue geladen,
falls dieser als Datei vorhanden ist. Wenn nicht, wird eine zufällige Map erstellt.
Wenn also ein Level abgespeichert wird, wird er als Datei abgelegt und bei
Betreten wieder geladen. Bei SektorX = 50 und SektorY = 100 heisst die zugehörige Datei
dann "50_100.map".
Erstelle im Hauptverzeichnis (dort befindet sich "Tutorial Game.exe") einen Ordner mit
dem Namen "Maps".
Deklariere folgende Variabeln in der Quelldatei "Tutorial Game.gbas":
// global coordinates
GLOBAL SectorX = 100, SectorY = 100
Tippe folgende Funktion zur Speicherung der Karten ein (Quelldatei: "Editor.gbas"):
// ------------------------------------------------------------- //
// --- FNSAVEMAP ---
// ------------------------------------------------------------- //
FUNCTION fnSaveMap:
LOCAL lx, ly
// open map directory
SETCURRENTDIR(MainDir$ + "/Maps")
OPENFILE(1, SectorX + "_" + SectorY + ".map", FALSE)
// save map data
FOR ly = 0 TO 29
FOR lx = 0 TO 39
WRITELINE 1, tMap[lx][ly].ground[0]
WRITELINE 1, tMap[lx][ly].ground[1]
WRITELINE 1, tMap[lx][ly].obstacle
WRITELINE 1, tMap[lx][ly].overlay
NEXT
NEXT
CLOSEFILE 1
// reset to main directory
SETCURRENTDIR(MainDir$)
ENDFUNCTION // FNSAVEMAP
Wenn du einen Level erstellt hast, vergiss nicht diesen über [F5] zu speichern, bevor
du den Nächsten ansteuerst, da sonst die Daten verloren gehen.
Mit dieser Funktion werden die Maps geladen:
// ------------------------------------------------------------- //
// --- FNLOADMAP ---
// ------------------------------------------------------------- //
FUNCTION fnLoadMap:
LOCAL lx, ly
// open map directory
SETCURRENTDIR(MainDir$ + "/Maps")
// load map when file exists
IF DOESFILEEXIST(SectorX + "_" + SectorY + ".map")
OPENFILE(1, SectorX + "_" + SectorY + ".map", TRUE)
// read map data
FOR ly = 0 TO 29
FOR lx = 0 TO 39
READLINE 1, tMap[lx][ly].ground[0]
READLINE 1, tMap[lx][ly].ground[1]
READLINE 1, tMap[lx][ly].obstacle
READLINE 1, tMap[lx][ly].overlay
NEXT
NEXT
CLOSEFILE 1
// reset to main directory
SETCURRENTDIR(MainDir$)
// create generic map if file doesn´t exists
ELSE
// initialize map
FOR ly = 0 TO 29
FOR lx = 0 TO 39
tMap[lx][ly].ground[0] = 2
tMap[lx][ly].ground[1] = 0
tMap[lx][ly].obstacle = 0
tMap[lx][ly].overlay = 0
NEXT
NEXT
// randomize tiles
SEEDRND SectorX + SectorY
fnRnd()
SEEDRND GETTIMERALL()
ENDIF
// reset to main directory
SETCURRENTDIR(MainDir$)
ENDFUNCTION // FNLOADMAP
Es wird geprüft, ob eine .map Datei des Sektors existiert. Wenn ja, wird diese geladen,
wenn nicht wird ein generischer Level erzeugt. SEEDRND soll sicherstellen dass dieser
bei jedem Betreten gleich aussieht.
Trage nun diesen Code in "fnEditor()" ein, damit du durch die Sektoren navigieren kannst:
// navigate trough sectors
IF KEY(200) AND KeyDelay = 0 AND SectorY > 0 // [ARROW UP]
DEC SectorY, 1; KeyDelay = 7; fnLoadMap()
ENDIF
IF KEY(208) AND KeyDelay = 0 // [ARROW DOWN]
INC SectorY, 1; KeyDelay = 7; fnLoadMap()
ENDIF
IF KEY(203) AND KeyDelay = 0 AND SectorX > 0 // [ARROW LEFT]
DEC SectorX, 1; KeyDelay = 7; fnLoadMap()
ENDIF
IF KEY(205) AND KeyDelay = 0 // [ARROW RIGHT]
INC SectorX, 1; KeyDelay = 7; fnLoadMap()
ENDIF
Um die aktuelle Karte zu speichern, füge folgenden Code ein:
// save map
IF KEY(63) // [F5]
fnSaveMap()
ENDIF
Damit der Charakter zu Beginn in einem fertigen Level steht, tragen wir folgenden Code
in "subINI:" ein:
// load map
SETCURRENTDIR(MainDir$ + "/Maps")
fnLoadMap()
Wechsle nun zur Quelldatei "TileEngine.gbas". Schreiben wir nun den Code, der die
overlay Grafiken zeichnet:
// ------------------------------------------------------------- //
// --- FNDRAWOVERLAY ---
// ------------------------------------------------------------- //
FUNCTION fnDrawOverlay:
LOCAL lx, ly
FOR ly = 0 TO 29
FOR lx = 0 TO 39
// draw overlay tiles
DRAWANIM 3, tMap[lx][ly].overlay, lx * 16, ly * 16
NEXT
NEXT
ENDFUNCTION // FNDRAWOVERLAY
Diese Funktion wird nach dem Zeichnen der Sprites aufgerufen. Da wir den Charakter aber
immer sichtbar haben wollen (wenn er z.B hinter einer Mauer steht), soll das overlay
weggeblendet werden, wenn er mit diesem Bereich in Kontakt kommt. Dazu wird das Tile
in dem sich der Charakter befindet, sowie die acht drumherum gescannt und mit ANIMCOLL()
auf Kollision geprüft. Um zu speichern, in welchem Tile sich der Charakter gerade
befindet, fügen wir folgende Variabeln in "TTchar" ein:
tileX // global x coordinate
tileY // global y coordinate
Diese werden für folgende Funktion gebraucht:
// ------------------------------------------------------------- //
// --- FNOVERLAYCOLLISION ---
// ------------------------------------------------------------- //
FUNCTION fnOverlayCollision:
LOCAL lx, ly, lx1, ly1, lx2, ly2, lColl
// set scanning coordinates
lx1 = tChar.tileX - 1
ly1 = tChar.tileY - 1
lx2 = tChar.tileX + 1
ly2 = tChar.tileY + 1
// correct scanning coordinates
IF lx1 < 0; lx1 = 0; ENDIF
IF ly1 < 0; ly1 = 0; ENDIF
IF lx2 > 39; lx2 = 39; ENDIF
IF ly2 > 29; ly2 = 29; ENDIF
// check for collision
FOR ly = ly1 TO ly2
FOR lx = lx1 TO lx2
IF ANIMCOLL(2, tChar.ani, tChar.x, tChar.y, 3, tMap[lx][ly].overlay, lx * 16, ly * 16)
lColl = TRUE
ENDIF
NEXT
NEXT
RETURN lColl
ENDFUNCTION // FNOVERLAYCOLLISION
Zuerst wird der Scan-Bereich initialisiert. Dann werden die Werte bei Bedarf korrigiert,
damit die Indizes nicht überschritten werden. Schliesslich wird geprüft, ob der
Charakter mit dem overlay Bereich in Kontakt kommt (wenn ja, wird lColl = TRUE
zurückgeliefert).
Damit das von "fnDrawOverlay()" registriert wird, ändern wir diese Funktion
folgendermassen ab:
// ------------------------------------------------------------- //
// --- FNDRAWOVERLAY ---
// ------------------------------------------------------------- //
FUNCTION fnDrawOverlay:
LOCAL lx, ly
IF fnOverlayCollision() = FALSE
FOR ly = 0 TO 29
FOR lx = 0 TO 39
// draw overlay tiles
DRAWANIM 3, tMap[lx][ly].overlay, lx * 16, ly * 16
NEXT
NEXT
ENDIF
ENDFUNCTION // FNDRAWOVERLAY
Bevor wir diese Funktionen wirksam machen, schreiben wir noch eine, die im Prinzip
wie "fnOverlayCollision()" funktioniert, nur dass diese dazu genutzt wird, den
Charakter daran zu hindern, sich durch ein tMap[][].obstacle zu bewegen:
// ------------------------------------------------------------- //
// --- FNOBSTACLECOLLISION ---
// ------------------------------------------------------------- //
FUNCTION fnObstacleCollision: lx, ly, lid, ltile
LOCAL lx1, ly1, lx2, ly2, lx3, ly3, lColl
// set scanning coordinates
lx1 = INTEGER( lx / 16) - 1
ly1 = INTEGER( ly / 16) - 1
lx2 = INTEGER( lx / 16) + 1
ly2 = INTEGER( ly / 16) + 1
// correct scanning coordinates
IF lx1 < 0; lx1 = 0; ENDIF
IF ly1 < 0; ly1 = 0; ENDIF
IF lx2 > 39; lx2 = 39; ENDIF
IF ly2 > 29; ly2 = 29; ENDIF
// check for collision
FOR ly3 = ly1 TO ly2
FOR lx3 = lx1 TO lx2
IF ANIMCOLL(lid, ltile, lx, ly, 3, tMap[lx3][ly3].obstacle, lx3 * 16, ly3 * 16)
lColl = TRUE
ENDIF
NEXT
NEXT
RETURN lColl
ENDFUNCTION // FNOBSTACLECOLLISION
Im Gegensatz zu "fnOverlayCollision()", werden hier Parameter übergeben, da diese
Funktion nicht nur vom Charakter genutzt wird. "lx" und "ly" sind die Koordinaten des
Objekts, "lid" der Index der ANIM und "ltile" das Bild.
Springe in die Hauptschleife und trage folgenden Code nach fnComputeChar() und DRAWANIM
ein:
fnDrawOverlay()
Springe nun zu "fnComputeChar()". Füge folgende Variabeln ein:
LOCAL lPrevX, lPrevY // stores previous coordinates
Diese Variabeln speichern die aktuellen Koordinaten des Spielers und ersetzen die
nachfolgenden, wenn er gegen ein Hinderniss (obstacle) gelaufen ist.
Ändere den Quellcode nach
SELECT lMoveDir
...
ENDSELECT
auf folgende Weise:
// save previous coordinates
lPrevX = tChar.x; lPrevY = tChar.y
// move character
IF lAngle <> -1
tChar.x = fnMoveX(lAngle, tChar.x, tChar.speed)
tChar.y = fnMoveY(lAngle, tChar.y, tChar.speed)
ENDIF
// check tile <<< NEW !!!
tChar.tileX = INTEGER( (tChar.x + 13) / 16)
tChar.tileY = INTEGER( (tChar.y + 8) / 16)
// check for obstacle collision / reset coordinates
IF fnObstacleCollision(tChar.x, tChar.y, 2, tChar.ani)
tChar.x = lPrevX; tChar.y = lPrevY
tChar.tileX = INTEGER( (tChar.x + 13) / 16)
tChar.tileY = INTEGER( (tChar.y + 8) / 16)
ENDIF
// load next map
IF tChar.x < 0 // west
DEC SectorX, 1
fnLoadMap()
tChar.x = 623
ENDIF
IF tChar.x > 623 // east
INC SectorX, 1
fnLoadMap()
tChar.x = 0
ENDIF
IF tChar.y < 0 // north
DEC SectorY, 1
fnLoadMap()
tChar.y = 463
ENDIF
IF tChar.y > 463 // south
INC SectorY, 1
fnLoadMap()
tChar.y = 0
ENDIF
Erst werden die Koordinaten in "lPrevX" und "lPrevY" gespeichert, dann wird der
Charakter bewegt. Danach wird überprüft in welchem Tile er sich befindet. Damit nicht
die linke obere Ecke als Bezugspunkt benutzt wird, sondern die Mitte des Sprites,
wird tChar.x mit 13 und tChar.y mit 8 addiert. Danach wird mittels
"fnObstacleCollision()" überprüft, ob der Charakter mit einem Bild der obstacle-
Ebene in Berührung gekommen ist. Wenn ja, werden die Koordinaten zurückgesetzt.
Wenn der Charakter einen Bildschirmrand erreicht, wird eine neue Karte geladen.
3. Action- und RPG-Elemente hinzufügen
Wir erweitern das Programm nun um folgende Elemente:
Wenn der linke Mausbutton gedrückt wird, entsteht ein Schuss dessen Bewegungswinkel
mittels "fnGetAngle()" zwischen Spielerkoordinaten und Mauscursor initialisiert wird.
Allerdings nur in bestimmten Intervallen. Wenn der Intervall also 15 beträgt, feuert
der Charakter, bei gedrückter Maustaste, 2 Schüsse pro Sekunde ab (30 FPS / 15).
Wenn ein Schuss ein "obstacle" berührt wird er gelöscht. Wenn er mit einem Gegner
kollidiert, wird er gelöscht und zieht diesem HP (Hitpoints) ab und dem Spieler
werden, wenn er einen Gegner besiegt, XP (Experience Points / Erfahrungspunkte)
gutgeschrieben. Die maximale Anzahl der Schüsse wird mit 3 initialisiert. Wenn also
bereits 3 davon existieren, wird der zuerst abgefeuerte durch einen neuen ersetzt.
Einige der Charakterwerte können verbessert werden, indem XP investiert werden.
Die Anzahl der Gegner wird bei Betreten eines durch Zufall erzeugten Levels (also
einem der nicht abgespeichert wurde) mit "RND()" ermittelt. Bei Gegnerkontakt werden
dem Spieler HP abgezogen. Zusätzlich kann jeder Gegner einen Schuss zur Zeit abfeuern,
der, je länger er existiert, weniger Schaden anrichtet. Wird ein Gegner besiegt, lässt
er einen "Gem" (Edelstein) zurück, den der Spieler aufsammeln kann. Gems werden in
der Sektion "Adventure-Elemente hinzufügen" als Währung benutzt.
Erweitern wir zunächst "TTchar" um folgende Variabeln:
shotpower // damage of shot
shotinterval // interval between shots in frames
interval_cnt // interval counter
shotspeed // speed in pixel per frame of shot
shotquantity // max number of shots
shotindex // current index of TTshot
gems // quantity of gems
maxHP // max hitpoints
HP // current hitpoints
healrate // healing rate per frame
XP // experience points
"shotpower" bestimmt wieviel HP dem Gegner bei einem Treffer abgezogen werden.
"shotinterval" ist der Intervall der nach jedem Schuss in "interval_cnt" eingetragen
wird. "interval_cnt" zählt dann rückwärts bis 0 - erst dann kann der nächste Schuss
abgefeuert werden. Der initiale Wert von "shotquantity" beträgt 3. "shotindex" zeigt
den aktuellen Index an, der für "tShot[]" (dazu kommen wir gleich) einen neuen Schuss
einträgt. Wenn dieser also 2 erreicht hat, springt er auf 0 zurück. "maxHP" ist der
maximale HP Wert des Spielers, "HP" der aktuelle, der mit "healrate" (Heilungsrate)
in jedem Frame inkrementiert wird. Folgende Werte sollen sich verbessern lassen:
speed, shotpower, shotinterval, shotspeed, shotquantity und maxHP.
In diesem TYPE werden dann die Werte für jeden Schuss gespeichert:
// type for shots
TYPE TTshot
x
y
angle
ENDTYPE
GLOBAL tShot[] AS TTshot
"angle" zeigt an, in welche Richtung sich der Schuss bewegt.
Dann brauchen wir noch ein TYPE für die Gegner:
// type for ghosts
TYPE TTghost
x
y
HP // hitpoints
attack_mode // 0 = hunt; 1 = random movement
attack_cnt // time to compute new attack mode
ani // current animation frame
ani_cnt // time to change animation frame
angle // movement angle
shot_x // x coord of shot
shot_y // y coord of shot
shot_cnt // duration counter of shot
shot_angle // movement angle of shot
ENDTYPE
GLOBAL tGhost[] AS TTghost
"attack_mode" bestimmt, ob sich der Gegner auf den Spieler zubewegt (0 = hunt) oder
sich in eine zufällige Richtung bewegt (1 = random). Der Wert von "attack_cnt" wird
in jedem Frame um 1 dekrementiert. Erreicht dieser 0 wird "attack_mode" neu berechnet.
"shot_x" und "shot_y" sind die aktuellen Koordinaten des Schusses. Erreicht "shot_cnt"
einen bestimmten Wert, wird ein neuer Schuss initialisiert.
Springe zu "subINI:" und initialisiere folgende Charakterwerte:
tChar.shotpower = 1 // damage of shot
tChar.shotinterval = 15 // interval between shots in frames
tChar.shotspeed = 4 // speed in pixel per frame of shot
tChar.shotquantity = 3 // maximal number of shots
tChar.HP = 100 // current hitpoints
tChar.maxHP = 100 // maximal hitpoints
tChar.healrate = 0.01 // initial healing rate / incrementor per frame
Dann wird "tShot[]" initialisiert:
// initialize shots
REDIM tShot[tChar.shotquantity]
FOR lcnt = 0 TO tChar.shotquantity - 1
tShot[lcnt].angle = -1
NEXT
Der Wert -1 bei "tShot[lcnt].angle" zeigt an dass es an dieser Stelle keinen Schuss
gibt.
Wir benötigen folgende Grafiken:
"Shot.png", "Ghost.png", "Gem.png", "GhostShot.png"
Lade diese:
// load shot
LOADSPRITE "Shot.png", 5
// load ghost anim
LOADANIM "Ghost.png", 6, 28, 32
// load gem GFX
LOADSPRITE "Gem.png", 7
// load ghost projectile
LOADSPRITE "GhostShot.png", 8
Schreiben wir nun eine Funktion, die die Spielerschüsse erschafft, bewegt und zeichnet:
// ------------------------------------------------------------- //
// --- FNCOMPUTESHOT ---
// ------------------------------------------------------------- //
FUNCTION fnComputeShot:
LOCAL lcnt
STATIC lInterval
// create and initialize shot
IF mbl AND tChar.interval_cnt = 0
INC tChar.shotindex, 1
IF tChar.shotindex = tChar.shotquantity
tChar.shotindex = 0
ENDIF
// initialize coordinates
tShot[tChar.shotindex].x = tChar.x - 3
tShot[tChar.shotindex].y = tChar.y - 8
// store angle between shot and mouse cursor
tShot[tChar.shotindex].angle = fnGetAngle(tShot[tChar.shotindex].x+16, tShot[tChar.shotindex].y+16, mx, my)
// set interval
tChar.interval_cnt = tChar.shotinterval
ENDIF
// move and draw shot
ALPHAMODE 0.75
FOR lcnt = 0 TO tChar.shotquantity - 1
// compute shot
IF tShot[lcnt].angle <> -1 // move shot if it´s existant
tShot[lcnt].x = fnMoveX(tShot[lcnt].angle, tShot[lcnt].x, tChar.shotspeed)
tShot[lcnt].y = fnMoveY(tShot[lcnt].angle, tShot[lcnt].y, tChar.shotspeed)
// draw shot
DRAWSPRITE 5, tShot[lcnt].x, tShot[lcnt].y
// delete shot when it leaves the screen
IF tShot[lcnt].x < - 16 OR tShot[lcnt].x > 656 OR tShot[lcnt].y < -16 OR tShot[lcnt].y > 496
tShot[lcnt].angle = -1
// delete shot when it collides with an obstacle
ELSEIF fnObstacleCollision(tShot[lcnt].x, tShot[lcnt].y, 5, 0)
tShot[lcnt].angle = -1
ENDIF
ENDIF
NEXT
ALPHAMODE 0
// decrease interval
DEC tChar.interval_cnt, 1; IF tChar.interval_cnt < 0; tChar.interval_cnt = 0; ENDIF
ENDFUNCTION // FNCOMPUTESHOT
Zuerst wird geprüft, ob der Intervall bei 0 liegt wenn die linke Maustaste gedrückt
wird. Ist beides TRUE, wird der neue Index für diesen Schuss für "tShot[]" ermittelt.
Danach werden die Koordinaten sowie die Bewegungsrichtung (angle) errechnet und der
Intervall neu eingestellt.
Wenn der Wert von "tShot[lcnt].angle" nicht -1 beträgt, ist der Schuss aktiv und
seine neuen Koordinaten sowie die Konditionen für seine Löschung werden berechnet.
Zum Schluss wird der Intervall für einen neuen Schuss dekrementiert.
Rufe die Funktion zwischen "fnComputeChar()" und "fnDrawOverlay()"
in der Hauptschleife auf.
Kommen wir nun zum Code, der die Gegner bewegt und zeichnet. Deklariere zunächst
folgende Variable:
// number of ghost units
GLOBAL NumGhosts
Hier wird die Anzahl der Gegner gespeichert.
Erstelle eine neue Funktion und nenne sie "fnComputeGhost". Füge folgenden Code ein:
LOCAL lcnt, lcnt2, lDistance, lAngle
In "lcnt" wird der aktuelle Index des zu bearbeitenden Gegners gespeichert. "lcnt2"
ist ein Zähler, der genutzt wird, um zu ermitteln ob ein Gegner mit einem Schuss
kollidiert. In "lDistance" wird die Distanz zwischen Spieler und Gegner gespeichert.
Abhängig von diesem Wert wird ermittelt welcher Angriffsmodus genutzt wird. Ist der
Charakter zu weit entfernt, wird "tGhost[].attack_mode" auf 0 (hunt) gesetzt.
Das heisst dass der Gegner sich dann auf den Spieler zubewegt. Um die Bewegungsrichtung
dafür zu ermitteln, wird "lAngle" benutzt.
Füge folgenden Code ein:
IF NumGhosts <> 0 // if any ghosts exist
FOR lcnt = 0 TO NumGhosts - 1
IF tGhost[lcnt].HP > 0 // alive
// compute attack mode
IF tGhost[lcnt].attack_cnt = 0
// compute distance / decide attack mode
lDistance = fnGetDistance(tGhost[lcnt].x, tGhost[lcnt].y, tChar.x + 13, tChar.y + 8)
IF lDistance > 250
tGhost[lcnt].attack_mode = 0 // hunt
ELSE
tGhost[lcnt].attack_mode = 1 // random
tGhost[lcnt].angle = fnGetAngle(tGhost[lcnt].x, tGhost[lcnt].y, RND(639), RND(479))
ENDIF
// set attack counter
tGhost[lcnt].attack_cnt = RND(15) + 15
ENDIF
Zuerst wird geprüft ob es überhaupt Gegner gibt. Wenn ja, werden diese der Reihe nach
berechnet. Wenn der Gegner noch am Leben ist (HP > 0) wird geprüft ob es an der Zeit
ist, den Angriffsmodus neu zu berechnen (tGhost[lcnt].attack_cnt = 0). Wenn der mit
"fnGetDistance()" ermittelte Wert über 250 liegt, schaltet der Angriffsmodus auf 0
(hunt). Danach wird der Angriffszähler (tGhost[lcnt].attack_cnt) auf einen Wert
zwischen 15 und 30 gesetzt.
Hiermit wird dieser dekrementiert:
// decrease attack counter
DEC tGhost[lcnt].attack_cnt, 1
Dann wird geprüft ob eine neue Animationsphase fällig ist:
// compute animation
DEC tGhost[lcnt].ani_cnt, 1
IF tGhost[lcnt].ani_cnt = 0
tGhost[lcnt].ani = 1 - tGhost[lcnt].ani
tGhost[lcnt].ani_cnt = 15
ENDIF
Die Animationsphase wechselt alle 15 Frames.
Hiermit wird der Gegner je nach Angriffsmodus bewegt:
// move ghost
IF tGhost[lcnt].attack_mode = 0 // hunt
lAngle = fnGetAngle(tGhost[lcnt].x, tGhost[lcnt].y, tChar.x + 13, tChar.y + 8)
tGhost[lcnt].x = fnMoveX(lAngle, tGhost[lcnt].x, 3)
tGhost[lcnt].y = fnMoveY(lAngle, tGhost[lcnt].y, 3)
ELSE // random
tGhost[lcnt].x = fnMoveX(tGhost[lcnt].angle, tGhost[lcnt].x, 3)
tGhost[lcnt].y = fnMoveY(tGhost[lcnt].angle, tGhost[lcnt].y, 3)
ENDIF
Schreiben wir nun den Code, der den Gegnerschuss bewegt, zeichnet und auf Kollision
prüft:
// compute ghost shot
DEC tGhost[lcnt].shot_cnt, 1
// initialize shot
IF tGhost[lcnt].shot_cnt = 0
tGhost[lcnt].shot_x = tGhost[lcnt].x
tGhost[lcnt].shot_y = tGhost[lcnt].y
tGhost[lcnt].shot_angle = fnGetAngle(tGhost[lcnt].x, tGhost[lcnt].y, tChar.x + 13, tChar.y + 8)
tGhost[lcnt].shot_cnt = 90
ENDIF
// move shot
tGhost[lcnt].shot_x = fnMoveX(tGhost[lcnt].shot_angle, tGhost[lcnt].shot_x, 5)
tGhost[lcnt].shot_y = fnMoveY(tGhost[lcnt].shot_angle, tGhost[lcnt].shot_y, 5)
// draw shot
ALPHAMODE (tGhost[lcnt].shot_cnt + 10) / 100
DRAWSPRITE 8, tGhost[lcnt].shot_x - 16, tGhost[lcnt].shot_y - 16
// check collision: ghost shot -> player
IF ANIMCOLL(8, 0, tGhost[lcnt].shot_x - 16, tGhost[lcnt].shot_y - 16, 2, tChar.ani, tChar.x, tChar.y)
DEC tChar.HP, tGhost[lcnt].shot_cnt / 10 // decrease HP
tGhost[lcnt].shot_x = 10000
tGhost[lcnt].shot_y = 10000
ENDIF
Wenn "tGhost[].shot_cnt" bei 0 liegt, wird ein neuer Schuss initialisiert. Nachdem
die Koordinaten und Bewegungsrichtung eingestellt wurden, wird "shot_cnt" auf 90
gesetzt. Dieser Wert wird in jedem Frame um 1 dekrementiert. Dass heisst dass der
Schuss 3 Sekunden lang existiert, bevor ein neuer initialisiert wird. Nachdem der
Schuss bewegt und gezeichnet wurde, wird geprüft ob er mit dem Spieler kollidiert.
Wenn ja, werden dem Charakter 10 HP abgezogen. Indem die Koordinaten dann auf 10000
gesetzt werden, wird vehindert dass der Schuss für die Dauer seiner Existenz noch
einmal Schaden anrichtet.
Schreibe nun folgenden Code um die Kollision zwischen Gegner und Spielerschuss
auszuwerten:
// check collision: ghost -> player shot
FOR lcnt2 = 0 TO tChar.shotquantity - 1
IF tShot[lcnt2].angle <> -1 // if shot is active
IF ANIMCOLL(6, 0, tGhost[lcnt].x - 14, tGhost[lcnt].y - 16, 5, 0, tShot[lcnt2].x, tShot[lcnt2].y)
DEC tGhost[lcnt].HP, tChar.shotpower // decrease ghost HP by shotpower
tShot[lcnt2].angle = -1 // "delete" shot
// causes sprite to "blink" when hit
DRAWANIM 6, tGhost[lcnt].ani, tGhost[lcnt].x - 14, tGhost[lcnt].y - 16
ENDIF
ENDIF
NEXT
Wenn der Gegner einen Spielerschuss berührt, werden seine HP je nach Schussstärke
reduziert. Danach wird der Schuss mittels "Shot[lcnt2].angle = -1" aus dem Verkehr
gezogen.
Sobald die HP des Gegners kleiner oder gleich 0 ist, werden die XP des Spielers um
10 inkrementiert:
// increase XP if ghost is dead
IF tGhost[lcnt].HP <= 0
INC tChar.XP, 10
tGhost[lcnt].HP = 0
ENDIF
Solange HP den Wert 0 hat, wird statt dem Gegner ein Gem gezeichnet. Wenn dieser
vom Charakter aufgenommen wird, wird HP auf -1 gesetzt (kommt später).
Wenn Gegner und Spieler kollidieren, werden Letzterem 1 HP pro Frame abgezogen:
// check collision: ghost -> player
IF ANIMCOLL(6, 0, tGhost[lcnt].x - 14, tGhost[lcnt].y - 16, 2, tChar.ani, tChar.x, tChar.y)
DEC tChar.HP, 1
ENDIF
Nun wird der Gegner gezeichnet:
// draw ghost
ALPHAMODE -0.5
DRAWANIM 6, tGhost[lcnt].ani, tGhost[lcnt].x - 14, tGhost[lcnt].y - 16
Hier folgt der Rest der Funktion:
ELSEIF tGhost[lcnt].HP = 0 // gem
// draw gem
ALPHAMODE 1
DRAWSPRITE 7, tGhost[lcnt].x - 16, tGhost[lcnt].y - 16
// get gem
IF ANIMCOLL(7, 0, tGhost[lcnt].x - 16, tGhost[lcnt].y - 16, 2, tChar.ani, tChar.x, tChar.y)
tGhost[lcnt].HP = -1
INC tChar.gems, 1
ENDIF
ENDIF
NEXT
ALPHAMODE 0
ENDIF
Das "ELSEIF" bezieht sich auf "IF tGhost[lcnt].HP > 0 // alive", prüft also nach ob der
Gem bei HP = 0 dargestellt oder bei HP = -1 (wenn er vom Charakter aufgenommen wurde)
ignoriert werden soll.
Rufe die Funktion in der Hauptschleife zwischen "fnComputeShot()" und "fnDrawOverlay()"
auf.
Wenn der Spieler eine generisch erzeugte Karte betritt, soll eine zufällige Anzahl an
Gegnern erzeugt werden. Hier ist der Code dazu:
// ------------------------------------------------------------- //
// --- FNCREATEGHOSTS ---
// ------------------------------------------------------------- //
FUNCTION fnCreateGhosts:
LOCAL lcnt
// randomize ghost quantity
NumGhosts = RND(10)
REDIM tGhost[NumGhosts]
// initialize ghosts
FOR lcnt = 0 TO NumGhosts - 1
tGhost[lcnt].x = RND(639)
tGhost[lcnt].y = RND(479)
tGhost[lcnt].HP = 5
tGhost[lcnt].attack_cnt = 1
tGhost[lcnt].ani = RND(1)
tGhost[lcnt].ani_cnt = RND(14) + 1
tGhost[lcnt].shot_x = 10000
tGhost[lcnt].shot_y = 10000
tGhost[lcnt].shot_cnt = RND(89) + 1
NEXT
ENDFUNCTION // FNCREATEGHOSTS
Springe nun zu "fnLoadMap()". Um sicherzustellen dass es keine Gegner in
abgespeicherten Karten gibt, fügen wir folgenden Code ein:
// reset to main directory
SETCURRENTDIR(MainDir$)
// set number of ghosts to 0
NumGhosts = 0
// create generic map if file doesn´t exists
ELSE
Und diesen nach dem ELSE:
// create ghosts
fnCreateGhosts()
Damit die abgegebenen Schüsse des Gegners nicht in einer neu betretenen Karte weiter
berechnet werden, löschen wir diese nach dem ENDIF mit folgendem Code:
// clear shots
FOR lx = 0 TO tChar.shotquantity -1
tShot[lx].angle = -1
NEXT
Kommen wir nun zu dem Code der dem Spieler Schaden zufügt und die Sterbesequenz
einleitet. Für die Sterbesequenz wird folgende Variable deklariert:
// counter for death animation
GLOBAL DeathCnt
Sie ist der Index der Sterbeanimation.
Springe zu "fnComputeChar()". Füge dort folgenden Code ein:
// heal hitpoints
INC tChar.HP, tChar.healrate
IF tChar.HP > tChar.maxHP; tChar.HP = tChar.maxHP; ENDIF
Der Charakter heilt seine HP in jedem Frame um den Wert, der in ".healrate" gespeichert
ist.
Um die Kampfanimation des Charakters bei gedrückter Maustaste darzustellen wird dieser
Code nach
SELECT lMoveDir
...
ENDSELECT
eingefügt:
// attack animation
IF mbl; INC tChar.ani, 20; ENDIF
Springe nun in die Hauptschleife. Ersetze diesen Code
fnComputeChar()
DRAWANIM 2, tChar.ani, tChar.x, tChar.y
durch diesen:
// compute character / death animation
IF tChar.HP > 0 // alive
fnComputeChar()
DRAWANIM 2, tChar.ani, tChar.x, tChar.y
ELSE // death
DRAWANIM 2, 40 + DeathCnt, tChar.x, tChar.y
INC DeathCnt, 0.3
IF DeathCnt > 11; DeathCnt = 11; ENDIF
PRINT "ENTER", 285, 233
IF KEY(28) // [ENTER]
tChar.HP = 1
SectorX = 100
SectorY = 100
tChar.x = 307; tChar.y = 232
tChar.gems = 0
DeathCnt = 0
fnLoadMap()
ENDIF
ENDIF
Wenn die HP des Spielers über 0 liegen, wird "fnComputeChar()" ausgeführt. Wenn die HP
unter 0 liegen wird die Sterbesequenz ausgeführt. Dabei wird "DeathCnt" in jedem Frame
um 0.3 inkrementiert und mit 40 (dem anfänglichen Index für die Sterbesequenz) addiert
bis alle zwölf Animationsphasen durchlaufen sind. Wenn die Enter-Taste gedrückt wird,
wird der Charakter in Sektor 100/100 zurückversetzt und seine Gems gelöscht.
Damit der Spieler seinen HP-Status überblicken kann, soll ein Gesundheitsbalken am
rechten Bildschirmrand eingeblendet werden. Füge folgenden Code nach
fnDrawOverlay()
ein:
// display health bar
IF tChar.HP > 0
ALPHAMODE 0.5
DRAWRECT 623, 471, 8, -tChar.HP, RGB(255, 0, 0)
ALPHAMODE 0
ENDIF
Dieser Balken ist so viele Pixel hoch, wie der Charakter HP hat.
Der Spieler soll die Möglichkeit haben, seine Attribute mit seinen XP zu "kaufen".
Das soll auf folgende Weise funktionieren:
Solange der Spieler die Taste [C] drückt wird der Statusbildschirm eingeblendet.
Hier stehen die aktuellen Attribute und die Kosten um sie zu erhöhen. Dahinter steht
der Inkrementor für die entsprechende Variable. Mit einem Linksklick erhöht man,
vorrausgesetzt die XP reichen aus, das Attribut:
// ------------------------------------------------------------- //
// --- FNATTRIBUTES ---
// ------------------------------------------------------------- //
FUNCTION fnAttributes:
LOCAL lIndex, lDelay = 7
// grab screen
GRABSPRITE 0, 0, 0, 640, 480
WHILE KEY(46) // [C]
GOSUB subReadMouse
// draw backgound
ALPHAMODE 0.5
DRAWSPRITE 0, 0, 0
ALPHAMODE 0
// compute index / draw rectangle
lIndex = INTEGER(my) / 32
IF lIndex > 1 AND lIndex < 8
ALPHAMODE 0.5
DRAWRECT 0, lIndex * 32, 640, 32, RGB(255, 0, 0)
ALPHAMODE 0
ENDIF
PRINT "XP: " + tChar.XP, 16, 16
// speed
PRINT "Geschwindigkeit = " + tChar.speed, 16, 64
PRINT "Kosten:" + ( tChar.speed * 10 + 100 ) + " / +0.1", 16, 80
IF mbl AND lIndex = 2 AND lDelay = 0 AND ( tChar.speed * 10 + 100 ) <= tChar.XP
DEC tChar.XP, ( tChar.speed * 10 + 100 )
INC tChar.speed, 0.1
lDelay = 7
ENDIF
// max HP
PRINT "max HP = " + tChar.maxHP, 16, 96
PRINT "Kosten:" + ( INTEGER(tChar.maxHP / 10) + 10 ) + " / +1", 16, 112
IF mbl AND lIndex = 3 AND lDelay = 0 AND ( INTEGER(tChar.maxHP / 10) + 10 ) <= tChar.XP
DEC tChar.XP, ( INTEGER(tChar.maxHP / 10) + 10 )
INC tChar.maxHP, 1
tChar.healrate = tChar.maxHP / 10000
lDelay = 7
ENDIF
// shotpower
PRINT "Magie: Schaden = " + tChar.shotpower, 16, 128
PRINT "Kosten:" + ( tChar.shotpower * 100 + 100) + " / +1", 16, 144
IF mbl AND lIndex = 4 AND lDelay = 0 AND ( tChar.shotpower * 100 + 100) <= tChar.XP
DEC tChar.XP, ( tChar.shotpower * 100 + 100)
INC tChar.shotpower, 1
lDelay = 7
ENDIF
// interval
PRINT "Magie: Intervall = " + tChar.shotinterval, 16, 160
PRINT "Kosten:" + ( (15 - tChar.shotinterval) * 100 + 100 ) + " / -1", 16, 176
IF mbl AND lIndex = 5 AND lDelay = 0 AND ( (15 - tChar.shotinterval) * 100 + 100 ) <= tChar.XP
DEC tChar.XP, ( (15 - tChar.shotinterval) * 100 + 100 )
DEC tChar.shotinterval, 1
lDelay = 7
ENDIF
// shotspeed
PRINT "Magie: Geschwindigkeit = " + tChar.shotspeed, 16, 192
PRINT "Kosten:" + ( tChar.shotspeed * 10 + 100 ) + " / +0.5", 16, 208
IF mbl AND lIndex = 6 AND lDelay = 0 AND ( tChar.shotspeed * 10 + 100 ) <= tChar.XP
DEC tChar.XP, ( tChar.shotspeed * 10 + 100 )
INC tChar.shotspeed, 0.5
lDelay = 7
ENDIF
// shotquantity
PRINT "Magie: Anzahl = " + tChar.shotquantity, 16, 224
PRINT "Kosten:" + ( tChar.shotquantity * 10 + 100 ) + " / +1", 16, 240
IF mbl AND lIndex = 7 AND lDelay = 0 AND ( tChar.shotquantity * 10 + 100 ) <= tChar.XP
DEC tChar.XP, ( tChar.shotquantity * 10 + 100 )
INC tChar.shotquantity, 1
REDIM tShot[tChar.shotquantity]
lDelay = 7
ENDIF
DEC lDelay, 1
IF lDelay < 0; lDelay = 0; ENDIF
// draw mouse pointer
DRAWSPRITE 1, mx-15, my-15
SHOWSCREEN
WEND
ALPHAMODE 0.5
DRAWSPRITE 0, 0, 0
ALPHAMODE 0
// delete sprite
LOADSPRITE "", 0
ENDFUNCTION // FNATTRIBUTES
Mit "GRABSPRITE" wird der gesamte Bildschirm kopiert und in jedem Frame mit
ALPHAMODE 0.5 gezeichnet um ihn abzudunkeln. In "lIndex" wird ermittelt, in welcher
der 32 Pixel hohen Zeile sich der Selektionsbalken befindet. Liegt der Index bei
2 wird bei einem Linksklick tChar.speed erhöht (wenn die XP ausreichen), bei lIndex = 3
die HP usw. Die Berechnungen der Kosten zur Verbesserung der Attribute sind schlecht
umgesetzt. Für ein fertiges Spiel sollten diese natürlich sorgfältig ausbalanciert
werden. Auch sollte der Attributsmodus dann visuell ansprechender gestaltet sein.
Nun muss nur noch dieser Code in die Hauptschleife geschrieben werden:
// call attributes screen
IF KEY(46) // [C]
fnAttributes()
ENDIF
Hier endet der erste Teil des Tutorials.
Die mit "<<< NEW !!!" markierten Stellen im Quelltext des ganzen Programms, sind die
in diesem Teil nicht Besprochenen (also die Adventure-Elemente).
Wenn jemand bessere/alternative Methoden vorschlagen kann - bitte posten!