Seite 1 von 1

Network Pong 2 mit Sync (spielbar, aber Sync noch unvollst.)

Verfasst: 19.01.2008 01:56
von ZeHa
Servus,

ich hatte mal vor Ewigkeiten ein Network Pong bereitgestellt, das war allerdings scheiße (waren sozusagen meine ersten Netzwerk-Erfahrungen). Nun hab ich eine neue Version, aber ich komm nicht dazu, weiterzumachen, und Milchshake hatte mich gefragt, ob ich den Code posten kann, weil er gerade auch mit Synchronisation zu kämpfen hat. Daher werd ich den jetzt einfach hier mal veröffentlichen, vielleicht hat jemand Lust und gute Ideen, die Synchronisation zu perfektionieren.

Es wurde übrigens auch noch nicht grad ausgiebig getestet (weil man ja immer einen zweiten Part braucht (localhost ist nicht allzu sinnvoll, weil man da ja keine Latenz hat)).

Hier erstmal noch eine Erklärung zum Code:
Das ganze läuft nach dem "Dead Reckoning"-Prinzip, d.h. der jeweils andere Rechner schaut in die Zukunft, um zu wissen, wo sich der andere Spieler gerade befindet. Das funktioniert so:
  • In jedem Schleifendurchlauf wird die aktuelle Zeit in Millisekunden ermittelt
  • Wenn die Positionen benötigt werden, wird anhand der zuletzt "gewußten" Position, der Bewegungsrichtung und der Zeit die momentane Position errechnet
  • Wenn eine Richtungsänderung (oder Stillstand) auftritt, dann wird dies dem anderen Rechner mitgeteilt
  • Server und Client machen am Anfang einen kleinen Verbindungstest und sorgen dafür, daß die "Uhren" sozusagen auf die gleiche Zeit gestellt werden
Was jetzt im Grunde noch fehlt:
  • Die Anfangs-Synchronisation macht nur einen einzigen Test. Das ist nicht grad realistisch, eigentlich sollten mehrere Tests gemacht werden und ein Durchschnittswert für die Latenz errechnet werden. Aber der Code an der Stelle ist eh noch sehr dürftig, daher hab ich da noch nix dran geändert
  • Natürlich kommt bei einer Richtungsänderung die Information beim anderen Rechner zu spät an, in dieser Zeit ist die Anzeige auf beiden Bildern natürlich nicht gleich (geht auch gar nicht). Optimal wäre es, wenn die Rechner dann interpolieren würden, damit wenigstens die Bewegungen einigermaßen flüssig aussehen und nicht so stark ruckeln (z.B. wenn der Schläger stillsteht und er sich dann plötzlich losbewegt, dann ist der Schläger beim anderen Rechner plötzlich schon ein ganzes Stück verschoben, das könnte man wenigstens "smoothen")
  • Letzendlich sollte es noch so sein, daß, wenn der Ball links rauszugehen droht, auch der entsprechende Rechner das überprüft (da er die aktuelleren Daten hat), und analog dazu natürlich der andere Rechner wenn der Ball rechts rausgeht. Somit ist das ganze am fairsten, aber das ist (soweit ich mich erinnern kann) noch nicht so implementiert. Problem ist natürlich, daß somit leichter gecheatet werden kann, aber das muß meiner Meinung nach jetzt nicht großartig beachtet werden.
Zu guter Letzt noch: Der Code ist hauptsächlich zum Testen entstanden. Daher entschuldigt, wenn er nicht ganz perfekt ist und wenn er sehr schlecht kommentiert ist (eigentlich ist nur die Anfangs-Synchronisation dokumentiert, weil ich sonst selbst nicht mehr durchgeblickt hätte). Ach ja, ich benutze PB 4.


So, nun der Code:

Code: Alles auswählen

;  ******************************
; ***                          ***
; ***      NETWORK PONG 2      ***
; ***                          ***
; ***       2007 by ZeHa       ***
; ***  http://www.dr-wuro.com  ***
; ***                          ***
;  ******************************
;
; License:
; Do what you want, just keep original
; credits. Maybe you can complete some
; stuff, here's the forum link with
; further explanations (German):
; http://www.purebasic.fr/german/viewtopic.php?p=184141

InitSprite()
InitKeyboard()
InitNetwork()


#SCREEN_WIDTH  = 320
#SCREEN_HEIGHT = 240


