Die ST-Assemblerecke befaßt sich mit der Joystick-Abfrage
In dieser Folge wollen wir uns einmal näher mit verschiedenen Problemen bei der Joystick-Abfrage in Assembler beschäftigen. Dabei wollen wir uns allerdings nicht auf die reine Abfrage der Joystick-Position beschränken, sondern vielmehr auf die darauffolgenden Aktionen eingehen. Es dreht sich hier also eher um die Steuerung einer Spielfigur oder eines Cursors.
Zunächst sei aber kurz erläutert, wie man beim ST an die Daten der Joystickports kommt. Wie sicher viele wissen, besitzt der ST einen internen Tastaturprozessor, der für alle Eingaben über Tastatur, Joystick und Maus zuständig ist. Dieser kann über den Chip MFP68901 einen Interrupt auslösen, um dem M68000 mitzuteilen, daß er neue Signale von der Tastatur usw. erhalten hat. In diesem Interrupt kann sich der M68000 dann die Daten über die Ports A und B vom Tastaturprozessor holen. Dieser Vorgang sowie die Interrupts werden glücklicherweise schon vom TOS erledigt, so daß wir uns damit nicht mehr abmühen müssen. Wir brauchen nur dem Tastaturprozessor mitzuteilen, daß wir die Joystick-Daten von ihm bekommen möchten. Dies geschieht, indem wir ihm mittels der XBIOS-Funktion 25 den Befehl $14 geben (s. Listing).
Außerdem müssen wir den vom TOS ungenutzten Vektor für die Joystick-Routine auf unsere eigene Abfrage umbiegen. Dazu rufen wir zunächst die XBIOS-Funktion 34 auf, die einen Zeiger auf ein Vektorfeld liefert. Addiert man zu der Basisadresse dieses Feldes 24, so erhält man die Position des Joystick-Vektors, den man nun umbelegen kann. Unserer Routine wird dann bei jeder Joystick-Bewegung ein Zeiger in Register A0 übergeben, der auf ein Paket aus zwei Bytes weist. Das erste Byte ist ein Header, der den Wert $FE oder $FF besitzt, je nachdem, welcher Joystick bewegt wurde. Das zweite Byte enthält die eigentlichen Joystick-Daten. Bit 7 repräsentiert hierbei den Button; die Bits 0 bis 3 stehen für die vier Richtungen. Dieses Byte wird also von unserer Routine in der Variablen JOYSTICK abgelegt, bevor wir sie mit RTS verlassen. Jetzt können wir die Joystick-Bewegungen erfragen. Dies soll uns hier aber nur als grundsätzliche Fähigkeit dienen.
Wer schon des öfteren mit Spielen oder Benutzeroberflächen zu tun hatte, die per Joystick bedient werden, kennt wohl den Ärger mit einer schlechten Steuerung. Im günstigsten Fall empfindet man sie nur als nervend. Oft wird dadurch aber ein ansonsten optimales Programm völlig uninteressant. Was nützen schließlich die schönsten Grafiken und der größte Spielwitz in einem Game, wenn es aufgrund der unpräzisen Steuerung unspielbar ist? Deshalb sollen hier einige Grundkonzepte gezeigt werden, mit deren Hilfe Sie Ihren eigenen Programmen eine benutzerfreundliche Steuerung verleihen können.
Beschäftigen wir uns zunächst mit den sogenannten Labyrinthspielen, zu denen viele Klassiker wie „PacMan“ oder „Boulder Dash“ gehören. Bei diesem Genre werden grundsätzlich nur die vier Grundrichtungen des Joysticks für Bewegungen benutzt. Deshalb bietet es sich als einfachste Lösung an, die Spielfigur analog zu den Joystick-Stellungen auf dem Bildschirm zu bewegen. Solange also der Joystick z.B. nach oben gedrückt ist, läuft auch die Figur nach oben. Läßt man ihn wieder los, bleibt sie sofort stehen. Diese Art der Steuerung bringt aber mehrere Probleme mit sich. So lassen sich z.B. Kurven in einem Labyrinth schlecht umrunden, da die Figur ja immer ziemlich genau auf die Höhe der Abzweigung gebracht werden muß. Das führt dazu, daß man sich oft verhakt, was wiederum den Spielfluß stark hemmt.
Deshalb bietet es sich hier an, die Spielfigur nicht pixelweise zu steuern, sondern sie stets ganze Strecken zurücklegen zu lassen. Günstigerweise sollte deren Länge mit der der Kanten der Blöcke übereinstimmen, aus denen das Labyrinth zusammengesetzt ist. Dadurch käme die Figur ja immer auf die richtigen Abzweigkoordinaten. Bei einem Labyrinth aus lauter 16 * 16-Pixel-Blöcken würde dies bedeuten, daß nach Drücken des Joysticks in eine Richtung die Spielfigur sich gleich um 16 Pixel in diese Richtung bewegt. Dies soll natürlich nicht in einem Schritt geschehen, weil das eine höchst ruckelige Sache wäre. Stattdessen wird bei den nächsten 16 Abfragen des Joysticks einfach so getan, als ob dieser immer noch in die gleiche Richtung gedrückt sei. In Wirklichkeit kann man ihn nach dem Anstoßen der Bewegung natürlich wieder loslassen, ohne daß dies den in Gang gesetzten Prozeß beeinträchtigt. Ein Nachteil dieser Methode ist, daß sich nun die Bewegungsrichtung nicht mehr blitzschnell ändern läßt. Dies ist erst möglich, nachdem eine Bewegung vollständig ausgeführt wurde.
Der größte Vorteil des beschriebenen Vorgehens liegt darin, daß der Spieler ohne Probleme und ohne jeglichen Zeitverlust die Ecken umrunden kann, da sich einerseits die Figur immer nur auf den passenden Koordinaten bewegt und man andererseits den Joystick schon in eine neue Richtung drücken kann, bevor die Figur die Abzweigung erreicht hat.
Wie simuliert man nun aber die Joystick-Bewegungen, damit dieses Verfahren funktioniert? Zunächst einmal benutzen wir als Grundtakt den VBL-Interrupt, der 50- bis 70mal pro Sekunde beim Bildschirmaufbau ausgelöst wird. Dort fragen wir zunächst ein Flag ab, in dem wir eintragen, ob wir uns im Simulationsmodus befinden oder nicht. Ist dies nicht der Fall, führen wir eine gewöhnliche Joystick-Abfrage durch und schreiben dabei gegebenenfalls die neue Richtung in eine Variable. Außerdem vermerken wir in unserem Flag, daß der Simulationsmodus aktiviert wurde. Dabei tragen wir einfach die Anzahl der durchzuführenden Schritte in das Flag ein.
Ist das Flag bei einem späteren Test gesetzt, so zählen wir es zunächst um 1 herunter und führen statt der Joystick-Abfrage einen Test der in der Variablen gespeicherten Richtung durch. Im Beispiel-Listing wird auf diese Weise ein Punkt über den Bildschirm gesteuert. Dabei läßt sich das Programm ohne Probleme so abwandeln, daß die Strecken kürzer oder länger werden. Dazu müssen Sie nur den Wert, der in FLAG geschrieben wird, von 15 in beispielsweise 31 ändern. Diese Steuerungsmethode bietet übrigens noch einen weiteren Vorteil. Da eine Bewegung über eine ganze Strecke geht, kann auch eine Animation damit gekoppelt werden. Wenn Sie z.B. eine Figur haben, die in acht Phasen animiert ist, wird einfach bei jedem zweiten Schritt eine Phase weitergeschaltet. Dies geschieht zwar bei einer pixelweisen Steuerung auch, doch kann es dort passieren, daß eine Animation schlagartig unterbrochen wird, sobald der Spieler die Richtung ändert. Das Resultat ist dann eine ziemlich unrealistische Bewegung. Bei unserer Methode wird jedoch stets eine komplette Animation zu Ende geführt.
Aus diesem Grund verwendet man das beschriebene Verfahren auch häufig bei Jump-and-Run-Games. Dort wird eine Figur ja auch durch ein Labyrinth bewegt. Allerdings kann der Spieler hier noch einige andere Bewegungen außer solchen in die Grundrichtungen ausführen. So sind meistens noch Sprünge möglich; die Figur kann Gegenstände aufsammeln, fallen lassen und werfen. Gerade bei Sprüngen und Würfen werden jedoch sehr viele Fehler gemacht. So weist z.B. in vielen Programmen ein Sprung die Form eines Dreiecks auf. Das kommt dadurch zustande, daß die Figur mit einer konstanten Geschwindigkeit bis zum höchsten Punkt des Sprungs geführt wird, um danach mit demselben Tempo wieder auf den Boden zu fallen. So etwas läßt sich zwar sehr einfach realisieren, doch wirkt die Bewegung auf den Betrachter sehr unnatürlich.
Ein guter Sprung oder Wurf besitzt dagegen die Form eines auseinandergezogenen Halbkreises. Das bedeutet, daß zwar die Geschwindigkeit in der Waagerechten über den ganzen Flug konstant bleibt, das Tempo in der Vertikalen jedoch bis zum höchsten Punkt abnimmt, um sich danach beim Herunterfallen wieder zu steigern. Auch in Wirklichkeit wird ein Gegenstand ja um so schneller, je länger er fällt. Außerdem ist der Geschwindigkeitszuwachs um so größer, je länger der Fall dauert. Dies berechnet sich nach der physikalischen Formal V=A*T *T. Dabei ist V das Tempo, A die Erdbeschleunigung, die im Normalfall ca. 10 beträgt, und T die Zeit, die bereits vergangen ist, seit die Figur ihren höchsten Punkt erreicht hat.
Bei der Realisierung in einem Programm können Sie die Erdbeschleunigung meist weglassen. Wichtig ist nur, daß die Geschwindigkeit, mit der die Figur zu Boden sinkt, quadratisch ansteigt. Das heißt, daß sie z.B. nach einer Sekunde mit Tempo 1 fällt, nach zwei mit Tempo 4, nach drei mit Tempo 9 usw. Für den ersten Teil der Flugkurve, den Sprung nach oben, gilt natürlich, daß die gesamte Bewegung rückwärts abläuft, das Tempo also immer mehr abnimmt. Wird jeder Flug eines Objekts so programmiert, ergeben sich sehr realistische und dynamisch wirkende Bewegungen.
Auch bei Baller- und Actionspielen sollten Sie nach Möglichkeit mit dynamischen, beschleunigten Bewegungen arbeiten. Man kann zwar ein Raumschiff auch einfach nur analog zu den Joystick-Stellungen über den Bildschirm fliegen lassen, in Wirklichkeit muß es jedoch beschleunigt oder abgebremst werden. Im Spiel „Thrust“ ist dies beispielsweise sehr gut umgesetzt. Dort wird das Schiff nicht einfach nach rechts, links usw. gesteuert, sondern die Antriebsraketen werden zum Beschleunigen, Bremsen und Gegenlenken genutzt.
Nun aber noch ein paar Worte zu Benutzeroberflächen, die per Joystick bedient werden. Bei einer Reihe von Adventures muß man einen Cursor auf verschiedene Icons oder Menüs bringen, um diese anzuwählen. Dabei taucht oft das Problem auf, daß die Steuerung zu ungenau wird oder die Geschwindigkeit des Cursors zu niedrig ist. Um dies zu umgehen, sollte man hier mit einem sich selbst beschleunigenden Cursor arbeiten. Das bedeutet, daß der Cursor schneller wird, wenn man den Joystick eine Weile in dieselbe Richtung drückt. Dadurch lassen sich auch große Strecken rasch zurücklegen. Soll dann ein kleines Icon angewählt werden, läßt man den Stick kurz los, um danach wieder mit einem langsamen Cursor pixelgenau zu hantieren.
; ASS19.S
; Joystick-Steuerung
;
; (c) 1989 ATARI-Magazin
start:
clr.l -(sp) ; Supervisormodus einschalten
move.w #32, -(sp)
trap #1
addq.l #6, sp
move.w #34, -(sp) ; Joystickirq
trap #14 ; Vektor auf
addq.l #2, sp ; eigene
add.l #24, d0 ; Routine
move.l d0, a1 ; verbiegen
move.l #Joyirq,(a1)
move.l #befehl, -(sp) ; Tastaturproz,
move.w #1, -(sp) ; den Befehl
move.w #25, -(SP) ; geben, die
trap #14 ; Joystickbem.
addq.l #8, sp ; zu melden.
move.l $456, a0 ; Ubl-Irq
test: ; in die
cmp.l #0, (a0)+ ; TOS-Liste
bne test ; einfuegen.
move.l #vblirq, -(a0)
move.w #2, -(sp) ; Startadresse
trap #14 ; der Bitmap
addq.l #2, sp ; ermitteln.
move.l d0, screen
ende:
bra ende
joyirq: ; Joystick 0 in
move.b 1(a0), joystick ; Variable
rts ; schreiben,
vblirq:
cmp.w #0, flag ; Noch Bewegung
beq joytest ; im Gange?
subq.w #1, flag ; Ja, dann -1
cmp.b #1, joyvariable ; und Richtung
beq rauf ; testen.
cmp.b #2, joyvariable
beq runter
cmp.b #8, joyvariable
beq rechts
cmp.b #4, joyvariable
beq links
joytest: ; Joystick
cmp.b #1, joystick ; Abfrage,
beq initrauf
cmp.b #2, joystick
beq initrunter
cmp.b #8, joystick
beq initrechts
cmp.b #4, joystick
beq initlinks
bra weiter
initrauf: ; Bewegungen
move.b #1, joyvariable ; initialisieren
move.w #15, flag ; und Flag auf
bra rauf ; 15 setzen.
initrunter:
move.b #2, joyvariable
move.w #15, flag
bra runter
initrechts:
move.b #8, joyvariable
move.w #15, flag
bra rechts
initlinks:
move.b #4, joyvariable
move.w #15, flag
bra links
rauf: ; Bewegungen
subq.w #1, y ; pixelweise
bra weiter ; ausführen.
runter:
addq.w #1, y
bra weiter
rechts:
addq.w #1, x
bra weiter
links:
subq.w #1, x
bra weiter
weiter:
move.l screen, a3 ; Startadresse
move.w x, d5 ; des Pixels
move.w y, d6 ; berechnen,
move.w d5, d7
and.w #15, d7
lsr.w #4, d5
lsl.w #3, d5
add.w d5, a3
mulu #160, d6
add.w d6, a3
move.w #$8000, d6 ; Pixel mit
ror.w d7, d6 ; XOR in die
eor.w d6, (a3) ; Bitmap
rts ; setzen.
x:
dc.w 96
y:
dc.w 96
joystick:
dc.b 0
joyvariable:
dc.b 0
flag:
dc.w 0
befehl:
dc.b $14, $14
screen:
dc.l $f8000