C Lernen mit cc65 und C64

Abend 6: Der springende Punkt

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.

Die while-Schleifen

Ihr kennt bereits die Kontrollstruktur for, in C gibt es auch noch andere. Zwei sehr wichtige sehen wir uns jetzt an.

Fußgesteuert while

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);
}

Kopfgesteuert while

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);
}

Aufgabe 1: Teste dieses Programm. Kompiliere es danach mit cc65 -O -T und schau Dir das entstandene Assemblerlisting an, um den Unterschied im entstandenen Programm nachzuvollziehen.

Die conio.h

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.

Im Gegensatz zu den Textausgabefunktionen der stdio.h sorgen die Funktionen der conio.h nicht dafür, dass am Ende des Bildschirms gescrollt oder der Text einfach abgeschnitten wird. Statt dessen müsst Ihr selbst aufpassen, dass Ihr nicht über die Grenzen des Bildschirms schreibt. Sonst könnten andere Speicherbereiche überschrieben werden, was zu Abstürzen oder merkwürdigem Programmverhalten führen kann. Die Grenzen werden in den conio-Funktionen aus Geschwindigkeitsgründen nicht überprüft.

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.

Neue Operatoren: "++" und "--"

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;

Beim Postfix-Operator muss sich der Code erst den alten Wert von i aus dem Speicher holen, diesen dann inkrementieren/dekrementieren und in den Speicher zurückschreiben. Dann kann/muss der Code den alten Wert von i weiterverwenden. Das führt aber dazu, dass der alte Wert von i zwischengespeichert werden muss, was zu längerem Code führt. Wenn der Wert nicht direkt verwendet wird, z.B. in for (i = 0; i < 5; ++i), sollte man sich deshalb immer für ++i statt für i++ entscheiden. Optimierende Compiler erzeugen übrigens in beiden Fällen den gleichen Code. Mit dem cc65 ohne Optimierung (also ohne -O) kann man den Unterschied sehr gut beobachten.

Festkommazahlen

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 :-)

Warum hüpft das O?

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.

Aufgabe 2: Schau Dir an, wie der cc65 die Divisionen und Multiplikationen mit 256 und 8 in Assembler umsetzt. Gibt es bei diesen Operationen einen offensichtlichen Unterschied, ob mit oder ohne Optimierung übersetzt wird? wie verändert sich der Code, wenn wir als Skalierungsfaktor statt 256 den Wert 100 benutzen? Beschreibe Deine Beobachtungen im unten verlinkten Thread im Forum64.

Eine Zusatzaufgabe für Knobler

Aufgabe 3: Benutze Funktionen der conio.h, um einen Oberflächenentwurf für ein Anwendungsprogramm umzusetzen. Das Programm braucht keine Funktionalität haben und braucht keine String-Funktionen. Alles an Text kann fest eingebaut sein (hartkodiert, hard coded), weil es ja nur ein Entwurf sein soll. Was das Programm können soll (wenn es existieren würde), findest Du in diesem Forums-Thread: http://www.forum64.de/wbb3/index.php?page=Thread&threadID=28299 Poste ein Screenshot und das Programm im unten verlinkten Thread im Forum64 (nicht in den Thread hier).

So, das war's für heute. Nächstes mal müssen wir uns noch ein paar Sachen ansehen, die wir heute ignoriert haben.

Fragen zu dieser Lektion

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

ckurs/06-abend6.txt · Zuletzt geändert: 11/05/2009 12:59 (Externe Bearbeitung)
www.chimeric.de Creative Commons License Driven by DokuWiki Recent changes RSS feed