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 | // Comment this line out to remove debug gizmo drawing.
// You probably want to do that for the release version.
#define DEBUG_DRAWTRAJECTORY
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if DEBUG_DRAWTRAJECTORY
using UnityEngine.UI;
#endif
public class Jumper : MonoBehaviour
{
// default settings
const float DEFAULT_GRAVITY = -9.8f;
const float DEFAULT_ASCENT = 5*9.8f;
// When true, adds a parabolic upward component to jumps.
[SerializeField]
private bool allowBallisticAscent = true;
// When true, checks if player goes to high and unceremoniously
// puts them back at the right hight if they go over.
// Old fix for precision issues. Safely experimented with, or ignored, like others.
[SerializeField]
private bool applyHeightOvershootCorrection = false;
// The speed at the which the character leaves the ground to jump. This and
// gravity (in PlayerMovement.cs) determine how much of the jump the character
// is in control of.
[SerializeField]
float maxAscentSpeed = DEFAULT_ASCENT;
// Flag that activates some special code in OffsetFromVelocity that
// corrects precision errors during a certain transfer to parabolic movement.
bool releaseTransition = false;
// Whether the jump is currently in the portion that is 'powered' aka non-ballistic.
bool inPoweredAscent = false;
// Normally ApplyToVerticalSpeed() only applies while inPoweredAscent, but sometimes a frame
// needs it's velocity pushed to PlayerMovement anyway. This flag sets that behaviour.
bool forcePushVerticalSpeed = false;
// During parabolic motion, maxAscentSpeed would be overwritten. Then maxAscentSpeed would be lost.
// Instead, most calculations use appliedAscentSpeed, so that maxAscentSpeed is kept in its initial
// state.
float appliedAscentSpeed = 0;
// Timer that keeps track of how much of the jump is powered vs parabolic.
Timer ascentTimer;
// Cached value of gravity. Update via SetGravity(). Very important that this matches
// PlayerMovement.gravity. (actually, PlayerMovement.gravity*PlayerMovement.gravMultiplier)
float gravity = DEFAULT_GRAVITY;
// Save the initial position of a jump. Used for tracking height.
Vector3 startPosition;
float currentJumpMaxHeight;
// Projected height at which poweredAscent will end and parabolic motion will take over.
float releaseHeight = 0;
// debug information used for drawing those arcs.
#if DEBUG_DRAWTRAJECTORY
float apex = 0;
int frameCounter = 0;
Vector3 lastPosition;
bool debug_draw = false;
#endif
void Start()
{
ascentTimer = new Timer();
appliedAscentSpeed = maxAscentSpeed;
startPosition = transform.position;
currentJumpMaxHeight = 0;
#if DEBUG_DRAWTRAJECTORY
lastPosition = transform.position;
#endif
}
void Update()
{
// If the jump is configured not to use parabolic motion,
// or to use overshoot correction, this block 0's out the
// vertical speed when we get too high. It also sets
// hitApex, which is used later.
bool hitApex = false;
if (applyHeightOvershootCorrection || !allowBallisticAscent)
{
float currentJumpHeight = transform.position.y - startPosition.y;
if (hitApex = currentJumpHeight > currentJumpMaxHeight)
{
forcePushVerticalSpeed = true;
appliedAscentSpeed = 0;
Vector3 position = transform.position;
position.y = startPosition.y + currentJumpMaxHeight;
transform.position = position;
}
}
#if DEBUG_DRAWTRAJECTORY
if (debug_draw)
{
frameCounter++;
float currentJumpHeight = transform.position.y - startPosition.y;
Color r1 = new Color(0.7f, 0.7f, 1f);
Color r2 = new Color(0f, 0f, 1f);
Color y1 = new Color(1f, 1f, 0f);
Color y2 = new Color(0.5f, 0.5f, 0f);
Color y = (frameCounter%2 == 0) ? y1 : y2;
Color r = (frameCounter%2 == 0) ? r1 : r2;
Color c = (inPoweredAscent) ? r : y;
Debug.DrawLine(lastPosition + Vector3.down, transform.position + Vector3.down,
c, 10f);
if (currentJumpHeight > currentJumpMaxHeight)
{
float diff = currentJumpHeight - currentJumpMaxHeight;
Debug.DrawLine(transform.position + Vector3.down, transform.position + Vector3.down + Vector3.down*diff,
Color.red, 10f);
}
float candidateApex = transform.position.y - startPosition.y;
apex = (candidateApex > apex) ? candidateApex : apex;
}
#endif
if (allowBallisticAscent)
{
if (inPoweredAscent)
{
float timerElapse = 0;
if (ascentTimer.UpdateAndCheck(ref timerElapse))
{
// if we're at the end of the powered ascent, switch to parabolic motion.
ReleaseJump(true);
}
}
}
else
{
// end the jump if we hit the top
if (hitApex && inPoweredAscent)
{ ReleaseJump(); }
}
#if DEBUG_DRAWTRAJECTORY
lastPosition = transform.position;
#endif
}
// PlayerMovement calls this to let jumper overwrite vertical
// speed, it may choose not to if not in powered ascent.
public float ApplyToVerticalSpeed(float vertVelocity)
{
float result = (inPoweredAscent || forcePushVerticalSpeed)
? appliedAscentSpeed : vertVelocity;
forcePushVerticalSpeed = false;
return result;
}
// Previously gravity was not factored into a frame's offset calculation at
// an analytical level. This generated a great deal of error. This
// function takes a velocity, and returns the ballistic offset the
// velocity would result in.
public float OffsetFromVelocity(float velocity)
{
if (releaseTransition)
{
releaseTransition = false;
float travelledHeight = transform.position.y - startPosition.y;
float overshoot = travelledHeight - releaseHeight;
float overshootRatio = overshoot / (velocity*Time.deltaTime);
float okayRatio = 1f-overshootRatio;
float ascentPortion = okayRatio*velocity*Time.deltaTime;
float ballisticTimePortion = Time.deltaTime*overshootRatio;
float ballisticPortion = velocity*ballisticTimePortion
+ 0.5f*gravity*(ballisticTimePortion*ballisticTimePortion);
return ascentPortion + ballisticPortion;
}
else if (inPoweredAscent)
{
return velocity*Time.deltaTime;
}
else
{
return velocity*Time.deltaTime+0.5f*gravity*(Time.deltaTime*Time.deltaTime);
}
}
public void SetGravity(float gravity)
{ this.gravity = gravity; }
public void SetMaxAscent(float ascent)
{ this.maxAscentSpeed = ascent; }
// End the powered ascent (blue) portion of the jump.
public void ReleaseJump(bool mixedModel = false)
{ // switch to parabolic
if (!inPoweredAscent)
{ return; }
inPoweredAscent = false;
if (allowBallisticAscent)
{
appliedAscentSpeed = maxAscentSpeed;
releaseTransition = mixedModel;
}
else
{
appliedAscentSpeed = 0f;
}
forcePushVerticalSpeed = !allowBallisticAscent;
}
// Start a jump to a given height.
public void Jump(float maxHeight)
{
// save the starting position of the jump.
startPosition = transform.position;
// save the intended max height for the jump.
currentJumpMaxHeight = maxHeight;
#if DEBUG_DRAWTRAJECTORY
apex = 0;
debug_draw = true;
#endif
// This portion runs even when the jump is not parabolic, but make no mistake,
// this block deals entirely with jumps that have parabolic components.
// The rest of the code simply doesn't use the info generated by this block if
// the jump isn't parabolic.
{
// Given that there is an established ascent speed, we can calculate how high
// that ascent will take us until gravity cancels it out. (the yellow part) The rest of the
// height will need to be covered by a controlled or 'powered' portion of
// the jump. (the blue part).
// Calculate the yellow part.
float parabolicTime = FlatParabolicFlightTime(maxAscentSpeed);
float parabolicHeight = MaxParabolicHeight(maxAscentSpeed, 0.5f*parabolicTime);
if (parabolicHeight < maxHeight)
{
// if the yellow part doesn't cover the whole jump, calculate a supplamental
// blue part.
inPoweredAscent = true;
float controlledHeight = ControlledJumpHeight(maxHeight, parabolicHeight);
float controlledTime = ControlledJumpTime(controlledHeight, maxAscentSpeed);
releaseHeight = controlledHeight;
appliedAscentSpeed = maxAscentSpeed;
ascentTimer.Start(controlledTime);
}
else
{
// if the jump max height is less than the parabolic height that the ascent height
// would create
float jumptime = (maxHeight == 0) ? 0 : 2.0f*FallTime(maxHeight);
releaseHeight = maxHeight;
appliedAscentSpeed = ParabolicVelocity(jumptime);
forcePushVerticalSpeed = true;
inPoweredAscent = false;
}
}
}
public void HitGround()
{
#if DEBUG_DRAWTRAJECTORY
debug_draw = false;
#endif
}
// PHYSICS STUFF =======================================================================
private float FlatParabolicFlightTime(float iniVel)
{ return (2.0f*iniVel)/Mathf.Abs(gravity); }
private float MaxParabolicHeight(float iniVel, float halfFlightTime)
{ // split calculation into multiple parts for debug examination
float vt = iniVel*halfFlightTime;
float halfgtt = 0.5f*Mathf.Abs(gravity)*(halfFlightTime*halfFlightTime);
return vt - halfgtt;
}
private float ParabolicVelocity(float parabolicTime)
{ return -0.5f*gravity*parabolicTime; }
private float ControlledJumpHeight(float maxJumpHeight, float parabolicHeight)
{ return maxJumpHeight - parabolicHeight; }
private float ControlledJumpTime(float controlledJumpHeight, float iniVel)
{ return controlledJumpHeight/iniVel; }
private float FallTime(float height)
{ return Mathf.Sqrt( Mathf.Abs((2*height)/gravity) ); }
}
|