#UPPER_BORDER = 0.08 * #SCREEN_HEIGHT
#LOWER_BORDER = 0.92 * #SCREEN_HEIGHT

#PLAYER_HEIGHT = 0.10 * #SCREEN_HEIGHT
#PLAYER_WIDTH  = 0.25 * #PLAYER_HEIGHT

#BALL_WIDTH = #PLAYER_WIDTH
#BALL_HEIGHT = #BALL_WIDTH

#FOREGROUND_COLOR = $00DD00
#BACKGROUND_COLOR = $663300

#PLAYER_SPEED = 0.001 * #SCREEN_HEIGHT
#BALL_SPEED   = 0.0004 * #SCREEN_HEIGHT


Enumeration
  #PLAYER
  #BALL
EndEnumeration




Structure Object
  type.l
  
  lastXPos.l
  lastYPos.l
  
  XDirection.f
  YDirection.f
  
  lastTime.l
  
  speed.f
EndStructure


Structure Player Extends Object
  score.l
EndStructure


Structure Ball Extends Object
  ; nothing more
EndStructure




Global *player1.Player
Global *player2.Player
Global *myself.Player
Global *opponent.Player

Global *ball.Ball

Global server.l
Global serverName$
Global port
Global connection
Global send
Global sendball
Global *networkBuffer.Object

Global firstTime.l
Global timediff.l
Global serverDiff.l

Global windowed

*networkBuffer = AllocateMemory(SizeOf(Object))



Procedure exit()
  CloseScreen()
  
  If windowed
    CloseWindow(0)
  EndIf
  
  WritePreferenceString("server", serverName$)
  WritePreferenceString("port", Str(port))
  ClosePreferences()
  
  End
EndProcedure


Procedure.l newPlayer(x, y)
  *new.Player = AllocateMemory(SizeOf(Player))
  
  *new\lastXPos = x
  *new\lastYPos = y
  *new\XDirection = 0
  *new\YDirection = 0
  
  *new\speed = #PLAYER_SPEED
  *new\type = #PLAYER
  
  ProcedureReturn *new
EndProcedure


Procedure.l newBall(x, y)
  *new.Ball = AllocateMemory(SizeOf(Ball))
  
  *new\lastXPos = x
  *new\lastYPos = y
  *new\XDirection = 0
  *new\YDirection = 0
  
  *new\speed = #BALL_SPEED
  *new\type = #BALL
  
  ProcedureReturn *new
EndProcedure


Procedure getXPos(*o.Object, time.l)
  ProcedureReturn (*o\lastXPos + *o\XDirection * (time - *o\lastTime) * (*o\speed))
EndProcedure

Procedure getYPos(*o.Object, time.l)
  ProcedureReturn (*o\lastYPos + *o\YDirection * (time - *o\lastTime) * (*o\speed))
EndProcedure

Procedure setXPos(*o.Object, time.l, x.l)
  *o\lastXPos = x
  *o\lastTime = time
EndProcedure

Procedure setYPos(*o.Object, time.l, y.l)
  *o\lastYPos = y
  *o\lastTime = time
EndProcedure


