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