HistoryCollider.cs 4.54 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
// Applies HistoryBounds to the physics world by projecting to a trigger Collider.
// This way we can use Physics.Raycast on it.
using UnityEngine;

namespace Mirror
{
    [DisallowMultipleComponent]
    [AddComponentMenu("Network/ Lag Compensation/ History Collider")]
    public class HistoryCollider : MonoBehaviour
    {
        [Header("Components")]
        [Tooltip("The object's actual collider. We need to know where it is, and how large it is.")]
        public Collider actualCollider;

        [Tooltip("The helper collider that the history bounds are projected onto.\nNeeds to be added to a child GameObject to counter-rotate an axis aligned Bounding Box onto it.\nThis is only used by this component.")]
        public BoxCollider boundsCollider;

        [Header("History")]
        [Tooltip("Keep this many past bounds in the buffer. The larger this is, the further we can raycast into the past.\nMaximum time := historyAmount * captureInterval")]
        public int boundsLimit = 8;

        [Tooltip("Gather N bounds at a time into a bucket for faster encapsulation. A factor of 2 will be twice as fast, etc.")]
        public int boundsPerBucket = 2;

        [Tooltip("Capture bounds every 'captureInterval' seconds. Larger values will require fewer computations, but may not capture every small move.")]
        public float captureInterval = 0.100f; // 100 ms
        double lastCaptureTime = 0;

        [Header("Debug")]
        public Color historyColor = new Color(1.0f, 0.5f, 0.0f, 1.0f);
        public Color currentColor = Color.red;

        protected HistoryBounds history = null;

        protected virtual void Awake()
        {
            history = new HistoryBounds(boundsLimit, boundsPerBucket);

            // ensure colliders were set.
            // bounds collider should always be a trigger.
            if (actualCollider == null)    Debug.LogError("HistoryCollider: actualCollider was not set.");
            if (boundsCollider == null)    Debug.LogError("HistoryCollider: boundsCollider was not set.");
            if (boundsCollider.transform.parent != transform) Debug.LogError("HistoryCollider: boundsCollider must be a child of this GameObject.");
            if (!boundsCollider.isTrigger) Debug.LogError("HistoryCollider: boundsCollider must be a trigger.");
        }

        // capturing and projecting onto colliders should use physics update
        protected virtual void FixedUpdate()
        {
            // capture current bounds every interval
            if (NetworkTime.localTime >= lastCaptureTime + captureInterval)
            {
                lastCaptureTime = NetworkTime.localTime;
                CaptureBounds();
            }

            // project bounds onto helper collider
            ProjectBounds();
        }

        protected virtual void CaptureBounds()
        {
            // grab current collider bounds
            // this is in world space coordinates, and axis aligned
            // TODO double check
            Bounds bounds = actualCollider.bounds;

            // insert into history
            history.Insert(bounds);
        }

        protected virtual void ProjectBounds()
        {
            // grab total collider encapsulating all of history
            Bounds total = history.total;

            // don't assign empty bounds, this will throw a Unity warning
            if (history.boundsCount == 0) return;

            // scale projection doesn't work yet.
            // for now, don't allow scale changes.
            if (transform.lossyScale != Vector3.one)
            {
                Debug.LogWarning($"HistoryCollider: {name}'s transform global scale must be (1,1,1).");
                return;
            }

            // counter rotate the child collider against the gameobject's rotation.
            // we need this to always be axis aligned.
            boundsCollider.transform.localRotation = Quaternion.Inverse(transform.rotation);

            // project world space bounds to collider's local space
            boundsCollider.center = boundsCollider.transform.InverseTransformPoint(total.center);
            boundsCollider.size   = total.size; // TODO projection?
        }

        // TODO runtime drawing for debugging?
        protected virtual void OnDrawGizmos()
        {
            // draw total bounds
            Gizmos.color = historyColor;
            Gizmos.DrawWireCube(history.total.center, history.total.size);

            // draw current bounds
            Gizmos.color = currentColor;
            Gizmos.DrawWireCube(actualCollider.bounds.center, actualCollider.bounds.size);
        }
    }
}