Advertisement
EdBighead

Client Side Prediction Example

Jan 16th, 2012
354
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 9.35 KB | None | 0 0
  1. import time
  2. import copy
  3.  
  4. from panda3d.core import Vec4, VBase3
  5. from direct.showbase.ShowBase import ShowBase
  6. from direct.showbase.DirectObject import DirectObject
  7.  
  8. (KEY_FWD,
  9. KEY_BACK,
  10. KEY_LEFT,
  11. KEY_RIGHT) = range(4)
  12.  
  13. YVEC = VBase3(0, 1, 0)
  14. XVEC = VBase3(1, 0, 0)
  15.  
  16. # Turn on this flag to view the correction.
  17. # The server knows of a 'wall' along the x = 1
  18. # axis that the client doesn't know about
  19. server_only_object = 1
  20.  
  21. class Main(ShowBase):
  22.    
  23.     # The simulated one way latency between the
  24.     # server and the client
  25.     one_way_delay = 250 / 1000.0
  26.    
  27.     def __init__(self):
  28.         ShowBase.__init__(self)
  29.        
  30.         self.client = Client(self)
  31.         self.server = Server(self)
  32.        
  33.         t = taskMgr.add(self.update, 'update')
  34.         t.last = 0
  35.        
  36.     # Updates both the client and the server
  37.     def update(self, task):
  38.         self.client.update()
  39.         self.server.update()
  40.        
  41.         return task.cont
  42.    
  43.     # Sends commands from the client to the server
  44.     # after the simulated delay
  45.     def sendCommands(self, cmds):
  46.         taskMgr.doMethodLater(Main.one_way_delay, self.server.getCommands, 'sendCommands', extraArgs = [cmds])
  47.    
  48.     # Sends the state of the player from the server
  49.     # to the client after the simulated delay
  50.     def sendServerState(self, serverState, clientInputTime):
  51.         taskMgr.doMethodLater(Main.one_way_delay, self.client.getServerState, 'sendServerState', extraArgs = [serverState, clientInputTime])
  52.        
  53. class State():
  54.    
  55.     # pos:
  56.     # The position of the player
  57.    
  58.     # t:
  59.     # The timestamp for this state
  60.    
  61.     def __init__(self, pos = VBase3(0, 0, 0), t = 0):
  62.         self.pos = pos
  63.         self.t = t
  64.        
  65. class InputCommands():
  66.    
  67.     # cmds:
  68.     # The list of keyboard input commands
  69.    
  70.     # t:
  71.     # Clients send this timestamp to the server and the server sends it back.
  72.    
  73.     # oldTime:
  74.     # Used by the server. 't' is updated every tick to calculate movement,
  75.     # so the server saves the last timestamp from the client
  76.     # in this variable
  77.    
  78.     def __init__(self, cmds = [], t = 0):
  79.         self.cmds = cmds
  80.         self.t = t
  81.         self.oldTime = 0
  82.        
  83. class Snapshot():
  84.    
  85.     # A snapshot consists of the state of the pawn and the
  86.     # commands last used
  87.    
  88.     def __init__(self, inputCommands, state):
  89.         self.inputCmds = inputCommands
  90.         self.state = state
  91.    
  92. class SharedCode():
  93.    
  94.     # This function is used by both servers and clients
  95.     # to control the movement of the 'player'. It is
  96.     # important that they use the same function so that
  97.     # the prediction is accurate
  98.     @staticmethod
  99.     def updateState(state, inputCmds):
  100.         dt = (inputCmds.t - state.t) / 1000.0
  101.         cmds = inputCmds.cmds
  102.        
  103.         moveVec = VBase3(0, 0, 0)
  104.            
  105.         if KEY_FWD in cmds:
  106.             moveVec += YVEC
  107.         elif KEY_BACK in cmds:
  108.             moveVec -= YVEC
  109.         if KEY_RIGHT in cmds:
  110.             moveVec += XVEC
  111.         elif KEY_LEFT in cmds:
  112.             moveVec -= XVEC
  113.            
  114.         moveVec.normalize()
  115.         newPos = state.pos + moveVec * 2 * dt
  116.        
  117.         return State(newPos, inputCmds.t + 0.0)
  118.    
  119.     # Returns the current time in milliseconds.
  120.     @staticmethod
  121.     def getTime():
  122.         return int(round(time.time() * 1000))
  123.        
  124. class Client(DirectObject):
  125.    
  126.     # How often the client should send
  127.     # it's commands to the server.
  128.     # Default set to 33Hz
  129.     command_send_delay = 1.0 / 33
  130.    
  131.     def __init__(self, main):
  132.         self.main = main
  133.         self.delay = 0
  134.         self.inputCommands = InputCommands()
  135.         self.myState = State()
  136.         self.historicalCmds = []
  137.         self.lastTime = 0
  138.        
  139.         self.predictedModel = loader.loadModel('pawn')
  140.         self.predictedModel.setColor( Vec4(0, 1, 0, 1) )
  141.         self.predictedModel.reparentTo(render)
  142.        
  143.         self.serverPosModel = loader.loadModel('pawn')
  144.         self.serverPosModel.setColor( Vec4(1, 0, 0, 1) )
  145.         self.serverPosModel.reparentTo(render)
  146.        
  147.         self.setupKeyListening()
  148.        
  149.     # Update tick for the client.
  150.     # During each update:
  151.     #
  152.     # - Update the timestamp for the commands.
  153.     # - Send our commands to the server if enough time has passed.
  154.     # - If so, save our commands and state to our historical list.
  155.     # - Update our local state.
  156.     # - Apply the state.
  157.     def update(self):
  158.         nowTime = SharedCode.getTime()
  159.         self.inputCommands.t = nowTime
  160.         self.delay += (nowTime - self.lastTime) / 1000.0
  161.         self.lastTime = nowTime
  162.        
  163.         if(self.delay > Client.command_send_delay):
  164.             self.inputCommands.cmds = self.getCommands()
  165.             self.historicalCmds.append(copy.deepcopy((self.inputCommands, self.myState)))
  166.             self.main.sendCommands(self.inputCommands)
  167.             self.delay = 0
  168.            
  169.         # Update the state of the predicted model
  170.         self.myState = SharedCode.updateState(self.myState, self.inputCommands)
  171.        
  172.         # Apply the state to the model
  173.         self.ApplyState(self.myState)
  174.        
  175.     # Applies the state to the player.
  176.     # i.e. move the player where he is supposed to be
  177.     def ApplyState(self, myState):
  178.         self.predictedModel.setPos(myState.pos)
  179.        
  180.     # Called when the server state is received by the
  181.     # client
  182.     def getServerState(self, serverState, clientInputTime):
  183.         self.serverPosModel.setPos(serverState.pos)
  184.         self.verifyPrediction(serverState, clientInputTime)
  185.        
  186.     # Here, we compare our historical location
  187.     # to where the server says we are. If somehow we are off
  188.     # by a lot, we recalculate our position by looping
  189.     # through the commands we saved
  190.     def verifyPrediction(self, serverState, clientInputTime):
  191.        
  192.         # Remove old commands
  193.         while len(self.historicalCmds) > 0 and self.historicalCmds[0][0].t < clientInputTime:
  194.             self.historicalCmds.pop(0)
  195.        
  196.         if self.historicalCmds:
  197.             diff =  (serverState.pos - self.historicalCmds[0][1].pos).length()
  198.             print diff
  199.            
  200.             # Recalculate position
  201.             if(diff > 0.2):
  202.                 for oldState in self.historicalCmds:
  203.                     serverState = SharedCode.updateState(serverState, oldState[0])
  204.                
  205.                 self.ApplyState(serverState)
  206.                 self.myState.pos = serverState.pos
  207.            
  208.     # Returns a list of the commands being issued
  209.     # by the player. i.e. the keys being pressed
  210.     def getCommands(self):
  211.         keys = []
  212.        
  213.         if (self.keyMap['KEY_FWD']):
  214.             keys.append(KEY_FWD)
  215.            
  216.         elif (self.keyMap['KEY_BACK']):
  217.             keys.append(KEY_BACK)
  218.            
  219.         if (self.keyMap['KEY_RIGHT']):
  220.             keys.append(KEY_RIGHT)
  221.            
  222.         elif (self.keyMap['KEY_LEFT']):
  223.             keys.append(KEY_LEFT)
  224.  
  225.         return keys
  226.    
  227.     def setKey(self, key, value):
  228.         self.keyMap[key] = value
  229.    
  230.     def setupKeyListening(self):
  231.         self.keyMap = {"KEY_FWD":0, "KEY_BACK":0, "KEY_LEFT":0, "KEY_RIGHT":0}
  232.          
  233.         self.accept("w", self.setKey, ['KEY_FWD', 1])
  234.         self.accept("s", self.setKey, ['KEY_BACK', 1])
  235.         self.accept("a", self.setKey, ['KEY_LEFT', 1])
  236.         self.accept("d", self.setKey, ['KEY_RIGHT', 1])
  237.        
  238.         self.accept("w-up", self.setKey, ['KEY_FWD', 0])
  239.         self.accept("s-up", self.setKey, ['KEY_BACK', 0])
  240.         self.accept("a-up", self.setKey, ['KEY_LEFT', 0])
  241.         self.accept("d-up", self.setKey, ['KEY_RIGHT', 0])
  242.    
  243.    
  244. class Server():
  245.    
  246.     # How often the client should send
  247.     # it's commands to the server.
  248.     # Default set to 20Hz
  249.     position_send_delay = 1.0 / 20
  250.    
  251.     def __init__(self, main):
  252.         self.main = main
  253.         self.delay = 0
  254.         self.playerState = State()
  255.         self.inputCommands = InputCommands()
  256.         self.inputCommands.oldTime = float('+infinity')
  257.         self.gotCmds = False
  258.         self.lastTime = 0
  259.    
  260.     # Update tick for the server.
  261.     # During each update:
  262.     #
  263.     # - Update the timestamp for the commands.
  264.     # - Send the state of the player if enough time has passed.
  265.     # - Update the local state.
  266.     def update(self):
  267.         nowTime = SharedCode.getTime()
  268.         self.inputCommands.t = nowTime
  269.         self.delay += (nowTime - self.lastTime) / 1000.0
  270.         self.lastTime = nowTime
  271.        
  272.         if(self.delay > Server.position_send_delay):
  273.             if(self.gotCmds):
  274.                 self.main.sendServerState(self.playerState, self.inputCommands.oldTime)
  275.             self.delay = 0
  276.                
  277.         self.playerState = SharedCode.updateState(self.playerState, self.inputCommands)
  278.        
  279.         # If the server knows about the wall that the
  280.         # client doesn't
  281.         if server_only_object:
  282.             if self.playerState.pos.getX() > 1:
  283.                 self.playerState.pos.setX(1)
  284.      
  285.     # Called when the client commands are received by the server
  286.     def getCommands(self, cmds):
  287.         self.gotCmds = True
  288.         self.inputCommands = cmds
  289.         self.inputCommands.oldTime = copy.deepcopy(cmds.t)
  290.  
  291. app = Main()
  292. app.run()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement