Netzwerspiel Probleme



  • Hallo,

    da ich schon eine Chatprogramm geschrieben hatte, dachte ich mir, hey, probierste mal nen Spiel aus. Also habe ich mir das einfachste vorgenommen, was mir eingefallen ist: Pong. (2 Schläger, ein Ball, kennt ja Jeder)

    Das Spiel war in 30 Minuten fertig und ich hatte es so geplant, dass ich die sachen mit den Sockets gut einbauen konnte. Und jetzt beginnen die Probleme.

    Ich habe die Sache mit den Sockets in einen eigenen Thread gepackt. Ungefehr so:

    DWORD WINAPI ThreadFuncServer(LPVOID data)			//Hier das Objekt annehmen
    {
    
    //Initialisieren
        while(rc!=SOCKET_ERROR)
      {
    
       //In dieser Schleife immer senden und empfangen
      }
    
    //Hier alles abbauen.
    
    }
    

    Die Fehler sind nun:

    1. Das Spiel, selbst hat nur 1 % Prozessorauslastung, aber mit dem Thread 100%
    (Wie umgehe ich das, dass ich permanent abfragen muß, ob Daten angekommen
    sind?)

    2. Obwohl dieser Thread einige 1000 mal pro Sekunde durchlaufen wird, schafft
    er es komischerweise nur ca 10 Integerwerte pro Sekunde zu empfangen
    (Senden deutlich mehr) und das bei ner 100Mbit leitung. (LAN)
    Dadurch ruckelt das Spiel natürlich beim Client wie bekloppt.
    Wie umgeht man das Problem? Soll der Client sebständig "zwischenwerte
    berechnen"? Aber wie mache ich das dann bei den Schlägern, da man ja nicht
    vorraussagen kann wie der gegenspieler sie bewegt.

    3. Für jeden übermittelden Wert muss ich mir von Hand ne Codierung überlegen,
    Wie ich die Werte zuordne. z.B Übersetzte ich die Double werte der
    Ballposition in Char[x], hänge "Ballx" bzw. "bally" an und sende es dann.
    Auf der anderen Seite muß ich dies wieder auseinanderfummeln in
    double übersetzen und richtig zuordnen (anhand der Textanhänge).
    Gibt es da nen besseren Weg?

    4. Selbst wenn es schnell genug übermittelt wird ist ja ne verzögerung geben
    und der Ball wird beim Client kurzzeitig die Spielfeldgrenze
    überschreiten. Wie verhindert man sowas.

    5. Wenn es schon solche Probleme bei sowas Einfachem gibt, wie machen es dann die Programmieren von Spelen wie WoW, oder Doom3. Gibt es da irgendwelche Tricks? (Gibt es also ne Anleitung oder Tutorial, was auf diese Art von konkreten Problemen eingeht?)

    Bitte nur ernst gmeinte hilfreiche Antworten.



  • zu 1) Senden brauchst du nicht in einer Schleife, sondern nur wenn sich Werte ändern und das empfangen (recv) blockiert doch normalerweise ehh asynchron, verbraucht also keine Rechenzeit.

    zu 2) Übertrage mal per UDP

    zu 3) Ich habe immer Strukturen versendet, wobei der erste Wert immer ein int/enum war, der den Typ angibt. Ein cast auf die passende Struktur "decodiert", und verbraucht keine Laufzeit! Interpolieren musst du schon, sonst wird das bei Action-Spielen nichts.

    zu 4) Wieso sollte der Ball rausfliegen? Der Client weiss doch auch, das die Wand da ist und lässt den Ball einfach genau so abprallen.

    zu 5) Ja. Die machen das. Auf jeden Fall.

    Bye, TGGC (Fakten)


  • Mod

    Andreas XXL schrieb:

    1. Das Spiel, selbst hat nur 1 % Prozessorauslastung, aber mit dem Thread 100%
    (Wie umgehe ich das, dass ich permanent abfragen muß, ob Daten angekommen
    sind?)

    indem du events verwendest, oder hast du für den maus und key input auch jeweils nen eigenen thread?

    Andreas XXL schrieb:

    2. Obwohl dieser Thread einige 1000 mal pro Sekunde durchlaufen wird, schafft
    er es komischerweise nur ca 10 Integerwerte pro Sekunde zu empfangen
    (Senden deutlich mehr) und das bei ner 100Mbit leitung. (LAN)
    Dadurch ruckelt das Spiel natürlich beim Client wie bekloppt.
    Wie umgeht man das Problem? Soll der Client sebständig "zwischenwerte
    berechnen"?

    bei so trivialen spielen überträgt man öfters nur die inputevents der tastatur bzw maus. wenn der spieler bei einem client auf <- oder -> klickt oder die taste loslässt, überträgst du das signal, in der zwischenzeit können die programme die , wie du es so schön sagtest, "zwischenwerte berechnen" da ohne inputänderung beide das selbe berechnen sollten... btw. dabei ist es wichtig nen "timestamp" bei jeder nachricht mitzuverschicken und bei den berechnungen zu berücksichtigen.

    Andreas XXL schrieb:

    3. Für jeden übermittelden Wert muss ich mir von Hand ne Codierung überlegen,
    Wie ich die Werte zuordne. z.B Übersetzte ich die Double werte der
    Ballposition in Char[x], hänge "Ballx" bzw. "bally" an und sende es dann.
    Auf der anderen Seite muß ich dies wieder auseinanderfummeln in
    double übersetzen und richtig zuordnen (anhand der Textanhänge).
    Gibt es da nen besseren Weg?

    Man kann sich pro übertragenen wert ein byte mit der typinformation mitschicken. in strings umwandeln und zurück sollte man nicht, das ist suboptimal. einfach z.b. ein byte z.b. 1 für ballX, 2 für bally usw. und dann die daten im anhang daran.
    für meinen vorschlag den input zu versenden, würde es dann reichen grundsätzlich immer den keyboardinput weiter zu schicken.

    Andreas XXL schrieb:

    4. Selbst wenn es schnell genug übermittelt wird ist ja ne verzögerung geben
    und der Ball wird beim Client kurzzeitig die Spielfeldgrenze
    überschreiten. Wie verhindert man sowas.

    indem du auf allen clients das spiel simulierst. wie schonmal gesagt, nur den input austauschen. das spiel solltest du dabei nur auf dem server als "richtig" simulieren. das heißt, dass auch wenn ein client sieht dass der ball beim gegner "einschlägt", wenn der server es anders simuliert, müssen alle clients die simulation für sich updaten und weiter darstellen was der server als richtig ansieht.

    Andreas XXL schrieb:

    5. Wenn es schon solche Probleme bei sowas Einfachem gibt, wie machen es dann die Programmieren von Spelen wie WoW, oder Doom3. Gibt es da irgendwelche Tricks? (Gibt es also ne Anleitung oder Tutorial, was auf diese Art von konkreten Problemen eingeht?)

    klar gibt es da tricks, und jeder netzwerkprogrammierer hat da seine eigenen, manche schreiben sich dafür die ganze protokollverwaltung über IP selbst. dazu kommen geschickte extrapolations algorithmen und fehlerbereinigungs bzw tolleranz mit denen du dich noch nichtmal befassen mußtest 😉

    auf www.gamasutra.com gibt es nen schöne artikel vom netzwerkproblem von xwing vs. tieFighter.[/quote]



  • Danke,

    Ich habe immer Strukturen versendet

    aber wie versedet man den was Anderes als Text?

    Jetzt nicht lachen aber ich habe das immer so gemacht:

    senden

    float zahl=pong->get_ballpositionx();
    	char aktuell[10];sprintf(aktuell, "%f", zahl);
        Sleep(33);
    	send(s,aktuell,strlen(aktuell),0);
    

    empfangen:

    rc=recv(connectedSocket,buf,256,0);
    	buf[rc]='\0';
    	stringstream Str; 
        Str << buf; 
        double d; 
        Str >> d; 
    
    	pong->set_ballpositionx(d);
    

    Könnt ihr mal nen bischen Code hinschreiben, wie man das richtig macht?



  • struct ballInfo
    {
       int MsgType;  // muss in jeder struktur ganz oben stehen
       int x;
       int y;
    
       ballInfo(int x_, int y_) : MsgType(1) { x = x_; y = y_; };
    };
    
    ballInfo i;
    send(&i, sizeof(ballInfo));
    
    // empfangen:
    int* ptr;
    ptr = new int[getrecvsize];
    recv(ptr);
    if(ptr[0] == 1)  // ballInfo
       ballInfo* info = (ballInfo)i;
    
    /7 dnan hast du wieder dein ballInfo
    


  • Hallo, irgendwie der code von "Maxi" nicht so ganz.
    (Ich benutze MS Visual c++ 6.0)
    Ich habe mir jetzt nach langem ausprobieren folgendes zusammengebastelt:

    #include <winsock2.h>
    #include <windows.h>
    #include <stdio.h>
    
    int startWinsock(void);
    
    struct ballInfo 
    { 
       int MsgType;  // muss in jeder struktur ganz oben stehen 
       int x; 
       int y; 
    
       ballInfo(int x_, int y_) : MsgType(1) { x = x_; y = y_; }; 
    }; 
    
    ballInfo i(8,2); 
    
    //////////////SERVER/////////////////
    int main()
    {
    
      long rc;
      SOCKET acceptSocket;
      SOCKET connectedSocket;
      SOCKADDR_IN addr;
      char buf[256];
    
      // Winsock starten
      rc=startWinsock();
    
      // Socket erstellen
      acceptSocket=socket(AF_INET,SOCK_STREAM,0);
    
      // Socket binden
      memset(&addr,0,sizeof(SOCKADDR_IN));
      addr.sin_family=AF_INET;
      addr.sin_port=htons(12345);
      addr.sin_addr.s_addr=ADDR_ANY;
      rc=bind(acceptSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR_IN));
    
      // In den listen Modus
      rc=listen(acceptSocket,10);
    
      // Verbindung annehmen
      connectedSocket=accept(acceptSocket,NULL,NULL);
    
      // Daten austauschen
      while(rc!=SOCKET_ERROR)
      {
    
        rc=recv(connectedSocket,buf,256,0);
    
        buf[rc]='\0';
        printf("Client sendet: %s\n",buf);
    	rc=send(connectedSocket,(const char*)&i, sizeof(ballInfo),0); 
    
      }
      closesocket(acceptSocket);
      closesocket(connectedSocket);
      WSACleanup();
      return 0;
    }
    
    int startWinsock(void)
    {
      WSADATA wsa;
      return WSAStartup(MAKEWORD(2,0),&wsa);
    }
    
    #include <winsock2.h>
    #include <windows.h>
    #include <stdio.h>
    
    //Prototypen
    int startWinsock(void);
    
    struct ballInfo 
    { 
       int MsgType;  // muss in jeder struktur ganz oben stehen 
       int x; 
       int y; 
    
       ballInfo(int x_, int y_) : MsgType(1) { x = x_; y = y_; }; 
    
    }; 
    
    ////////////Client
    
    int main()
    {
      long rc;
      SOCKET s;
      SOCKADDR_IN addr;
      char buf[256];
    
      // Winsock starten
      rc=startWinsock();
      if(rc!=0)
      {
        printf("Fehler: startWinsock, fehler code: %d\n",rc);
        return 1;
      }
      else
      {
        printf("Winsock gestartet!\n");
      }
    
      // Socket erstellen
      s=socket(AF_INET,SOCK_STREAM,0);
      if(s==INVALID_SOCKET)
      {
        printf("Fehler: Der Socket konnte nicht erstellt werden, fehler code: %d\n",WSAGetLastError());
        return 1;
      }
      else
      {
        printf("Socket erstellt!\n");
      }
    
      // Verbinden
      memset(&addr,0,sizeof(SOCKADDR_IN)); // zuerst alles auf 0 setzten
      addr.sin_family=AF_INET;
      addr.sin_port=htons(12345); // wir verwenden mal port 12345
      addr.sin_addr.s_addr=inet_addr("127.0.0.1"); // zielrechner ist unser eigener
    
      rc=connect(s,(SOCKADDR*)&addr,sizeof(SOCKADDR));
      if(rc==SOCKET_ERROR)
      {
        printf("Fehler: connect gescheitert, fehler code: %d\n",WSAGetLastError());
        return 1;
      }
      else
      {
        printf("Verbunden mit 127.0.0.1..\n");
      }
    
      // Daten austauschen
      while(rc!=SOCKET_ERROR)
      {
    
        gets(buf);  // nur um auf eine Eingabe zu warten...
        send(s,buf,strlen(buf),0);
    
    	ballInfo i(5,2);
    	recv (s,( char*)&i,sizeof(ballInfo),0);
    
    	ballInfo info=(ballInfo)i;
    	printf( "%d\n", i.x ); 
        printf( "%d\n", i.y );
    
      }
      closesocket(s);
      WSACleanup();
      return 0;
    }
    
    int startWinsock(void)
    {
      WSADATA wsa;
      return WSAStartup(MAKEWORD(2,0),&wsa);
    }
    

    Es scheint so als ob die Werte 2 und 8 tatsächlich vom Server an den Client gesendet werden, denn dieser gibt 8,2 richtig aus.

    Jetzt habe ich noch 3 Fragen:

    1. Ist da irgend etwas (total) falsch?

    2. Bei meiner Version kann ich leider nicht genau bestimmen welches Objekt gesendet wurde, wenn es verschiedene gibt. Wie baue ich das ein?

    3. Kann es eigentlich passieren, dass man bei recv(...) nur Teile des gesendeten Objektes erhält (es also zerstückelt ist?) oder das mittlerweile mehrere im Buffer sind?

    Wenn ja wie gehe ich mit diesem dann um?



  • zu 1) Ja. Du testest nicht, ob die Daten wirklich ein ballinfo sind. Ausserdem kannst du nur Daten mit 12 Byte Länge empfangen.

    zu 2) Indem du anschaust, was bei MsgType steht. Ich habe immer eine Basisstruktur benutzt (von der ich alle Nachrichten ableite), dann kann man sicher in diese casten und Daten die immer übertragen werden sollen (z.b. Typ, Länge) dort rein schreiben.

    zu 3) Steht in der Doku zu send und recv. Musst dann halt zusammenstückeln und auseinanderfriemeln.

    Bye, TGGC (Fakten)



  • Danke TGGC.

    Zu 1. Ist dies 12 Byte so wegen dem dritten Pararmeter von

    rc=send(connectedSocket,(const char*)&i, sizeof(ballInfo),0);
    

    Was muß ich denn da hinschreiben. Denn selbst wenn ich eine Basisklasse mache und von dieser Ableite, weiß ich doch nicht genau, wie groß das Objekt ist(vorher meine ich) weil ich ja nicht weiß welches abgeleitete Objekt es im Endeffekt ist .Oder verstehe ich da etwas falsch?

    zu 2. Super Idee, mache ich jetzt auch so!!!

    Zu 3 Reicht es nachzusehen, ob der Buffer genug Daten für ein Objekt zusammen habe und erst dan recv aufrufen?
    Wie bestimme ich denn wieviel im Buffer ist ohne diese daten mit recv ausgelesen zu haben?

    Wenn im Buffer zu viel steht, kann man ja das Objekt auslesen und den Rest danach , oder. Aber auch hier habe ich das selbe Problem wie in 1. Wie kann ich den genauen Typ bestimmen, denn um das Objekt auszulesen, brauche ich ja die Länge. Diese kann ich aber erst bestimmen, wennn ich das Objekt ausgelsen habe und den Typ durch "MsgType" abgelsen habe.
    Ich verstehe nicht ganau wie man dieses macht, da jedes für das andere vorrausetzung ist. Also ohne auslesen kein Typ, ohne Laänge aus Typ gewonnen kein auslesen!?


Anmelden zum Antworten