Procedure renderPlayer(*p.Player, time.l)
  Box(getXPos(*p, time), getYPos(*p, time), #PLAYER_WIDTH, #PLAYER_HEIGHT, #FOREGROUND_COLOR)
EndProcedure


Procedure renderBall(*b.Ball, time.l)
  Box(getXPos(*b, time), getYPos(*b, time), #BALL_WIDTH, #BALL_HEIGHT, #FOREGROUND_COLOR)
EndProcedure


Procedure moveUp(*o.Object, time.l)
  If (*o\YDirection = -1)
    ProcedureReturn
  EndIf
  
  *o\lastXPos = getXPos(*o, time)
  *o\lastYPos = getYPos(*o, time)
  *o\lastTime = time
  *o\YDirection = -1
EndProcedure


Procedure moveDown(*o.Object, time.l)
  If (*o\YDirection = 1)
    ProcedureReturn
  EndIf
  
  *o\lastXPos = getXPos(*o, time)
  *o\lastYPos = getYPos(*o, time)
  *o\lastTime = time
  *o\YDirection = 1
EndProcedure


Procedure moveLeft(*o.Object, time.l)
  If (*o\XDirection = -1)
    ProcedureReturn
  EndIf
  
  *o\lastXPos = getXPos(*o, time)
  *o\lastYPos = getYPos(*o, time)
  *o\lastTime = time
  *o\XDirection = -1
EndProcedure


Procedure moveRight(*o.Object, time.l)
  If (*o\XDirection = 1)
    ProcedureReturn
  EndIf
  
  *o\lastXPos = getXPos(*o, time)
  *o\lastYPos = getYPos(*o, time)
  *o\lastTime = time
  *o\XDirection = 1
EndProcedure


Procedure stopMoving(*o.Object, time.l)
  If (*o\XDirection = 0 And *o\YDirection = 0)
    ProcedureReturn
  EndIf
  
  *o\lastXPos = getXPos(*o, time)
  *o\lastYPos = getYPos(*o, time)
  *o\lastTime = time
  *o\XDirection = 0
  *o\YDirection = 0
EndProcedure


Procedure render(time.l)
  ClearScreen(#BACKGROUND_COLOR)
  
  StartDrawing(ScreenOutput())
  
  Line(0, #UPPER_BORDER,    #SCREEN_WIDTH, 0, #FOREGROUND_COLOR)
  Line(0, #UPPER_BORDER -4, #SCREEN_WIDTH, 0, #FOREGROUND_COLOR)
  
  Line(0, #LOWER_BORDER,    #SCREEN_WIDTH, 0, #FOREGROUND_COLOR)
  Line(0, #LOWER_BORDER +4, #SCREEN_WIDTH, 0, #FOREGROUND_COLOR)
  
  renderPlayer(*player1, time)
  renderPlayer(*player2, time)
  renderBall(*ball, time)
  
  If (0 And send)
    DrawText(5, 5, "sending network data", #FOREGROUND_COLOR, #BACKGROUND_COLOR)
  EndIf
  
  StopDrawing()
  
  FlipBuffers()
EndProcedure


Procedure controls(time.l)
  ExamineKeyboard()
  
  If (KeyboardReleased(#PB_Key_Up) Or KeyboardReleased(#PB_Key_Down))
    stopMoving(*myself, time)
    send = #True
  EndIf
  
  If (KeyboardPushed(#PB_Key_Up))
    moveUp(*myself, time)
    send = #True
  EndIf
  
  If (KeyboardPushed(#PB_Key_Down))
    moveDown(*myself, time)
    send = #True
  EndIf
EndProcedure


Procedure collision(*p.Player, time.l)
  If (getYPos(*p, time) < #UPPER_BORDER)
    setYPos(*p, time, #UPPER_BORDER)
    stopMoving(*p, time)
    
    If (*p = *myself)
      send = #True
    EndIf
  EndIf
  
  If (getYPos(*p, time) > #LOWER_BORDER - #PLAYER_HEIGHT)
    setYPos(*p, time, #LOWER_BORDER - #PLAYER_HEIGHT)
    stopMoving(*p, time)
    
    If (*p = *myself)
      send = #True
    EndIf
  EndIf
EndProcedure


Procedure network()
  If (server)
    event = NetworkServerEvent()
    
    If (event = #PB_NetworkEvent_Data)
      ReceiveNetworkData(connection, *networkBuffer, SizeOf(Object))
      
      If (*networkBuffer\type = #PLAYER)
        CopyMemory(*networkBuffer, *player2, SizeOf(Object))
      EndIf
    EndIf
    
    If (sendball)
      SendNetworkData(connection, *ball, SizeOf(Object))
      sendball = #False
    EndIf
  Else
    event = NetworkClientEvent(connection)
    
    If (event = #PB_NetworkEvent_Data)
      ReceiveNetworkData(connection, *networkBuffer, SizeOf(Object))
      
      If (*networkBuffer\type = #BALL)
        CopyMemory(*networkBuffer, *ball, SizeOf(Object))
      ElseIf (*networkBuffer\type = #PLAYER)
        CopyMemory(*networkBuffer, *player1, SizeOf(Object))
      EndIf
    EndIf
  EndIf
  
  
  If (send)
    SendNetworkData(connection, *myself, SizeOf(Object))
    send = #False
  EndIf
  
  Delay(0)
EndProcedure


Procedure ball(*ball.Ball, time.l)
  reset = #False
  
  If (getXPos(*ball, time) < 0)
    *player2\score +1
    reset = #True
  EndIf
  
  If (getXPos(*ball, time) > #SCREEN_WIDTH)
    *player1\score +1
    reset = #True
  EndIf
  
  If (getXPos(*ball, time) >= getXPos(*player1, time) + #PLAYER_WIDTH - #BALL_WIDTH/4)
    If (getXPos(*ball, time) <= getXPos(*player1, time) + #PLAYER_WIDTH)
      If (getYPos(*ball, time) >= getYPos(*player1, time) - #BALL_HEIGHT)
        If (getYPos(*ball, time) <= getYPos(*player1, time) + #PLAYER_HEIGHT)
          moveRight(*ball, time)
          sendball = #True
        EndIf
      EndIf
    EndIf
  EndIf
  
  If (getXPos(*ball, time) >= getXPos(*player2, time) - #BALL_WIDTH - #BALL_WIDTH / 4)
    If (getXPos(*ball, time) <= getXPos(*player2, time) - #BALL_WIDTH)
      If (getYPos(*ball, time) >= getYPos(*player2, time) - #BALL_HEIGHT)
        If (getYPos(*ball, time) <= getYPos(*player2, time) + #PLAYER_HEIGHT)
          moveLeft(*ball, time)
          sendball = #True
        EndIf
      EndIf
    EndIf
  EndIf
  
  If (getYPos(*ball, time) >= #LOWER_BORDER - #BALL_HEIGHT)
    moveUp(*ball, time)
    
    If (*ball\XDirection = 0)
      If (Int(Random(1)))
        moveLeft(*ball, time)
      Else
        moveRight(*ball, time)
      EndIf
    EndIf
    
    sendball = #True
  EndIf
  
  If (getYPos(*ball, time) <= #UPPER_BORDER)
    moveDown(*ball, time)
    sendball = #True
  EndIf
  
  If (reset)
    setXPos(*ball, time, #SCREEN_WIDTH/2 - #BALL_WIDTH/2)
    setYPos(*ball, time, #SCREEN_HEIGHT/2 - #BALL_HEIGHT/2)
    stopMoving(*ball, time)
    moveDown(*ball, time)
    sendball = #True
  EndIf
EndProcedure


Procedure initGame()
  *player1 = newPlayer(#SCREEN_WIDTH/16, #SCREEN_HEIGHT/2 - #PLAYER_HEIGHT/2)
  *player2 = newPlayer(#SCREEN_WIDTH - #SCREEN_WIDTH/16 - #PLAYER_WIDTH, #SCREEN_HEIGHT/2 - #PLAYER_HEIGHT/2)
  
  If (server)
    *myself   = *player1
    *opponent = *player2
  Else
    *myself   = *player2
    *opponent = *player1
  EndIf
  
  *ball = newBall(#SCREEN_WIDTH/2 - #BALL_WIDTH/2, #SCREEN_HEIGHT/2 - #BALL_HEIGHT/2)
  *ball\XDirection = 0
  *ball\YDirection = 1
EndProcedure


Procedure.l getTime()
  If (server)
    ProcedureReturn ElapsedMilliseconds() + timediff
  Else
    ProcedureReturn ElapsedMilliseconds() - serverDiff
  EndIf
EndProcedure


Procedure runGame()
  Repeat
    time = getTime()

    controls(time)
    collision(*myself, time)
    network()
    collision(*opponent, time)
    ball(*ball, time)
    render(time)
    
    If (windowed)
      WindowEvent()
    EndIf
    Delay(0)
    
  Until KeyboardReleased(#PB_Key_Escape)
EndProcedure


Procedure synchronize()
  If (server)
    ; send server's current time
    firstTime = ElapsedMilliseconds()
    SendNetworkData(connection, @firstTime, 4)
    
    Repeat
      event = NetworkServerEvent()
    Until event = #PB_NetworkEvent_Data
    
    ; calculate time difference (server to client)
    time2 = ElapsedMilliseconds()
    timediff = (time2 - firstTime) / 2
    ReceiveNetworkData(connection, @time2, 4)
    
    ; send this difference to client
    SendNetworkData(connection, @timediff, 4)
  Else
    Repeat
      event = NetworkClientEvent(connection)
    Until event = #PB_NetworkEvent_Data
    
    ; receive server's current time
    serverTime = 0
    ReceiveNetworkData(connection, @serverTime, 4)
    serverDiff = ElapsedMilliseconds() - serverTime
    
    ; answer from client to server
    firstTime = ElapsedMilliseconds()
    SendNetworkData(connection, @firstTime, 4) 
    
    Repeat
      event = NetworkClientEvent(connection)
    Until event = #PB_NetworkEvent_Data
    
    ; receive time difference
    ReceiveNetworkData(connection, @timediff, 4)
    
    serverDiff = serverDiff - timediff
  EndIf
  
  ;For i=1 To 100: Delay(10):Next i
EndProcedure


Procedure initConnection()
  If (server)
    If CreateNetworkServer(0, port, #PB_Network_TCP)
      Repeat
        ClearScreen(#BACKGROUND_COLOR)
        StartDrawing(ScreenOutput())
        DrawText(5, 5, "waiting for a client...", #FOREGROUND_COLOR, #BACKGROUND_COLOR)
        StopDrawing()
        FlipBuffers()
        
        ExamineKeyboard()
        If (KeyboardReleased(#PB_Key_Escape))
          exit()
        EndIf
        
        If (windowed)
          WindowEvent()
        EndIf
        Delay(0)
        
        event = NetworkServerEvent()
      Until event = #PB_NetworkEvent_Connect
      
      connection = EventClient()
      
      synchronize()
    Else
      MessageRequester("Pong", "Server could not be created!")
      exit()
    EndIf
  Else
    ClearScreen(#BACKGROUND_COLOR)
    StartDrawing(ScreenOutput())
    DrawText(5, 5, "connecting to server...", #FOREGROUND_COLOR, #BACKGROUND_COLOR)
    StopDrawing()
    FlipBuffers()
    
    connection = OpenNetworkConnection(serverName$, port, #PB_Network_TCP)
    
    If Not connection
      MessageRequester("Pong", "Could not connect to server!")
      exit()
    EndIf
    
    synchronize()
  EndIf
EndProcedure


Procedure main()
  If (MessageRequester("Pong", "Fullscreen?", #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes)
    OpenScreen(#SCREEN_WIDTH, #SCREEN_HEIGHT, 32, "Pong")
    windowed = 0
  Else
    OpenWindow(0, #SCREEN_WIDTH, #SCREEN_HEIGHT, #SCREEN_WIDTH, #SCREEN_HEIGHT, "Pong", #PB_Window_ScreenCentered)
    OpenWindowedScreen(WindowID(0), 0, 0, #SCREEN_WIDTH, #SCREEN_HEIGHT, 0, 0, 0)
    windowed = 1
  EndIf
  
  If (MessageRequester("Pong", "Server?", #PB_MessageRequester_YesNo) = #PB_MessageRequester_Yes)
    server = 1
  Else
    server = 0
    serverName$ = InputRequester("Pong", "Server Address?", serverName$)
  EndIf
  
  initConnection()
  initGame()
  runGame()

  exit()
EndProcedure




OpenPreferences("pongtest.ini")
serverName$ = ReadPreferenceString("server", "localhost")
port = Val(ReadPreferenceString("port", "6666"))


main()


Verfasst: 19.01.2008 13:10
von Vermilion
Weißt du was der hammer wäre? Wenn sich das Spiel über 2 Monitore erstrecken würde. Hatte ich im Herbst vor, nachdem mein Kumpel und ich (wir sitzen in der Schule nebeneinander) die Idee hatten, Pong "Bildschirmübergreifend" an den Schul PCs zu machen, hammer wäre es ja gewesen, wenn der Ball noch zwischen den Monitoren fliegt :D . Naja, nichts draus geworden.

Ok, aber mit diesem Code kann ich mal meine Version nochmal versuchen. Danke jedenfalls.

Verfasst: 19.01.2008 13:13
von ZeHa
Ist im Grunde gar kein Problem, Du mußt einfach nur ein bißchen am Maßstab feilen, die Positionen ändern und dann eben ein Kamera-Offset einbauen, das je nach Spieler nur die eine oder die andere Hälfte zeigt.

Idee ist super ;)


Ach ja, noch was wichtiges (wo Du schon grad von der Schule sprichst):
Der obige Code läuft natürlich tadellos im LAN. Überhaupt kein Problem, da gibt's Latenzen von vielleicht 5-10 Millisekunden, somit kann man eigentlich sagen, daß beides 100%ig synchron abläuft.

Das mit der "Dead Reckoning"-Methode ist nur dann wichtig, wenn man übers Internet spielt, wo die Latenz schnell mal bei 200 ms liegen kann, und das ist dann bereits fatal.

Also wer einfach nur mal Lust hat, im LAN zu pongen, der kann das obige Programm natürlich so wie es ist benutzen. Fehlt höchstens noch sowas wie Punktestand und Gewinnen/Verlieren.