-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathAnimatedGridMovement.cs
258 lines (217 loc) · 7.98 KB
/
AnimatedGridMovement.cs
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
/**************************************************************
* The AnimatedGridMovement script performs "Dungeon Master"/ *
* "Legend of Grimrock" style WSADEQ movement in your Unity3D *
* game. *
* *
* Written by: Lutz Grosshennig, 06/27/2020 *
* MIT Licence *
* ************************************************************/
using UnityEngine;
public class AnimatedGridMovement : MonoBehaviour
{
private const float LeftHand = -90.0f;
private const float RightHand = +90.0f;
[Header("Grid settings")]
[SerializeField] private float gridSize = 4.0f;
[Header("Movement settings")]
[SerializeField] private float rotationSpeed = 5.0f;
[SerializeField] private float movementSpeed = 1.0f;
[Header("Free look settings")]
[SerializeField] private float freelookSensitivity = 1.0f;
[Range(45.0f, 89.0f)]
[SerializeField] private float freelookAngle = 85.0f;
private bool freelookModeEnabled = false;
private Vector3 moveTowardsPosition;
private Quaternion rotateFromDirection;
private Quaternion rotateTowardsDirection;
private float rotationTime = 0.0f;
void Start()
{
moveTowardsPosition = transform.position;
rotateTowardsDirection = transform.rotation;
rotateFromDirection = transform.rotation;
}
private void FixedUpdate()
{
if (IsStationary())
{
// Personaly I would prefer a dictionary<KeyCode,Action> lookup but this does not seem possible with Unity3D.Input
if (Input.GetKey(KeyCode.W))
{
MoveForward();
}
else if (Input.GetKey(KeyCode.S))
{
MoveBackward();
}
else if (Input.GetKey(KeyCode.Q))
{
TurnLeft();
}
else if (Input.GetKey(KeyCode.E))
{
TurnRight();
}
else if (Input.GetKey(KeyCode.A))
{
StrafeLeft();
}
else if (Input.GetKey(KeyCode.D))
{
StrafeRight();
}
}
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Mouse1))
{
EnterFreeLookMode();
}
else if (Input.GetKeyUp(KeyCode.Mouse1))
{
ExitFreeLookMode();
}
if (freelookModeEnabled)
{
// calling this function causes a native call to the c++ side, so we only want to do that once and reuse the result.
Vector3 localEulerAngles = transform.localEulerAngles;
float lookAtAngle_Y = localEulerAngles.y + Input.GetAxis("Mouse X") * freelookSensitivity;
float lookAtAngle_X = localEulerAngles.x - Input.GetAxis("Mouse Y") * freelookSensitivity;
// calling this function causes a native call to the c++ side, so we only want to do that once and reuse the result.
float viewDirection = rotateTowardsDirection.eulerAngles.y;
float minClampRange = viewDirection - freelookAngle;
float maxClampRange = viewDirection + freelookAngle;
// Since we are in Euler space we need to prevent a "Gimble look", however Euler angles are modular so we
// need to take this into account when we want to clamp the angles.
lookAtAngle_X = ClampAngle(lookAtAngle_X, -freelookAngle, freelookAngle);
lookAtAngle_Y = ClampAngle(lookAtAngle_Y, minClampRange, maxClampRange);
transform.localEulerAngles = new Vector3(lookAtAngle_X, lookAtAngle_Y, 0.0f);
}
else
{
if (IsMoving())
{
var step = Time.deltaTime * gridSize * movementSpeed;
AnimateMovement(step);
}
if (IsRotating())
{
AnimateRotation();
}
}
}
private void AnimateRotation()
{
rotationTime += Time.deltaTime;
transform.rotation = Quaternion.Slerp(rotateFromDirection, rotateTowardsDirection, rotationTime * rotationSpeed);
CompensateRotationRoundingErrors();
}
private void AnimateMovement(float step)
{
transform.position = Vector3.MoveTowards(transform.position, moveTowardsPosition, step);
}
private void CompensateRotationRoundingErrors()
{
// Bear in mind that floating point numbers are inaccurate by design.
// The == operator performs a fuzy compare which means that we are only approximatly near the target rotation.
// We may not entirely reached the rotateTowardsViewAngle or we may have slightly overshot it already (both within the margin of error).
if (transform.rotation == rotateTowardsDirection)
{
// To compensate rounding errors we explictly set the transform to our desired rotation.
transform.rotation = rotateTowardsDirection;
}
}
private void MoveForward()
{
CollisonCheckedMovement(CalculateForwardPosition());
}
private void MoveBackward()
{
CollisonCheckedMovement(-CalculateForwardPosition());
}
private void StrafeRight()
{
CollisonCheckedMovement(CalculateStrafePosition());
}
private void StrafeLeft()
{
CollisonCheckedMovement(-CalculateStrafePosition());
}
private void CollisonCheckedMovement(Vector3 movementDirection)
{
Vector3 targetPosition = moveTowardsPosition + movementDirection;
// TODO: Replace the true flag with your collision detection code.
bool canMove = true;
if (canMove)
{
moveTowardsPosition = targetPosition;
}
}
private void TurnRight()
{
TurnEulerDegrees(RightHand);
}
private void TurnLeft()
{
TurnEulerDegrees(LeftHand);
}
private void TurnEulerDegrees(in float eulerDirectionDelta)
{
rotateFromDirection = transform.rotation;
rotationTime = 0.0f;
rotateTowardsDirection *= Quaternion.Euler(0.0f, eulerDirectionDelta, 0.0f);
}
private bool IsStationary()
{
return !(IsMoving() || IsRotating());
}
private bool IsMoving()
{
return transform.position != moveTowardsPosition;
}
private bool IsRotating()
{
return transform.rotation != rotateTowardsDirection;
}
private Vector3 CalculateForwardPosition()
{
return transform.forward * gridSize;
}
private Vector3 CalculateStrafePosition()
{
return transform.right * gridSize;
}
private void OnDisable()
{
ExitFreeLookMode();
}
private void EnterFreeLookMode()
{
freelookModeEnabled = true;
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
}
private void ExitFreeLookMode()
{
freelookModeEnabled = false;
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
// Euler angles are modular and cant be simply clamped and therefore need special treatment.
// I.E. 90° is the same as 450° or -270° and there is no gurantee which value you are getting
// so we need to filter this out. This function is most likly suited to be placed some where else though
// but the idea here is to have everything in a single simple script that you can attach to your camera.
private float ClampAngle(float current, float min, float max)
{
float deltaAngle = Mathf.Abs(((min - max) + 180.0f) % 360.0f - 180.0f);
float deltaAngleHalf = deltaAngle * 0.5f;
float middelAngle = min + deltaAngleHalf;
float offset = Mathf.Abs(Mathf.DeltaAngle(current, middelAngle)) - deltaAngleHalf;
if (offset > 0.0f)
{
current = Mathf.MoveTowardsAngle(current, middelAngle, offset);
}
return current;
}
}