-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathlogic.go
344 lines (286 loc) · 10.7 KB
/
logic.go
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
package main
import (
"fmt"
"log"
"math"
"os"
"sync"
"time"
"github.com/go-gl/mathgl/mgl32"
"github.com/shurcooL/eX0/eX0-go/packet"
)
// Tau is the constant τ, which equals to 6.283185... or 2π.
// Reference: https://oeis.org/A019692
const Tau = 2 * math.Pi
type logic struct {
// Input channel is only read during ticks when components.client != nil.
Input chan func() packet.Move
started time.Time
GlobalStateSequenceNumber uint8 // TODO: Use int.
NextTickTime float64
TotalPlayerCount uint8
// TODO: There's also some overlap with server.connections, shouldn't that be resolved?
playersStateMu sync.Mutex
playersState map[uint8]playerState // Player ID -> Player State.
particleSystem particleSystem
level *level
}
func newLogic() *logic {
return &logic{
Input: make(chan func() packet.Move, 1),
started: time.Now(),
GlobalStateSequenceNumber: 0,
NextTickTime: 0,
playersState: make(map[uint8]playerState),
}
}
func (l *logic) start() {
go l.gameLogic()
}
func (l *logic) gameLogic() {
var debugFirstJoin = true
var doInput func() packet.Move
for {
tick := false
state.Lock()
l.playersStateMu.Lock() // For GlobalStateSequenceNumber.
now := time.Since(l.started).Seconds()
for now >= l.NextTickTime {
l.NextTickTime += 1.0 / commandRate
l.GlobalStateSequenceNumber++
tick = true
}
l.playersStateMu.Unlock()
state.Unlock()
if debugFirstJoin && components.client != nil {
state.Lock()
playerID := components.client.playerID
state.Unlock()
l.playersStateMu.Lock()
ps, ok := l.playersState[playerID]
if ok && ps.Team != packet.Spectator {
debugFirstJoin = false
logicTime := float64(l.GlobalStateSequenceNumber) + (now-l.NextTickTime)*commandRate
fmt.Fprintf(os.Stderr, "%.3f: Pl#%v (%q) joined team %v at logic time %.2f/%v [logic].\n", now, playerID, l.playersState[playerID].Name, ps.Team, logicTime, l.GlobalStateSequenceNumber)
}
l.playersStateMu.Unlock()
}
if tick && components.client != nil {
state.Lock()
playerID := components.client.playerID
state.Unlock()
select {
case doInput = <-l.Input:
default:
}
if doInput != nil {
l.playersStateMu.Lock()
ps, ok := l.playersState[playerID]
if ok && ps.Team != packet.Spectator && ps.Health > 0 {
// Fill all missing commands (from last authed until one we're supposed to be by now (based on GlobalStateSequenceNumber time).
for lastState := ps.LatestPredicted(); int8(lastState.SequenceNumber-l.GlobalStateSequenceNumber) < 0; lastState = ps.LatestPredicted() {
move := doInput()
newState := l.nextState(lastState, move)
ps.unconfirmed = append(ps.unconfirmed, predictedMove{
move: move,
predicted: newState,
})
}
l.playersState[playerID] = ps
}
l.playersStateMu.Unlock()
}
if components.client.serverConn != nil && components.client.serverConn.JoinStatus >= IN_GAME {
// Send a ClientCommand packet to server.
// TODO: This should be done via Local/Network State Auther. This currently hardcodes network state auther.
state.Lock()
l.playersStateMu.Lock() // For GlobalStateSequenceNumber.
ps, ok := l.playersState[playerID]
if ok && ps.Team != packet.Spectator && len(ps.unconfirmed) > 0 {
var p packet.ClientCommand
p.CommandSequenceNumber = l.GlobalStateSequenceNumber - 1
p.CommandSeriesNumber = 1 // TODO: Don't hardcode this.
for _, unconfirmed := range ps.unconfirmed {
p.Moves = append(p.Moves, unconfirmed.move)
}
//fmt.Printf("%.3f: sending ClientCommand with %v moves, clientLastAckedCSN=%v, G-1=%v\n", now, len(p.Moves), clientLastAckedCmdSequenceNumber, l.GlobalStateSequenceNumber-1)
/*for i, unconfirmed := range ps.unconfirmed {
fmt.Println(i, "unconfirmed.predicted.SequenceNumber:", unconfirmed.predicted.SequenceNumber, "dir:", unconfirmed.move.MoveDirection)
}*/
p.MovesCount = uint8(len(p.Moves)) - 1
err := sendUDPPacket(components.client.serverConn, &p)
if err != nil {
panic(err)
}
}
l.playersStateMu.Unlock()
state.Unlock()
}
l.particleSystem.Tick(now)
}
state.Lock() // For started and NextTickTime.
now = time.Since(l.started).Seconds()
sleep := time.Duration((l.NextTickTime - now) * float64(time.Second))
state.Unlock()
time.Sleep(sleep)
}
}
type gameMoment float64
// SNAndTick returns game moment m as sequence number and tick.
// Tick is in [0, 1) range.
func (m gameMoment) SNAndTick() (sequenceNumber uint8, tick float64) {
sn, tick := math.Modf(float64(m) * commandRate)
sn2 := int(sn) // Workaround for https://github.com/gopherjs/gopherjs/issues/733.
return uint8(sn2), tick
}
type playerPosVel struct {
X, Y, Z float32
VelX, VelY float32
}
type sequencedPlayerPosVel struct {
playerPosVel
SequenceNumber uint8 // TODO: Use int.
}
type predictedMove struct {
move packet.Move
predicted sequencedPlayerPosVel
}
// TODO: Split into positions (there will be many over time) and current name, team, connection, etc.
type playerState struct {
authed []sequencedPlayerPosVel
unconfirmed []predictedMove
Name string
Team packet.Team
// TODO: Might want to move Health to []sequencedPlayerPosVel, so its history is preserved, and DeadState won't be needed.
Health float32 // Health is in [0, 100] range.
DeadState playerPosVel // DeadState is player state at the moment they died.
// TODO: Move this to a better place.
conn *Connection
lastServerUpdateSequenceNumber uint8 // Sequence Number of last packet.ServerUpdate sent to this connection. // TODO: This should go into a serverToClient connection struct.
}
func (ps playerState) LatestAuthed() sequencedPlayerPosVel {
return ps.authed[len(ps.authed)-1]
}
func (ps playerState) LatestPredicted() sequencedPlayerPosVel {
if len(ps.unconfirmed) > 0 {
return ps.unconfirmed[len(ps.unconfirmed)-1].predicted
}
return ps.authed[len(ps.authed)-1]
}
func (ps *playerState) PushAuthed(logic *logic, newState sequencedPlayerPosVel) {
if len(ps.authed) > 0 && newState.SequenceNumber == ps.authed[len(ps.authed)-1].SequenceNumber {
// Skip updates that are not newer.
return
}
// Drop unconfirmed predicted moves once they've been authed.
for len(ps.unconfirmed) > 0 && newState.SequenceNumber != ps.unconfirmed[0].predicted.SequenceNumber {
//fmt.Fprintf(os.Stderr, "PushAuthed: dropping unmatched ps.unconfirmed\n")
ps.unconfirmed = ps.unconfirmed[1:]
}
if len(ps.unconfirmed) > 0 && newState.SequenceNumber == ps.unconfirmed[0].predicted.SequenceNumber {
if same := mgl32.FloatEqualThreshold(newState.X, ps.unconfirmed[0].predicted.X, 0.001) &&
mgl32.FloatEqualThreshold(newState.Y, ps.unconfirmed[0].predicted.Y, 0.001); same {
//fmt.Fprintf(os.Stderr, "PushAuthed: dropping matched ps.unconfirmed, same!\n")
} else {
fmt.Fprintf(os.Stderr, "PushAuthed: dropping matched ps.unconfirmed, diff by %v, %v\n", newState.X-ps.unconfirmed[0].predicted.X, newState.Y-ps.unconfirmed[0].predicted.Y)
}
// Keep the locally-predicted velocity.
newState.VelX = ps.unconfirmed[0].predicted.VelX
newState.VelY = ps.unconfirmed[0].predicted.VelY
ps.unconfirmed = ps.unconfirmed[1:]
}
// TODO: GC.
//fmt.Fprintln(os.Stderr, "PushAuthed:", newState.SequenceNumber)
ps.authed = append(ps.authed, newState)
// Replay remaining ones.
prevState := newState
for i := range ps.unconfirmed {
ps.unconfirmed[i].predicted = logic.nextState(prevState, ps.unconfirmed[i].move)
prevState = ps.unconfirmed[i].predicted
}
}
func (ps *playerState) NewSeries() {
// TODO: Consider preserving.
ps.authed = nil
ps.unconfirmed = nil
}
func (ps playerState) InterpolatedOrDead(gameMoment gameMoment, playerID uint8) playerPosVel {
if ps.Health <= 0 {
return ps.DeadState
}
// When we don't have perfect information about present, return position 100 ms in the past.
if components.client == nil || components.client.playerID != playerID {
gameMoment -= 0.1
}
pos := ps.interpolated(gameMoment)
// HACK, TODO: Clean this up. Currently assumes asking for latest time for client player.
if components.client != nil && components.client.playerID == playerID {
components.client.TargetZMu.Lock()
pos.Z = components.client.TargetZ
components.client.TargetZMu.Unlock()
}
return pos
}
func (ps *playerState) SetDead(gameMoment gameMoment, playerID uint8) {
// When we don't have perfect information about present, return position 100 ms in the past.
if components.client == nil || components.client.playerID != playerID {
gameMoment -= 0.1
}
pos := ps.interpolated(gameMoment)
// HACK, TODO: Clean this up. Currently assumes asking for latest time for client player.
if components.client != nil && components.client.playerID == playerID {
components.client.TargetZMu.Lock()
pos.Z = components.client.TargetZ
components.client.TargetZMu.Unlock()
}
ps.DeadState = pos
}
func (ps playerState) interpolated(gameMoment gameMoment) playerPosVel {
desiredAStateSN, tick := gameMoment.SNAndTick()
// Gather all authed and predicted states to iterate over.
states := append([]sequencedPlayerPosVel(nil), ps.authed...)
for _, unconfirmed := range ps.unconfirmed {
states = append(states, unconfirmed.predicted)
}
if len(states) == 0 {
log.Println("playerState.interpolated called when there are no states")
return playerPosVel{}
}
if len(states) == 1 {
return states[0].playerPosVel
}
ai := len(states) - 1
a := states[ai]
// Check if we're looking for a sequence number newer than history contains.
if int8(desiredAStateSN-a.SequenceNumber) > 0 {
// Point A is not yet in history, so we'd need to extrapolate into future a lot.
// TODO: Extrapolate into future?
//fmt.Println("warning: using LatestAuthed because:", int8(desiredAStateSN-a.SequenceNumber))
return states[len(states)-1].playerPosVel
}
// Scroll index of a back until it's the desired sn (or earlier).
for int8(a.SequenceNumber-desiredAStateSN) > 0 {
ai--
if ai < 0 {
// Point A is not in history, so we'd need to extrapolate into past... Just return earliest state for now.
// TODO: Extrapolate in past?
return states[0].playerPosVel
}
a = states[ai]
}
bi := ai + 1
if bi >= len(states) {
// Point B is not yet in history, so we'd need to extrapolate into future a little.
// TODO: Extrapolate into future?
return states[len(states)-1].playerPosVel
}
b := states[bi]
interp := float32(desiredAStateSN-a.SequenceNumber) + float32(tick)
interpDistance := float32(b.SequenceNumber - a.SequenceNumber)
interp /= interpDistance // Normalize.
return playerPosVel{
X: (1-interp)*a.playerPosVel.X + interp*b.playerPosVel.X,
Y: (1-interp)*a.playerPosVel.Y + interp*b.playerPosVel.Y,
Z: (1-interp)*a.playerPosVel.Z + interp*b.playerPosVel.Z,
}
}