Autor Beitrag
battledevil
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 96

WinXP, Win7
C#, C++, VBNET
BeitragVerfasst: Do 17.11.05 20:05 
Hallo Leute!

Ich dachte mir, ich schreib mal ein Tutorial für alle, die gerne mal ein kleines Spiel programmieren wollen. Es wird sich dabei um eine Version des bekannten Zwei-Panzer-Stehen-In-Der-Landschaft-Und-Versuchen-Sich-Gegenseitig-Abzuschiessen-Spiels, den Älteren auch besser bekannt als Artillery Duel vom C64, handeln. Den Jüngeren ist vielleicht Pocket Tanks oder Scorched Earth ein Begriff.
Ganz so klein ist das Spiel leider nicht, wie man schon an der Länge des Tutorials erkennen kann. Aber ich hoffe, daß die Ausführlichkeit nicht allzu viele Fragen offen läßt.

Zuerst mal, was gibts es in diesem Tutorial zu lernen:
- wie man mit Delphi ohne extra Grafikschnittstellen wie DirectX oder OpenGL ein Spiel programmieren kann
- Verwendung von Bitmaps und Buffering
- Generierung einer Landschaft
- Berechnung der Flugbahn (Parabel) unter Einbeziehung von Wind
- Allgemein ein bißchen was zur Spielelogik, soweit es dieses Spiel betrifft

Das werde ich nicht behandeln:
- wie programmiere ich einen Computergegner - das allein ist schon ein eigenes Tutorial wert
- wie bekommt man eine große Landschaft, durch die man scrollen kann - ohne OpenGL oder DirectX nicht so simpel

Ok.
Wen das jetzt immer noch interessiert, der lese weiter...

Teil 1 - Wir bauen uns eine Landschaft
Zuallererst muß ich voranstellen, dass es für diesen Aspekt bereits ein gutes Tutorial gibt: www.delphi-library.d...20einer%20landschaft
Ich werde also nicht alle Möglichkeiten darlegen, sondern einfach dafür sorgen, dass eine zufällig generierte Landschaft angezeigt wird.

Dazu brauchen wir erstmal ein Formular und ein Image. Das Image stelle ich auf eine Größe von 800x600 Pixel ein.
Bevor wir uns aber an die eigentliche Landschaft machen, ein kleiner Grafikkurs.

1.1. - Double Buffering und Bitmaps
Da das Spiel ohne OpenGL und DirectX auskommt, müssen wir andere Mittel einsetzen, um es trotzdem flüssig und grafisch ansprechend gestalten zu können. Wir werden eine altbekannte Technik namens Double Buffering einsetzen, um die Grafikausgabe zu realisieren. Diese Methode hat verschiedene Vorteile, wie wir noch feststellen werden. Wie funktioniert das Double Buffering? Für gewöhnlich zeichnen wir die grafischen Objekte einfach auf das Image und sie werden angezeigt. Wenn jedoch viele Sachen zu zeichnen sind, kann es passieren, dass der Computer nicht so schnell zeichnet, wie die Grafik angezeigt wird. Dadurch kann das angezeigte Bild flackern. Beim Double Buffering wird jedoch nicht direkt auf die sichtbare Oberfläche des Programms gezeichnet, sondern auf eine Art unsichtbare Zeichenfläche, der sogenannten Bitmap. Wenn alles in die Bitmap gezeichnet ist, dann wird der Inhalt in das Image kopiert und angezeigt. Diese Methode verhindert das Flackern, weil die Grafik erst angezeigt wird, wenn sie komplett gezeichnet wurde.
Ein weiterer Vorteil ist die Möglichkeit, beliebig viele Bitmaps einzusetzen und sie auch miteinander zu kombinieren.

Zunächst brauchen wir erstmal zwei Bitmaps: eine, die später die gesamte Spielszene enthalten wird und eine, in der nur die Landschaft dargestellt ist. Bitmaps gibt es zum Glück schon in Delphi und zwar den Typ TBitmap. Deswegen legen wir erstmal zwei davon an.
ausblenden Delphi-Quelltext
1:
var Landschaft, Buffer : TBitmap;					

Buffer wird später die gesamte Spielszene enthalten, die wir dann in das Image kopieren. Landschaft wird nur die Landschaft enthalten.
Das reicht leider noch nicht ganz, die Bitmaps müssen mittels der Funktion Create erst noch erzeugt werden. Das erledigen wir im OnCreate-Ereignis der Form, damit uns die Bitmaps direkt beim Programmstart zur Verfügung stehen. Wir stellen auch gleich die Größe (800x600, so groß wie das Image) ein.

Das ganze sieht wie folgt aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
procedure TForm1.FormCreate(Sender: TObject);
begin
    Landschaft:=TBitmap.Create;
    Landschaft.Width:=800;
    Landschaft.Height:=600;

    Buffer:=TBitmap.Create;
    Buffer.Width:=800:
    Buffer.Height:=600;
end;


Damit haben wir die Grundlage für die grafische Darstellung des Spielgeschehens geschaffen.

1.2. - Die Landschaft
Jetzt kommen wir zum eigentlichen Teil des ersten Kapitels, der Landschaft. Wie bereits oben angesprochen, gibt es dazu schon ein sehr gutes Tutorial. Wer also daran interessiert ist, verschiedenartige Landschaften zu erzeugen, dem sei es ans Herz gelegt.
Wie gehen wir vor? Wir haben die Bitmaps, jetzt müssen wir also die Landschaft mittels eines geeigneten Algorithmus erzeugen und auf die Landschafts-Bitmap zeichnen. Zunächst müssen wir uns entscheiden, wie wir die Landschaft generieren wollen.
Klar ist, dass die Landschaft zufällig generiert werden soll, sonst hätten wir bei jedem Programmstart die gleiche Landschaft und das wäre ziemlich langweilig. Wir brauchen also die Zufallsfunktionen von Delphi. Im OnCreate rufen wir mit dem Befehl Randomize die Initialisierung des Zufallszahlengenerators von Delphi auf, d.h. bei jedem Programmstart werden andere Zufallszahlen erzeugt.

