Seite 1 von 2

Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 30.10.2012 21:22
von Agent
Hallo liebe PBler.

ich habe eine Frage zu Netzwerkperformance und -synchronisation.

Folgendes Szenario (hier in einem Netzwerkspiel):

Jeder Client arbeitet grundsätzlich selbstständig. Alles was der User dort macht/verändert wird an den Server übertragen mittels string (64 Byte). Der Server verteilt diese Information dann an alle Clients.

Hierbei kommt es allerdings vor, dass Informationen vom Server beim Client verloren gehen - und zwar dann - wenn er offensichtlich gerade mit etwas anderem beschäftigt ist. Da der Client wie üblich alles (also ohne Threads) abarbeitet ist dieser natürlich mit vielem anderen beschäftigt und kann nicht immer auf den Server hören.

Gibt es hierfür bessere Lösungsansätze? Wie synchronisiert ihr Clients untereinander?

Da das Problem seitens des Servers auch schon aufgetreten war (Server durchläuft Clientliste und sendet an jeden während ein anderer versucht dem Server was zu senden wobei diese Information dann unter geht) bin ich hier nur über einen Trick weiter gekommen: Der Server fügt jede Information in eine List die er dann einzeln (wenn keiner was sendet) abarbeitet. Das führt aber zu Verzögerungen.

Insgesamt muss es doch besserer Lösungen geben.

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 13:10
von Agent
Hi.

Bisher keine Posts zu meinem Problem. Hat keiner Ideen? Ist das Problem klar oder soll ich versuchen es zu präzisieren?

Es handelt sich dabei um ein Aufbau-Strategie-Spiel im Old-School.
Hier einfach nochmal der Aufbau:

Das Spiel (der Client) arbeitet eigenständig. Alle Berechnungen finden also clientseitig statt. NIcht nur die Grafik etc. Das heißt als Beispiel:
Spieler A baut eine Einheit, Procedure ADDUNIT() wird aufgerufen. Ebenso wenn er ein Gebäude baut ADDSTRUCTURE() oder ein Schuss abgibt ADDPROJECTILE() oder forscht ADDRESEARCH(). Keine Routine hat eine längere Schleife, alles wird mit LinkLists verwaltet (Units(), Structures(), Projectiles(), Researches(), Windows(), Explosions() etc).

Jede dieser Veränderungen (bauen, bewegen) wird per SendNetworkString (reduziert auf 64 bytes aus Performance-Gründen) an den Server geschickt. Dieser sendet den "Befehl" einfach an alle Clients weiter wieder per Sendnetworkstring (64 Bytes).

Jeder Client empfängt dies und ruft schlicht und ergreifend die entsprechende Procedure (ADDUNIT() etc) auf. Ich weiß das dies Zeitkritisch ist, da - falls es zu Verzögerungen kommt - zu Spieldifferenzen führen kann. Da die Berechnungen von Rohstoffen etc ja auf Clientseite erfolgen. Ich habe vor zeitweise eine Synchronisation laufen zu lassen, die Differenzen löst. Aber dazu muss das Spiel erstmal "synchron" laufen bis dato.

Ich hatte zuvor einen Lösungsansatz, der nach jeden Senden von Client nach Server der Client UNBEDINGT auf ein OK wartet. Der Server hat dann an alle Clients gesendet und ebenfalls UNBEDINGT auf OK gewartet damit er am Ende (nachallen OKs) dem eigentlichen Sender (der erste Client) das OK schicken kann damit dieser weiter machen kann. Das sichert natürlich die Zustellung der Information, führte aber zu extremen Lags da ja alles stehen bleibt solange. Zumal das Problem auftrat, wenn der SERVER auf OK wartet und ein anderer Client gerade einen Befehl schickt hängt sich alles auf, weil der SERVER ja auf OK wartet und ein ADDUNIT bekommt oder so.

Es ist alles ohne THREADS bisher gelöst, sodass die Hauptroutine des Spiels in etwa so aussieht:

Code: Alles auswählen

Repeat

 CaptureMouseInputs()

 CaptureKeyboardInputs()

 PaintPlayfield()

 GetServerMessages()

 ...

Until bMainExit = #true

Der Server macht nichts anderes als die Spieler in einer Liste verwalten und fast alles durchreichen. Lediglich ein paar wenige Befehle werden verarbeitet wie das Starten des Spiels oder das erzeugen des (zufälligen) Spielfeldes was an den Client übertragen wird.


Meine Kerfrage also:

Wie löst man diesen Datenaustausch so, dass es a) keine Lags gibt und b) kein Datenverlust

