LagCompensator.cs 8.11 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
// Add this component to a Player object with collider.
// Automatically keeps a history for lag compensation.
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Mirror
{
    public struct Capture3D : Capture
    {
        public double timestamp { get; set; }
        public Vector3 position;
        public Vector3 size;

        public Capture3D(double timestamp, Vector3 position, Vector3 size)
        {
            this.timestamp = timestamp;
            this.position = position;
            this.size = size;
        }

        public void DrawGizmo()
        {
            Gizmos.DrawWireCube(position, size);
        }

        public static Capture3D Interpolate(Capture3D from, Capture3D to, double t) =>
            new Capture3D(
                0, // interpolated snapshot is applied directly. don't need timestamps.
                Vector3.LerpUnclamped(from.position, to.position, (float)t),
                Vector3.LerpUnclamped(from.size, to.size, (float)t)
            );

        public override string ToString() => $"(time={timestamp} pos={position} size={size})";
    }

    [DisallowMultipleComponent]
    [AddComponentMenu("Network/ Lag Compensation/ Lag Compensator")]
    [HelpURL("https://mirror-networking.gitbook.io/docs/manual/general/lag-compensation")]
    public class LagCompensator : NetworkBehaviour
    {
        [Header("Components")]
        [Tooltip("The collider to keep a history of.")]
        public Collider trackedCollider; // assign this in inspector

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

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

        [Header("Debugging")]
        public Color historyColor = Color.white;

        [ServerCallback]
        protected virtual void Update()
        {
            // 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();
            }
        }

        [ServerCallback]
        protected virtual void Capture()
        {
            // capture current state
            Capture3D capture = new Capture3D(
                NetworkTime.localTime,
                trackedCollider.bounds.center,
                trackedCollider.bounds.size
            );

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

        protected virtual void OnDrawGizmos()
        {
            // draw history
            Gizmos.color = historyColor;
            LagCompensation.DrawGizmos(history);
        }

        // sampling ////////////////////////////////////////////////////////////
        // sample the sub-tick (=interpolated) history of this object for a hit test.
        // 'viewer' needs to be the player who fired!
        // for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
        [ServerCallback]
        public virtual bool Sample(NetworkConnectionToClient viewer, out Capture3D sample)
        {
            // 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.
            // note that passing 'rtt' is fine: EstimateTime halves it to latency.
            double estimatedTime = LagCompensation.EstimateTime(NetworkTime.localTime, viewer.rtt, NetworkClient.bufferTime);

            // sample the history to get the nearest snapshots around 'timestamp'
            if (LagCompensation.Sample(history, estimatedTime, lagCompensationSettings.captureInterval, out Capture3D resultBefore, out Capture3D resultAfter, out double t))
            {
                // interpolate to get a decent estimation at exactly 'timestamp'
                sample = Capture3D.Interpolate(resultBefore, resultAfter, t);
                return true;
            }
            else Debug.Log($"CmdClicked: history doesn't contain {estimatedTime:F3}");

            sample = default;
            return false;
        }

        // convenience tests ///////////////////////////////////////////////////
        // there are multiple different ways to check a hit against the sample:
        // - raycasting
        // - bounds.contains
        // - increasing bounds by tolerance and checking contains
        // - threshold to bounds.closestpoint
        // let's offer a few solutions directly and see which users prefer.

        // bounds check: checks distance to closest point on bounds in history @ -rtt.
        //   'viewer' needs to be the player who fired!
        //   for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
        // this is super simple and fast, but not 100% physically accurate since we don't raycast.
        [ServerCallback]
        public virtual bool BoundsCheck(
            NetworkConnectionToClient viewer,
            Vector3 hitPoint,
            float toleranceDistance,
            out float distance,
            out Vector3 nearest)
        {
            // first, sample the history at -rtt of the viewer.
            if (Sample(viewer, out Capture3D capture))
            {
                // now that we know where the other player was at that time,
                // we can see if the hit point was within tolerance of it.
                // TODO consider rotations???
                // TODO consider original collider shape??
                Bounds bounds = new Bounds(capture.position, capture.size);
                nearest = bounds.ClosestPoint(hitPoint);
                distance = Vector3.Distance(nearest, hitPoint);
                return distance <= toleranceDistance;
            }
            nearest = hitPoint;
            distance = 0;
            return false;
        }

        // raycast check: creates a collider the sampled position and raycasts to hitPoint.
        //   'viewer' needs to be the player who fired!
        //   for example, if A fires at B, then call B.Sample(viewer, point, tolerance).
        // this is physically accurate (checks against walls etc.), with the cost
        // of a runtime instantiation.
        //
        //  originPoint: where the player fired the weapon.
        //  hitPoint: where the player's local raycast hit.
        //  tolerance: scale up the sampled collider by % in order to have a bit of a tolerance.
        //             0 means no extra tolerance, 0.05 means 5% extra tolerance.
        //  layerMask: the layer mask to use for the raycast.
        [ServerCallback]
        public virtual bool RaycastCheck(
            NetworkConnectionToClient viewer,
            Vector3 originPoint,
            Vector3 hitPoint,
            float tolerancePercent,
            int layerMask,
            out RaycastHit hit)
        {
            // first, sample the history at -rtt of the viewer.
            if (Sample(viewer, out Capture3D capture))
            {
                // instantiate a real physics collider on demand.
                // TODO rotation??
                // TODO different collier types??
                GameObject temp = new GameObject("LagCompensatorTest");
                temp.transform.position = capture.position;
                BoxCollider tempCollider = temp.AddComponent<BoxCollider>();
                tempCollider.size = capture.size * (1 + tolerancePercent);

                // raycast
                Vector3 direction = hitPoint - originPoint;
                float maxDistance = direction.magnitude * 2;
                bool result = Physics.Raycast(originPoint, direction, out hit, maxDistance, layerMask);

                // cleanup
                Destroy(temp);
                return result;
            }

            hit = default;
            return false;
        }
    }
}