- C-Kurs
- C-Kurs: Exkurse
Heute lassen wir mal ein 'O' springen. Endlich kommt mal etwas Bewegung in's Spiel. Noch nicht in einem Grafikmodus, aber wir können ja nicht alles auf einmal machen. So sieht's aus:
Und hier ist das dazugehörige Programm, was ihr voraussichtlich wieder am Ende dieses Abends vollständig verstehen werdet:
#include <conio.h> #include <c64.h> #define PITCH_TOP_Y 4 #define PITCH_LEFT_X 0 #define PITCH_RIGHT_X 19 #define GRAVITY 2 int ballX; int ballY; signed char speedX; signed char speedY; /******************************************************************************/ /* * Bereitet den Bildschirm fuer die Ballerei vor. * Er wird geloescht und in die Linke untere Ecke wird ein tolles Dreieck * gezeichnet. */ void prepareScreen(void) { unsigned char x, y; bordercolor(COLOR_YELLOW); bgcolor(COLOR_PURPLE); clrscr(); textcolor(COLOR_YELLOW); // Zeichne das Dreieck unten links for (x = PITCH_LEFT_X; x <= PITCH_RIGHT_X; ++x) { y = PITCH_TOP_Y + x + 1; revers(0); cputcxy(x, y++, 0xbf); revers(1); while (y < 25) { cputcxy(x, y++, ' '); } } revers(0); textcolor(COLOR_BLACK); } /******************************************************************************/ /* * Initialisiere die Position des Balls und dessen Geschwindigkeit. */ void initBall(void) { ballX = 3 * 256; ballY = 0 * 256; speedX = 0; speedY = 0; } /******************************************************************************/ /* * Warte einen Frame. Es wird auf Rasterzeile 255 gewartet, unter der Annahme, * dass wir nicht mehr in Zeile 255 sind. */ void waitNextFrame(void) { while (VIC.rasterline != 255) ; } /******************************************************************************/ /* * Hier geht's los. */ int main(void) { int limit; unsigned char speedTmp; prepareScreen(); initBall(); do { waitNextFrame(); // alte Ballposition loeschen cputcxy(ballX / 256, ballY / 256, ' '); // Gravitation, aber v auf Maximalwert begrenzen if (speedY < 100) speedY += GRAVITY; // Ball bewegen ballX += speedX; ballY += speedY; if (ballX < 0) { // dann abprallen lassen speedX = -(speedX - speedX / 8); ballX = 0; } // sonst: Ist der Ball im Bereich der schraegen Flaeche? else if (ballX / 256 <= PITCH_RIGHT_X) { // ist er auf oder unterhalb der schraegen Flaeche? limit = ballX + PITCH_TOP_Y * 256; if (ballY >= limit) { // dann abprallen lassen speedTmp = speedX; speedX = speedY - speedY / 8; speedY = speedTmp - speedTmp / 8; ballY = limit; } } // sonst: Ist der Ball am rechten Rand? else if (ballX / 256 > 39) { // Dann gib ihm einen Tritt nach links speedX = -100; ballX = 39 * 256; } // ist der Ball am oberen Rand? if (ballY < 0) { speedY = -(speedY - speedY / 8); ballY = 0; } // ist der Ball am unteren Rand? else if (ballY / 256 > 24) { // dann abprallen lassen speedY = -(speedY - speedY / 8); ballY = 24 * 256; } // neue Ballposition zeichnen cputcxy(ballX / 256, ballY / 256, 'O'); } while (!kbhit()); clrscr(); }
Heute sehen wir uns alles an, was wir in dem Programm noch nicht kennen.
Ihr kennt bereits die Kontrollstruktur for, in C gibt es auch noch andere. Zwei sehr wichtige sehen wir uns jetzt an.
Im folgenden BASIC-Programm ist eine fußgesteuerte Schleife implementiert. Fußgesteuert bedeutet, dass die Schleifenbedingung am Ende des Schleifenkörpers überprüft wird.
10 I=0 20 REM START DER SCHLEIFE 30 : PRINT I 40 : I=I+1 50 IF I<5 GOTO 30
Das Beispiel könnte zugegebenermaßen mit einer FOR-Schleife umgesetzt werden, mir ging es aber um ein einfaches Beispiel.
In C benutzt man statt GOTO einen eigenen Schleifentyp mit der folgenden Form:
do
Anweisung
while (Bedingung);
Da die Schleife fußgesteuert ist, wird der Schleifenkörper immer mindestens einmal durchlaufen, bevor die Bedingung überprüft wird. Wir können das oben abgebildete Programm in C so umsetzen:
#include <stdio.h> int main(void) { int i = 0; do { printf("%d\n", i); i += 1; } while (i < 5); }
Es gibt noch eine andere Form der while-Schleife, die kopfgesteuert ist. Bei ihr wird die Bedingung vor dem Ausführen des Schleifenkörpers geprüft. In BASIC könnten wir das so umsetzen:
10 I=0 20 IF I<5 GOTO 60 30 : PRINT I 40 : I=I+1 50 GOTO 20 60 REM SCHLEIFENENDE
In C benutzt man für diesen Schleifentyp auch das Schlüsselwort while. Nur diesmal steht's oben:
while (Bedingung)
Anweisung
Wenn wir das zweite BASIC-Programm mit dem kopfgesteuerten while umsetzen, sieht das so aus:
#include <stdio.h> int main(void) { int i = 0; while (i < 5) { printf("%d\n", i); i += 1; } }
Erinnert ihr Euch noch an das falsche zusätzliche Semikolon hinter for? Der gleiche Fehler kann mit dem kopfgesteuerten while auch passieren. Also nicht vergessen: Ein Semikolon hinter dem kopfgesteuertem while ist eine leere Anweisung und macht vermutlich nicht, was ihr wollt.
Prinzipiell lässt sich jede Schleifenform (for, do while, while) irgendwie durch die jeweils anderen ausdrücken. Es gibt aber immer eine, die passender oder lesbarer ist als die anderen.
Zum Schluss nochmal ein Beispiel, um ganz klar den Unterschied zwischen dem kopfgesteuerten und dem fußgesteuerten while zu zeigen:
#include <stdio.h> int main(void) { int i = 777; while (i < 5) { puts("In Schleife 1"); } do { puts("In Schleife 2"); } while (i < 5); }
So langsam wollen wir und in die Richtung bewegen, auf die sicher die meisten Leser warten: Systemnähe. In der Header-Datei conio.h sind einige Funktionen zu finden, die uns u.a. Textausgaben mit Farben an beliebigen Stellen des Bildschirms ermöglichen. Die tatsächliche Implementierungen der Funktionen sind in den Bibliotheken zu finden, die mit dem cc65 kommen.
Wenn ihr Funktionen aus der conio.h benutzt, solltet ihr Euch bewusst sein, dass das Programm nicht mehr auf jedem System übersetzbar ist. Das liegt daran, dass diese Funktionen nicht standardisiert sind und deshalb nicht in jeder C-Bibliothek zu finden sind.
Die Dokumentation zu den verschiedenen Funktionen findet ihr auf http://www.cc65.org/doc/funcref-14.html. Hier ist eine Übersicht über die Funktionen:
| Funktion | Aufgabe | Beispiel |
|---|---|---|
| bgcolor | Setzt die Hintergrundfarbe | bgcolor(0); |
| bordercolor | Setzt die Rahmenfarbe | bordercolor(1); |
| cclear | Löscht einen Teil einer Zeile | cclear(7); |
| cclearxy | Löscht einen Teil einer Zeile an einer bestimmten Position | cclearxy(5, 5, 7); |
| cgetc | Holt ein Zeichen von der Tastatur | key = cgetc(); |
| chline | Zeichnet eine horizontale Linie aus Textzeichen | chline(20); |
| chlinexy | Zeichnet eine horizontale Linie an einer bestimmten Position | chlinexy(0, 0, 20); |
| clrsrc | Löscht den Bildschirminhalt | clrscr(); |
| cprintf | Wie printf nur etwas kleiner und schneller | cprintf(“%d“, 7); |
| cputc | Gibt ein Zeichen aus | cputc('a'); |
| cputcxy | Gibt ein Zeichen an einer bestimmten Position aus | cputcxy(4, 8, 'a'); |
| cputs | Gibt einen String aus | cputs(„Hello world!“); |
| cputsxy | Gibt einen String an einer bestimmten Position aus | cputsxy(10, 10, „Hello world“); |
| cursor | Erlaubt/verbietet blinkenden Cursor während cgetc | cursor(1); |
| chline | Zeichnet eine vertikale Linie aus Textzeichen | chline(10); |
| chlinexy | Zeichnet eine vertikale Linie an einer bestimmten Position | chlinexy(0, 1, 10); |
| gotox | Bewegt den Cursor an eine bestimmte X-Position | gotox(10); |
| gotoxy | Bewegt den Cursor an eine bestimmte X/Y-Position | gotoxy(10, 10); |
| gotoy | Bewegt den Cursor an eine bestimmte Y-Position | gotoy(10); |
| kbhit | Prüft, ob ein Tastendruck im Tastaturpuffer wartet | isKeyPressed = kbhit(); |
| revers | Schaltet inverse Schrift an/aus. | revers(1); |
| screensize | Holt die Größe des Textbildschirms. | screensize(&width, &height); |
| textcolor | Setzt die Schriftfarbe | textcolor(1); |
| vcprintf | Wie vprintf, im Moment zu kompliziert... | |
| wherex | Gibt die X-Koordinate des Cursors zurück. | x = wherex(); |
| wherey | Gibt die Y-Koordinate des Cursors zurück. | x = wherey(); |
Ihr solltet auf alle Fälle die Dokumentation zu den Funktionen lesen, die Ihr benutzen möchtet!
Bei screensize(&width, &height); fällt Euch sicher der noch nicht besprochene Operator “&“ auf. Den werden wir bald kennenlernen. Soviel sei jetzt schon gesagt: Der sorgt in diesem Beispiel dafür, dass die Funktion screensize unsere beiden Variablen width und height beschreiben kann. Bei einer normalen Wertübergabe von Argumenten (Call by value) bekommt die aufgerufene Funktion für jeden Parameter eine eigene lokale Variable. Sie kann also die Variablen der aufrufenden Funktion nicht direkt verändern. Die Der Typ der Variablen width und height muss in unserem Beispiel unsigned char sein.
Als Vorteil der Funktionen aus conio.h sei neben der größeren Flexibilität auch erwähnt, dass sie meistens schneller und kleiner als ihre Gegenstücke aus stdio.h sind. Dafür nimmt man aber in Kauf, dass sich Bildschirmausgaben z.B. nicht wie sonst auf einen Drucker umleiten lassen. Aber das müssen sie ja auch nicht immer.
Ein weiteres denkbares Anwendungsgebiet dieser Funktionen ist, neben dem Hüpfen von Buchstaben, das Umsetzen von Benutzeroberflächen für Anwendungsprogramme.
Die Operatoren ++ und -- heißen Inkrement- und Dekrement-Operatoren. Die Assembler-Programmierer unter Euch werden anhand dieser Namen schon vermuten, dass folgende Anweisungen äquivalent sind:
| Inkrementieren | ++i; | i += 1; | i = i + 1; |
| Dekrementieren | --i; | i -= 1; | i = i - 1; |
Für beide Operatoren gibt es eine Präfix- und eine Postfix-Notation. Für sich allein gestellt machen sie prinzipiell das gleiche:
| Präfix-Operator | Postfix-Operator |
|---|---|
++i; | i++; |
--i; | i--; |
Es gibt jedoch einen wichtigen Unterschied, den wir uns nun ansehen. Darauf solltet ihr besonders achten, wenn ihr den Wert des Ausdrucks weiterverwertet. Beim Präfix-Operator wird erst inkrementiert/dekrementiert und dann der Wert des Ausdrucks gebildet. Beim Postfix-Operator ist es andersrum: Der alte Wert der Variable ist der Wert des Ausdrucks und dann wird inkrementiert/dekrementiert.
Schauen wir uns den Unterschied beim Präfix-Operator einmal anhand von Ersatz-Code an:
k = ++i;
Ist das gleiche wie:
i = i + 1; k = i;
Auch für den Postfix-Operator können wir Ersatz-Code schreiben:
k = i++;
Ist das gleiche wie:
k = i; i = i + 1;
Im C64-BASIC wird grundsätzlich mit Fließkomma-Zahlen gerechnet, selbst bei Ganzzahlvariablen wie A%. In C gibt es auch Fließkommazahlen, die werden aber vom cc65 noch nicht unterstützt.
Ein wesentlicher Nachteil von Fließkommazahlen auf einfachen Rechnern wie dem C64, ist der wesentlich größere Prozessorhunger. Zum Beispiel für Spiele sind sie deswegen oft ungeeignet.
Eine einfache Alternative sind Festkommazahlen. Das sind Zahlen, bei denen die Position des Kommas feststeht. Man kann sie gut mit unseren Dezimalzahlen mit einer festen Zahl an Nachkommastellen vergleichen:
00,00 00,01 00,02 ... 99,98 99,99
Wir können uns das Komma einfach wegdenken. Oder einfach alle Zahlen mit 100 multiplizieren, dann erhalten wir:
0000 0001 0002 ... 9998 9999
Und wenn wir die ursprünglichen Werte wiederhaben wollen, dividieren wir sie wieder durch 100. Das klingt zunächst etwas umständlich. Aber da ein einfacher Prozessor naturgemäß mit Ganzzahlen am besten umgehen kann, ist diese Darstellung für einfache Rechenschritte sehr vorteilhaft.
In unserem Programm haben wir Festkommazahlen verwendet, um z.B. Geschwindigkeiten wie „ein halbes Kästchen je Frame“ ausdrücken zu können.
Ein Prozessor rechnet aber nunmal binär. Deswegen wäre es ziemlich dämlich, den Faktor 100 für die Skalierung zu benutzen. Statt dessen benutzt man Zweierpotenzen, die durch Hin- und Herschieben von Bits umgesetzt werden können. Wir haben uns den Faktor 256 ausgesucht.
In der folgenden Abbildung seht ihr ein paar Beispiele aus dem binären Zahlensystem mit dem Faktor 256 (8 Bit):
Wer mit Binärzahlen rechnen kann, wird feststellen, dass das auch gut mit negativen Zahlen funktioniert. Hier setzen wir wieder die Darstellung im Zweierkomplement voraus. Und wer von Euch nicht mit Binärzahlen rechnen kann, nimm das jetzt einfach mal so hin
Schauen wir uns das Programm einmal genauer an.
Etwas weiter unten werden wir Präprozessor-Konstanten für die Farben verwenden, z.B. COLOR_YELLOW. Die Werte für die Farben sind vom Computer bzw. vom dort eingebauten Video-Chip abhängig. Es gibt nur eine conio.h für alle vom cc65 unterstützen Plattformen. Diese haben aber unterschiedliche Farben. Deshalb findet man die Konstanten für die Farben in einer Hardware-abhängigen Datei:
#include <c64.h>
Als nächstes legen wir selbst ein paar Präprozessor-Makros an:
#define PITCH_TOP_Y 4 #define PITCH_LEFT_X 0 #define PITCH_RIGHT_X 19 #define GRAVITY 2
Die ersten drei werden für das Layout benötigt und enthalten Bildschirmkoordinaten. Der Koordinatenursprung ist übrigens oben links, X steigt nach rechts, Y steigt nach unten.
Der letzte Wert ist die Fallbeschleunigung (also „g“ aus dem Physikunterricht). Als Einheit haben wir hier aber nicht m/s², sondern was C64-artiges: Die 2 ist eine Festkommazahl, die mit 256 skaliert wurde. Der „echte“ Wert ist also 2 / 256 = 0,0078125. Statt Meter haben wir die Einheit, die wir der conio übergeben, also „Cursor-Kästchen“. Unser Zeitraster ist nicht 1 Sekunde, sondern, wie wir später noch sehen werden, 1/50 Sekunde.
Wenn man es ganz genau wissen möchte, bedeutet die 2 also (2/256) Kästchen/(1/50s)².
Den Zustand des Balls speichern wir in diesen vier Variablen:
int ballX; int ballY; signed char speedX; signed char speedY;
Die ersten zwei enthalten die Position (für Physiker: den zweidimensionalen Ortsvektor). In ballX steht die X-Komponente. Auch das ist eine Festkommazahl. Der Wert liegt in unserem Programm in diesem Bereich: 0 <= X < 39 * 256. Da wir beim Dividieren immer abrunden, kann der rechte Grenzwert sogar 40 * 256 - 1 sein. Den Datentyp int haben wir gewählt, damit alle Werte ohne Überlauf reinpassen. Wir haben vorzeichenbehaftete Typen benutzt, um Unterläufe leicht feststellen zu können, z.B. wenn der Ball links von 0 positionert werden würde. Die Y-Komponente funktioniert genauso.
Die aktuelle Geschwindigkeit ist auch in einem zweidimensionalen Vektor enthalten; (speedX, speedY), jeweils mit 256 skaliert. Und das Zeitraster ist wieder 1/50 Sekunde. Steht in speedX also 64, ist damit eine Geschwindigkeit von 64/256 = 0,25 Kästchen/(1/50)s gemeint. Für die andere Komponente speedY gelten die gleichen Regeln.
Bei speedX und speedY müssen wir aufpassen: Wir haben hier nur signed char benutzt. Der Wert der beiden Komponenten muss also immer zwischen -128 und 127 liegen. Dieser Wertebereich reicht für die Geschwindigkeit, deshalb brauchen wir für speedX und speedY keinen größeren Typ als signed char. Ein negativer Wert bedeutet eine Bewegung nach links bzw. oben.
void prepareScreen(void)
Diese Funktion bereitet den Bildschirm vor. Sie setzt die richtigen Farben und malt die Rampe in der linken unteren Ecken. Wenn Ihr Euch die Dokumentationen zu den unbekannten Funktionen aus der conio.h anseht und die neuen Sachen aus dieser Lektion kapiert habt, sollte die Funktion leicht zu verstehen sein.
Die Funktionen, die Farbwerte erwarten, haben wir nicht mit den vom C64 bekannten Farbwerten (0 = schwarz, 1 = weiß etc.) aufgerufen, sondern mit schönen Makros aus der c64.h - die aber auch nur die bekannten Werte einsetzen.
Das ist noch eine interessante Funktion:
void waitNextFrame(void) { while (VIC.rasterline != 255) ; }
Wir haben uns noch nicht alles genau angesehen, was man zum Verständnis dieses Konstrukts braucht. Stellt Euch VIC.rasterline einfach als Variable vor, die den Wert des Registers 0xD012 enthält. Wir warten in der Funktion also in einer Schleife, bis der Rasterstrahl in der Zeile 255 ankommt. Diese Zeile liegt knapp unterhalb des normalen Bildschirmbereichs. Diese Warteschleife führt dazu, dass unser Programm das schon erwähnte Zeitraster von 1/50 Sekunde bekommt, da bei PAL pro Sekunde 50 (Halb-)Bilder aufgebaut werden.
Das Warten auf Zeile 255 hat auch noch den Vorteil, dass der Rasterstrahl nicht gerade das „O“ zeichnet, wenn wir es umsetzen. Das würde zu einem hässlichen Flackern führen.
In der Funktion oben wird übrigens das Semikolon als leere Anweisung benutzt. Diesmal mit Absicht
Schauen wir uns noch ein paar interessante Stellen aus dem Hauptprogramm an:
// alte Ballposition loeschen cputcxy(ballX / 256, ballY / 256, ' ');
Das ist eine der Stellen, an denen wir die Festkomma-Skalierung mit 256 wieder zurückrechnen. Durch die Ganzzahldivision wird hier immer abgerundet. Wenn ballX z.B. den Wert 502 enthält, ist der Ball an x-Position 1.
// Gravitation, aber v auf Maximalwert begrenzen if (speedY < 100) speedY += GRAVITY; // Ball bewegen ballX += speedX; ballY += speedY;
Hier wird nach den Regeln der Physik die Bewegung des Balls berechnet. Jedenfalls näherungsweise. Dazu dienen die Gleichungen:
v = v + a * dt
s = s + v * dt
dt ist bei uns 1 (und zwar 1 * 1/50s), übrig bleibt jeweils eine Addition. Oder nochmal mit SI-Einheiten erklärt: Eine Fallbeschleunigung von 9,81 m/s² bedeutet etwa, dass die Geschwindigkeit nach unten nach einer Sekunde 9,81 m/s größer wird, z.B:
19,81 m/s = 10 m/s + 9,81 m/s² * 1s
Mit der Position geht es ähnlich:
40 m = 30 m + 10 m/s * 1s
Danach kommen eine Reihe von Tests, die den Ball an den Kanten abprallen lassen. Wir schauen uns hier stellvertretend die erste Regel an:
<code c> if (ballX < 0) {
// dann abprallen lassen speedX = -(speedX - speedX / 8); ballX = 0;
}
Wenn sich der Ball durch die zuvor berechnete Bewegung links vom linken Rand befindet, muss er abprallen. Da das eine senkrechte Kante ist, brauchen wir nur das Vorzeichen des X-Anteil des Geschwindigkeitsvektors umdrehen. Und um etwas Reibung zu simulieren, vermindern wir die Geschwindigkeit bei jedem Abprallen um 1/8.
Da das hier ein Programmier- und kein Physikkurs ist, soll das als Erklärung für die Bewegungen ausreichen. Wer sich dafür interessiert, kann sich den Rest auch noch ansehen. Und wer nicht, das schaut sich einfach nur den C-Code an.
Eine Ausnahme ist übrigens der rechte Rand. Dort geben wir dem Ball einen kräftigen Schubs, damit er durch die Reibung nicht einschläft. Für die Schräge benutzen wir eine Geradengleichung, wie sie manche von Euch vielleicht noch aus der Schule kennen.
So, das war's für heute. Nächstes mal müssen wir uns noch ein paar Sachen ansehen, die wir heute ignoriert haben.
In diesem Thread im Forum64 können Fragen zu dieser Lektion gestellt werden:
http://www.forum64.de/wbb3/index.php?page=Thread&threadID=28715
In diesem Thread diskutieren Experten, was hier nicht stimmt und wie der Kurs verbessert werden kann:
http://www.forum64.de/wbb3/index.php?page=Thread&threadID=28716
Hier sind ein paar interessante Fragen aus dem oben genannten Forum:
(Noch keine)
So, das war's schon für heute, weiter geht's mit 07-abend7