So weit, so gut???
Lasst uns nun das Puzzle vorläufig abschließen. Wir haben im letzten Beitrag ja schon alles vorbereitet, sodass wir jetzt nur noch die Bildschirme fürs Weiterkommen oder Game Over anzeigen müssen.
Game Over
Kümmern wir uns zunächst ums Game Over. Wir wollen im Falle eines scheiterns, einen abschließenden Bildschirm anzeigen. Dazu entwerfen wir im Char Screen Editor unseren Game Over Bildschirm mit dem Namen „GameOver.charscreen“.
Jetzt noch die Funktion gameOver anpassen:
;******************************************************************************* ;*** Der Spieler hat verloren ;******************************************************************************* ;*** Übergabe: - ;******************************************************************************* ;*** Rückgabe: - ;******************************************************************************* ;*** ändert : A, SR ;******************************************************************************* !zone gameOver gameOver lda #$00 ;Alle Sprites sta SPRITEACTIVE ;abschalten ;*** GameOver-BS anzeigen ldx #<screenGameOver ;LSB ins X-Register ldy #>screenGameOver ;MSB ins Y-Register jsr drawScreen ;Bild ausgeben .loop jsr joystickInput ;warten bis and #JOY_FIRE ;der Feuerknopf gedrückt wurde bne .loop .loop1 jsr joystickInput ;und jetzt warten bis and #JOY_FIRE ;der Feuerknopf wieder losgelassen wurde beq .loop1 jmp main ;Programm neu starten
Wir schalten zu Beginn alle Sprites ab, die benötigen wir hier ja nicht mehr. Dann rufen wir unsere bekannte Routine drawScreen zum Anzeigen der Game Over Meldung auf. Abschließend warten wir darauf, dass der Feuerknopf gedrückt und wieder losgelassen wird, bevor wir zum Titelbild zurückspringen.
Gewonnen!
Jetzt noch etwas Ähnliches für das Lösen des Puzzles.
Auch hier zeigen wir einen Bildschirm „Solved.charscreen“ an, der den Spieler darüber informiert, dass er es geschafft hat und was als nächstes geschieht.
Auch hier die dazugehörige Funktion puzzleSolved anpassen:
;******************************************************************************* ;*** Der Spieler hat gewonnen ;******************************************************************************* ;*** Übergabe: - ;******************************************************************************* ;*** Rückgabe: - ;******************************************************************************* ;*** ändert : A, SR ;******************************************************************************* !zone puzzleSolved puzzleSolved lda #$00 ;abschalten aller sta SPRITEACTIVE ;Sprites ;*** 'Geschaff'-BS anzeigen ldx #<screenSolved ;LSB ins X-Register ldy #>screenSolved ;MSB ins Y-Register jsr drawScreen ;Bild ausgeben .loop jsr joystickInput ;prüfen, ob der and #JOY_FIRE ;Feuerknopf gedrückt wurde bne .loop .loop1 jsr joystickInput ;prüfen, ob der and #JOY_FIRE ;Feuerknopf wieder losgelassen wurde beq .loop1 jmp main ;Noch gibt es keinen weiteren Level, ;also erstmal einfach neu starten.
Eigentlich ist alles so wie bei gameOver, nur das hier ein anderer Bildschirm angezeigt wird. Da noch kein weiterer Level vorhanden ist, springen wir auch hier erstmal zurück zum Startbild.
Damit unsere beiden Bildschirme gefunden werden, müssen wir die natürlich noch einbinden, wie wäre es hinter screenPuzzle:
;*** Verloren: Die Zeit ist abgelaufen screenGameOver !media "GameOver.charscreen",charcolor ;*** Geschafft, das Puzzle wurde gelöst screenSolved !media "Solved.charscreen",charcolor
Wenn ihr das Programm nun startet, sollten die entsprechenden Bildschirme angezeigt werden. Allerdings klappt der Neustart noch nicht so ganz.
Da durchs Spielen einige Werte geändert wurden, müssen wir diese für einen erneuten Versuch natürlich wieder auf die korrekten Startwerte zurückstellen.
Fügen wir hinter puzzleMainLoop diese neue Routine ein:
;******************************************************************************* ;*** Ein neues Puzzle beginnen ;******************************************************************************* ;*** Übergabe: - ;******************************************************************************* ;*** Rückgabe: Alle veränderbaren Werte wurden zurückgestellt ;******************************************************************************* ;*** ändert : A, X, Y, SR ;******************************************************************************* !zone puzzleStart puzzleStart lda #COLOR_BLACK ;Schwarz in den Akku und sta VICBACKGROUNDCOLOR ;Hintergrund- sowie sta VICBORDERCOLOR ;Rahmenfarbe setzen ;*** Puzzle-BS anzeigen ldx #<screenPuzzle ;LSB ins X-Register ldy #>screenPuzzle ;MSB ins Y-Register jsr drawScreen ;Bild ausgeben lda #$ff ;leeres Feld wieder sta tileOrder+8 ;rechts unten lda #$08 ;auch die Hilfsvariable sta freetilePos ;zurückstellen ldx #$07 ;Ein gelöstest Puzzle erstellen .loop txa sta tileOrder,X dex bpl .loop jsr loadSprites ;Sprites laden jsr shuffleTiles ;Puzzle mischen lda #$14 ;Timer zurückstellen sta timer lda #$00 ;Uhr auf NULL stellen sta CIA1_CLOCK_SECONDS ;Sekunden und sta CIA1_CLOCK_TENTH ;zehntel reichen uns rts ;zurück
Das Meiste stammt aus puzzleMain und wird dort von uns gleich entfernt. Wir setzen die Rahmen- und Hintergrundfarbe auf schwarz und zeigen den Puzzle-Bildschirm an. Dann sorgen wir dafür, dass wir ein gelöstes Puzzle haben, bevor wir die Sprites laden und das Puzzle mischen. Dann wieder unseren Zähler für den timer auf 20 zurückstellen und die Uhrzeit im CIA1 auf Null setzen.
Wie erwähnt können wir jetzt puzzleMain bereinigen. Zwischen den beiden Sprungmarken benötigen wir nur noch den Aufruf unserer neuen Funktion (siehe Markierte Zeile).
;******************************************************************************* ;*** Das Puzzlespiel starten ;******************************************************************************* !zone puzzleMain puzzleMain jsr puzzleStart ;Alles auf Anfang stellen ;******************************************************************************* ;*** Eingabeschleife für das Puzzle ;******************************************************************************* puzzleMainLoop ;Start der Endlosschleife jsr checkInput ;Joysticks prüfen & Eingaben verarbeiten jsr drawTimer ;ggf. den Zeitbalken aktualisieren jmp puzzleMainLoop ;Endlosschleife
Mäuschen mach mal Piep 😉
Eigentlich sind wir nun am fertig mit unserem ersten Spiel, aber ich möchte doch noch eine Kleinigkeit hinzufügen. So ganz ohne akustische Rückmeldung will ich das Puzzle jetzt nicht abschließen. Also lasst uns einen kleinen Ton wiedergeben, wenn ein Puzzleteil bewegt wird. Das ist jetzt wirklich nichts umwerfendes, daher verzichte ich hier auch auf einen eigenen Beitrag zum Thema Sound (der kommt später).
Wie so häufig benutze ich wieder einige Konstanten, fügt diese daher in den Source ein.
;******************************************************************************* ;*** Die SID Register - ANFANG *** ;******************************************************************************* SID_BASE = $d400 ;(Nr) Basisadresse des SID_BASE SID_VOICE1_FREQ_MSB = $d401 ;(01) 1. Stimme: High-Byte der Frequenz SID_VOICE1_WAVEFORM = $d404 ;(04) 1. Stimme: Wellenform SID_SUSTAIN_RELEASE = $d406 ;(06) BIT 7-4: Anschlag (sustain) ; 3-0: Abschwellen (release) SID_VOLUME = $d418 ;(24) BIT 7: 3. Stimme Filtermodus AUS ; 6: -''- Hochpass ; 5: -''- Bandpass ; 4: -''- Tiefpass ; 3-0: Lautstärke SID_WAVE_TRIANGLE = $11 ;Wellenform: Dreieck SID_WAVE_SAWTOOTH = $21 ;Wellenform: Sägezahn SID_WAVE_PULSE = $41 ;Wellenform: Rechteck SID_WAVE_WHITENOISE = $81 ;Wellenform: Rauschen ;******************************************************************************* ;*** Die SID Register - ENDE *** ;*******************************************************************************
Dies sind erstmal nur die gleich von uns benötigten Konstanten, später lernen wir alle SID-Adressen kennen.
Jetzt brauchen wir noch eine kleine Routine, die unseren Sound wiedergibt. Fügt hinter checkInput die nächsten Zeilen ein:
;******************************************************************************* ;*** Ton fürs Bewegen eines Puzzleteils ;******************************************************************************* ;*** Übergabe: X = Lautstärke ;******************************************************************************* ;*** Rückgabe: - ;******************************************************************************* ;*** ändert : A, SR ;******************************************************************************* !zone playTileSound playTileSound lda #$00 ;Anschlag & Abschwellen auf 0 sta SID_SUSTAIN_RELEASE lda #SID_WAVE_WHITENOISE ;Rausen sta SID_VOICE1_WAVEFORM ;für die 1. Stimme lda #$05 ;Niedrige Frequenz sta SID_VOICE1_FREQ_MSB ;für die erste Stimme stx SID_VOLUME ;Lautstärke laut X-Register rts ;zurück
Da unser Sound nur sehr kurz wiedergegeben wird, haben einige Register keine bzw. kaum eine Auswirkung. Wir initialisieren diese aber trotzdem.
Zu Beginn setzen wir Anschlag und Abklingen auf 0, dann die Wellenform für die erste Stimme auf Rauschen und schließlich die Frequenz. Wir beschränken uns bei der Frequenz auf das MSB und wählen eine relativ niedrige aus. Ganz am Schluß setzen wir die Lautstärke auf den Wert, den wir im X-Register an die Routine übergeben haben und schon sind wir fertig.
Natürlich muss unser Sound auch noch aufgerufen werden. Sucht in der Funktion performInput nach dem Label .refresh und fügt dort die markierten Zeilen ein:
.refresh ldx #$0f ;max. Lautstärke jsr playTileSound ;Ton wiedergeben jsr spriteSetPositions ;Sprites positionieren jsr drawTiles ;Hintergründe zeichnen lda #$00 ;Lautstärke auf sta SID_VOLUME ;0 setzen! rts ;zurück
Wir starten vor dem erneuten Zeichnen unseren Ton mit der maximalen Lautstärke. Dann wird alles neu gemalt, danach setzen wir die Lautstärke einfach auf 0. Damit wird unser Ton also während des Bewegen der Puzzleteile wiedergegeben. Da das Mischen auch diese Funktion verwendet, erhalten wir auch zum Start einen Sound.
Wir gehen hier natürlich mit der Brechstange vor. Einfach die komplette Lautstärke auf 0 zusetzen ist natürlich nicht die feine Art. Da wir nur diesen einen Ton verwenden, hat das zur Zeit aber keine negativen Auswirkungen.
Damit wären wir am Ende des Puzzlespiels.
Das Programm hätte man (evtl. sogar einfacher und schneller) auch unter BASIC entwickeln können. Hier ging es aber in erster Linie darum ein erstes, kleines, lauffähiges Programm in Assembler zu erstellen.
Natürlich läßt sich hier noch vieles verbessern z. B. wären richtige Sounds und eine Hintergrundmusik sehr schön. Auch hübschere Grafiken würden das Programm aufwerten. Man kann auch überlegen, statt mit Sprites das Puzzle nur mit dem Zeichensatz zu erzeugen. Unsere bereits erwähnte Speicherplatzvergeudung (seht euch nur die vier Bildschirme an) sollten wir durchaus noch mal überdenken.
Aber das soll uns zunächst nicht interessieren, die Planung ist ja so angelegt, dass wir nach und nach die benötigten Fertigkeiten erlernen.
Nun geht es, wie geplant, mit Level 2 – Die Landung weiter…
Schritt 10 - Puzzle 010
Du schreibst “kleines Programm”, doch ich bin stolz darauf, alles korrekt übernommen zu haben. Nur einmal musste ich im Quellcode nachschauen, weil er nach dem Laden der Daten einfach so abbrach. Schlussendlich funktionierte es nach dem Umstellen der Unterprogramme und dem Verschieben der Daten ganz nach hinten.
Ich denke, dies wäre ein guter Hinweis am Anfang, dass alle Daten hinter die Unterprogramme “gehören”. Ich hatte diese halt zwischen drin verteilt, je nach weiter hinzugefügtem Abschnitt.
Ich gratuliere Dir, dass Du mich dazu gebracht hast, bis hierher dran zu bleiben, auch wenn ich manche Codeausschnitte erst durch Deine Erklärungen nachvollziehen konnte und manche Schritte immer noch “geheimnissvoll” bleiben wie etwa das Überschreiben der Zeitleiste mittels Timer.