Börsen-Infos bei
|
OGL_Henrys - Eine kleine Welt im Eigenbau
OGL_Henrys-Tutorial von Daniel Schwamm (02.01.2008)
Aus "Heimat des Dilettantismus"
http://www.henrys.de/daniel/index.php?cmd=software_dirty-progs_opengl-henrys_index.htm
Mit Open GL neue Welten entdecken
|
|
| Demo-Movie #1: Im Flur von HENRY's Auktionshaus |
|
|
| Demo-Movie #2: Flug über HENRY's Auktionshaus |
|
|
| Demo-Movie #3: Auf dem Weg in Daniels Büro |
Verlixt, es ist exakt 3:15 Uhr, als ich diesen Bericht beginne. Kann nicht
schlafen. Blödes Hirn will nicht aufhören zu arbeiten. Bin ich halt an den PC
gekrabbelt und tippe etwas runter ...
Das gutes, alte VRML
1997 hatte ich meine erste Berührung mit 3D-Welten, die man selbst basteln konnte.
Mit VRML, einer Scriptsprache, die man Netscape beibringen konnte. Damit liessen sich
Ebenen im Raum schaffen, über die man hinwegfliegen konnte. Mit Texturen versehen
sah das schon recht beeindruckend aus. Allerding ging mein damaliger PC ziemlich
schnell in die Knie, so wie die Welten etwas komplexer wurden.
Seitdem hat sich einiges getan: Die Rechner wurden schneller, die Grafikkarten
leistungsfähiger, die Software ausgereifter. Allerding, so scheint es, ist VRML tot.
Delphi kann auch OGL
Im Laufe der Zeit habe ich einige "3D-Engines" in Delphi programmiert, für Spiele
und Grafikdemos. War eine ziemlich mühseelige Geschichte, mathematisch auch nie so
ganz ausgereift. Mein bestes Ergebnis war eine Art 3D-Klötzchen-Welt (aus dem Spiel
"WertherSpace"), in der man sich immerhin "rechtwinklig" bewegen konnte, d.h., nach
oben, unten, links und rechts, aber eine Drehung um 5 Grad oder so war nicht möglich.
"WertherSpace": Ein Klötzchenwelt vom Schwamm mittels eigener 3D-Engine
Dann stolperte ich im Web über ein Bericht, bei dem es um Open GL für Delphi ging.
"delphigl.exe": Ein Installer für eine OGL-Entwicklungsumgebung für Delphi
Mein Interesse erwachte auf's neue; ich zog mir das Material und schaute mir die
mitgelieferten Demos an. Diese beschränkten sich aber meist auf die Darstellung einfacher
Objekte wie Quader, die im Raum schwebten und sich um alle Achsen drehen liessen.
Nicht sehr aufregend, das konnte VRML auch schon.
Mehr Source fand ich eigentlich nicht zu OGL und Delphi. Ich fand aber fertige Programme,
die ziemlich ausgefeilte Welten zeigten. Und die waren angeblich auch mit OGL programmiert
worden (vermutlich aber eher in C++ als Delphi). Es ging also. Nur wie?
Mach 's dir selbst, Programmiere!
Ich beschloss, es herauszubekommen. Und so begann das kleine Projekt "OGL_HENRY", eine Art
virtuelles Abbild meiner Arbeitsstätte, dem Auktionshaus HENRY's.
Über die URL "http://www.delphigl.com/" kommt man an die Bibliotheken mit den OGL-DLLs
und Units, die für das Projekt benötigt werden. Die Units müssen ins Projekt eingebunden
werden, damit der Source kompiliert werden kann, und die DLLs müssen der EXE zur Verfügung
stehen, sonst sieht man nach dem Start ausser Fehlermeldungen nichts.
Initialisierungsarbeiten
Unser Projekt beginnt - wie eigentlich jedes Delphi-Projekt - mit einer leeren Form.
Angesichts der zentralen Rolle, die ihr zu kommt, habe ich sie "hauptf" genannt.
Diese wird schnell mit ein paar Komponenten gefüllt, die alle als nicht sichtbar
definiert sind.
"Hauptf": Hauptform mit Filelistboxen, die auf Textur-Ordner zeigen,
Memos, die die Textmaps speichern, und einer Listbox für die Textur-IDs.
Die Units, die wir von DelphiGL verwenden, sind: "DGLOpenGL", "easySDL", "SDL"
und "SDL_Image". Die kommen in den "uses"-Part der Haupt-Unit:
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, FileCtrl, ImgList, Grids, SortGrid,
DGLOpenGL,easySDL,SDL,SDL_Image;
|
Kompiliert man das Ganze, gibt's übrigens 'ne Menge Warnings zu den OGL-Units, die man aber
getrost ignorieren kann.
Im OnCreate-Ereignis der Form werden ein paar Variablen gesetzt und die OGL-Geschichte
initialisiert:
procedure Thauptf.FormCreate(Sender: TObject);
begin
homedir:=extractfilepath(application.exename);
pbmp:=tbitmap.Create;
ppbmp:=tbitmap.Create;
DC:=GetDC(Handle);
if not InitOpenGL then Application.Terminate;
RC:=CreateRenderingContext(
DC,
[opDoubleBuffered],
32, //farbbits
24,
0,0,0,
0
);
ActivateRenderingContext(DC,RC);
lightok:=_lightok;
setupgl;
ccolor:=clred;
init;
Application.OnIdle:=IdleHandler;
width:=640;
height:=480;
end;
|
In "homedir" speichern wir den Arbeitsordner des Programmes weg; diese Information
wird später noch benötig, wenn die Texturen geladen werden sollen.
Die Bitmaps "pbmp" und "ppbmp" verwenden wir dazu, eine kleine 2D-Ansicht unserer
3D-Welt wiederzugeben (die "Minimap"). Ausserdem werden wir später sehen, wie man
darüber eine einfache Kollisionskontrolle realisieren kann.
"GetDC" ist eine API-Funktion, die uns den DeviceContext "DC" der Form liefert.
Über die OGL-Funktionen "CreateRenderingContext" und "ActivateRenderingContext" sorgen
wir anschliessend dafür, dass alle OGL-Ausgaben auf eben diesem "DC" erfolgen.
Ach ja, mit den Parametern von "CreateRenderingContext" habe ich herumgespielt,
aber viel bewirkt hat das nicht. Ich dachte z.B., ich könnte durch Reduzierung der
Farbtiefe mehr Speed rauskitzeln. Das klappte aber nicht. Vielleicht habe ich da aber
auch etwas falsch gemacht. Die Bedeutung der einzelnen Parameter habe ich inzwischen
ohnehin wieder vergessen.
Es folgen noch zwei Initialisierungsroutinen, "SetupGL" und "Init", die wir gleich näher
betrachten werden.
Gut geklaut ist die Idee, das OnIdle-Ereignis der Applikation auf eine eigene Funktion
umzubiegen. Wann immer die Applikation Luft hat, wird diese Funktion aufgerufen. Klar,
dass wir hier unsere ganzen Grafikarbeiten stattfinden lassen. Raffiniert!
Zuletzt wird die Fensterdimension festgelegt. Das ist an dieser Stelle nicht ganz unwichtig,
denn das bewirkt ein OnResize-Ereignis der Form, was noch weitere Initialisierungen zur Folge hat.
Bei dem lahmen Rechner, den ich mein eigen nenne, hat es sich übrigens als nützlich erwiesen,
in der Programmierphase mit einer kleinen Form zu arbeiten, denn die Grafikausgaben
wurden dadurch enorm beschleunigt. Dadurch kommt man schneller an die Positionen in unserer
3D-Welt, die gerade entwickelt werden.
Noch mehr Initialisierungen
Einige Eigenschaften unserer Welt müssen nur einmal vorgegeben werden und ändern sich
danach nicht mehr (oder nur selten). Zum Beispiel, von wo aus das Licht einfällt und
mit welcher Intensität, ob Objekte, die weiter hinten im Raum liegen, von denen
überdeckt werden, die weiter vorne sind, die Materialeigenschaften der Objekte usw.
Das erledigen wir mit der Funktion "SetupGL", die von FormCreate aufgerufen wird:
procedure Thauptf.SetupGL;
var
light0_ambient,
light0_diffuse,
light0_pos,
lmodel_ambient,
mat_shininess,
mat_specular:array[0..3] of single;
fogCol:array[0..3]of single;
begin
//glDepthrange(1,mapz*100);
glDepthFunc(gl_less);
//Tiefentest aktivieren
glenable(GL_ALPHA_TEST);
glShadeModel(GL_SMOOTH);
glEnable(GL_BLEND );
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(TRUE);
glDepthFunc(GL_LESS );
glenable(GL_DEPTH_TEST);
//lichtwerte
filllighta(light0_ambient,0.5,0.5,0.5,1.0); //hintergrundlicht
filllighta(light0_diffuse,1,1,1,1.0); //gerichtetet licht
filllighta(light0_pos,0,0,2,1.0);
filllighta(lmodel_ambient,1.0,1.0,1.0,1.0);
filllighta(mat_specular,1,1,1,1); //reflektionslicht
filllighta(mat_shininess,90.0,0,0,0); //0=harter übergang bis 128
// Lichtquelle 0 einstellen: links fenster
glLightfv(GL_LIGHT0,GL_AMBIENT,@light0_ambient);
glLightfv(GL_LIGHT0,GL_DIFFUSE,@light0_diffuse);
glLightfv(GL_LIGHT0,GL_POSITION,@light0_pos);
//glLightfv(GL_LIGHT0,GL_SPOT_DIRECTION,@light0_pos);
// Beleuchtungsmodell waehlen ...
glLightModelfv(GL_LIGHT_MODEL_AMBIENT,@lmodel_ambient);
//glLightModeli(GL_LIGHT_MODEL_TWO_SIDE,GL_FALSE);
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER,GL_FALSE);
// Materialeigenschaften definieren
glMaterialfv(GL_FRONT_AND_BACK,GL_SHININESS,@mat_shininess);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,@mat_specular);
// ...und Beleuchtung einschalten.
if lightok then begin
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
end;
//nebel
//GL_EXP, GL_EXP2, GL_LINEAR
glFogi(GL_FOG_MODE,GL_LINEAR);
fogcol[0]:=0.5;fogcol[1]:=0.5;fogcol[2]:=0.5;
glFogfv(GL_FOG_COLOR,@fogCol); // farbe
glFogf(GL_FOG_DENSITY,0.35); // dichter je grösser
//gl_dont_care, gl_nicest, gl_fastest
glHint(GL_FOG_HINT,GL_DONT_CARE); // Fog Hint Value
glFogf(GL_FOG_START,5.0); // start nebel
glFogf(GL_FOG_END,10.0); // ende nebel
if fogok then glEnable(GL_FOG);
glenable(GL_TEXTURE_MATRIX);
end;
|
Es lohnt sich, mit den Parametern der Funktionen herumzuspielen. Die Bedeutung der
einzelnen Parameter kann man über Google ersurfen.
Schön finde ich bei OGL, dass man Umgebungsvariablen eines bestimmten "Themas" wie
etwa "Beleuchtung" oder "Nebel" erst setzen kann, und sich diese Themen
anschliessend über eine einzelne "enable"-Funktion en block an- und auschalten lassen.
Mit Hilfe einiger Globals wie "lightok" oder "fogok", die über Tastaturcodes geändert
werden können, lassen sich so schnell grosse Änderungen am Erscheinungsbild unserer 3D-Welt
vornehmen.
Eine Welt wird gebaut
Ich habe dann doch noch Schlaf gefunden. Nun ist ein neuer Tag und ich bin ausgeruht,
es kann also weitergehen.
Die nächste Initialisierungsroutine "Init" baut nun unsere Welt zusammen, die in die
eben definierte OGL-Umgebung plaziert wird.
procedure Thauptf.init;
procedure settx(fn:string;r:integer);
var
tex:PSDL_Surface;
begin
tex:=IMG_Load(pchar(fn));
if assigned(tex) then begin
//glGenTextures(1,@tx);
glGenTextures(1,@txa[r]);
glBindTexture(GL_TEXTURE_2D,txa[r]);
glEnable(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(gl_texture_2d,GL_GENERATE_MIPMAP_SGIS,GL_TRUE);
// Achtung! Einige Bildformate erwarten statt GL_RGB, GL_BGR.
//Diese Konstante fehlt in den Standard-Headern
glTexImage2D(
GL_TEXTURE_2D,0,3,tex^.w,tex^.h,0,
GL_RGB,GL_UNSIGNED_BYTE,
tex^.pixels
);
SDL_FreeSurface(tex);
end;
end;
var
r:integer;
begin
for r:=0 to _txamax-1 do begin
txa[r]:=0;
end;
//personen texturen als erstes
pflb.Directory:=homedir+'\personen';
for r:=0 to pflb.items.count-1 do begin
settx(pflb.Directory+'\'+pflb.Items[r],r);
end;
if _startmap='Info' then mapu.setmap_info
else if _startmap='Flur' then mapu.setmap_flur
else if _startmap='Mode' then mapu.setmap_mode
else if _startmap='EDV' then mapu.setmap_edv;
boxmode:=_box;
p.visible:=_panelok;
FormResize(Self);
sethome;
end;
|
Die Konstante "_txamax" gibt die maximale Anzahl von Objekten wieder,
die in unserer Welt verwaltet werden können. Bei uns sind das 200 Objekte.
Zunächst wird das Integer-Array "txa" komlett auf Null gesetzt. Hier drin
werden die IDs der einzelnen Texturen gespeichert.
Anschliessend durchlaufen wir den Unterordner "personen", in dem die Texturbilder
der Menschen unserer Welt liegen. Wie alle anderen Texturen, die wir verwenden,
handelt es sich um JPGs mit der Dimension 256x256 Pixel.
Aus der möglichen Grösse der Texturen wurde ich nie so ganz schlau. OGL kann
durchaus mit grösseren Bildern als 256x256 Pixeln arbeiten. Auf meinem Geschäfts-PC
gab's damit jedenfalls keine Probleme. Aber bei meinem HomePC werden solche Texturen
nicht angezeigt. Noch erstaunlicher ist, dass ein zweiter Geschäfts-PC sie auch
nicht anzeigte, obwohl er mehr Speicher und Power als der erste hatte. Ich vermute
mal, es ist die Grafikkarte, die entscheidet, wie gross die Texturen werden dürfen.
Also, jedes gefundene Bild wird an die inerne Funktion "settx" übergeben. Die läd das
Bild mittels der OGL-Funktion "IMG_Load" ein und bindet es in die OGL-Umgebung
ein. Als Referenz erhalten wir eine ID zurück, die über die Befehle "glGenTextures"
und "glBindTexture" im "txa"-Array gesichert wird.
Das wir die Personenbilder an dieser Stelle und am Anfang unserer Textur-Arrays laden
hat einen einfachen Grund: Im Gegensatz zu den Raum-Objekten, die wir anschliessend
verarbeiten, können die Personen in allen Räumen auftauchen, nicht nur in einem. Für
ortsgebundene Objekte wie ein Tisch gilt das nicht (klar, in der realen Welt kann man
natürlich auch einen Tisch in einen anderen Raum tragen, aber in unserer viruellen Welt
ist das nicht vorgesehen).
Die Welt von HENRY's
OGL_Henrys ist eine Welt, die aus einem quasi unendlich grossem Raum besteht,
in dem vier Unterräume mit Objekten liegen. Es sind dies die Räume "Info",
"Flur", "Mode" und "EDV". Aus Platz- und Geschwindigkeitsgründen ist zu einer Zeit
immer nur einer der vier Räume "mit Leben gefüllt".
Grob sieht die HENRY's Welt in etwa so aus:
leerer, unendlicher Raum
################################
# # #
# MODE # EDV #
# # #
# # #
# # #
# # #
################################
# FLUR #
################################
# #
# INFO #
# #
################################
leerer, unendlicher Raum
|
Befinden wir uns im Raum "Info", wird in "init" die Funktion "mapu.setmap_info"
aufgerufen, sind wir in den Raum "EDV" gegangen, dann wird "mapu.setmap_edv"
aufgerufen usw. Die Unit "mapu" sehen wir uns gleich an.
Zuletzt wird "FormResize" aufgerufen, wodurch unsere Welt neu auf die Form gezeichnet
wird. Die Funktion "sethome" bringt uns an eine definierten Position innerhalb des
gewählten Unter-Raums. Das ist wichtig, denn natürlich landet man z.B. an einer anderen
Position im Flur, je nachdem, ob man ihn von der Info oder der Mode aus betreten hat.
procedure Thauptf.FormResize(Sender: TObject);
begin
//hilfefenster an formgrösse anpassen
helpm.left:=(width-helpm.width) div 2;
helpm.top:=(height-helpm.height) div 2;
//viewport an fensterdimension anpassen
glViewport(0,0,ClientWidth,ClientHeight);
glMatrixMode(GL_PROJECTION);
glLoadIdentity;
gluPerspective(_brennweite,1.5,_NearClipping,_FarClipping);
glMatrixMode(GL_MODELVIEW);
//geänderte ansicht neu malen
DrawScene;
end;
//springe im raum an eine zuvor gewählte position
procedure thauptf.sethome;
begin
//raum-koordinaten
px:=map.startpos.x;
py:=map.startpos.y;
pz:=map.startpos.z;
//richtung, in die man schaut
rotx:=map.startrot.x;
roty:=map.startrot.y;
rotz:=map.startrot.z;
end;
|
Die Unit "mapu"
Die Funktionen, die den Aufbau unserer vier Unter-Räume übernehmen,
sind in der Unit "mapu" gekapselt.
Beim Betreten eines neuen Raumes wird - wie in "init" gesehen - eine
zugehörige "set_map"-Funktion aufgerufen. Die wollen wir uns nun mal näher
am Beispiel des "Info"-Raums ansehen.
procedure setmap_info;
begin
//startwert
hauptf.map.startpos.x:=15.22;
hauptf.map.startpos.y:=1.77;
hauptf.map.startpos.z:=20.06;
hauptf.map.startrot.x:=0;
hauptf.map.startrot.y:=20;
hauptf.map.startrot.z:=0;
hauptf.map.h:=3;
//durchgänge-chars
clrdurchgang;
hauptf.map.durchgang[0]:='i';
//lese map, objekte und personen
rdmap('Info',hauptf.map);
//decken/boden-aufteilung adaptieren
hauptf.map.txdeckewc:=hauptf.map.w/2;
hauptf.map.txdeckedc:=hauptf.map.d/2;
hauptf.map.txbodenwc:=hauptf.map.w/2;
hauptf.map.txbodendc:=hauptf.map.d/2;
//ausgänge setzen
clrouta;
hauptf.map.outa[0].ch:='D';
hauptf.map.outa[0].map:='Flur';
hauptf.map.outa[0].mapstart:='90';
//springe startpos
hauptf.sethome;
end;
|
Zunächst wird die Startposition unserers "Avatars" im Raum festgelegt, wobei die
Zahlenwerte als Meter und Zentimeter interpretiert werden können. Die 0/0/0-Koordinate
entspricht dem unteren linken Eck des "Info"-Raums. Wir stehen also etwa 15 m rechts
davon und etwa 20 m tiefer im Raum drin. Die Augenhöhe liegt bei 1,77 m, denn so gross
bin ich selbst. Wer HENRY's aus Sicht eines Hundes erleben will, kann hier ja mal
50 cm oder so eintragen :-)
Für grafikerfahrene Programmierer bleibt übrigens festzuhalten, dass sich der Nullpunkt bei OGL
stets unten links befindet, nicht wie beim Microsoft üblich oben links! Die OGL-Variante
ist die natürlichere, wie ich finde; das Umdenken tat teilweise aber trotzdem weh - insbesondere,
weil wir später auch noch eine 2D-Map machen, bei der wieder die MS-Regeln gelten.
Die nächsten drei Werte geben an, in welche Richtung wir schauen (und uns bewegen).
Die Angaben sind als Gradwerte zu verstehen, reichen also von 0 bis 359. 0/0/0 bedeutet,
wir schauen geradeaus, in den Raum hinein. In unserem Fall mache wir eine leichte
Drehung von 20 Grad um die Y-Achse, wie eine Tänzerin an einer Stange.
Startet man das Programm, stellen wir vielleicht überrascht fest, dass wir
nach links schauen, nicht nach rechts, wie man vermuten könnte. Denn tatsächlich ist es
bei OGL so, das wir, der Betrachter, uns gar nicht bewegen oder rotieren, sondern nur der
Raum um uns herum! Und wenn der sich 20 Grad nach rechts dreht, schauen wir eben 20 Grad
nach links.
Um die Sache noch verwickelter zu machen: Bei den Bewegungskoordinaten habe ich die Werte
offenbar doch so umgerechnet, als würden wir uns bewegen, nicht der Raum. Beim Rotieren jedoch
habe ich das - wie gerade beschrieben - nicht getan. Mh ... wohl vergessen. Egal, leben wir
nun damit.
Die Variable "hauptf.map.h" gibt die Höhe unseres Raumes an. Der "Info"-Raum hat also
eine Höhe von drei Metern (so wie's auch der Realität entspricht). Andere Räume, wie etwa
der Flur, sind sehr viel höher.
Textmaps und GrafikRenderer
An dieser Stelle sei kurz angedeutet, wie wir unsere Welt eigentlich in die OGL-Umgebung
bekommen. Wenn man so will, war dies der für mich kreativtse Teil der ganzen Arbeit. Denn
Anfangs hatte ich keinen blassen Schimmer, wie ich das anfangen soll.
Bei vielen OGL-Programmen, die man im Web findet, erkennt man im Source, wie die Objekte
in sehr langen Listen einzeln definiert werden. Also ihre genaue Position im Raum,
ihre Rotation, ihre exakten Ausmasse, die "aufgeklebten" Texturen dazu usw.
Ein Mörder-Job!
Nix für einen faulen Hund wie mich. Und wenn es auch eine drastische Einschränkung
der Wiedergabe der Realität darstellte, war mir schnell klar, dass ich einen anderen,
sehr viel einfacheren Weg beschreiten würde.
Ich füllte einfach eine Textdatei mit bestimmten Zeichen als quasi Zwei-Dimensionale
Ansicht meines Raumes, wobei jedes Zeichen für ein Objekt bestimmter Grösse und Art
steht. So verwende ich z.B. das Zeichen "W" als Wand, "T" als Theke, "t" als Tisch
usw. Gruppiert man gleiche Zeichen, dann wird das Objekt entsprechend vergrössert.
Dabei legte ich fest, dass (fast) jedes Zeichen-Objekt die Breite und Tiefe von
50 cm hat. Klar, das ergibt reichlich dicke Wände. Und eine Rotation der Objekte
liess sich so auch nicht ohne weiteres realisieren; alles ist stur an den Raumkoordinaten
ausgerichtet.
Aber hey, das genügt. Wir wollen hier ja keine Gebäude virtuell errichten, die später
Realität werden sollen (und in denen dann eventuell sogar Menschen leben sollen).
Wir bauen nur virtuell nach, was bereits Realität ist.
Okay, sehen wir uns mal an einem kleinen Beispiel an, wie man eine solche Textmap
getalten kann:
Ein leerer Raum:
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
W W
W W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
Mit Tisch ist's schöner:
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
W W
W W
W ttttt W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
Jetzt noch eine Theke davor. Der Tisch wird etwas tiefer:
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
WTTTTTTTTTTTTTTTT W
W ttttt W
W ttttt W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWW
Und nun noch rasch einen Raum nebenan gebaut:
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
WTTTTTTTTTTTTTTTT W W
W ttttt W W
W ttttt W W
W W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
|
Auf die Idee mit den Textmaps sind natürlich schon viele vor mir gekommen. Ich habe
nur keinen OGL-Source gefunden, der mir gezeigt hätte, wie ich diese Maps letztlich
an den GrafikRenderer verfüttern kann.
Denn das ist natürlich der nächste Schritt: Eine Art Parser muss die Textmaps
durchlaufen, die gefundenen Zeichen interpretieren sowie ihre Position berücksichtigen
und daraus die entsprechende OGL-Objekte generieren, ganz so wie bei den direkt
programmierten Objekt-Listen.
Mh ... was ich hier GrafikRenderer nennen, ist vielleicht gar kein GrafikRenderer.
Ist mir aber Wurst, ich nenne das Teil jedenfalls so. Das ist der Part im Programm,
der meine Liste an Objekten durchgeht und sie an die definierten Positionen im Raum
malt, wobei dafür gesorgt wird, das Objekte weiter hinten perspektivisch verkleinert
werden und keine Objekte verdecken, die weiter vorne liegen. Praktisch bei OGL ist,
dass es die ganze Mathematik dahinter für uns erledigt.
Zu diesem GrafikRenderer kommen wir aber erst später.
Kollisionsfreie Durchgänge
Machen wir weiter mit der "setmap_info"-Prozedur.
Nachdem wir unsere Position im Raum definiert haben, füllen wir nun das Array
"hauptf.map.durchgang" mit bestimmten Zeichen, die der GrafikRenderer später als
"Durchgänge" begreifen soll. Naja, eigentlich sind es eher Objekte, die keine
Kollision bewirken sollen, wenn man sie erreicht. Üblicherweise sollte man ja
durch einen Tisch nicht durchlaufen können. An einer Bodenkachel hängen zu bleiben
macht dagegen wenig Sinn.
Im Falle der "Info"-Map bezeichnet das Zeichen "i" eine solche Bodenkachel. Bei anderen
Maps, wie etwa dem Flur, entspricht "i" einem Teppich, der auf dem Boden liegt, "F"
eine Art Tor, unter dem man durchgehen kann usw.
MapLoading & Boden-Decken-Kachelung
Im nächsten Schritt wird die aktuelle MAP-Struktur geladen: rdmap('Info',hauptf.map).
Dazu gleich mehr.
Nun folgen ein paar "Grafik"-Definitionen, die speziell für diesen Raum gelten. Boden und
Decke unserer Räume haben eine einheitliche Textur verpasst bekommen. Sie werden
nämlich nicht, wie alle anderen Objekte im Raum, über die MAP-Struktur plaziert,
sondern einfach - mathematisch berechnet - über die komplette Raumgrösse gespannt.
Um doch noch ein wenig Abwechslung rein zu bringen, werden die Boden- und Decken-Texturen
über ein paar Struktur-Variablen in raumspezifischer Weise "gekachelt".
Lass mich rein, lass mich raus, oh Anna!
Nun kann man Räume betreten und wieder verlassen. Dafür werden Ausgänge definiert.
Das machen wir über ein Array "hauptf.map.outa". Es ist Teil der Map-Struktur und
wie folgt deklariert:
tout=record
ch:char;
map:string;
mapstart:string;
end;
tmap=record
...
outa:array[0..5]of tout;
durchgang:array[0..5]of char;
end;
|
Dabei gilt: Jedes Zeichen, dass im Ausgang-Array in "ch" aufgelistet wird, hat für
den GrafikRenderer die spezielle Bedeutung eines Ausgangs (ähnlich wie beim Durchgang).
Kommen wir an eine solche Stelle, sagt die Kollisionskontrolle nicht "Autsch! Du bis
gegen eine Wand gerannt", sondern veranlasst vielmehr die Neu-Initialisierung der sich
am Aus-/Eingang anschliessenden MAP (beschrieben in "map" mit Startkennung "mapstart").
Konkret heisst das in unserem Fall: Komme ich im "Info"-Raum an eine Stelle, die in der
Textmap mit "D" markiert ist, dann wechselt das Programm in die Map "Flur" und sucht
dort nach den Zeichen "90", um mich genau an diese Stelle zu plazieren.
Das "outa"-Array erlaubt es, maximal fünf verschiedene Ausgänge je Map setzen zu können.
Die "Info"-Map hat aber nur einen.
Konvertierung der TextMap zu OGL-Objekten
Betrachten wir nun die "rdmap"-Prozedure, die unsere Textmap von der Platte liest,
zeilen- und zeichenweise durchgeht, und daraus die zu den Zeichen gehörenden OGL-Objekte
generiert.
procedure rdmap(fn:string;var map:tmap);
procedure settx(fn:string;r:integer);
var
tex:PSDL_Surface;
begin
tex:=IMG_Load(pchar(fn));
if assigned(tex) then begin
glGenTextures(1,@hauptf.txa[r]);
glBindTexture(GL_TEXTURE_2D,hauptf.txa[r]);
glEnable(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(gl_texture_2d,GL_GENERATE_MIPMAP_SGIS,GL_TRUE);
// Achtung! Einige Bildformate erwarten statt GL_RGB, GL_BGR.
//Diese Konstante fehlt in den Standard-Headern
glTexImage2D(
GL_TEXTURE_2D,0,3,tex^.w,tex^.h,0,
GL_RGB,GL_UNSIGNED_BYTE,
tex^.pixels
);
SDL_FreeSurface(tex);
end;
end;
procedure getdim(ch:char;x,z:byte;var w,d:byte);
var
s:string;
r,c:integer;
fullok:bool;
begin
//startpunkt immer, daher löschen
s:=hauptf.mapm.lines[z];
s[x]:='<';hauptf.mapm.lines[z]:=s;
//wie breit ist objekt?
w:=1;
c:=x+1;
while(c<length(s))and(s[c]=ch) do begin
//markier breiteblöcke
s[c]:='_';hauptf.mapm.lines[z]:=s;
inc(w);
inc(c);
end;
//wie tief ist objekt
d:=1;
fullok:=_spanok;
//fullok:=false;
for r:=z+1 to hauptf.mapm.lines.count-1 do begin
s:=hauptf.mapm.lines[r];
if length(s)<x then break;
if s[x]<>ch then break;
//markiere tiefe-blöcke
s[x]:='|';hauptf.mapm.lines[r]:=s;
if _spanok then begin
//volle breite mit objekt-blöcken?
if fullok then begin
c:=x+1;
while(c<x+w)and(s[c]=ch) do begin
//markiere breiteblöcke
s[c]:='>';hauptf.mapm.lines[r]:=s;
inc(c);
end;
if c<x+w then fullok:=false;
end;
end;
inc(d);
end;
//einzelblock?
if(w=1)and(d=1)then exit;
//raum nicht voll aufgespannt?
if not fullok then begin
//setze full-blöcke zurück
s:=hauptf.mapm.text;
s:=stringreplace(s,'>',ch,[rfreplaceall]);
if w>=d then begin
d:=1;
s:=stringreplace(s,'_',' ',[rfreplaceall]);
s:=stringreplace(s,'|',ch,[rfreplaceall]);
end
else begin
w:=1;
s:=stringreplace(s,'_',ch,[rfreplaceall]);
s:=stringreplace(s,'|',' ',[rfreplaceall]);
end;
hauptf.mapm.text:=s;
end
else begin
s:=hauptf.mapm.text;
s:=stringreplace(s,'>',' ',[rfreplaceall]);
s:=stringreplace(s,'_',' ',[rfreplaceall]);
s:=stringreplace(s,'|',' ',[rfreplaceall]);
hauptf.mapm.text:=s;
end;
end;
var
r,c:word;
s,ss,bufs:string;
anz:word;
w,d:byte;
begin
hauptf.map.name:=fn;
//alte texturen löschen (ausser personen)
hauptf.texturesfree;
//texturen laden
hauptf.flb.Directory:=hauptf.homedir+fn;
for r:=0 to hauptf.flb.items.count-1 do begin
settx(
hauptf.flb.Directory+'\'+hauptf.flb.Items[r],
r+hauptf.pflb.Items.count
);
end;
//map-standard-texturen
hauptf.map.txdecke:=
hauptf.flb.Items.indexof('txdecke.jpg')+
hauptf.pflb.Items.count;
hauptf.map.txboden:=
hauptf.flb.Items.indexof('txboden.jpg')+
hauptf.pflb.Items.count;
//lade map in memo
hauptf.mapm.lines.loadfromfile(hauptf.homedir+fn+'/_map.txt');
//map retten
bufs:=hauptf.mapm.Text;
//zähle objekte-blöcke
s:=trim(hauptf.mapm.Text);
anz:=0;
for c:=1 to length(s) do begin
if(s[c]=' ')or(s[c]=#10)or(s[c]=#13)then continue;
inc(anz);
end;
//map löschen und erstmal über-dimenesionieren
setlength(map.quads,0);
setlength(map.quads,anz);
map.anz:=anz;
hauptf.map.w:=0;
hauptf.map.d:=0;
//hole objekte+personen
anz:=0;
for r:=0 to hauptf.mapm.lines.count-1 do begin
s:=hauptf.mapm.lines[r];
if length(s)>hauptf.map.w then hauptf.map.w:=length(s);
for c:=1 to length(s) do begin
if s[c]=' ' then continue;
if s[c]='<' then continue;
//person?
if s[c] in ['0'..'9'] then begin
ss:=s[c]+s[c+1];
s[c]:='<';s[c+1]:=' ';
hauptf.mapm.lines[r]:=s;
w:=1;
d:=1;
end
else begin
//neues objekt: bestimme dimension
getdim(s[c],c,r,w,d);
ss:=s[c];
end;
//binde textur und höhe an objekt
if s2quad(ss,c-1,r,w,d,map.quads[anz]) then begin
//erhöhe objektcounter
inc(anz);
end;
s:=hauptf.mapm.lines[r];
end;
hauptf.mapm.lines[r]:=s;
end;
//korrigiere anzahl objekte
map.anz:=anz;//map.anz-anzc;
hauptf.map.d:=hauptf.mapm.lines.count;
//decke und boden textur-aufteilung
map.txdeckewc:=hauptf.map.w;
map.txdeckedc:=hauptf.map.d;
map.txbodenwc:=hauptf.map.w;
map.txbodendc:=hauptf.map.d;
//hauptf.showtxa;
//map zurücksetzen
hauptf.mapm.Text:=bufs;
//panel füllen
panelu.initp;
end;
|
Zunächst werden alle alten Texturen von eventuell zuvor geladenen Maps
gelöscht, um den Speicher wieder frei zu bekommen.
Dann werden alle Texturen geladen, die sich im zugehörigen Unterordner
auf der Platte befinden. Das Verfahren ähnelt sehr dem, dass wir schon
weiter oben für die Personen-Texturen verwendet haben. Wichtig ist, dass
das Array nicht von vorne aufgefüllt wird, sondern ab Position
"hauptf.pflb.Items.count" - die Personen-Filelistbox zeigt dabei auf
den Ordner mit den Personenbildern. Dadurch gehen die zuvor geladenen
Personentexturen nicht verloren. Und das Programm ist so adaptiv, dass
jederzeit neue Personenbilder hinzukommen können, ohne dass dadurch die
Indizes verrutschen.
Anschliessend werden die Boden- und Decken-Texturen gesondert geladen,
da deren Verarbeitung anders erfolgt als die der Textmap-Objekte (wie
weiter oben erwähnt).
Jetzt wird die Textmap in ein TMemo "hauptf.mapm" geladen.
In einem nächsten Schritt wird die Textmap durchlaufen und alle
Zeichen gezählt, die ungleich Carriage Return und Leezeichen sind.
Diese Zahl "anz" gibt uns damit die Anzahl der Objekte wieder, die
in der Map Verwendung finden.
Es handelt sich um eine maximale Anzahl, die nur im Worst Case wirklich
gegeben ist. Denn wie wir gleich sehen werden, lassen sich Gruppen von
Zeichen zusammenfassen und bilden so statt z.B. 20 Einzelobjekten nur ein
grosses Objekt.
Mann, das war eine Schufterei bis diese Gruppierung von Objekten zu
Einzelobjekten korrekt funktioniert hat. Aber der Vorteil liegt auf
der Hand: Wenn der GrafikRenderer statt 200 Objekten nur 30 verwalten
muss, wird die Ausgabe erheblich flüssiger. Wo ich vorher nur ruckelnd
durch die Räume kam, kann ich jetzt geradezu fliegen :-)
Zunächst allokieren wir aber erst einmal Speicher für den Worst Case.
Wir verwenden dazu die Funktion "setlength", um eine "anz"-grosse
Struktur von "map.quads" anzulegen, die wie folgt definiert sind:
type
tpos=record
x,y,z:single;
end;
tdim=record
w,h,d:single;
end;
trot=record
x,y,z:smallint;
end;
ttex=record
wc,dc:byte;
o,u,l,r,v,h:byte;
end;
tquad=record
pos:tpos;
dim:tdim;
tex:ttex;
end;
tout=record
ch:char;
map:string;
mapstart:string;
end;
tqa=array of tquad;
tmap=record
name:string; //name der map, z.B. 'info'
anz:word; //anzahl tquads
quads:tqa; //folge von tquads
fn:string; //dateiname
txdecke:byte; //id der decken-textur
txdeckewc:single; //decken-kachelung breite
txdeckedc:single; //decken-kachelung tiefe
//keine Höhen-Kachelung,
//denn die Höhe der Decke ist Null
// 'zwischendecke' (nur für 'Flur')
txdeckez:byte;
txdeckezwc:single;
txdeckezdc:single;
txboden:byte; //id boden-textur
txbodenwc:single; //boden-kacheln breite
txbodendc:single; //boden-kacheln tiefe
startpos:tpos; //homeposition des betrachters
startrot:trot; //homerotation des betrachters
w,d,h:single; //breite, tiefe und höhe der Map
outa:array[0..5]of tout; //definierte ausgänge
durchgang:array[0..5]of char; //definierte durchgänge
end;
|
"Quads" sind - wie der Name schon sagt - einfache Quader.
Sie besitzen die Attribute "pos" mit ihrem Raumkoordinaten,
"dim" mit ihren Raumausmassen, und "tex" mit der ID der zugehörigen
Textur.
Die HENRY's Welt besteht also nur aus einer Aneinanderreihung von
Quadern. Runde Formen sucht man darin vergeblich. Es wäre allerdings nicht
all zu schwer, auch Kugeln oder Zylinder in das Programm zu integrieren,
man müsste halt zusätzlich Strukturen wie "tkugel" oder "tzylinder" mit
spezifischen Attributen wie "Radius" definieren.
Die Struktur MAP wird jeweils nur von einem Raum "besetzt" - obwohl man natürlich
auch leicht ein Array von Maps realisieren könnte. Aber da wir uns ja immer nur
in einem Raum zu einer Zeit befinden können und die Neu-Initialiserung der Map
nur wenige Sekunden dauert, wäre das eher Speicherverschwendung.
Map-Dimensionen
Die TextMap wird nun zeilenweise aufgerufen. Wir prüfen jeweils, an welcher Position das
äusserste rechte Element zu finden ist. Das Maximum dieser Werte ergibt die Breite
unserer Map und wird in "hauptf.map.w" gesichert.
Die Tiefe der Map ergibt sich aus der Anzahl der Zeilen.
Die Höhe der Map wurde in der "setmap"-Prozedur festgeschrieben.
Objekt- und Personen-Parsing
In "s" steht jeweil eine Zeile unserer Textmap. Diese wird nun
zeichenweise durchgegangen. Leerzeichen werden ignoriert; sie stellen
objektfreien Raum dar.
Auch das "<"-Zeichen wird ignoriert: Damit kennzeichnen wir Objekte,
die zuvor in ein Gruppenobjekt zusammen gefasst wurden, d.h., kein
eigenständiges Objekt bilden. Leerzeichen dürfen hier nicht gesetzt
werden, denn sonst befände sich hier ja für den GrafikRenderer später
nichts, wodurch man durch Wände laufen könnte.
Achtung! Dieses Verfahren der Gruppen-Kennzeichnung bedeutet natürlich,
das bei der Erstellung der Textmaps tunlichst auf die Verwendung des
"<"-Zeichens verzichtet werden sollte! Das gilt auch für die Zeichen
"|" und "_", die hier ebenfalls intern Verwendung finden.
Von Nummern zur Person
Finden wir dagegen ein Zeichen zwischen '0' und '8', dann wird an diese
Stelle kein Objekt plaziert, sondern eine Person. Personen werden stets
durch zwei aufeinander folgende Zahlen gekennzeichnet, wobei die so
entstandene Nummern 00-89 gerade dem Filenamen der zugehörigen Textur
im Personen-Dateiordner entsprechen.
Die Nummernfolge "90" bis "99" ist übrigens reaserviert für einen anderen
Zweck: Hiermit werden die Ziel definiert, die wir erreichen, wenn wir einen
neuen Raum betreten. Dazu kommen wir aber erst später
So heisst mein Textur-File z.B. "tx_00.jpg". Um meinen Personen-Avatar also
in einer Texmap zu plazieren, müssen irgendwo die Zeichen "00" auftauchen.
Das heisst wiederum auch, dass nur maximal 90 verschiedene Personen
in den Maps untergebracht werden können (die aber beliebig oft, was aber
zugegebenermassen wenig Sinn macht).
Objekt-Gruppierung
Finden wir ein anderes Zeichen, dann handelt es sich um ein neues Objekt.
Genauer: Es handelt sich um die obere linke Ecke eines neuen Objekts.
Die interne Funktion "getdim" prüft, ob und in wie weit sich anschliessende
Objekte zu einer Gruppe zusammen fassen lassen.
Das will ich hier mal an einem Beispiel verdeutlichen:
Textmap zu Beginn, der Parser sitzt bei P, wo vorher ein W stand.
Breite und Tiefe des Objekts ist 1.
PWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W W
W tttttttttt W
W tttttttttt W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Der Parser hat 'W' gefunden, also ein neues Objekt. Nun läuft er
nach rechts und prüft, ob er dort weitere 'W' findet. Jedes andere
Zeichen unterbricht den Prozess. In unserem Fall kommt er bis
Breite 31:
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWP
W W
W W
W tttttttttt W
W tttttttttt W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Nun geht der Parser in die nächste Zeile (Tiefe=2) und prüft, ob er dort
genausoviele 'W' wie in der ersten Zeile findet. Das tut er nicht,
also verwirft er die neue Höhe von 2 und setzt sich auf 1 zurück.
Das gruppierte Objekt hat also die Dimension 31 x 1.
Damit die zur Gruppe gehörenden Objekte nicht nocheinmal geparst
werden, werden sie mit dem '<'-Zeichen markiert:
WP<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
W W
W W
W tttttttttt W
W tttttttttt W
W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Der Parser rennt weiter und erkennt als nächstes zwei neue Gruppenobjekte,
nämlich die seitlichen Wände, jeweils mit der Dimension 1 x 7:
WP<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
W W
P <
< tttttttttt <
< tttttttttt <
< <
< <
<WWWWWWWWWWWWWWWWWWWWWWWWWWWWW<
Leereichen und '<' werden ignoriert, er finden schliesslich das 't'-Zeichen
und macht daraus ein weiteres Objekt mit der Dimension 2 x 10:
WP<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
W W
P <
< t<<<<<<<<< <
< <<<<<<<<<< <
< <
< <
<WWWWWWWWWWWWWWWWWWWWWWWWWWWWW<
|
Also ehrlich gesagt weiss ich nicht mehr, ob der Algorithmus exakt so
wie beschrieben vorgeht. In Zwischenschritten wird auch noch mit den
Zeichen "|" und "_" gearbeitet. Zudem dürfte ich mich im Beispiel sicher
irgendwo bei den Dimensionswerten verzählt haben. Aber das Prinzip sollte
klarer geworden sein.
Haben wir eine Person oder ein Objekt und dessen zugehörige Ausmasse in
Breite und Tiefe ermittelt, werden diese Informationen an die Funktion
"s2quad" geschickt. Die schauen wir uns gleich an. Ausserdem wird der
Objektzähler "anz" neu hochgezählt, so dass er am Ende auf der echten
Anzahl Objekte in der Map steht.
Schliesslich haben wir die TextMap durchgeparst und alle Objekte
erstellt. Nun werden noch ein paar Map-Variablen gesetzt und ganz am
Schluss mit "panelu.initp" die 2D-Minimap initialisiert.
Jetzt aber: MapObjekt zu OGL-Objekt
Grrr! Was eine Fummelei mit diesen albernen Text-Zeichen bisher. Wann kommen
endlich die OGL-Objekte dran?
Geduld, wir nähern uns!
Wir betrachten zunächst allerdings die Funktion "s2quads", die unsere Textmap-Objekte
mit zugehöriger Dimension übergeben bekommt:
function s2quad(ss:string;x,z,w,d:byte;var q:tquad):bool;
function filter(s:string;ch:char;v:byte):byte;
var
c:byte;
begin
result:=1;
//tx_ww_h4_ww_d11.jpg
//vorbau weg -> _ww_d11.jpg
s:=copy(s,9,length(s));
//suche parameter
c:=pos('_'+ch,s);
if c=0 then exit;
//parameter weg -> w_d11.jpg
s:=copy(s,c+2,length(s));
if length(s)=0 then begin
beep;
exit;
end;
//x-dimensional?
if s[1]=ch then begin
result:=v;
exit;
end;
//anzahl extrahieren
c:=1;while(c<length(s))and(s[c] in ['0'..'9'])do inc(c);
try
result:=strtoint(copy(s,1,c-1));
except
beep;
end;
end;
var
fn,s:string;
foundok:bool;
r:integer;
tx:gluint;
y,h:single;
ch:char;
begin
result:=true;
ch:=ss[1];
if(ch>='0')and(ch<='9')then begin
//person: bestimme tex-index
s:='tx_'+ss+'.jpg';
r:=hauptf.pflb.Items.indexof(s);
if r=-1 then begin
result:=false;
exit;
end;
y:=1;h:=1;
setpersonquad(r,x,y,z,w,h,d,q);
end
else begin
//eintrag holen
foundok:=false;
tx:=0;
for r:=0 to hauptf.flb.Items.count-1 do begin
s:=lowercase(hauptf.flb.Items[r]);
//grossbuchstabe?
if ch=uppercase(ch) then foundok:=copy(s,4,2)=lowercase(ch+ch)
else foundok:=copy(s,4,2)=lowercase(ch+'0');
if foundok then begin
tx:=r+hauptf.pflb.Items.count;
fn:=hauptf.flb.Items[r];
break;
end;
end;
if not foundok then begin
beep;
exit;
end;
//höhe des objekt (h) und höhenposition des objekt (y) bestimmen
//tx_ww_h4.jpg
s:=copy(s,7,2);
y:=0;h:=hauptf.map.h;
if s='h0' then begin h:=0;y:=0.01;end
else if s='h1' then h:=0.8
else if s='h2' then begin h:=0.6;y:=0.8;end
else if s='h3' then h:=2
else if s='h4' then begin h:=0.8;y:=1.8;end
else if s='h5' then h:=hauptf.map.h-4
else if s='h6' then begin h:=hauptf.map.h-4;y:=4;end
else if s='h7' then h:=hauptf.map.h-2
else if s='h8' then h:=hauptf.map.h-1
else if s='h9' then h:=hauptf.map.h
else begin
beep;
end;
setobjquad(tx,x,y,z,w,h,d,q);
//if fn='tx_rr_h9_w3_d3.jpg' then beep;
//aufteilung textur breite
//tx_ww_h4_ww_d11.jpg
q.tex.wc:=filter(fn,'w',w);
q.tex.dc:=filter(fn,'d',d);
//textur andere seite?
fn:=copy(fn,1,length(fn)-4);
r:=hauptf.flb.items.indexof(fn+'_o.jpg');
if r>-1 then q.tex.o:=r++hauptf.pflb.Items.count;
r:=hauptf.flb.items.indexof(fn+'_r.jpg');
if r>-1 then q.tex.r:=r+hauptf.pflb.Items.count;
r:=hauptf.flb.items.indexof(fn+'_v.jpg');
if r>-1 then q.tex.v:=r+hauptf.pflb.Items.count;
r:=hauptf.flb.items.indexof(fn+'_h.jpg');
if r>-1 then q.tex.h:=r+hauptf.pflb.Items.count;
end;
end;
|
Wir schauen anhand des ermittelten Zeichencodes nach, ob es sich bei dem zu generierenden
Objekt um eine Person oder um ein Objekt handelt.
Ist es eine Personen, können wir aus dem - in diesem Fall stets zweistelligem - Zeichencode "ss"
den zugehörigen Dateinamen der Textturdatei bestimmen: "tx_"+ss+".jpg". Über die IndexOf-Funktion
der Personenfilelistbox "pflb" finden wir den Index der Datei, der gerade dem Index unserers
Textur-Arrays "txa" entspricht. Dann rufen wir "setpersonquad" auf.
Handelt es sich um ein Objekt-Zeichencode, dann muss etwas aufwendiger vorgegangen werden.
Infos im Objekt-Textur-Dateinamen
Personen in der HENRY's-Welt werden ziemlich primitiv und einheitlich dargestellt: Als
mit Texturen rundherum beklebte Quader mit immer den gleichen Ausmassen.
Objekte sind auch nur Quader, sollen aber teilweise z.B. von oben anders aussehen als von
der Seite (wie etwa bei einem Tisch). Das verlangt verschiedene Texturen je Objekt. Zudem
haben Objekte verschiedene Höhen. Und manchmal ist es darüberhinaus auch wünschenswert,
einen Seitenwand nicht mit einer durchgehenden Textur zu versehen, sondern diese zu kacheln,
wie es etwa bei einem Schrank oder einem Regal an der Wand sinnvoll sein könnte.
All diese Zusatzinformationen zu einem Objekt können nicht in unsere 2D-Textmaps eingearbeitet
werden, denn dort steht uns ja nur ein Zeichen je Objekt zur Verfügung, was die maximale Anzahl
verschiedener Objekte je Raum ohnehin schon stark einschränkt.
Um unseren Objekt-Parser einfach und flexibel zu halten und ohne am Ende doch lange Listen
von Objekten direkt im Source codieren zu müssen, packen wir die Zusatzinformationen zu den
Objekten einfach in deren Textur-Filennamen! Das will ich am Zeichen "r" für ein Regal
verdeutlichen:
Die Funktion "s2quad" hat im Zeichencode "s" den Wert "r" übermittelt bekommen. Es handelt
sich um ein kleines "r", so merken wir uns "r0" (sonst "rr"). Wir suchen in der Filelistbox
der Objekte "flb" nach mindestens einem Dateinamen, der an passender Stelle (4 und 5 Zeichen)
diese beiden Buchstaben enthält.
Im "Info"-Ordner werden wir fündig; der Dateinamen lautet "tx_r0_h9_w3_d3.jpg"
"tx_r0_h9_w3_d3.jpg": Die Textur für ein Regal in der "Info"
Wir scannen nun nach dem Höhencode, der mit "_h" beginnt, gefolgt von einer Zahl zwischen
"0" und "9". Diese Zahl ist der Code für eine von 10 verschiedenen Höhendefinitionen,
die einmal die Höhe des Objekts angeben ("h"), zum anderen seine Höhenposition im Raum ("y").
Ist die Zahl "0", hat das Objekt keine Höhe (wie etwa ein Teppich) und "schwebt" ganz knapp
über dem Boden, ist die Zahl "1", ist das Objekt 80 cm hoch und steht auf dem Boden, "2"
bedeutet 60 cm hoch, beginnend bei einer Höhe von 80 cm (nützlich z.B. für ein Objekt, welches
auf einem Tisch steht).
Die grösseren Zahlencodes gehen gewissermassen umgedreht vor: Von der raumspezfischen Höhe
aus subtrahieren sie einen bestimmten Wert. So bedeutet etwa der Code "8" eine Höhe von
"Raumhöhe minus einem Meter und bis zum Boden reichend". Das hat den Vorteil, dass man
nachträglich die Gebäudehöhe ändern kann und die Objekte quasi mitwachsen oder -schrumpfen.
In unserem Fall finden wir den Höhencode "9", was gleichbedeutend mit "Raumhöhe bis zum
Boden" ist, in dem sich das Objekt befindet. Der "Info"-Raum ist, wie weiter oben angegeben,
3 m hoch. Regale in der "Info", die mit einem kleinen "r" in der Textmap markiert sind, haben
also eine Höhe von 3 Metern.
Diese Information geben wir an die Prozedur "setobjquad" weiter - ganz so wie bei Personen
mit der Prozedur "setpersonquad". In beiden Fällen füllen wir damit eine Struktur, die der
GrafikRenderer später "zu Papier" bringen kann.
Anders als bei Personen geht 's mit den Objekten danach aber weiter: Sie können mehr als nur
eine Textur besitzen und die Textur kann in spezifischer Weise über eine Seitenfläche
gekachelt werden.
In unserem Beispiel finden wir im Regal-Textur-Dateinamen "tx_r0_h9_w3_d3.jpg" zwar keinen
Code für mehrere Signaturen. Jedoch sagen uns die Codes "_w3" und "_d3", dass die Textur je
Seite in drei Teile gekachelt werden soll, jeweils in der Breite ("_w") und in der Tiefe ("_d").
Textur-Kachelung: Ohne Textur-Kachelung (rechts) wirkt der Regalinhalt unnatürlich gross
Sehen wir uns noch schnell das Zeichen "T" für Theke an: Hier finden wir in der Textur-Filelistbox
gleich zwei Texturen mit den Zeichencode "_tt":
"tx_tt_h1.jpg" und "tx_tt_h1_o.jpg": Zwei Texturen für 'ne Theke
Die Filenamen liefern uns den Höhencode "_h1", d.h., das Objekt soll 80 cm hoch sein und
bis zum Boden reichen. Es gibt keine Codes für die Textur-Kachelung, d.h., jede Textur
wird über die komplette Seite aufgespannt.
Anders als im vorherigen Fall finden wir aber im Dateinamen "tx_tt_h1_o.jpg" den Code "_o".
Der bedeutet, dass diese Textur nur auf die Oberseite des zugehörigen Objektes "T" geklebt
werden soll. Alle anderen Seiten bekommen die "seiten-neutralen" Textur "tx_tt_h1.jpg".
Neben "_o" für "Oben" gibt 's noch die Codes "_v" und "_h" für "Vorne" und "Hinten"
Wollen wir also sämtlichen Theken in der "Info" von vorne ein anderes Erscheinungsbild
verpassen, muss nicht eine Zeile neu programmiert werden; es genügt, ein Texturfile mit
passendem Namen in den Datei-Ordner zu schmeissen:
"tx_tt_h1_v.jpg": Eine alternative Forderfront für Theken in der "Info"
In gleicher Weise kann man auch Objekte höhenverändern, in dem man einfach deren
Textur-Dateinamen-Höhencodes ändert:
"tx_tt_h1.jpg" wird zu "tx_tt_h3.jpg": Geheimnisvolles Thekenwachstum in der "Info"
Endlich: Die Textmap-Objekte füllen die Strukturen für den GrafikRenderer
Wir haben unsere Textmap eingelesen und alle Objekte und Personen herausgeparst. Dann
haben wir über die zugehörigen Texttur-Dateinamen zusätzliche Informationen zum
Erscheinungsbild gewonnen. Bleibt nur, diese Informationen an Strukturen zu übergebe,
die der GrafikRenderer später "verstehen" kann.
Das ist eine ziemlich unspektakuläre Aktion, wie man hier sieht:
//personen in quad-struktur
procedure setpersonquad(tx:gluint;x,y,z,w,h,d:single;var q:tquad);
begin
q.pos.x:=x;q.pos.y:=y;q.pos.z:=z;
q.dim.w:=w;q.dim.h:=h;q.dim.d:=d;
//q.rot.x:=0;q.rot.y:=0;q.rot.z:=0;
q.tex.wc:=1;
q.tex.dc:=1;
q.tex.o:=tx;q.tex.u:=tx;
q.tex.l:=tx;q.tex.r:=tx;
q.tex.v:=tx;q.tex.h:=tx;
end;
//objekte in quad-struktur
procedure setobjquad(tx:gluint;x,y,z,w,h,d:single;var q:tquad);
begin
q.pos.x:=x;q.pos.y:=y;q.pos.z:=z;
q.dim.w:=w;q.dim.h:=h;q.dim.d:=d;
//q.rot.x:=0;q.rot.y:=0;q.rot.z:=0;
q.tex.wc:=1;
q.tex.dc:=1;
q.tex.o:=tx;q.tex.u:=tx;
q.tex.l:=tx;q.tex.r:=tx;
q.tex.v:=tx;q.tex.h:=tx;
end;
|
Wir füllen je Person bzw. Objekt einfach die zugehörige Quad-Struktur
der "hauptf.map". Alle nötigen Variablen haben wir zuvor ermittelt.
Wie man sieht, ist hier etwas Source auskommentiert worden, der eine Rotation
der Objekte ermöglicht. Ich habe das nicht weiter verfolgt, in "OGL_Henrys"
ist alles orthogonal an den Achsen ausgerichtet. Hier wäre aber die richtige
Stelle, das Modell zu erweitern. Aber natürlich müsste dann auch der GrafikRenderer
entsprechend angepasst werden.
Kein bisschen Zeit wird verschwendet
Unsere Anfangs beschriebene Prozedur "init" ist nun komplett abgearbeitet.
Das Programm hat nichts mehr zu tun, es ist "idle". Nun schlägt die Stunde unseres
IdleHandlers, den wir in "FormCreate" auf die Funktion "IdleHandler" umgebogen haben.
Denn diese wird nun automatisch aufgerufen.
procedure Thauptf.IdleHandler(Sender: TObject; var Done: Boolean);
begin
drawscene;
if _bremse>0 then sleep(_bremse);
done:=false;
end;
|
Die Konstante "_bremse" kann man verwenden, um die Grafikausgabe zu verlangsamen.
Das kann sinnvoll sein, wenn man in der Programmierphase Malaktionen im Detail
analysieren will. Üblicherweise hat "_bremse" aber natürlich den Wert "0".
Wir widmen uns nun der Funktion "drawscene", die über den IdleHandler aufgerufen
wird, wann immer das Programm Zeit dazu findet.
//binde textur-id ins modell ein-----------------------
procedure thauptf.coltex(tx:byte);
begin
glBindTexture(GL_TEXTURE_2D,txa[tx]);
glEnable(GL_TEXTURE_2D);
end;
//male decke, boden und alle quads des modells---------
procedure Thauptf.DrawScene;
var
x,y,z,r:integer;
dd,w,h,d:single;
scenerotz,sceneroty,xtrans,ytrans,ztrans:double;
collision:bool;
begin
//collisionscheck
panelu.paintp(collision);
if collision then begin
px:=lastpos.x;
py:=lastpos.y;
pz:=lastpos.z;
end;
//hintergrundfarbe
if fogok then glClearColor(0.5,0.5,0.5,1)
else glClearColor(0.0,0.0,0.0,1);
//bildpuffer komplett löschen
glClear(GL_COLOR_BUFFER_BIT OR GL_DEPTH_BUFFER_BIT);
glLoadIdentity;
xtrans:=-px;
ytrans:=-py-walkbias;
ztrans:=-pz;
lookupdown:=360.0-rotx;
sceneroty:=360.0-roty;
scenerotz:=360.0-rotz;
glRotatef(lookupdown,1.0,0,0);
glRotatef(sceneroty,0,1.0,0);
glRotatef(scenerotz,0,0,1.0);
glTranslatef(xtrans,ytrans,ztrans);
if _infocap then begin
caption:=
ftos(px)+'/'+ftos(py)+'/'+ftos(pz)+
' | '+
ftos(rotx)+'/'+ftos(roty)+'/'+ftos(rotz)+
' | '+
'Objekte: '+inttostr(map.anz);
end;
//room
x:=0;y:=0;z:=0;
w:=map.w/2;h:=hauptf.map.h;d:=map.d/2;
//boxmode: 0 nicht, 1=nur boden, 2=nur decke, 3=alles
if(boxmode=3)or(boxmode=2)then begin
//decke
coltex(map.txdecke);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(0,map.txdeckedc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(map.txdeckewc,map.txdeckedc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(map.txdeckewc,0);glVertex3d(x+w,y+h,z+0);
glEnd();
end;
if(boxmode=3)or(boxmode=1)then begin
//boden
h:=0;
coltex(map.txboden);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(0,map.txbodendc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(map.txbodenwc,map.txbodendc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(map.txbodenwc,0);glVertex3d(x+w,y+h,z+0);
glEnd();
end;
if map.name='Flur' then begin
//zwischendecke
coltex(map.txdeckez);
//längsbalken
dd:=1;
for z:=0 to 2 do begin
dd:=dd+2;
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,5.2,dd+0);
glTexCoord2f(0,1);glVertex3d(x+0,5.2,dd+0.5);
glTexCoord2f(w,1);glVertex3d(x+w,5.2,dd+0.5);
glTexCoord2f(w,0);glVertex3d(x+w,5.2,dd+0);
glEnd();
end;
//querbalken
dd:=0;
for x:=0 to ((trunc(w)-1)div 5)-1 do begin
dd:=dd+5;
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(dd,5.3,0);
glTexCoord2f(0,1);glVertex3d(dd,5.3,d);
glTexCoord2f(1,1);glVertex3d(dd+0.5,5.3,d);
glTexCoord2f(1,0);glVertex3d(dd+0.5,5.3,0);
glEnd();
end;
end;
for r:=0 to map.anz-1 do begin
drawquad(map.quads[r]);
end;
SwapBuffers(DC);
end;
|
Die Funktion "DrawScene" ist im Prinzip dieser ominöse GrafikRenderer, von dem
immer wieder die Rede war. Hier endlich also werden die zuvor gefüllten TQuads der
TMap-Struktur zu OGL-Objekten gewandelt und auf unserer Hauptform als OGL-Grafik
ausgegeben.
Zunächst wird geprüft, ob unsere aktuelle Position im Modell eine Kollision verursacht,
d.h., ob wir in eine Wand oder einen Schrank oder ähnliches gerannt sind.
Sollte dies der Fall sein, bleiben wir einfach auf der letzten zuvor möglichen Stelle
("lastpos") stehen.
Dann bekommt das Ausgabefenster eine Hintergrundfarbe verpasst. Die ändert
sich, je nachdem, ob der Nebelmodus ("fogok") aktiv ist oder nicht.
Die OGL-Befehle "glClear" und "glLoadIdentity" löschen die OGL-Puffer und
bringen uns im Modell an die Ursprungsposition.
Nun werden eine paar Variablen berechnet, die die Position und Sehrichtung
im Modell wiedergeben, die unser Avatar aktuell inne hat. Diese Werte können
sich seit der letzten "DrawScene"-Aktion natürlich geändert haben.
Wie bereits erwähnt, bewegen nicht wir uns im Modell, sondern das Modell bewegt
sich um uns. Durch das vorherige "glLoadIdentity" befinden wir uns zur Zeit am
Ursprungsort. Um nun das Modell an die richtige, eben berechnete Stelle zu rücken
und zu drehen, verwenden wir die OGL-Befehle "glRotatef" und "glTranslatef".
"glRotatef" rotiert das Modell gemäss unserer Blickrichtung bzw. in genau
umgekehrter Weise, wie weiter oben beschrieben. Schauen wir 20 Grad nach unten,
kippt das Modell 20 Grad nach oben, blicken wir 180 Grad "aus dem Modell heraus",
wird das Modell um 180 Grad um die Y-Achse gedreht, so dass wir wieder scheinbar
in das Modell hineinblicken.
Nach der Rotation sorgt "glTranslatef" dafür, dass sich das komplette Modell an
den Achsen entlang verschiebt, so das wir scheinbar an der zuvor berechneten Position
zu stehen kommen. Tatsächlich bleiben wir aber stets am Ursprungsort fixiert.
Die Reihenfolge der Bewegung des Modells ist nicht einerlei
Ich erinnere mich noch, dass ich Anfangs die Befehle "glRotatef" und "glTranslatef"
in umgekehrter Reihenfolge ausgeführt habe. Ich wechselte also erst die Position,
dann begann ich die Rotation. Ich dachte, dass käme auf das gleiche heraus. Dem ist
aber nicht so.
Wenn wir am Ursprungsort stehen und das Modell rotieren lassen, bildet unsere Position
den Schnittpunkt der X-, Y- und Z-Achse. Egal wie wüst wir auch die Sehrichtung verändern,
wir bleiben doch stets im unteren, nein, besser: vorderen linken Ecke des Modells stehen.
Bewegen wir uns aber mittels "glTranslatef" vom Ursprungsort weg und rotiere
anschliessend das Modell, dann liegen die Drehachsen ein Stück entfernt von uns.
Das hat zur Folge, dass uns durch die Rotation quasi der Boden unter den Füssen
weggezogen wird und wir so erneut den Standort im Modell wechseln - der dann nichts
mehr mit dem zuvor berechneten zu tun hat!
Holla, die Waldfee! Das hat mich ein paar Stunden gekostet, bis ich 's endlich
geblickt hatte :-)
Ein Deckel für den Topf, ein Boden für's Fass
Weiter im Source: Wir haben unser Model um uns zurechtgerückt, nun kann es perspektivisch
korrekt gemalt werden. Da wir uns in einem Zimmer-Raum befinden und Zimmer üblicherweise
einen Boden und eine Decke haben, beginnen wir mit diesen.
Wie bereits erwähnt werden Decke und Boden unserer Räume anders als Objekte behandelt,
da wir diese im 2D-Textmap-Modell ja nicht eintragen können; Boden und Decke nehmen
schliesslich in der 2D-Ansicht den gleichen Raum ein. Und da sie überall vorhanden sind,
wäre auch kein Platz mehr gewesen, um sonstige Objekte eintragen zu können.
Wir berechnen daher einfach die Ausmasse von Decke und Boden. Das ist aber trivial.
Zunächst zur Decke: In der Map-Struktur haben wir die Breite "map.w" und Tiefe "map.d" des
aktuellen Raumes vermerkt - der Boden ist natürlich entsprechend breit und tief. Die Y-Position,
an der die Decke "im Raum schwebt", ergibt sich logischerweise aus der Höhe des Raums "map.h".
Faulheit und die Folgen
Sieht man sich obigen Source an, stellt man vielleicht überrascht fest, dass als Breite
und Tiefe für die Decke nur jeweils die Hälfte der Map-Werte angegeben ist, die Höhe aber
1:1 übernommen wurde.
Das hat mit Faulheit zu tun. Meine ursprünglichen Textmaps waren nämlich so ausgelegt, dass
jedes Zeichen darin einen Quader mit einem Meter Breite und einem Meter Tiefe beschrieb.
Das schuf aber schnell Platzprobleme; die Maps wurden zu grob, um alle gewünschten Objekte
darin sauber plazieren zu können. Ausserdem waren Wände mit einem Meter Dicke auch sehr weit
von der Realität bei Henry's entfernt. Ich arbeite ja nicht in Fort Knox :-)
Ich änderte also kurzerhand den Massstab: Jedes Zeichen in der Textmap bedeutete ab da nur
noch 50 cm Breite und 50 cm Tiefe. Weil die Höhe in den Textmaps keine Rolle spielt, blieb
deren Massstab davon unberührt.
Aber natürlich mussten alle Textmaps entsprechend aufgebohrt werden: Um einen Meter
Breite zu versinnbildlichen waren jetzt ja zwei statt einem Zeichen nötig. Die ganzen
Maps mussten also quasi Zeichen für Zeichen verdoppelt werden. War 'ne elende Schufterei,
denn zu dem Zeitpunkt waren meine Textmaps mit den 1 x 1 m Ausmassen eigentlich schon
fertig gewesen.
Naja, nicht nur die Textmaps waren fertig, auch der GrafikRenderer arbeitete bereits.
Und da ich bisher mit den "natürlichen" Meter-Werten rechnete, und nun plötzlich 20 Zeichen
Breite in der Textmap nur noch 10 Meter Breite im realen Modell entsprechen, halbierte ich
einfach die Map-Werte bei der Übergabe an den GrafikRenderer.
Ist alles so schön bunt hier
Okay, wir wissen die Dimension der Decke und wo sie im Modell "anzuheften" ist.
Aber einfach nur eine farblose Decke ist etwas öde. Daher bekommt die Decke wie
alle anderen Objekte im Modell eine Textur verpasst.
Die Textur der Decke haben wir zuvor in der "setmap"-Prozedur geladen. Ihre ID steht in
der Map-Variablen "map.txdecke". Mittels der "coltex"-Funktion machen wir nun die
Decken-Textur zur aktuellen OGL-Textur. Alle nachfolgenden Textur-Befehle,
eingekapselt in "glBegin" und "glEnd", beziehen sich dann automatisch auf sie.
Über die OGL-Befehle "glTexCoord2f" und "glVertex3d" wird die aktuelle Textur an ein
Objekt "geklebt", optional in gekachelter Weise. Das kann man ungefähr so lesen:
Nimm die Textur-Ecke links unten: glTexCoord2f(0,0);
Plaziere dieses Eck links vorne im Modell: glVertex3d(0,h,0);
Nimm die Textur-Ecke links oben: glTexCoord2f(0,map.txdeckedc);
Plaziere dieses Eck links hinten im Modell: glVertex3d(0,h,d);
Nimm die Textur-Ecke rechts oben: glTexCoord2f(map.txdeckewc,map.txdeckedc);
Plaziere dieses Eck rechts hinten im Modell: glVertex3d(w,h,d);
Nimm die Textur-Ecke rechts unten: glTexCoord2f(map.txdeckewc,0);
Plaziere dieses Eck rechts vorne im Modell: glVertex3d(w,h,0);
|
Der Boden unserer Welt wird in gleicher Weise realisert. Wichtigster Unterschied
ist, dass eine andere Textur verwendet wird ("map.txboden"), und das bei der Textur-Kleberei
die Höhe "h" auf Null gesetzt wird.
Zwischenwelten
Eine Besonderheit bei HENRY's ist der "Flur" zwischen "Info" und "Mode". Er ist mit 7 Metern
sehr hoch und besitzt eine Art Zwischendecke. Dem wollte ich auch im Modell Rechnung tragen.
Erkennt das Programm am Map-Namen "map.name", dass wir uns im Flur befinden, wird nun auch
noch eine Zwischendecke generiert, die sich in etwa 5 Metern Höhe befindet.
Diese Zwischendecke ist eine Art Gitterstruktur, so dass man problemlos durchschauen kann,
und darüber die "echte" Decke sieht. Sie baut sich aus einer Reihe von Quer- und Längsbalken
auf, die im Prinzip genauso wie die vollständige Decke erzeugt wird, allerdings über eine
Schleife realisiert, wobei jeweils neue Koordinaten für die Textur-Geschichte berechnet
werden.
Objekte der Begierde
Jetzt endlich werden unsere Personen und Objekte in's OGL-Modell integriert. Eine einzige
kleine Schleife erledigt den Job, bei der alle Map-Objekte "maps.quads" an die Prozedur
"drawquad" übergeben werden.
procedure Thauptf.drawquad(q:tquad);
var
x,y,z,w,h,d:single;
begin
x:=q.pos.x;y:=q.pos.y;z:=q.pos.z;
w:=q.dim.w;h:=q.dim.h;d:=q.dim.d;
x:=x/2;y:=y;z:=z/2;
w:=w/2;h:=h;d:=d/2;
if h<hauptf.map.h then begin
//oben
coltex(q.tex.o);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+h,z+0);
glEnd();
end;
//links
coltex(q.tex.l);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+0,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+0,y+0,z+d);
glEnd();
//rechts
coltex(q.tex.r);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+w,y+0,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+w,y+h,z+0);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+0,z+d);
glEnd();
//vorne
coltex(q.tex.v);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+0,z+d);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+d);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+d);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+0,z+d);
glEnd();
//hinten
coltex(q.tex.h);
glBegin(GL_QUADS);
glTexCoord2f(0,0);glVertex3d(x+0,y+0,z+0);
glTexCoord2f(0,-q.tex.dc);glVertex3d(x+0,y+h,z+0);
glTexCoord2f(-q.tex.wc,-q.tex.dc);glVertex3d(x+w,y+h,z+0);
glTexCoord2f(-q.tex.wc,0);glVertex3d(x+w,y+0,z+0);
glEnd();
end;
|
Die "Fertigung" der Quads unterscheidet nicht mehr zwischen Personen
und Objekten; sie werden auf die genau gleiche Weise generiert.
Das Verfahren ähnelt auch sehr dem, was wir schon kennen, um Boden
und Decke zu erzeugen. Anders als Boden und Decke besitzen unsere
Quader aber mehrere Seiten, die "bemalt" werden müssen, sprich, mit
Texturen versehen.
Höhe, Breite, Tiefe, sowie die Koordinaten sind alle in der übergebenen
Quads-Struktur vermerkt. Breite, Tiefe, X- und Y-Position müssen wieder
aus den oben genannten Gründen halbiert werden. Höhe und Y-Position
bleiben dagegen unverändert.
Jetzt können wir Seite für Seite die Quader mit Texturen versehen.
Ist ein Objekt genauso hoch wie die Decke - das gilt z.B. für
eine Wand - dann ersparen wir es uns, die "Kopf"-Textur anzukleben.
Ebenso bemalen wir die Unterseite der Objekte nicht, da diese i.d.R.
auf dem Boden aufliegen, also ohnehin nie zu sehen sind. Das bringt
etwas Speed-Gewinn.
Bei den "glTexCoord2f"-Kommandos habe ich's mir offensichtlich einfach gemacht:
Egal welche Seite gerade bearbeitet wird, stets übergebe ich die gleichen
Parameter.
Nur bei den "glVertex3d"-Parametern muss man logisch vorgehen, damit an die
richtigen Stellen gemalt wird. Basierend auf die für ein Objekt fixen "x"-,
"y"- und "z"-Werten wird nur jeweils die Breite "w", die Höhe "h" und/oder
die Tiefe "d" dazu addiert, damit's passt. Keine grosse Sache also.
End of Rendering
Gehen wir zurück in die "DrawScene"-Funktion. Hier fehlt uns nach
Abschluss des ... mh ... Renderings ... nur noch ein Kommando: SwapBuffers(DC)
Dadurch wird die eben in einem Puffer generierte OGL-Szenerie mit einem Schlag
auf unseren ganz am Anfang definierten Device Context "DC" kopiert, sprich, im
Hauptfenster zu Anzeige gebracht.
That's it! Unsere Welt existiert!
OGL_HENRYs: Home-Position in der "Info"
Und sie bewegt sich doch
Wir können das Hauptfenster beliebig vergrössern und verkleinern, die Grafik passt sich
voll-automatisch an. Ist 'ne prima Sache. Das haben wir durch Abfangen des OnResize-Ereignis
der Form erreicht.
Aber egal, wie sehr wir nun auch am Fenster rütteln und schütteln mögen, wir erhalten
stets nur den gleichen Einblick in unsere Welt, 20 Grad nach links in die "Info" hinein.
Das ist öde. Da muss mehr Action rein!
Bewegung wird in OGL_Henrys über verschiedene Tastaturcodes realisiert. Dazu basteln
wir uns eine Funktion zum OnKeyDown-Ereignis der Form:
procedure Thauptf.FormKeyDown(
Sender: TObject; var Key: Word;Shift: TShiftState
);
begin
lastpos.x:=px;
lastpos.y:=py;
lastpos.z:=pz;
if ssctrl in shift then begin
if key=vk_up then rotx:=rotx-_rstep
else if key=vk_down then rotx:=rotx+_rstep;
if key=vk_left then rotz:=rotz-_rstep
else if key=vk_right then rotz:=rotz+_rstep;
exit;
end;
if key=vk_right then roty:=roty-_rstep
else if key=vk_left then roty:=roty+_rstep
else if key=vk_prior then py:=py+_pstep/2
else if key=vk_next then py:=py-_pstep/2
else if key=vk_up then begin
px:=px-sin(roty*_piover180)*_pstep;
pz:=pz-cos(roty*_piover180)*_pstep;
if walkbiasangle>=359.0 then walkbiasangle:=0.0
else walkbiasangle:=walkbiasangle+90;
walkbias:=sin(walkbiasangle*_piover180)/20.0;
end
else if key=vk_down then begin
px:=px+sin(roty*_piover180)*_pstep;
pz:=pz+cos(roty*_piover180)*_pstep;
if walkbiasangle<=1.0 then walkbiasangle:=359.0
else walkbiasangle:=walkbiasangle-90;
walkbias:=sin(walkbiasangle*_piover180)/20.0;
end
else if key=ord('L') then begin
if lightok then begin
gldisable(GL_LIGHT0);
gldisable(GL_LIGHTING);
lightok:=false;
end
else begin
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
lightok:=true;
end;
end
else if key=ord('B') then begin
inc(boxmode);if boxmode>3 then boxmode:=0;
end
else if key=vk_space then sethome
else if key=ord('H') then helpm.visible:=not helpm.visible
else if key=ord('P') then p.visible:=not p.visible
else if key=ord('N') then begin
if fogok then begin
gldisable(GL_FOG);
fogok:=false;
end
else begin
glEnable(GL_FOG);
fogok:=true;
end;
end
else if key=ord('1') then mapu.setmap_info
else if key=ord('2') then mapu.setmap_flur
else if key=ord('3') then mapu.setmap_mode
else if key=ord('4') then mapu.setmap_edv
else if key=vk_escape then close;
end;
|
Unsere Position im Modell wird durch die Form-Variablen "px", "py"
und "pz" definiert. Die Blickrichtung steckt in den Form-Variablen
"rotx", "roty" und "rotz".
Bevor wir eine Bewegung ausführen, retten wir erst einmal unsere aktuelle
Position in "lastpos". Denn es könnte ja sein, dass uns unser nächster "Schritt"
nach vorne direkt in eine Wand führt. Da dies aber in der Realität schlechterdings
möglich ist, muss uns das Programm anschliessend wieder auf die vorherige Position
zurücksetzen.
Die Koodinaten-Änderungen erfolgen über die Cursortasten. "Cursor hoch" führt uns
tiefer ins Modell rein, "Curser runter" weiter raus.
Beim Vorwärts- und Rückwärtsgehen habe ich mir eine Idee geklaut, die ich
bei jemanden anderem im Source gefunden habe: Um das Auf und Ab beim Gehen
zu simulieren, ändert sich durch die Bewegung die Sichthöhe "py" um einen
Cosinus-Sinus-Wert "walkbias". Die Mathematik dahinter kapiere ich zwar nicht
ganz, aber es funzt ganz nett.
Mit den Cursor-Tasten "links" und "rechts" können wir uns um die Y-Achse drehen. Drücken
wir gleichzeitig die "STRG"-Taste, dann können wir die Blickrichtung neigen und senken
bzw. uns in Schieflage begeben.
Schräge Perspektive: Gekippt im "Flur"
Ach ja, die "Gewichtigkeit", mit der ein Tastendruck eine Koordinaten- und/oder
Rotations-Variable ändert, wird in Konstanten festgeschrieben. So bewirkt etwa ein
"Cursor hoch"-Tastaturdruck eine Bewegung in den Raum um "_pstep" Meter. In meinem Modell
hat "_pstep" den konstanten Wert "1", was ja auch in etwa der Länge eines Schrittes
entspricht.
Die "B"-Taste schaltet in abwechselnder Reihenfolge die Anzeige von Boden und Decke ab.
Das erlaubt ungewohnte Einsichten von unten und oben in das Modell hinein.
Moden ohne Boden: Unter uns nur ein schwarzes Loch
Um nach oben und unten "schweben" zu können, kann man die "Bild auf"- und Bild ab"-Tasten
verwenden. Das ermöglicht schöne Rundflüge über das komplette Modell. So muss sich
Supermann fühlen, wenn es wieder heisst "Ist es ein Vogel, ist es ein Flugzeug ...?"
Bild Hoch/Runter in der "Info": Nur echtes Fliegen ist schöner
Mit "Space" springt man an die HomePosition des Raums zurück, in dem man sich gerade befindet.
Mit den Zahlen "1" bis "4" kann man schnell von einem Raum zum anderen springen:
1) "Info"-Raum: Das Büro des ollen Schwamms
2) "Flur"-Raum: Hier kommt sonst kein Kunde hin
3) "Mode"-Raum: Was für'n Spanner macht 'n da auf der Toilette mit der Kamera rum?
4) "EDV"-Raum: Chaos für die Chaoten
Mittels "L"-Taste kann ein anderes Beleuchtungsmodell gewählt werden.
4) "Flur"-Raum: Im Dunkeln lässt sich gut Munkeln
"N" schaltet Nebel an und aus. Vielleicht nützlich für Feuerwehr-Übungs-Simulationen :-)
The Fog: Nebel des Grauens im "Flur"
Die "H"-Taste dient dazu, einen Hilfeschirm anzuzeigen oder abzustellen.
Hilfe-Schirm: A little help for my friends
Und zu guter letzt aktiviert bzw. deaktiviert "P" die Anzeige der Minimap. Ist die
Minimap zu sehen, findet übrigens ein Kollisions-Check statt, ist sie nicht zu sehen,
können wir wie die Geister durch Wände gehen.
Minimap: Die Welt in 2D mit Kollisionsgarantie
Feste Körper
Unser Modell liefert uns bereits eine schöne kleine Welt, in der wir frei umher
wandern können. Grenzenlos frei. Rein gar nichts hält uns auf. Bisher! Denn das wollen
wir ändern; geht im wahren Leben ja auch nicht.
Soweit ich das überblickt habe, scheint es in OGL durchaus die Möglichkeit zu geben,
festzustellen, ob sich zwei Körper "berühren", ob also eine Kollision zwischen ihnen
stattgefunden hat. Das könnte man sicher irgendwie nutzen, um zu verhindern, dass
unser Avatar durch Wände oder sonstige Objekte gehen kann.
Die Geschichte habe ich aber nie richtig kapiert. Und auch zu wenig Informationen
darüber gefunden. So blieb ich lieber auf vertrautem Terrain, und programmierte mir
eine einfache Kollisions-Kontrolle selbst. Benutzt habe ich dazu die 2D-Minimap, um
die wir uns jetzt kümmern wollen.
Von 2D nach 3D nach 2D
Wir erinnern uns: In der "mapu"-Unit-Funktion "rdmap" haben wir das 2D-Textmap-Modell
eingelesen, alle Objekte darin ausgeparst und die zugehörigen TQuads generiert. Ganz
am Schluss wurde noch "panelu.initp" aufgerufen, bevor das Idle-Ereignis eintrat und
der GrafikRenderer los legen konnte.
"panelu.initp" sorgt dafür, dass die eben generierten TQuads gleich wieder in ein
2D-Modell zurück konvertiert werden - nämlich in eine "Minimap", die man im Programm
aktivieren kann und die dann oben links - über der OGL-Grafik liegend - gemalt wird.
Die Minimap zeigt uns den Raum, in dem wir uns gerade befinden, senkrecht von oben,
als vereinfachtes 2D-Modell. Unsere eigene Position im Modell wird darin durch einen
blinkenden Punkt hervorgehoben.
Die Unit "panelu"
Alle Funktionen zur Minimap und zur Kollisionskontrolle sind in der Unit "panelu"
gekapselt. "panelu" deshalb, weil die Minimap letztlich auf ein TPanel gemalt wird.
Okay, der Name ist nicht sehr clever gewählt, "minimapu" oder "collchku" wäre sicher
aussagekräftiger gewesen.
Die Initialisierung der Minimap erfolgt - wie eben gesehen - durch Aufruf von
"panelu.initp":
const
_dw=1;
_dd=1;
_pcolor=clwhite;
type
tp=class(tpanel)
end;
procedure initp;
var
cv:tcanvas;
rr,x,z,r,c,d,w:integer;
s:string;
collision:bool;
ok:bool;
begin
w:=trunc(hauptf.map.w*_dw)+10;
d:=trunc(hauptf.map.d*_dd)+10;
hauptf.p.clientWidth:=w;
hauptf.p.clientheight:=d;
//tp(hauptf.p).canvas.brush.Color:=clblack;
hauptf.pbmp.Width:=w;
hauptf.pbmp.height:=d;
//cv:=tp(hauptf.p).canvas;
cv:=hauptf.pbmp.canvas;
cv.Brush.color:=_pcolor;
cv.Rectangle(0,0,w,d);
for r:=0 to hauptf.mapm.lines.count-1 do begin
z:=r*_dd+5;
s:=hauptf.mapm.Lines[r];
for c:=1 to length(s) do begin
if s[c]=' ' then continue;
if s[c] in ['0'..'9'] then continue;
// durch bestimmte zeichen kann durchgegangen
// werden, z.B. Vorhang
ok:=false;
for rr:=0 to high(hauptf.map.durchgang)-1 do begin
if s[c]= hauptf.map.durchgang[rr] then begin
ok:=true;
break;
end;
end;
if ok then continue;
x:=(c-1)*_dw+5;
cv.Pixels[x,z]:=clblack;
end;
end;
hauptf.ppbmp.width:=w;
hauptf.ppbmp.height:=d;
paintp(collision);
end;
|
Wie man sieht, holen wir uns die Breite und Tiefe der aktuellen Map, multiplizieren
sie mit dem Konstanten "_dw" und "_dd", addieren noch jeweils 10 Pixel dazu, und
verpassen dann dem TPanel "hauptf.p" die entsprechenden Ausmasse.
Da "_dw" den Wert "1" hat, gilt: Ist der aktuelle Raum 100 Meter breit, hat die
Variable "hauptf.map.w" den Wert "200" (200*50cm=100m), und das heisst, die Minimap
wird 210 Pixel breit. Die 10 überschüssigen Pixel werden benötigt, um um das 2D-Modell
noch etwas Rand zu lassen.
Entsprechendes gilt für die Tiefe der Map, die im 2D-Modell zur Höhe des Panels wird.
Wir holen uns den Canvas einer zuvor in der Hauptform initialisierten Bitmap
"hauptf.pbmp.canvas" und färben ihn einheitlich ein.
Anschliessend durchlaufen wir die Textmap zeilen- und zeichenweise. Ähnlich wie bei
der TQuad-Generierung suchen wir nach Objekt-Zeichen. Anders als dort werden aber
Personen-Zeichen ignoriert; Personen wollen wir in der Minimap nicht anzeigen (was
auch zur Folge hat, dass wir später problemlos durch Personen durchlaufen können;
wen das stört, der kann hier ansetzen, um's zu ändern).
Mir fällt dabei gerade auf, dass ich weiter oben gelogen habe: Wir verwenden hier
gar nicht die generierten Quads des 3D-Modells, um daraus ein 2D-Modell zu extrahieren,
sondern wir setzten die 2D-Textmap direkt in eine 2D-Pixelmap um. Ist ja auch viel
einfacher zu realisieren. Die paar Pixel werden so schnell gemalt, dass eine
Objekt-Gruppierung wahrlich nicht nötig ist. Hatte ich nur vergessen.
Wenn wir also in der Textmap ein Objekt-Zeichen finden, wird dessen Position in Pixel
umgerechnet und an die entsprechende Stelle im "hauptf.pbmp.canvas" ein schwarzer
Punkt gemalt. Zuvor wird aber noch geprüft, ob es sich bei dem gefundenen Objekt-Zeichen
nicht um einen Durchgang handelt. Durchgänge werden in der Minimap nämlich auch nicht
angezeigt, was ebensolche Folgen wie bei Personen hat, diesmal aber sogar beabsichtigt: Wir
können später durch ihre 3D-Repräsentanten hindurchgehen.
Am Ende der Schleife entält die Bitmap "hauptf.pbmp" ein Abbild der Textmap, bei der
nur die "festen" Körper in Form schwarzer Punkte eingezeichnet sind. Bis zum Wechsel
in einen anderen Raum bleibt diese Bitmap nun unverändert.
Um unsere eigene Person in der Minimap darstellen zu können, die sich ja permanent
ändern kann, benötigen wir eine zweite Bitmap "hauptf.ppbmp". Diese wird nun noch
grössenmässig an die "hauptf.pbmp" angepasst.
Zuletzt wird "paintp" aufgerufen.
Hat's geknallt? Und überhaupt, wo bin ich hier?
In der Bitmap "hauptf.pbmp" ist der sich nicht ändernde Teil der Minimap gespeichert.
Nun gilt es, eine zweite Bitmap "hauptf.ppbmp" zu füllen, die darüberhinaus auch unsere
aktuelle Position im Modell anzeigt.
Umgerechnet auf die Pixeldimension der Minimap kann nun geprüft werden, ob sich
an unserer aktuellen Position bereits ein schwarzer Punkt, sprich, ein festes Objekt,
befindet. Ist dies der Fall, wird der Var-Parameter "collision" auf "true" gesetzt,
ansonsten auf "false".
Im "false"-Fall wird ein roter Punkt gesetzt, der unsere aktuelle Position im Modell
wiedergibt. Oder ein gelber Punkt, je nachdem, welchen Wert die Globale "hauptf.ccolor"
trägt. Dadurch blinkt unsere Position in der Minimap abwechselnd rot und gelb, wodurch
sie leichter zu erkennen ist.
Darüberhinaus ist auch noch zu prüfen, ob wir auf einem der Map-Ausgänge gelandet
sind. Haben wir nämlich einen solchen Ausgang erreicht, bedeutet das für das Programm,
dass wir in den nächsten Raum "transferiert" werden müssen.
Zu guter letzt wird die adaptive Bitmap "hauptf.ppbmp" mit unserer Cursor-Position auf
den Canvas des TPanel "hauptf.p" kopiert, wodurch die Minimap angezeigt wird.
All das erledigt die Prozedur "paintp":
procedure paintp(var collision:bool);
var
r,x,z,w,d:integer;
s:string;
begin
collision:=false;
if not hauptf.p.Visible then exit;
//dimensionen
w:=hauptf.pbmp.Width;
d:=hauptf.pbmp.height;
//mapbackground
bitblt(
hauptf.ppbmp.canvas.Handle,0,0,w,d,
hauptf.pbmp.canvas.handle,0,0,
srccopy
);
//aktuelle position
x:=trunc(hauptf.px)*2*_dw+5;
z:=trunc(hauptf.pz)*2*_dd+5;
if(x>1)and(x<w-2)and(z>1)and(z<d-2)then begin
//kollision?
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x,z])<>_pcolor);
if not collision then
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x,z+1])<>_pcolor);
if not collision then
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x+1,z])<>_pcolor);
if not collision then
collision:=(tcolor(hauptf.ppbmp.canvas.pixels[x+1,z+1])<>_pcolor);
if not collision then begin
hauptf.ppbmp.canvas.pixels[x,z]:=hauptf.ccolor;
end
else begin
//ausgang erwischt?
x:=trunc(hauptf.px)*2;
z:=trunc(hauptf.pz)*2;
if
(x>-1)and(x<trunc(hauptf.map.w))and
(z>-1)and(z<trunc(hauptf.map.d))
then begin
s:=hauptf.mapm.Lines[z];
s:=s[x+1];
for r:=0 to high(hauptf.map.outa)-1 do begin
if s=hauptf.map.outa[r].ch then begin
collision:=false;
chgmap(hauptf.map.outa[r]);
exit;
end;
end;
end;
end;
end;
if hauptf.ccolor=clred then hauptf.ccolor:=clyellow
else hauptf.ccolor:=clred;
try
bitblt(
tp(hauptf.p).canvas.Handle,0,0,w,d,
hauptf.ppbmp.canvas.handle,0,0,
srccopy
);
except
end;
end;
|
Wir prüfen hier zunächst, ob die Minimap überhaupt angezeigt werden soll.
Ist dies nicht der Fall, dann springen wir direkt aus der Prozedur raus,
wodurch der Var-Parameter "collision" stets "false" bleibt. Was wiederum
zur Folge hat, dass wir im Modell von festen Objekten generell nicht behindert
werden und sie problemlos durchqueren können.
Ansonsten kopieren wir die fixe Bitmap "hauptf.pbmp" auf die adaptive Bitmap
"hauptf.ppbmp".
Anhand unserer Modell-Positions-Variablen "hauptf.px" und "hauptf.pz" bestimmen
wir die Pixel-Position in der Minimap, an der wir uns gerade befinden. Wir prüfen
dann die Farbe der Pixel "hauptf.ppbmp" rund um uns herum. Taucht dort ein Pixel
mit schwarzer Farbe auf, dann haben wir uns zu dicht an einen "festen Körper"
herangewagt; es wird das Kollisionsflag "collision" gesetzt.
Liegt keine Kollision vor, wird unsere Position in der Bitmap markiert.
Liegt eine Kollision vor, bleibt zu prüfen, ob diese Kollision durch einen
Ausgang verursacht wurde. Dazu klopfen wir das "hauptf.map.outa"-Array ab.
Sollten wir tatsächlich einen Ausgang getroffen haben, wird die Funktion
"chgmap" aufgerufen und die Prozedur verlassen. Ansonsten wird fortgefahren und
die Minimap zur Anzeige gebracht.
Ach ja, letztlich wird der Wert von "collision" übrigens im GrafikRenderer
geprüft, denn der wird ja permanent aufgerufen, eine Kollision kann also nicht
"verloren" gehen. Wir wir dort gesehen haben, bewirkt eine Kollision, dass wir
nur einfach an unsere vorherige Position "lastpos" zurück versetzt werden.
Wo soll's denn hingehen, Schätzchen?
Einige Kollisionen mit festen Körpern bedeuten, dass wir einen Ausgang erreicht
haben. In der Prozedur "paintp" wurde auch schon festgestellt, um welchen Ausgang
der aktuellen Map es sich genau handelt. In den "setmap"-Prozeduren wurden zudem
bereits die Eigenschaften der einzelnen Ausgänge definiert. Dies kommt uns nun zu
gute, wenn die Funktion "chgmap" aufgerufen wird:
procedure chgmap(o:tout);
var
r,c:integer;
s,mapstart:string;
rx,ry,rz:single;
begin
//positionswerte merken
mapstart:=o.mapstart;
rx:=hauptf.rotx;
ry:=hauptf.roty;
rz:=hauptf.rotz;
//map laden
if o.map='Info' then setmap_info
else if o.map='Flur' then setmap_flur
else if o.map='Mode' then setmap_mode
else if o.map='EDV' then setmap_edv;
//finde startposition
for r:=0 to hauptf.mapm.Lines.count-1 do begin
s:=hauptf.mapm.Lines[r];
c:=pos(mapstart,s);
if c>0 then begin
hauptf.px:=(c+1)/2;
hauptf.pz:=r/2;
//rotation anpassen
hauptf.rotx:=rx;
hauptf.roty:=ry;
hauptf.rotz:=rz;
exit;
end;
end;
end;
|
In "o" bekommt die Funktion den "getroffenen" Ausgang übergeben.
In dessen Struktur-Attribut "o.map" ist vermerkt, wo die Reise
hinzugehen hat.
Treffen wir z.B. auf den Nord-Ausgang des "Flur", dann steht hier,
dass der nächste Raum die "Mode" zu sein hat. Treffen wir dagegen
auf den Süd-Ausgang, dann muss die "Info" folgen.
Die entsprechende "setmap"-Prozedur wird also aufgerufen. Nun gilt es noch,
im neuen Raum auch an der richtigen Stelle "herauszukommen".
Die Information steckt im Attribut "o.mapstart", und zwar in Form einer
zweistelligen Zahl, die mit "9" beginnt. Folgendes Beispiel soll dies kurz
illustrieren:
Ausgang 'D' führt in 'RAUM II' an Position '92'
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
W W
W RAUM I W
W W
W 90 W
WWWWWWWWWWWWWWWWWDDDWWWWWWWWWWWWWWWWWWW
--------------------------------------------
Ausgang 'd' führt in 'RAUM I' an Position '90'
Ausgang 'E' führt in 'RAUM III' an Position '90'
Ausgang 'D' führt in 'RAUM III' an Position '91'
WWWWWWWWWWWWWWWWWdddWWWWWWWWWWWWWWWWWWW
W 92 W
W RAUM II W W
W W W
W 90 W 91 W
WWWWWEEEWWWWWWWWWDDDWWWWWWWWWWWWWWWWWWW
--------------------------------------------
Ausgang 'E' führt in 'RAUM II' an Position '90'
Ausgang 'd' führt in 'RAUM II' an Position '91'
WWWWWEEEWWWWWWWWWdddWWWWWWWWWWWWWWWWWWW
W 90 W 91 W
W RAUM III W W
W W W
W W
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
|
Wir suchen also in der Textmap mittels der "pos"-Funktion nach der
Zahlen-Zeichenfolge "o.mapstart" und berechnen daraus dann unsere neuen
"hauptf.px"- und "hauptf.pz"-Werte, die uns im Modell an die richtige
Stelle bringen.
Zuletzt wird noch die Rotation unserers Avatars angepasst (denn die wurde
durch die "setmap"-Funktion rückgesetzt) so dass wir den neuen Raum
mit der gleichen "Orientierung" betreten, wie wir den letzten verlassen
haben. Wir könnten ja auf die komische Idee kommen, neue Räume nur noch
rückwärtsgewand zu betreten - und solche Spleens unterstützen wir doch
gerne :-).
Damit hätten wir's nun wirklich geschafft: Die "OGL_HENRYs"-Welt ist fertig!
So ganz fertig war die OGL-Welt dann natürlich doch noch nicht. Denn einen
wesentlichen Aspekt habe ich noch gar nicht genannt, der aber entscheidend
zur Optik beitrug: Die Anfertigung der Texturen.
Das hat Spass gemacht. Mit einer Digi-Kamera bewaffnet bin ich durch's
ganze Haus gelaufen und habe an den unmöglichsten Stellen Fotos gemacht.
Wie zum Beispiel in den Damen-Toiletten :-)
Als Vollblut-Programmierer, der ich bin, verlasse ich ja nur relativ selten
meinen "Bunker". Was Sonnenlicht und frische Luft ist, lass ich mir von
kleinen Kindern erzählen. Und so erntete ich doch einige erstaunte Blicke
von Kollegen und Kunden, als sie mich plötzlich überall auf Händen und Knien
durch's Gebäude robben sahen.
Einige dachten sogar, ich sei Reporter, der einen Bericht über HENRY's
verfassen würde. Andere wollten unbedingt mit auf's Bild. Wiederum andere
begegneten mir eher misstrauisch und entfernten noch rasch den letzten
Steuerbescheid von ihrem Schreibtisch, bevor ich dessen Oberfläche knipste,
um daraus eine Textur zu machen.
Wie gesagt, es war ein grosser Spass :-)
Ein Fazit
"OGL_HENRYs" lässt sich relativ leicht und schnell an eigene Bedürfnisse
anpassen. Man muss sich ja nur die Textmaps vornehmen und die diversen
Zeichen so umplazieren, dass sie den eignen Räumlichkeiten entsprechen. Dann
noch ein paar Fotos gemacht, und diese - passend benamt - in die Textur-Ordner
der Räume schmeissen, schon hat man seine eigene Welt erschaffen.
Gleichzeitig birgt die "Engine" aber auch zahlreiche Mängel. So kann man
z.B. nicht ohne weiteres Objekte übereinander stellen; die Textmaps erlauben
ja immer nur ein Objekt an einem Ort. Um also etwa PCs auf Schreibtischen
zu plazieren, wie wir es in "OGL_HENRYs" sehen, musste ich tricksen: Ich lasse
die PCs quasi über einem Loch im Schreibtisch schweben, wobei der Boden des PCs
genau dort anfängt, wo der Tisch nebenan aufhört, so dass das Loch unter dem PC
nicht zu sehen ist. Betrachtet man die Szenerie von unten, sieht man jedoch sehr
schnell solche Ungereimtheiten.
Blöd ist ebenso, dass die Anzahl möglicher Objekte in einem Raum durch die
Anzahl der Zeichen des ASCII-Zeichensatzes grundsätzlich beschränkt ist. Ja,
viele ASCII-Zeichen (wie etwa BEEP, RETURN, BACKSPACE usw.) können erst gar
nicht eingesetzt werden. Und übersichtlich oder gar aussagekräftig sind all
die Zeichen aber einer gewissen Menge auch nicht sonderlich.
Schwerer noch vielleicht wiegt das stupide Aussehen der Objekte, die ja letztlich
alle nur auf simplen Quadern basieren, auch wenn dies durch die Texturen schon
recht deutlich verdeckt wird. Fussballspielen kann man bei "OGL_HENRYs" jedenfalls
bestenfalls mit einem quaderförmigen Ball.
Personen sehen sogar völlig lächerlich aus: Fliegende Quader, bei denen von allen
Seiten das gleiche Gesicht grinst. Und dazu schweben sie nur starr und dumm im Raum
herum. Es wäre allerdings kein grosses Problem, sie auch zu bewegen. Mann könnte
z.B. einen Timer auf die Form werfen, der regelmässig alle Personen in zufälliger
Weise die Position ändern lässt, inklusive einer Kollisionskontrolle, die
verhindert, dass die Jungs früher oder später ins Nirvana, d.h. in den
"unendlichen schwarzen Raum", abwandern.
Auch die Kollisionskontrolle hakt ab und an; so manches mal habe ich mich schon
durch Raumecken zwängen können, indem ich es nur hartnäckig genug versuchte.
Tja, so dünn bin selbst ich nicht, dass das auch im realen Leben klappen würde ...
Soetwas verdirbt mir jedenfalls nicht den Spass an meinem Proggy. Ist halt nichts 100%iges.
But who cares?
Eine ganze Welt in ein paar MB
"OGL_HENRYs" wurde mit Delphi 7 programmiert. Der komplette Source, die Texturen, die
Textmaps, die EXE und die nötigen OGL-DLLs sind alle in diesem ZIP-Archiv verpackt:
OGL-Henrys.zip (ca. 2,2 MB)
Das Original-OGL-Packet 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!
"OGL_HENRYs": Eine kleine virtuelle Welt im Eigenbau
|