Hat da jemand Erfahrung?

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 13:35
von HeX0R
Also ohne Code schwelgen wir mal in der Theorie.
In der Theorie (und Praxis) geht da nichts verloren, egal ob du die Pakete ne Sekunde später abholst oder sofort.
Das einzige was passieren kann, ist dass der Ein- oder Ausgangspuffer voll läuft, dann gehen natürlich Informationen verloren.
Aber bei 64Byte-Paketen kann ich mir das nicht vorstellen, das müsste ja ewig dauern...

Oder benutzt du das UDP-Protokoll?

[Edit]
Ein Tipp vielleicht noch:
Überprüfe beim Senden auf jeden Fall den Rückgabewert!
Sollte der Eingangspuffer des Empfängers voll sein, wirst du eine -1 zurückbekommen.
Blöd nur, dass du auch eine -1 zurückbekommst, wenn der Server gestorben ist.
Aber naja, ich werde es nicht mehr erleben, dass die Netzwerk-Lib von PB mal vernünftig überarbeitet wird...

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 13:39
von WPö
Servus!

Tja, was soll man da raten? Die Übertragung von Information läuft nunmal nicht unendlich schnell ab. Mit einigen 100ms mußt Du schon rechnen, bis ein Datum transferiert ist - und das nur deutschlandweit. Wenn Du erwartest, weltweit halbwegs akzeptable Übertragungsgeschwindigkeiten zu erzielen, wirste ganz böse enttäuscht werden, denn etliche Länder haben gemietete Proxys in Drittstaaten dazwischengeschaltet. Ich weiß das, weil die Datenübertragung Deutschland-Paraguay desöfteren in Zeitüberschreitungen läuft. Pings brauchen bis zu 3s!

Am besten ist es wohl, alle klientenseitigen Möglichkeiten auszuschöpfen, die interne Verarbeitung zu beschleunigen. Ich fürchte, da kommste an Nebenläuferprozessen und verbesserter Signalverarbeitung innhalb des Programms nicht vorbei. Beschäftige Dich also am besten gleich mit Threads, Mutex, Semaphoren. So wird ein Schuh draus. Ob die sequentielle Abarbeitung auch der Weisheit letzter Schluß ist, bezweifle ich auch stark. Versiche es mit UDP. Die Wikipedia ist Dein Freund.

Gruß - WPö

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 14:17
von Agent
Hi.

Ich habe bereits überlegt auf Client-seite einen Thread zu starten, der permanent auf Netzwerk (Server) Infos hört und in eine To-Do-Liste schreibt. Dies dann, wenn das Hauptprogramm an der zu verarbeitenden Stelle ist eben die Liste abarbeitet.

Mutex und Co sind mir bekannt - allerdings entsprechend langsam. Dies wäre meine Momentane Lösung:

Thread der alle Netzwerkdaten einliest. solange ist dort ein Mutex für reserviert. Kommt das Hauptprogramm an die stelle wird der Thread angehalten (per stop oder mutex) und alles abgearbeitet. Danach sozusagen "zurückgeschaltet" und der Thread bekommt den Mutex. Alle To-Dos in einer Linklist (strukturiert mit ClientID, NetString$).

Das ganze basiert übrigens auf dem TCP-Protokoll.

Gerne würde ich etwas Code zur Verfügung stellen, aber ich kann wohl kaum die 5.900 Zeilen hier posten oder? :shock:


Aber wie wäre es hiermit:


Die Netzwerk-Procedures so far:

Code: Alles auswählen

Procedure.s SK_ReceiveNetworkString64(connectionID, TimeOut = 250)
  
  Protected lReceivedBytes.l, ReceivedData$
  Protected *buffer
  
  ; Speicher reserverieren
  *buffer = AllocateMemory(64)
  
  If *buffer And ConnectionID
    ; Empfangsstring leeren
    ReceivedData$ = ""
    
    ; Daten holen
    Repeat
      
      Delay(1)
      TimeOut - 1      
      
      ; auf Daten warten
      lReceivedBytes = ReceiveNetworkData(ConnectionID, *buffer, 64)      
      
      ; Haben wir was? Speichern!
      If lReceivedBytes >0
        ReceivedData$ + PeekS(*buffer, lReceivedBytes)
      EndIf
      
    Until lReceivedBytes > 0 Or TimeOut <= 0; da kommt nichts mehr or Timer abgelaufen
    
    ; Speicher wieder freigeben
    FreeMemory(*buffer)
  EndIf 
  
  ProcedureReturn RTrim(ReceivedData$)  
  
