OGL_ISS - International Space Station in Danger
OGL_ISS-Tutorial von Daniel Schwamm (18.08.2008)
Inhalt
Ihr erinnert euch? Beim
"OGL_Planets"-Tutorial
habe ich bedauert, dass es angesichts der gewaltigen Dimensionen des
Sonnensystems nicht möglich war, Objekte darzustellen, deren Ausmasse
kleiner als einen Kubikkilometer sind. Denn damit kam der verflixte
Tiefenpuffer von OpenGL nicht zurecht. Solche
Objekte verschwanden einfach, wenn man sich ihnen zu weit näherte.
Das war schade. Denn gerne hätte ich der Erde neben dem Mond noch einen
weiteren Trabanten gegönnt, nämlich die ISS. Klein und unscheinbar
sollte sie dort als der entfernteste Aussenposten der Menschheit still ihre
Kreise ziehen.
Echt ärgerlich, dass das nicht geklappt hat. Wo sie doch so schön
anzuschauen ist:
Zum Glück muss man als Programmierer mit solchem Ärger nicht leben.
Wenn es eine ISS sein soll, dann bastelt man sich halt eine. Dazu musste nur
der innere Schweinehund überwunden werden.
Wachte dann mal wieder zu früh auf, so um 6 Uhr herum. Passiert mir
im Sommer öfter. An Schlaf war nicht mehr zu denken. Und noch zwei elend
lange Stunden Zeit, bevor es zur Arbeit ging. Also, was tun?
Wie üblich gab es Kaffee, Kippen und Platzierung am PC. Das Ding habe ich
mir "um" mein Zweit-Bett gebaut, was kuschelig-gemütliches Programmieren
ermöglicht.
Daniels Home-PC: Kaffee, Zigaretten, Bett, Computer und ewig flimmernder Monitor. Hier lässt es sich gemütlich, aber intensiv und störungsfrei arbeiten.
Es ging online. Zu Wikipedia. Zufällige Artikel. Und landete so
beim Lemma
Internationale Raumstation.
Die ISS. Da ist sie wieder. Da war doch noch was ...
Die Kombination aus Erinnerung, neuen Informationen, PC und zu viel Zeit
liess mich ein neues Projekt in Delphi beginnen.
Bis es zur Arbeit ging, war die Erde weitgehend fertig. Die ISS eine
simple Sphäre. Eine Flugsteuerung noch nicht vorhanden.
Drei Tage später konnte man sich bereits virtuell durch die verschiedenen,
345 km über dem Erdboden schwebenden, Module meiner Raumstation bewegen.
Noch einmal fünf Tage später war die Fliegerei im 3-dimensionalen Raum
hinreichend optimiert und alle Elemente mit Texturen versehen.
Nur Herumfliegen und -gucken war aber langweilig. Also wurde ein Spiel
eingebaut. Mit Phaserfeuer, Explosionen, Soundeffekten, Level und
Highscore-Listen.
Nach zwei Wochen Programmierrausch war die Sache dann gegessen.
Jetzt fehlte nur noch das obligatorische Tutorial ...
Wie schon
"OGL_Henrys"
und
"OGL_Planets"
wurde "OGL_ISS" in DelphiGL programmiert.
Das Projekt besteht aus drei Units:
- DanGeoU.pas: Funktionssammlung zur Vektor- und Matrizen-Verwaltung
- SoundsU.pas: const-Arrays mit den WAV-Daten für die Sound-Effekte
- HauptU.pas: Eigentlicher Programm-Code von "OGL_ISS"
Trotz mehrerer OpenGL-Programme auf dem Kerbholz habe ich mich stets
gekonnt vor der Mathematik dahinter gedrückt.
Das war bei "OGL_ISS" leider nicht drin. Punktgenaues Navigieren in 3D
ohne etwas Ahnung von Trigonometrie ist ... frustrierend. Da kommt man
einfach nicht vom Fleck. Ich weiss es genau, denn ich habe Tage damit
verschwendet, durch wüstes Trial-and-Error mit Sinus- und Kosinus-Kombinationen
irgendetwas Sinnvolles hinzubekommen.
Also hiess es hinsetzen und lernen. Was ist ein Vektor? Was bringen mir
Matrizen? Kann ich die für Rotationen einsetzen? Es entstand eine kleine
Sammlung von Typen und Funktionen, die in der Unit "DanGeoU.pas" verpackt
wurde.
Fangen wir mit den Konstanten und Typen an:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
unit dangeou;
interface
uses math;
const
_rad2deg=180/pi;
_deg2rad=pi/180;
type
//vector
Tv=record
x,y,z:single;
end;
//matrix
Tm=array[0..2,0..2]of single;
Winkel lassen sich bekanntlich in Grad und im Bogenmass angeben.
Winkelmasse: Winkel lassen sich im Grad- und Bogenmass angeben. Ein Mass lässt sich aber jederzeit leicht in das andere Mass umrechnen.
Bogenmass-Winkel repräsentieren Längen von Einheitskreisabschnitten.
Sie sind mathematisch einfacher zu bearbeiten als Grad-Winkel,
da hier keine "künstlichen Grenzwerte" wie etwa der 360 Grad-Winkel
berücksichtigt werden müssen.
Das dürfte auch der Grund sein, weshalb bei den meisten OpenGL-Funktionen
Bogenmass-Winkel als Parameter erwartet werden. Gleiches gilt für die
"DanGeoU.pas". Sollte eine Prozedur dennoch Grad-Winkel erwarten, dann
verfügt sie über das Postfix "_deg" im Funktionsnamen.
Mit Hilfe der Konstanten "_rad2deg" und "_deg2rad" können Winkel
leicht von einem Format in das andere umgerechnet werden:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
//converts the radian number to the equivalent number in degrees
function rad2deg(rad:single):single;
begin
result:=rad*_rad2deg;
end;
//converts the degree number to the equivalent number in radians
function deg2rad(deg:single):single;
begin
result:=deg*_deg2rad;
end;
//converts any degree number to a 'normed' angle between 0-359
function deg_norm(d:single):single;
begin
if d>360 then d:=d-360;
if d<0 then d:=360+d;
if d=360 then d:=0;
result:=d;
end;
Der Typ "Tv" beschreibt Vektoren. Einen Vektor kann man als
Punkt im n-dimensionalen Raum interpretieren oder als ein vom
Ursprung ausgehender Pfeil, der eine Richtung besitzt.
Die Vektoren, die in der "DanGeoU.pas" Verwendung finden, gelten
ausschliesslich für n=3, also den 3-dimensionalen Raum mit
den Achsen X, Y und Z:
Die x-/y-/z-Achsen des OpenGL-Raumes: Man beachte hierbei, dass die z-Achse bei OpenGL auf den Beobachter
hin ausgerichtet ist (anders als bei Direct3D). Will man also einen
Punkt tiefer in den Raum hinein wandern lassen, muss der z-Wert der Koordinaten
negiert vergrössert werden.
Intern arbeitet OpenGL mit 4-dimensionalen Vektoren, wobei die vierte
Dimension, die w-Achse, etwas mit der Projektion des Ergebnisses auf
dem Monitor zu tun hat, und uns hier nicht weiter interessieren muss.
Auf Vektoren lassen sich verschiedene, wohldefinierte Operationen anwenden.
Beginnen wir mit Plus und Minus:
Addition und Subtraktion von Vektoren: Geometrische Veranschaulichung der Wirkung von Addition (oben) und
Subtraktion (unten) von Vektoren, hier zum leichteren Verständnis nur
im zweidimensionalen Raum wiedergegeben.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
//set vector position
function v_fill(x,y,z:single):Tv;
begin
result.x:=x;
result.y:=y;
result.z:=z;
end;
//add two vectors
function v_add(v1,v2:Tv):Tv;
begin
Result:=v_fill(v1.x+v2.x,v1.y+v2.y,v1.z+v2.z);
end;
//subtract two vectors
function v_sub(v1,v2:tv):tv;
begin
Result:=v_fill(v1.x-v2.x,v1.y-v2.y,v1.z-v2.z);
end;
Die Länge eines Vektors (oder auch sein Betrag) lässt sich
mithilfe des Satzes von Pythagoras ermitteln:
Länge eines Vektors: Die Länge eines Vektors lässt sich mithilfe des Satzes von Pythagoras ermitteln.
Der Vektor fungiert dabei als Hypotenuse eines gleichschenkligen Dreiecks.
00001
00002
00003
00004
00005
//length of vector
function v_len(v:Tv):Single;
begin
result:=sqrt(v.x*v.x+v.y*v.y+v.z*v.z);
end;
Teilt man die Werte eines Vektors durch seine Länge, erhält man den
auf den Einheitskreis normierten Vektor. Das ist nützlich, wenn
nur die Richtung eines Vektors interessiert, nicht aber seine
Position im Raum.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
//normalize vector to length=1
function v_norm(var v:tv):tv;
var
len:single;
begin
len:=v_len(v);if len=0 then len:=1;
result.x:=v.x/len;
result.y:=v.y/len;
result.z:=v.z/len;
end;
Um einen Vektor in gegebener Richtung zu verlängern, zu verkürzen
oder zu negieren bietet sich die Skalierung an:
00001
00002
00003
00004
00005
00006
00007
//make vector longer/shorter
function v_scale(v:Tv;sf:Single):tv;
begin
result.x:=v.x*sf;
result.y:=v.y*sf;
result.z:=v.z*sf;
end;
Den Winkel zwischen zwei Vektoren findet man mithilfe der
Vektor-Länge (siehe weiter oben), dem Vektor-Punkt-Produkt
und einer schicken kleinen Formel:
Winkel zwischen zwei Vektoren
|
Punktprodukt=
(v1.x*v2.x+v1.y*v2.y)=
(1*2+2*1)=
4
Länge v1=
wurzel(v1.x*v1.x+v1.y*v1.y)=
wurzel(2*2+1*1)=
wurzel(5)
Länge v2=v1
Winkel a =
arcos( 4/(wurzel(5)*wurzel(5)) )=
arcos( 4/5 ) ~
36.8 Grad
|
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
{
vector-dot-product:
is needed to calculate the angle between two vectors
}
function v_dot(v1,v2:tv):single;
begin
result:=v1.x*v2.x+v1.y*v2.y+v1.z*v2.z;
end;
//calculate angle between two vectors
function v_angle(v1,v2:tv):single;
var
vdot,vlen1,vlen2:single;
begin
vdot:=v_dot(v1,v2);
vlen1:=v_len(v1);
vlen2:=v_len(v2);
result:=arccos(vdot/(vlen1*vlen2));
end;
function v_angle_deg(v1,v2:tv):single;
begin
result:=rad2deg(v_angle(v1,v2));
end;
Das Vektor-Kreuz-Produkt ist hilfreich, um die Senkrechte
einer Fläche zu ermitteln, die durch zwei Vektoren aufgespannt
wird. Die Rechte-Hand-Regel zeigt uns, in welche Richtung
die resultierende Senkrechte weisen wird. Es gilt:
Vektor-Kreuz-Produkt: Mithilfe des Vektor-Kreuz-Produkts kann die Senkrechte der durch
zwei Vektoren aufgespannten Fläche im dreidimensionalen Raum berechnet werden.
|
- Zeigefinger in v1-Richtung
- Mittelfinger in v2-Richtung
- Daumen ergibt Senkrechte
|
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
{
vector cross product:
=> normale of the plane given by v1 and v2
change of v1 and v2 => negative vector
right hand rule:
v1=trigger finger
v2=middle finger,
=> cross=thumb
}
function v_cross(v1,v2:tv):tv;
begin
result.x:=v1.y*v2.z-v1.z*v2.y;
result.y:=v1.z*v2.x-v1.x*v2.z;
result.z:=v1.x*v2.y-v1.y*v2.x;
end;
Für die Rotation eines Vektors um die Achsen des
3-dimensionalen Raumes gelten die folgenden Formeln:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
//rotate vector about x-axis
function v_rot_x(v:tv;rad:extended):tv;
var
asin,acos:extended;
begin
math.SinCos(rad,asin,acos);
result.x:=v.x;
result.y:=v.y*acos-v.z*asin;
result.z:=v.y*asin+v.z*acos;
end;
function v_rot_x_deg(v:tv;deg:extended):tv;
begin
result:=v_rot_x(v,deg2rad(deg));
end;
//rotate vector about y-axis
function v_rot_y(v:tv;rad:extended):tv;
var
asin,acos:extended;
begin
math.SinCos(rad,asin,acos);
result.x:=v.x*acos+v.z*asin;
result.y:=v.y;
result.z:=-v.x*asin+v.z*acos;
end;
function v_rot_y_deg(v:tv;deg:extended):tv;
begin
result:=v_rot_y(v,deg2rad(deg));
end;
//rotate vector avout z-axis
function v_rot_z(v:tv;rad:extended):tv;
var
asin,acos:extended;
begin
math.SinCos(rad,asin,acos);
result.x:=v.x*acos-v.y*asin;
result.y:=v.x*asin+v.y*acos;
result.z:=v.z;
end;
function v_rot_z_deg(v:tv;deg:extended):tv;
begin
result:=v_rot_z(v,deg2rad(deg));
end;
Wie erklären sich diese Formeln? Wie kommen zum Beispiel all
die Sinus- und Kosinus-Werte in die Berechnungen?
Sinus-Kurve: Die trigonometrische Sinus-Funktion durchläuft den Ursprung bei 0 und hat
ihre Minima und Maxima jeweils bei Vielfachen von PI/2.
Kosinus-Kurve: Die trigonometrische Kosinus-Funktion durchläuft den Ursprung bei 1 und hat
ihre Minima und Maxima jeweils bei Vielfachen von PI.
Die Trigonometrie lehrt uns folgenden Zusammenhang zwischen
den y- und y-Koordinaten, wenn ein Vektor um die z-Achse mit
dem Winkel "a" rotiert wird:
Rotation eines Vektors um die z-Achse: Wird ein Vektor (x/y) um die z-Achse rotiert, dann ergeben sich die neuen
Koordinaten anhand einer Kombination aus Radius (Vektorlänge), Sinus und Kosinus.
Wird nun der Winkel "a" um einen Winkel "b" ergänzt, dann gilt für die
neuen Koordinaten "xneu" und "yneu":
Erweiterte Rotation eines Vektors um die z-Achse: Wird ein Vektor /x/y) um die Winkel 'a' und 'b' rotiert, dann können
die neuen Koordinaten (xneu/yneu) mittels einer Formel berechnet werden.
Damit lässt sich nach "xneu" und "yneu" auflösen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
cos(a) = x/r
sin(a) = y/r
cos(a+b) = cos(a)*cos(b) - sin(a)*sin(b)
sin(a+b) = sin(a)*cos(b) + cos(a)*sin(b)
cos(a+b) = xneu/r
sin(a+b) = yneu/r
-------
xneu = r* cos(a+b)
= r*( cos(a)*cos(b) - sin(a)*sin(b) )
= r* cos(a)*cos(b) - r*sin(a)*sin(b)
= r* (x/r) *cos(b) - r*(y/r) *sin(b)
= x *cos(b) - y *sin(b)
yneu = r* sin(a+b)
= r*( sin(a)*cos(b) + cos(a)*sin(b) )
= r* sin(a)*cos(b) + r*cos(a)*sin(b)
= r* (y/r) *cos(b) + r*(x/r) *sin(b)
= y *cos(b) + x *sin(b)
= x *sin(b) + y *cos(b)
Entsprechend kann für die Rotationen um die x- und y-Achse verfahren werden.
Zu beachten ist, dass die Reihenfolge der Rotationen nicht
einerlei ist. Es dauerte eine Weile, bis mir das klar war.
Die folgenden 90 Grad-Rotationen um verschiedenen Achsen mögen dies
illustrieren:
Vektorrotation um die Achsen y, z und x: Rotiert man einen Vektor mit jeweils 90 Grad in der Reihenfolge y-, z- und x-Achse,
dann weist der Vektor letztlich wieder in die ursprüngliche Richtung.
Vektorrotation um die Achsen z, y und x: Rotiert man einen Vektor dagegen mit jeweils 90 Grad in der Reihenfolge
z-, y- und x-Achse, dann weist der Vektor in eine andere Richtung als
der ursprüngliche Vektor.
Um die Distanz zwischen zwei "Punkt"-Vektoren zu ermitteln,
hilft einmal mehr der Satz von Pythagoras:
00001
00002
00003
00004
00005
00006
00007
00008
00009
//distance between two points
function v_distance(v1,v2:tv):single;
begin
result:=sqrt(
(v1.x-v2.x)*(v1.x-v2.x)+
(v1.y-v2.y)*(v1.y-v2.y)+
(v1.z-v2.z)*(v1.z-v2.z)
);
end;
Weil das Rechnen mit Sinus und Kosinus irrationale Zahlen
hervorbringt, bleiben Rundungsfehler nicht aus. Werte, die mathematisch
eigentlich exakt null sein sollten, behalten dann häufig einen Restwert.
Um den Ursprungs-Vektor (0,0,0) trotz enthaltener Restwerte
identifizieren zu können, wurde die Prüfungsfunktion "v_iszero" mit
Toleranz-Parameter implementiert:
00001
00002
00003
00004
00005
00006
00007
00008
00009
//check if vector is nearly zero
function v_iszero(v:tv;tol:single):boolean;
begin
result:=false;
if abs(v.x)>tol then exit;
if abs(v.y)>tol then exit;
if abs(v.z)>tol then exit;
result:=true;
end;
Der Typ "Tm" beschreibt eine 3 x 3 Matrix. Matrizen werden in
OpenGL verwendet, um Vektoren auf elegante Weise zu rotieren und
zu verschieben (dort werden jedoch aufgrund der w-Dimension 4 x 4
Matrizen eingesetzt).
Die folgende Funktion zur Erzeugung einer Einheitsmatrix zeigt,
wie die Matrizen in der "DanGeoU.pas" aufgebaut sind:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
{
1 0 0 m[0,0]=1 m[1,0]=0 m[2,0]=0
( 0 1 0 ) <=> m[0,1]=0 m[1,1]=1 m[2,1]=0
0 0 1 m[0,2]=0 m[1,2]=0 m[2,2]=1
}
function m_identity:tm;
begin
result[0,0]:=1;result[1,0]:=0;result[2,0]:=0;
result[0,1]:=0;result[1,1]:=1;result[2,1]:=0;
result[0,2]:=0;result[1,2]:=0;result[2,2]:=1;
end;
Für eine Einheitsmatrix gilt im Übrigen, dass sie, multipliziert
mit einem Vektor, diesen unverändert lässt.
Mit Nullen füllen kann man die "Tm"-Matrix über die Funktion "m_clr":
00001
00002
00003
00004
00005
00006
00007
00008
00009
//clear matrix
function m_clr:tm;
var
c,r:integer;
begin
for r:=0 to 2 do
for c:=0 to 2 do
result[c,r]:=0;
end;
In "OGL_ISS" benötigen wir Matrizen zur Achsen-Rotation.
Matrizen haben den Vorteil, dass man sie miteinander multiplizieren
kann, wobei ihre Rotationseigenschaften erhalten bleiben.
Auf diese Weise lässt sich eine generalisierte Rotationsmatrix
bilden, die einen Vektor mit nur einer Multiplikation um alle drei Achsen
rotiert.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
Vektor-Rotation Matrix-Rotation
------------------------------ ------------------------------
vector = v_rot_x(vector,alpha) <==> vector = m_rot_x(alpha)*vector
vector = v_rot_y(vector, beta) <==> vector = m_rot_y( beta)*vector
vector = v_rot_z(vector,gamma) <==> vector = m_rot_z(gamma)*vector
oder auch:
matrix = m_rot_x(alpha)*
m_rot_y(beta) *
m_rot_z(gamma)
vector = matrix*vector
Die einzelnen Achsen-Rotationsmatrizen sind:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
//create x-rotation-matrix
function m_fill_rot_x(rad:single):tm;
var
acos,asin:single;
begin
asin:=sin(rad);
acos:=cos(rad);
result[0,0]:=1;result[1,0]:= 0;result[2,0]:= 0;
result[0,1]:=0;result[1,1]:=acos;result[2,1]:=-asin;
result[0,2]:=0;result[1,2]:=asin;result[2,2]:= acos;
end;
function m_fill_rot_x_deg(deg:single):tm;
begin
result:=m_fill_rot_x(deg2rad(deg));
end;
//create y-rotation-matrix
function m_fill_rot_y(rad:single):tm;
var
acos,asin:single;
begin
asin:=sin(rad);
acos:=cos(rad);
result[0,0]:= acos;result[1,0]:=0;result[2,0]:=asin;
result[0,1]:= 0;result[1,1]:=1;result[2,1]:= 0;
result[0,2]:=-asin;result[1,2]:=0;result[2,2]:=acos;
end;
function m_fill_rot_y_deg(deg:single):tm;
begin
result:=m_fill_rot_y(deg2rad(deg));
end;
//create z-rotation-matrix
function m_fill_rot_z(rad:single):tm;
var
acos,asin:single;
begin
asin:=sin(rad);
acos:=cos(rad);
result[0,0]:=acos;result[1,0]:=-asin;result[2,0]:=0;
result[0,1]:=asin;result[1,1]:= acos;result[2,1]:=0;
result[0,2]:= 0;result[1,2]:= 0;result[2,2]:=1;
end;
function m_fill_rot_z_deg(deg:single):tm;
begin
result:=m_fill_rot_z(deg2rad(deg));
end;
Zwei "Tm"-Matrizen multipliziert man folgendermassen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
//multiplication of two matrices
function m_mul(m1,m2:tm):tm;
begin
result[0,0]:=m1[0,0]*m2[0,0]+m1[1,0]*m2[0,1]+m1[2,0]*m2[0,2];
result[1,0]:=m1[0,0]*m2[1,0]+m1[1,0]*m2[1,1]+m1[2,0]*m2[1,2];
result[2,0]:=m1[0,0]*m2[2,0]+m1[1,0]*m2[2,1]+m1[2,0]*m2[2,2];
result[0,1]:=m1[0,1]*m2[0,0]+m1[1,1]*m2[0,1]+m1[2,1]*m2[0,2];
result[1,1]:=m1[0,1]*m2[1,0]+m1[1,1]*m2[1,1]+m1[2,1]*m2[1,2];
result[2,1]:=m1[0,1]*m2[2,0]+m1[1,1]*m2[2,1]+m1[2,1]*m2[2,2];
result[0,2]:=m1[0,2]*m2[0,0]+m1[1,2]*m2[0,1]+m1[2,2]*m2[0,2];
result[1,2]:=m1[0,2]*m2[1,0]+m1[1,2]*m2[1,1]+m1[2,2]*m2[1,2];
result[2,2]:=m1[0,2]*m2[2,0]+m1[1,2]*m2[2,1]+m1[2,2]*m2[2,2];
end;
Und zuletzt haben wir noch die Multiplikation einer "Tm"-Matrix
mit einem "Tv"-Vektor:
00001
00002
00003
00004
00005
00006
00007
//multiplication of a matrix with a vector
function m_v_mul(m:tm;v:tv):tv;
begin
result.x:=m[0,0]*v.x+m[1,0]*v.y+m[2,0]*v.z;
result.y:=m[0,1]*v.x+m[1,1]*v.y+m[2,1]*v.z;
result.z:=m[0,2]*v.x+m[1,2]*v.y+m[2,2]*v.z;
end;
Um (JPG-)Bilder in ein Delphi-Programm zu integrieren, genügt es,
sie mittels TImage im Objektinspektor einzuladen. Kompiliert man
dann das Projekt, sind die Daten des Bildes im laufenden Programm
direkt verfügbar, müssen also nicht erst von der Festplatte
nachgeladen werden.
Leider existiert keine Klasse TSound, mit der man Gleiches für
WAV-Daten realisieren könnte. Üblicherweise muss daher
der Delphi-eigene Ressourcen-Editor bemüht werden,
um WAV-Daten in eine EXE-Datei integrieren zu können.
Da dieses Verfahren recht kompliziert ist, habe ich mir ein kleines Tool
namens "File2Array" geschrieben, welches eine beliebige Datei ausliest und
als konstantes Delphi-Array wiedergibt. Diese Daten können dann einfach
in eine Unit kopiert und wie normaler Source kompiliert werden.
Folgendes Beispiel soll dies demonstrieren:
Wir haben die Text-Datei "hallo.txt" auf Festplatte gespeichert:
Daraus macht dann "File2Array.exe" folgende Text-Datei:
00001
00002
00003
00004
00005
const
_hallo:array[0..11] of byte=(
72, 97,108,108,111, 44, 32, 87,101,108,116, 33
);
// H a l l o , W e l t !
Da unter Windows letztlich alle Daten eine Folge von Bytes sind,
funktioniert diese Methode mit jeder Art von Datei, egal, ob
sie nun Bilder, Sounds, Texte oder Filme enthält.
Auch wenn es eigentlich nicht nötig ist, wurden die drei so generierten
WAV-Datenströme bei "OGL_ISS" in eine eigene Unit "SoundsU.pas" gelegt.
Das Ganze sieht dann in etwa so aus:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
unit soundsu;
interface
const
_sound_explode:array[0..107789] of byte=(
82, 73, 70, 70, 6,165, 1, 0, 87, 65, 86, 69,102,109,116, 32, 16, 0, 0, 0,
1, 0, 1, 0, 17, 43, 0, 0, 34, 86, 0, 0, 2, 0, 16, 0,100, 97,116, 97,
[...]
103, 0, 23, 1,204, 1,131, 2, 67, 3, 3, 4,202, 4,152, 5,108, 6, 67, 7,
28, 8, 92, 8, 91, 8, 88, 8, 88, 8
);
//----------------------------------------------------------------
_sound_shot:array[0..111453] of byte=(
82, 73, 70, 70, 86,179, 1, 0, 87, 65, 86, 69,102,109,116, 32, 16, 0, 0, 0,
1, 0, 1, 0, 17, 43, 0, 0, 34, 86, 0, 0, 2, 0, 16, 0,100, 97,116, 97,
[...]
3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 0, 0, 2, 0, 1, 0, 1, 0, 2, 0,
0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0
);
//-----------------------------------------------------------------------
_sound_alarm:array[0..30905] of byte=(
82, 73, 70, 70,178,120, 0, 0, 87, 65, 86, 69,102,109,116, 32, 16, 0, 0, 0,
1, 0, 1, 0, 34, 86, 0, 0, 68,172, 0, 0, 2, 0, 16, 0,100, 97,116, 97,
[...]
97, 58, 50, 65, 52, 67,228, 76, 73, 78, 8, 59,208, 24, 73,246, 36,216, 57,194,
71,196, 33,180,147,213
);
implementation
end.
Wie wir letztlich diese Array-Daten im Programm nutzen können,
sehen wir gleich noch.
Der eigentliche Programm-Code von "OGL_ISS" befindet sich komplett
in der Unit "HauptU.pas". Schöner wäre es wohl gewesen, mehrere Units
zu verwenden, etwa eine für die Steuerung, eine für die Spielfunktionen
und eine für das Rendern. Jedoch ist der Quellcode insgesamt so
knapp ausgefallen, dass das gar nicht nötig war.
Die Applikation begnügt sich zudem mit nur einer TForm, auf der sich
alles abspielt. Hier ein Screenshot der "hauptf":
Delphi-Form 'thauptf': Die Hauptform von 'OpenGL ISS'. Hier spielt sich alles ab. Edits, Bildcontainer,
Panels, Labels, Timer und alles Restliche, was zur Anzeige nötig ist.
Die in das Projekt eingebundenen Units sind:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
unit hauptu;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs,ExtCtrls, StdCtrls, ComCtrls,
XPMan,Math,JPEG,IniFiles,MMSystem,
DGLOpenGL,
DanGeoU,SoundsU;
Neben den Standard-Units gibt es "XPMan" (XPManifest) für die schönere Optik,
"Math" für trigonometrische Funktionen, "JPEG" für JPG-Bilder, "IniFiles"
für Konfigurationsdateien und "MMSystem" für die Sound-API.
Die OpenGL-Funktionalität wird über "DGLOpenGL" eingebunden. Anders als bei meinen
vorherigen OpenGL-Projekten werden die Units "SDL" und "SDL_Image" hier nicht
benötigt.
Zuletzt werden dann noch meine eigenen, weiter oben beschriebenen Units
"DanGeoU" und "SoundsU" hinzugefügt.
Die verwendeten Konstanten, Typen und "globalen" Variablen sind:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
const
_cap='OGL_ISS V1.0 2008 by Daniel Schwamm';
_inifn='ogl_iss.ini';
//earth----------------------------------------
_earth_r =6368000;
_earth_y =-_earth_r-345000;
_earth_drot =360/(90*60*10); //one rotation in 90 min
//home parameter
_home_pos_z=200;
_iss_len=94;
//space dimension--------------
_space_r =2*_earth_r;
//speed factors-------------------
_pstep=0.6; //flight speed adder
_rstep=0.6; //rotation speed adder
_pspeed_max=30;
_rspeed_max=10;
//depth buffer
_NearClipping=1;
_FarClipping=-1;
//game
_game_level_train =1;
_game_level_rookie =2;
_game_level_advanced=3;
_game_level_profi =4;
_game_level_mad =5;
//number of same-time shots-----------
_shot_c=50;
//meteor----------------
_meteor_max=50;
_meteor_r=20;
_meteor_state_none =0;
_meteor_state_fly =1;
_meteor_state_explode=2;
_meteor_alarm=100;
//path-------------------
_path_c=2000;
type
//meteor record-----------
pmeteor=^tmeteor;
Tmeteor=record
posv:tv;
radius,speed:single;
state:byte;
count:integer;
end;
//shot-record--------------
pshot=^tshot;
Tshot=record
posv,poslastv,dirv:tv;
count:integer;
end;
Thauptf = class(TForm)
[...]
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
homedir:string;
//opengl environment
handle_dc:HDC;
handle_rc:HGLRC;
//my position and axis-angles
posv,dposv:tv;
rotv:tv;
//speed values: fly and axis-rotation
pspeed,rx_speed,ry_speed,rz_speed:single;
//some flags & counters
draw_visor_ok:bool;
draw_path_ok:bool;
flyhome_c:integer;
//game parameters-----------
game_iss_meteor,
game_meteor_dim,
game_points,
game_hs_rookie,
game_hs_advanced,
game_hs_profi,
game_hs_mad:integer;
game_level,
game_meteor_c:byte;
game_meteor_speed:single;
game_status:string;
//sounds
sound_shot:TMemoryStream;
sound_explode:TMemoryStream;
sound_alarm:TMemoryStream;
sound_c:integer;
sound_ok:bool;
//time-counters-----------
frequ_low,frequ_fast:single;
//key holder----------------
akey:word;
actrl:bool;
//grapcic-mode
graphic_mode:byte;
graphic_txok,
graphic_earthok,
graphic_spaceok:bool;
//space----------------
space_quad:PGLUquadric;
space_tx:gluint;
//earth----------------
earth_quad:PGLUquadric;
earth_tx:gluint;
earth_rot:single;
//iss---------------------
iss_quad:PGLUquadric;
iss_startv,iss_endv:tv;
iss_big_tx:gluint;
iss_big_sail_tx:gluint;
iss_small_tx:gluint;
iss_small_sail_tx:gluint;
iss_small_sail_rot:single;
iss_big_sail_rot:single;
//shot--------------------------------
shot_a:array[0.._shot_c]of tshot;
shot_quad:PGLUquadric;
shot_tx:gluint;
shot_c:integer;
//meteor------------------------------
meteor_a:array[0.._meteor_max-1]of tmeteor;
meteor_imga:array[0.._meteor_max-1]of timage;
meteor_quad:PGLUquadric;
meteor_tx:gluint;
meteor_explode_tx:gluint;
//path----------------------------------
path_a:array[0.._path_c]of tv;
path_c:integer;
[...]
end;
Eine Erläuterung erspare ich mir. Die Liste dient nur als Referenz-Liste,
wenn bei der Source-Beschreibung weiter unten diese Bezeichner auftauchen.
Wie bei Delphi üblich, wird nach dem Programmstart automatisch die
Prozedur "FormCreate" aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
//create form-------------------------------------------------------
procedure Thauptf.FormCreate(Sender: TObject);
var
r:integer;
p:tpanel;
img:timage;
begin
homedir:=extractfilepath(application.exename);
caption:=_cap;
randomize;
frequ_low:=0;
frequ_fast:=0;
sound_c:=0;
//preparing sounds-------------------
sound_shot:=TMemoryStream.Create;
sound_shot.writeBuffer(_sound_shot,sizeof(_sound_explode));
sound_explode:=TMemoryStream.Create;
sound_explode.writeBuffer(_sound_explode,sizeof(_sound_shot));
sound_alarm:=TMemoryStream.Create;
sound_alarm.writeBuffer(_sound_alarm,sizeof(_sound_alarm));
//set panels all same style
for r:=0 to componentcount-1 do begin
if not(components[r] is tpanel)then continue;
p:=tpanel(components[r]);
p.parentbackground:=false;
p.BevelOuter:=bvnone;
p.color:=clblack;
p.font.color:=clyellow;
end;
//arrange components
backp.align:=alclient;
viewp.align:=alclient;
footimg.align:=alclient;
gamestartimg.Align:=alclient;
gamestartimg.picture.assign(footimg.Picture);
gamestartp.visible:=false;
gameoverimg.Align:=alclient;
gameoverimg.picture.assign(footimg.Picture);
gameoverp.visible:=false;
meteorimg.align:=alclient;
//create meteor-counter-images
for r:=0 to _meteor_max-1 do begin
img:=timage.Create(meteorcp);
//img.Proportional:=true;
img.stretch:=true;
img.Picture.assign(meteorimg.picture);
img.align:=alleft;
img.Width:=meteorcp.Width div _meteor_max;
img.height:=meteorcp.Height;
img.parent:=meteorcp;
img.Visible:=true;
meteor_imga[r]:=img;
end;
//intialize opengl
handle_DC:=GetDC(viewp.Handle);
if not InitOpenGL then Application.Terminate;
handle_RC:=CreateRenderingContext(handle_DC,[opDoubleBuffered],24,32,0,0,0,0);
ActivateRenderingContext(handle_DC,handle_RC);
glenable(GL_DEPTH_TEST);
//read ini parameters
with tinifile.create(homedir+_inifn) do begin
draw_visor_ok:=readbool('param','draw_visor_ok',true);
draw_path_ok:=readbool('param','draw_path_ok',false);
footp.visible:=readbool('param','draw_cockpit_ok',true);
sound_ok:=readbool('param','sound_ok',true);
graphic_mode:=readinteger('param','graphic_mode',7);
game_hs_rookie:=readinteger('param','game_hs_rookie',0);
game_hs_advanced:=readinteger('param','game_hs_advanced',0);
game_hs_profi:=readinteger('param','game_hs_profi',0);
game_hs_mad:=readinteger('param','game_hs_mad',0);
frequencytb.position:=readinteger('param','frequencytb',frequencytb.position);
free;
end;
//create objects
initobjects;
//start action
self.WindowState:=wsmaximized;
frequencyt.enabled:=true;
game_start_show;
end;
Schauen wir uns mal an, was bei dieser Prozedur so alles passiert.
Zu Beginn werden die konstanten WAV-Arrays aus der "SoundsU.pas"
in TMemoryStream-Objekte eingelesen. Diese lassen sich später
an die Prozedur "dosound" übergeben, wo sie letztlich über die
API-Funktion "sndPlaySound" abgespielt werden:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
//-----------------------------------------------------
procedure thauptf.dosound(ms:tmemorystream);
var
flags:cardinal;
begin
if not sound_ok then begin
sndPlaySound(nil,0);
exit;
end;
if sound_c>0 then exit;
if gameoverp.visible then exit;
flags:=SND_MEMORY or SND_ASYNC;
if ms=nil then begin
sndPlaySound(nil,0);
exit;
end;
sndPlaySound(ms.memory,flags);
sound_c:=3;
end;
"sound_c" ist ein Zähler, der angibt, wie lange (das heisst,
wie viele "Zeiteinheiten") ein Sound mindestens abgespielt wird,
bevor er durch einen anderen ersetzt werden darf. Für
Multi-Sound-Effekte hätte ich mich sonst mit der
DirectSound-API herumschlagen müssen - und dann
wäre der Source gewiss umfangreicher ausgefallen.
Anschliessend werden alle TComponents der TForm "hauptf" durchlaufen
und nach TPanels durchsucht. Wird ein TPanel gefunden, so wird
es u.a. schwarz eingefärbt und die Eigenschaft "parentbackground"
wird auf "false" gesetzt. Das ist notwendig wegen eines lästigen Bugs
des XP-Manifests - ohne diese Korrektur "merkt" sich TPanel sonst
seine Background-Farbe nicht.
Über eine weitere Schleife wird das TImage-Array "meteor_imga" gefüllt.
Dazu wird pro Meteor ein TImage erzeugt, mit dem TPicture aus TImage
"meteorimg" versehen und auf dem TPanel "meteorcp" abgelegt. Dort sind
später alle noch im Spiel vorhandenen Meteore aufgelistet. Wird
ein Meteor abgeschossen, so verschwindet sein zugehöriges Bild einfach
vom TPanel.
Jetzt wird die OpenGL-Umgebung initialisiert. Kommt es dabei zu
Problemen, bricht das Programm ab. Als Ziel der Grafikausgabe
wird das TPanel "viewp" definiert.
Es folgt die Routine zum Einlesen der Programm-Parameter aus dem
Konfigurationsfile "ogl_iss.ini". Hier wird u.a. festgehalten,
welcher Grafik-Modus aktiv ist, ob Sound zu hören ist oder nicht,
oder ob das Cockpit angezeigt werden soll. Des Weiteren sind hier
die Highscores der vier Spiel-Level "rookie", "advanced", "profi"
und "mad" abgelegt.
Danach wird die Prozedur "initobjects" aufgerufen, die wir uns gleich
näher ansehen werden.
Mittels "self.WindowState:=wsmaximized" wird das Programm-Fenster
maximiert. Das hat einige weitere Designarbeiten zur
Folge. Ausserdem wird dabei die OpenGL-Umgebung auf die neuen
Bildausgabe-Verhältnisse angepasst:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
//----------------------------------------------------------------
procedure Thauptf.viewpResize(Sender: TObject);
begin
if not visible then exit;
//center cockpit
cockpitp.Left:=(footp.width-cockpitp.Width)div 2;
cockpitp.top:=(footp.height-cockpitp.height)div 2;
//center game start panel
gamestartp.Left:=(viewp.width-gamestartp.Width)div 2;
gamestartp.top:=(viewp.height-gamestartp.height)div 2;
//center game over panel
gameoverp.Left:=(viewp.width-gameoverp.Width)div 2;
gameoverp.top:=(viewp.height-gameoverp.height)div 2;
//ogl-adaptionen
glViewport(0,0,viewp.ClientWidth,viewp.ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(
50,
viewp.ClientWidth/viewp.ClientHeight,
_NearClipping,
_FarClipping
);
glMatrixMode(GL_MODELVIEW);
Draw_Scene;
end;
Anschliessend wird der TTimer "frequencyt" aktiviert, wodurch die sich
periodisch wiederholende Grafikausgabe begonnen wird.
Und zuletzt wird dann noch die Prozedur "game_start_show" aufgerufen.
Das Gegenstück zu "FormCreate" ist "FormDestroy", welches beim Beenden
des Programms ebenfalls automatisch aufgerufen wird. Hier werden alle
Ressourcen wieder freigegeben und einige Programm-Parameter sowie die
(aktualisierten) Highscore-Listen in die Konfigurationsdatei geschrieben:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
//clear program-------------------------------------------------------
procedure Thauptf.FormDestroy(Sender: TObject);
var
r:integer;
begin
//write ini parameters
deletefile(homedir+_inifn);
with tinifile.create(homedir+_inifn) do begin
writebool('param','draw_path_ok',draw_path_ok);
writebool('param','draw_cockpit_ok',footp.visible);
writebool('param','draw_visor_ok',draw_visor_ok);
writebool('param','sound_ok',sound_ok);
writeinteger('param','graphic_mode',graphic_mode);
writeinteger('param','game_hs_rookie',game_hs_rookie);
writeinteger('param','game_hs_advanced',game_hs_advanced);
writeinteger('param','game_hs_profi',game_hs_profi);
writeinteger('param','game_hs_mad',game_hs_mad);
writeinteger('param','frequencytb',frequencytb.position);
free;
end;
//free textures
glDeleteTextures(1,@space_tx);
glDeleteTextures(1,@earth_tx);
glDeleteTextures(1,@iss_big_tx);
glDeleteTextures(1,@iss_big_sail_tx);
glDeleteTextures(1,@iss_small_tx);
glDeleteTextures(1,@iss_small_sail_tx);
glDeleteTextures(1,@meteor_tx);
glDeleteTextures(1,@meteor_explode_tx);
//free quads
gluDeleteQuadric(earth_quad);
gluDeleteQuadric(iss_quad);
gluDeleteQuadric(space_quad);
gluDeleteQuadric(meteor_quad);
//free meteor-counter-images
for r:=0 to _meteor_max-1 do meteor_imga[r].Free;
//free OpenGL environment
DeactivateRenderingContext;
DestroyRenderingContext(handle_RC);
ReleaseDC(viewp.Handle,handle_DC);
//free sounds
sound_alarm.Free;
sound_shot.Free;
sound_explode.free;
end;
Wie wir gesehen haben, wird in "FormCreate" die Prozedur
"initobjects" aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
//initialize objects----------------------------------------------------
procedure thauptf.initobjects;
begin
//space-----------------------------------------------
space_quad:=gluNewQuadric;
gluQuadricTexture(space_quad,TGLboolean(true));
space_tx:=img2tx(spaceimg);
//earth-----------------------------------------------
earth_quad:=gluNewQuadric;
gluQuadricTexture(earth_quad,TGLboolean(true));
earth_tx:=img2tx(earthimg);
//ISS-------------------------------------------------
iss_quad:=gluNewQuadric;
gluQuadricTexture(iss_quad,TGLboolean(true));
iss_big_tx:=img2tx(issbigimg);
iss_big_sail_tx:=img2tx(issbigsailimg);
iss_small_tx:=img2tx(isssmallimg);
iss_small_sail_tx:=img2tx(isssmallsailimg);
iss_startv:=v_fill(0,0,-_iss_len/2);
iss_endv:=v_fill(0,0,_iss_len/2);
iss_small_sail_rot:=0;
iss_big_sail_rot:=0;
//meteors-----------------------------------------------
meteor_quad:=gluNewQuadric;
gluQuadricTexture(meteor_quad,TGLboolean(true));
meteor_tx:=img2tx(meteorimg);
meteor_explode_tx:=img2tx(meteorexpimg);
end;
Hier werden "gluQuadric"-Objekte erzeugt. Solche Objekte lassen
sich mit bestimmten Eigenschaften versehen und später an diverse
OpenGL-Funktionen übergeben.
Das "gluQuadric"-Objekt "space_quad" wird zum Beispiel später als
Kugel dargestellt, an deren Mittelpunkt sich die ISS befindet. Sie
ist so gross, dass sie als Art "Himmelszelt" fungiert. Egal, in welche
Richtung wir später auch schauen, wir sehen stets die Innenwände dieser
Kugel im Hintergrund. Über die Funktion "img2tx" wird eine passende
Textur an das Objekt gebunden. Wie das genau funktioniert,
sehen wir uns gleich an.
Textur von 'space_quad': Ein 256 x 128 Pixel grosses JPG-Bild für den Weltraum.
Einsatz der Space-Textur: Der Weltraum um uns herum ist nicht einfach schwarz,
sondern von blau leuchtenden Nebelbändern durchzogen.
Ähnlich wie "space_quad" ist auch "earth_quad" konzipiert.
Hierbei handelt es sich um den "Zeiger" auf die Erdkugel,
die 345 km unter der ISS um ihre eigene Achse rotiert.
Sie hilft uns bei der Orientierung im Raum, wenn wir später
um die ISS herumfliegen. Obgleich natürlich solche Dinge
wie oben und unten im schwerelosen Zustand nicht wirklich
eine Rolle spielen.
Textur von 'earth_quad': Ein geometrisch angepasste Grafik mit den Kontinenten und Ozeanen,
512 x 512 Pixel gross - und damit die mit Abstand grösste verwendete Textur.
Einsatz der Earth-Texture: Die blaue Heimat, rotierend unter der ISS, mit seinen einigermassen
korrekt wiedergegebenen Strukturen.
Die ISS selbst verfügt über das "gluQuad"-Objekt "iss_quad".
Dieses wird später beim Rendern mehrfach verwendet, für jedes
Einzelteil der ISS erneut. Ausmasse und Texturen werden dabei
jeweils zuvor angepasst. Die verwendeten Texturen sind:
Textur 'iss_big_tx': Grafik für den Hauptkörper der ISS.
|
Textur 'iss_big_sail_tx': Grafik für die grossen Segel der ISS.
|
Textur 'iss_small_tx': Grafik für die kleinen Gondeln der ISS.
|
Textur 'iss_small_sail_tx': Grafik für die kleinen Gondeln der ISS.
|
Einsatz der ISS-Texturen: Die ISS von Aussen und Innen. Nur vier Texturen genügen,
um unserer Raumstation ein hinreichend realistisches Aussehen zu verleihen.
Zusätzlich wird der Positionsvektor der ISS definiert. Sie wird
so platziert, dass ihr Zentrum genau auf dem Ursprung (0,0,0) liegt.
Zuletzt werden dann noch die Meteore mit zwei Texturen versehen:
Textur 'meteor_tx': Grafik für die Meteore, die die ISS umschwirren.
|
Textur 'meteor_explode_tx': Grafik für explodierende Meteore.
|
Einsatz der Meteor-Texturen: Meteore steuern auf die ISS zu - und explodieren bisweilen ...
Bei den Quellcodes zu OpenGL, die man im Web so findet, werden
Texturen üblicherweise nach dem Programmstart von der Festplatte
nachgeladen. Die Units "sdl" und "sdl_image" bieten dafür auch
in Delphi die nötige Funktionalität.
Die Methode hat jedoch den Nachteil, dass für die Installation neben
dem Programm stets alle Bilder extra mitgeliefert werden müssen
(und unter Umständen auch die Bibliotheksdateien "sdl.dll" und
"sdl_image.dll").
Bei "OGL_ISS" ist das nicht nötig, denn hier können die Texturen
direkt aus TImage-Bildern generiert werden. Das Ganze wurde dabei
von mir so gelöst, das die Bibliotheksdateien "SDL"- und "SDL_image.dll"
ebenfalls nicht vorhanden sein müssen.
Betrachten wir zunächst die Funktion "img2tx":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
//return texture-pointer from timage --------------------
function thauptf.img2tx(img:timage):TGLuint;
var
data:Array of LongWord;
w,x:integer;
h,y:integer;
bmp:tbitmap;
c:longword;
line:^longword;
begin
bmp:=TBitmap.Create;
bmp.Assign(img.Picture.Graphic);
//ogl braucht 32bit-pics
bmp.PixelFormat:=pf32bit;
w:=bmp.width;
h:=bmp.height;
SetLength(data,w*h);
for y:=0 to h-1 do begin
line:=bmp.scanline[h-y-1];
for x:=0 to w-1 do begin
c:=line^ and $FFFFFF; // Need to do a color swap
data[x+(y*w)]:=
(((c and $FF)shl 16)+
(c shr 16)+
(c and $FF00)) or $FF000000;
inc(line);
end;
end;
bmp.free;
result:=CreateTexture(w,h,addr(Data[0]));
end;
An die Funktion wird das TImage-Objekt "img" übergeben, welches ein
beliebiges, vorgeladenes Bild enthält. Es kann sich dabei ebenso um ein
GIF, wie ein JPG, eine Bitmap oder irgendein anderes Bildformat handeln.
Denn im ersten Schritt wird das enthaltene TPicture sowieso in eine Bitmap
"bmp" mit 32 bit Farbtiefe umgewandelt.
Anschliessend wird das LongWord-Array "data" erzeugt, welches gross genug ist,
um alle anfallenden Bilddaten komplett aufnehmen zu können.
Das Bild wird nun pixelweise durchlaufen, der Farbwert jedes Pixels auf
geheimnisvolle Art umgerechnet - den Teil habe ich mir im Web geklaut - und
in das "data"-Array eingetragen.
Das "data"-Array wird daraufhin an die Funktion "createTexture" übergeben,
welche letztlich einen von OpenGL verwendbaren "Textur-Index-Wert"
zurück liefert:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
//return texture-pointer from image-memory-data --------------------
function thauptf.CreateTexture(Width,Height:Word;pData:Pointer):Integer;
var
Texture:TGLuint;
begin
glGenTextures(1,@Texture);
glBindTexture(GL_TEXTURE_2D,Texture);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST_MIPMAP_LINEAR);
gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGB,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,pData);
result:=Texture;
end;
Als ich begann, "OGL_ISS" mit einem kleinen Spiel zu versehen, wurde
schnell klar, dass man nur dann spielbare Ergebnisse erzielen würde,
wenn das Ablauftempo des Ganzen halbwegs "normiert" wird.
Computer sind nämlich bekanntlich verschieden schnell - und einige
besitzen 3D-Grafikkarten, andere nicht. Bedingt dadurch wird die
Geschwindigkeit, mit der OpenGL die Objekte rendern und letztlich zur
Anzeige bringen kann, für den Programmierer schwer abzuschätzen.
Aus diesem Grund wird die Grafikverarbeitung in "OGL_ISS" nicht über das
OnIdle-Ereignis der Applikation gelöst, wie sonst meist üblich, weil
es die beste Performanz bringt. Stattdessen gibt es einen TTimer
"frequencyt", der diesen Job in regelmässigen, definierten Zeitabständen
erledigt.
Wann immer dieser TTimer "feuert" - und dies geschieht alle paar
Millisekunden, je nach Einstellung der TTrackBar "frequencytb" -, wird
die folgende Prozedur ausgeführt:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
//float 2 short string---------------------
function thauptf.f2s_cut(d:single):string;
begin
result:=format('%.2f',[d]);
end;
//------------------------------------------------------------------
procedure Thauptf.frequencytTimer(Sender: TObject);
function flight(vz:integer):tv;
begin
pspeed:=pspeed+vz*_pstep;
if pspeed> _pspeed_max then pspeed:= _pspeed_max;
if pspeed<-_pspeed_max then pspeed:=-_pspeed_max;
result:=getlookv;
end;
procedure rotate_axis(vz:integer;var rspeed:single);
begin
rspeed:=rspeed+vz*_rstep;
if rspeed> _rspeed_max then rspeed:= _rspeed_max;
if rspeed<-_rspeed_max then rspeed:=-_rspeed_max;
end;
procedure slowspeed(var speed:single);
begin
if speed=0 then exit;
if speed>0 then speed:=speed-0.2;
if speed<0 then speed:=speed+0.2;
if abs(speed)<0.2 then speed:=0;
end;
begin
activecontrol:=nil;
frequ_fast:=deg_norm(frequ_fast+1);
frequ_low:=deg_norm(frequ_low+0.1);
//rotation earth
earth_rot:=deg_norm(earth_rot+_earth_drot);
//rotation small solar sail
if trunc(frequ_low)mod 5=4 then
iss_small_sail_rot:=deg_norm(iss_small_sail_rot+1);
//rotation big solar sail
if trunc(frequ_low)mod 8=7 then
iss_big_sail_rot:=deg_norm(iss_big_sail_rot+random(2));
//change position/direction?
if akey<>0 then begin
if actrl then begin
//rotation x-axis
if akey=vk_up then rotate_axis( 1,rx_speed)
else if akey=vk_down then rotate_axis(-1,rx_speed);
//rotation z-axis
if akey=vk_left then rotate_axis( 1,rz_speed)
else if akey=vk_right then rotate_axis(-1,rz_speed);
end
//flying
else if akey=vk_right then rotate_axis( 1,ry_speed)
else if akey=vk_left then rotate_axis(-1,ry_speed)
else if akey=vk_up then dposv:=flight( 1)
else if akey=vk_down then dposv:=flight(-1);
end;
if pspeed<>0 then begin
if dposv.x<>0 then posv.x:=posv.x+dposv.x*pspeed;
if dposv.y<>0 then posv.y:=posv.y+dposv.y*pspeed;
if dposv.z<>0 then posv.z:=posv.z+dposv.z*pspeed;
if draw_path_ok then path;
end
else begin
dposv:=v_fill(0,0,0);
end;
if rx_speed<>0 then rotv.x:=deg_norm(rotv.x-rx_speed);
if ry_speed<>0 then rotv.y:=deg_norm(rotv.y-ry_speed);
if rz_speed<>0 then rotv.z:=deg_norm(rotv.z-rz_speed);
if flyhome_c<=0 then begin
slowspeed(pspeed);
slowspeed(rx_speed);
slowspeed(ry_speed);
slowspeed(rz_speed);
end;
//cockpit info
if cockpitp.Visible then begin
pxp.caption:=f2s_cut(posv.x);rotxp.caption:=f2s_cut(rotv.x)+'°';
pyp.caption:=f2s_cut(posv.y);rotyp.caption:=f2s_cut(rotv.y)+'°';
pzp.caption:=f2s_cut(posv.z);rotzp.caption:=f2s_cut(rotv.z)+'°';
if flyhome_c>0 then
game_statusp.caption:='*** FLY HOME '+inttostr(flyhome_c)+' ***'
else
game_statusp.caption:=game_status;
game_pointsp.caption:=inttostr(game_points);
end;
draw_scene;
if flyhome_c>0 then begin
dec(flyhome_c);
if flyhome_c=0 then begin
pspeed:=0;
rx_speed:=0;ry_speed:=0;rz_speed:=0;
//fly home because of iss destroying?
if game_iss_meteor<>-1 then begin
//let the dirty meteor explode
meteor_a[game_iss_meteor].state:=_meteor_state_explode;
meteor_a[game_iss_meteor].count:=30;
dosound(sound_explode);
end;
end;
end;
dec(sound_c);if sound_c<0 then sound_c:=0;
end;
Am Anfang werden die Rotationszähler "frequ_fast", "frequ_low"
und "earth_rot" auf verschieden starke Weise erhöht. Sie dienen
dazu, periodisch einige Elemente der ISS zu bewegen, etwa die grossen
Solar-Segel, oder die Erde genau so schnell rotieren zu lassen,
dass in 90 Minuten eine komplette Rotation erfolgt (so viel bzw.
wenig Zeit benötigt nämlich auch die echte ISS für die Erdumrundung).
Anschliessend wird geprüft, ob der Anwender eine der Flugsteuerungstasten
betätigt hat. Ist dies der Fall, werden sie entsprechend abgearbeitet.
Wurde zusätzlich die "STRG"-Taste ("actrl") gedrückt, dann gilt:
- Cursor hoch/runter: Rotation um x-Achse (Rotations-Delta: "rx_speed")
- Cursor recht/links: Rotation um z-Achse (Rotations-Delta: "rz_speed")
Ansonsten gilt:
- Cursor hoch/runter: Flug geradeaus nach vorne/hinten (Positions-Delta: "pspeed" und "dposv")
- Cursor recht/links: Rotation um y-Achse (Rotations-Delta: "ry_speed")
Über die internen Funktionen "rotate_axis" und "flight" wird dazu
jeweils das zugehörige Rotations- bzw. Positionsdelta
berechnet (siehe obige Liste).
Das Rotationsdelta wird einfach um einen konstanten
Wert erhöht oder gemindert. Wird dabei der Rotationswinkel von 360 Grad
überschritten, beginnt es wieder bei 0 Grad. Die Stärke der
Änderung wird auf "_rspeed_max" begrenzt, damit wir am Ende nicht
völlig orientierungslos und in in rasender Geschwindigkeit durch
den Raum rotieren.
Das Positionsdelta ist komplizierter zu berechnen.
Dafür müssen wir nämlich feststellen, in welche Richtung aktuell
geschaut wird, damit wir exakt geradeaus steuern können. Hier kommen
die Rotationsmatrizen zum Einsatz, die wir in "DanGeoU.pas" definiert
haben. Dem Thema widmen wir uns weiter unten im Kapitel "Fliegen im
dreidimensionalem Raum".
Je länger eine Steuerungstaste betätigt wird, desto grösser
wird das Rotations- bzw. Positionsdelta pro Timer-Aufruf;
man beschleunigt also. Wird die Taste wieder losgelassen, sorgt die
interne Prozeduren "slowspeed" dafür, dass die verschiedenen Delta-Variablen
langsam wieder auf null zurückgehen; man bremst also ab.
Ist der automatische Heimflug aktiviert ("flyhome_c" ist grösser Null),
dann werden übrigens alle Steuerungseingaben ignoriert. Der Pilot muss
in diesem Fall warten, bis der Ursprungsort erreicht ist, bevor er
wieder selbst aktiv werden kann.
Nachdem unsere Position und Geschwindigkeit im Raum neu gesetzt wurde,
wird weiter geprüft, ob das Cockpit sichtbar ist. Ist dem so, dann
werden jetzt diverse Statusmeldungen auf dem Bildschirm ausgegeben.
Cockpit: Das Cockpit unseres Raumschiffs mit den Status-Informationen (oben). Es lässt
sich auch jederzeit optional abschalten, was den Sichtbereich vergrössert (unten).
Anschliessend wird über die Prozedur "draw_scene" die aktuelle Grafikausgabe
initiiert.
Zuletzt wird dann noch einmal geprüft, ob wir uns im automatischen Heimflug
befinden, und ob wir das Ziel bereits erreicht haben. Wenn ja, werden alle
Deltas auf null gesetzt - unser Raumschiff steht still. Weiterhin
wird geklärt, ob der automatische Heimflug freiwillig geschah oder
erzwungen wurde, weil die ISS von einem Meteor getroffen worden ist.
In letzterem Fall wird eine schöne, grosse, finale Explosion vorbereitet :-)
Die Programmierung von "OGL_ISS" lief eigentlich wie am Schnürchen. Die
einzige Geschichte, bei der ich wirklich zu kämpfen hatte, war der
vermeintlich einfache Flug geradeaus.
Bei
"OGL_Henrys"
und
"OGL_Planets"
wurde bereits eine Steuerung zum
Navigieren durch einen 3-dimensionalen Raum realisiert. Dort konnte
man jedoch nur senkrecht zu der y-Achse nach oben und unten fliegen,
nicht jedoch in "schräger" Weise gleichzeitig nach vorne und
nach unten.
Einfacher gesagt: Was ich diesmal wollte, war eine Steuerung, die das
Raumschiff immer exakt in Richtung des im Fadenkreuz ausgewiesene
Zieles fliegen liess, egal, wie sehr die Raum-Achsen auch verstellt waren.
Ich malte also viele kleine Bildchen, kombinierte Sinus- und Kosinus-Werte,
spielte mit dem Vorzeichen herum und versuchte mich am Ende sogar an der
Verwendung der Tangens-Funktion.
Mathematische Gehversuche: Die blöde Rotation zur zielgenauen Navigation im dreidimensionalen Raum
muss doch irgendwie in dem Griff zu bekommen sein ...
Nach drei Tagen (!) gab ich auf. Musste in Mathematik Nachsitzen. Las Vektoren-
und Matrizen-Tutorials. Baute dabei die "DanGeoU.pas" zusammen. Und knackte
so schliesslich das Problem mit etwas Web-Hilfe.
Schauen wir uns noch einmal den - etwas vereinfachten - Code zur Modifikation
des Positionsdeltas aus der "frequencytTimer"-Prozedur an:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
procedure Thauptf.frequencytTimer(Sender: TObject);
function flight(vz:integer):tv;
begin
pspeed:=pspeed+vz*_pstep;
result:=getlookv;
end;
begin
[...]
if akey=vk_up then dposv:=flight( 1)
else if akey=vk_down then dposv:=flight(-1);
posv.x:=posv.x+dposv.x*pspeed;
posv.y:=posv.y+dposv.y*pspeed;
posv.z:=posv.z+dposv.z*pspeed;
[...]
end;
Das eigentliche Positionsdelta "pspeed" wird ähnlich wie die
Rotationsdeltas einfach nur um einen konstanten Wert erhöht
oder gemindert. Damit wissen wir schon einmal, wie stark
unsere Position im Raum geändert werden soll.
Damit ist aber noch nicht geklärt, in welcher Richtung sich
die Änderung auswirken soll. Für diesen Job muss der aktuelle
Richtungsvektor "dposv" bestimmt werden.
Sind alle Achsen-Winkel auf null, dann ist die Richtung klar: Da wir als
Piloten in den Raum hineinschauen, müssen wir uns nur entlang der z-Achse
ins Negative bewegen, um nach vorne zu fliegen. Der Richtungsvektor
ist in diesem Fall einfach v=(0,0,-1). Die x- und y-Positionen bleiben
völlig unverändert.
Richtungsvektor vor der Rotation: Der ursprüngliche Richtungsvektor 'v' zeigt exakt nach vorne, in die gewünschte Richtung.
Rotieren wir nun aber 45 Grad um die y-Achse (positive Rotationen erfolgen
übrigens immer entgegen dem Uhrzeigersinn - auch so eine Sache,
die ich Stoffel erst nach einer ganzen Weile merkte), dann bedeutet ein Flug
entlang der z-Achse, dass wir nicht mehr geradeaus fliegen, sondern schräg nach
links hinten. Um die korrekte Richtung einzuhalten, muss in diesem Fall nämlich
auch die x-Position geändert werden.
Richtungsvektor nach der Rotation: Der Richtungsvektor 'v' wurde um 45 Grad um die y-Achse rotiert. Nun zeigt
er nach schräg nach links hinten. Tatsächlich wollen wir uns aber in
die neue Richtung 'vneu' bewegen. Wie finden wir diesen Vektor?
Noch komplizierter wird es natürlich, wenn zudem um die x-Achse rotiert
wird. Spätestens jetzt gilt es, alle Koordinaten-Werte, also x, y und z,
durch den Richtungsvektor "dposv" anzupassen, um in die vom Fadenkreuz
vorgegebene Richtung zu fliegen.
Und wie erhalten wir nun den gesuchten Richtungsvektor "vneu" (="dposv")?
Indem wir den ursprünglichen Richtungsvektor, nämlich v=(0,0,-1),
alle vorgegebenen Achsen-Rotationen nachmachen lassen. Denn dann
sollte dieser wieder genau in die Richtung weisen, in die wir selbst gerade
schauen.
Der Source-Code dazu ist:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
//give back rotated look vector about all axis-----------------------
function thauptf.getlookv;
var
v:tv;
mx,my,mz,m:tm;
begin
//fill rotation matrices
mx:=m_fill_rot_x_deg(rotv.x);
my:=m_fill_rot_y_deg(rotv.y);
mz:=m_fill_rot_z_deg(rotv.z);
//create 'general' rotation matrix
m:=m_mul(mz,my);
m:=m_mul(m,mx);
//rotate the look vector about all axis
v:=v_fill(0,0,-1);
v:=m_v_mul(m,v);
result:=v;
end;
Wir bilden uns zuerst die generalisierte Rotationsmatrix,
indem wir die drei Achsen-Rotationsmatrizen bestimmen
und diese dann miteinander multiplizieren. Wie in "DanGeoU.pas"
beschrieben, "merkt" sich die so entstandene Matrix "m" alle
Rotationen ihrer Teiler-Matrizen "mx", "my" und "mz".
Jetzt muss man nur noch den ursprünglichen Richtungsvektor
mit der generalisierten Rotationsmatrix "m" multiplizieren.
Als Ergebnis erhält man den denjenigen Vektor, der in unserem
Modell exakt nach vorne weist. Multipliziert mit dem Positionsdelta
"pspeed" und aufaddiert zum Positionsvektor "posv" erhalten wir
schliesslich die neue Position im Raum.
Eigentlich doch ganz einfach ...
Bevor der TTimer "frequencyt" in "FormCreate" aktiviert wird,
wird "game_start_show" aufgerufen (siehe weiter oben).
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
//--------------------------------------------------
procedure thauptf.game_start_show;
var
r:integer;
begin
cockpitp.Color:=clblack;
game_pointsp.caption:='';
detect_issp.Caption:='';
detect_mep.Caption:='';
meteorp.color:=clblack;
meteorcp.caption:='';
gameoverp.visible:=false;
gamestartp.visible:=true;
game_points:=0;
game_level:=_game_level_train;
game_status:='Select Dirty Game Mode!';
//clear shots-----------------------------------------------
for r:=0 to _shot_c-1 do shot_a[r].dirv:=v_fill(0,0,0);
shot_c:=0;
//clear meteors-images----------------------------------------
for r:=0 to _meteor_max-1 do meteor_imga[r].Visible:=false;
//clear path-----------------------------------------------
for r:=0 to _path_c-1 do path_a[r]:=v_fill(0,0,0);
path_c:=0;
//go home position
posv:=v_fill(0,0,_home_pos_z);dposv:=v_fill(0,0,0);
rotv:=v_fill(0,0,0);
//no movement or key action
akey:=0;
actrl:=false;
pspeed:=0;
rx_speed:=0;
ry_speed:=0;
rz_speed:=0;
game_iss_meteor:=-1;
end;
Hier werden einige Programm-Parameter zurückgesetzt und das
TPanel "gamestartp" sichtbar gemacht. Über die Tastatur kann
dann u.a. der gewünschte Spiel-Level ausgewählt werden.
Spielstart-Fenster: Erklärung der Tastaturkürzel und die Auswahl verschiedener Programm-Optionen.
Mittels der TTrackBar "frequencytb" lässt sich einstellen, mit
welcher Taktfrequenz die "frequencytTimer"-Prozedur
aufgerufen wird (und damit auch "draw_scene"). Das Intervall reicht
von einer bis bis 100 Millisekunden. Je nach Rechnergeschwindigkeit
kann der Anwender hier einen für sich passenden Wert einstellen.
Alle Tastatur-Eingaben des Benutzers werden über die Prozeduren
"FormKeyDown" und "FormKeyp" verwaltet.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
//--------------------------------------------------
procedure Thauptf.FormKeyDown(
Sender: TObject;
var Key:Word;
Shift:TShiftState
);
begin
akey:=0;
if flyhome_c>0 then exit;
//shift & ctrl state
actrl:=(ssctrl in shift);
//flight key pressed?
if
(key=vk_up)or
(key=vk_down)or
(key=vk_left)or
(key=vk_right)
then begin
akey:=key;
exit;
end;
if gameoverp.Visible then begin
if key=vk_return then game_start_show;
exit;
end;
if gamestartp.Visible then begin
if key=ord('1') then game_start(_game_level_train)
else if key=ord('2') then game_start(_game_level_rookie)
else if key=ord('3') then game_start(_game_level_advanced)
else if key=ord('4') then game_start(_game_level_profi)
else if key=ord('5') then game_start(_game_level_mad)
else if key=vk_escape then begin
close;
exit;
end;
end;
if key=ord('G') then begin
graphic_mode:=graphic_mode+1;
if graphic_mode>7 then graphic_mode:=0;
end
else if key=ord('C') then footp.Visible:=not footp.Visible
else if key=ord('F') then flyhome
else if key=ord('P') then draw_path_ok:=not draw_path_ok
else if key=ord('S') then sound_ok:=not sound_ok
else if key=ord('V') then draw_visor_ok:=not draw_visor_ok
else if key=vk_space then shot
else if key=vk_escape then game_over_show;
end;
//set key holder back-----------------------------------------------------
procedure Thauptf.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
akey:=0;
actrl:=false;
end;
Zunächst wird geprüft, ob der automatische Heimflug aktiviert ist.
In diesem Fall werden alle Benutzer-Eingaben ignoriert.
Der Status der "Strg"-Taste wird in der Variablen "actrl" gesichert.
Sollte eine der Flugsteuerungstasten betätigt worden sein, so wird
"akey" gesetzt und die Prozedur verlassen. Wie wir gesehen haben,
erfolgt deren Abarbeitung in der Prozedur "frequencytTimer".
Wird das TPanel "gameoverp" mit der aktuellen Highscore-Liste angezeigt,
befinden wir uns am Spiel-Ende. In diesem Fall wird nur auf die
"Return"-Taste reagiert.
Befinden wir uns am Spiel-Start, dann ist das TPanel "gamestartp"
sichtbar. Jetzt reagieren wir insbesondere auf die Tasten "1" bis "5",
mit denen sich der Spiel-Level auswählen lässt. Dazu wird in der
Folge die Prozedur "game_start" aufgerufen.
Sind übrigens die TPanels "gamestartp" und "gameoverp" nicht
sichtbar, dann befinden wir uns im laufenden Spiel.
Die "G"-Taste wechselt durch Erhöhung des "graphic_mode"-Zählers den
Grafik-Modus, mit dem in "draw_scene" das Modell gerendert wird.
Die Tasten "C", "P", "S" und "V" aktivieren bzw. deaktivieren die
Anzeige des Cockpits, die Sichtbarkeit des Flug-Pfades,
die Sound-Effekte und das Visier mit dem Fadenkreuz.
Mittels "F"-Taste wird der automatische Heimflug in Gang gesetzt.
Über "Space" werden Schüsse abgegeben.
Und "Escape" bricht letztlich das laufende Spiel ab oder beendet
das Programm.
Haben wir ein Spiel-Level ausgewählt, wird die Prozedur "game_start"
aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
//--------------------------------------------------------
procedure thauptf.game_start(level:byte);
var
r:integer;
begin
//home position, no movement
flyhome_c:=0;
posv:=v_fill(0,0,_home_pos_z);dposv:=v_fill(0,0,0);
rotv:=v_fill(0,0,0);
rx_speed:=0;
ry_speed:=0;
rz_speed:=0;
gamestartp.visible:=false;
game_level:=level;
game_iss_meteor:=-1;
if game_level=_game_level_train then begin
game_status:='TRAINING';
game_meteor_c:=0;
end
else if game_level=_game_level_rookie then begin
game_status:='ROOKIE (Highscore '+inttostr(game_hs_rookie)+')';
game_meteor_c:=10;
game_meteor_dim:=5000;
game_meteor_speed:=0.5;
end
else if game_level=_game_level_advanced then begin
game_status:='ADVANCED (Highscore '+inttostr(game_hs_advanced)+')';
game_meteor_c:=20;
game_meteor_dim:=2000;
game_meteor_speed:=1;
end
else if game_level=_game_level_profi then begin
game_status:='PROFI (Highscore '+inttostr(game_hs_profi)+')';
game_meteor_c:=30;
game_meteor_dim:=2000;
game_meteor_speed:=2;
end
else begin
game_status:='MAD (Highscore '+inttostr(game_hs_mad)+')';
game_meteor_c:=_meteor_max;
game_meteor_dim:=2000;
game_meteor_speed:=3;
end;
//set/show active meteors
for r:=0 to game_meteor_c-1 do begin
meteor_imga[r].width:=round(meteorcp.Width/game_meteor_c);
meteor_imga[r].Visible:=true;
meteor_set(r);
end;
end;
Der Benutzer wird an die Startposition im Spiel transportiert,
indem die Achsen-Rotation auf null gesetzt und der Positions-Vektor
"posv" auf 200 m ("_pos_home_z") vor der ISS initialisiert wird.
Je nach Spiel-Level wird nun die Anzahl der Meteore "game_meteor_c"
differiert, die auf die ISS zufliegen werden. Ihre maximale Entfernung
von der ISS zu Beginn des Spieles steht in "game_meteor_dim". Und
über "game_meteor_speed" wird eingestellt, wie schnell die Meteore
später fliegen dürfen.
Es gilt: Je höher der Spiel-Level, desto grösser die Anzahl der
Meteore, desto kleiner die Entfernung von der ISS, und desto schneller
die Meteore. Während es beim Spiel-Level "_game_level_train" gar keine
Meteore gibt, sind es bei "_game_level_mad" deren sehr viele, sehr
schnelle und sehr nahe :-)
Alle im Spiel aktiven Meteore werden anschliessend auf dem TPanel
"meteorcp" sichtbar gemacht, anschliessend die Start-Bedingungen
über die Prozedur "meteor_set" gesetzt.
Indem das TPanel "gamestartp" unsichtbar gemacht wird, wechseln
wir automatisch in in den aktiven Spiel-Modus.
Der Spiel-Level gibt vor, wie viel Meteore im Spiel vorkommen und
abgeschossen werden müssen. Auch ihre Verteilung im Raum und ihre
Geschwindigkeit hängt davon ab. Die nötigen Einstellungen nimmt
die Prozedur "meteor_set" für jeden einzelnen Meteor extra vor:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
//---------------------------------------------------------------------
procedure thauptf.meteor_set(r:integer);
var
d:single;
pm:pmeteor;
begin
pm:=@meteor_a[r];
//set meteors away from iss
repeat
pm.posv.x:=random(game_meteor_dim)-random(game_meteor_dim);
pm.posv.y:=random(game_meteor_dim)-random(game_meteor_dim);
pm.posv.z:=random(game_meteor_dim)-random(game_meteor_dim);
d:=v_distance(iss_startv,pm.posv);
until d>game_meteor_dim/2;
pm.count:=0;
pm.radius:=5+random(_meteor_r)/10;
pm.state:=_meteor_state_fly;
pm.speed:=0.05+random(trunc(game_meteor_speed*100))/100;
meteor_imga[r].Visible:=true;
end;
Zuerst wird per Zufall eine Position des Meteors gesetzt. Danach
wird geprüft, ob die Distanz des Meteors zur ISS dem Spiel-Parameter
"game_meteor_dim/2" genügt (das wird über die "DanGeoU"-Funktion
"v_distance" berechnet). Ist der Meteor zu nahe, dann wird eine
neue, zufällige Position vergeben.
Anschliessend erhält der Meteor einen zufallsbedingt grossen
Radius, wird auf den Status "_meteor_state_fly" gesetzt,
und bekommt eine bestimmte Flug-Geschwindigkeit verpasst.
Das TPanel "gamestartp" ist unsichtbar, der TTImer "frequencyt"
ist aktiviert, die Spiel-Parameter alle gesetzt, alle Meteore
initialisiert - das Spiel hat begonnen.
Wie oben gezeigt, wird durch die Prozedur "frequencytTimer" in
regelmässigen Abständen die zentrale Prozedur "draw_scene"
aufgerufen, die wir uns jetzt näher anschauen wollen.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
//------------------------------------------------------------------
procedure thauptf.draw_scene;
begin
//get graphic mode
graphic_txok :=(graphic_mode and 4)>0;
graphic_earthok:=(graphic_mode and 2)>0;
graphic_spaceok:=(graphic_mode and 1)>0;
if graphic_txok then glPolygonMode(GL_FRONT_AND_BACK,GL_FILL)
else glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
//rotation & positioning
glRotatef(360-rotv.x,1.0,0,0);
glRotatef(360-rotv.y,0,1.0,0);
glRotatef(360-rotv.z,0,0,1.0);
glTranslatef(-posv.x,-posv.y,-posv.z);
if graphic_spaceok then draw_space;
if graphic_earthok then draw_earth;
//iss is gone away?
if
(game_iss_meteor<>-1)and
(meteor_a[game_iss_meteor].state=_meteor_state_explode)and
(meteor_a[game_iss_meteor].count<20)
then begin
end
else draw_iss;
//scene is active?
if
not gamestartp.visible and
not gameoverp.Visible or
(gameoverp.Visible and(game_iss_meteor<>0))
then begin
if game_level<>_game_level_train then draw_meteors;
draw_shots;
end;
if draw_path_ok then draw_path;
if draw_visor_ok then draw_visor;
SwapBuffers(handle_DC);
end;
Zu Beginn wird anhand des Parameters "graphic_mode" bestimmt,
welche Elemente der Szenerie gemalt werden sollen und welche
nicht. Zudem wird eingestellt, ob Texturen Verwendung finden
oder stattdessen nur ein Gittermodell angezeigt wird.
Insgesamt gibt es acht verschiedene Grafik-Modi, die die
Ausgabe über die booleschen Variablen "graphic_txok", "graphic_earthok"
und "graphic_spaceok" regulieren. Es gilt:
Grafik-Modus graphic_mode |
Texturen graphic_txok |
Erde graphic_earthok |
Weltall space_txok |
Beispiel |
0 |
Nein |
Nein |
Nein |
|
1 |
Nein |
Nein |
Ja |
|
2 |
Nein |
Ja |
Nein |
|
3 |
Nein |
Ja |
Ja |
|
4 |
Ja |
Nein |
Nein |
|
5 |
Ja |
Nein |
Ja |
|
6 |
Ja |
Ja |
Nein |
|
7 |
Ja |
Ja |
Ja |
|
Nachdem der Grafik-Modus bestimmt ist, wird mit "glClear" der
OpenGL-Speicher gelöscht und mit "glLoadIdentity" zum Ursprungsort
transportiert.
Es folgt die Ausrichtung des Modells gemässt des Rotation-Vektors
"rotv" sowie unsere Positionierung im Raum gemäss des Position-Vektors
"posv".
Je nach Grafik-Modus wird dann das Weltall und/oder die Erde
gezeichnet. Diese "draw_"-Routinen sehen wir uns gleich näher an.
Danach wird geprüft, ob die ISS noch vorhanden oder bereits durch
einen Meteor zerstört wurde. Gemalt wird sie natürlich nur, wenn
sie noch existiert.
Anschliessend werden die Meteore gezeichnet, sofern wir uns nicht
gerade im Spiel-Level "_game_level_train" befinden. Die Schüsse,
die wir abgegeben haben, werden nur so lange angezeigt, wie die ISS
nicht von einem Meteor getroffen wurde.
Der boolesche Parameter "draw_path_ok" gibt vor, ob unsere bisherige
Flugroute im Modell optisch gekennzeichnet werden soll.
Ein weiterer boolescher Parameter, "draw_visor_ok", reguliert,
ob das Visier vorhanden ist oder nicht.
Mittels der OpenGL-Prozedur "SwapBuffers" wird das Modell letztlich
auf dem TPanel "viewp" zur Anzeige gebracht.
Um den Hintergrund etwas interessanter zu gestalten, wird um
unserer Position herum eine gewaltige Kugel gezeichnet, innerhalb der
wir uns bewegen. Wir können diese Kugel praktisch nicht verlassen und
sehen daher stets auf ihre Innenwände.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
//space-------------------------------------------------
procedure thauptf.draw_space;
begin
glPushMatrix();
gldisable(GL_DEPTH_TEST);
glTranslatef(0,0,0);
glRotatef(90,1,0,0);
graphic_mode_bind(space_tx,0.1,0.1,0.1);
gluSphere(space_quad,_space_r,20,20);
graphic_mode_off;
glenable(GL_DEPTH_TEST);
glPopMatrix();
end;
Die Kugel wird mittels der OpenGL-Prozedur "gluSphere" realisiert.
Ihr Mittelpunkt ist im Ursprung, ihr Radius "_space_r" doppelt so
gross wie der Erdradius ("_earth_r", das sind 6368000 m).
Zuvor wird über die Prozedur "graphic_bind_mode" bestimmt, ob die
Kugel mit Texturen versehen oder als Gittermodell
gezeichnet werden soll.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
//--------------------------------------------------------
procedure thauptf.graphic_mode_bind(tx:gluint;r,g,b:single);
begin
if not graphic_txok then begin
glColor3f(r,g,b);
exit;
end;
glenable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D,tx);
end;
//--------------------------------------------------------
procedure thauptf.graphic_mode_off;
begin
glColor3f(1,1,1);
glDisable(GL_TEXTURE_2D);
end;
Falls der Grafik-Modus ein reines Gittermodell vorsieht, bleiben
die Texturen deaktiviert. Stattdessen wird jedoch die Farbe
angepasst, mit der die Gitterlinien gezeichnet werden sollen.
Die Prozedur "graphic_mode_off" schaltet Texturen generell ab,
egal, ob sie zuvor aktiviert wurden oder nicht.
Prozedur 'draw_space': Der Hintergrund wird gemalt. Eine riesige Sphäre um den Ursprung.
it einer dunklen Textur, die das Weltall wiedergibt.
Auch die Erde wird mittels "gluSphere" gezeichnet. Ihr Mittelpunkt
befindet sich zu Beginn des Spiels 345 km (Höhe ISS) plus 6368 km
(Erd-Radius) unter dem Ursprung.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
//----------------------------------------------------------------------
{ (0/0/0)
ISS
|
|y
|
(0/-345 km/0)
#######
#############################
#############################################
###################################################
###################################################
####################(0/-6368-345/0)####################
###################################################
###################################################
##############################################
#############################
########
}
//----------------------------------------------------------------------
procedure thauptf.draw_earth;
const
_scale=100; //needed because of depth buffer problems
begin
glPushMatrix();
//keep constant distance so you never reach surface
//needed because of the scaled hight
//which is much less then in reality
glTranslatef(posv.x,posv.y+_earth_y/_scale,posv.z);
graphic_mode_bind(earth_tx,0.5,0.5,1);
glRotatef(140,1,0,0);
glRotatef(85,0,1,0);
glRotatef(earth_rot,1,0,0);
gluSphere(earth_quad,_earth_r/_scale,50,50);
graphic_mode_off;
glPopMatrix();
end;
Anders als die Weltall-Kugel sehen wir die Erde von aussen.
Daraus resultierten - Grrr! - wieder einmal Probleme mit dem
Tiefenpuffer, denn die Erd-Kugel erschient stellenweise
transparent, sodass man durch sie hindurch ihre "Rückwand" sehen
konnte.
Wiedergabefehler bei der Prozedur 'draw_earth': Aufgrund ihrer immensen Grösse erscheint die Erdkugel zur Hälfte
transparent, weil der Tiefenpuffer-Bereich überschritten wird. Dies
muss verhindert werden. Doch wie?
Aus diesem Grund wird die Erde um den Faktor "_scale" (=100)
verkleinert, ebenso ihr Abstand zur ISS. Damit kam der
Tiefenpuffer glücklicherweise zurecht.
Jetzt bestand jedoch die "Gefahr", dass man in die
Erd-Kugel eintauchte, wenn ein paar Minuten senkrecht nach unten
geflogen wurde. Um dies zu verhindern, wandert der Mittelpunkt
der Sphäre parallel zu unserer Raum-Position mit, sodass stets ein
konstanter Abstand eingehalten wird. Egal, wie lange wir
auch fliegen, wir erreichen den Erdboden nie.
Bisschen billig, die Lösung, aber sie funzt.
Die ISS ist komplizierter aufgebaut als Weltall und Erde.
Sie setzt sich aus mehreren, sich teilweise wiederholenden,
Elementen zusammen. Die ASCII-Grafiken über dem Funktions-Rumpf
deuten an, wo wann was wie gross gemalt wird.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224
00225
00226
00227
00228
00229
00230
00231
00232
00233
00234
00235
00236
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
//----------------------------------------------------------------------
{
z-Axis (+)
|
|
|
#########*########## part_bigsolarsail
#########*########## part_bigsolarsail
---# part_smallsolarsail
* +
+++++*++++ part_center
* *
---# part_smallsolarsail
#########*########## part_bigsolarsail
#########*########## part_bigsolarsail
-----------+------------------------------------------ x-Axis (+)
(0/0/0)
|
|
z-Axis (-)
}
//----------------------------------------------------------------------
procedure thauptf.draw_iss;
const
_cylr=2.5;
_cyls=20;
_bigsailw =4;_bigsailh =70;
_smallsailw=1;_smallsailh=20;
//------------------------------------------
{
y-Axis (+)
|
|
#####
#####
#####
#####
##### h
#####
#####
-----------+--------------------- x-Axis (+)
|
w
}
//------------------------------------------
procedure solarsail(width,height:single;tx:gluint);
begin
graphic_mode_bind(tx,1,1,1);
glbegin(gl_quads);
gltexcoord2f( 0,0);glvertex3f(0-(width/2),0+(height/2),0);
gltexcoord2f( 0,1);glvertex3f(0+(width/2),0+(height/2),0);
gltexcoord2f(10,1);glvertex3f(0+(width/2),0-(height/2),0);
gltexcoord2f(10,0);glvertex3f(0-(width/2),0-(height/2),0);
glend;
graphic_mode_off
end;
//------------------------------------------
{
A*
E###############***#################B
***
D###############***#################C 4m
70m ***
5m
}
//------------------------------------------
procedure part_bigsolarsail;
begin
glPushMatrix();
//A) draw body-cylinder
graphic_mode_bind(iss_big_tx,1,0.5,0.5);
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
graphic_mode_off;
//positioning for solar sails
glRotatef(90,1,0,0); //tilt back/forward
glRotatef(90,0,0,1); //turn right
glTranslatef(13-_bigsailw,0,0); //shift forward
glTranslatef(0,_bigsailh/2+_cylr,0); //shift right
//B)big solar sail
solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);
//C)big solar sail
glTranslatef(-_bigsailw-1,0,0); //shift back
solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);
//D)big solar sail
glTranslatef(0,-_bigsailh-_cylr*2,0); //shift left
solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);
//E)big solar sail
glTranslatef(_bigsailw+1,0,0); //shift forward
solarsail(_bigsailw,_bigsailh,iss_big_sail_tx);
//small solar sail under cylinder
glTranslatef(0,_bigsailh/2+_cylr,0); //shift right
glTranslatef(-2.5,0,0); //shift back
glTranslatef(0,0,_smallsailh/2/2+_cylr); //shift down
glRotatef(90,1,0,0);
solarsail(_smallsailw,_smallsailh/2,iss_small_sail_tx);
glPopMatrix();
end;
//------------------------------------------
{
z-Axis (+) y-Axis (+)
| |
A** A*****
*** B#######******
ABC######*** C#######******
20m *** D#######******
*** ******
| |
---------+-------- -----------+----- x-Axis (+)
}
//------------------------------------------
procedure part_smallsolarsail;
begin
glPushMatrix();
//A)body cylinder
graphic_mode_bind(iss_small_tx,1,1,1);
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
graphic_mode_off;
//positioning for sails
glRotatef(90,0,0,1); //turn right
glTranslatef(0,0,13/2); //shift forward
glTranslatef(0,-_smallsailh/2-_cylr,0); //shift left
glTranslatef(1,0,0); //shift up
//B)small solar sail
solarsail(_smallsailw,_smallsailh,iss_small_sail_tx);
//C)small solar sail
glTranslatef(-_smallsailw-0.2,0,0); // shift down
solarsail(_smallsailw,_smallsailh,iss_small_sail_tx);
//D)small solar sail
glTranslatef(-_smallsailw-0.2,0,0); // shift down
solarsail(_smallsailw,_smallsailh,iss_small_sail_tx);
glPopMatrix();
end;
//------------------------------------------
{
z-Axis (+) y-Axis (+)
| |
A** B** |
*** *** 20m | 13m
D#######***C######*** ##########A**C#######B**
########***#######*** ##########***########***
20m *** 13m *** G** F**
*** *** *** ***
| ***E##***
| ***###***
| T-Part U-PART |
----------+-------- -----------+----- x-Axis (+)
}
//------------------------------------------
procedure part_center;
begin
glPushMatrix();
graphic_mode_bind(iss_small_tx,0.5,0.5,0.5);
//A)body cylinder
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
//t-part-------------------------------------------
//B)parallel cylinder right
glTranslatef(-13-_cylr,0,0); //shift right
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
//C)vertical connection
glTranslatef(1,0,0); //shift left
glTranslatef(0,0,13/2); //shift forward
glRotatef(90,0,1,0); //rotate y-axis
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
//u-part-------------------------------------------
//D)cylinder left
glTranslatef(0,0,20-_cylr-1); //shift left
glucylinder(iss_quad,_cylr,_cylr,20,_cyls,_cyls);
//E)cylinder paralell under
glTranslatef(0,-13,0); //shift down
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
//F)horizontal right
glRotatef(90,1,0,0);
glTranslatef(0,0,-11); //shift up
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
//G)parallel left
glTranslatef(0,11,0);
glucylinder(iss_quad,_cylr,_cylr,13,_cyls,_cyls);
graphic_mode_off;
glPopMatrix();
end;
begin
glPushMatrix();
glTranslatef(iss_startv.x,iss_startv.y,iss_startv.z);
//parts with big solar sail
glRotatef(-iss_big_sail_rot*10,0,0,1);
part_bigsolarsail;glTranslatef(0,0,13);
glRotatef(-5,0,0,1);
part_bigsolarsail;glTranslatef(0,0,13);
glRotatef(+iss_big_sail_rot*10+5,0,0,1);
//part width small solar sail
glRotatef(iss_small_sail_rot,0,0,1);
part_smallsolarsail;glTranslatef(0,0,13);
glRotatef(-iss_small_sail_rot,0,0,1);
//part in the middle width t- and u-part
part_center;glTranslatef(0,0,13);
//part width small solar sail
glRotatef(iss_small_sail_rot,0,0,1);
part_smallsolarsail;glTranslatef(0,0,13);
glRotatef(-iss_small_sail_rot,0,0,1);
//parts with big solar sail
glRotatef(frequ_low*5,0,0,1);
part_bigsolarsail;glTranslatef(0,0,13);
glRotatef(5,0,0,1);
part_bigsolarsail;glTranslatef(0,0,13);
glPopMatrix();
end;
Die Konstruktion im 3-dimensionalen Raum ist relativ schwierig,
weil nach jeder Rotation die Winkel in eine andere Richtung weisen,
als man vielleicht vermuten würde.
Statt z.B. ein Element der ISS, etwa ein Solar-Segel, durch Verringerung
des "z"-Wertes weiter nach hinten zu platzieren, passiert es dann, dass
es nach einer Rotation von 90 Grad um die y-Achse weiter links
landet. Tatsächlich müsste jetzt nämlich der "x"-Wert erhöht
werden, um den gewünschten Effekt zu erreichen.
Aus diesem Grund habe ich in der Konstruktionsphase einen zusätzliche
Positions- und einen zusätzlichen Rotations-Vektor verwaltet. Diese
liessen sich über bestimmte Tastatur-Eingaben verändern. Vor der
Platzierung eines ISS-Elements liess ich das Modell dann gemäss
dieser Vektoren rotieren und transportieren. So war leicht zu erkennen,
welcher Wert optisch was bewirkt.
Die Erläuterung, wie die ISS nun genau konstruiert wird, ist müssig
und erspare ich uns hier.
Prozedur 'draw_iss': Wiedergabe der internationale Raumstation ISS in unserem OpenGL-Raum.
Hier sieht man sie schräg von unten. Was man nicht sieht: Die grossen und
kleinen Segel rotieren in periodischen Abständen stufenweise um den Rumpf.
Nach der ISS werden in "draw_scene" die Meteore gezeichnet - auf ihrem
zerstörerischen Weg direkt zur Raumstation ...
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
00064
00065
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081
00082
00083
00084
00085
00086
00087
00088
00089
00090
00091
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141
00142
00143
00144
00145
00146
00147
00148
00149
00150
00151
00152
00153
00154
00155
00156
00157
00158
00159
00160
00161
00162
00163
00164
//--------------------------------------------------
procedure thauptf.game_points_set;
begin
game_pointsp.caption:=inttostr(game_points);
end;
//-----------------------------------------------------
procedure thauptf.draw_meteors;
//-------------------------------------------
procedure draw_meteor(r:integer);
var
pm:pmeteor;
begin
pm:=@meteor_a[r];
glPushMatrix();
glTranslatef(pm.posv.x,pm.posv.y,pm.posv.z);
//exploding meteor?
if pm.state=_meteor_state_explode then begin
graphic_mode_bind(meteor_explode_tx,1,0.8,0.6);
pm.radius:=pm.radius*1.3;
pm.count:=pm.count-1;
gluSphere(meteor_quad,pm.radius,10,10);
end
else begin
graphic_mode_bind(meteor_tx,0.8,0.8,0.8);
glRotatef(90+frequ_fast*4,1,1,0);
gluSphere(meteor_quad,pm.radius,5,5);
end;
graphic_mode_off;
glPopMatrix();
end;
//-------------------------------------------
function gozero(r:integer):tv;
var
av,nv:tv;
begin
//get meteor position
av:=meteor_a[r].posv;
//norm direction
nv:=v_norm(av);
//adapt speed of change
nv:=v_scale(nv,meteor_a[r].speed);
//calculate new position
av:=v_sub(av,nv);
result:=av;
end;
var
r,rr:integer;
diss1,diss2,dissmin,
dme,dmemin,
dshot:single;
pm:pmeteor;
ps:pshot;
gameoverok:bool;
begin
glPushMatrix();
dissmin:=game_meteor_dim;
dmemin:=game_meteor_dim;
gameoverok:=true;
//walk all meteors
for r:=0 to game_meteor_c-1 do begin
pm:=@meteor_a[r];
if pm.state=_meteor_state_none then continue;
//at least one active meteor: game is running
gameoverok:=false;
//meteor is destroyed?
if(pm.state=_meteor_state_explode)and(pm.count=0) then begin
//dirty meteor who destroyed the iss?
if game_iss_meteor=r then begin
draw_meteor(r);
game_over_show;
exit;
end;
//hide meteor
meteor_imga[r].Visible:=false;
pm.state:=_meteor_state_none;
//more distance to iss gives more game_points
dshot:=v_distance(iss_startv,pm.posv);
game_points:=game_points+trunc(dshot)*10;
game_points_set;
continue;
end;
//let meteor fly to iss
pm.posv:=gozero(r);
//meteor is solid?
if pm.state<>_meteor_state_explode then begin
//mark nearest distance from iss
diss1:=v_distance(iss_startv,pm.posv);
if diss1>=pm.radius then diss1:=diss1-pm.radius;
diss2:=v_distance(iss_endv,pm.posv);
if diss2>=pm.radius then diss2:=diss2-pm.radius;
if diss1<dissmin then dissmin:=diss1;
if diss2<dissmin then dissmin:=diss2;
//meteor hits iss?
if LineInSphere(iss_startv,iss_endv,pm.posv,pm.radius,false)then begin
cockpitp.Color:=clred;
detect_issp.caption:=f2s_cut(0);
if game_iss_meteor=-1 then begin
game_iss_meteor:=r;
flyhome;
exit;
end;
end;
end;
//meteor is hitting by shot?
for rr:=0 to _shot_c-1 do begin
ps:=@shot_a[rr];if ps.count=0 then continue;
if LineInSphere(ps.posv,ps.poslastv,pm.posv,pm.radius,false)then begin
//yes, change state to explode
pm.state:=_meteor_state_explode;
pm.count:=10;
dosound(sound_explode);
end;
end;
//draw dirty meteor
draw_meteor(r);
//detect nearest meteor to me
dme:=v_distance(posv,pm.posv);
if dme>=pm.radius then dme:=dme-pm.radius;
if dme<dmemin then dmemin:=dme;
end;
glPopMatrix();
//iss meteor alarm?
detect_issp.caption:=f2s_cut(dissmin);
if dissmin<_meteor_alarm then begin
meteorp.color:=clred;
if game_iss_meteor=-1 then dosound(sound_alarm);
end
else begin
meteorp.color:=clblack;
end;
//nearest meteor to me
detect_mep.caption:=f2s_cut(dmemin);
//all meteors destroyed?
if gameoverok then game_over_show;
end;
Die maximale Anzahl aktiver Meteore wird Level-abhängig von
"game_meteor_c" vorgegeben. In einer Schleife werden alle Meteore
des "meteor_a"-Arrays einzeln abgerufen und über den Pointer "pm" angesprochen.
Meteore kennen drei Status ("pm.state"):
- "_meteor_state_none": Der Meteor ist inaktiv/zerstört
- "_meteor_state_fly": Der Meteor ist unterwegs zur ISS
- "_meteor_state_explode": Der Meteor ist gerade am explodieren
Hat der aktuell betrachtete Meteor den Status "_meteor_state_non", dann ist er
inaktiv und wird nicht weiter beachtet.
Hat ein Meteor den Status "_meteor_state_explode", dann fliegt er gerade
auseinander. Die Dauer der Explosion hängt vom Meteor-eigenem Zähler
"pm.count" ab. Dieser wird zu Beginn auf einen bestimmten Wert hochgesetzt
und innerhalb jeder Zeiteinheit um eins dekrementiert. Steht der Zähler
schliesslich auf null, hat der Meteor ausgedient und verpufft ins Nichts.
Im Explosionsfall wird zusätzlich geprüft, ob die Zerstörung des Meteors
durch einen Anwender-Schuss verursacht wurde oder durch Kollision
mit seinem Ziel, der ISS.
Bei Explosion durch ISS-Kontakt endet das Spiel an dieser Stelle; es wird
dann nur noch die Prozedur "game_over_show" aufgerufen.
Hat dagegen der Anwender den Meteor erfolgreich zerstört, bekommt
er Punkte auf seinem Konto gutgeschrieben. Dabei gilt, dass er
umso mehr Punkte bekommt, je weiter der Meteor von der
ISS entfernt war.
Ist der Meteor dagegen noch fröhlich unterwegs, wird über die interne
Prozedur "gozero" seine Position neu berechnet. Diese wird so
kalkuliert, dass der Meteor sich der ISS unweigerlich nähert.
Falls der Meteor noch völlig intakt sein sollte ("pm.state ungleich _meteor_state_explode"),
wird sein aktueller Abstand zur ISS berechnet. Die dabei gefundene
geringste Distanz aller Meteore wird in "dissmin" gemerkt.
Weiterhin bleibt zu prüfen, ob der Meteor von einem Anwender-Schuss
getroffen wurde. Dazu werden in einer Schleife alle Schüsse des
"shot_a"-Arrays durchgegangen.
Steht der Zähler "ps.count" des aktuell betrachteten Schusses auf null,
befindet er sich noch im Magazin und wird daher ignoriert.
Ansonsten wird seine aktuelle Position ("ps.posv") und seine vorherige
Position "ps.poslastv" als Anfang und Ende einer Linie interpretiert.
Die Funktion "LineInSphere" testet anschliessend, ob diese Linie an irgendeiner
Stelle die Meteor-Kugel berührt.
Ist dies der Fall, wurde der Meteor getroffen. Er wechselt auf den
Status "_state_meteor_explode" und bekommt seinen Explosions-Zähler
"pm.count" hochgesetzt.
Es folgt die Ausgabe des Meteors über die interne Prozedur "draw_meteor".
Je nach Status wird eine passende Textur vergeben und im Explosionsfall
zusätzlich der Radius vergrössert sowie der Explosions-Zähler
dekrementiert.
Prozedur 'draw_meteors': Alle Meteore werden über nur eine Prozedur im OpenGL-Raum wiedergegeben.
Hier hat einer dieser Gesteinsbrocken gerade die ISS erwischt - und
explodiert nun als hell erleuchtete Feuerkugel.
Schliesslich wird noch der Abstand zu unserem Raumschiff berechnet.
Die so gefundene geringste Distanz aller Meteore wird in "dmemin"
gesichert und später im TPanel ""detect_mep" ausgegeben:
Meteor-Detektor Raumschiff (ME): Die MD-ME-Anzeige zeigt an, wie weit der nächste Meteor von unserem Raumschiff
entfernt ist. Man kann diese Anzeige daher nutzen, um die Meteore in den Weiten
des Alls aufzuspüren. So kann man sie gezielt vernichten, bevor sie die ISS erreichen.
Nachdem schliesslich alle Meteore abgearbeitet wurden, bleibt nur noch
zu prüfen, ob der der ISS nächste Meteor den kritischen Wert von
"_meteor_alarm" Metern unterschritten hat. Ist dem so, wird ein
Alarmsignal initiiert.
Meteor-Detektor ISS: Die MD-ISS-Anzeige zeigt an, wie weit weg sich der nächste Meteor von der
ISS befindet. Hier ist einer dieser Gesteinsbrocken gerade innerhalb des
kritischen Bereichs um die ISS geflogen - und der Detektor heult sofort los.
Den folgenden Source habe ich im Web gefunden und nur etwas für
meine Zwecke adaptiert.
Die in C++ geschriebene Funktion hiess ursprünglich
"LineHitsSphere" und lieferte nur dann "true" zurück,
wenn die Linie die Kugel komplett durchquerte.
Mir genügt es, wenn auch nur Teile der Linie irgendwo im
Inneren der Kugel auftauchen. Daher heisst das Teil bei mir
auch "LineInSphere":
Funktion 'LineInSphere' - Fall 1: Der Vektor 'v' berührt die Sphäre 'S' an keiner Stelle. 'LineInSphere' liefert 'false' zurück.
Funktion 'LineInSphere' - Fall 2: Der Vektor 'v' beginnt im Inneren der Sphäre 'S',
durchkreuzt sie also nicht vollständig. 'LineInSphere' liefert in diesem Fall
nur dann 'true' zurück, wenn 'crossok' inaktiv ist.
Funktion 'LineInSphere' - Fall 3: Der Vektor 'v' durchquert die Sphäre 'S'
vollständig. 'LineInSphere' liefert unabhängig von 'crossok'
stets 'true' zurück.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
00059
00060
00061
00062
00063
// check if a line starts in sphere or cross the sphere.
// crossok is true: result only true if line cross the sphere completly
// http://www.koders.com/cpp/fidF5B34643282700B33C8E7A9A45877FD5F77D9FB8.aspx?s=cdef%3Atree
function thauptf.LineInSphere(v1,v2,spherev:tv;radius:single;crossok:bool):bool;
var
dirv,diffv:tv;
linelen,a,b,Discriminant,root,cross:single;
begin
result:=false;
// Länge und normalisierte Richtung der Linie berechnen
//Vector3D LineDir(LineB - LineA);
//const float LineLength = LineDir.Length();
//LineDir /= LineLength;
//direction of line
dirv:=v_sub(v2,v1);
linelen:=v_len(dirv);
//norm line direction
dirv:=v_norm(dirv);
// Die zwei Hilfsvariablen a und b berechnen
//const Vector3D Diff(LineA - Sphere);
diffv:=v_sub(v1,spherev);
{
const float a = 2.0f * (LineDir.x * Diff.x +
LineDir.y * Diff.y +
LineDir.z * Diff.z);
}
a:=2*(dirv.x*diffv.x+dirv.y*diffv.y+dirv.z*diffv.z);
//const float b = Diff.LengthSq() - Radius * Radius;
b:=v_len(diffv);
b:=b*b-radius*radius;
// Die Diskriminante berechnen (a²/4 - b)
//const float Discriminant = ((a * a) * 0.25f) - b;
Discriminant:=((a*a)*0.25)-b;
// Wenn die Diskriminante kleiner als null ist,
// dann gibt es keine Schnittpunkte.
if Discriminant<0 then exit;
// Die Wurzel und danach die zweite Lösung der Gleichung berechnen
//const float Root = sqrtf(Discriminant);
root:=sqrt(Discriminant);
//const float s = a * -0.5f - Root;
cross:=-a/2-root;
if cross>LineLen then exit;
// Ist dieser Schnittpunkt in Ordnung?
//nur true, wenn line ganz durchgeht
//if(cross<0)or(cross>LineLen)then exit;
//true
if crossok and(cross<0)then exit;
result:=true;
end;
Ich habe mir nicht die Mühe gemacht, den Source völlig zu verstehen.
Daher gibt es hier auch keine weitere Erklärung dazu.
Wie wir im Kapitel "Wenn der User drückt" gesehen haben, bewirkt die
"Space"-Taste, dass die Prozedur "shot" aufgerufen wird. Hier werden
die Start-Parameter der Schüsse berechnet, die in "draw_scene" mittels
Prozedur "draw_shots" ausgegeben werden.
Betrachten wir zunächst "shots":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
//generate a shot-circle----------------------------------------
procedure thauptf.shot;
const
_max=10;
var
v:tv;
c,r:integer;
deg:single;
ps:pshot;
begin
dosound(sound_shot);
v:=getlookv;
for c:=0 to _max-1 do begin
//next free shot
ps:=nil;
for r:=0 to _shot_c-1 do begin
if shot_a[r].count=0 then begin
ps:=@shot_a[r];
break;
end;
end;
if ps=nil then break;
//direction & start-position
ps.dirv:=v;
ps.posv:=posv;
ps.count:=20;
if c=0 then begin
//after first shot adapt angle for circle
v:=v_rot_y_deg(v,2);
v:=v_rot_x_deg(v,2);
end;
//calculate angles for circle
deg:=(c+1)*360/_max;
v:=v_rot_y_deg(v,sin(deg)*5);
v:=v_rot_x_deg(v,cos(deg)*5);
end;
inc(shot_c);
//every shot costs game_points
game_points:=game_points-100;
if game_points<0 then game_points:=0;
game_points_set;
end;
Ähnlich wie beim Geradeausflug wollen wir, dass die Geschosse
exakt in diejenige Richtung fliegen, die unser Blick
in den Raum hinein vorgibt.
Dazu holen wir uns den aktuellen Richtungsvektor - also
den ursprünglichen Richtungsvektor (0,0,-1) rotiert um alle
Achsen - über die bereits beschriebene Funktion "getlookv".
Jetzt wird das Schuss-Array "shot_a" durchlaufen und nach insgesamt
"_max" (=10) freien Geschossen durchsucht. Finden wir keine,
können wir nichts abfeuern und müssen warten, bis die letzten, noch
aktiven Schüsse abgeklungen sind.
Haben wir ein freies Geschoss "ps" gefunden, so erhält es unsere aktuelle
Position ("ps.posv") und die berechnete Richtung ("ps.dirv"). Der Zähler
"ps.count" wird hochgesetzt und gibt an, wie viele Zeiteinheiten der
Schuss unterwegs sein wird.
Der erste Schuss fliegt exakt in die Mitte, die weiteren
Schüsse werden so berechnet, dass sie sich kreisförmig um das Zentrum
anordnen. Dazu nutzen wir die Formeln für die Vektor-Rotation
um x- und y-Achse.
Am Schluss werden dem Spieler noch Punkte abgezogen. Man sollte also
nicht zu wahllos herumschiessen, wenn man ernsthaft einen neuen Highscore
anvisiert.
Und nun die Ausgabe der Geschosse durch die Prozedur "draw_shots":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
//draw the shot-circle-------------------------------------------
procedure thauptf.draw_shots;
var
r:integer;
v:tv;
ps:pshot;
begin
graphic_mode_bind(meteor_tx,1,1,0.5);
for r:=0 to _shot_c-1 do begin
ps:=@shot_a[r];
//shot on the qay?
if ps.count=0 then continue;
//save actual position
v:=ps.posv;
ps.poslastv:=v;
//calculate new position
v:=v_add(v,ps.dirv);
ps.posv:=v;
//speed up shot
ps.dirv:=v_scale(ps.dirv,1.5);
//decrement activity-counter
ps.count:=ps.count-1;
//draw shot
glTranslatef(v.x,v.y,v.z);
gluSphere(meteor_quad,1,5,5);
glTranslatef(-v.x,-v.y,-v.z);
end;
graphic_mode_off
end;
Das Schuss-Array "shot_a" wird durchlaufen und nach aktiven
Geschossen ("count ungleich 0") durchsucht. Werden keine gefunden,
gibt es nichts weiter zu tun und wir verlassen die Prozedur wieder.
Aktive Geschosse erhalten den Pointer "ps". Wir sichern die
aktuelle Position des Schusses in "ps.poslastv". Die neue
Position ergibt sich durch Vektor-Addition der aktuellen
Position mit dem Richtungsvektor, der in der Prozedur "shots"
in "ps.dirv" eingetragen wurde.
Die letzte Position und die neue aktuelle Position
des Schusses beschreiben eine virtuelle Linie, die das
Geschoss innerhalb der letzten Zeiteinheit durchflogen hat. Diese
Linie wird verwendet, um festzustellen, ob ein Meteor getroffen
wurde (siehe Kapitel "Angriff der Killer-Meteore" und
"Linie trifft Sphäre").
Mittels "v_scale", angewendet auf "ps.dirv", sorgen wir weiterhin
dafür, dass das Geschoss zunehmend schneller wird.
Der Zähler "ps.count" wird dekrementiert. Hat er Null erreicht,
wissen wir später, dass das Geschoss verpufft ist. Es steht dann
für den nächsten Schuss wieder frei zur Verfügung.
Zuletzt wird der Schuss-Salve dann über die OpenGL-Prozedur "gluSphere"
in den Grafikpuffer gemalt.
Prozedur 'draw_shots': Wiedergabe der Schüsse, die von unserem Raumschiff ausgehen. Hier ist
gerade eine Schuss-Salve unterwegs zur ISS,
was glücklicherweise aber keine negativen Auswirkungen auf dieselbe hat.
Kehren wir zu "draw_scene" zurück. Weltall, Erde und ISS wurden
gezeichnet. Die Meteore, so weit vorhanden, neu positioniert. Unsere
Schüsse sind unterwegs. Nun folgt die Prozedur "draw_path":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
//set new path-point----------------------------------------------------
procedure thauptf.path;
begin
path_a[path_c]:=posv;
inc(path_c);if path_c>_path_c-1 then path_c:=0;
end;
//draw the path in fast triangle mode------------------------------------
procedure thauptf.draw_path;
var
r:integer;
begin
glPushMatrix();
for r:=0 to _path_c-1-1 do begin
if(path_a[r].x=0)or(path_a[r+1].x=0) then continue;
//change color
if r mod 2=0 then glColor3f(1.0,0.5,0.5)
else glColor3f(0.5,1.0,0.5);
glBegin(GL_TRIANGLE_STRIP);
glVertex3f(path_a[r].x,path_a[r].y,path_a[r].z);
glVertex3f(path_a[r].x+1,path_a[r].y+1,path_a[r].z+1);
glVertex3f(path_a[r+1].x,path_a[r+1].y,path_a[r+1].z);
glEnd();
end;
glColor3f(1.0,1.0,1.0);
glPopMatrix();
end;
Hier wird eine Art Kondensstreifen aufgebaut, der veranschaulicht,
welchen Weg unser Raumschiff zurückgelegt hat. Die Pfadpunkte
des Arrays "path_a" werden dazu in der Prozedur "path" gesetzt,
welche periodisch über die Prozedur "frequencytTimer" aufgerufen
wird.
Zur Anzeige des Pfades wird dann in "draw_path" einfach das
"path_a"-Array durchlaufen und aus aktuellem und folgendem Punkt
ein Dreieck generiert, was gemäss seiner Koordinaten als
OpenGL-Grafik vom Typ "GL_TRIANGLE_STRIP" wiedergegeben wird.
Über einen Modulo-Vergleich mit der Schleifenvariablen "r" wird
zusätzlich dafür gesorgt, dass die Dreiecke abwechselnd rot und
grün gezeichnet werden.
Prozedur 'draw_path': Der Weg, den unser Schiff innerhalb eines bestimmten Zeitraumes im Raum zurückgelegt hat, kann optional angezeigt werden.
Als letzte "draw_"-Aktion in "draw_scene" fehlt jetzt nur noch
das Visier mit dem Fadenkreuz, welches angibt,
wohin wir beim Geradeaus-Flug steuern bzw. schiessen. Diesen
Job übernimmt die Prozedur "draw_visor":
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
//visor-------------------------------------------------------
procedure thauptf.draw_visor;
var
rd:integer;
begin
glPushMatrix();
gldisable(GL_DEPTH_TEST);
glLoadIdentity;
glTranslatef(0,0,-20);
glLineWidth(2);
rd:=3;
//cross
glLineWidth(1);
glBegin(GL_LINES);
glVertex3f(-rd,-rd,rd);glVertex3f(rd,rd,rd);
glVertex3f(rd,-rd,rd);glVertex3f(-rd,rd,rd);
glEnd();
//frame
glLineWidth(2);
glBegin(GL_LINE_LOOP);
glVertex3f( rd, rd, rd);
glVertex3f(-rd, rd, rd);
glVertex3f(-rd,-rd, rd);
glVertex3f( rd,-rd, rd);
glEnd();
glLineWidth(1);
glenable(GL_DEPTH_TEST);
glPopMatrix();
end;
Der Tiefenpuffer wird abgeschaltet, damit das Fadenkreuz
nicht von weiter vorne liegenden Objekten verdeckt wird.
Anschliessend wird der OpenGL-"Malstift" 20 Meter vor uns
positioniert. Dorthin wird dann das Fadenkreuz nebst einem
Rahmen platziert.
Prozedur 'draw_visor': Wiedergabe des Visiers im Cockpit unseres Raumschiffes. Hier wurde gerade
ein Meteor in die Mitte genommen. Bald hat er
sein Leben ausgehaucht. Und das möglichst, bevor er die ISS erreicht ...
Ein paar Mal wurde der automatische Heimflug bereits erwähnt.
Wenn wir uns zu weit von der ISS entfernt oder die Orientierung verloren
haben, dann genügt ein Druck auf die "F"-Taste, und wir werden vom
Bordcomputer wieder in die Ausgangsposition direkt vor der ISS gebracht.
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
//-----------------------------------------------------------
procedure thauptf.flyhome;
begin
flyhome_c:=100;
//delta position
pspeed:=1;
dposv.x:=-(posv.x)/flyhome_c;
dposv.y:=-(posv.y)/flyhome_c;
dposv.z:=-(posv.z-_home_pos_z)/flyhome_c;
//delta rotation
rx_speed:=(rotv.x)/flyhome_c;
ry_speed:=(rotv.y)/flyhome_c;
rz_speed:=(rotv.z)/flyhome_c;
end;
Obige Prozedur "flyhome" setzt den Zähler "flyhome_c" hoch.
Abschliessend werden die Positions- und Rotations-Deltas berechnet,
die nötig sind, um uns aus aktueller Position und Richtung in
"flyhome_c" Zeiteinheiten bis zur Start-Position zurückzuführen.
Um möglichst viele Spiel-Punkte zu ergattern, muss man sich im Spiel
möglichst weit von der ISS entfernen und Meteore zerstören. Dabei
sollte man gut den Meteor-Detector im Auge behalten. Einige Sekunden,
bevor der losheult, kann man dann über den automatischen Heimflug
rasch zur ISS zurückkehren.
Man beachte jedoch, dass, während der Bordcomputer das Kommando
hat, keinerlei Aktionen des Piloten möglich sind ...
Das Spiel ist aus, wenn ...
- ... die "Escape"-Taste gedrückt wurde
- ... alle Meteore zerstört sind
- ... die ISS von einem Meteor getroffen wurde
In allen drei Fällen wird die Prozedur "game_over_show" aufgerufen:
00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
//--------------------------------------------------
procedure thauptf.game_over_show;
var
hs:integer;
s:string;
begin
if game_level=_game_level_train then begin
//no high score needed
game_start_show;
exit;
end;
if game_iss_meteor<>-1 then game_status:='THE ISS IS GONE AWAY :-('
else game_status:='YOU LUCKY BASTARD WIN :-)';
if game_level=_game_level_rookie then begin
if game_points>game_hs_rookie then game_hs_rookie:=game_points;
s:='Rookie';
hs:=game_hs_rookie;
end
else if game_level=_game_level_advanced then begin
if game_points>game_hs_advanced then game_hs_advanced:=game_points;
s:='Advanced';
hs:=game_hs_advanced;
end
else if game_level=_game_level_profi then begin
if game_points>game_hs_profi then game_hs_profi:=game_points;
s:='Profi';
hs:=game_hs_profi;
end
else begin
if game_points>game_hs_mad then game_hs_mad:=game_points;
s:='Mad';
hs:=game_hs_mad;
end;
game_pointsl.Caption:=inttostr(game_points);
game_levell.caption:='LEVEL '+s+' HIGHSCORE';
game_hsl.caption:=inttostr(hs);
gameoverp.visible:=true;
end;
Ist der Spiel-Level "_game_level_train" aktiv, wird über die
Prozedur "game_start_show" direkt zum Start-Panel weitergeleitet.
Ansonsten wird der Level-Highscore ermittelt und
gegebenenfalls mit dem aktuellen Punktestand überschrieben.
Zuletzt wird dann einfach das TPanel "gameoverp" sichtbar gemacht.
Prozedur 'game_over_show' - Fall 1: Spielende. Und diesmal war es das Ende einer erfolgreichen Schlacht.
Prozedur 'game_over_show' - Fall 2: Spielende. Du hast verloren. Und damit auch der ISS ein trauriges Ende bereitet.
Zum Schluss noch ein paar Screenshots von "OGL_ISS" in Action:
Ach ja, ich liebe OpenGL!
Als Kind hatte mich auf dem C64 nichts mehr fasziniert,
als Vektor-Grafik-Spiele wie "Elite" und "Escape from Mercenary".
Monatelang habe ich mich damals damit beschäftigt, deren virtuelle Welten
zu erforschen. Nicht in meinen kühnsten Träumen hätte ich mir jedoch
zugetraut, je auch nur etwas annähernd Ähnliches selbst
programmieren zu können.
Klar, "OGL_ISS" hat bei Weitem nicht die Komplexität oder den
Spielwitz der grossen Vorbilder. Aber optisch schlägt es
sie allemal.
Ecken und Macken gibt es dennoch genug, obwohl "OGL_ISS" für meine
Verhältnisse eigentlich ziemlich fehlerfrei geraten ist.
Die Flug-Steuerung macht manchmal nicht das, was sie tun
sollte. Möglich, dass das mit dem berüchtigten
"Gimbal Lock"
zu tun hat - ein Problem, mit dem offenbar sogar die NASA-Ingenieure
zu kämpfen haben.
Des Weiteren purzeln die Sound-Effekte gerne wüst durcheinander.
Und die Highscore-Listen - hüstel! - sind mit nur einem Eintrag
je Level wahrlich auch auch mehr als bescheiden ausgefallen ...
So etwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy.
Ist halt nichts 100%iges. But who cares?
"OGL_ISS" wurde mit Delphi 7 programmiert. Der komplette Source, die EXE
und die Bibliotheksdatei "opengl32.dll" sind alle in diesem ZIP-Archiv verpackt:
OGL-ISS.zip (ca. 2,6 MB)
Das Original-OpenGL-Paket für Delphi habe ich von "http://www.delphigl.com"
gesaugt. Das ZIP-File des Installers der Version "DGLSDK 2006.1",
die ich verwendet habe, findet ihr hier:
dglsdk-2006-1.zip (ca. 8 MB)
Have fun!