Spiel übers Internet synchron halten



  • Ich schreibe zur Übung gerade ein kleines Spiel, soweit funktioniert mittlerweile das meiste, allerdings geht nichts mehr, sobald ein Ping ins Spiel kommt.
    Ich habe nun keine richtige Idee, wie ich dieses Problem lösen kann, da ich befürchte, dass ich dann mit dem Senden der Datenpakete aus dem Tritt komme.
    Ich hänge mal meine Spielschleife an, evt. kann mir jemand sagen, an welchen Rahmen ich mich halten muss.

    EDIT:
    Ok, die Schleife habe ich erstmal entfernt, weil es ja nur um den Grundgedanken geht: Wie muss ich vorgehen, um ein Spiel übers Internet bei beliebigem Ping möglichst synchron zu halten?
    Mein Ansatz bist jetzt (eher Pseudocode):

    double spielgeschwindigkeit = 0.02; //so lang sollte eine Runde dauern, damit alles flüssig ist
    char * data_buffer = calloc(500 , sizeof(char));
    double * data = calloc(60 , sizeof(double));
    double schleifendauer;
    while(Spiel_lauft)
    {    zeit_messen(& beginn);
         if(client)
         {    Spieleraktion_aufnehmen(& caktion);
              Paket_fuer_Server_packen(caktion, ... , data_buffer); //was muss man übergeben, um möglichst die Bandbreite zu schonen und trotzdem synchron zu bleiben?
    //Koordinaten des Spielers? Bewegungsrichtung/-geschwindigkeit?
              recv(data_socket , data_buffer , 500 , 0);
              send(data_socket , data_buffer , 500 , 0);
              Paket_des_Servers_auspacken(data_buffer , ... , data);
              Serveraktion_ausfuehren(data);
         }
         if(server)
         {    Spieleraktion_aufnehmen(& saktion);
              Paket_fuer_Client_packen(saktion, ... , data_buffer); //der Server sendet einfach alle Daten, die er hat, oder?
              send(data_socket , data_buffer , 500 , 0);
              recv(data_socket , data_buffer , 500 , 0);
              Paket_des_Clienten_auspacken(data_buffer , ... , data);
              Clientaktion_ausfuehren(data);
         }
         bild_neu_rendern(alle_moegliechen_Koordinaten);
         zeit_messen(& ende);
         //Was nun?
         schleifendauer = ende - beginn;
         SleepEx(spielgeschwindigkeit - schleifendauer , 0);
    }
    

    So sieht mein Programm schematisch aus, allerdings will mir nicht einfallen, wie ich für Synchronität sorgen kann, ohne dass das Spiel überall ruckelt.

    Es findet sich doch bestimmt jemand mit Erfahrung, oder?



  • Deine Netzwerkroutinen müssen natürlich asynchron zur Game-Loop laufen!!!

    Bis jetzt wartest du ja mit der recv-Funktion solange, bis etwas empfangen wurde (oder hast du deine Verbindung auf NON_BLOCKING gestellt?).

    Oder anders gefragt: läßt bzw. willst du dein Bild nur neu zeichnen (lassen), wenn sich etwas geändert hat?

    Und welche Render-Engine benutzt du?



  • Sobald etwas passiert, worüber das der Client (bzw. der Server) informiert werden muss, einfach so schnell wie möglich eine Nachricht hinausschicken.
    Wenn du Bandbreite sparen willst, kannst du zu sendende Nachrichten erstmal auf Halde legen und dann in festen Abständen rausschicken, das kann einiges an Overhead für TCP/IP-Pakete sparen. Trifft natürlich nur für den Server zu, beim Client dürfte das Datenaufkommen sowieso vernachlässigbar sein.



  • Das ich da was ändern muss, ist klar, allerdings habe ich eben keine Lösung dafür. Ich kenne mich den send() und recv() nur bedingt aus, ich weiß z.B. nicht, was aus einem send() wird, wenn auf der Gegenseite noch kein recv() läuft 😕.
    Aber das nur nebenbei. Momentan läuft die Verbindung im Blocked-Modus, bei einem Spiel per LAN hält sich das ganze so automatisch synchron.

    Als Render-Engine nutze ich OpenGL, eigentlich soll das Bild ständig neu gerendert werden - über den Ansatz, es nur bei Änderung neu zu ändern habe ich noch gar nicht nachgedacht. Da müsste ich am Programm wohl so einiges anpassen.

    Wo kann ich denn send() und recv() positionieren, damit ich a)die Übertragung eines Datenpaketes garantieren kann und b)das Spiel nicht unnötig bremse?

    Meine Verbindung läuft über TCP/IP, falls das von Interesse ist.

    @Nanyuki: Das soll heißen, ich sende NUR, wenn sich etwas geändert hat, oder? Das sollte bei meinem Spiel schon einige Male pro Sekunde der Fall sein.
    Ein Problem beim "Nachrichten auf Halde legen": Das bedeutet ja, dass der Client stets eine Weile in der Luft schwebt, weil er eigentlich nicht weiß, was los ist. Ich wollte das mit der Annahme korrigieren, dass eine vom Server gesendete Nachricht sich auch für alle weiteren Zyklen wiederholen würde, aber ich bin nicht sicher, ob das gut wäre. (Server bewegt sich in einem Zyklus nach oben, Senden, 5 Zyklen nichts tun; Client bewegt Server einen Zyklus nach oben und wiederholt diese Bewegung für die nächsten 5 Zyklen - das wäre ziemlich holprig)



  • Drei Möglichkeiten:

    1. du lagerst allen Netzwerkverkehr in einen eigenen Thread aus.
    2. du verwendest nonblocking sockets.
    3. du überprüfst vor recv mit select, ob überhaupt Daten da sind.

    Es gehen mit TCP keine Daten verloren, auch wenn die Gegenseite gerade nicht in einem blockierenden recv() auf deine Nachricht wartet. Die kann mit dem nächsten Aufruf von recv() abgeholt werden.

    Desweiteren empfiehlt sich, den ganzen Socketkram in eine Klasse auszulagern. Mit diesen Funktionen direkt zu arbeiten ist furchtbar.



  • Danke für die Information, ich habe mir Sorgen gemacht, dass die Daten verloren gehen würden.
    Klassenauslagerung ist momentan nicht möglich (bzw. ungewollt), da ich mich in C befinde. Aber ich denke doch, dass das auch so zu bewältigen ist.

    Zu deinen Vorschlägen:
    1. Schwebte mir auch schon vor, allerdings habe ich davon keine Ahnung.
    2. Klingt ziemlich gut, wobei ich mich frage, was aus einem recv() wird, das keine Daten empfängt?
    3. Nie davon gehört, ich google gleich mal.

    EDIT:
    So, bei MSDN steht dies hier: http://msdn.microsoft.com/en-us/library/ms740141(VS.85).aspx

    int select(
      __in     int nfds,
      __inout  fd_set *readfds,
      __inout  fd_set *writefds,
      __inout  fd_set *exceptfds,
      __in     const struct timeval *timeout
    );
    

    Worauf müsste ich da prüfen? readfds klingt vernünftig, aber warum sollte ich gerade nicht in einen Socket schreiben können? Weil von der anderen Seiten her Daten durch die Leitung schießen?

    EDIT2:
    Noch eine wichtige Frage: Werden die durch send() gesendeten Daten "gestapelt"? Das würde bei einem regelmäßig vom Server durchgeführten send() und einem seltener vom Client durchgeführten recv() ja die Synchronität völlig zerstören.



  • Zur Verwendung von select:

    bool canReceive(SOCKET* s)
    {
      fd_set set;
      FD_ZERO(&set);
      FD_SET(s,&set);
      timeval tout;
      tout.tv_sec=0;
      tout.tv_usec=0;
      select(0,&set,NULL,NULL,&tout);
      return FD_ISSET(s,&set);
    }
    

    Wenn man von einem Socket lesen, aber gerade nichts schreiben kann, dann liegt das oft daran, dass du gerade was größeres gesendet hast, das aber noch nicht rausgeschickt wurde (Bandbreite ist ja begrenzt) und die Puffer noch voll sind.

    Stiefel2000 schrieb:

    Noch eine wichtige Frage: Werden die durch send() gesendeten Daten "gestapelt"? Das würde bei einem regelmäßig vom Server durchgeführten send() und einem seltener vom Client durchgeführten recv() ja die Synchronität völlig zerstören.

    Bin mir nicht sicher, ob du das meinst, aber sobald etwas mit recv() empfangen wird, wird es aus der Warteschlange entfernt, d.h. du wirst es beim nächsten Aufruf nicht noch einmal empfangen.

    2. Klingt ziemlich gut, wobei ich mich frage, was aus einem recv() wird, das keine Daten empfängt?

    Steht mit Sicherheit ausführlich erklärt in der MSDN. Ich verwende ausschließlich select.

    Das soll heißen, ich sende NUR, wenn sich etwas geändert hat, oder? Das sollte bei meinem Spiel schon einige Male pro Sekunde der Fall sein.

    Ja, alles ändere wäre ja Ressourcenverschwendung. Bei nur ein paar Mal pro Sekunde fangen noch lange keine Probleme an, schließlich dürften die meisten Nachrichten nur wenige Bytes lang sein.

    Ein Problem beim "Nachrichten auf Halde legen": Das bedeutet ja, dass der Client stets eine Weile in der Luft schwebt, weil er eigentlich nicht weiß, was los ist.

    Das ist richtig, es erhöht natürlich die durchschnittliche Latenz. Wenn überhaupt, sind da Abstände über 100 ms auch nicht wirklich sinnvoll. Wenn wenige Clients vorhanden sind oder eine gute Verbindung vorhanden ist, sollte das ganz entfallen.

    Ich wollte das mit der Annahme korrigieren, dass eine vom Server gesendete Nachricht sich auch für alle weiteren Zyklen wiederholen würde, aber ich bin nicht sicher, ob das gut wäre. (Server bewegt sich in einem Zyklus nach oben, Senden, 5 Zyklen nichts tun; Client bewegt Server einen Zyklus nach oben und wiederholt diese Bewegung für die nächsten 5 Zyklen - das wäre ziemlich holprig)

    Ja, wenn mehrere Bewegungsnachrichten (vom selben Objekt stammend) innerhalb eines Sendeintervalls anfallen, wird's holprig. Deswegen: weglassen und einfach ohne Verzögerung senden, wenn möglich.



  • Erstmal vielen Dank für die Hilfe, ich werde dann ein paar Dinge ausprobieren.

    Nanyuki schrieb:

    Stiefel2000 schrieb:

    Noch eine wichtige Frage: Werden die durch send() gesendeten Daten "gestapelt"? Das würde bei einem regelmäßig vom Server durchgeführten send() und einem seltener vom Client durchgeführten recv() ja die Synchronität völlig zerstören.

    Bin mir nicht sicher, ob du das meinst, aber sobald etwas mit recv() empfangen wird, wird es aus der Warteschlange entfernt, d.h. du wirst es beim nächsten Aufruf nicht noch einmal empfangen.

    Ich meinte das so:
    Der Server sendet ein Paket. Dann sendet er noch ein Paket. Und noch eins. Erst jetzt stellt der Client aber auf recv(), weil er vorher anderes zu tun hatte. Welches Paket kommt jetzt an? Das erste, das letzte oder alle auf einmal?



  • Eines ist mir noch an deinem Code aufgefallen:

    recv(data_socket , data_buffer , 500 , 0);
    send(data_socket , data_buffer , 500 , 0);
    

    Dies macht überhaupt keinen Sinn, daß du die empfangenen Daten gleich wieder zurückschickst! 😕

    Jeder Spieler (egal ob Client oder Server) muß seine eigenen Aktionen verschicken und die des anderen Spielers empfangen.



  • Sozusagen kommen dann alle auf einmal an, aber in der richtigen Reihenfolge, d.h. zweites Paket fängt direkt nach dem ersten an. Du musst die Nachrichten so aufbauen, dass du weißt, wo die erste aufhört und die nächste anfängt.



  • Ich empfehle Dir asynchrone Sockets, da wird sich sozusagen intern um die parallele Verarbeitung gekümmert.

    Hier zwei Links, schau Dir die mal an:

    http://johnnie.jerrata.com/winsocktutorial/
    http://www.gamedev.net/reference/articles/article1297.asp

    Viel Erfolg noch!



  • Th69 schrieb:

    Eines ist mir noch an deinem Code aufgefallen:

    recv(data_socket , data_buffer , 500 , 0);
    send(data_socket , data_buffer , 500 , 0);
    

    Dies macht überhaupt keinen Sinn, daß du die empfangenen Daten gleich wieder zurückschickst!

    Ja, mir fiel vorhin schon auf, dass das wenig Sinn macht. In meinem Programm hatte ich verschiedene Buffer dafür, diese direkte Aneinanderreihung habe ich gemacht, um die Block-Funktion auszunutzen. Ich dachte, der Fehler fällt vielleicht niemandem auf, es handelt sich ja noch immer nur um "Pseudocode" ;).

    Nanyuki schrieb:

    Sozusagen kommen dann alle auf einmal an, aber in der richtigen Reihenfolge, d.h. zweites Paket fängt direkt nach dem ersten an. Du musst die Nachrichten so aufbauen, dass du weißt, wo die erste aufhört und die nächste anfängt.

    Ok, das bringt mich zur nächsten Frage (schonmal danke für die erste Klärung).

    //Server
    send(s , char_buffer , 10 , 0);
    _char_buffer_manipulieren(char_buffer);
    send(s , char_buffer , 10 , 0);
    _char_buffer_manipulieren(char_buffer);
    send(s , char_buffer , 10 , 0);
    _char_buffer_manipulieren(char_buffer);
    send(s , char_buffer , 10 , 0);
    
    //soweit, so gut
    //Client
    recv(s , neuer_char_buffer , 10 , 0); //Auf die Nachrichtenlänge achten
    

    Was macht der Client hier? Holt er sich genau die erstern 10 Byte aus dem "Verbindungs-Puffer" und kann sich den Rest später holen? Oder "verfällt" der Rest? Denn in diesem Moment müsste ich ja zwecks Synchronität eher das letzte als das erste Datenpaket annehmen und verarbeiten. Vermutlich könnte man das umgehen:

    //Client
    memset(neuer_char_buffer , 127 , 10001);
    recv(s , neuer_char_buffer , 10001 , 0);
    for(i = 0 ; neuer_char_buffer[i] != 127 ; i ++);
    //wichtige Nachricht beginnt jetzt an neuer_char_buffer[i - 10]
    

    Aber vielleicht kann man das ja umgehen.

    iop schrieb:

    Ich empfehle Dir asynchrone Sockets, da wird sich sozusagen intern um die parallele Verarbeitung gekümmert.

    Hier zwei Links, schau Dir die mal an:

    http://johnnie.jerrata.com/winsocktutorial/
    http://www.gamedev.net/reference/articles/article1297.asp

    Viel Erfolg noch!

    Vielen Dank für die Links, bin schon am Lesen.

    Es kommt mir so vor, als würde ich von der einen Baustelle in die nächste stolpern...



  • Was macht der Client hier? Holt er sich genau die erstern 10 Byte aus dem "Verbindungs-Puffer" und kann sich den Rest später holen? Oder "verfällt" der Rest? Denn in diesem Moment müsste ich ja zwecks Synchronität eher das letzte als das erste Datenpaket annehmen und verarbeiten.

    Der Rest verfällt nicht, sondern wartet, bis du ihn irgendwann mal abholst.
    Warum das letzte zuerst? Die Nachrichten sollten ausnahmslos in der Reihenfolge abgearbeitet werden, in der sie gesendet wurden, sonst bringt das nur Probleme. Wenn du eine andere Reihenfolge benötigst, dann sende sie eben in dieser.
    Anstatt deine Nachrichten mit einem bestimmten Zeichen abzuschließen, bietet es sich vielleicht eher an, am Anfang jeder Nachricht deren Größe mit 1-4 Bytes anzugeben, sonst kannst du nicht alle Bytewerte in deinen Nachrichten verwenden, was gleichzeitig ausschließt, dass du deine Nachrichten binär kodieren kannst, was wiederum bedeuten würde, dass du jede Menge Bandbreite und CPU-Zeit aus dem Fenster werfen musst.



  • Ich habe jetzt eine ganze Weile mit select() und WsaAsyncselect() herumprobiert. Ersteres funktionierte zwar, allerdings war noch immer alles etwas ruckelig. Bei letzterem scheine ich entweder nicht an die Nachricht zu kommen oder es meldet mir nicht, wann ein Socket Zeit zur Arbeit hat.
    Hat vielleicht jemand ein Code-Beispiel für den Umgang mit der Nachricht? MSDN und die von iop geposteten Links habe ich schon hoch- und runtergewälzt, aber irgendwie hat es nicht geholfen.



  • Was funktioniert denn genau nicht? Welche Fehlermeldung bekommst Du? Fragst Du generell alle Fehler ab?
    Zeig doch mal Deinen Code, also das Aufsetzen der Sockets, die Windows Nachrichtenschleife und das Senden und Empfangen.



  • EDIT: Ich habe den Beitrag mal überarbeitet, da ich mittlerweile scheinbar die richtigen Nachrichten Abfange. Den Quelltext lasse ich hier, da sicher noch ein paar Dinge zu klären sein werden:

    WNDCLASS wc;
    	HWND hWnd;
    	HDC hDC;
    	HGLRC hRC;
    	MSG msg;
    	SOCKET data_socket;
    	SOCKET accept_socket;
    	WSADATA wsa;
            //...
    	//Netzwerk-Initialisierung
    	if(server == 0)						//Client
    	{	SOCKADDR_IN saddr;
    		if(WSAStartup(MAKEWORD(2 , 0) , & wsa))
    			quit = 1;
    		data_socket = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);
    		if(data_socket==INVALID_SOCKET)
    			quit = 1;
    		saddr.sin_family = AF_INET;
    		saddr.sin_port = htons(port);
    		saddr.sin_addr.s_addr = inet_addr(server_ip);
    		saddr.sin_zero[0] = '\0';
    		connect(data_socket , (SOCKADDR*)&saddr , sizeof(SOCKADDR));
    		if(WSAAsyncSelect(data_socket , hWnd , SOCKET_EVENT , FD_READ | FD_WRITE))
    			quit = 1;
    	}
    	if(server == 1)						//Server
    	{	SOCKADDR_IN saddr;
    		if(WSAStartup(MAKEWORD(2 , 0) , & wsa))
    			quit = 1;
    		accept_socket = socket(AF_INET , SOCK_STREAM , IPPROTO_TCP);
    		if(accept_socket==INVALID_SOCKET)
    			quit = 1;
    		saddr.sin_family = AF_INET;
    		saddr.sin_port = htons(port);
    		saddr.sin_addr.s_addr = ADDR_ANY;
    		saddr.sin_zero[0] = '\0';
    		bind(accept_socket , (SOCKADDR*)&saddr , sizeof(SOCKADDR_IN));
    		listen(accept_socket , 2);
    		data_socket = accept(accept_socket , NULL , NULL);
    		if(data_socket==INVALID_SOCKET)
    			quit = 1;
    		if(WSAAsyncSelect(data_socket , hWnd , SOCKET_EVENT , FD_READ))
    			quit = 1;
    	}
    

    Soweit zu den Sockets, evt. fehlt da jetzt die Erstellung einiger Variablen, aber das sollte wohl keine Rolle spielen.

    Hier Teile meiner Programmschleife:

    while (!quit)
    	{	round_beg = _win_exact_time_64();
    		if (PeekMessage(& msg , NULL , 0 , 0 , PM_REMOVE))
    		{	if(msg.message == WM_QUIT)
    				quit = 1;
    			if(msg.message == SOCKET_EVENT)  //Hier meine Abfrage über die Lesbarkeit des Sockets
    				data_received = 1;
    			else
    			{	TranslateMessage(& msg);
    				DispatchMessage(& msg);
    			}
    		}
    		if(server == 0)
    		{	player_action(player2x , player2y , & ctrl_buf_own , 0 , 0);
    			if(data_received && t_buffer_send_recv + 0.05 < _win_exact_time_64())
    			{	printf("Client in action\n");
    				cp.p.player2x = player2x[0]; cp.p.player2y = player2y[0]; cp.p.ctrl_buffer = ctrl_buf_own;
    				t_buffer_send_recv = _win_exact_time_64();
    				send(data_socket , cp.text , sizeof(struct cdata) , 0);
    				recv(data_socket , sp.text , sizeof(struct sdata) , 0);
    				ctrl_buf_other = 0;
    				data_received = 0;
    				_extr_server_package(ballx , bally , ballv , player1x , player1y , player2x , player2y , & ctrl_buf_other);
    				_calculate_pos(player1x , player1y , player2x , player2y , ballx , bally);
    				traffic += sizeof(struct sdata) + sizeof(struct cdata);
    			}
    			else
    			{	if(!ball_redrawn)
    					ball_repositioning(ballx , bally , ballv , player1x , player1y , player2x , player2y);
    				player_action(player1x , player1y , & ctrl_buf_other , 1 , 1);
    			}
    			_control_sound();
    			_correct_points(pointsP1_i , pointsP2_i , pointsP1 , pointsP2);
    			redraw_scenario(player1x , player1y , player2x , player2y , ballx , bally , ballv , 0 , hDC);
    		}
    

    Dieser Teil enthält das Senden und Empfangen (es wird auf die Variable "data_received" geprüft.
    Ich muss erstmal ein wenig weitertesten, bevor neue Fragen kommen ;). Vielen Dank auf jeden Fall für die bisher geleistete Hilfe :).



  • Ich habe jetzt noch einige Zeit investiert und das Spiel scheint mehr oder minder gut über das Internet zu laufen (ich sende aktuell alle 30ms ein Datenpaket vom Server zum Clienten und umgekehrt).
    Realisiert habe ich das ganze mit einer Kombination aus select() und WSAAsyncSelect(), damit bin ich ganz zufrieden.

    Nun mal ein etwas neueres Problem, vielleicht kann man dagegen etwas tun:
    Momentan läuft das Spiel im Prinzip komplett auf dem Server, dieser sendet die Spieldaten an den Clienten, der "entscheidet" sich für eine Reaktion und sendet diese zurück. Daraufhin baut der Server die Clientenaktion in seinen Ablauf ein.
    Das Problem an dieser Vorgehensweise ist, dass mir effektiv der doppelte Ping im Weg steht (sowie evt. ein Aufschlag wegen meiner Pakete, die nur alle 30ms gesendet werden).
    Gibt es hierfür ein effizienteres Verfahren? Das Spiel ist relativ zeitkritisch und eine Verzögerung von 100ms (die aktuell ja durchaus entstehen kann) ist deutlich zu merken.




Anmelden zum Antworten