EndProcedure

Procedure SK_SendNetworkString64(ConnectionID, Text$)
  Protected sendBytes.w, TimeOut = 250, lTimer, counter, dummy$
  
  If ConnectionID
    sendBytes = SendNetworkString(ConnectionID, LSet(Text$, 64, Chr(32)))
  EndIf
  
  ProcedureReturn sendBytes
EndProcedure

Die Client-Routine zum Auswerten der weitergeleiteten Befehle:

Code: Alles auswählen


Procedure GetServerEvents()
  Protected x, y, tox, toy, playerid, ammopower, speed, ammotype, playername$, uniqueID, targetuniqueID, spriteID, dummy, angle, attackeruniqueID
  Protected DistanceX, DistanceY, attackerElementID, *targetSpriteHND, AttackerSpriteID, *buffer, *hndPackFile
  Protected lFile
  
    ; get the event
    Select NetworkClientEvent(netServerID)
    
      Case #PB_NetworkEvent_Data 
        netServerString$ = SK_ReceivenetworkString(netServerID)
        
        ; netmessage interpreter
        Select StringField(netServerString$, 1, Chr(32))
            
          Case "SET"
            Select StringField(netServerString$, 2, Chr(32))
              Case "OWNER"
                ; "SET OWNER " + Str(\uniqueID) + Chr(32) + LocalPlayerName$
                targetuniqueID = Val(StringField(netServerString$, 3, Chr(32)))
                playername$ = StringField(netServerString$, 4, Chr(32))
                
                ; Get player ID
                ForEach Players()
                  If Players()\Name$ = playername$
                    playerid = ListIndex(Players())
                    Break
                  EndIf
                Next
                
                ; Find the right Unit to change owner
                ForEach unit()
                  If unit()\uniqueID = targetuniqueID 
                    unit()\OwnerID = playerid
                    AddSystemMessage("An Agent has hacked a " + unit()\Name$)
                  EndIf 
                Next
                
                
            EndSelect            
          
            
          Case "+RH"
            ; NEWRESEARCH TypeofResearch Playername$
            playername$ = StringField(netServerString$, 2, Chr(32))
            
            If playername$ <> LocalPlayerName$
              
              dummy = Val( StringField(netServerString$, 3, Chr(32)))
              
              AddResearch(dummy, playername$)
            EndIf 
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)            
            
            
          Case "MOVE" ; OK
            ; MOVE playername uniqueID fromx fromy tox toy speed
            ;    1      2         3           4     5   6   7     8
            
            playername$ = StringField(netServerString$, 2, Chr(32))
            
            If playername$ <> LocalPlayerName$
              
              uniqueID = Val(StringField(netServerString$, 3, Chr(32)))
              x = Val(StringField(netServerString$, 4, Chr(32)))
              y = Val(StringField(netServerString$, 5, Chr(32)))
              tox = Val(StringField(netServerString$, 6, Chr(32)))
              toy = Val(StringField(netServerString$, 7, Chr(32)))
              speed = Val(StringField(netServerString$, 8, Chr(32)))
              
              ForEach Unit()
                With Unit()
                  
                  ; catch the right unit
                  If \uniqueID = uniqueID
                    
                    ; Clear VP
                    VirtualPlayfield(px2Coord(x), px2Coord(y)) = #False  
                      
                    ; Calculate Distance 
                    DistanceX = Abs(px2Coord(x) + MapPosX - px2Coord(\posX))
                    DistanceY = Abs(px2Coord(y) + MapPosY - px2Coord(\posY))
                    
                    \Energy - \EnergyToMove * (DistanceX + DistanceY)
                    
                    \MoveToX = tox
                    \MoveToY = toy
                    \OperationMode = #unit_op_moving 
                    
                    ; Play sound
                    PlayEffect(\SpriteID, #PB_Sound_Loop)
                    
                    Break
                  EndIf 
                EndWith
              Next            
            EndIf 
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)            
            
            
            
            
          Case "CHAT" ; OK
            AddSystemMessage(Right(netServerString$, Len(netServerString$) - 5))
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)            
            
            
            
          Case "WELCOME" ; OK
            AddSystemMessage(netServerString$)
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)
            
            
            
          Case "+PLAYER"   
            playername$ = StringField(netServerString$, 2, Chr(32))
            If playername$ <> LocalPlayerName$
              ; We think there is a new player
              dummy = #True
              ; check if player is already listed
              ForEach Players()                
                ; Player already here
                If Players()\Name$ = playername$
                  dummy = #False
                  Break
                EndIf 
              Next
              
              ; Do we have a new player?
              If dummy = #True
                SelectElement(Players(), hndLocalPlayerElementID)
                AddPlayer(#structure_research, playername$)
                AddSystemMessage(playername$ + " has joined the battle")    
              EndIf 
            EndIf 
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)
            
            
            
          Case "STARTUNIT"
            Debug netServerString$
            ; STARTUNIT  SpriteID, PlayerName$, x, y, uniqueID
            playername$ = StringField(netServerString$, 3, Chr(32))
            
           
              ; Get rest of information
              spriteID = Val(StringField(netServerString$, 2, Chr(32)))
              x = Val(StringField(netServerString$, 4, Chr(32)))
              y = Val(StringField(netServerString$, 5, Chr(32)))
              uniqueID = Val(StringField(netServerString$, 6, Chr(32)))
              
              ; Find the right player (for listID) and add the unit
              ForEach Players()
                
                If Players()\Name$ = playername$
                  If Players()\Name$ = LocalPlayerName$
                    AddSystemMessage("You begin with a " + UnitNames(spriteID) + " at " + px2CoordStr(x, y))
                    MapPosX = px2Coord(x) - PlayfieldViewWidth / 2
                    MapPosY = px2Coord(y) - PlayfieldViewHeight / 2
                    If MapPosX <0 : MapPosX = 0 : EndIf 
                    If MapPosY <0 : MapPosY = 0 : EndIf 
                  EndIf   
                  
                  AddUnit(spriteid, ListIndex(players()), x, y, 0, uniqueID)                  
                  Break
                EndIf 
              Next
              ; Server is waiting, sending an OK so he can continue
              SK_SendNetworkString64(netServerID, #server_ok)
              
              
              
          Case "+UT" ; OK
            ; +UNT TypeOfUnit, OwnerPlayername, PositionX.l, PositionY.l, Angle.w = 0, uniqueID.l = 0
            ;   1       2             3             4           5             6           7
            
            ; We need the playername / owner
            playername$ = StringField(netServerString$, 3, Chr(32))
            
            ; Owner should not the local player
            If playername$ <> LocalPlayerName$
              ; Get rest of information
              spriteID = Val(StringField(netServerString$, 2, Chr(32)))
              x = Val(StringField(netServerString$, 4, Chr(32)))
              y = Val(StringField(netServerString$, 5, Chr(32)))
              angle = Val(StringField(netServerString$, 6, Chr(32)))
              uniqueID = Val(StringField(netServerString$, 7, Chr(32)))
              
              ; Find the right player (for listID) and add the unit
              ForEach Players()
                If Players()\Name$ = playername$
                  AddUnit(spriteid, ListIndex(players()), x, y, angle, uniqueID)
                EndIf 
              Next
              
            EndIf
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)
            
            
            
          Case "-PLAYER" ; OK
            ; PLAYERLOST Name$
            playername$ = StringField(netServerString$, 2, Chr(32))
            AddSystemMessage(playername$ + " has quit the game")                        
            
            ForEach Players()
              If Players()\Name$ = playername$
                
                ; Delete players units
                ForEach Unit()
                  If Unit()\OwnerID = ListIndex(Players())
                    CreateSpriteExplosion(Unit()\PosX, Unit()\PosY, 200)
                    
                    ; Free coords on Virtualplayfield
                    VirtualPlayfield(px2Coord(Unit()\PosX), px2Coord(Unit()\PosY)) = #False 
                    
                    DeleteElement(unit())
                  EndIf
                Next
                
                ; Delete Players Structures
                ForEach Structures()
                  If Structures()\OwnerID = ListIndex(Players())
                    CreateSpriteExplosion(Structures()\PosX, Structures()\PosY, 400)
                    
                    ; Free coords on Virtualplayfield
                    ; VirtualPlayfield(px2Coord(Structures()\PosX), px2Coord(Structures()\PosY)) = #False                     
                    For x = 0 To px2Coord(Structures()\SpriteWidth) -1
                      For y = 0 To px2Coord(Structures()\SpriteHeight) -1
                        VirtualPlayfield(px2Coord(Structures()\PosX) + x, px2Coord(Structures()\PosY) + y) = #False 
                      Next y        
                    Next x
                    
                    DeleteElement(Structures())
                  EndIf
                Next
                
                ; Delete Players Projectiles
                ForEach Projectiles()
                  If Projectiles()\OwnerID = ListIndex(Players())
                    DeleteElement(Projectiles())
                  EndIf
                Next
                
                ; Delete Player from List
                DeleteElement(Players())
                RenewPlayerInfos()
              EndIf 
            Next 
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)
            
            
            
          Case "+SE" ; OK
            ; +STR " + Str(TypeOfUnit) + Chr(32) + Players()\Name$ + Chr(32) + Str(pxCoord(\PosX)) + Chr(32) + Str(pxCoord(\PosY)) + Chr(32) + Str(\Angle) + Chr(32) + Str(\uniqueID))
            ;       1               2                             3                       4                                       5                          6                    7
            
            ; We need the playername / owner
            playername$ = StringField(netServerString$, 3, Chr(32))
            
            ; Owner should not the local player
            If playername$ <> LocalPlayerName$
              ; Get rest of information
              spriteID = Val(StringField(netServerString$, 2, Chr(32)))
              x = Val(StringField(netServerString$, 4, Chr(32)))
              y = Val(StringField(netServerString$, 5, Chr(32)))
              angle = Val(StringField(netServerString$, 6, Chr(32)))
              uniqueID = Val(StringField(netServerString$, 7, Chr(32)))
              
              ; Find the right player (for listID) and add the unit
              ForEach Players()
                If Players()\Name$ = playername$
                  AddStructure(spriteid, ListIndex(players()), x, y, angle, uniqueID)
                EndIf 
              Next
              
            EndIf
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)       
            
            
          Case "+PE" ; #### TODO
            ; "+PJT " + Str(SpriteID) + Chr(32) + Players()\Name$ + Chr(32) + Str(\AmmoPower) + Chr(32) + Str(\AmmoSpeed) + Chr(32) + Str(\uniqueID) + Chr(32) + Str(AttackerUniqueID) + Chr(32) + Str(TargetUniqueID))
            ;       1               2                           3                             4                           5                         6                           7                               8
            playername$ = StringField(netServerString$, 3, Chr(32))
            
            Debug "NET: " + netServerString$
            
            ; should not be hisself
            If playername$ <> LocalPlayerName$
              attackerElementID = 0
              *targetSpriteHND = 0
              
              ; Get rest of information
              spriteID = Val(StringField(netServerString$, 2, Chr(32)))
              ammopower = Val(StringField(netServerString$, 4, Chr(32))) ; ammopower
              speed = Val(StringField(netServerString$, 5, Chr(32))) ; ammospeed
              uniqueID = Val(StringField(netServerString$, 6, Chr(32)))
              
              attackeruniqueID = Val(StringField(netServerString$, 7, Chr(32)))
              targetuniqueID = Val(StringField(netServerString$, 8, Chr(32)))
              
              ForEach Unit()
                ; Do we have the attacker? Save Listid
                If Unit()\uniqueID = attackeruniqueID
                  ; save ListID
                  AttackerSpriteID = unit()\SpriteID
                  ; save start-position
                  x = Unit()\PosX
                  y = Unit()\PosY
                  
                ; Do we have the target? Save handle of list
                ElseIf Unit()\uniqueID = targetuniqueID
                  ; save Handle of target
                  *targetSpriteHND = @Unit()
                  ; save end-position
                  tox = Unit()\PosX
                  toy = Unit()\PosY
                  
                EndIf                
              Next
              
              ForEach Structures()
                ; Do we have the attacker? Save Listid
                If Structures()\uniqueID = attackeruniqueID
                  ; save ListID
                  AttackerSpriteID = Structures()\SpriteID
                  ; save start-position
                  x = Structures()\PosX
                  y = Structures()\PosY
                  
                ; Do we Structures the target? Save handle of list
                ElseIf Structures()\uniqueID = targetuniqueID
                  ; save Handle of target
                  *targetSpriteHND = @Structures()
                  ; save end-position
                  tox = Structures()\PosX
                  toy = Structures()\PosY
                  
                EndIf                  
              Next              
              
              ; Do we have everything?
              If IsSprite(AttackerSpriteID) And *targetSpriteHND
                ForEach Players()
                  
                  ; Find the right owner for the projectile
                  If players()\Name$ = playername$
                    
                    AddProjectile(spriteID, ListIndex(Players()), AttackerSpriteID, *targetSpriteHND, x, y, tox, toy, ammopower, speed, uniqueID, attackeruniqueID, targetuniqueID)
                    AddSystemMessage(playername$ + " fired a " + ProjectileName(spriteID) + " from " + px2CoordStr(x, y) + " To " + px2CoordStr(tox, toy))
                    
                  EndIf 
                  
                Next
              Else
                AddSystemMessage("ERROR: Cannot start projectile. Missing Units")
              EndIf 
              
            EndIf 
            ; Server is waiting, sending an OK so he can continue
            SK_SendNetworkString64(netServerID, #server_ok)       
            
            ; +PJT SpriteID.l, OwnerPlayerID, AttackerElementID, *TargetSpriteHND, FromX, FromY, ToX, ToY, AmmoPower, AmmoSpeed, owneruniqueid, targetuniqueid
            ;   1             2               3           4                   5               6       7   8     9     10          11          12              13
            
            
            
            
          Default
            AddSystemMessage("SERVER: " + netServerString$)
            SK_SendNetworkString64(netServerID, #server_ok)
            
        EndSelect
        
        
      Case #PB_NetworkEvent_File
        
        ; Receive network file
        If ReceiveNetworkFile(netServerID, ExePath$ + LocalPlayerName$ + "_Map.pak")
          
          Debug "received networkfile"
          ; open packed file
          If OpenPack(ExePath$ + LocalPlayerName$ + "_Map.pak")
            
            Debug "map-pak open"
            ; delete old file
            DeleteFile(ExePath$ + LocalPlayerName$ + "_Map.txt")
            
            ; unpack file
            *hndPackFile = NextPackFile()
            If *hndPackFile      
              
              Debug "map unpacked"
;               *buffer = AllocateMemory(PackFileSize())
;               UnpackMemory(*hndPackFile, *buffer)
              
              ; create new map-file
              lFile = CreateFile(#PB_Any,  ExePath$ + LocalPlayerName$ + "_Map.txt")
              If lFile
                If WriteData(lFile, *hndPackFile, PackFileSize()) = PackFileSize()
                  
                  Debug "Map saved, loading"
                  CloseFile(lFile)
                  ClosePack()
                
  
                  LoadPlayfield(ExePath$ + LocalPlayerName$ + "_Map.txt")
                  CreateVirtualPlayfield()
                  
                  AddSystemMessage("New Map received and loaded. Size: " + Str(PlayfieldSizeX) + "x" + Str(PlayfieldSizeY))
                Else
                  AddSystemMessage("ERROR unpacking and saving map")
                EndIf 
              Else
                AddSystemMessage("ERROR cannot create map-file (txt)")
              EndIf             
            Else
              AddSystemMessage("ERROR no file in pack")
            EndIf 
          Else
            AddSystemMessage("ERROR creating map (pakfile)")
          EndIf 
        Else
          AddSystemMessage("ERROR receiving map (network)")
        EndIf  
      
    EndSelect
    
EndProcedure

Ein Auszug aus dem Server (hier sind die Ankommenden Daten schon in Liste organisiert):

Code: Alles auswählen


    If netSE = #PB_NetworkEvent_Connect
      netClientID = EventClient()
      
      SysLog("Client " + Str(netClientID) + " connected.")

      
      
    ;
    ; Someone send DATA?
    ;
    ElseIf netSE = #PB_NetworkEvent_Data      
      netClientID = EventClient()
      ClientString$ = SK_ReceiveNetworkString64(netClientID)
      
      
      If Left(ClientString$, 1) = "/" ; direkt server command
        
        ServerCommand$ = ClientString$
        ClientString$ = ""
        
      EndIf 
      
      
      Select StringField(ClientString$, 1, Chr(32))
          
        Case "BROADCAST"
          
          ; Server-Command?
          If Left(StringField(ClientString$, 3, Chr(32)), 1) = "/"
            ServerCommand$ = Right(ClientString$, Len(ClientString$) - FindString(ClientString$, "/"))
            SysLog("Server-Command: " + ServerCommand$)
          Else
                    
            ; BROADCAST FROM TEXT
            ForEach players()
              If AddElement(NetJob())
                Debug "adding job: " + Players()\Name$ + ": " + ClientString$
                NetJob()\clientID = Players()\NetID
                NetJob()\NetString$ = "CHAT " + Right(ClientString$, Len(ClientString$) - FindString(ClientString$, Chr(32), 1))
              EndIf 
            Next
            NetworkLog(ClientString$)          
          EndIf 
          
          
        Case "HELLO"
          netClientID = EventClient()
          dummy$ = StringField(ClientString$, 3, Chr(32))
        
          If dummy$ = #sw_version
            ; Get right Player to add Name
      
            If AddElement(Players())
              Players()\NetID = netClientID
              Players()\Name$ = StringField(ClientString$, 2, Chr(32))
            EndIf             
            
            SysLog(Str(netClientID) + ": Client-Version verificated: " + dummy$ + ". Username: " + Players()\Name$ + " (" +Str(Players()\NetID) + ")")
            
            ; Welcome the new player
            bytessend = SK_SendNetworkString64(netClientID, "WELCOME at " + ServerName$)
            SysLog("Sended " + Players()\Name$ + "(" +Str(Players()\NetID) + ") a WELCOME. " + Str(bytessend))   
            
            ; BROADCAST NEW PLAYER
            ForEach Players()
              
              ; remember entry no
              c = ListIndex(Players())
              lastPlayer$ = players()\Name$
              
              ForEach players()
                If players()\Name$ <> lastPlayer$
                  If AddElement(NetJob())
                    Debug "adding job: " + Players()\Name$ + ": " + ClientString$
                    NetJob()\clientID = Players()\NetID
                    NetJob()\NetString$ = "+PLAYER " + lastPlayer$
                    SysLog("Sending " + Players()\Name$ + " (" +Str(Players()\NetID) + "): new player " + lastPlayer$ + ". " +Str(bytessend))                
                  EndIf 
                EndIf 
              Next 
              
              ; back to the previous
              SelectElement(Players(), c)
            Next
            
          Else
            ; Wrong version, bye!
            netClientID = EventClient()
            SysLog("-ERROR- Wrong Client-Version: " + dummy$)
            SK_SendNetworkString64(netClientID, "Wrong client version! Server is " + #sw_version)
            Delay(250)
            
            ; Remove player from list
            ForEach Players()
              If Players()\NetID = netClientID
                DeleteElement(Players())
                Break
              EndIf 
            Next
            
            ; Cut him off
            CloseNetworkConnection(netClientID)
          EndIf 
          
          
          
          
          ; PLAYER needs the playfield, send it him!
        Case "PLAYFIELD"
          netClientID = EventClient()
          
          SysLog("Sending " + Str(netClientID) + " Playfield...")
          If SendNetworkFile(netClientID, ExePath$ + "Playfield.pak")
            SysLog("...sended.")
          Else
            SysLog("...NOT SEND")
          EndIf 

          
          
        Default
          If ClientString$
            ; Who send?
            netClientID = EventClient()
            ; Broadcast it
            NetworkLog("BROADCASTING: '" + ClientString$ + "'")
            Debug ClientString$
            
            ; Send unknown to everyone excluding the sender
            ForEach Players()
              If Players()\NetID <> netClientID                
                If AddElement(NetJob())
                  Debug "adding job: " + players()\Name$ + ": " + ClientString$
                  NetJob()\clientID = Players()\NetID
                  NetJob()\NetString$ = ClientString$         
                EndIf 
              EndIf 
            Next
          EndIf 

      EndSelect
      
    ;
    ; Someone DISCONNECT
    ;
    ElseIf netSE = #PB_NetworkEvent_Disconnect
      netClientID = EventClient()
      
      ; detect lost player
      ForEach Players()
        If Players()\NetID = netClientID
          LostPlayer$ = Players()\Name$
          SysLog(LostPlayer$ + " lost connection")
          DeleteElement(players())          
          Break          
        EndIf
      Next
      
      ; broadcast to the others
      ForEach  players()
        bytessend = SK_SendNetworkString64(Players()\NetID, "-PLAYER " + LostPlayer$)
        SysLog("Informed " + Players()\Name$ + "(" + Str(Players()\NetID) + ") about disconnect of " + LostPlayer$ + "(" + Str(netClientID) + ")")
      Next
      
      SysLog("Client #" + Str(netClientID) + " disconnected.")
      
    EndIf       
  EndIf 
  
  
  
  
  ;
  ; JOBLIST
  ;
  
  ForEach NetJob()
      
      ClientString$ = NetJob()\NetString$
      netClientID = NetJob()\clientID
      
      Debug "Doing Job: Client: " + Str(netClientID) + ": "+ ClientString$
      
      SK_SendNetworkString64(netClientID, ClientString$)
      
      DeleteElement(NetJob())
      Break
      
  Next
  

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 14:47
von NicTheQuick
Vielleicht helfen dir diese Artikel weiter, die ich mal so auf die schnelle ergoogelt habe. Sie sind allerdings in Englisch.
Network Game Programming und Simple online pong game network synchronization

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 15:13
von Agent
Hi Nic.

Englisch ist nicht das Problem soweit.

Im Groben ist mir klar um was es geht. Teilweise von mir auch schon umgesetzt. Insbesondere (alte Version) die Bestätigung des Servers einen Befehl erhalten zu haben. Bisher ist der Client an dieser Stelle stehen geblieben und hat gewartet was definitiv zu Asynkronität geführt hat, da andere Proceduren weiter laufen müssen.

Vor- wie Nachteil hat die clientseitige Behandlung aller Ereignisse. Der Server ist "dumm" sozusagen. Damit schließe ich Probleme aus wie in dem ersten Artikel beschrieben.
Es wird sozusagen immer nur ein Startbefehl weiter gegeben und sobald dieser ankommt ausgeführt - wie z.B. das erzeugen eines Schusses oder setzen einer Einheit.

Ich bin dazu über gegangen, jedem relevanten Listeneintrag (Unit, Structure, Projectile) eine eindeutige ID zuzuordnen. Nur so ließ sich sicherstellen, dass die richtigen Einheiten angesprochen werden auf Seite der anderen Clients.

Vielleicht sollte die Übermittlung eines "Änderungsbefehls" wie "ADDUNIT()" etc zwar angelegt aber nicht ausgeführt werden bis ein OK kommt vom Server. Dies würde aber nochmehr Befehle bedeuten.
Ich habe versucht die Befehle so kurz wie möglich zu halten und nur wirklich relevante Daten zu übermitteln. Bei einer Bewegung zum Beispiel: MOVE playername UnituniqueID fromX fromY toX toY Speed

Gut, hier könnte noch gekürzt werden, da FromX/FromY klar sein sollte (wo die Unit gerade ist). Dies bedingt, dass vorher alles Synchron ist. Speed ebenso, da die Anfangswerte gleich sind. Sollte die Einheit ein Upgrade erhalten haben (ADDRESEARCH) wurde dies ja (wenn alles glatt geht) ebenfalls ordnungsgemäß ausgeführt. Hier liese sich also noch optimieren. Aber machen die paar Bytes es dann auch aus? Letztenendes übermittle ich so oder so 64 bytes. Weniger wirds nicht.

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 18:53
von WPö
Moin!

Die Datenübertragung dauert
Anfragezeit + Datenmenge / Datentransfergeschwindigkeit
und somit ist es bei heutigen DSL-Leitungen irrelevant, ob Du 10 Bytes, 100 Bytes oder 600 Bytes überträgst. Entscheidend ist die Anfragezeit, die Du bspw. mit ping sehr schnell ermitteln kannst. Frag einfach, wenn Du nicht weißt, wie's geht. Diese Zeit liegt bei z.B. 100ms oder 300ms und gibt die Dauer an, bis die beiden Teilnehmer im Netz über weiß der Geier wie viele Zwischenstationen Kontakt aufgenommen haben.

Die obige Formel ist übrigens stark vereinfacht, weil wir noch das MTU berücksichtigen müßten, was aber bei den paar Bytes keine Geige spielt. Also kann es für Dich nicht darum gehen, die 64 Bytes Daten noch weiter einzudampfen, sondern nur noch, der internen Signalbearbeitung Beine zu machen.

Gruß - WPö

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 02.11.2012 19:49
von 7x7
Hi Agent,

dein Empfangsbuffer des Servers (64 Byte??????) ist viel zu klein. Der wird sofort vollgeschossen. Deshalb gehen deine Daten von den Clients verloren. Erhöhe den auf 65535. Den Rest habe ich mir nicht so genau angesehen.

Re: Netzwerksynchronisation zwischen Clients mittels Server

Verfasst: 19.02.2013 18:21
von Agent
Ich wollte mal eben einen neuen Stand durchgeben.

Das Problem ist im Wesentlichen gelöst indem der Server die größte Reaktionszeit (Ping) ermittelt. Sobald ein Client z.b. Ein MOVE oder BUILD oder SHOT befehl an den Server gibt, broadcastet der diesen an alle anderen inkl dem Sender mit einem "AuszuführenInMillisekunden" Zusatz, der dem der maximalen Ping-Zeit entspricht. Somit stelle ich sicher, dass alle Clients zur gleichen Zeit den Befehl ausführen. Jeder Client muss diesen Befehl an den Server bestätigen. Erfolgt dies nicht wird ein Warte-Befehl mit neuem Timer an alle für diesen Befehl geschickt.

Nachdem alles einige zeit recht gut lief kam ich beim Testen auf anderen PCs (Multiplayer) nun zu einem irreführenden Szenario im Bezug auf dem Befehl ElapsedMilliseconds()

Wie kann es sein, dass auf einem PC der Wert für ElapsedMilliseconds() negativ ist?

In Folge dessen werden in meinem Spiel die Einheiten GARNICHT gebaut, da der Timer negativ ist und niemals über den WarteTimer kommt (zb. erst in ein paar Jahren *lol)