/
init.lua
507 lines (411 loc) · 12.9 KB
/
init.lua
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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
--!native
-- Shake
-- Stephen Leitnick
-- December 09, 2021
local RunService = game:GetService("RunService")
--[=[
@within Shake
@type UpdateCallbackFn () -> (position: Vector3, rotation: Vector3, completed: boolean)
]=]
type UpdateCallbackFn = () -> (Vector3, Vector3, boolean)
export type Shake = {
Amplitude: number,
Frequency: number,
FadeInTime: number,
FadeOutTime: number,
SustainTime: number,
Sustain: boolean,
PositionInfluence: Vector3,
RotationInfluence: Vector3,
TimeFunction: () -> number,
Start: (self: Shake) -> (),
Stop: (self: Shake) -> (),
IsShaking: (self: Shake) -> boolean,
StopSustain: (self: Shake) -> (),
Update: (self: Shake) -> (Vector3, Vector3, boolean),
OnSignal: (
self: Shake,
signal: RBXScriptSignal,
callback: (Vector3, Vector3, boolean) -> ()
) -> RBXScriptConnection,
BindToRenderStep: (self: Shake, name: string, priority: number, callback: (Vector3, Vector3, boolean) -> ()) -> (),
Clone: (self: Shake) -> Shake,
Destroy: (self: Shake) -> (),
}
local rng = Random.new()
local renderId = 0
--[=[
@class Shake
Create realistic shake effects, such as camera or object shakes.
Creating a shake is very simple with this module. For every shake,
simply create a shake instance by calling `Shake.new()`. From
there, configure the shake however desired. Once configured,
call `shake:Start()` and then bind a function to it with either
`shake:OnSignal(...)` or `shake:BindToRenderStep(...)`.
The shake will output its values to the connected function, and then
automatically stop and clean up its connections once completed.
Shake instances can be reused indefinitely. However, only one shake
operation per instance can be running. If more than one is needed
of the same configuration, simply call `shake:Clone()` to duplicate
it.
Example of a simple camera shake:
```lua
local priority = Enum.RenderPriority.Last.Value
local shake = Shake.new()
shake.FadeInTime = 0
shake.Frequency = 0.1
shake.Amplitude = 5
shake.RotationInfluence = Vector3.new(0.1, 0.1, 0.1)
shake:Start()
shake:BindToRenderStep(Shake.NextRenderName(), priority, function(pos, rot, isDone)
camera.CFrame *= CFrame.new(pos) * CFrame.Angles(rot.X, rot.Y, rot.Z)
end)
```
Shakes will automatically stop once the shake has been completed. Shakes can
also be used continuously if the `Sustain` property is set to `true`.
Here are some more helpful configuration examples:
```lua
local shake = Shake.new()
-- The magnitude of the shake. Larger numbers means larger shakes.
shake.Amplitude = 5
-- The speed of the shake. Smaller frequencies mean faster shakes.
shake.Frequency = 0.1
-- Fade-in time before max amplitude shake. Set to 0 for immediate shake.
shake.FadeInTime = 0
-- Fade-out time. Set to 0 for immediate cutoff.
shake.FadeOutTime = 0
-- How long the shake sustains full amplitude before fading out
shake.SustainTime = 1
-- Set to true to never end the shake. Call shake:StopSustain() to start the fade-out.
shake.Sustain = true
-- Multiplies against the shake vector to control the final amplitude of the position.
-- Can be seen internally as: position = shakeVector * fadeInOut * positionInfluence
shake.PositionInfluence = Vector3.one
-- Multiplies against the shake vector to control the final amplitude of the rotation.
-- Can be seen internally as: position = shakeVector * fadeInOut * rotationInfluence
shake.RotationInfluence = Vector3.new(0.1, 0.1, 0.1)
```
]=]
local Shake = {}
Shake.__index = Shake
--[=[
@within Shake
@prop Amplitude number
Amplitude of the overall shake. For instance, an amplitude of `3` would mean the
peak magnitude for the outputted shake vectors would be about `3`.
Defaults to `1`.
]=]
--[=[
@within Shake
@prop Frequency number
Frequency of the overall shake. This changes how slow or fast the
shake occurs.
Defaults to `1`.
]=]
--[=[
@within Shake
@prop FadeInTime number
How long it takes for the shake to fade in, measured in seconds.
Defaults to `1`.
]=]
--[=[
@within Shake
@prop FadeOutTime number
How long it takes for the shake to fade out, measured in seconds.
Defaults to `1`.
]=]
--[=[
@within Shake
@prop SustainTime number
How long it takes for the shake sustains itself after fading in and
before fading out.
To sustain a shake indefinitely, set `Sustain`
to `true`, and call the `StopSustain()` method to stop the sustain
and fade out the shake effect.
Defaults to `0`.
]=]
--[=[
@within Shake
@prop Sustain boolean
If `true`, the shake will sustain itself indefinitely once it fades
in. If `StopSustain()` is called, the sustain will end and the shake
will fade out based on the `FadeOutTime`.
Defaults to `false`.
]=]
--[=[
@within Shake
@prop PositionInfluence Vector3
This is similar to `Amplitude` but multiplies against each axis
of the resultant shake vector, and only affects the position vector.
Defaults to `Vector3.one`.
]=]
--[=[
@within Shake
@prop RotationInfluence Vector3
This is similar to `Amplitude` but multiplies against each axis
of the resultant shake vector, and only affects the rotation vector.
Defaults to `Vector3.one`.
]=]
--[=[
@within Shake
@prop TimeFunction () -> number
The function used to get the current time. This defaults to
`time` during runtime, and `os.clock` otherwise. Usually this
will not need to be set, but it can be optionally configured
if desired.
]=]
--[=[
@return Shake
Construct a new Shake instance.
]=]
function Shake.new(): Shake
local self = setmetatable({}, Shake)
self.Amplitude = 1
self.Frequency = 1
self.FadeInTime = 1
self.FadeOutTime = 1
self.SustainTime = 0
self.Sustain = false
self.PositionInfluence = Vector3.one
self.RotationInfluence = Vector3.one
self.TimeFunction = if RunService:IsRunning() then time else os.clock
self._timeOffset = rng:NextNumber(-1e6, 1e6)
self._startTime = 0
self._running = false
self._signalConnections = {}
self._renderBindings = {}
return self
end
--[=[
Apply an inverse square intensity multiplier to the given vector based on the
distance away from some source. This can be used to simulate shake intensity
based on the distance the shake is occurring from some source.
For instance, if the shake is caused by an explosion in the game, the shake
can be calculated as such:
```lua
local function Explosion(positionOfExplosion: Vector3)
local cam = workspace.CurrentCamera
local renderPriority = Enum.RenderPriority.Last.Value
local shake = Shake.new()
-- Set shake properties here
local function ExplosionShake(pos: Vector3, rot: Vector3)
local distance = (cam.CFrame.Position - positionOfExplosion).Magnitude
pos = Shake.InverseSquare(pos, distance)
rot = Shake.InverseSquare(rot, distance)
cam.CFrame *= CFrame.new(pos) * CFrame.Angles(rot.X, rot.Y, rot.Z)
end
shake:BindToRenderStep("ExplosionShake", renderPriority, ExplosionShake)
end
```
]=]
function Shake.InverseSquare(shake: Vector3, distance: number): Vector3
if distance < 1 then
distance = 1
end
local intensity = 1 / (distance * distance)
return shake * intensity
end
--[=[
Returns a unique render name for every call, which can
be used with the `BindToRenderStep` method optionally.
```lua
shake:BindToRenderStep(Shake.NextRenderName(), ...)
```
]=]
function Shake.NextRenderName(): string
renderId += 1
return ("__shake_%.4i__"):format(renderId)
end
--[=[
Start the shake effect.
:::note
This **must** be called before calling `Update`. As such, it should also be
called once before or after calling `OnSignal` or `BindToRenderStep` methods.
:::
]=]
function Shake:Start()
self._startTime = self.TimeFunction()
self._running = true
end
--[=[
Stops the shake effect. If using `OnSignal` or `BindToRenderStep`, those bound
functions will be disconnected/unbound.
`Stop` is automatically called when the shake effect is completed _or_ when the
`Destroy` method is called.
]=]
function Shake:Stop()
self._running = false
for _, name in self._renderBindings do
RunService:UnbindFromRenderStep(name)
end
table.clear(self._renderBindings)
for _, conn in self._signalConnections do
conn:Disconnect()
end
table.clear(self._signalConnections)
end
--[=[
Returns `true` if the shake instance is currently running,
otherwise returns `false`.
]=]
function Shake:IsShaking(): boolean
return self._running
end
--[=[
Schedules a sustained shake to stop. This works by setting the
`Sustain` field to `false` and letting the shake effect fade out
based on the `FadeOutTime` field.
]=]
function Shake:StopSustain()
local now = self.TimeFunction()
self.Sustain = false
self.SustainTime = (now - self._startTime) - self.FadeInTime
end
--[=[
Calculates the current shake vector. This should be continuously
called inside a loop, such as `RunService.Heartbeat`. Alternatively,
`OnSignal` or `BindToRenderStep` can be used to automatically call
this function.
Returns a tuple of three values:
1. `position: Vector3` - Position shake offset
2. `rotation: Vector3` - Rotation shake offset
3. `completed: boolean` - Flag indicating if the shake is finished
```lua
local hb
hb = RunService.Heartbeat:Connect(function()
local offsetPosition, offsetRotation, isDone = shake:Update()
if isDone then
hb:Disconnect()
end
-- Use `offsetPosition` and `offsetRotation` here
end)
```
]=]
function Shake:Update(): (Vector3, Vector3, boolean)
local done = false
local now = self.TimeFunction()
local dur = now - self._startTime
local noiseInput = ((now + self._timeOffset) / self.Frequency) % 10000
local multiplierFadeIn = 1
local multiplierFadeOut = 1
if dur < self.FadeInTime then
-- Fade in
multiplierFadeIn = dur / self.FadeInTime
end
if not self.Sustain and dur > self.FadeInTime + self.SustainTime then
if self.FadeOutTime == 0 then
done = true
else
-- Fade out
multiplierFadeOut = 1 - (dur - self.FadeInTime - self.SustainTime) / self.FadeOutTime
if not self.Sustain and dur >= self.FadeInTime + self.SustainTime + self.FadeOutTime then
done = true
end
end
end
local offset = Vector3.new(
math.noise(noiseInput, 0) / 2,
math.noise(0, noiseInput) / 2,
math.noise(noiseInput, noiseInput) / 2
) * self.Amplitude * math.min(multiplierFadeIn, multiplierFadeOut)
if done then
self:Stop()
end
return self.PositionInfluence * offset, self.RotationInfluence * offset, done
end
--[=[
@param signal Signal | RBXScriptSignal
@param callbackFn UpdateCallbackFn
@return Connection | RBXScriptConnection
Bind the `Update` method to a signal. For instance, this can be used
to connect to `RunService.Heartbeat`.
All connections are cleaned up when the shake instance is stopped
or destroyed.
```lua
local function SomeShake(pos: Vector3, rot: Vector3, completed: boolean)
-- Shake
end
shake:OnSignal(RunService.Heartbeat, SomeShake)
```
]=]
function Shake:OnSignal(signal, callbackFn: UpdateCallbackFn)
local conn = signal:Connect(function()
callbackFn(self:Update())
end)
table.insert(self._signalConnections, conn)
return conn
end
--[=[
@param name string -- Name passed to `RunService:BindToRenderStep`
@param priority number -- Priority passed to `RunService:BindToRenderStep`
@param callbackFn UpdateCallbackFn
Bind the `Update` method to RenderStep.
All bond functions are cleaned up when the shake instance is stopped
or destroyed.
```lua
local renderPriority = Enum.RenderPriority.Camera.Value
local function SomeShake(pos: Vector3, rot: Vector3, completed: boolean)
-- Shake
end
shake:BindToRenderStep("SomeShake", renderPriority, SomeShake)
```
]=]
function Shake:BindToRenderStep(name: string, priority: number, callbackFn: UpdateCallbackFn)
RunService:BindToRenderStep(name, priority, function()
callbackFn(self:Update())
end)
table.insert(self._renderBindings, name)
end
--[=[
@return Shake
Creates a new shake with identical properties as
this one. This does _not_ clone over playing state,
and thus the cloned instance will be in a stopped
state.
A use-case for using `Clone` would be to create a module
with a list of shake presets. These presets can be cloned
when desired for use. For instance, there might be presets
for explosions, recoil, or earthquakes.
```lua
--------------------------------------
-- Example preset module
local ShakePresets = {}
local explosion = Shake.new()
-- Configure `explosion` shake here
ShakePresets.Explosion = explosion
return ShakePresets
--------------------------------------
-- Use the module:
local ShakePresets = require(somewhere.ShakePresets)
local explosionShake = ShakePresets.Explosion:Clone()
```
]=]
function Shake:Clone()
local shake = Shake.new()
local cloneFields = {
"Amplitude",
"Frequency",
"FadeInTime",
"FadeOutTime",
"SustainTime",
"Sustain",
"PositionInfluence",
"RotationInfluence",
"TimeFunction",
}
for _, field in cloneFields do
shake[field] = self[field]
end
return shake
end
--[=[
Alias for `Stop()`.
]=]
function Shake:Destroy()
self:Stop()
end
return {
new = Shake.new,
InverseSquare = Shake.InverseSquare,
NextRenderName = Shake.NextRenderName,
}