Nun erstellen wir eine neue Prozedur namens Landschaftsgenerator. Hier wird jetzt die Landschaft erzeugt. Ohne jetzt lange die Vor-und Nachteile verschiedener Algorithmen auszudiskutieren, nehme ich einen ganz einfachen. Ich stell mir die Landschaft einfach als eine Zickzack-Kurve aus Punkten vor, die miteinander verbunden sind.
Zur Erzeugung gehe ich so vor:
Die Punktkette sei zunächst einfach eine horizontale Linie. Ich nehme den Mittelpunkt der Punktkette und verschiebe ihn nach oben. Damit entstehen zwei Teilketten, eine links und eine rechts des Mittelpunktes. Von diesen Teilketten nehme ich wiederum die Mittelpunkte, die ich dann senkrecht nach oben oder unten verschiebe, sodass neue Teilketten entstehen usw.
Man nennt das auch die Methode der Mittelpunktverschiebung, sie ist recht gebräuchlich zur Landschaftsgenerierung in der Computergrafik.
Ich werde 17 Punkte verwenden, das macht 16 Abschnitte auf der x-Achse und somit alle 50 Pixel einen Punkt bei 800 Pixeln Breite.
Durch diese Festlegung kann ich den Algorithmus nichtrekursiv schreiben, was den Code leichter verständlich machen sollte.
Ein paar Einschränkungen werde ich noch vornehmen, damit immer ein Berg in der Mitte entsteht.
Doch was erzähl ich hier lange rum, schaut euch erstmal die komplette Prozedur Landschaftsgenerator an:

ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
procedure TForm1.Landschaftsgenerator();
var punkt:array[1..19of TPoint;
    i : integer;
begin
    punkt[ 1].y:=Random(100)+450;
    punkt[17].y:=Random(100)+450;

    punkt[ 9].y:=(punkt[1].y+punkt[17].y) div 2;
    punkt[ 9].y:=punkt[9].y-Random(200)-200;

    punkt[ 5].y:=(punkt[ 1].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[13].y:=(punkt[ 9].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 3].y:=(punkt[ 1].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 7].y:=(punkt[ 5].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[11].y:=(punkt[ 9].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[15].y:=(punkt[13].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 2].y:=(punkt[ 1].y+punkt[ 3].y)div 2 + Random(100)-50;
    punkt[ 4].y:=(punkt[ 3].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 6].y:=(punkt[ 5].y+punkt[ 7].y)div 2 + Random(100)-50;
    punkt[ 8].y:=(punkt[ 7].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[10].y:=(punkt[ 9].y+punkt[11].y)div 2 + Random(100)-50;
    punkt[12].y:=(punkt[11].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[14].y:=(punkt[13].y+punkt[15].y)div 2 + Random(100)-50;
    punkt[16].y:=(punkt[15].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[18].x:=800;punkt[18].y:=600;
    punkt[19].X:=0;punkt[19].Y:=600;

    for i:=1 to 17 do punkt[i].X:=(i-1)*50;

    with Landschaft.Canvas do
    begin
        Pen.Color:=clWhite;
        Brush.Color:=clWhite;
        Rectangle(0,0,800,600);

        Pen.Color:=clBlack;
        Brush.Color:=clBlack;
        Polygon(punkt);
    end;

    Buffer:=Landschaft;

    Image1.Picture.Bitmap:=Buffer;
end;


Ist schon eine ganze Menge, nicht wahr? gehen wir es mal durch:
Zeile2: das Array punkt enthält die Punktwerte für die Punkte, aus denen unsere Landschaft aufgebaut ist. Ich nehme 19 Punkte statt 17, da ich aus den 17 Punkten der Landschaft und den Eckpunkten links unten und rechts unten die Landschaft direkt zeichnen kann.
Zeile3: i brauche ich als Laufvariable für die Schleife.
Zeile5 und 6: Hier lege ich die y-Werte der beiden Randpunkte fest. Da die Punkte von links nach rechts durchgezählt werden, ist der linke Rand die 1 und der rechte die 17. Die Werte für die Zufallsfunktion Random sind so gewählt, dass die Landschaft am Rand nicht zu hoch liegt.
Zeile 8 und 9: Punkt 9 ist der Mittelpunkt und deshalb auch die Spitze des Berges, den ich in der Landschaft haben will.
Die Höhe wird zunächst als Mittelwert der beiden Randpunkte berechnet und dann kommt noch ein Zufallswert hinzu.
Die Zufallsfunktion ist hier so gewählt, dass eigentlich immer ein Berg entsteht.
Zeile 11 und 12: Die Punkte 5 und 13 sind jeweils die Mittelpunkte zwischen dem Mittelpunkt Punkt 9 und dem linken bzw. rechten Rand. Sie werden auch wieder als Mittelwerte berechnet und dann um einen Zufallsfaktor senkrecht nach oben oder unten verschoben.
Zeile 14-26: Die gleichen Berechnungen passieren mit den restlichen Punkten.
Zeile 28 und 29: Die Koordinaten für die Eckpunkte rechts unten und links unten sind die beiden Punkte 18 und 19. Diese beiden Punkte werden benötigt, um zusammen mit den anderen Punkten mit dem Befehl Polygon die Landschaft zeichnen zu können.
Zeile 31: Berechnung der x-Werte der Punkte 1-17. Da diese gleichverteilt sind, sind sie in einem Abstand von 50 Pixeln zueinander.
Zeile 35-37: Ich zeichne zunächst das komplette Landschaftsbitmap weiss, indem ich ein weißes Rechteck in Größe der Bitmap male. So habe ich erstmal eine einheitliche Grundfarbe.
Zeile 39-40: Die Stiftfarbe und die Pinselfarbe setze ich auf schwarz, da ich die Landschaft erstmal schwarz malen will.
Zeile 41: Das eigentliche Zeichnen der Landschaft. Der Befehl Polygon zeichnet mit einem gegebenen Punktarray ein Vieleck auf den Bildschirm.

Zeile 44: Die Landschafts-Bitmap wird in die Buffer-Bitmap kopiert, wo später die gesamte Spielegrafik zwischengespeichert wird.
Zeile 46: Die Buffer-Bitmap wird in das Image kopiert, damit es auf dem Bildschirm erscheint.

Im FormCreate muss jetzt noch die Prozedur Landschaftsgenerator aufgerufen werden, damit die Landschaft beim Start generiert wird.
Jetzt kann das Programm gestartet werden und die Landschaft wird angezeigt:


user defined image

Soviel zum ersten Teil, mehr demnächst.

Hier nochmal der komplette Quellcode des Projektes.
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private-Deklarationen }
    procedure Landschaftsgenerator();
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

var Landschaft, Buffer : TBitmap;


// Programmstart
procedure TForm1.FormCreate(Sender: TObject);
begin
    // Bitmaps anlegen & Größe einstellen
    Landschaft:=TBitmap.Create;
    Landschaft.Width:=800;
    Landschaft.Height:=600;

    Buffer:=TBitmap.Create;
    Buffer.Width:=800;
    Buffer.Height:=600;

    Randomize;

    Landschaftsgenerator();
end;


procedure TForm1.Landschaftsgenerator();
var punkt:array[1..19of TPoint;
    i : integer;
begin
    punkt[ 1].y:=Random(100)+450;
    punkt[17].y:=Random(100)+450;

    punkt[ 9].y:=(punkt[1].y+punkt[17].y) div 2;
    punkt[ 9].y:=punkt[9].y-Random(200)-200;

    punkt[ 5].y:=(punkt[ 1].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[13].y:=(punkt[ 9].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 3].y:=(punkt[ 1].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 7].y:=(punkt[ 5].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[11].y:=(punkt[ 9].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[15].y:=(punkt[13].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 2].y:=(punkt[ 1].y+punkt[ 3].y)div 2 + Random(100)-50;
    punkt[ 4].y:=(punkt[ 3].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 6].y:=(punkt[ 5].y+punkt[ 7].y)div 2 + Random(100)-50;
    punkt[ 8].y:=(punkt[ 7].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[10].y:=(punkt[ 9].y+punkt[11].y)div 2 + Random(100)-50;
    punkt[12].y:=(punkt[11].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[14].y:=(punkt[13].y+punkt[15].y)div 2 + Random(100)-50;
    punkt[16].y:=(punkt[15].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[18].x:=800;punkt[18].y:=600;
    punkt[19].X:=0;punkt[19].Y:=600;

    for i:=1 to 17 do punkt[i].X:=(i-1)*50;

    with Landschaft.Canvas do
    begin
        Pen.Color:=clWhite;
        Brush.Color:=clWhite;
        Rectangle(0,0,800,600);

        Pen.Color:=clBlack;
        Brush.Color:=clBlack;
        Polygon(punkt);
    end;

    Buffer:=Landschaft;

    Image1.Picture.Bitmap:=Buffer;
end;

end.



1.3. - Pimp my Landschaft - Part1
Da bin ich wieder. Da es schon spät ist, kommt jetzt nur ein kleiner Abschnitt.
Schaut man sich kommerzielle Produkte wie z.B. Pocket Tanks an (www.blitwise.com/ptanks.html), dann sieht man schnell, dass unsere Landschaft doch ziemlich eintönig ist :cry:
Das muss geändert werden!

Schön wäre so ein kleiner angedeuteter Farbverlauf. Dazu wird die Generator-Prozedur ein wenig erweitert. Anstatt nur einmal das Polygon in einer Farbe zu zeichnen, zeichne ich es mehrfach. Zuerst wird es einmal mit der ersten Farbe gezeichnet, dann ein Stück nach unten verschoben und mit der zweiten Farbe gezeichnet, dann wieder ein Stück nach unten verschoben und mit der dritten Farbe gezeichnet und so weiter...
So entsteht ein horizontaler Farbverlauf, der dem Verlauf der Landschaft folgt und fertig ist die gepimpte Landschaft.
Hier die neue Prozedur, danach ein paar Erklärungen:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
procedure TForm1.Landschaftsgenerator();
var punkt:array[1..19of TPoint;
    i,j : integer;
begin
    punkt[ 1].y:=Random(100)+450;
    punkt[17].y:=Random(100)+450;

    punkt[ 9].y:=(punkt[1].y+punkt[17].y) div 2;
    punkt[ 9].y:=punkt[9].y-Random(200)-200;

    punkt[ 5].y:=(punkt[ 1].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[13].y:=(punkt[ 9].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 3].y:=(punkt[ 1].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 7].y:=(punkt[ 5].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[11].y:=(punkt[ 9].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[15].y:=(punkt[13].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 2].y:=(punkt[ 1].y+punkt[ 3].y)div 2 + Random(100)-50;
    punkt[ 4].y:=(punkt[ 3].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 6].y:=(punkt[ 5].y+punkt[ 7].y)div 2 + Random(100)-50;
    punkt[ 8].y:=(punkt[ 7].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[10].y:=(punkt[ 9].y+punkt[11].y)div 2 + Random(100)-50;
    punkt[12].y:=(punkt[11].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[14].y:=(punkt[13].y+punkt[15].y)div 2 + Random(100)-50;
    punkt[16].y:=(punkt[15].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[18].x:=800;punkt[18].y:=600;
    punkt[19].X:=0;punkt[19].Y:=600;

    for i:=1 to 17 do punkt[i].X:=(i-1)*50;

    with Landschaft.Canvas do
    begin
        pen.Color:=clWhite;
        brush.Color:=clWhite;
        rectangle(0,0,800,600);
    end;

    for i:=0 to 10 do
    begin
        with Landschaft.Canvas do
        begin
            Pen.Color:=RGB(0,255-i*20,0);
            Brush.Color:=Pen.Color;

            Polygon(punkt);

            for j:=1 to 17 do punkt[j].Y:=punkt[j].Y+5;
        end;
    end;

    Buffer:=Landschaft;

    Image1.Picture.Bitmap:=Buffer;
end;
end;


Zeile 3: Hier ist j als neue Variable dazugekommen, sie dient zur Verschiebung des Polygons.
Zeile 40: Die For-Schleife legt die Anzahl der Farben für den Farbverlauf fest.
Zeile 44: Hier wird der Farbwert in Abhängigkeit von der Schleifenvariable i berechnet. Die Funktion RGB erzeugt den Farbwert aus den RGB-Werten (Rot,Grün,Blau). Die Stiftfarbe erhält diesen Farbwert.
Zeile 45: Die Füllfarbe erhält den gleichen Farbwert.
Zeile 47: Das Polygon wird wie gewohnt gezeichnet.
Zeile 49: Die y-Werte der Punkte 1-17 werden neu berechnet, indem zu jedem y-Wert 5 hinzuaddiert wird. Das entspricht einer Verschiebung des Polygons um 5 Pixel nach unten. Das verschobene Polygon wird dann im nächsten Schleifendurchlauf mit der neu berechneten Farbe gezeichnet.

Wenn wir das Programm nun starten, sollte eine Landschaft zu sehen sein, die dem Bild ähnelt:
user defined image

Soviel für heute, gute Nacht!


Zuletzt bearbeitet von battledevil am Mi 23.11.05 20:29, insgesamt 3-mal bearbeitet
battledevil Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 96

WinXP, Win7
C#, C++, VBNET
BeitragVerfasst: Fr 18.11.05 11:44 
Titel: Spieletutorial für Einsteiger - Artillery Teil2
Und schon geht es weiter mit dem Tutorial. Als erstes werden wir eine kleine strukturelle Schwäche beheben. Bisher haben wir das Erzeugen und Anzeigen der Landschaft in der Prozedur Landschaftsgenerator durchgeführt. Das Anzeigen werden wir jedoch nun in eine eigene Prozedur auslagern, da es mit dem Erzeugen der Landschaft nichts zu tun hat. Dazu erzeugen wir eine neue Prozedur, ich habe sie SpielfeldZeichnen genannt. Dorthin werden die beiden letzten Befehle der Generator-Prozedur verschoben.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
procedure TForm1.Spielfeldzeichnen();
begin
    Buffer:=Landschaft;             // Landschaft in die Buffer-Bitmap kopieren
    Image1.Picture.Bitmap:=Buffer;  // Buffer-Bitmap in das Image kopieren
end;

Natürlich müssen wir jetzt die neue Prozedur im FormCreate aufrufen, sonst sehen wir kein Bild.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
procedure TForm1.FormCreate(Sender: TObject);
begin
    // Bitmaps anlegen & Größe einstellen
    Landschaft:=TBitmap.Create;
    Landschaft.Width:=800;
    Landschaft.Height:=600;

    Buffer:=TBitmap.Create;
    Buffer.Width:=800;
    Buffer.Height:=600;

    Randomize;

    Landschaftsgenerator();
    SpielfeldZeichnen();
end;


Der Nutzen wird schnell klar, wenn man sich vor Augen führt, dass die Landschaft nur zu Beginn des Spieles generiert wird, das Spielfeld jedoch immer wieder neu gezeichnet werden muss, wenn sich etwas verändert.
Deswegen beginnen wir nach dieser kleinen Modifikation mit dem 2. Teil.

Teil 2 - Panzer, Gravitation und der ganze Rest
Kurz mal Nachdenken. Was ist noch alles zu machen?

- Panzer in die Landschaft einzeichnen, die Position soll zufällig sein
- Bedienelemente auf der Form plazieren, mit denen die beiden Spieler den Winkel und die Schußstärke einstellen können
- eine Anzeige für den Wind, damit man sieht wie stark und in welche Richtung er weht
- Feuerknopf, mit dem ein Schuss ausgelöst wird
- Flugbahn des Geschosses zeichnen
- Geschoss bei Kontakt mit Landschaft oder Panzer explodieren lassen, Landschaft wird zerstört und Panzer erhalten Schaden
- Wenn ein Panzer eine bestimmt Menge Schaden erhalten hat, dann ist er zerstört und der andere Spieler hat gewonnen

Wow, klingt viel. Ist auch viel. Deswegen fange ich gleich an.

2.1. - Panzer in die Landschaft zeichnen
Bevor wir uns ans Zeichnen machen, ermitteln wir per Zufall die Position für die beiden Panzer. Also brauchen wir für jeden Panzer eine neue Variable, am besten vom Typ TPoint, der stellt uns schon das x und y für die Position bereit. Ich werde einen Panzer rot und den anderen blau malen, deswegen nenne ich die Variablen PanzerRot und PanzerBlau. Sie werden wie die Bitmaps global deklariert.

ausblenden Delphi-Quelltext
1:
2:
var Landschaft, Buffer : TBitmap;
    PanzerRot,PanzerBlau : TPoint;


Nun legen wir eine neue Prozedur namens PositionGenerieren an. Ein Panzer soll links stehen, der andere rechts. Deswegen werde ich zuerst per Zufall festlegen, welcher Panzer links und welcher rechts liegt. Danach wird für jeden Panzer eine Position in seiner Spielfeldhälfte erzeugt. Nur die x-Werte werden so bestimmt. Da die Panzer ja auf der Landschaft sitzen sollen, ist der y-Wert automatisch durch die Höhe der Landschaft gegeben und muss aus dem Landschafts-Bitmap ausgelesen werden. Hier die Prozedur:

ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
procedure TForm1.PositionGenerieren();
var zufall:byte;
    i:integer;
begin
    zufall:=Random(100);

    if zufall<50 then
    begin
        PanzerRot.X:=Random(300)+50;
        PanzerBlau.X:=random(300)+450
    end
    else
    begin
        PanzerRot.X:=Random(300)+450;
        PanzerBlau.X:=random(300)+50
    end;

    for i:=1 to 600 do
        if Landschaft.Canvas.Pixels[PanzerRot.X,i]<>clWhite then
        begin
            PanzerRot.Y:=i;
            break;
        end;

    for i:=1 to 600 do
        if Landschaft.Canvas.Pixels[PanzerBlau.X,i]<>clWhite then
        begin
            PanzerBlau.Y:=i;
            break;
        end;
end;


Zeile 2 und 3: Die Variable zufall brauche ich, um festzulegen welcher Panzer links und welcher rechts positioniert wird. Das i wird zur Ermittlung der y-Werte aus der Landschafts-Bitmap benötigt.
Zeile 5: Die Variable zufall erhält einen Zufallswert zwischen 0 und 99.
Zeile 7-16: wenn zufall kleiner als 50 ist, wird der rote Panzer links und der blaue Panzer rechts positioniert, ansonsten ist es genau umgedreht. Die Werte für Random sind so gewählt, dass die Panzer jeweils in ihrer Spielhälfte positioniert werden und dabei nicht zu dicht am Rand oder in der Mitte stehen.
Zeile 18 - 23: Für den roten Panzer wird der y-Wert ermittelt. In der Landschaftsbitmap wird nachgeschaut, wo der Himmel aufhört und die Landschaft beginnt. Dazu überprüft eine Schleife senkrecht an der x-Position des Panzers von oben nach unten, ob das Pixel weiß (also Himmel) ist oder nicht. Wenn nicht, wird diese Position als Panzerposition gespeichert und die Schleife mit break abgebrochen.
Zeile 25 - 30: Ebenso wird die y-Position des blauen Panzers bestimmt.

Damit haben wir für beide Panzer eine Position. Die Prozedur PositionGenerieren tragen wir in das FormCreate ein.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
procedure TForm1.FormCreate(Sender: TObject);
begin

    ....

    Randomize;

    Landschaftsgenerator();
    PositionGenerieren();
    SpielfeldZeichnen();
end;


Nun können wir uns an das Zeichnen machen.
Die einfachste Möglichkeit wäre, an der Position ein Viereck oder einen Halbkreis zu zeichnen. Das werde ich nicht tun. Ich benutze wieder den Zeichenbefehl Polygon, um eine etwas panzerähnliche Form zu erzeugen.
Zuerst legen wir eine neue Prozedur an.
ausblenden Delphi-Quelltext
1:
procedure PanzerZeichnen(farbe:string);					

Wir werden ihr die Farbe als String übergeben. In dieser Farbe wird dann der Panzer gezeichnet. Ich werde eine weitere Bitmap namens Panzer anlegen, um dort das Bild des Panzers zwischenzuspeichern. Das wäre an dieser Stelle vielleicht nicht unbedingt notwendig, aber so kann ich mal zeigen, wie man kleine Bitmaps (Panzer) in eine große Bitmap (Buffer) kopiert und dabei Transparenz verwendet. Die Bitmap wird im OnCreate angelegt und die Größe auf 15x7 Pixel festgelegt.

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
var Landschaft, Buffer, Panzer : TBitmap;

 ...

procedure TForm1.FormCreate(Sender: TObject);
begin
    ...

    Panzer:=TBitmap.Create;
    Panzer.Width:=15;
    Panzer.Height:=7;

    ...
end;


Dann folgt die PanzerZeichnen-Prozedur:

ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
procedure TForm1.PanzerZeichnen(farbe:string);
var x,y:integer;
begin
    with Panzer.Canvas do
    begin
        pen.Color:=clWhite;
        brush.Color:=clWhite;
        Rectangle(0,0,Panzer.Width,Panzer.Height);
    end;

    Panzer.TransparentColor:=clWhite;
    Panzer.Transparent:=true;

    if farbe='rot' then
    begin
        Panzer.Canvas.Brush.Color:=clRed;
        Panzer.Canvas.Pen.Color:=RGB(150,0,0);
    end;

    if farbe='blau' then
    begin
        Panzer.Canvas.Brush.Color:=clBlue;
        Panzer.Canvas.Pen.Color:=RGB(0,0,150);
    end;

    with Panzer.Canvas do
    begin
        Polygon([Point(0,3),Point(0,2),Point(1,1),Point(2,1),Point(3,0),Point(11,0),
                Point(12,1),Point(13,1),Point(14,2),Point(14,3),Point(13,4),Point(1,4)]);
        Polygon([Point(2,5),Point(3,4),Point(11,4),Point(12,5),Point(11,6),Point(3,6)]);
    end;
end;


Zeile 6-9: Die Panzer-Bitmap wird komplett weiß ausgemalt. Weiß wird die transparente Farbe für dieses Bitmap sein.
Zeile 11: Weiß wird als transparente Farbe festgelegt.
Zeile 12: Die Transparenz wird aktiviert.
Zeile 14-24: Je nachdem, ob der rote oder der blaue Panzer gezeichnet werden sollen, werden die Stift- und die Füllfarbe eingestellt.
Zeile 26-31: Der Panzer wird gezeichnet. Es sind einfach zwei Polygone, die ich mir ausgedacht habe.

Wer aufgepasst hat, der weiß, dass der auf die Panzer-Bitmap gezeichnete Panzer erst noch in die Buffer-Bitmap kopiert werden muss.
Da das zur Darstellung des Spielfeldes gehört, schreibe ich das in die Prozedur SpielfeldZeichnen.

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
procedure TForm1.Spielfeldzeichnen();
begin
    Buffer:=Landschaft;             // Landschaft in die Buffer-Bitmap kopieren

    PanzerZeichnen('rot');
    Buffer.Canvas.Draw(PanzerRot.X-7,PanzerRot.Y-3,Panzer);

    Panzerzeichnen('blau');
    Buffer.Canvas.Draw(PanzerBlau.X-7,PanzerBlau.Y-3,Panzer);

    Image1.Picture.Bitmap:=Buffer;  // Buffer-Bitmap in das Image kopieren
end;


Erst rufe ich die Prozedur PanzerZeichnen mit der Farbe auf. Sie zeichnet den Panzer in die Panzer-Bitmap.
Dann kopiere ich mit dem Befehl Draw die Panzer-Bitmap in die Buffer-Bitmap. Dazu muss ich Koordinaten angeben, wo ich die Panzer-Bitmap innerhalb der Buffer-Bitmap hinkopiert haben will. Das sind jeweils die Positionskoordinaten des Panzers. Die Abzüge von -7 und -3 für x und y muss ich einführen, da ich die Koordinaten der Panzer als Mittelpunktkoordinaten angebe, die Funktion Draw will aber die Koordinaten der linken oberen Ecke. Wenn wir das Programm jetzt starten, sollten die beiden Panzer eingezeichnet werden.

user defined image

Anbei das gesamte Projekt, da es doch schon einige Zeilen Quellcode sind.
home.arcor.de/origam...r/tut/tutorial01.zip
Einloggen, um Attachments anzusehen!


Zuletzt bearbeitet von battledevil am Mo 21.11.05 16:45, insgesamt 1-mal bearbeitet
battledevil Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 96

WinXP, Win7
C#, C++, VBNET
BeitragVerfasst: So 20.11.05 13:42 
Titel: Spieletutorial für Einsteiger - Artillery Teil3
Und weiter geht es mit dem Artillery-Tutorial. Wird langsam Zeit, dass wir das Spiel zum Laufen bringen.
Eine Sache ist vorher noch zu erledigen: Wir haben noch keine Eingabemöglichkeit. Deswegen erstellen wir jetzt die ganzen Komponenten, mit denen das Spiel später gesteuert wird.

2.2 - Die Gestaltung der Programmoberfläche
Die Komponenten werden unterhalb des Images angeordnet.
Zuerst brauchen wir ein Panel, das die ganzen anderen Komponenten aufnimmt. Die Caption-Eigenschaft löschen wir, wir brauchen die Anzeige des Namens nicht, die würde nur stören.
Damit man immer die Panzerungen beider Panzer im Blick hat, nehmen wir zwei Labels, die wir LabelPanzerungRot und LabelPanzerungBlau nennen. Dann brauchen wir noch zwei Trackbars, ich habe sie TrackbarWinkel und TrackbarGeschwindigkeit genannt. Wie zu vermuten, werden damit der Winkel und die Geschwindigkeit eingestellt. Trackbarwinkel erhält als Min 0 und als Max 180, TrackbarGeschwindigkeit Min=0 und Max=100. Die Frequency stelle ich auf 10 ein, die Thumblength auf 8. Dann kommen noch zwei Labels hinzu, um die Werte für Winkel und Geschwindigkeit auch als Zahlen anzuzeigen. Ich nenne sie - wie könnte es anders sein - LabelWinkel und LabelGeschwindigkeit. Es fehlt noch ein Button zum Abfeuern des Schusses, ich nenne ihn ButtonFeuer. Als letztes brauchen wir noch die Anzeige des Windes, dafür verwende ich ein Label und eine Controlbar, ich nenne sie LabelWind und ControlbarWind.
Auf dem folgenden Bild seht ihr die Anordnung:
user defined image
Jetzt koppeln wir die Anzeige der Labels für Geschwindigkeit und Winkel mit den Trackbars, damit beim Verändern der Position des Reglers die korrekte Größe angezeigt wird. Dazu werden die Change-Ereignisse der Trackbars genutzt.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
procedure TForm1.TrackBarWinkelChange(Sender: TObject);
begin
    LabelWinkel.Caption:='Winkel: '+IntToStr(TrackbarWinkel.Position);
end;

procedure TForm1.TrackBarGeschwindigkeitChange(Sender: TObject);
begin
    LabelGeschwindigkeit.Caption:='Geschwindigkeit: '+IntToStr(TrackbarGeschwindigkeit.Position);
end;

Wie zu sehen ist, reicht jedesmal eine einzige Zeile, um den Positionswert der Trackbar im Label anzuzeigen.
Damit ich später die Stärke des Windes grafisch in der Controlbar darstellen kann, sind noch ein paar Modifikationen notwendig. Ich setze die Breite auf 200 und die Höhe auf 20. Die Eigenschaft BevelKind stelle ich auf bkFlat ein, das sieht nachher besser aus.
Dann muss ich noch die Bitmap der Controlbar erzeugen, das geschieht wie immer im FormCreate.
ausblenden Delphi-Quelltext
1:
2:
3:
    ControlbarWind.Picture.Bitmap:=TBitmap.Create;
    ControlbarWind.Picture.Bitmap.Width:=200;
    ControlbarWind.Picture.Bitmap.Height:=20;


Jetzt ist es wichtig, die Variablen für die Eingabe festzulegen. Wir brauchen für jeden Panzer eine Variable für Winkel und Geschwindigkeit, schließlich wollen wir uns merken, was jeder Spieler im letzten Zug eingestellt hat. Sonst müsste er die Einstellung von Winkel und Geschwindigkeit jede Runde neu vornehmen, das wäre nicht sehr komfortabel.
Insgesamt haben wir jetzt schon 5 Variablen, die abhängig von jedem Panzer sind: Neben Winkel und Geschwindigkeit sind das die x und die y-Koordinate jedes Panzers und die Stärke der Panzerung. Deswegen lohnt sich das Erstellen eines eigenen Datentypes. Also löschen wir die Zeile
ausblenden Delphi-Quelltext
1:
    PanzerRot,PanzerBlau : TPoint;					

und stattdessen sieht die Variablendeklaration wie folgt aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
type TPanzer=record
    X : integer;
    Y : integer;
    Winkel : integer;
    Geschwindigkeit : integer;
    Panzerung : byte;
end;

var Landschaft, Buffer, Panzer : TBitmap;
    PanzerRot,PanzerBlau : TPanzer;

Ein eigener Datentyp wird mit type angelegt und kann beliebige Datentypen aufnehmen.
Wir stellen für den Spielstart Werte für Winkel, Geschwindigkeit und Panzerung der Panzer ein. Das erledigen wir in der PositionGenerieren-Prozedur, dort werden ja schon die Koordinaten für die Panzer erzeugt.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
    PanzerRot.Winkel:=90;
    PanzerRot.Geschwindigkeit:=50;
    PanzerRot.Panzerung:=100;

    PanzerBlau.Winkel:=90;
    PanzerBlau.Geschwindigkeit:=50;
    PanzerBlau.Panzerung:=100;


Wenn später im Spiel ein Panzer von einem Geschoss getroffen wird, so wird er beschädigt und der Wert der Panzerung sinkt. Sinkt der Wert der Panzerung auf Null, so hat der betreffende Spieler verloren.

2.3. Pimp my Landschaft - Teil 2
Bevor wir uns dem Ernst der Spieleprogrammierung richtig zuwenden, pimpen wir die Landschaft noch ein wenig auf. Sie ist zwar schon ganz gut, doch der langweilige weiße Himmel stört. Dort sollte besser ein Farbverlauf sein. Doch da wir diese weiße Fläche in der Landschaftsbitmap zur Kollisionserkennung brauchen, müssen wir das anders lösen. Wir zeichnen den Himmel einfach zuerst in die Buffer-Bitmap, setzen weiß als transparente Farbe in der Landschaftsbitmap und kopieren dann die Landschaft in den Buffer.
Dazu modifizieren wir die prozedur SpielfeldZeichnen.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
procedure TForm1.SpielfeldZeichnen();
var i:integer;
begin
    with Buffer.Canvas do
    begin
        for i:=1 to 600 do
        begin
            Pen.Color:=RGB(255 - i div 3,255 - i div 3,255);
            MoveTo(0,i);
            LineTo(800,i);
        end;
    end;

    Buffer.Canvas.Draw(0,0,Landschaft);

    PanzerZeichnen('rot');
    Buffer.Canvas.Draw(PanzerRot.X-7,PanzerRot.Y-3,Panzer);

    Panzerzeichnen('blau');
    Buffer.Canvas.Draw(PanzerBlau.X-7,PanzerBlau.Y-3,Panzer);

    Image1.Picture.Bitmap:=Buffer;  // Buffer-Bitmap in das Image kopieren
end;

Zeile 2: Die Variable i brauchen wir für den Farbverlauf.
Zeile 4-12: Hier wird der Farbverlauf erzeugt, indem der Farbwert von i abhängig gemacht wird. Zugleich ist i die Pixelzeile, die in der Farbe gezeichnet wird.
Zeile 14: Die Landschaftsbitmap wird jetzt mit dem Draw-Befehl in den buffer kopiert, sonst funktioniert das mit der Transparenz nicht.
Apropo Transparenz: In die Prozedur Landschaftsgenerator muss noch die Zeile
ausblenden Delphi-Quelltext
1:
Landschaft.TransparentColor:=clWhite;					

eingefügt werden. Nun haben wir einen schönen Farbverlauf von weiß nach blau als Himmel.

3. Die Spiellogik
Kommen wir zum wichtigsten Teil, der Spiellogik. Ohne eine vernünftige Logik ist kaum ein Programm und auch kein Spiel möglich. Normalerweise macht man sich schon Gedanken darüber, bevor man damit anfängt zu programmieren. Ich habe mir das bis hier aufgespart, weil es für euch bestimmt interessanter ist, direkt loszulegen anstatt sich erst viele Gedanken zu machen. Aber jetzt wird es höchste Zeit dafür.

3.1. - Der Programmablaufplan
Es gibt verschiedene Hilfsmittel, um die Logik eines Programms zu erstellen. Eines davon ist der Programmablaufplan. Wer mehr dazu wissen will, sollte hier mal nachschauen:
de.wikipedia.org/wiki/DIN_66001
Ich habe einen für dieses Spiel erstellt, hier ist er:
user defined image
Schaut ihn euch genau an, er ist die Übersicht über den gesamten Ablauf des Programms.
Wir haben uns bisher nur um die Initialisierung gekümmert, darunter fallen auch die ganzen grafischen Ausgaben. Ihr werdet euch jetzt fragen, wieviel Arbeit denn noch auf euch zukommt, doch mit den bisherigen Prozeduren und dem Programmablaufplan sind die schwierigsten Hürden schon gemeistert und der noch fehlende Code ist gar nicht mal so viel, auch wenn es im Programmablaufplan zunächst anders aussieht.
Es ist wichtig, dass der Plan korrekt ist, denn wenn hier schon Fehler in der Logik sind, dann werden diese später auch im Programm auftreten.
Wie wir diesen Plan in Programmcode umsetzen folgt im nächsten Teil.


Zuletzt bearbeitet von battledevil am Di 22.11.05 11:54, insgesamt 1-mal bearbeitet
battledevil Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 96

WinXP, Win7
C#, C++, VBNET
BeitragVerfasst: Mo 21.11.05 17:59 
Titel: Re: Spieletutorial für Einsteiger - Artillery Teil3
3.2. Startspieler festlegen und Programmoberfläche aktualisieren
Jetzt wird es spannend. Wir setzen den Programmablaufplan um. Zuerst müssen wir einen Startspieler festlegen. Dazu brauchen wir eine globale Variable, um uns den Spieler zu merken und eine Prozedur, wo der Spieler per Zufallsfunktion bestimmt wird.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
var Landschaft, Buffer, Panzer : TBitmap;
    PanzerRot,PanzerBlau : TPanzer;
    Spieler : String;

 ...

procedure TForm1.Startspieler();
var i : byte;
begin
    i:=Random(2);
    if i<1 then Spieler:='rot' else Spieler:='blau';
end;

Das war nicht weiter schwer. Falls ihr euch fragt, warum ich für diese 2 Zeilen Code eine extra Prozedur schreibe, dann gibt es zwei Gründe. Erstens wird sie an zwei Stellen im Programm aufgerufen, nämlich beim Programmstart im FormCreate und dann nochmal, wenn das Spiel zu Ende ist und ein neues Spiel gestartet wird. Zweitens dient es der Übersichtlichkeit, man findet den Code schneller, wenn er in sinnvoll benannte Prozeduren verpackt wird. Wie gesagt, wird die Prozedur jetzt noch ins FormCreate eingetragen.

Kommen wir zum nächsten Feld im Programmablaufplan. Wir wollen die Programmoberfläche aktualisieren. Das bedeutet, wir werden alle Werte für den aktiven Spieler anzeigen und per Zufall eine Windstärke festlegen und anzeigen. Ich nenne die neue Prozedur für diese Aufgaben AnzeigeAktualisieren. Hier der Code:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
procedure TForm1.AnzeigeAktualisieren();
begin
    Wind:=Round(Random(20)-10);

    LabelWind.Caption:='Windstärke: '+IntToStr(Wind);

    with ControlbarWind.Picture.Bitmap.Canvas do
    begin
        Pen.Color:=clWhite;
        Brush.Color:=clWhite;
        Rectangle(1,1,200,20);

        Pen.Color:=clBlue;
        Brush.Color:=clBlue;

        if Wind>0 then Rectangle(100,1,100+Wind*10,20);
        if Wind<0 then Rectangle(100+Wind*10,1,100,20);
    end;

    LabelPanzerungRot.Caption:='Panzer Rot:'+IntToStr(PanzerRot.Panzerung);
    LabelPanzerungBlau.Caption:='Panzer Blau:'+IntToStr(PanzerBlau.Panzerung);

    if Spieler='rot' then
    begin
        TrackbarWinkel.Position:=180-PanzerRot.Winkel;
        TrackbarGeschwindigkeit.Position:=PanzerRot.Geschwindigkeit;
        Panel1.Color:=RGB(255,50,50);
    end;

    if Spieler='blau' then
    begin
        TrackbarWinkel.Position:=180-PanzerBlau.Winkel;
        TrackbarGeschwindigkeit.Position:=PanzerBlau.Geschwindigkeit;
        Panel1.Color:=RGB(100,100,255);
    end;
end;

Zeile 3: Der Wind wird als Zufallswert zwischen -10 und +10 bestimmt. Der Wert ist die Windstärke, das Vorzeichen kennzeichnet die Windrichtung (minus ist von rechts nach links, plus ist von links nach rechts). Die Richtung ergibt sich automatisch aus der Formel der Flugbahn, doch die wird erst später behandelt.
Zeile 5: Der erzeugte Zufallswert des Windes wird im Label ausgegeben.
Zeile 7-18: Die grafische Anzeige der Windstärke in der Controlbar wird als Rechteck umgesetzt.
Zeile 20+21: Die Panzerungen werden auf den Labels angezeigt.
Zeile 23-28: Falls der rote Spieler an der Reihe ist, werden die Werte des roten Panzers für Winkel und Geschwindigkeit angezeigt. Zusätzlich wird das Panel rot eingefärbt, damit ist klar, dass der rote Spieler an der Reihe ist.
Zeile 30-35: Das gleiche wird für den blauen Spieler gemacht.

Im FormCreate wird der Aufruf der Prozedur hinzugefügt. Hier das komplette FormCreate:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
procedure TForm1.FormCreate(Sender: TObject);
begin
    Landschaft:=TBitmap.Create;
    Landschaft.Width:=800;
    Landschaft.Height:=600;

    Buffer:=TBitmap.Create;
    Buffer.Width:=800;
    Buffer.Height:=600;

    Panzer:=TBitmap.Create;
    Panzer.Width:=15;
    Panzer.Height:=7;

    ControlbarWind.Picture.Bitmap:=TBitmap.Create;
    ControlbarWind.Picture.Bitmap.Width:=200;
    ControlbarWind.Picture.Bitmap.Height:=20;

    Randomize;

    Landschaftsgenerator();
    PositionGenerieren();
    SpielfeldZeichnen();
    StartSpieler();
    AnzeigeAktualisieren();
end;

Wenn wir jetzt das Programm starten, sollten alle Werte angezeigt werden.

Irgendwas fehlt doch noch ... was war das bloß?
Achja, unseren Panzern fehlen doch tatsächlich die Kanonenrohre! Wie konnte das passieren???
Ich hab mir das bis jetzt aufgespart, weil die Rohre beim Einstellen des Winkels mitbewegt werden sollen. Außerdem soll immer nur das Rohr des aktiven Spielers bewegt werden.
Die Rohre werden nur wenige Pixel lang sein und es wäre komplette Ressourcenverschwendung, wenn wir nur wegen der Bewegung eines Rohres das gesamte Spielfeld neu zeichnen würden. Also wird hier ein bißchen getrickst. Ich benutze hier nur eine kleine Bitmap, die den Ausschnitt des Hintergrundes enthält, in dem das Kanonenrohr gezeichnet wird. Das Rohr wird dann in die Bitmap gezeichnet und direkt auf das Image kopiert. Das sollte schnell genug sein, damit ich es direkt mit der Trackbar des Winkels koppeln kann. Das ist jetzt schon die 4. Bitmap, die wir brauchen. Ich verspreche, es werden nicht mehr viele hinzukommen :-)
ausblenden Delphi-Quelltext
1:
var Landschaft, Buffer, Panzer, Rohr : TBitmap;					

Natürlich fehlen da noch 3 Zeilen im FormCreate:
ausblenden Delphi-Quelltext
1:
2:
3:
    Rohr:=TBitmap.Create;
    Rohr.Width:=15;
    Rohr.Height:=7;

Die Prozedur TrackbarWinkelChange wird nun ein ganzes Stück erweitert.
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
procedure TForm1.TrackBarWinkelChange(Sender: TObject);
var x,y:integer;
    winkel:double;
begin
    LabelWinkel.Caption:='Winkel: '+IntToStr(180-TrackbarWinkel.Position);

    winkel:=degtorad(TrackbarWinkel.Position+180);

    x:=round(cos(winkel)*7+7);
    y:=round(sin(winkel)*7+6);

    with Rohr.Canvas do
    begin
        Pen.Color:=clBlack;

        if Spieler='rot' then
        begin
            CopyRect(Rect(0,0,15,7),Buffer.Canvas,Rect(PanzerRot.X-7,PanzerRot.Y-10,PanzerRot.X+7,PanzerRot.Y-3));
            MoveTo(7,6);
            LineTo(x,y);
            Image1.Canvas.Draw(PanzerRot.X-7,PanzerRot.Y-10,Rohr);
        end;

        if Spieler='blau' then
        begin
            CopyRect(Rect(0,0,15,7),Buffer.Canvas,Rect(PanzerBlau.X-7,PanzerBlau.Y-10,PanzerBlau.X+7,PanzerBlau.Y-3));
            MoveTo(7,6);
            LineTo(x,y);
            Image1.Canvas.Draw(PanzerBlau.X-7,PanzerBlau.Y-10,Rohr);
        end;
    end;
end;

Zeile 2 und 3: Diese Variablen sind notwendig, um die Ausrichtung des Rohres zu bestimmen und es zeichnen zu können.
Zeile 5: Hier ist ein 180- hinzugekommen, da die Trackbarwerte immer von links nach rechts steigen, wir aber das genaue Gegenteil benötigen.
Zeile 7: Der Winkel wird mit der Funktion DegToRad ins Bogenmaß umgewandelt, da die Funktionen für Sinus und Cosinus nur Winkel im Bogenmaß akzeptieren. Diese ganzen Funktionen befinden sich in der Mathematikbibliothek von Delphi, deswegen muss am Beginn des Quellcodes, wo hinter uses alle Bibliotheken eingebunden werden, der Eintrag Math hinzugefügt werden. Erst dann "kennt" Delphi die mathematischen Funktionen.
Zeile 9 und 10: Ausgehend von dem Startpunkt (7,6) und dem Winkel wird der Endpunkt des Kanonenrohres berechnet.
Zeile 16-22: Falls der rote Spieler an der Reihe ist, wird der Hintergrund an der Stelle des Kanonenrohres des roten Panzers aus der Buffer-Bitmap in die Rohr-Bitmap kopiert. Um einen Teil einer Bitmap in eine andere Bitmap zu kopieren, verwende ich den Befehl CopyRect. Dann wird das Kanonenrohr in die Rohr-Bitmap eingezeichnet. Zum Schluß wird die Rohr-Bitmap in das Image mit dem Draw-Befehl kopiert und dadurch angezeigt.
Zeile 24-30. Gleiche Vorgehensweise für den blauen Spieler.

Wir haben jetzt die Oberfläche komplett funktionsfähig.
Hier nochmal der gesamte Quellcode des Projektes zum Vergleichen:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212:
213:
214:
215:
216:
217:
218:
219:
220:
221:
222:
223:
224:
225:
226:
227:
228:
229:
230:
231:
232:
233:
234:
235:
236:
237:
238:
239:
240:
241:
242:
243:
244:
245:
246:
247:
248:
249:
250:
251:
252:
253:
254:
255:
256:
257:
258:
259:
260:
261:
262:
263:
264:
265:
266:
267:
268:
269:
270:
271:
272:
273:
274:
275:
276:
277:
278:
279:
280:
281:
282:
283:
284:
285:
286:
287:
288:
289:
290:
291:
292:
293:
294:
295:
296:
297:
298:
299:
300:
301:
302:
303:
304:
305:
306:
307:
308:
309:
310:
311:
312:
313:
314:
315:
316:
317:
318:
319:
320:
321:
322:
323:
324:
325:
326:
327:
328:
329:
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls, ComCtrls, Math;

type
  TForm1 = class(TForm)
    Image1: TImage;
    Panel1: TPanel;
    LabelPanzerungRot: TLabel;
    LabelPanzerungBlau: TLabel;
    LabelWinkel: TLabel;
    LabelGeschwindigkeit: TLabel;
    LabelWind: TLabel;
    ButtonFeuer: TButton;
    TrackBarWinkel: TTrackBar;
    TrackBarGeschwindigkeit: TTrackBar;
    ControlBarWind: TControlBar;
    procedure FormCreate(Sender: TObject);
    procedure TrackBarWinkelChange(Sender: TObject);
    procedure TrackBarGeschwindigkeitChange(Sender: TObject);
  private
    { Private-Deklarationen }
    procedure Landschaftsgenerator();
    procedure SpielfeldZeichnen();
    procedure PositionGenerieren();
    procedure PanzerZeichnen(farbe:string);
    procedure Startspieler();
    procedure AnzeigeAktualisieren();
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type TPanzer=record
    X : integer;
    Y : integer;
    Winkel : integer;
    Geschwindigkeit : integer;
    Panzerung : byte;
end;

var Landschaft, Buffer, Panzer, Rohr : TBitmap;
    PanzerRot,PanzerBlau : TPanzer;
    Spieler:String;
    Wind:ShortInt;

// Programmstart
procedure TForm1.FormCreate(Sender: TObject);
begin
    // Bitmaps anlegen & Größe einstellen
    Landschaft:=TBitmap.Create;
    Landschaft.Width:=800;
    Landschaft.Height:=600;

    Buffer:=TBitmap.Create;
    Buffer.Width:=800;
    Buffer.Height:=600;

    Panzer:=TBitmap.Create;
    Panzer.Width:=15;
    Panzer.Height:=7;

    Rohr:=TBitmap.Create;
    Rohr.Width:=15;
    Rohr.Height:=7;

    ControlbarWind.Picture.Bitmap:=TBitmap.Create;
    ControlbarWind.Picture.Bitmap.Width:=200;
    ControlbarWind.Picture.Bitmap.Height:=20;

    Randomize;

    Landschaftsgenerator();
    PositionGenerieren();
    SpielfeldZeichnen();
    Startspieler();
    AnzeigeAktualisieren();
end;


procedure TForm1.Landschaftsgenerator();
var punkt:array[1..19of TPoint;
    i,j : integer;
begin
    punkt[ 1].y:=Random(100)+450;
    punkt[17].y:=Random(100)+450;

    punkt[ 9].y:=(punkt[1].y+punkt[17].y) div 2;
    punkt[ 9].y:=punkt[9].y-Random(200)-200;

    punkt[ 5].y:=(punkt[ 1].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[13].y:=(punkt[ 9].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 3].y:=(punkt[ 1].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 7].y:=(punkt[ 5].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[11].y:=(punkt[ 9].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[15].y:=(punkt[13].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[ 2].y:=(punkt[ 1].y+punkt[ 3].y)div 2 + Random(100)-50;
    punkt[ 4].y:=(punkt[ 3].y+punkt[ 5].y)div 2 + Random(100)-50;
    punkt[ 6].y:=(punkt[ 5].y+punkt[ 7].y)div 2 + Random(100)-50;
    punkt[ 8].y:=(punkt[ 7].y+punkt[ 9].y)div 2 + Random(100)-50;
    punkt[10].y:=(punkt[ 9].y+punkt[11].y)div 2 + Random(100)-50;
    punkt[12].y:=(punkt[11].y+punkt[13].y)div 2 + Random(100)-50;
    punkt[14].y:=(punkt[13].y+punkt[15].y)div 2 + Random(100)-50;
    punkt[16].y:=(punkt[15].y+punkt[17].y)div 2 + Random(100)-50;

    punkt[18].x:=800;punkt[18].y:=600;
    punkt[19].X:=0;punkt[19].Y:=600;

    for i:=1 to 17 do punkt[i].X:=(i-1)*50;

    with Landschaft.Canvas do
    begin
        pen.Color:=clWhite;
        brush.Color:=clWhite;
        rectangle(0,0,800,600);
    end;

    for i:=0 to 10 do
    begin
        with Landschaft.Canvas do
        begin
            Pen.Color:=RGB(0,255-i*20,0);
            Brush.Color:=Pen.Color;

            Polygon(punkt);

            for j:=1 to 17 do punkt[j].Y:=punkt[j].Y+5;
        end;
    end;

    Landschaft.TransparentColor:=clWhite;
    Landschaft.Transparent:=true;
end;


procedure TForm1.SpielfeldZeichnen();
var i:integer;
begin
    with Buffer.Canvas do
    begin
        for i:=1 to 600 do
        begin
            Pen.Color:=RGB(255 - i div 3,255 - i div 3,255);
            MoveTo(0,i);
            LineTo(800,i);
        end;
    end;

    Buffer.Canvas.Draw(0,0,Landschaft);

    PanzerZeichnen('rot');
    Buffer.Canvas.Draw(PanzerRot.X-7,PanzerRot.Y-3,Panzer);

    Panzerzeichnen('blau');
    Buffer.Canvas.Draw(PanzerBlau.X-7,PanzerBlau.Y-3,Panzer);

    Image1.Picture.Bitmap:=Buffer;  // Buffer-Bitmap in das Image kopieren
end;


procedure TForm1.PositionGenerieren();
var zufall:byte;
    i:integer;
begin
    zufall:=Random(100);

    if zufall<50 then
    begin
        PanzerRot.X:=Random(300)+50;
        PanzerBlau.X:=random(300)+450
    end
    else
    begin
        PanzerRot.X:=Random(300)+450;
        PanzerBlau.X:=random(300)+50
    end;

    for i:=1 to 600 do
        if Landschaft.Canvas.Pixels[PanzerRot.X,i]<>clWhite then
        begin
            PanzerRot.Y:=i;
            break;
        end;

    for i:=1 to 600 do
        if Landschaft.Canvas.Pixels[PanzerBlau.X,i]<>clWhite then
        begin
            PanzerBlau.Y:=i;
            break;
        end;

    PanzerRot.Winkel:=90;
    PanzerRot.Geschwindigkeit:=50;
    PanzerRot.Panzerung:=100;

    PanzerBlau.Winkel:=90;
    PanzerBlau.Geschwindigkeit:=50;
    PanzerBlau.Panzerung:=100;
end;


procedure TForm1.PanzerZeichnen(farbe:string);
var x,y:integer;
begin
    with Panzer.Canvas do
    begin
        pen.Color:=clWhite;
        brush.Color:=clWhite;
        Rectangle(0,0,Panzer.Width,Panzer.Height);
    end;

    Panzer.TransparentColor:=clWhite;
    Panzer.Transparent:=true;

    if farbe='rot' then
    begin
        Panzer.Canvas.Brush.Color:=clRed;
        Panzer.Canvas.Pen.Color:=RGB(150,0,0);
    end;

    if farbe='blau' then
    begin
        Panzer.Canvas.Brush.Color:=clBlue;
        Panzer.Canvas.Pen.Color:=RGB(0,0,150);
    end;

    with Panzer.Canvas do
    begin
        Polygon([Point(0,3),Point(0,2),Point(1,1),Point(2,1),Point(3,0),Point(11,0),
                Point(12,1),Point(13,1),Point(14,2),Point(14,3),Point(13,4),Point(1,4)]);
        Polygon([Point(2,5),Point(3,4),Point(11,4),Point(12,5),Point(11,6),Point(3,6)]);
    end;
end;

procedure TForm1.TrackBarWinkelChange(Sender: TObject);
var x,y:integer;
    winkel:double;
begin
    LabelWinkel.Caption:='Winkel: '+IntToStr(180-TrackbarWinkel.Position);

    winkel:=degtorad(TrackbarWinkel.Position+180);

    x:=round(cos(winkel)*7+7);
    y:=round(sin(winkel)*7+6);

    with Rohr.Canvas do
    begin
        Pen.Color:=clBlack;

        if Spieler='rot' then
        begin
            CopyRect(Rect(0,0,15,7),Buffer.Canvas,Rect(PanzerRot.X-7,PanzerRot.Y-10,PanzerRot.X+7,PanzerRot.Y-3));
            MoveTo(7,6);
            LineTo(x,y);
            Image1.Canvas.Draw(PanzerRot.X-7,PanzerRot.Y-10,Rohr);
        end;

        if Spieler='blau' then
        begin
            CopyRect(Rect(0,0,15,7),Buffer.Canvas,Rect(PanzerBlau.X-7,PanzerBlau.Y-10,PanzerBlau.X+7,PanzerBlau.Y-3));
            MoveTo(7,6);
            LineTo(x,y);
            Image1.Canvas.Draw(PanzerBlau.X-7,PanzerBlau.Y-10,Rohr);
        end;
    end;
end;

procedure TForm1.TrackBarGeschwindigkeitChange(Sender: TObject);
begin
    LabelGeschwindigkeit.Caption:='Geschwindigkeit: '+IntToStr(TrackbarGeschwindigkeit.Position);
end;

procedure TForm1.Startspieler();
var i : byte;
begin
    i:=Random(2);
    if i<1 then Spieler:='rot' else Spieler:='blau';
end;

procedure TForm1.AnzeigeAktualisieren();
begin
    Wind:=Round(Random(20)-10);

    LabelWind.Caption:='Windstärke: '+IntToStr(Wind);

    with ControlbarWind.Picture.Bitmap.Canvas do
    begin
        Pen.Color:=clWhite;
        Brush.Color:=clWhite;
        Rectangle(1,1,200,20);

        Pen.Color:=clBlue;
        Brush.Color:=clBlue;

        if Wind>0 then Rectangle(100,1,100+Wind*10,20);
        if Wind<0 then Rectangle(100+Wind*10,1,100,20);
    end;

    LabelPanzerungRot.Caption:='Panzer Rot:'+IntToStr(PanzerRot.Panzerung);
    LabelPanzerungBlau.Caption:='Panzer Blau:'+IntToStr(PanzerBlau.Panzerung);

    if Spieler='rot' then
    begin
        TrackbarWinkel.Position:=180-PanzerRot.Winkel;
        TrackbarGeschwindigkeit.Position:=PanzerRot.Geschwindigkeit;
        Panel1.Color:=RGB(255,50,50);
    end;

    if Spieler='blau' then
    begin
        TrackbarWinkel.Position:=180-PanzerBlau.Winkel;
        TrackbarGeschwindigkeit.Position:=PanzerBlau.Geschwindigkeit;
        Panel1.Color:=RGB(100,100,255);
    end;
end;

end.

Na das ist doch schon echt was. Wenn wir das Programm starten, dann haben wir einen aktiven Spieler (=Farbe des Panels) und beim Einstellen des Winkels sollte sich das Kanonenrohr des entsprechenden Panzers bewegen.
user defined image
Wer zu faul zum Tippen ist, hier ist das komplette Projekt bis zu diesem Entwicklungsstand.
home.arcor.de/origam...r/tut/tutorial02.zip
Einloggen, um Attachments anzusehen!


Zuletzt bearbeitet von battledevil am Mi 23.11.05 12:33, insgesamt 3-mal bearbeitet
battledevil Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 96

WinXP, Win7
C#, C++, VBNET
BeitragVerfasst: Di 22.11.05 12:07 
Titel: Re: Spieletutorial für Einsteiger - Artillery Teil3
3.3. Physik für Programmierer
Der nächste Abschnitt wäre die Eingabe von Winkel und Geschwindigkeit durch den Spieler. Das ist aber schon sogut wie komplett, da die Oberfläche ja schon fertig ist. Wir speichern die Werte, wenn der Spieler auf den Feuer-Button drückt und dann kommen wir auch schon zur Berechnung der Flugbahn des Geschosses.
Für die Flugbahn, auch Wurfparabel genannt, brauchen wir noch ein paar globale Variablen:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
var x0, y0, geschwindigkeit : integer;
    winkel, zeit : double;

const gravitation=9.81;

Die Variablen x0 und y0 sind der Startpunkt der Flugbahn des Geschosses, das ist das Ende das Kanonenrohres des Panzers des aktiven Spielers. Die Variablen für geschwindigkeit, winkel und zeit sind für die Formel der Flugbahn notwendig, ich werde sie noch näher erklären.
Die Konstante gravitation gibt die Beschleunigung des Geschosses durch die Gravitationskraft an. Der Wert von 9.81 ist ein Durchschnittswert für die Erdanziehungskraft.
Damit das Geschoss richtig schön fliegt, müssen wir diese Aktion über einen Timer laufen lassen, der alle paar Millisekunden die neue Position des Geschossen berechnet und es zeichnet. Also legen wir eine neue Timer-Komponente in die Form. Ich nenne Sie TimerFlugbahn. Die Enabled-Eigenschaft wird auf False gesetzt, Interval auf 100.
Nun kümmern wir uns um den Feuer-Button. Die Prozedur des OnClick-Ereignisses sieht wie folgt aus:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
procedure TForm1.ButtonFeuerClick(Sender: TObject);
begin
    if Spieler='rot' then
    begin
        PanzerRot.Winkel:=180-TrackbarWinkel.Position;
        PanzerRot.Geschwindigkeit:=TrackbarGeschwindigkeit.Position;
        winkel:=degtorad(PanzerRot.Winkel);
        geschwindigkeit:=PanzerRot.Geschwindigkeit;
        x0:=round(cos(winkel)*10+PanzerRot.X);
        y0:=round(-sin(winkel)*10+PanzerRot.Y-4);
    end;

    if Spieler='blau' then
    begin
        PanzerBlau.Winkel:=180-TrackbarWinkel.Position;
        PanzerBlau.Geschwindigkeit:=TrackbarGeschwindigkeit.Position;
        winkel:=degtorad(PanzerBlau.Winkel);
        geschwindigkeit:=PanzerBlau.Geschwindigkeit;
        x0:=round(cos(winkel)*10+PanzerBlau.X);
        y0:=round(-sin(winkel)*10+PanzerBlau.Y-4);
    end;

    zeit:=0;

    TimerFlugbahn.Enabled:=true;
end;

Zeile 3-11: Falls der rote Spieler dran ist, werden dessen eingestellter Winkel und Geschwindigkeit in den Variablen von PanzerRot und in winkel und geschwindigkeit gespeichert. Der Winkel wird gleich mit DegToRad ins Bogenmaß umgerechnet. Dann wird mit x0 und y0 der Startpunkt der Flugbahn des Geschosses berechnet, die Formeln entsprechen denen für die Berechnung des Kanonenrohres.
Zeile 13-21: Gleiches gilt für den blauen Spieler.
Zeile 23: Die Zeitvariable zur Flugbahnberechnung wird auf Null zurückgesetzt.
Zeile 24: Der Timer wird aktiviert.

Mehr ist an dieser Stelle nicht zu machen, die ganze Berechnung und Anzeige des Geschosses wird im Timer-Ereignis durchgeführt.
Wir brauchen eine Formel für die Berechnung der Flugbahn. Das Tafelwerk oder ein Physikbuch kann uns da weiterhelfen.
Dort steht, dass wir die Position eines Geschosses wie folgt berechnen können:

x = geschwindigkeit * cosinus(winkel) * zeit
y = geschwindigkeit * sinus(winkel) * zeit - 0.5 * gravitation * zeit²

Es gibt noch eine andere Formel, bei der y in Abhängigkeit von x berechnet wird. Sie ist jedoch ungeeignet, da sie für den Wert winkel=90° nicht lösbar ist und durch die Abhängigkeit nur eine begrenzte Genauigkeit für die Berechnung liefert.
In den Gleichungen kommt der Wind nicht vor. Und das aus gutem Grund. Will man eine realistische Flugbahnberechnung mit Wind und Luftwiderstand usw. durchführen, dann wird die ganze Sache unnötig verkompliziert, da z.B. die Form des Geschosses eine Rolle spielt, die Dichte der Luft und einiges mehr. Das ist alles für dieses kleine Spiel unwichtig. Deswegen wird der Wind einfach wie eine weitere Kraft behandelt, die auf das Geschoss einwirkt. Entsprechend der Gravitationskraft, die mit - 0.5 * gravitation * zeit² in die Gleichung für y eingeht, wird der Wind mit + 0.5 * wind * zeit² zur Gleichung für x hinzugefügt, denn er soll ja horizontal wehen.
Außerdem müssen wir noch den Startpunkt, also das Ende des Kanonenrohres, zur Gleichung hinzufügen.
damit erhalten wir:

x = geschwindigkeit * cosinus(winkel) * zeit + 0.3 * wind * zeit² + startx
y = geschwindigkeit * sinus(winkel) * zeit - 0.5 * gravitation * zeit² - starty

Warum -starty ? Das liegt daran, dass das karthesische Koordinatensystem nicht mit unserem grafischen Koordinatensystem in Delphi übereinstimmt, da müssen die Koordinaten umgekehrt werden. Ich hab den Faktor beim Wind kleiner als für die Gravitation gewählt, damit der Wind nicht zu starken Einfluss auf die Flugbahn hat. Ihr könnt ja im fertigen Programm mal mit verschiedenen Faktoren experimentieren.
Jetzt reicht es aber mit der Theorie, kommen wir zur Praxis und damit zur OnTimer Prozedur von ZimerFlugbahn:
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
procedure TForm1.TimerFlugbahnTimer(Sender: TObject);
var x,y : integer;
begin
    zeit:=zeit+0.08;

    x := Round(x0 + geschwindigkeit*cos(winkel)*zeit + wind*zeit*zeit*0.3);
    y := Round(y0 - (geschwindigkeit*sin(winkel)*zeit - 0.5*gravitation*zeit*zeit));

    with Geschoss.Canvas do
    begin
        CopyRect(Rect(0,0,5,5),Buffer.Canvas,Rect(x-2,y-2,x+2,y+2));
        Pen.Color:=RGB(0,0,0);;
        Brush.Color:=RGB(255,0,0);
        Ellipse(1,1,4,4);
    end;

    with Image1.Canvas do
    begin
        CopyRect(Rect(Bahn_alt.x-2,Bahn_alt.y-2,Bahn_alt.x+2,Bahn_alt.y+2),Buffer.Canvas,Rect(Bahn_alt.x-2,Bahn_alt.y-2,Bahn_alt.x+2,Bahn_alt.y+2));
        Draw(x-2,y-2,Geschoss);
    end;

    Bahn_alt.x:=x;
    Bahn_alt.y:=y;

    if (x<0or (x>800then
    begin
        TimerFlugbahn.Enabled:=false;
        Spielerwechsel(); exit;
    end;
    if (y>600then
    begin
        TimerFlugbahn.Enabled:=false;
        Explosion(x,y); exit;
    end;

    if (x>PanzerRot.X-8and (x<PanzerRot.X+8and (y>PanzerRot.Y-4and (y<PanzerRot.Y+4then
    begin
        TimerFlugbahn.Enabled:=false;
        Explosion(x,y); exit;
    end;
    if (x>PanzerBlau.X-8and (x<PanzerBlau.X+8and (y>PanzerBlau.Y-4and (y<PanzerBlau.Y+4then
    begin
        TimerFlugbahn.Enabled:=false;
        Explosion(x,y); exit;
    end;

    if (Landschaft.Canvas.Pixels[x,y]<>clWhite) and (x>0and (x<800and (y>0and (y<600then
    begin
        TimerFlugbahn.Enabled:=false;
        Explosion(x,y); exit;
    end;
end;


Zeile 4: Hier wird die Variable für die Zeit hochgezählt. Je schneller sie hochgezählt wird, desto größer die Schritte in der Berechnung. Dadurch wird das Geschoss im Spiel schneller, aber die Auflösung der Flugbahn und damit die Qualität der Kollisionserkennung geringer. Ich halte Werte zwischen 0.5 und 1 für sinnvoll.
Zeile 6 und 7: Die schon angesprochenen Formeln zur Berechnung der Flugbahn des Geschosses.
Zeile 9-15: Überraschung! Das Geschoss bekommt auch ein eigenes Bitmap. Wie alle bisherigen Bitmaps wird es erzeugt, die Höhe und Breite habe ich auf 5 festgelegt. Aber zurück zum Code. Zuerst wird der Hintergrund an der Position des Geschosses aus der Buffer-Bitmap in die Geschoss-Bitmap kopiert. Dann wird das Geschoss mit Ellipse eingezeichnet.
Zeile 17-21: Was hier mit dem Image.Canvas passiert, bedarf einer Erklärung. In Zeile 19 wird ein Stück Hintergrund aus dem Buffer auf die Image kopiert und zwar an der Stelle, wo sich im letzten Timerdurchlauf das Geschoss befand. Es wird also gelöscht. Für diesen Vorgang brauche ich eine globale TPoint-Variable Bahn_alt, die die Position der letzten Berechnung speichern. In Zeile 20 wird das Geschoss an der neu berechneten Position in das Image kopiert.
Zeile 23+24: Hier wird die berechnete Position in Bahn_alt zwischengespeichert.

Zeile 26-35: Ab hier beginnt die Kollisionsabfrage. Zuerst wird nachgeschaut, ob das Geschoss das Spielfeld verlassen hat, indem ich die linke, rechte und untere Grenze überprüfe. Der obere Bildschirmrand wird nicht gecheckt, da das Geschoss ja wieder runterkommen soll. Sollte das Geschoss das Spielfeld links oder rechts verlassen haben, dann stoppe ich den Timer und rufe die Prozedur Spielerwechsel auf, trifft es den unteren Bildschirmrand, dann soll es explodieren. Die Prozeduren Spielerwechsel und Explosion werden wir nachher gleich anlegen.
Zeile 37-46: Hier wird überprüft, ob das Geschoss einen der beiden Panzer trifft. Wenn das der Fall sein sollte, dann wird der Timer abgeschalten und die Prozedur Explosion aufgerufen. Sie wird ebenfalls später erklärt, aber ihr könnt schon mal raten, was sie wohl macht...
Zeile 48-52: Die letzte Kollisionsabfrage überprüft anhand der Landschaftsbitmap, ob das Geschoss die Landschaft getroffen hat, indem ich den Farbwert der Landschaft an der Stelle des Geschosses abfrage. Sollte das der Fall sein, wird der Timer abgeschalten und die Prozedur Explosion aufgerufen.

Damit wir die Flugbahn mal testen können, müssen wir zuerst noch die zwei neuen Prozeduren anlegen, die im Timer aufgerufen werden, die Prozedur Spielerwechsel und die Prozedur Explosion.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
procedure TForm1.Explosion(x,y:integer);
begin
    Spielerwechsel();
end;


procedure TForm1.Spielerwechsel();
begin
    if Spieler='rot' then Spieler:='blau'
    else if Spieler='blau' then Spieler:='rot';

    SpielfeldZeichnen();
    AnzeigeAktualisieren();
end;

Um die eigentliche Explosion und die Schadensverteilung können wir uns später kümmern, deswegen rufe ich in der Prozedur Explosion vorerst nur die Prozedur Spielerwechsel auf. Diese ist schnell programmiert. Die globale Spieler-Variable wird einfach auf die andere Farbe umgestellt, dann wird das Spielfeld neu gezeichnet und die Anzeige aktualisiert.
Wenn wir das Programm jetzt starten, dann sollten die Panzer schon aufeinander schießen können.

Eine kleine Sache noch: Wenn ein Spieler auf der Feuer-Button klickt, dann deaktiviere ich ihn, damit er nicht mehrfach draufklicken kann. erst wenn der andere Spieler dran ist, wird der Button wieder aktiviert. Dazu muß in der Prozedur ButtonFeuerClick folgende Zeile eingefügt werden, um den Button zu deaktivieren.
ausblenden Delphi-Quelltext
1:
    ButtonFeuer.Enabled:=false;					

Und in der Prozedur AnzeigeAktualisieren wird der Button wieder aktiviert.
ausblenden Delphi-Quelltext
1:
    ButtonFeuer.Enabled:=true;					

Hier ist der gesamte Quellcode: home.arcor.de/origam...r/tut/tutorial03.zip
Einloggen, um Attachments anzusehen!
battledevil Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 96

WinXP, Win7
C#, C++, VBNET
BeitragVerfasst: Mi 23.11.05 18:23 
Titel: Re: Spieletutorial für Einsteiger - Artillery Teil3
3.4. - Badabumm!
Nun sind wir fast schon durch mit diesem Tutorial. Es fehlen noch die Explosionen, die Schadenszuweisung, die Anzeige des Gewinners und ein paar weitere Kleinigkeiten.
Die Explosion will ich animieren, deswegen brauchen wir einen zweiten Timer, den ich TimerExplosion nenne. Die Eigenschaft Enabled wird wieder auf False gesetzt, Interval auf 20. Zusätzlich brauchen wir noch die Koordinaten als globale Variable:
ausblenden Delphi-Quelltext
1:
    ExKoord : TPoint;					

Die Prozedur Explosion müssen wir umschreiben, damit der Timer gestartet wird und die Koordinaten global gespeichert werden.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
procedure TForm1.Explosion(x,y:integer);
begin
    ExKoord.X:=x;
    ExKoord.Y:=y;

    TimerExplosion.Enabled:=true;
end;

Nun zum Timer.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
procedure TForm1.TimerExplosionTimer(Sender: TObject);
begin
    TimerExplosion.Tag:=TimerExplosion.Tag+1;

    with Image1.Canvas do
    begin
        Pen.Color:=clRed;
        Brush.Color:=clRed;
        Ellipse(ExKoord.X-TimerExplosion.Tag,ExKoord.Y-TimerExplosion.Tag,ExKoord.X+TimerExplosion.Tag,ExKoord.Y+TimerExplosion.Tag)
    end;

    if TimerExplosion.Tag=20 then
    begin
        TimerExplosion.Enabled:=false;
        TimerExplosion.Tag:=0;

        with Landschaft.Canvas do
        begin
            Pen.Color:=clWhite;
            Brush.Color:=clWhite;
            Ellipse(ExKoord.X-20,ExKoord.Y-20,ExKoord.X+20,ExKoord.Y+20);
        end;

        SchadenBerechnen();
        PositionNeuBerechnen();
        Spielerwechsel();
    end;
end;

Zeile 3: Ich benutze den Tag des Timers für die Größe der Explosion, deshalb wird er erhöht.
Zeile 5-10: Hier wird die Explosion als roter Kreis gezeichnet.
Zeile 12: Die Maximalgröße der Explosion setze ich auf 20 fest
Zeile 14+15: Wenn die Maximalgröße der Explosionsgröße erreicht ist, wird der Timer abgeschaltet und der Tag zurück auf null gesetzt.
Zeile 17-22: Dann wird das Loch der Explosion in die Landschaftsbitmap eingezeichnet, schließlich soll es uns erhalten bleiben.
Zeile 24: Die Schadensberechnung wird in einer eigenen Prozedur behandelt.
Zeile 25: Die Position der Panzer muss neu berechnet werden, es kann ja sein, dass der Boden unter ihm weggeschossen wird.
Zeile 26: Dann ist der andere Spieler and der Reihe.

Wir brauchen also zwei weitere Prozeduren, SchadenBerechnen und PositionNeuBerechnen.
SchadenBerechnen wir die Entfernung zwischen der Explosion und der Position der Panzer berechnen und dann in Abhängigkeit von dem ermittelten Wert dem Panzer Schaden zuweisen oder nicht.
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
procedure TForm1.SchadenBerechnen();
var abstand : double;
begin
    abstand:=sqrt(sqr(abs(PanzerRot.X-ExKoord.X))+sqr(abs(PanzerRot.Y-ExKoord.Y)));
    if abstand<=23 then PanzerRot.Panzerung:=PanzerRot.Panzerung-25;

    abstand:=sqrt(sqr(abs(PanzerBlau.X-ExKoord.X))+sqr(abs(PanzerBlau.Y-ExKoord.Y)));
    if abstand<=23 then PanzerBlau.Panzerung:=PanzerBlau.Panzerung-25;

    if PanzerRot.Panzerung<=0 then
    begin
        if MessageDlg('Blau hat das Spiel gewonnen. Neues Spiel?',mtConfirmation, [mbYes, mbNo], 0) = mrno then close
        else
        begin
            Landschaftsgenerator();
            PositionGenerieren();
            SpielfeldZeichnen();
            Startspieler();
            AnzeigeAktualisieren();
        end;
    end;

    if PanzerBlau.Panzerung<=0 then
    begin
        if MessageDlg('Rot hat das Spiel gewonnen. Neues Spiel?',mtConfirmation, [mbYes, mbNo], 0) = mrno then close
        else
        begin
            Landschaftsgenerator();
            PositionGenerieren();
            SpielfeldZeichnen();
            Startspieler();
            AnzeigeAktualisieren();
        end;
    end;
end;

Der Abstand zwischen Explosion und Panzer wird über den Satz von Pythagoras berechnet. Den Schaden hab ich auf 25 festgelegt, also sind 4 Treffer notwendig, um einen Panzer abzuschießen. Hier wird auch gleich überprüft, ob ein Panzer zerstört wurde. Sollte das der Fall sein, wird in einer Messagebox der Gewinner angezeigt und bei Wusch ein neues Spiel gestartet.

Die Prozedur PositionNeuBerechnen überprüft, ob die Panzer noch auf der Landschaft sitzen oder der Boden unter ihnen weggeschossen wurde. Sollte das der Fall sein, dann wird die Position neu berechnet. Die Vorgehensweise entspricht der in der Prozedur PositionGenerieren.
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
procedure TForm1.PositionNeuBerechnen();
var i:integer;
begin
    if (Landschaft.Canvas.Pixels[PanzerRot.X,PanzerRot.Y+3]=clWhite) and (PanzerRot.Y+3<600then
    begin
        for i:=PanzerRot.Y+3 to 600 do
        if (Landschaft.Canvas.Pixels[PanzerRot.X,i]<>clWhite) or (i=600then
        begin
            PanzerRot.Y:=i;
            break;
        end;
    end;

    if (Landschaft.Canvas.Pixels[PanzerBlau.X,PanzerBlau.Y+3]=clWhite) and (PanzerBlau.Y+3<600then
    begin
        for i:=PanzerBlau.Y+3 to 600 do
        if (Landschaft.Canvas.Pixels[PanzerBlau.X,i]<>clWhite) or (i=600then
        begin
            PanzerBlau.Y:=i;
            break;
        end;
    end;
end;

Damit wäre das Programm eigentlich fertig. Aber es gibt da noch ein paar kleine Schönheitskorrekturen, die wir vornehmen.

Das Kanonenrohr wird erst dann angezeigt, wenn der aktive Spieler den Winkel verändert. Damit das Rohr schon vorher gezeichnet wird, rufe ich die Prozedur TrackbarWinkelChange in der Prozedur SpielfeldZeichnen auf:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
procedure TForm1.SpielfeldZeichnen();
var i:integer;
begin

    ...

    Panzerzeichnen('blau');
    Buffer.Canvas.Draw(PanzerBlau.X-7,PanzerBlau.Y-3,Panzer);

    Image1.Picture.Bitmap:=Buffer;

    TrackbarWinkelChange(nil);
end;

Eine weitere Sache, die noch stört sind die Farben der Trackbars. Sie werden nicht immer korrekt in der Farbe des aktiven Spielers angezeigt. Dummerweise haben die Trackbars keine Color-Eigenschaft, die wir einstellen könnten. Aber zum Glück genügt es, dass die Trackbars den Fokus bekommen, damit sie die farbe des panels annehmen. Wir erweitern die AnzeigeAktualisieren-Prozedur:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
procedure TForm1.AnzeigeAktualisieren();
begin

    ...

    if Spieler='blau' then
    begin
        TrackbarWinkel.Position:=180-PanzerBlau.Winkel;
        TrackbarGeschwindigkeit.Position:=PanzerBlau.Geschwindigkeit;
        Panel1.Color:=RGB(100,100,255);
    end;

    if Form1.Visible=true then
    begin
        TrackbarWinkel.SetFocus;
        TrackbarGeschwindigkeit.SetFocus;
    end;
end;

So, geschafft! Es ist durch den Wind nicht ganz einfach, den gegnerischen Panzer zu treffen, wem es zu schwierig ist, der kann ja den Wind aus der Formel rausnehmen.
user defined image
Hier das komplette Projekt zum Runterladen und Ausprobieren,
Viel Spaß!
home.arcor.de/origam...t/tutorial-final.zip
Einloggen, um Attachments anzusehen!