Registriert seit: 14. Feb 2007
84 Beiträge
Turbo Delphi für Win32
|
AW: Eigenes 3D MMORPG mit Delphi möglich?
9. Dez 2011, 06:28
Danke für den regen Zuspruch!
Ich werde mal noch etwas mehr Licht ins Dunkel bringen. Immer wenn ich etwas höre/lese, was mir aufzeigt, wo andere die Grenzen sehen, fällt mir immer Recht spontan ein: Wie würde es in der Realität umgesetzt? Damit meine ich, wenn ein paar Leute sich in der echten Welt treffen und irgendwas machen, würde da auch auffallen, wenn bei einem etwas nicht mit rechten Dingen zugeht. Weil man sich gegenseitig beobachtet. Wenn nun in der Matrix die Clients sich gegenseitig ihre Daten senden und diese von dem Empfänger daraufhin geprüft wird, ob alles mit rechten Dingen zugeht, so kann erkannt werden, ob gecheatet wird. Da nur noch einzelne Cluster übertragen werden könnte der Spieler nur noch eben diesen Zerstören. Vielleicht könnte man einen Hashwert von den Clustern bilden, und wenn dieser nicht korrekt ist, wird der vom Server bzw. von den anderen Clients nicht angenommen... also so, dass nicht einfach jemand das Cluster nach seinen Gutdünken umstrickt und dann an andere verteilt. Aber das ist noch Zukunftsmusik. Ich will das überhaupt erstmal ans laufen bekommen, bevor ich mir über Cheats Gedanken mache, denn ich muss immer ein Schritt nach dem anderen gehen, alle auf einmal kann ich nicht bewältigen.
Der WebSpace Server muss eigentlich kaum was machen und auch die Packete werden sich erstaunlich gering halten. Denn was passiert? Wenn der Spieler startet, werden alle Cluster runtergeladen. Im Moment noch wirklich einzeln. D.h. jede Clusterdatei wird einzeln vom Server geladen. Später soll es so sein, dass eine PHP Seite alle benötigten Cluster lädt, und die Information auf der Webseite ausgibt... da ja auch hier nur die Cluster in Frage kommen, die auch wirklich vom Spieler erstmal direkt sichtbar sind, rechne ich mal vielleicht mit 1 MB... im Moment sind es 160 KB auf 354 Cluster verteilt. Da die Welten eher flächig gebaut werden, als dreidimensional, wächst der Speicherverbauch hier auch zum Glück nur quadratisch, statt cubisch. Außerdem ist es ja erstmal wichtig, dass die Cluster, die in unmittelbarer Nähe sind korrekt dargestellt werden, was weiter im Hintergrund liegt, kann nach und nach nachgeladen werden. Auf Festplatte sollte das ganze natürlich noch zwischengespeichert um das Laden nochmal wesentlich zu beschleunigen. Mir ist Usability und kurze Ladezeiten sehr wichtig. Beim hochladen ist noch wesentlich weniger Traffic notwendig. Man Stelle sich vor, man hat 10 Spieler... die arbeiten gerade zusammen und nach einer Minuten haben sie an insgesamt 4 Clustern gearbeitet. Nun wird nach dieser Minute an vier der Spieler die Aufgabe verteilt, das Cluster auf dem Webserver zu sichern. D.H. jeder der vier ruft dann eine PHP-Seite auf, die die Clusterdaten, 32 byte - 50 kbyte entgegen nimmt. Das sollte man von einem Webserver als machbar erwarten. Ansonsten kann man das Intervall auch größer machen. Wichtig ist, dass vor dem Hochladen der Daten, der Client das entsprechende Cluster noch einmal runterlädt und die Voxel gemerged werden nach Timestamp.
Im Moment ist es noch so, dass die Daten direkt hochgeladen werden, sobald das Programm beendet wird. Damit wirklich auch die letzten Änderungen auch zwischen einem Intervall gespeichert werden.
Ich würden noch generell gerne was zu meiner Programmierweise sagen, denn ich glaube, die unterscheidet sich schon von anderen. Ich bin absoluter Try&Error programmierer. Ich habe schon Schwierigkeiten bei verketteten AND & OR Anweisungen (Also jetzt nicht, wenn man nur eine Art davon verknüpfen muss, sondern schon bei etwas längeren vor allem verschachtelten Anweisungen). Da probiere ich lieber direkt aus, Delphi kompiliert so wahnsinnig schnell. Ein Fullbuild von StoneQuest braucht, wenn keine Hinweise drin sind, nur etwa 2 Sekunden. Ich liebe Delphi dafür!!! Außerdem programmiere ich aus dem Bauch heraus. Ich mache mir kaum Gedanken über irgendwas. Es kommt einfach immer auf mich zu. Und wenns mal nicht klappt, schmeißt dann wieder raus und probiert es auf einen anderen Weg. Jedesmal lernt man dazu. Damit bin ich schneller, als andere, die lange für sowas planen. Außerdem bin ich, was Mathe betrifft der absolute Noob. Aber meine Projekte beweisen, man kann das alles ohne große Mathekenntnisse schaffen. Manche halten das bestimmt jetzt auch wieder für verarsche, ist es aber nicht. Und ich kann das auch logisch erklären: Ich habe zwar kein Plan, was mathematisch hinter Skalarprodukten und Kreuzprodukten hat, obwohl das gewiss mal in einer der langweiligen Mathevorlesungen erklärt wurde (aber da hab ich eh nichts verstanden und wenn ich mal hingegangen bin, dann konnte ich nicht wach bleiben). Aber ich weiß, wie man diese Dinge als Werkzeuge einsetzt. Und das ist alles was zählt.
Fast alles, was ich bisher gemacht habe, und ich habe euch jetzt noch nichtmals 2% von dem gezeigt, habe ich mir selbst ausgedacht. Ich bin autodidakt. Wenn man mir ein Verfahren als Animation zeigt, dann versteht ich das meist Instant, dafür verstehe ich das in Form von Formeln oder Quellcode überhaupt nicht. Man sagte mir damals dass man Physik machen kann, indem man einfach gewährleistet, dass Punkte zueinander immer den gleichen Abstand haben. Und das die Verletintegration für sowas besser geeignet ist, wo dann Geschwindigkeit immer aus den Positionen herausextrahiert wird. Zwei Wochen später hatte ich eine Reifensimulation (in wesentlich abgespeckterer Form war das nämlich Aufgabe für Multimediaprogrammieren 2). Da ich nicht weiß, in wie weit die Texturen dafür frei waren, werde ich es mal lassen, das hier als Anhang dazu zu legen. Aber ich werde diese Physik auch sehr bald in SQ haben.
Aber nun wollte ich ja noch ein Wort dazu sagen, wiso man auch ohne Mathe mathematische Probleme lösen kann: Nehmen wir mal etwas komplexes wie Inverse Kinematik. Mathematisch wohl hoch komplex. Aber trotzdem ist jeder Mensch und ich denke auch, jedes Tier in der Lage, invers kinematisch zu hantieren. Es wird einfach gelernt. Man lernt, wenn man einen Punkt im Raum mit einer gewissen Körperstelle erreichen möchte, dieses zu tun. Wenn ich so darüber schreibe, dann fällt mir ein, dass es nicht auf unseren Körper beschränkt ist. Wenn man sich einen Stock zur Hand nimmt, kann man das auch damit schaffen. Nun was passiert eigentlich? Man versucht den Stock so zu bewegen, dass dieser näher an den Punkt kommt, den man erreichen möchte. Entfernt man sich weiter, ist die Bewegung wohl falsch, also versucht man eine andere. Nicht anders ist auch meine IK gelöst... das wird bestimmt nicht die schnellste Möglichkeit sein, dass zu berechnen, weil es hier ja auch ein Try&Error Ansatz ist, aber erstmal funktioniert ist. Optimieren kann man immer noch.
Ein weiterer Punkt, der mich beim programmieren von anderen sehr unterscheidet ist, mein unübersichtlicher zusammengequetschter Code. Ich kann nicht so breit gefächert programmieren und schon gar nicht, mit Kommentaren. Für mich ist nämlich wichtig, dass die Codestelle ein gewisses Aussehen hat. Wenn man das so breit aufstellt, dann geht mir da die Übersicht komplett verloren.
Hier mal ein kleines Codebeispiel, allerdings in C++ geschrieben. Das einzige, was ich in Delphi vermisse sind Defines... aber vielleicht auch gut, weil sonst würde mein Quellcode noch viel "dreckiger" aussehen. Bitte sagt mir nicht, dass das schlecht ist, wie ich es mache. Das mag für andere zutreffen, aber für mich ist es Ideal.
Bei dem Code handelt es sich um eine relativ komplette Vektor- und Matrix-Klasse.
Code:
#define forExt(i, t) for(int (i)=0; (i)<int(t); (i)++)
#define forExt2D(i,j,t1,t2) forExt(i, t1) forExt(j, t2)
#define forExt3D(i,j,k,t1,t2,t3) forExt2D(i,j,t1,t2) forExt(k, t3)
#define VEC1(exp) TVec3 r; forExt(i, 3) r.v[i]=exp; return r;
#define VEC2(exp) forExt(i, 3) v[i]exp; return *this;
class TVec3{
public:
float v[3];
TVec3&operator = (float s) { forExt(i, 3) v[i]=s; return *this; };
TVec3 operator - () { VEC1( -v[i] ) };
TVec3 operator - (TVec3 opp) { VEC1( v[i]-opp.v[i] ) };
TVec3 operator - (float s) { VEC1( v[i]-s ) };
TVec3 operator + (TVec3 opp) { VEC1( v[i]+opp.v[i] ) };
TVec3 operator + (float s) { VEC1( v[i]+s ) };
TVec3 operator * (float s) { VEC1( v[i]*s ) };
TVec3 operator / (float s) { DIV0( s, return operator=(0); , return operator*(1.0/s); ) };
TVec3&operator -=(TVec3 opp) { VEC2( -=opp.v[i] ) };
TVec3&operator -=(float s) { VEC2( -=s ) };
TVec3&operator +=(TVec3 opp) { VEC2( +=opp.v[i] ) };
TVec3&operator +=(float s) { VEC2( +=s ) };
TVec3&operator *=(float s) { VEC2( *=s ) };
TVec3&operator /=(float s) { DIV0( s, return *this; , VEC2( /=s; ) ) };
float&operator [](int index) { return v[index]; };
float lngSQ() { return sqr(v[0])+sqr(v[1])+sqr(v[2]); };
float lng() { return sqrt(lngSQ()); };
void writeTo( char *p ) { *(p++)=_Byte(v[0]); *(p++)=_Byte(v[1]); *p=_Byte(v[2]); } ;
};
TVec3 crt(float a, float b, float c) { TVec3 r={a,b,c}; return r; };
float dot(TVec3 v1, TVec3 v2) { float r=0; forExt(i,3) r+=v1[i]*v2[i]; return r; };
TVec3 crs(TVec3 v1, TVec3 v2) { TVec3 r; forExt(i,3) r.v[i]=v1[(i+1)%3]*v2[(i+2)%3]-v1[(i+2)%3]*v2[(i+1)%3]; return r; };
TVec3 nrm(TVec3 v) { return v / v.lng(); }
TVec3 lrp(TVec3 v1, TVec3 v2, float s) { TVec3 r; forExt(i,3)r.v[i]=v1[i]+s*(v2[i]-v1[i]); return r; };
TVec3 normal(TVec3 v1, TVec3 v2, TVec3 v3) { return crs(v2-v1, v3-v1); }
TVec3 sat(TVec3 v) { VEC1( sat(v[i]) ) };
bool same(TVec3*v1, TVec3*v2, float s) { return (*v1-*v2).lng()<s; };
TVec3 min(TVec3 v1, TVec3 v2) { VEC1( min(v1[i], v2[i]) ) };
TVec3 max(TVec3 v1, TVec3 v2) { VEC1( max(v1[i], v2[i]) ) };
TVec3 min(TVec3 v, float s) { VEC1( min(v[i],s) ) };
TVec3 max(TVec3 v, float s) { VEC1( max(v[i],s) ) };
TVec3 reflect(TVec3 i, TVec3 n) { return i-(n*dot(i,n)*2); }
TVec3 refract(TVec3 i, TVec3 n, float ind) { float w,k; w=-(dot(i,n)*ind); k=1.0+(w-ind)*(w+ind);
return k>-ZERO_TOLERANCE?i*ind+n*(w-sqrt(k)):reflect(i, n); }
TVec3 barycentric(TVec3* v1, TVec3* v2, TVec3* v3, float f, float g) { return *v1 + (*v2-*v1)*f + (*v3-*v1)*g; };
float dist_point_line(TVec3 v, TVec3 lpos, TVec3 ldir) { return crs(v-lpos,ldir).lng()/ldir.lng(); };
float projdist_point_line(TVec3 v, TVec3 lpos, TVec3 ldir) { return dot(v-lpos,ldir); };
#undef VEC1
#undef VEC2
//---------------------------------------------------------------------------
class TMatrix4x4 {
public:
float m[16];
TMatrix4x4&operator = (float s) { forExt(i, 16) m[i]=s; return *this; };
TMatrix4x4 operator * (TMatrix4x4 mat) { TMatrix4x4 r; r=0; forExt3D(i,j,k,4,4,4) r.m[i*4+j]+=m[k*4+j]*mat.m[i*4+k]; return r; };
TMatrix4x4&operator *=(TMatrix4x4 mat) { TMatrix4x4 r=*this; *this=0; forExt3D(i,j,k,4,4,4) m[i*4+j]+=r.m[k*4+j]*mat.m[i*4+k]; return *this; };
TVec3 getAxisX(){ return crt(m[0], m[4], m[8]); };
TVec3 getAxisY(){ return crt(m[1], m[5], m[9]); };
TVec3 getAxisZ(){ return crt(m[2], m[6], m[10]); };
TVec3 getPos() { return crt(m[3], m[7], m[11]); };
TVec3 transformCoord(TVec3 v) { TVec3 r=transformNormal(v); forExt(i,3)r[i]+=m[i*4+3]; return r; };
TVec3 transformNormal(TVec3 v) { TVec3 r; r=0; forExt2D(i,j,3,3)r.v[i]+=v[j]*m[i*4+j]; return r; };
};
TMatrix4x4 matScaling(TVec3 v) { TMatrix4x4 m={v[0],0,0,0,0,v[1],0,0,0,0,v[2],0,0,0,0,1}; return m; };
TMatrix4x4 matScaling(float s) { TMatrix4x4 m={s,0,0,0,0,s,0,0,0,0,s,0,0,0,0,1}; return m; };
TMatrix4x4 matRot(float, TVec3);
TMatrix4x4 matRotX(float a) { TMatrix4x4 m={1,0,0,0,0,cos(a),-sin(a),0,0,sin(a),cos(a),0,0,0,0,1}; return m; };
TMatrix4x4 matRotY(float a) { TMatrix4x4 m={cos(a),0,sin(a),0,0,1,0,0,-sin(a),0,cos(a),0,0,0,0,1}; return m; };
TMatrix4x4 matRotZ(float a) { TMatrix4x4 m={cos(a),-sin(a),0,0,sin(a),cos(a),0,0,0,0,1,0,0,0,0,1}; return m; };
TMatrix4x4 matRotZXY(TVec3 v) { return matRotZ(v[2])*matRotX(v[0])*matRotY(v[1]); };
TMatrix4x4 matIdentity() { TMatrix4x4 m={1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1}; return m; };
TMatrix4x4 matTranslation(TVec3 v) { TMatrix4x4 m={1,0,0,v[0],0,1,0,v[1],0,0,1,v[2],0,0,0,1}; return m; };
TMatrix4x4 transpose(TMatrix4x4 m) { TMatrix4x4 r; forExt2D(i,j,4,4)r.m[i*4+j]=m.m[j*4+i]; return r; };
Der komplette Code für das aktuelle StoneQuest hat etwas über 31k Zeilen. Soweit ich weiß, ist das relativ wenig Code (vor allem, wenn man sich das Projekt mal wirklich anschaut und nicht einfach nur als Terraingenerator abtut ).
Ich hoffe dass ich einen kleinen Einblick in meine Arbeitsweise geben konnte. Und auch wenn das viele vielleicht nun erschrecken sollte und die sich an den Kopf fassen, will ich ja nur eins erreichen:
Macht es so, wie ihr es für richtig haltet. Lasst euch nicht immer in eine Norm zwängen, wenn diese nicht eurer Denkweise entspricht! Letztendlich zählt das Ergebnis. Den Spieler interessiert herzlich wenig, in wie weit sauberes UML eingehalten wurde. Hauptsache es läuft hinterher auch stabil. Das tut meine Software nicht, aber eben nur am Anfang... denn auch da ist es Try&Error... dafür versuch ich auf alles zu verzichten, was das ganze zu komplex werden lässt. In dem SQ verwende ich keine eben keine Klassen (bis auf die, die Delphi für komplexere Dinge wie Streaming oder so bereit stellt). Auch Pointer sind bei mir Tabu... Pointer bringen mir früher oder später Exceptions... vielleicht bin ich nur zu blöd, damit umzugehen, wer weiß... bei mir ist alles programmtechnisch relativ einfach gelöst... z.B. wenn ich einen Fore/Back Mechanismus brauche, wo dann in den Backbuffer geschrieben wird, also als Datentyp gesehen ist es einfach ein Array[0..1] of TWasWeißIch und dann noch zwei Integer Variablen Fore = 0 und Back = 1... und dann kann man in den Index von Back schreiben und Fore benutzen... und wenn man fertig ist einfach Fore und Back swappen. Vielleicht wird sowas auch generell so gemacht, ich weiß es nicht. Aber ich könnte mir vorstellen, das einige da auch gleich mit Pointern anfangen. Wie auch immer, ist ja jedem auch überlassen wie er mag. Will ja nur erzählen, wie ich das mache.
Und noch etwas was mir einfällt, ich tendiere dazu, eher Blob-Strukturen zu schreiben. Früher hatte ich in der alten Engine (wo auch das Editor Video von ist) alles als Klassen realisiert. Also für den Modifierstack hatte ich z.B. ein Decorator Pattern verwendet. Es ist auch sehr einfach gewesen, das dann Instanztechnisch zu verbinden. Aber letztlich habe ich das Gefühl gehabt, die Komplexität über das Programm zu verteilen. Und das war nicht für mich. Mein neuer Ansatz wird sein, den Modifierstack einfach als Array zu realisieren, der angibt, was passieren soll. Eine einfache Baufunktion bekommt diese Blaupause und spuckt das fertige Objekt aus. Man hat zwar dann eine große bau Funktion, die auch zugegebenermaßen entsprechend unübersichtlich wird (aber mit Codefolding kann man das schön zusammenschrumpfen lassen), aber für mich ist das übersichtlicher, als die ganze Funktionalität auf etliche Klassen zu verteilen.
Ich versuche halt immer, die einfachste Lösung zu nehmen. Z.B. das Undo und Redo. Normalerweise würde man hergehen und dafür Klassen schreiben. Ich muss zugeben, ich kann mir garnicht genau vorstellen, wie das gehen soll. Soweit ich weiß würde das alles dann über Befehle laufen, die mit Protokolliert würden. Jeder Befehl müsste ja dann auch eine Art vorwärts und rückwärts Schritt beinhalten. Um ehrlich zu sein, dass ist mir schon wieder viel zu aufwendig... und faul bin ich auch! Und da im Editor die Objekte aus sehr kleinen Daten bestand, habe ich mir gedacht, warum nicht nach jeder Aktion die komplette Szene speichern. Und so ist es gemacht. Bei einem Undo wird einfach die Szene von davor geladen. Das ging so schnell, dass man es beim Editieren kaum merkt. Aber der Vorteil, den ich mir damit erkauft habe, war extrem. Ich hatte mit nichtmal 100 Zeilen Code ein funktionierendes Undo/Redo. Man sagt immer, man kann mit Klassen den Code einfacher warten. Ich denke, noch effektiver ist Code, der nicht existiert.
Was die Blob-Strukturen angeht, so ist es für mich deswegen einfacher, weil ich nicht so viele verschiedene Dinge benutzen muss. Auch wenn es sich seltsam anhört, aber die Namen sind für mich ausschlaggebend, ob ich etwas begreife oder nicht. Deswegen habe ich mir kurze Funktionen geschrieben, die bestimmte Aktionen machen... z.B. um bestimmte Renderstates zu setzen. Meine wohl unsauberste Struktur im Moment ist der TBuffer... in diesem werden 2D-, 3D-, Cube-Texturen, Surfaces, Vertex- und Index- und sogar Sounds abgelegt. Dabei werden die Funktionen soweit es geht zusammengezogen. Im Grunde passiert sowas ja auch in ähnlicher Form bei Klassen und Polymorphie. Allerdings verliere ich da den Überblick. Wenn ich eine Lock Funktion aufrufe und da drin einfach eine Case Anweisung ist, die je nach Typ entscheidet, was gemacht wird, so ist das für mich wesentlich angenehmer. Übrigens werden später 2D-, 3D-, 4D-Vektoren, Color, Quaternionen, Planes und Kugeln auch zu einem Typ zusammengezogen.
Noch eine Randbemerkung: Natürlich ist ein solches arbeiten im Team unmöglich. Das verstehe ich. Die meisten erwähnten Dinge funktionieren nur, weil ich schließlich auch alleine Entwickel. Ich kann auch überhaupt nicht im Team arbeiten. Ich will auch überhaupt nicht im Team arbeiten. Ich bin quasi der schrecken jedes Arbeitgebers. Deswegen muss ich eben einen anderen Weg finden. Davon mal abgesehen, will ich auch einen anderen Weg finden, denn für jemand anderen programmieren empfinde ich als Quälerei. Vor allem, wenn man die Sachen, die man für krass hält, für nen Appel und nen Ei dann anderen überlässt!
Verdammt ist das wieder viel geworden! Ich hoffe, das liest auch jemand und hoffentlich bringt es auch jemandem was...
Diesmal im Anhang, der Reifen, IK und Kleidungsphysik.
Und aus tiefster Seele kann ich nur sagen:
Leb deinen Traum
und
Ich bin nur ein großer Träumer
Zudo
Geändert von Zudomon ( 9. Dez 2011 um 07:09 Uhr)
|