ServerCube.cs 8.35 KB
Newer Older
cann-alberto's avatar
cann-alberto committed
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
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;

namespace Mirror.Examples.LagCompensationDemo
{
    public class ServerCube : MonoBehaviour
    {
        [Header("Components")]
        public ClientCube client;
        [FormerlySerializedAs("collider")]
        public BoxCollider col;

        [Header("Movement")]
        public float distance = 10;
        public float speed = 3;
        Vector3 start;

        [Header("Snapshot Interpolation")]
        [Tooltip("Send N snapshots per second. Multiples of frame rate make sense.")]
        public int sendRate = 30; // in Hz. easier to work with as int for EMA. easier to display '30' than '0.333333333'
        public float sendInterval => 1f / sendRate;
        float lastSendTime;

        [Header("Lag Compensation")]
        public LagCompensationSettings lagCompensationSettings = new LagCompensationSettings();
        double lastCaptureTime;

        // lag compensation history of <timestamp, capture>
        Queue<KeyValuePair<double, Capture2D>> history = new Queue<KeyValuePair<double, Capture2D>>();

        public Color historyColor = Color.white;

        // store latest lag compensation result to show a visual indicator
        [Header("Debug")]
        public double resultDuration = 0.5;
        double resultTime;
        Capture2D resultBefore;
        Capture2D resultAfter;
        Capture2D resultInterpolated;

        [Header("Latency Simulation")]
        [Tooltip("Latency in seconds")]
        public float latency = 0.05f; // 50 ms
        [Tooltip("Latency jitter, randomly added to latency.")]
        [Range(0, 1)] public float jitter = 0.05f;
        [Tooltip("Packet loss in %")]
        [Range(0, 1)] public float loss = 0.1f;
        [Tooltip("Scramble % of unreliable messages, just like over the real network. Mirror unreliable is unordered.")]
        [Range(0, 1)] public float scramble = 0.1f;

        // random
        // UnityEngine.Random.value is [0, 1] with both upper and lower bounds inclusive
        // but we need the upper bound to be exclusive, so using System.Random instead.
        // => NextDouble() is NEVER < 0 so loss=0 never drops!
        // => NextDouble() is ALWAYS < 1 so loss=1 always drops!
        System.Random random = new System.Random();

        // hold on to snapshots for a little while before delivering
        // <deliveryTime, snapshot>
        List<(double, Snapshot3D)> queue = new List<(double, Snapshot3D)>();

        // latency simulation:
        // always a fixed value + some jitter.
        float SimulateLatency() => latency + Random.value * jitter;

        // this is the average without randomness. for lag compensation math.
        // in a real game, use rtt instead.
        float AverageLatency() => latency + 0.5f * jitter;

        void Start()
        {
            start = transform.position;
        }

        void Update()
        {
            // move on XY plane
            float x = Mathf.PingPong(Time.time * speed, distance);
            transform.position = new Vector3(start.x + x, start.y, start.z);

            // broadcast snapshots every interval
            if (Time.time >= lastSendTime + sendInterval)
            {
                Send(transform.position);
                lastSendTime = Time.time;
            }

            Flush();

            // capture lag compensation snapshots every interval.
            // NetworkTime.localTime because Unity 2019 doesn't have 'double' time yet.
            if (NetworkTime.localTime >= lastCaptureTime + lagCompensationSettings.captureInterval)
            {
                lastCaptureTime = NetworkTime.localTime;
                Capture();
            }
        }

        void Send(Vector3 position)
        {
            // create snapshot
            // Unity 2019 doesn't have Time.timeAsDouble yet
            Snapshot3D snap = new Snapshot3D(NetworkTime.localTime, 0, position);

            // simulate packet loss
            bool drop = random.NextDouble() < loss;
            if (!drop)
            {
                // simulate scramble (Random.Next is < max, so +1)
                bool doScramble = random.NextDouble() < scramble;
                int last = queue.Count;
                int index = doScramble ? random.Next(0, last + 1) : last;

                // simulate latency
                float simulatedLatency = SimulateLatency();
                // Unity 2019 doesn't have Time.timeAsDouble yet
                double deliveryTime = NetworkTime.localTime + simulatedLatency;
                queue.Insert(index, (deliveryTime, snap));
            }
        }

        void Flush()
        {
            // flush ready snapshots to client
            for (int i = 0; i < queue.Count; ++i)
            {
                (double deliveryTime, Snapshot3D snap) = queue[i];

                // Unity 2019 doesn't have Time.timeAsDouble yet
                if (NetworkTime.localTime >= deliveryTime)
                {
                    client.OnMessage(snap);
                    queue.RemoveAt(i);
                    --i;
                }
            }
        }

        void Capture()
        {
            // capture current state
            Capture2D capture = new Capture2D(NetworkTime.localTime, transform.position, col.size);

            // insert into history
            LagCompensation.Insert(history, lagCompensationSettings.historyLimit, NetworkTime.localTime, capture);
        }

        // client says: "I was clicked here, at this time."
        // server needs to rollback to validate.
        // timestamp is the client's snapshot interpolated timeline!
        public bool CmdClicked(Vector2 position)
        {
            // never trust the client: estimate client time instead.
            // https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
            // the estimation is very good. the error is as low as ~6ms for the demo.
            double rtt = AverageLatency() * 2; // the function needs rtt, which is latency * 2
            double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, rtt, client.bufferTime);

            // compare estimated time with actual client time for debugging
            double error = Math.Abs(estimatedTime - client.localTimeline);
            Debug.Log($"CmdClicked: serverTime={NetworkTime.localTime:F3} clientTime={client.localTimeline:F3} estimatedTime={estimatedTime:F3} estimationError={error:F3} position={position}");

            // sample the history to get the nearest snapshots around 'timestamp'
            if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out resultBefore, out resultAfter, out double t))
            {
                // interpolate to get a decent estimation at exactly 'timestamp'
                resultInterpolated = Capture2D.Interpolate(resultBefore, resultAfter, t);
                resultTime = NetworkTime.localTime;

                // check if there really was a cube at that time and position
                Bounds bounds = new Bounds(resultInterpolated.position, resultInterpolated.size);
                if (bounds.Contains(position))
                {
                    return true;
                }
                else Debug.Log($"CmdClicked: interpolated={resultInterpolated} doesn't contain {position}");
            }
            else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");

            return false;
        }

        void OnDrawGizmos()
        {
            // should we apply special colors to an active result?
            bool showResult = NetworkTime.localTime <= resultTime + resultDuration;

            // draw interpoalted result first.
            // history meshcubes should write over it for better visibility.
            if (showResult)
            {
                Gizmos.color = Color.black;
                Gizmos.DrawCube(resultInterpolated.position, resultInterpolated.size);
            }

            // draw history
            Gizmos.color = historyColor;
            LagCompensation.DrawGizmos(history);

            // draw result samples after. useful to see the selection process.
            if (showResult)
            {
                Gizmos.color = Color.cyan;
                Gizmos.DrawWireCube(resultBefore.position, resultBefore.size);
                Gizmos.DrawWireCube(resultAfter.position, resultAfter.size);
            }
        }
    }
}