Author Topic: Weiterführendes Tutorial Teil 1  (Read 2357 times)

Offline Corax

  • Mr. Drawsprite
  • **
  • Posts: 57
    • View Profile
Weiterführendes Tutorial Teil 1
« on: 2013-May-24 »
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:

Code: GLBasic [Select]
// --------------------------------- //
// 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.

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// -=#  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).

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// -=#  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.

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// -=#  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

Offline Corax

  • Mr. Drawsprite
  • **
  • Posts: 57
    • View Profile
Re: Weiterführendes Tutorial Teil 1
« Reply #1 on: 2013-May-24 »
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:

Code: GLBasic [Select]
   // 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):

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:":

Code: GLBasic [Select]
   // initialize character values
   tChar.x = 307; tChar.y = 232
   tChar.speed = 3
 

Jetzt muss nur noch folgender Code in die Hauptschleife eingefügt werden:

Code: GLBasic [Select]
   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):

Code: GLBasic [Select]
// --------------------------------- //
// 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
 

Offline Corax

  • Mr. Drawsprite
  • **
  • Posts: 57
    • View Profile
Re: Weiterführendes Tutorial Teil 1
« Reply #2 on: 2013-May-24 »
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:

Code: GLBasic [Select]
   // load tiles
   LOADANIM "Tiles.png", 3, 16, 16

   // load selector
   LOADSPRITE "Selector.png", 4
 

Erstelle eine Type-Struktur, die sämtliche Leveldaten enthält:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
   CONSTANT cNUM_TILES = 76 // number of tiles
 

und

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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":

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // quit editor mode
      IF KEY(15) AND KeyDelay = 0 // [TAB]
         lLoop = FALSE
         KeyDelay = 7
      ENDIF

      SHOWSCREEN

   UNTIL lLoop = FALSE
 

Offline Corax

  • Mr. Drawsprite
  • **
  • Posts: 57
    • View Profile
Re: Weiterführendes Tutorial Teil 1
« Reply #3 on: 2013-May-24 »
Hiermit wird der Editor von der Hauptschleife aus aufgerufen:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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":

Code: GLBasic [Select]
      // random tiles
      IF KEY(156) // [ZB ENTER]
         fnRnd()
      ENDIF
 

Diese Funktion löscht die gesamte Karte:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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":

Code: GLBasic [Select]
   // global coordinates
   GLOBAL SectorX = 100, SectorY = 100
 

Tippe folgende Funktion zur Speicherung der Karten ein (Quelldatei: "Editor.gbas"):

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
   // load map
   SETCURRENTDIR(MainDir$ + "/Maps")
   fnLoadMap()
 

Wechsle nun zur Quelldatei "TileEngine.gbas". Schreiben wir nun den Code, der die
overlay Grafiken zeichnet:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
      tileX // global x coordinate
      tileY // global y coordinate
 

Diese werden für folgende Funktion gebraucht:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
   fnDrawOverlay()
 

Springe nun zu "fnComputeChar()". Füge folgende Variabeln ein:

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
   // 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.

Offline Corax

  • Mr. Drawsprite
  • **
  • Posts: 57
    • View Profile
Re: Weiterführendes Tutorial Teil 1
« Reply #4 on: 2013-May-24 »
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:

Code: GLBasic [Select]
      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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
   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:

Code: GLBasic [Select]
            // decrease attack counter
            DEC tGhost[lcnt].attack_cnt, 1
 

Dann wird geprüft ob eine neue Animationsphase fällig ist:

Code: GLBasic [Select]
            // 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:

Code: GLBasic [Select]
            // 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:

Code: GLBasic [Select]
            // 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:

Code: GLBasic [Select]
            // 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:

Code: GLBasic [Select]
            // 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:

Code: GLBasic [Select]
            // 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:

Code: GLBasic [Select]
            // draw ghost
            ALPHAMODE -0.5
            DRAWANIM 6, tGhost[lcnt].ani, tGhost[lcnt].x - 14, tGhost[lcnt].y - 16
 

Hier folgt der Rest der Funktion:

Code: GLBasic [Select]
         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.

Offline Corax

  • Mr. Drawsprite
  • **
  • Posts: 57
    • View Profile
Re: Weiterführendes Tutorial Teil 1
« Reply #5 on: 2013-May-24 »
Wenn der Spieler eine generisch erzeugte Karte betritt, soll eine zufällige Anzahl an
Gegnern erzeugt werden. Hier ist der Code dazu:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
      // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // counter for death animation
   GLOBAL DeathCnt
 

Sie ist der Index der Sterbeanimation.

Springe zu "fnComputeChar()". Füge dort folgenden Code ein:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
   // 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:

Code: GLBasic [Select]
// ------------------------------------------------------------- //
// ---  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:

Code: GLBasic [Select]
   // 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!