· 5 years ago · Feb 24, 2020, 10:36 AM
1--[[
2Surface Walker Controller - by EgoMoose and Goldenstein64
306/04/19 - 9:29PM EST
4
5Quick Notes:
6 - Place the PlayerModule under game.StarterPlayer.StarterPlayerScripts
7 - Any actively rotating parts should be name "rPart"
8 - Groups of rParts that should not have their normal's actively updated should have a certain format
9 * The model MUST have a primary part. This primary part is the part that will be used as the relative part
10 for every rPart in the model.
11 * The model MUST have a "Vector3Value" titled "GroupNormal" as its child. This value represents the object space normal
12 that should be used for the rPart group.
13
14API:
15
16Constructors:
17 Controller.new(player Instance)
18 > Creates a controller object for the player.
19
20Properties:
21 [Read Only]
22 Controller.Enabled
23 > Whether or not the controller is active.
24 Controller.NormalUpdateTick
25 > The last tick() when the :SetNormal method was successfully called and updated.
26 > This is useful for doing thing like debounces between transitioning normals.
27 [Set/Get]
28 Controller.CustomCameraEnabled
29 > If true then the camera will rotate along with the player, otherwise it will behave like the default camera.
30 Controller.CustomCameraSpinWithrParts
31 > If true and Controller.CustomCameraEnabled is also true then the camera will spin along with rParts.
32 > Defaults to true.
33 Controller.Ignore
34 > Object (and descendants) which the fake world will ignore. This should at minimum be set to the player's
35 > character which it is by default.
36 Controller.Cast.Ignore
37 > Object (and descendants) which raycasts will ignore. This should at minimum be set to the player's
38 > character which it is by default.
39
40Methods:
41 Controller:SetEnabled(bool Boolean)
42 > Sets whether the controller is active or not.
43 Controller:SetNormal(normal Vector3, part Instance)
44 > Sets the world relative to the part where normal is "up".
45 Controller:Update(dt Float)
46 > Must be called after camera updates every frame.
47 > dt is delta time.
48--]]
49
50local debugger = require(script:WaitForChild("Debugger"))
51local castClass = require(script:WaitForChild("Cast"))
52local animateR15 = script:WaitForChild("AnimateR15")
53local animateR6 = script:WaitForChild("AnimateR6")
54local sound = script:WaitForChild("LocalSound")
55
56local PI = math.pi
57local EPSILON = 1E-4
58local TRANSPARENCY = 1
59local UP = Vector3.new(0, 1, 0)
60local ZERO = Vector3.new(0, 0, 0)
61local HORIZONTAL = Vector3.new(1, 0, 1)
62local TERRAIN = game.Workspace.Terrain
63local CAMERA = game.Workspace.CurrentCamera
64local CFVECTORS = {"RightVector", "UpVector", "LookVector"}
65local REPLICATED_HUMANOID_PROPERTIES = {"WalkSpeed", "JumpPower", "Jump", "UseJumpPower", "Sit", "MaxSlopeAngle"}
66
67local STARTER_PLAYER = game:GetService("StarterPlayer")
68
69-- Private Functions
70
71local function characterDict(character)
72 return {
73 Character = character,
74 Humanoid = character:WaitForChild("Humanoid"),
75 HRP = character:WaitForChild("HumanoidRootPart")
76 }
77end
78
79local function cloneArchivable(item)
80 local arch = item.Archivable
81 item.Archivable = true
82 local clone = item:Clone()
83 item.Archivable = arch
84 return clone
85end
86
87local function hideFake(fake)
88 fake.Humanoid:RemoveAccessories()
89 fake.Humanoid.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff
90 fake.Humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
91
92 local face = fake.Character:WaitForChild("Head"):FindFirstChild("face")
93 if (face) then
94 face:Destroy()
95 end
96
97 local children = fake.Character:GetChildren()
98 for i = 1, #children do
99 if (children[i]:IsA("BasePart")) then
100 children[i].Transparency = TRANSPARENCY
101 end
102 end
103end
104
105local function createForces(real)
106 local attach0 = Instance.new("Attachment")
107 local attach1 = Instance.new("Attachment")
108
109 local alignPosition = Instance.new("AlignPosition")
110 alignPosition.Enabled = false
111 alignPosition.MaxForce = 10^6
112 alignPosition.Responsiveness = 200
113 --alignPosition.RigidityEnabled = true
114 alignPosition.Attachment0 = attach0
115 alignPosition.Attachment1 = attach1
116
117 local alignOrientation = Instance.new("AlignOrientation")
118 alignOrientation.Enabled = false
119 alignOrientation.MaxTorque = 10^6
120 alignOrientation.Responsiveness = 200
121 --alignOrientation.RigidityEnabled = true
122 alignOrientation.Attachment0 = attach0
123 alignOrientation.Attachment1 = attach1
124
125 attach0.Parent = real.HRP
126 attach1.Parent = TERRAIN
127 alignPosition.Parent = real.HRP
128 alignOrientation.Parent = real.HRP
129
130 return {
131 Attach0 = attach0;
132 Attach1 = attach1;
133 AlignPosition = alignPosition;
134 AlignOrientation = alignOrientation;
135 }
136end
137
138local function updateFakeWorld(self, hit)
139 local tracker = self.FakeWorldTracker
140
141 local pos = self.Real.HRP.Position
142 local size = Vector3.new(10, 10, 10)
143 local region = Region3.new(pos - size, pos + size)
144 local surroundings = game.Workspace:FindPartsInRegion3(region, self.Ignore, math.huge)
145
146 local baseCF = self.FakeWorldCenter * self.Last.SurfaceCF * (self.Last.HitCF - self.Last.HitCF.p)
147 local partCFInv = hit.CFrame:Inverse()
148
149 local isSurroundings = {}
150
151 for i = 1, #surroundings do
152 local real = surroundings[i]
153 local fake = tracker[real]
154
155 isSurroundings[real] = true
156
157 if (not fake) then
158 fake = real:Clone()
159 fake:ClearAllChildren()
160 fake.Anchored = true
161 fake.Transparency = 1
162 fake.RotVelocity = Vector3.new(0, 0, 0)
163 fake.Velocity = Vector3.new(0, 0, 0)
164 fake.Parent = self.FakeWorld
165
166 tracker[real] = fake
167 end
168
169 if (fake) then
170 fake.CanCollide = real.CanCollide
171 fake.Transparency = TRANSPARENCY
172 fake.CFrame = baseCF * (partCFInv * real.CFrame)
173 end
174 end
175
176 for real, fake in next, tracker do
177 if (not isSurroundings[real]) then
178 fake.Transparency = 1
179 fake.CanCollide = false
180 end
181 end
182end
183
184local function rPartExtraAngle(self, normal)
185 if (self.Last.Hit and self.Last.Hit.Name == "rPart") then
186 local rCross, currProp
187 local hitCF, oldHitCF = self.Last.Hit.CFrame, self.Last.HitCF
188 for _, prop in next, CFVECTORS do
189 local midCross = hitCF[prop]:Cross(oldHitCF[prop])
190 if (not rCross or math.abs(normal:Dot(midCross)) > math.abs(normal:Dot(rCross))) then
191 currProp = prop
192 rCross = midCross
193 end
194 end
195
196 local dot = normal:Dot(UP)
197 rCross = math.sign(dot*rCross.y)*rCross
198
199 local rDot = hitCF[currProp]:Dot(oldHitCF[currProp])
200 local nDot = math.abs(normal:Dot(rCross))
201
202 if (rCross:Dot(rCross) > 0) then-- and rDot < 0.95 and nDot > 0.05) then
203 local axisCF = CFrame.new(ZERO, rCross)
204 local rPartLook = (axisCF:Inverse() * oldHitCF)[currProp]
205 local newLook = (axisCF:Inverse() * hitCF)[currProp]
206 local angDiff = math.atan2(newLook.x, newLook.y) - math.atan2(rPartLook.x, rPartLook.y)
207
208 if (math.abs(angDiff) > PI) then
209 angDiff = angDiff - math.sign(angDiff)*2*PI
210 end
211
212 self.Last.ExtraAngle = self.Last.ExtraAngle + angDiff
213 end
214 end
215end
216
217local function getSurfaceCFrame(self, normal)
218 local surfaceCF = CFrame.new()
219
220 local dot = UP:Dot(normal)
221 local cross = UP:Cross(normal)
222
223 if (cross:Dot(cross) > 0) then
224 cross = cross.Unit
225 surfaceCF = CFrame.fromAxisAngle(cross, -math.acos(dot))
226
227 local angDiff = math.atan2(cross.x, cross.z) - math.atan2(self.Last.Axis.x, self.Last.Axis.z)
228 local absDiff = math.abs(angDiff)
229 if (absDiff > PI) then
230 angDiff = angDiff - math.sign(angDiff)*2*PI
231 elseif (absDiff == math.clamp(absDiff, PI-EPSILON, PI+EPSILON)) then
232 angDiff = 0
233 end
234
235 self.Last.ExtraAngle = self.Last.ExtraAngle + angDiff * (1-dot)
236 self.Last.Axis = cross
237 elseif (dot < 0) then
238 surfaceCF = CFrame.fromAxisAngle(self.Last.Axis, -math.pi)
239 end
240
241 return surfaceCF
242end
243
244-- Public Constructors
245
246local Controller = {}
247Controller.__index = Controller
248
249function Controller.new(player)
250 local self = setmetatable({}, Controller)
251 local character, fakeCharacter, playerModule, newAnimate, newSound do
252 -- characters
253 character = player.Character or player.CharacterAdded:Wait()
254 if (not player:HasAppearanceLoaded() and not STARTER_PLAYER:FindFirstChild("StarterCharacter")) then
255 player.CharacterAppearanceLoaded:Wait()
256 end
257 fakeCharacter = cloneArchivable(character)
258 local realHum = character:WaitForChild("Humanoid")
259 local fakeHum = fakeCharacter:WaitForChild("Humanoid")
260 local isR15 = (realHum.RigType == Enum.HumanoidRigType.R15)
261
262 -- animation
263 newAnimate = (isR15 and animateR15 or animateR6):Clone()
264 local humObj = newAnimate:WaitForChild("Humanoid")
265
266 local realAnimate = character:WaitForChild("Animate")
267 local children = realAnimate:GetChildren()
268 for i = 1, #children do
269 children[i]:Clone().Parent = newAnimate
270 end
271
272 newAnimate.Parent = character
273 humObj.Value = fakeHum
274
275 -- sound
276 newSound = sound:Clone()
277 local humObj = newSound:WaitForChild("Humanoid")
278 humObj.Value = fakeHum
279
280 newSound.Parent = character:WaitForChild("Sound")
281
282 -- camera/controls
283 playerModule = require(player.PlayerScripts:WaitForChild("PlayerModule"))
284 end
285
286 self.Enabled = false
287 self.CustomCameraEnabled = true
288 self.CustomCameraSpinWithrParts = true
289
290 self.Ignore = character
291 self.Cast = castClass.new(self, character)
292
293 self.CameraModule = playerModule:GetCameras()
294 self.ControlModule = playerModule:GetControls()
295
296 self.Real = characterDict(character)
297 self.Fake = characterDict(fakeCharacter)
298 self.Animate = newAnimate
299 self.Sound = newSound
300 self.Forces = createForces(self.Real)
301
302 self.FakeWorld = Instance.new("Model", CAMERA)
303 self.FakeWorldCenter = CFrame.new(10000, 0, 0)
304 self.FakeWorldTracker = {}
305
306 self.NormalUpdateTick = tick()
307
308 self.Last = {
309 Hit = nil,
310 HitCF = CFrame.new(),
311 Axis = Vector3.new(1, 0, 0),
312 Normal = UP,
313 ObjectNormal = UP,
314 SurfaceCF = CFrame.new(),
315 Move = ZERO,
316
317 ExtraAngle = 0,
318 CameraDifference = CFrame.new()
319 }
320
321 hideFake(self.Fake)
322 game:GetService("UserInputService").JumpRequest:Connect(function() self.Fake.Humanoid.Jump = true end)
323 self.Real.Humanoid.Died:Connect(function() self:SetEnabled(false) end)
324 self.Fake.Humanoid.Died:Connect(function() self.Real.Humanoid.Health = 0 end)
325
326 for i = 1, #REPLICATED_HUMANOID_PROPERTIES do
327 local prop = REPLICATED_HUMANOID_PROPERTIES[i]
328 self.Real.Humanoid:GetPropertyChangedSignal(prop):Connect(function() self.Fake.Humanoid[prop] = self.Real.Humanoid[prop] end)
329 end
330
331 return self
332end
333
334-- Public Methods
335
336function Controller:SetEnabled(bool)
337 self.Forces.Attach1.CFrame = self.Real.HRP.CFrame
338 self.Forces.AlignPosition.Enabled = bool
339 self.Forces.AlignOrientation.Enabled = bool
340
341 self.Fake.HRP.CFrame = self.FakeWorldCenter * self.Real.HRP.CFrame
342 self.Fake.HRP.Anchored = not bool
343 self.Fake.Character.Parent = bool and CAMERA or nil
344
345 self.Real.Humanoid.PlatformStand = bool
346 self.Real.Humanoid.AutoRotate = not bool
347
348 self.Animate.Disabled = not bool
349 self.Sound.Disabled = not bool
350
351 self.Enabled = bool
352 if (not bool) then
353 self.FakeWorld:ClearAllChildren()
354 self.FakeWorldTracker = {}
355 end
356end
357
358function Controller:SetNormal(normal, part)
359 normal = part and normal or self.Last.Normal
360 normal = normal.Unit
361
362 -- if we're on a rPart we shouldn't update the normal unless it's changed relatively (else we get locked in place)
363 local shouldUpdate = (part and (normal ~= self.Last.Normal or part ~= self.Last.Hit))
364 if (part and part.Name == "rPart") then
365 local parentIsModel = (part.Parent.ClassName == "Model")
366 local rPartGroupNormal = (parentIsModel and part.Parent.Name == "rPartGroup" and part.Parent:FindFirstChild("GroupNormal"))
367 if (rPartGroupNormal or not parentIsModel) then
368 if (rPartGroupNormal) then
369 local groupNorm = rPartGroupNormal.Value
370 part = part.Parent.PrimaryPart
371 normal = part.CFrame:VectorToWorldSpace(groupNorm)
372 end
373
374 local partCF = part.CFrame
375 local n, ln = partCF:VectorToObjectSpace(normal), self.Last.ObjectNormal
376 shouldUpdate = (part ~= self.Last.Hit or n:Dot(ln) < 0.99)
377 else
378 shouldUpdate = false
379 end
380 end
381
382 -- move the character to the de-rotated world and store last variables
383 if (shouldUpdate) then
384 local hrp, humanoid = self.Real.HRP, self.Real.Humanoid
385
386 local surfaceCF = getSurfaceCFrame(self, normal)
387 if (self.CustomCameraSpinWithrParts) then
388 rPartExtraAngle(self, normal) -- bless goldenstein64 for this
389 end
390
391 local height = humanoid.HipHeight + hrp.Size.y/2
392 local pivot = part.CFrame:ToObjectSpace(hrp.CFrame * CFrame.new(0, -height, 0))
393
394 self.Fake.HRP.CFrame = self.FakeWorldCenter * surfaceCF * (part.CFrame - part.CFrame.p) * pivot * CFrame.new(0, height, 0)
395
396 self.Last.Hit = part
397 self.Last.HitCF = part.CFrame
398 self.Last.Normal = normal:Dot(normal) > 0 and normal or UP
399 self.Last.ObjectNormal = part.CFrame:VectorToObjectSpace(self.Last.Normal)
400 self.Last.SurfaceCF = surfaceCF
401
402 self.NormalUpdateTick = tick()
403 end
404end
405
406function Controller:Update(dt)
407 if (self.Enabled) then
408 updateFakeWorld(self, self.Last.Hit or TERRAIN)
409
410 -- update camera
411 local diff = self.Last.CameraDifference
412 local extra = CFrame.Angles(0, self.Last.ExtraAngle, 0)
413
414 local hitCF = self.Last.Hit.CFrame - self.Last.Hit.CFrame.p
415 local hitCFOld = self.Last.HitCF - self.Last.HitCF.p
416
417 if (self.CustomCameraEnabled) then
418 local fakeCF = self.Last.SurfaceCF * (self.CustomCameraSpinWithrParts and hitCFOld or hitCF)
419 diff = hitCF * fakeCF:Inverse() * extra
420 diff = self.Last.CameraDifference:Lerp(diff, 0.1*60*dt)
421 end
422 self.Last.CameraDifference = diff
423
424 local oldCamCF = CAMERA.CFrame
425 local focusCF = CFrame.new(self.CameraModule.activeCameraController:GetSubjectPosition())
426 local camCF = focusCF * diff * focusCF:ToObjectSpace(CAMERA.CFrame)
427 if (self.CameraModule.activeOcclusionModule) then
428 camCF, focusCF = self.CameraModule.activeOcclusionModule:Update(dt, camCF, focusCF)
429 end
430
431 CAMERA.CFrame = camCF
432 CAMERA.Focus = focusCF
433
434 if (self.CameraModule.activeTransparencyController) then
435 self.CameraModule.activeTransparencyController:Update()
436 end
437
438 -- update movement
439
440 local adjustedCamCF = self.Last.SurfaceCF * hitCFOld * hitCF:ToObjectSpace(camCF)
441 local move = self.ControlModule:GetMoveVector()
442 move = adjustedCamCF:VectorToWorldSpace(move) * HORIZONTAL
443 move = move:Dot(move) > 0 and move.Unit or ZERO
444
445 self.Fake.Humanoid:Move(move, false)
446
447 if (self.Last.Hit) then
448 local hitRot = self.Last.HitCF - self.Last.HitCF.p
449 local offset = self.FakeWorldCenter * self.Last.SurfaceCF * hitRot
450 self.Forces.Attach1.CFrame = self.Last.Hit.CFrame * (offset:Inverse() * self.Fake.HRP.CFrame)
451 end
452
453 -- check we haven't fallen too far
454 if ((self.Fake.HRP.CFrame.p - self.FakeWorldCenter.p).y < game.Workspace.FallenPartsDestroyHeight) then
455 self.Fake.Humanoid.Health = 0
456 end
457 else
458 -- if disabled update the camera as usual
459 local camCF, focusCF = CAMERA.CFrame, CAMERA.Focus
460 if (self.CameraModule.activeOcclusionModule) then
461 camCF, focusCF = self.CameraModule.activeOcclusionModule:Update(dt, camCF, focusCF)
462 end
463 if (self.CameraModule.activeTransparencyController) then
464 self.CameraModule.activeTransparencyController:Update()
465 end
466 CAMERA.CFrame = camCF
467 CAMERA.Focus = focusCF
468 end
469
470 debugger.step()
471end
472
473--
474
475return Controller
476
477local Character = script.Parent
478local Humanoid = Character:WaitForChild("Humanoid")
479local pose = "Standing"
480
481local userNoUpdateOnLoopSuccess, userNoUpdateOnLoopValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoUpdateOnLoop") end)
482local userNoUpdateOnLoop = userNoUpdateOnLoopSuccess and userNoUpdateOnLoopValue
483local userAnimationSpeedDampeningSuccess, userAnimationSpeedDampeningValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimationSpeedDampening") end)
484local userAnimationSpeedDampening = userAnimationSpeedDampeningSuccess and userAnimationSpeedDampeningValue
485
486local adjustHumanoidRootPartFlagExists, adjustHumanoidRootPartFlagEnabled = pcall(function()
487 return UserSettings():IsUserFeatureEnabled("UserAdjustHumanoidRootPartToHipPosition")
488end)
489local FFlagUserAdjustHumanoidRootPartToHipPosition = adjustHumanoidRootPartFlagExists and adjustHumanoidRootPartFlagEnabled
490
491local animateScriptEmoteHookFlagExists, animateScriptEmoteHookFlagEnabled = pcall(function()
492 return UserSettings():IsUserFeatureEnabled("UserAnimateScriptEmoteHook")
493end)
494local FFlagAnimateScriptEmoteHook = animateScriptEmoteHookFlagExists and animateScriptEmoteHookFlagEnabled
495
496local AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent")
497local HumanoidHipHeight = FFlagUserAdjustHumanoidRootPartToHipPosition and 2 or 1.35
498
499local EMOTE_TRANSITION_TIME = 0.1
500
501local currentAnim = ""
502local currentAnimInstance = nil
503local currentAnimTrack = nil
504local currentAnimKeyframeHandler = nil
505local currentAnimSpeed = 1.0
506
507local runAnimTrack = nil
508local runAnimKeyframeHandler = nil
509
510local animTable = {}
511local animNames = {
512 idle = {
513 { id = "http://www.roblox.com/asset/?id=507766666", weight = 1 },
514 { id = "http://www.roblox.com/asset/?id=507766951", weight = 1 },
515 { id = "http://www.roblox.com/asset/?id=507766388", weight = 9 }
516 },
517 walk = {
518 { id = "http://www.roblox.com/asset/?id=507777826", weight = 10 }
519 },
520 run = {
521 { id = "http://www.roblox.com/asset/?id=507767714", weight = 10 }
522 },
523 swim = {
524 { id = "http://www.roblox.com/asset/?id=507784897", weight = 10 }
525 },
526 swimidle = {
527 { id = "http://www.roblox.com/asset/?id=507785072", weight = 10 }
528 },
529 jump = {
530 { id = "http://www.roblox.com/asset/?id=507765000", weight = 10 }
531 },
532 fall = {
533 { id = "http://www.roblox.com/asset/?id=507767968", weight = 10 }
534 },
535 climb = {
536 { id = "http://www.roblox.com/asset/?id=507765644", weight = 10 }
537 },
538 sit = {
539 { id = "http://www.roblox.com/asset/?id=2506281703", weight = 10 }
540 },
541 toolnone = {
542 { id = "http://www.roblox.com/asset/?id=507768375", weight = 10 }
543 },
544 toolslash = {
545 { id = "http://www.roblox.com/asset/?id=522635514", weight = 10 }
546 },
547 toollunge = {
548 { id = "http://www.roblox.com/asset/?id=522638767", weight = 10 }
549 },
550 wave = {
551 { id = "http://www.roblox.com/asset/?id=507770239", weight = 10 }
552 },
553 point = {
554 { id = "http://www.roblox.com/asset/?id=507770453", weight = 10 }
555 },
556 dance = {
557 { id = "http://www.roblox.com/asset/?id=507771019", weight = 10 },
558 { id = "http://www.roblox.com/asset/?id=507771955", weight = 10 },
559 { id = "http://www.roblox.com/asset/?id=507772104", weight = 10 }
560 },
561 dance2 = {
562 { id = "http://www.roblox.com/asset/?id=507776043", weight = 10 },
563 { id = "http://www.roblox.com/asset/?id=507776720", weight = 10 },
564 { id = "http://www.roblox.com/asset/?id=507776879", weight = 10 }
565 },
566 dance3 = {
567 { id = "http://www.roblox.com/asset/?id=507777268", weight = 10 },
568 { id = "http://www.roblox.com/asset/?id=507777451", weight = 10 },
569 { id = "http://www.roblox.com/asset/?id=507777623", weight = 10 }
570 },
571 laugh = {
572 { id = "http://www.roblox.com/asset/?id=507770818", weight = 10 }
573 },
574 cheer = {
575 { id = "http://www.roblox.com/asset/?id=507770677", weight = 10 }
576 },
577}
578
579-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote
580local emoteNames = { wave = false, point = false, dance = true, dance2 = true, dance3 = true, laugh = false, cheer = false}
581
582local PreloadAnimsUserFlag = false
583local PreloadedAnims = {}
584local successPreloadAnim, msgPreloadAnim = pcall(function()
585 PreloadAnimsUserFlag = UserSettings():IsUserFeatureEnabled("UserPreloadAnimations")
586end)
587if not successPreloadAnim then
588 PreloadAnimsUserFlag = false
589end
590
591math.randomseed(tick())
592
593function findExistingAnimationInSet(set, anim)
594 if set == nil or anim == nil then
595 return 0
596 end
597
598 for idx = 1, set.count, 1 do
599 if set[idx].anim.AnimationId == anim.AnimationId then
600 return idx
601 end
602 end
603
604 return 0
605end
606
607function configureAnimationSet(name, fileList)
608 if (animTable[name] ~= nil) then
609 for _, connection in pairs(animTable[name].connections) do
610 connection:disconnect()
611 end
612 end
613 animTable[name] = {}
614 animTable[name].count = 0
615 animTable[name].totalWeight = 0
616 animTable[name].connections = {}
617
618 local allowCustomAnimations = true
619 local AllowDisableCustomAnimsUserFlag = false
620
621 local success, msg = pcall(function()
622 AllowDisableCustomAnimsUserFlag = UserSettings():IsUserFeatureEnabled("UserAllowDisableCustomAnims2")
623 end)
624
625 if (AllowDisableCustomAnimsUserFlag) then
626 local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end)
627 if not success then
628 allowCustomAnimations = true
629 end
630 end
631
632 -- check for config values
633 local config = script:FindFirstChild(name)
634 if (allowCustomAnimations and config ~= nil) then
635 table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
636 table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))
637
638 local idx = 0
639 for _, childPart in pairs(config:GetChildren()) do
640 if (childPart:IsA("Animation")) then
641 local newWeight = 1
642 local weightObject = childPart:FindFirstChild("Weight")
643 if (weightObject ~= nil) then
644 newWeight = weightObject.Value
645 end
646 animTable[name].count = animTable[name].count + 1
647 idx = animTable[name].count
648 animTable[name][idx] = {}
649 animTable[name][idx].anim = childPart
650 animTable[name][idx].weight = newWeight
651 animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
652 table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
653 table.insert(animTable[name].connections, childPart.ChildAdded:connect(function(property) configureAnimationSet(name, fileList) end))
654 table.insert(animTable[name].connections, childPart.ChildRemoved:connect(function(property) configureAnimationSet(name, fileList) end))
655 end
656 end
657 end
658
659 -- fallback to defaults
660 if (animTable[name].count <= 0) then
661 for idx, anim in pairs(fileList) do
662 animTable[name][idx] = {}
663 animTable[name][idx].anim = Instance.new("Animation")
664 animTable[name][idx].anim.Name = name
665 animTable[name][idx].anim.AnimationId = anim.id
666 animTable[name][idx].weight = anim.weight
667 animTable[name].count = animTable[name].count + 1
668 animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
669 end
670 end
671
672 -- preload anims
673 if PreloadAnimsUserFlag then
674 for i, animType in pairs(animTable) do
675 for idx = 1, animType.count, 1 do
676 if PreloadedAnims[animType[idx].anim.AnimationId] == nil then
677 Humanoid:LoadAnimation(animType[idx].anim)
678 PreloadedAnims[animType[idx].anim.AnimationId] = true
679 end
680 end
681 end
682 end
683end
684
685------------------------------------------------------------------------------------------------------------
686
687function configureAnimationSetOld(name, fileList)
688 if (animTable[name] ~= nil) then
689 for _, connection in pairs(animTable[name].connections) do
690 connection:disconnect()
691 end
692 end
693 animTable[name] = {}
694 animTable[name].count = 0
695 animTable[name].totalWeight = 0
696 animTable[name].connections = {}
697
698 local allowCustomAnimations = true
699 local AllowDisableCustomAnimsUserFlag = false
700
701 local success, msg = pcall(function()
702 AllowDisableCustomAnimsUserFlag = UserSettings():IsUserFeatureEnabled("UserAllowDisableCustomAnims2")
703 end)
704
705 if (AllowDisableCustomAnimsUserFlag) then
706 local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end)
707 if not success then
708 allowCustomAnimations = true
709 end
710 end
711
712 -- check for config values
713 local config = script:FindFirstChild(name)
714 if (allowCustomAnimations and config ~= nil) then
715 table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end))
716 table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end))
717 local idx = 1
718 for _, childPart in pairs(config:GetChildren()) do
719 if (childPart:IsA("Animation")) then
720 table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end))
721 animTable[name][idx] = {}
722 animTable[name][idx].anim = childPart
723 local weightObject = childPart:FindFirstChild("Weight")
724 if (weightObject == nil) then
725 animTable[name][idx].weight = 1
726 else
727 animTable[name][idx].weight = weightObject.Value
728 end
729 animTable[name].count = animTable[name].count + 1
730 animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight
731 idx = idx + 1
732 end
733 end
734 end
735
736 -- fallback to defaults
737 if (animTable[name].count <= 0) then
738 for idx, anim in pairs(fileList) do
739 animTable[name][idx] = {}
740 animTable[name][idx].anim = Instance.new("Animation")
741 animTable[name][idx].anim.Name = name
742 animTable[name][idx].anim.AnimationId = anim.id
743 animTable[name][idx].weight = anim.weight
744 animTable[name].count = animTable[name].count + 1
745 animTable[name].totalWeight = animTable[name].totalWeight + anim.weight
746 -- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")")
747 end
748 end
749
750 -- preload anims
751 if PreloadAnimsUserFlag then
752 for i, animType in pairs(animTable) do
753 for idx = 1, animType.count, 1 do
754 Humanoid:LoadAnimation(animType[idx].anim)
755 end
756 end
757 end
758end
759
760-- Setup animation objects
761function scriptChildModified(child)
762 local fileList = animNames[child.Name]
763 if (fileList ~= nil) then
764 configureAnimationSet(child.Name, fileList)
765 end
766end
767
768script.ChildAdded:connect(scriptChildModified)
769script.ChildRemoved:connect(scriptChildModified)
770
771
772for name, fileList in pairs(animNames) do
773 configureAnimationSet(name, fileList)
774end
775
776-- ANIMATION
777
778-- declarations
779local toolAnim = "None"
780local toolAnimTime = 0
781
782local jumpAnimTime = 0
783local jumpAnimDuration = 0.31
784
785local toolTransitionTime = 0.1
786local fallTransitionTime = 0.2
787
788local currentlyPlayingEmote = false
789
790-- functions
791
792function stopAllAnimations()
793 local oldAnim = currentAnim
794
795 -- return to idle if finishing an emote
796 if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then
797 oldAnim = "idle"
798 end
799
800 if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then
801 oldAnim = "idle"
802 currentlyPlayingEmote = false
803 end
804
805 currentAnim = ""
806 currentAnimInstance = nil
807 if (currentAnimKeyframeHandler ~= nil) then
808 currentAnimKeyframeHandler:disconnect()
809 end
810
811 if (currentAnimTrack ~= nil) then
812 currentAnimTrack:Stop()
813 currentAnimTrack:Destroy()
814 currentAnimTrack = nil
815 end
816
817 -- clean up walk if there is one
818 if (runAnimKeyframeHandler ~= nil) then
819 runAnimKeyframeHandler:disconnect()
820 end
821
822 if (runAnimTrack ~= nil) then
823 runAnimTrack:Stop()
824 runAnimTrack:Destroy()
825 runAnimTrack = nil
826 end
827
828 return oldAnim
829end
830
831function getHeightScale()
832 if Humanoid then
833 if FFlagUserAdjustHumanoidRootPartToHipPosition then
834 if not Humanoid.AutomaticScalingEnabled then
835 return 1
836 end
837 end
838
839 local scale = Humanoid.HipHeight / HumanoidHipHeight
840 if userAnimationSpeedDampening then
841 if AnimationSpeedDampeningObject == nil then
842 AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent")
843 end
844 if AnimationSpeedDampeningObject ~= nil then
845 scale = 1 + (Humanoid.HipHeight - HumanoidHipHeight) * AnimationSpeedDampeningObject.Value / HumanoidHipHeight
846 end
847 end
848 return scale
849 end
850 return 1
851end
852
853local smallButNotZero = 0.0001
854function setRunSpeed(speed)
855 local speedScaled = speed * 1.25
856 local heightScale = getHeightScale()
857 local runSpeed = speedScaled / heightScale
858
859 if runSpeed ~= currentAnimSpeed then
860 if runSpeed < 0.33 then
861 currentAnimTrack:AdjustWeight(1.0)
862 runAnimTrack:AdjustWeight(smallButNotZero)
863 elseif runSpeed < 0.66 then
864 local weight = ((runSpeed - 0.33) / 0.33)
865 currentAnimTrack:AdjustWeight(1.0 - weight + smallButNotZero)
866 runAnimTrack:AdjustWeight(weight + smallButNotZero)
867 else
868 currentAnimTrack:AdjustWeight(smallButNotZero)
869 runAnimTrack:AdjustWeight(1.0)
870 end
871 currentAnimSpeed = runSpeed
872 runAnimTrack:AdjustSpeed(runSpeed)
873 currentAnimTrack:AdjustSpeed(runSpeed)
874 end
875end
876
877function setAnimationSpeed(speed)
878 if currentAnim == "walk" then
879 setRunSpeed(speed)
880 else
881 if speed ~= currentAnimSpeed then
882 currentAnimSpeed = speed
883 currentAnimTrack:AdjustSpeed(currentAnimSpeed)
884 end
885 end
886end
887
888function keyFrameReachedFunc(frameName)
889 if (frameName == "End") then
890 if currentAnim == "walk" then
891 if userNoUpdateOnLoop == true then
892 if runAnimTrack.Looped ~= true then
893 runAnimTrack.TimePosition = 0.0
894 end
895 if currentAnimTrack.Looped ~= true then
896 currentAnimTrack.TimePosition = 0.0
897 end
898 else
899 runAnimTrack.TimePosition = 0.0
900 currentAnimTrack.TimePosition = 0.0
901 end
902 else
903 local repeatAnim = currentAnim
904 -- return to idle if finishing an emote
905 if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then
906 repeatAnim = "idle"
907 end
908
909 if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then
910 if currentAnimTrack.Looped then
911 -- Allow the emote to loop
912 return
913 end
914
915 repeatAnim = "idle"
916 currentlyPlayingEmote = false
917 end
918
919 local animSpeed = currentAnimSpeed
920 playAnimation(repeatAnim, 0.15, Humanoid)
921 setAnimationSpeed(animSpeed)
922 end
923 end
924end
925
926function rollAnimation(animName)
927 local roll = math.random(1, animTable[animName].totalWeight)
928 local origRoll = roll
929 local idx = 1
930 while (roll > animTable[animName][idx].weight) do
931 roll = roll - animTable[animName][idx].weight
932 idx = idx + 1
933 end
934 return idx
935end
936
937local function switchToAnim(anim, animName, transitionTime, humanoid)
938 -- switch animation
939 if (anim ~= currentAnimInstance) then
940
941 if (currentAnimTrack ~= nil) then
942 currentAnimTrack:Stop(transitionTime)
943 currentAnimTrack:Destroy()
944 end
945
946 if (runAnimTrack ~= nil) then
947 runAnimTrack:Stop(transitionTime)
948 runAnimTrack:Destroy()
949 if userNoUpdateOnLoop == true then
950 runAnimTrack = nil
951 end
952 end
953
954 currentAnimSpeed = 1.0
955
956 -- load it to the humanoid; get AnimationTrack
957 currentAnimTrack = humanoid:LoadAnimation(anim)
958 currentAnimTrack.Priority = Enum.AnimationPriority.Core
959
960 -- play the animation
961 currentAnimTrack:Play(transitionTime)
962 currentAnim = animName
963 currentAnimInstance = anim
964
965 -- set up keyframe name triggers
966 if (currentAnimKeyframeHandler ~= nil) then
967 currentAnimKeyframeHandler:disconnect()
968 end
969 currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)
970
971 -- check to see if we need to blend a walk/run animation
972 if animName == "walk" then
973 local runAnimName = "run"
974 local runIdx = rollAnimation(runAnimName)
975
976 runAnimTrack = humanoid:LoadAnimation(animTable[runAnimName][runIdx].anim)
977 runAnimTrack.Priority = Enum.AnimationPriority.Core
978 runAnimTrack:Play(transitionTime)
979
980 if (runAnimKeyframeHandler ~= nil) then
981 runAnimKeyframeHandler:disconnect()
982 end
983 runAnimKeyframeHandler = runAnimTrack.KeyframeReached:connect(keyFrameReachedFunc)
984 end
985 end
986end
987
988function playAnimation(animName, transitionTime, humanoid)
989 local idx = rollAnimation(animName)
990 local anim = animTable[animName][idx].anim
991
992 switchToAnim(anim, animName, transitionTime, humanoid)
993end
994
995function playEmote(emoteAnim, transitionTime, humanoid)
996 switchToAnim(emoteAnim, emoteAnim.Name, transitionTime, humanoid)
997 currentlyPlayingEmote = true
998end
999
1000-------------------------------------------------------------------------------------------
1001-------------------------------------------------------------------------------------------
1002
1003local toolAnimName = ""
1004local toolAnimTrack = nil
1005local toolAnimInstance = nil
1006local currentToolAnimKeyframeHandler = nil
1007
1008function toolKeyFrameReachedFunc(frameName)
1009 if (frameName == "End") then
1010 playToolAnimation(toolAnimName, 0.0, Humanoid)
1011 end
1012end
1013
1014
1015function playToolAnimation(animName, transitionTime, humanoid, priority)
1016 local idx = rollAnimation(animName)
1017 local anim = animTable[animName][idx].anim
1018
1019 if (toolAnimInstance ~= anim) then
1020
1021 if (toolAnimTrack ~= nil) then
1022 toolAnimTrack:Stop()
1023 toolAnimTrack:Destroy()
1024 transitionTime = 0
1025 end
1026
1027 -- load it to the humanoid; get AnimationTrack
1028 toolAnimTrack = humanoid:LoadAnimation(anim)
1029 if priority then
1030 toolAnimTrack.Priority = priority
1031 end
1032
1033 -- play the animation
1034 toolAnimTrack:Play(transitionTime)
1035 toolAnimName = animName
1036 toolAnimInstance = anim
1037
1038 currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc)
1039 end
1040end
1041
1042function stopToolAnimations()
1043 local oldAnim = toolAnimName
1044
1045 if (currentToolAnimKeyframeHandler ~= nil) then
1046 currentToolAnimKeyframeHandler:disconnect()
1047 end
1048
1049 toolAnimName = ""
1050 toolAnimInstance = nil
1051 if (toolAnimTrack ~= nil) then
1052 toolAnimTrack:Stop()
1053 toolAnimTrack:Destroy()
1054 toolAnimTrack = nil
1055 end
1056
1057 return oldAnim
1058end
1059
1060-------------------------------------------------------------------------------------------
1061-------------------------------------------------------------------------------------------
1062-- STATE CHANGE HANDLERS
1063
1064function onRunning(speed)
1065 if speed > 0.5 then
1066 local scale = 16.0
1067 playAnimation("walk", 0.2, Humanoid)
1068 setAnimationSpeed(speed / scale)
1069 pose = "Running"
1070 else
1071 if emoteNames[currentAnim] == nil then
1072 playAnimation("idle", 0.2, Humanoid)
1073 pose = "Standing"
1074 end
1075 end
1076end
1077
1078function onDied()
1079 pose = "Dead"
1080end
1081
1082function onJumping()
1083 playAnimation("jump", 0.1, Humanoid)
1084 jumpAnimTime = jumpAnimDuration
1085 pose = "Jumping"
1086end
1087
1088function onClimbing(speed)
1089 local scale = 5.0
1090 playAnimation("climb", 0.1, Humanoid)
1091 setAnimationSpeed(speed / scale)
1092 pose = "Climbing"
1093end
1094
1095function onGettingUp()
1096 pose = "GettingUp"
1097end
1098
1099function onFreeFall()
1100 if (jumpAnimTime <= 0) then
1101 playAnimation("fall", fallTransitionTime, Humanoid)
1102 end
1103 pose = "FreeFall"
1104end
1105
1106function onFallingDown()
1107 pose = "FallingDown"
1108end
1109
1110function onSeated()
1111 pose = "Seated"
1112end
1113
1114function onPlatformStanding()
1115 pose = "PlatformStanding"
1116end
1117
1118-------------------------------------------------------------------------------------------
1119-------------------------------------------------------------------------------------------
1120
1121function onSwimming(speed)
1122 if speed > 1.00 then
1123 local scale = 10.0
1124 playAnimation("swim", 0.4, Humanoid)
1125 setAnimationSpeed(speed / scale)
1126 pose = "Swimming"
1127 else
1128 playAnimation("swimidle", 0.4, Humanoid)
1129 pose = "Standing"
1130 end
1131end
1132
1133function animateTool()
1134 if (toolAnim == "None") then
1135 playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle)
1136 return
1137 end
1138
1139 if (toolAnim == "Slash") then
1140 playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action)
1141 return
1142 end
1143
1144 if (toolAnim == "Lunge") then
1145 playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action)
1146 return
1147 end
1148end
1149
1150function getToolAnim(tool)
1151 for _, c in ipairs(tool:GetChildren()) do
1152 if c.Name == "toolanim" and c.className == "StringValue" then
1153 return c
1154 end
1155 end
1156 return nil
1157end
1158
1159local lastTick = 0
1160
1161function stepAnimate(currentTime)
1162 local amplitude = 1
1163 local frequency = 1
1164 local deltaTime = currentTime - lastTick
1165 lastTick = currentTime
1166
1167 local climbFudge = 0
1168 local setAngles = false
1169
1170 if (jumpAnimTime > 0) then
1171 jumpAnimTime = jumpAnimTime - deltaTime
1172 end
1173
1174 if (pose == "FreeFall" and jumpAnimTime <= 0) then
1175 playAnimation("fall", fallTransitionTime, Humanoid)
1176 elseif (pose == "Seated") then
1177 playAnimation("sit", 0.5, Humanoid)
1178 return
1179 elseif (pose == "Running") then
1180 playAnimation("walk", 0.2, Humanoid)
1181 elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then
1182 stopAllAnimations()
1183 amplitude = 0.1
1184 frequency = 1
1185 setAngles = true
1186 end
1187
1188 -- Tool Animation handling
1189 local tool = Character:FindFirstChildOfClass("Tool")
1190 if tool and tool:FindFirstChild("Handle") then
1191 local animStringValueObject = getToolAnim(tool)
1192
1193 if animStringValueObject then
1194 toolAnim = animStringValueObject.Value
1195 -- message recieved, delete StringValue
1196 animStringValueObject.Parent = nil
1197 toolAnimTime = currentTime + .3
1198 end
1199
1200 if currentTime > toolAnimTime then
1201 toolAnimTime = 0
1202 toolAnim = "None"
1203 end
1204
1205 animateTool()
1206 else
1207 stopToolAnimations()
1208 toolAnim = "None"
1209 toolAnimInstance = nil
1210 toolAnimTime = 0
1211 end
1212end
1213
1214-- connect events
1215
1216local conHumanoid = script:WaitForChild("Humanoid").Value
1217conHumanoid.Died:connect(onDied)
1218conHumanoid.Running:connect(onRunning)
1219conHumanoid.Jumping:connect(onJumping)
1220conHumanoid.Climbing:connect(onClimbing)
1221conHumanoid.GettingUp:connect(onGettingUp)
1222conHumanoid.FreeFalling:connect(onFreeFall)
1223conHumanoid.FallingDown:connect(onFallingDown)
1224conHumanoid.Seated:connect(onSeated)
1225conHumanoid.PlatformStanding:connect(onPlatformStanding)
1226conHumanoid.Swimming:connect(onSwimming)
1227
1228-- setup emote chat hook
1229game:GetService("Players").LocalPlayer.Chatted:connect(function(msg)
1230 local emote = ""
1231 if (string.sub(msg, 1, 3) == "/e ") then
1232 emote = string.sub(msg, 4)
1233 elseif (string.sub(msg, 1, 7) == "/emote ") then
1234 emote = string.sub(msg, 8)
1235 end
1236
1237 if (pose == "Standing" and emoteNames[emote] ~= nil) then
1238 playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)
1239 end
1240end)
1241
1242-- emote bindable hook
1243if FFlagAnimateScriptEmoteHook then
1244 script:WaitForChild("PlayEmote").OnInvoke = function(emote)
1245 -- Only play emotes when idling
1246 if pose ~= "Standing" then
1247 return
1248 end
1249
1250 if emoteNames[emote] ~= nil then
1251 -- Default emotes
1252 playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid)
1253
1254 return true
1255 elseif typeof(emote) == "Instance" and emote:IsA("Animation") then
1256 -- Non-default emotes
1257 playEmote(emote, EMOTE_TRANSITION_TIME, Humanoid)
1258 return true
1259 end
1260
1261 -- Return false to indicate that the emote could not be played
1262 return false
1263 end
1264end
1265
1266-- initialize to idle
1267playAnimation("idle", 0.1, Humanoid)
1268pose = "Standing"
1269
1270-- loop to handle timed state transitions and tool animations
1271while Character.Parent ~= nil do
1272 local _, currentGameTime = wait(0.1)
1273 stepAnimate(currentGameTime)
1274end
1275
1276local debugger = require(script.Parent:WaitForChild("Debugger"))
1277
1278local PI = math.pi
1279local PHI = (1 + math.sqrt(5)) / 2;
1280local UP = Vector3.new(0, 1, 0)
1281local CAST_COUNT = 32
1282local EPSILON = 1E-4
1283
1284-- private functions
1285
1286local function sunflower(n, alpha)
1287 local i, phi2 = 1, PHI*PHI
1288 local b = math.ceil(alpha*math.sqrt(n))
1289 return function()
1290 if (i <= n) then
1291 i = i + 1
1292 local r = i > (n - b) and 1 or math.sqrt(i - 0.5)/math.sqrt(n - (b + 1)/2)
1293 local t = 2*PI*i/(phi2)
1294 return r*math.cos(t), r*math.sin(t)
1295 end
1296 return
1297 end
1298end
1299
1300local function icosphere(n)
1301 local verts = {
1302 Vector3.new(-1, PHI, 0),
1303 Vector3.new(1, PHI, 0),
1304 Vector3.new(-1, -PHI, 0),
1305 Vector3.new(1, -PHI, 0),
1306
1307 Vector3.new(0, -1, PHI),
1308 Vector3.new(0, 1, PHI),
1309 Vector3.new(0, -1, -PHI),
1310 Vector3.new(0, 1, -PHI),
1311
1312 Vector3.new(PHI, 0, -1),
1313 Vector3.new(PHI, 0, 1),
1314 Vector3.new(-PHI, 0, -1),
1315 Vector3.new(-PHI, 0, 1)
1316 }
1317
1318 local indices = {
1319 1, 12, 6,
1320 1, 6, 2,
1321 1, 2, 8,
1322 1, 8, 11,
1323 1, 11, 12,
1324
1325 2, 6, 10,
1326 6, 12, 5,
1327 12, 11, 3,
1328 11, 8, 7,
1329 8, 2, 9,
1330
1331 4, 10, 5,
1332 4, 5, 3,
1333 4, 3, 7,
1334 4, 7, 9,
1335 4, 9, 10,
1336
1337 5, 10, 6,
1338 3, 5, 12,
1339 7, 3, 11,
1340 9, 7, 8,
1341 10, 9, 2
1342 }
1343
1344 local splits = {}
1345
1346 local function split(i, j)
1347 local key = i < j and (i .. "," .. j) or (j .. "," .. i)
1348
1349 if (not splits[key]) then
1350 verts[#verts+1] = (verts[i] + verts[j]) / 2
1351 splits[key] = #verts
1352 end
1353
1354 return splits[key]
1355 end
1356
1357 for _ = 1, n do
1358 for i = #indices, 1, -3 do
1359 local v1, v2, v3 = indices[i - 2], indices[i - 1], indices[i]
1360 local a = split(v1, v2)
1361 local b = split(v2, v3)
1362 local c = split(v3, v1)
1363
1364 indices[#indices+1] = v1
1365 indices[#indices+1] = a
1366 indices[#indices+1] = c
1367
1368 indices[#indices+1] = v2
1369 indices[#indices+1] = b
1370 indices[#indices+1] = a
1371
1372 indices[#indices+1] = v3
1373 indices[#indices+1] = c
1374 indices[#indices+1] = b
1375
1376 indices[#indices+1] = a
1377 indices[#indices+1] = b
1378 indices[#indices+1] = c
1379
1380 table.remove(indices, i)
1381 table.remove(indices, i - 1)
1382 table.remove(indices, i - 2)
1383 end
1384 end
1385
1386 -- normalize
1387 for i = 1, #verts do
1388 verts[i] = verts[i].unit
1389 end
1390
1391 return verts
1392end
1393
1394-- Public Constructors
1395
1396local ICOSPHERE = icosphere(1)
1397
1398local Cast = {}
1399Cast.__index = Cast
1400
1401function Cast.new(controller, ignore)
1402 local self = setmetatable({}, Cast)
1403
1404 self.Controller = controller
1405 self.Ignore = ignore
1406
1407 return self
1408end
1409
1410-- Public Methods
1411
1412function Cast:Line(modifier)
1413 modifier = modifier or 0
1414 local char, hum, hrp = self.Controller.Real.Character, self.Controller.Real.Humanoid, self.Controller.Real.HRP
1415 local hipHeight = (hum.RigType == Enum.HumanoidRigType.R15) and hum.HipHeight or 2
1416
1417 local down = -hrp.CFrame.UpVector
1418 local origin = hrp.Position
1419 local height = (hipHeight + hrp.Size.y/2) + modifier
1420
1421 local ray = Ray.new(origin, down * (height + 1))
1422 local hit, pos, normal = game.Workspace:FindPartOnRay(ray, self.Ignore)
1423 local valid = (pos - origin).Magnitude - 0.1 <= height
1424
1425 debugger.point(origin + down * (height + 1), Color3.new(1, 0, 1))
1426
1427 if (valid) then
1428 normal = (normal ~= normal) and UP or normal
1429 return hit, pos, normal
1430 end
1431 return nil, pos, UP
1432end
1433
1434function Cast:Ball(modifier)
1435 modifier = modifier or 0
1436 local char, hum, hrp = self.Controller.Real.Character, self.Controller.Real.Humanoid, self.Controller.Real.HRP
1437 local hipHeight = (hum.RigType == Enum.HumanoidRigType.R15) and hum.HipHeight or 2
1438
1439 local down = -hrp.CFrame.UpVector
1440 local origin = hrp.Position
1441 local radius = (hipHeight + hrp.Size.y) + modifier
1442 local height = (hipHeight + hrp.Size.y/2) + modifier
1443
1444 local count = 0
1445 local ray = Ray.new(origin, down * radius)
1446 local hit, pos, baseNormal = game.Workspace:FindPartOnRay(ray, self.Ignore)
1447 local normal = Vector3.new(0, 0, 0)
1448
1449 hit = (pos - origin).Magnitude - 0.1 < height and hit or nil
1450
1451 for i = 1, #ICOSPHERE do
1452 local v = ICOSPHERE[i]
1453 if (v:Dot(down) > EPSILON) then
1454 local ray = Ray.new(origin, v * radius)
1455 local hit2, pos2, normal2 = game.Workspace:FindPartOnRay(ray, self.Ignore)
1456
1457 if (hit2) then
1458 count = count + 1
1459 normal = normal + normal2
1460 end
1461
1462 debugger.point(origin + v*radius, Color3.new(1, 0, 1))
1463 end
1464 end
1465
1466 return hit, pos, (count > 0) and normal / count or baseNormal
1467end
1468
1469function Cast:Cylinder(radius, modifier)
1470 modifier = modifier or 0
1471 radius = radius or 2
1472 local char, hum, hrp = self.Controller.Real.Character, self.Controller.Real.Humanoid, self.Controller.Real.HRP
1473 local hipHeight = (hum.RigType == Enum.HumanoidRigType.R15) and hum.HipHeight or 2
1474
1475 local origin = hrp.CFrame
1476 local down = -(hipHeight + hrp.Size.y) * origin.UpVector
1477 local height = (hipHeight + hrp.Size.y/2) + modifier
1478
1479 local count = 0
1480 local ray = Ray.new(origin.p, down)
1481 local hit, pos, baseNormal = game.Workspace:FindPartOnRay(ray, self.Ignore)
1482 local normal = Vector3.new(0, 0, 0)
1483
1484 hit = (pos - origin.p).Magnitude - 0.1 < height and hit or nil
1485
1486 for x, z in sunflower(CAST_COUNT, 2) do
1487 local start = origin * Vector3.new(x*radius, 0, z*radius)
1488
1489 local ray = Ray.new(start, down)
1490 local hit2, pos2, normal2 = game.Workspace:FindPartOnRay(ray, self.Ignore)
1491 local dist = (pos2 - origin.p).Magnitude
1492
1493 if (hit2 and dist - 0.1 < height) then
1494 count = count + 1
1495 normal = normal + normal2
1496 end
1497
1498 debugger.point(start + down, Color3.new(1, 0, 1))
1499 end
1500
1501 return hit, pos, (count > 0) and normal / count or baseNormal
1502end
1503
1504--
1505
1506return Cast
1507
1508local terrain = game:GetService("Workspace").Terrain
1509
1510local deb = {enabled = false}
1511
1512local usedPoints = {}
1513local usedVectors = {}
1514local unusedPoints = {}
1515local unusedVectors = {}
1516
1517function deb.print(...)
1518 if (deb.enabled) then
1519 print(...)
1520 end
1521end
1522
1523function deb.warn(...)
1524 if (deb.enabled) then
1525 warn(...)
1526 end
1527end
1528
1529function deb.error(...)
1530 if (deb.enabled) then
1531 error(...);
1532 end
1533end
1534
1535function deb.point(position, colour)
1536 if not (deb.enabled) then
1537 return
1538 end
1539
1540 local instance = table.remove(unusedPoints)
1541
1542 if not instance then
1543 instance = Instance.new("SphereHandleAdornment")
1544 instance.ZIndex = 1
1545 instance.Name = "Debug Handle"
1546 instance.AlwaysOnTop = true
1547 instance.Radius = 0.12
1548 instance.Adornee = terrain
1549 instance.Parent = terrain
1550 end
1551
1552 instance.CFrame = CFrame.new(position)
1553 instance.Color3 = colour
1554
1555 table.insert(usedPoints, instance)
1556end
1557
1558function deb.vector(position, direction, color)
1559 if not (deb.enabled) then
1560 return
1561 end
1562
1563 local instance = table.remove(unusedVectors)
1564
1565 if not instance then
1566 instance = Instance.new("BoxHandleAdornment")
1567 instance.Color3 = Color3.new(1, 1, 1)
1568 instance.AlwaysOnTop = true
1569 instance.ZIndex = 2
1570 instance.Transparency = 0.25
1571 instance.Parent = terrain
1572 instance.Adornee = terrain
1573 end
1574
1575 instance.Size = Vector3.new(0.1, 0.1, direction.magnitude)
1576 instance.CFrame = CFrame.new(position + direction/2, position + direction)
1577 instance.Color3 = color
1578
1579 table.insert(usedVectors, instance)
1580end
1581
1582function deb.step()
1583 while (#unusedPoints > 0) do
1584 table.remove(unusedPoints):Destroy()
1585 end
1586
1587 while (#unusedVectors > 0) do
1588 table.remove(unusedVectors):Destroy()
1589 end
1590
1591 usedPoints, unusedPoints = unusedPoints, usedPoints
1592 usedVectors, unusedVectors = unusedVectors, usedVectors
1593end
1594
1595return deb
1596
1597--[[
1598 PlayerModule - This module requires and instantiates the camera and control modules,
1599 and provides getters for developers to access methods on these singletons without
1600 having to modify Roblox-supplied scripts.
1601
1602 2018 PlayerScripts Update - AllYourBlox
1603--]]
1604
1605local PlayerModule = {}
1606PlayerModule.__index = PlayerModule
1607
1608function PlayerModule.new()
1609 local self = setmetatable({},PlayerModule)
1610 self.cameras = require(script:WaitForChild("CameraModule"))
1611 self.controls = require(script:WaitForChild("ControlModule"))
1612 return self
1613end
1614
1615function PlayerModule:GetCameras()
1616 return self.cameras
1617end
1618
1619function PlayerModule:GetControls()
1620 return self.controls
1621end
1622
1623function PlayerModule:GetClickToMoveController()
1624 return self.controls:GetClickToMoveController()
1625end
1626
1627return PlayerModule.new()
1628
1629--[[
1630 CameraModule - This ModuleScript implements a singleton class to manage the
1631 selection, activation, and deactivation of the current camera controller,
1632 character occlusion controller, and transparency controller. This script binds to
1633 RenderStepped at Camera priority and calls the Update() methods on the active
1634 controller instances.
1635
1636 The camera controller ModuleScripts implement classes which are instantiated and
1637 activated as-needed, they are no longer all instantiated up front as they were in
1638 the previous generation of PlayerScripts.
1639
1640 2018 PlayerScripts Update - AllYourBlox
1641--]]
1642
1643local CameraModule = {}
1644CameraModule.__index = CameraModule
1645
1646-- NOTICE: Player property names do not all match their StarterPlayer equivalents,
1647-- with the differences noted in the comments on the right
1648local PLAYER_CAMERA_PROPERTIES =
1649{
1650 "CameraMinZoomDistance",
1651 "CameraMaxZoomDistance",
1652 "CameraMode",
1653 "DevCameraOcclusionMode",
1654 "DevComputerCameraMode", -- Corresponds to StarterPlayer.DevComputerCameraMovementMode
1655 "DevTouchCameraMode", -- Corresponds to StarterPlayer.DevTouchCameraMovementMode
1656
1657 -- Character movement mode
1658 "DevComputerMovementMode",
1659 "DevTouchMovementMode",
1660 "DevEnableMouseLock", -- Corresponds to StarterPlayer.EnableMouseLockOption
1661}
1662
1663local USER_GAME_SETTINGS_PROPERTIES =
1664{
1665 "ComputerCameraMovementMode",
1666 "ComputerMovementMode",
1667 "ControlMode",
1668 "GamepadCameraSensitivity",
1669 "MouseSensitivity",
1670 "RotationType",
1671 "TouchCameraMovementMode",
1672 "TouchMovementMode",
1673}
1674
1675--[[ Roblox Services ]]--
1676local Players = game:GetService("Players")
1677local RunService = game:GetService("RunService")
1678local UserInputService = game:GetService("UserInputService")
1679local StarterPlayer = game:GetService("StarterPlayer")
1680local UserGameSettings = UserSettings():GetService("UserGameSettings")
1681
1682-- Camera math utility library
1683local CameraUtils = require(script:WaitForChild("CameraUtils"))
1684
1685-- Load Roblox Camera Controller Modules
1686local ClassicCamera = require(script:WaitForChild("ClassicCamera"))
1687local OrbitalCamera = require(script:WaitForChild("OrbitalCamera"))
1688local LegacyCamera = require(script:WaitForChild("LegacyCamera"))
1689
1690-- Load Roblox Occlusion Modules
1691local Invisicam = require(script:WaitForChild("Invisicam"))
1692local Poppercam do
1693 local success, useNewPoppercam = pcall(UserSettings().IsUserFeatureEnabled, UserSettings(), "UserNewPoppercam4")
1694 if success and useNewPoppercam then
1695 Poppercam = require(script:WaitForChild("Poppercam"))
1696 else
1697 Poppercam = require(script:WaitForChild("Poppercam_Classic"))
1698 end
1699end
1700
1701-- Load the near-field character transparency controller and the mouse lock "shift lock" controller
1702local TransparencyController = require(script:WaitForChild("TransparencyController"))
1703local MouseLockController = require(script:WaitForChild("MouseLockController"))
1704
1705-- Table of camera controllers that have been instantiated. They are instantiated as they are used.
1706local instantiatedCameraControllers = {}
1707local instantiatedOcclusionModules = {}
1708
1709-- Management of which options appear on the Roblox User Settings screen
1710do
1711 local PlayerScripts = Players.LocalPlayer:WaitForChild("PlayerScripts")
1712
1713 PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default)
1714 PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow)
1715 PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic)
1716
1717 PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default)
1718 PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow)
1719 PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic)
1720end
1721
1722
1723function CameraModule.new()
1724 local self = setmetatable({},CameraModule)
1725
1726 -- Current active controller instances
1727 self.activeCameraController = nil
1728 self.activeOcclusionModule = nil
1729 self.activeTransparencyController = nil
1730 self.activeMouseLockController = nil
1731
1732 self.currentComputerCameraMovementMode = nil
1733
1734 -- Connections to events
1735 self.cameraSubjectChangedConn = nil
1736 self.cameraTypeChangedConn = nil
1737
1738 -- Adds CharacterAdded and CharacterRemoving event handlers for all current players
1739 for _,player in pairs(Players:GetPlayers()) do
1740 self:OnPlayerAdded(player)
1741 end
1742
1743 -- Adds CharacterAdded and CharacterRemoving event handlers for all players who join in the future
1744 Players.PlayerAdded:Connect(function(player)
1745 self:OnPlayerAdded(player)
1746 end)
1747
1748 self.activeTransparencyController = TransparencyController.new()
1749 self.activeTransparencyController:Enable(true)
1750
1751 if not UserInputService.TouchEnabled then
1752 self.activeMouseLockController = MouseLockController.new()
1753 local toggleEvent = self.activeMouseLockController:GetBindableToggleEvent()
1754 if toggleEvent then
1755 toggleEvent:Connect(function()
1756 self:OnMouseLockToggled()
1757 end)
1758 end
1759 end
1760
1761 self:ActivateCameraController(self:GetCameraControlChoice())
1762 self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
1763 self:OnCurrentCameraChanged() -- Does initializations and makes first camera controller
1764 RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, function(dt) self:Update(dt) end)
1765
1766 -- Connect listeners to camera-related properties
1767 for _, propertyName in pairs(PLAYER_CAMERA_PROPERTIES) do
1768 Players.LocalPlayer:GetPropertyChangedSignal(propertyName):Connect(function()
1769 self:OnLocalPlayerCameraPropertyChanged(propertyName)
1770 end)
1771 end
1772
1773 for _, propertyName in pairs(USER_GAME_SETTINGS_PROPERTIES) do
1774 UserGameSettings:GetPropertyChangedSignal(propertyName):Connect(function()
1775 self:OnUserGameSettingsPropertyChanged(propertyName)
1776 end)
1777 end
1778 game.Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
1779 self:OnCurrentCameraChanged()
1780 end)
1781
1782 self.lastInputType = UserInputService:GetLastInputType()
1783 UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
1784 self.lastInputType = newLastInputType
1785 end)
1786
1787 return self
1788end
1789
1790
1791
1792
1793
1794function CameraModule:GetCameraMovementModeFromSettings()
1795 local cameraMode = Players.LocalPlayer.CameraMode
1796
1797 -- Lock First Person trumps all other settings and forces ClassicCamera
1798 if cameraMode == Enum.CameraMode.LockFirstPerson then
1799 return CameraUtils.ConvertCameraModeEnumToStandard(Enum.ComputerCameraMovementMode.Classic)
1800 end
1801
1802 local devMode, userMode
1803 if UserInputService.TouchEnabled then
1804 devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevTouchCameraMode)
1805 userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.TouchCameraMovementMode)
1806 else
1807 devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMode)
1808 userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
1809 end
1810
1811 if devMode == Enum.DevComputerCameraMovementMode.UserChoice then
1812 -- Developer is allowing user choice, so user setting is respected
1813 return userMode
1814 end
1815
1816 return devMode
1817end
1818
1819function CameraModule:ActivateOcclusionModule( occlusionMode )
1820 local newModuleCreator = nil
1821 if occlusionMode == Enum.DevCameraOcclusionMode.Zoom then
1822 newModuleCreator = Poppercam
1823 elseif occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
1824 newModuleCreator = Invisicam
1825 else
1826 warn("CameraScript ActivateOcclusionModule called with unsupported mode")
1827 return
1828 end
1829
1830 -- First check to see if there is actually a change. If the module being requested is already
1831 -- the currently-active solution then just make sure it's enabled and exit early
1832 if self.activeOcclusionModule and self.activeOcclusionModule:GetOcclusionMode() == occlusionMode then
1833 if not self.activeOcclusionModule:GetEnabled() then
1834 self.activeOcclusionModule:Enable(true)
1835 end
1836 return
1837 end
1838
1839 -- Save a reference to the current active module (may be nil) so that we can disable it if
1840 -- we are successful in activating its replacement
1841 local prevOcclusionModule = self.activeOcclusionModule
1842
1843 -- If there is no active module, see if the one we need has already been instantiated
1844 self.activeOcclusionModule = instantiatedOcclusionModules[newModuleCreator]
1845
1846 -- If the module was not already instantiated and selected above, instantiate it
1847 if not self.activeOcclusionModule then
1848 self.activeOcclusionModule = newModuleCreator.new()
1849 if self.activeOcclusionModule then
1850 instantiatedOcclusionModules[newModuleCreator] = self.activeOcclusionModule
1851 end
1852 end
1853
1854 -- If we were successful in either selecting or instantiating the module,
1855 -- enable it if it's not already the currently-active enabled module
1856 if self.activeOcclusionModule then
1857 local newModuleOcclusionMode = self.activeOcclusionModule:GetOcclusionMode()
1858 -- Sanity check that the module we selected or instantiated actually supports the desired occlusionMode
1859 if newModuleOcclusionMode ~= occlusionMode then
1860 warn("CameraScript ActivateOcclusionModule mismatch: ",self.activeOcclusionModule:GetOcclusionMode(),"~=",occlusionMode)
1861 end
1862
1863 -- Deactivate current module if there is one
1864 if prevOcclusionModule then
1865 -- Sanity check that current module is not being replaced by itself (that should have been handled above)
1866 if prevOcclusionModule ~= self.activeOcclusionModule then
1867 prevOcclusionModule:Enable(false)
1868 else
1869 warn("CameraScript ActivateOcclusionModule failure to detect already running correct module")
1870 end
1871 end
1872
1873 -- Occlusion modules need to be initialized with information about characters and cameraSubject
1874 -- Invisicam needs the LocalPlayer's character
1875 -- Poppercam needs all player characters and the camera subject
1876 if occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
1877 -- Optimization to only send Invisicam what we know it needs
1878 if Players.LocalPlayer.Character then
1879 self.activeOcclusionModule:CharacterAdded(Players.LocalPlayer.Character, Players.LocalPlayer )
1880 end
1881 else
1882 -- When Poppercam is enabled, we send it all existing player characters for its raycast ignore list
1883 for _, player in pairs(Players:GetPlayers()) do
1884 if player and player.Character then
1885 self.activeOcclusionModule:CharacterAdded(player.Character, player)
1886 end
1887 end
1888 self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
1889 end
1890
1891 -- Activate new choice
1892 self.activeOcclusionModule:Enable(true)
1893 end
1894end
1895
1896-- When supplied, legacyCameraType is used and cameraMovementMode is ignored (should be nil anyways)
1897-- Next, if userCameraCreator is passed in, that is used as the cameraCreator
1898function CameraModule:ActivateCameraController( cameraMovementMode, legacyCameraType )
1899 local newCameraCreator = nil
1900
1901 if legacyCameraType~=nil then
1902 --[[
1903 This function has been passed a CameraType enum value. Some of these map to the use of
1904 the LegacyCamera module, the value "Custom" will be translated to a movementMode enum
1905 value based on Dev and User settings, and "Scriptable" will disable the camera controller.
1906 --]]
1907
1908 if legacyCameraType == Enum.CameraType.Scriptable then
1909 if self.activeCameraController then
1910 self.activeCameraController:Enable(false)
1911 self.activeCameraController = nil
1912 return
1913 end
1914 elseif legacyCameraType == Enum.CameraType.Custom then
1915 cameraMovementMode = self:GetCameraMovementModeFromSettings()
1916
1917 elseif legacyCameraType == Enum.CameraType.Track then
1918 -- Note: The TrackCamera module was basically an older, less fully-featured
1919 -- version of ClassicCamera, no longer actively maintained, but it is re-implemented in
1920 -- case a game was dependent on its lack of ClassicCamera's extra functionality.
1921 cameraMovementMode = Enum.ComputerCameraMovementMode.Classic
1922
1923 elseif legacyCameraType == Enum.CameraType.Follow then
1924 cameraMovementMode = Enum.ComputerCameraMovementMode.Follow
1925
1926 elseif legacyCameraType == Enum.CameraType.Orbital then
1927 cameraMovementMode = Enum.ComputerCameraMovementMode.Orbital
1928
1929 elseif legacyCameraType == Enum.CameraType.Attach or
1930 legacyCameraType == Enum.CameraType.Watch or
1931 legacyCameraType == Enum.CameraType.Fixed then
1932 newCameraCreator = LegacyCamera
1933 else
1934 warn("CameraScript encountered an unhandled Camera.CameraType value: ",legacyCameraType)
1935 end
1936 end
1937
1938 if not newCameraCreator then
1939 if cameraMovementMode == Enum.ComputerCameraMovementMode.Classic or
1940 cameraMovementMode == Enum.ComputerCameraMovementMode.Follow or
1941 cameraMovementMode == Enum.ComputerCameraMovementMode.Default then
1942 newCameraCreator = ClassicCamera
1943 elseif cameraMovementMode == Enum.ComputerCameraMovementMode.Orbital then
1944 newCameraCreator = OrbitalCamera
1945 else
1946 warn("ActivateCameraController did not select a module.")
1947 return
1948 end
1949 end
1950
1951 -- Create the camera control module we need if it does not already exist in instantiatedCameraControllers
1952 local newCameraController = nil
1953 if not instantiatedCameraControllers[newCameraCreator] then
1954 newCameraController = newCameraCreator.new()
1955 instantiatedCameraControllers[newCameraCreator] = newCameraController
1956 else
1957 newCameraController = instantiatedCameraControllers[newCameraCreator]
1958 end
1959
1960 -- If there is a controller active and it's not the one we need, disable it,
1961 -- if it is the one we need, make sure it's enabled
1962 if self.activeCameraController then
1963 if self.activeCameraController ~= newCameraController then
1964 self.activeCameraController:Enable(false)
1965 self.activeCameraController = newCameraController
1966 self.activeCameraController:Enable(true)
1967 elseif not self.activeCameraController:GetEnabled() then
1968 self.activeCameraController:Enable(true)
1969 end
1970 elseif newCameraController ~= nil then
1971 self.activeCameraController = newCameraController
1972 self.activeCameraController:Enable(true)
1973 end
1974
1975 if self.activeCameraController then
1976 if cameraMovementMode~=nil then
1977 self.activeCameraController:SetCameraMovementMode(cameraMovementMode)
1978 elseif legacyCameraType~=nil then
1979 -- Note that this is only called when legacyCameraType is not a type that
1980 -- was convertible to a ComputerCameraMovementMode value, i.e. really only applies to LegacyCamera
1981 self.activeCameraController:SetCameraType(legacyCameraType)
1982 end
1983 end
1984end
1985
1986-- Note: The active transparency controller could be made to listen for this event itself.
1987function CameraModule:OnCameraSubjectChanged()
1988 if self.activeTransparencyController then
1989 self.activeTransparencyController:SetSubject(game.Workspace.CurrentCamera.CameraSubject)
1990 end
1991
1992 if self.activeOcclusionModule then
1993 self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
1994 end
1995end
1996
1997function CameraModule:OnCameraTypeChanged(newCameraType)
1998 if newCameraType == Enum.CameraType.Scriptable then
1999 if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then
2000 UserInputService.MouseBehavior = Enum.MouseBehavior.Default
2001 end
2002 end
2003
2004 -- Forward the change to ActivateCameraController to handle
2005 self:ActivateCameraController(nil, newCameraType)
2006end
2007
2008-- Note: Called whenever workspace.CurrentCamera changes, but also on initialization of this script
2009function CameraModule:OnCurrentCameraChanged()
2010 local currentCamera = game.Workspace.CurrentCamera
2011 if not currentCamera then return end
2012
2013 if self.cameraSubjectChangedConn then
2014 self.cameraSubjectChangedConn:Disconnect()
2015 end
2016
2017 if self.cameraTypeChangedConn then
2018 self.cameraTypeChangedConn:Disconnect()
2019 end
2020
2021 self.cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
2022 self:OnCameraSubjectChanged(currentCamera.CameraSubject)
2023 end)
2024
2025 self.cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):Connect(function()
2026 self:OnCameraTypeChanged(currentCamera.CameraType)
2027 end)
2028
2029 self:OnCameraSubjectChanged(currentCamera.CameraSubject)
2030 self:OnCameraTypeChanged(currentCamera.CameraType)
2031end
2032
2033function CameraModule:OnLocalPlayerCameraPropertyChanged(propertyName)
2034 if propertyName == "CameraMode" then
2035 -- CameraMode is only used to turn on/off forcing the player into first person view. The
2036 -- Note: The case "Classic" is used for all other views and does not correspond only to the ClassicCamera module
2037 if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then
2038 -- Locked in first person, use ClassicCamera which supports this
2039 if not self.activeCameraController or self.activeCameraController:GetModuleName() ~= "ClassicCamera" then
2040 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(Enum.DevComputerCameraMovementMode.Classic))
2041 end
2042
2043 if self.activeCameraController then
2044 self.activeCameraController:UpdateForDistancePropertyChange()
2045 end
2046 elseif Players.LocalPlayer.CameraMode == Enum.CameraMode.Classic then
2047 -- Not locked in first person view
2048 local cameraMovementMode =self: GetCameraMovementModeFromSettings()
2049 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
2050 else
2051 warn("Unhandled value for property player.CameraMode: ",Players.LocalPlayer.CameraMode)
2052 end
2053
2054 elseif propertyName == "DevComputerCameraMode" or
2055 propertyName == "DevTouchCameraMode" then
2056 local cameraMovementMode = self:GetCameraMovementModeFromSettings()
2057 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
2058
2059 elseif propertyName == "DevCameraOcclusionMode" then
2060 self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
2061
2062 elseif propertyName == "CameraMinZoomDistance" or propertyName == "CameraMaxZoomDistance" then
2063 if self.activeCameraController then
2064 self.activeCameraController:UpdateForDistancePropertyChange()
2065 end
2066 elseif propertyName == "DevTouchMovementMode" then
2067
2068 elseif propertyName == "DevComputerMovementMode" then
2069
2070 elseif propertyName == "DevEnableMouseLock" then
2071 -- This is the enabling/disabling of "Shift Lock" mode, not LockFirstPerson (which is a CameraMode)
2072
2073 -- Note: Enabling and disabling of MouseLock mode is normally only a publish-time choice made via
2074 -- the corresponding EnableMouseLockOption checkbox of StarterPlayer, and this script does not have
2075 -- support for changing the availability of MouseLock at runtime (this would require listening to
2076 -- Player.DevEnableMouseLock changes)
2077 end
2078end
2079
2080function CameraModule:OnUserGameSettingsPropertyChanged(propertyName)
2081
2082 if propertyName == "ComputerCameraMovementMode" then
2083 local cameraMovementMode = self:GetCameraMovementModeFromSettings()
2084 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
2085 end
2086end
2087
2088
2089
2090--[[
2091 Main RenderStep Update. The camera controller and occlusion module both have opportunities
2092 to set and modify (respectively) the CFrame and Focus before it is set once on CurrentCamera.
2093 The camera and occlusion modules should only return CFrames, not set the CFrame property of
2094 CurrentCamera directly.
2095--]]
2096function CameraModule:Update(dt)
2097 if self.activeCameraController then
2098 local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
2099 self.activeCameraController:ApplyVRTransform()
2100 if self.activeOcclusionModule then
2101 --newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus)
2102 end
2103
2104 -- Here is where the new CFrame and Focus are set for this render frame
2105 game.Workspace.CurrentCamera.CFrame = newCameraCFrame
2106 game.Workspace.CurrentCamera.Focus = newCameraFocus
2107
2108 -- Update to character local transparency as needed based on camera-to-subject distance
2109 if self.activeTransparencyController then
2110 --self.activeTransparencyController:Update()
2111 end
2112 end
2113end
2114
2115-- Formerly getCurrentCameraMode, this function resolves developer and user camera control settings to
2116-- decide which camera control module should be instantiated. The old method of converting redundant enum types
2117function CameraModule:GetCameraControlChoice()
2118 local player = Players.LocalPlayer
2119
2120 if player then
2121 if self.lastInputType == Enum.UserInputType.Touch or UserInputService.TouchEnabled then
2122 -- Touch
2123 if player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then
2124 return CameraUtils.ConvertCameraModeEnumToStandard( UserGameSettings.TouchCameraMovementMode )
2125 else
2126 return CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode )
2127 end
2128 else
2129 -- Computer
2130 if player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then
2131 local computerMovementMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
2132 return CameraUtils.ConvertCameraModeEnumToStandard(computerMovementMode)
2133 else
2134 return CameraUtils.ConvertCameraModeEnumToStandard(player.DevComputerCameraMode)
2135 end
2136 end
2137 end
2138end
2139
2140
2141function CameraModule:OnCharacterAdded(char, player)
2142 if self.activeOcclusionModule then
2143 self.activeOcclusionModule:CharacterAdded(char, player)
2144 end
2145end
2146
2147function CameraModule:OnCharacterRemoving(char, player)
2148 if self.activeOcclusionModule then
2149 self.activeOcclusionModule:CharacterRemoving(char, player)
2150 end
2151 if self.activeCameraController and player == Players.LocalPlayer then
2152 self.activeCameraController.xValue = 0
2153 self.activeCameraController.yValue = 0
2154 end
2155end
2156
2157function CameraModule:OnPlayerAdded(player)
2158 player.CharacterAdded:Connect(function(char)
2159 self:OnCharacterAdded(char, player)
2160 end)
2161 player.CharacterRemoving:Connect(function(char)
2162 self:OnCharacterRemoving(char, player)
2163 end)
2164end
2165
2166function CameraModule:OnMouseLockToggled()
2167 if self.activeMouseLockController then
2168 local mouseLocked = self.activeMouseLockController:GetIsMouseLocked()
2169 local mouseLockOffset = self.activeMouseLockController:GetMouseLockOffset()
2170 if self.activeCameraController then
2171 self.activeCameraController:SetIsMouseLocked(mouseLocked)
2172 self.activeCameraController:SetMouseLockOffset(mouseLockOffset)
2173 end
2174 end
2175end
2176
2177
2178
2179return CameraModule.new()
2180
2181--[[
2182 BaseCamera - Abstract base class for camera control modules
2183 2018 Camera Update - AllYourBlox
2184--]]
2185
2186--[[ Local Constants ]]--
2187local UNIT_Z = Vector3.new(0,0,1)
2188local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
2189
2190local THUMBSTICK_DEADZONE = 0.2
2191local DEFAULT_DISTANCE = 12.5 -- Studs
2192local PORTRAIT_DEFAULT_DISTANCE = 25 -- Studs
2193local FIRST_PERSON_DISTANCE_THRESHOLD = 1.0 -- Below this value, snap into first person
2194
2195local CAMERA_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
2196
2197-- Note: DotProduct check in CoordinateFrame::lookAt() prevents using values within about
2198-- 8.11 degrees of the +/- Y axis, that's why these limits are currently 80 degrees
2199local MIN_Y = math.rad(-80)
2200local MAX_Y = math.rad(80)
2201
2202local VR_ANGLE = math.rad(15)
2203local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0)
2204local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0)
2205local VR_LOW_INTENSITY_REPEAT = 0.1
2206local VR_HIGH_INTENSITY_REPEAT = 0.4
2207
2208local ZERO_VECTOR2 = Vector2.new(0,0)
2209local ZERO_VECTOR3 = Vector3.new(0,0,0)
2210
2211local TOUCH_SENSITIVTY = Vector2.new(0.0045 * math.pi, 0.003375 * math.pi)
2212local MOUSE_SENSITIVITY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi )
2213
2214local SEAT_OFFSET = Vector3.new(0,5,0)
2215local VR_SEAT_OFFSET = Vector3.new(0,4,0)
2216local HEAD_OFFSET = Vector3.new(0,1.5,0)
2217local R15_HEAD_OFFSET = Vector3.new(0,2,0)
2218local R15_HEAD_OFFSET_NO_SCALING = Vector3.new(0, 2, 0)
2219local HUMANOID_ROOT_PART_SIZE = Vector3.new(2, 2, 1)
2220
2221local GAMEPAD_ZOOM_STEP_1 = 0
2222local GAMEPAD_ZOOM_STEP_2 = 10
2223local GAMEPAD_ZOOM_STEP_3 = 20
2224
2225local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
2226 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority2")
2227end)
2228local FFlagPlayerScriptsBindAtPriority2 = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
2229
2230local adjustHumanoidRootPartFlagExists, adjustHumanoidRootPartFlagEnabled = pcall(function()
2231 return UserSettings():IsUserFeatureEnabled("UserAdjustHumanoidRootPartToHipPosition")
2232end)
2233local FFlagUserAdjustHumanoidRootPartToHipPosition = adjustHumanoidRootPartFlagExists and adjustHumanoidRootPartFlagEnabled
2234
2235local thirdGamepadZoomStepFlagExists, thirdGamepadZoomStepFlagEnabled = pcall(function()
2236 return UserSettings():IsUserFeatureEnabled("UserThirdGamepadZoomStep")
2237end)
2238local FFlagUserThirdGamepadZoomStep = thirdGamepadZoomStepFlagExists and thirdGamepadZoomStepFlagEnabled
2239
2240if FFlagUserAdjustHumanoidRootPartToHipPosition then
2241 R15_HEAD_OFFSET = Vector3.new(0, 1.5, 0)
2242end
2243
2244local Util = require(script.Parent:WaitForChild("CameraUtils"))
2245local ZoomController = require(script.Parent:WaitForChild("ZoomController"))
2246
2247--[[ Roblox Services ]]--
2248local Players = game:GetService("Players")
2249local UserInputService = game:GetService("UserInputService")
2250local StarterGui = game:GetService("StarterGui")
2251local GuiService = game:GetService("GuiService")
2252local ContextActionService = game:GetService("ContextActionService")
2253local VRService = game:GetService("VRService")
2254local UserGameSettings = UserSettings():GetService("UserGameSettings")
2255
2256--[[ The Module ]]--
2257local BaseCamera = {}
2258BaseCamera.__index = BaseCamera
2259
2260-- Modded Additions
2261
2262BaseCamera.xValue = 0
2263BaseCamera.yValue = 0
2264
2265function BaseCamera:GetCameraLookVector()
2266 return CFrame.fromEulerAnglesYXZ(self.yValue, self.xValue, 0) * Vector3.new(0, 0, 1)
2267end
2268
2269function BaseCamera:CalculateNewLookCFrame(suppliedLookVector)
2270 self.xValue = self.xValue - self.rotateInput.x
2271 self.yValue = Util.Clamp(MIN_Y,MAX_Y, self.yValue + self.rotateInput.Y)
2272 return CFrame.new(Vector3.new(), self:GetCameraLookVector())
2273end
2274
2275-- Modded Additions end
2276
2277function BaseCamera.new()
2278 local self = setmetatable({}, BaseCamera)
2279
2280 -- So that derived classes have access to this
2281 self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD
2282
2283 self.cameraType = nil
2284 self.cameraMovementMode = nil
2285
2286 local player = Players.LocalPlayer
2287 self.lastCameraTransform = nil
2288 self.rotateInput = ZERO_VECTOR2
2289 self.userPanningCamera = false
2290 self.lastUserPanCamera = tick()
2291
2292 self.humanoidRootPart = nil
2293 self.humanoidCache = {}
2294
2295 -- Subject and position on last update call
2296 self.lastSubject = nil
2297 self.lastSubjectPosition = Vector3.new(0,5,0)
2298
2299 -- These subject distance members refer to the nominal camera-to-subject follow distance that the camera
2300 -- is trying to maintain, not the actual measured value.
2301 -- The default is updated when screen orientation or the min/max distances change,
2302 -- to be sure the default is always in range and appropriate for the orientation.
2303 self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE)
2304 self.currentSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE)
2305
2306 self.inFirstPerson = false
2307 self.inMouseLockedMode = false
2308 self.portraitMode = false
2309 self.isSmallTouchScreen = false
2310
2311 -- Used by modules which want to reset the camera angle on respawn.
2312 self.resetCameraAngle = true
2313
2314 self.enabled = false
2315
2316 -- Input Event Connections
2317 self.inputBeganConn = nil
2318 self.inputChangedConn = nil
2319 self.inputEndedConn = nil
2320
2321 self.startPos = nil
2322 self.lastPos = nil
2323 self.panBeginLook = nil
2324
2325 self.panEnabled = true
2326 self.keyPanEnabled = true
2327 self.distanceChangeEnabled = true
2328
2329 self.PlayerGui = nil
2330
2331 self.cameraChangedConn = nil
2332 self.viewportSizeChangedConn = nil
2333
2334 self.boundContextActions = {}
2335
2336 -- VR Support
2337 self.shouldUseVRRotation = false
2338 self.VRRotationIntensityAvailable = false
2339 self.lastVRRotationIntensityCheckTime = 0
2340 self.lastVRRotationTime = 0
2341 self.vrRotateKeyCooldown = {}
2342 self.cameraTranslationConstraints = Vector3.new(1, 1, 1)
2343 self.humanoidJumpOrigin = nil
2344 self.trackingHumanoid = nil
2345 self.cameraFrozen = false
2346 self.subjectStateChangedConn = nil
2347
2348 -- Gamepad support
2349 self.activeGamepad = nil
2350 self.gamepadPanningCamera = false
2351 self.lastThumbstickRotate = nil
2352 self.numOfSeconds = 0.7
2353 self.currentSpeed = 0
2354 self.maxSpeed = 6
2355 self.vrMaxSpeed = 4
2356 self.lastThumbstickPos = Vector2.new(0,0)
2357 self.ySensitivity = 0.65
2358 self.lastVelocity = nil
2359 self.gamepadConnectedConn = nil
2360 self.gamepadDisconnectedConn = nil
2361 self.currentZoomSpeed = 1.0
2362 self.L3ButtonDown = false
2363 self.dpadLeftDown = false
2364 self.dpadRightDown = false
2365
2366 -- Touch input support
2367 self.isDynamicThumbstickEnabled = false
2368 self.fingerTouches = {}
2369 self.numUnsunkTouches = 0
2370 self.inputStartPositions = {}
2371 self.inputStartTimes = {}
2372 self.startingDiff = nil
2373 self.pinchBeginZoom = nil
2374 self.userPanningTheCamera = false
2375 self.touchActivateConn = nil
2376
2377 -- Mouse locked formerly known as shift lock mode
2378 self.mouseLockOffset = ZERO_VECTOR3
2379
2380 -- [[ NOTICE ]] --
2381 -- Initialization things used to always execute at game load time, but now these camera modules are instantiated
2382 -- when needed, so the code here may run well after the start of the game
2383
2384 if player.Character then
2385 self:OnCharacterAdded(player.Character)
2386 end
2387
2388 player.CharacterAdded:Connect(function(char)
2389 self:OnCharacterAdded(char)
2390 end)
2391
2392 if self.cameraChangedConn then self.cameraChangedConn:Disconnect() end
2393 self.cameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
2394 self:OnCurrentCameraChanged()
2395 end)
2396 self:OnCurrentCameraChanged()
2397
2398 if self.playerCameraModeChangeConn then self.playerCameraModeChangeConn:Disconnect() end
2399 self.playerCameraModeChangeConn = player:GetPropertyChangedSignal("CameraMode"):Connect(function()
2400 self:OnPlayerCameraPropertyChange()
2401 end)
2402
2403 if self.minDistanceChangeConn then self.minDistanceChangeConn:Disconnect() end
2404 self.minDistanceChangeConn = player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(function()
2405 self:OnPlayerCameraPropertyChange()
2406 end)
2407
2408 if self.maxDistanceChangeConn then self.maxDistanceChangeConn:Disconnect() end
2409 self.maxDistanceChangeConn = player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(function()
2410 self:OnPlayerCameraPropertyChange()
2411 end)
2412
2413 if self.playerDevTouchMoveModeChangeConn then self.playerDevTouchMoveModeChangeConn:Disconnect() end
2414 self.playerDevTouchMoveModeChangeConn = player:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function()
2415 self:OnDevTouchMovementModeChanged()
2416 end)
2417 self:OnDevTouchMovementModeChanged() -- Init
2418
2419 if self.gameSettingsTouchMoveMoveChangeConn then self.gameSettingsTouchMoveMoveChangeConn:Disconnect() end
2420 self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function()
2421 self:OnGameSettingsTouchMovementModeChanged()
2422 end)
2423 self:OnGameSettingsTouchMovementModeChanged() -- Init
2424
2425 UserGameSettings:SetCameraYInvertVisible()
2426 UserGameSettings:SetGamepadCameraSensitivityVisible()
2427
2428 self.hasGameLoaded = game:IsLoaded()
2429 if not self.hasGameLoaded then
2430 self.gameLoadedConn = game.Loaded:Connect(function()
2431 self.hasGameLoaded = true
2432 self.gameLoadedConn:Disconnect()
2433 self.gameLoadedConn = nil
2434 end)
2435 end
2436
2437 return self
2438end
2439
2440function BaseCamera:GetModuleName()
2441 return "BaseCamera"
2442end
2443
2444function BaseCamera:OnCharacterAdded(char)
2445 self.resetCameraAngle = self.resetCameraAngle or self:GetEnabled()
2446 self.humanoidRootPart = nil
2447 if UserInputService.TouchEnabled then
2448 self.PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
2449 for _, child in ipairs(char:GetChildren()) do
2450 if child:IsA("Tool") then
2451 self.isAToolEquipped = true
2452 end
2453 end
2454 char.ChildAdded:Connect(function(child)
2455 if child:IsA("Tool") then
2456 self.isAToolEquipped = true
2457 end
2458 end)
2459 char.ChildRemoved:Connect(function(child)
2460 if child:IsA("Tool") then
2461 self.isAToolEquipped = false
2462 end
2463 end)
2464 end
2465end
2466
2467function BaseCamera:GetHumanoidRootPart()
2468 if not self.humanoidRootPart then
2469 local player = Players.LocalPlayer
2470 if player.Character then
2471 local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
2472 if humanoid then
2473 self.humanoidRootPart = humanoid.RootPart
2474 end
2475 end
2476 end
2477 return self.humanoidRootPart
2478end
2479
2480function BaseCamera:GetBodyPartToFollow(humanoid, isDead)
2481 -- If the humanoid is dead, prefer the head part if one still exists as a sibling of the humanoid
2482 if humanoid:GetState() == Enum.HumanoidStateType.Dead then
2483 local character = humanoid.Parent
2484 if character and character:IsA("Model") then
2485 return character:FindFirstChild("Head") or humanoid.RootPart
2486 end
2487 end
2488
2489 return humanoid.RootPart
2490end
2491
2492function BaseCamera:GetSubjectPosition()
2493 local result = self.lastSubjectPosition
2494 local camera = game.Workspace.CurrentCamera
2495 local cameraSubject = camera and camera.CameraSubject
2496
2497 if cameraSubject then
2498 if cameraSubject:IsA("Humanoid") then
2499 local humanoid = cameraSubject
2500 local humanoidIsDead = humanoid:GetState() == Enum.HumanoidStateType.Dead
2501
2502 if VRService.VREnabled and humanoidIsDead and humanoid == self.lastSubject then
2503 result = self.lastSubjectPosition
2504 else
2505 local bodyPartToFollow = humanoid.RootPart
2506
2507 -- If the humanoid is dead, prefer their head part as a follow target, if it exists
2508 if humanoidIsDead then
2509 if humanoid.Parent and humanoid.Parent:IsA("Model") then
2510 bodyPartToFollow = humanoid.Parent:FindFirstChild("Head") or bodyPartToFollow
2511 end
2512 end
2513
2514 if bodyPartToFollow and bodyPartToFollow:IsA("BasePart") then
2515 local heightOffset
2516 if FFlagUserAdjustHumanoidRootPartToHipPosition then
2517 if humanoid.RigType == Enum.HumanoidRigType.R15 then
2518 if humanoid.AutomaticScalingEnabled then
2519 heightOffset = R15_HEAD_OFFSET
2520 if bodyPartToFollow == humanoid.RootPart then
2521 local rootPartSizeOffset = (humanoid.RootPart.Size.Y/2) - (HUMANOID_ROOT_PART_SIZE.Y/2)
2522 heightOffset = heightOffset + Vector3.new(0, rootPartSizeOffset, 0)
2523 end
2524 else
2525 heightOffset = R15_HEAD_OFFSET_NO_SCALING
2526 end
2527 else
2528 heightOffset = HEAD_OFFSET
2529 end
2530 else
2531 heightOffset = humanoid.RigType == Enum.HumanoidRigType.R15 and R15_HEAD_OFFSET or HEAD_OFFSET
2532 end
2533
2534 if humanoidIsDead then
2535 heightOffset = ZERO_VECTOR3
2536 end
2537
2538 result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:vectorToWorldSpace(heightOffset + humanoid.CameraOffset)
2539 end
2540 end
2541
2542 elseif cameraSubject:IsA("VehicleSeat") then
2543 local offset = SEAT_OFFSET
2544 if VRService.VREnabled then
2545 offset = VR_SEAT_OFFSET
2546 end
2547 result = cameraSubject.CFrame.p + cameraSubject.CFrame:vectorToWorldSpace(offset)
2548 elseif cameraSubject:IsA("SkateboardPlatform") then
2549 result = cameraSubject.CFrame.p + SEAT_OFFSET
2550 elseif cameraSubject:IsA("BasePart") then
2551 result = cameraSubject.CFrame.p
2552 elseif cameraSubject:IsA("Model") then
2553 if cameraSubject.PrimaryPart then
2554 result = cameraSubject:GetPrimaryPartCFrame().p
2555 else
2556 result = cameraSubject:GetModelCFrame().p
2557 end
2558 end
2559 else
2560 -- cameraSubject is nil
2561 -- Note: Previous RootCamera did not have this else case and let self.lastSubject and self.lastSubjectPosition
2562 -- both get set to nil in the case of cameraSubject being nil. This function now exits here to preserve the
2563 -- last set valid values for these, as nil values are not handled cases
2564 return
2565 end
2566
2567 self.lastSubject = cameraSubject
2568 self.lastSubjectPosition = result
2569
2570 return result
2571end
2572
2573function BaseCamera:UpdateDefaultSubjectDistance()
2574 local player = Players.LocalPlayer
2575 if self.portraitMode then
2576 self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, PORTRAIT_DEFAULT_DISTANCE)
2577 else
2578 self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE)
2579 end
2580end
2581
2582function BaseCamera:OnViewportSizeChanged()
2583 local camera = game.Workspace.CurrentCamera
2584 local size = camera.ViewportSize
2585 self.portraitMode = size.X < size.Y
2586 self.isSmallTouchScreen = UserInputService.TouchEnabled and (size.Y < 500 or size.X < 700)
2587
2588 self:UpdateDefaultSubjectDistance()
2589end
2590
2591-- Listener for changes to workspace.CurrentCamera
2592function BaseCamera:OnCurrentCameraChanged()
2593 if UserInputService.TouchEnabled then
2594 if self.viewportSizeChangedConn then
2595 self.viewportSizeChangedConn:Disconnect()
2596 self.viewportSizeChangedConn = nil
2597 end
2598
2599 local newCamera = game.Workspace.CurrentCamera
2600
2601 if newCamera then
2602 self:OnViewportSizeChanged()
2603 self.viewportSizeChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
2604 self:OnViewportSizeChanged()
2605 end)
2606 end
2607 end
2608
2609 -- VR support additions
2610 if self.cameraSubjectChangedConn then
2611 self.cameraSubjectChangedConn:Disconnect()
2612 self.cameraSubjectChangedConn = nil
2613 end
2614
2615 local camera = game.Workspace.CurrentCamera
2616 if camera then
2617 self.cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
2618 self:OnNewCameraSubject()
2619 end)
2620 self:OnNewCameraSubject()
2621 end
2622end
2623
2624function BaseCamera:OnDynamicThumbstickEnabled()
2625 if UserInputService.TouchEnabled then
2626 self.isDynamicThumbstickEnabled = true
2627 end
2628end
2629
2630function BaseCamera:OnDynamicThumbstickDisabled()
2631 self.isDynamicThumbstickEnabled = false
2632end
2633
2634function BaseCamera:OnGameSettingsTouchMovementModeChanged()
2635 if Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then
2636 if (UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.DynamicThumbstick
2637 or UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.Default) then
2638 self:OnDynamicThumbstickEnabled()
2639 else
2640 self:OnDynamicThumbstickDisabled()
2641 end
2642 end
2643end
2644
2645function BaseCamera:OnDevTouchMovementModeChanged()
2646 if Players.LocalPlayer.DevTouchMovementMode.Name == "DynamicThumbstick" then
2647 self:OnDynamicThumbstickEnabled()
2648 else
2649 self:OnGameSettingsTouchMovementModeChanged()
2650 end
2651end
2652
2653function BaseCamera:OnPlayerCameraPropertyChange()
2654 -- This call forces re-evaluation of player.CameraMode and clamping to min/max distance which may have changed
2655 self:SetCameraToSubjectDistance(self.currentSubjectDistance)
2656end
2657
2658function BaseCamera:GetCameraHeight()
2659 if VRService.VREnabled and not self.inFirstPerson then
2660 return math.sin(VR_ANGLE) * self.currentSubjectDistance
2661 end
2662 return 0
2663end
2664
2665function BaseCamera:InputTranslationToCameraAngleChange(translationVector, sensitivity)
2666 local camera = game.Workspace.CurrentCamera
2667 if camera and camera.ViewportSize.X > 0 and camera.ViewportSize.Y > 0 and (camera.ViewportSize.Y > camera.ViewportSize.X) then
2668 -- Screen has portrait orientation, swap X and Y sensitivity
2669 return translationVector * Vector2.new( sensitivity.Y, sensitivity.X)
2670 end
2671 return translationVector * sensitivity
2672end
2673
2674function BaseCamera:Enable(enable)
2675 if self.enabled ~= enable then
2676 self.enabled = enable
2677 if self.enabled then
2678 self:ConnectInputEvents()
2679 if FFlagPlayerScriptsBindAtPriority2 then
2680 self:BindContextActions()
2681 end
2682
2683 if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then
2684 self.currentSubjectDistance = 0.5
2685 if not self.inFirstPerson then
2686 self:EnterFirstPerson()
2687 end
2688 end
2689 else
2690 self:DisconnectInputEvents()
2691 if FFlagPlayerScriptsBindAtPriority2 then
2692 self:UnbindContextActions()
2693 end
2694 -- Clean up additional event listeners and reset a bunch of properties
2695 self:Cleanup()
2696 end
2697 end
2698end
2699
2700function BaseCamera:GetEnabled()
2701 return self.enabled
2702end
2703
2704function BaseCamera:OnInputBegan(input, processed)
2705 if input.UserInputType == Enum.UserInputType.Touch then
2706 self:OnTouchBegan(input, processed)
2707 elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
2708 self:OnMouse2Down(input, processed)
2709 elseif input.UserInputType == Enum.UserInputType.MouseButton3 then
2710 self:OnMouse3Down(input, processed)
2711 end
2712 -- Keyboard
2713 if not FFlagPlayerScriptsBindAtPriority2 then
2714 if input.UserInputType == Enum.UserInputType.Keyboard then
2715 self:OnKeyDown(input, processed)
2716 end
2717 end
2718end
2719
2720function BaseCamera:OnInputChanged(input, processed)
2721 if input.UserInputType == Enum.UserInputType.Touch then
2722 self:OnTouchChanged(input, processed)
2723 elseif input.UserInputType == Enum.UserInputType.MouseMovement then
2724 self:OnMouseMoved(input, processed)
2725 elseif input.UserInputType == Enum.UserInputType.MouseWheel then
2726 self:OnMouseWheel(input, processed)
2727 end
2728end
2729
2730function BaseCamera:OnInputEnded(input, processed)
2731 if input.UserInputType == Enum.UserInputType.Touch then
2732 self:OnTouchEnded(input, processed)
2733 elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
2734 self:OnMouse2Up(input, processed)
2735 elseif input.UserInputType == Enum.UserInputType.MouseButton3 then
2736 self:OnMouse3Up(input, processed)
2737 end
2738 -- Keyboard
2739 if not FFlagPlayerScriptsBindAtPriority2 then
2740 if input.UserInputType == Enum.UserInputType.Keyboard then
2741 self:OnKeyUp(input, processed)
2742 end
2743 end
2744end
2745
2746function BaseCamera:ConnectInputEvents()
2747 self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
2748 self:OnInputBegan(input, processed)
2749 end)
2750
2751 self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
2752 self:OnInputChanged(input, processed)
2753 end)
2754
2755 self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
2756 self:OnInputEnded(input, processed)
2757 end)
2758
2759 self.menuOpenedConn = GuiService.MenuOpened:connect(function()
2760 self:ResetInputStates()
2761 end)
2762
2763 self.gamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
2764 if self.activeGamepad ~= gamepadEnum then return end
2765 self.activeGamepad = nil
2766 self:AssignActivateGamepad()
2767 end)
2768
2769 self.gamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum)
2770 if self.activeGamepad == nil then
2771 self:AssignActivateGamepad()
2772 end
2773 end)
2774
2775 if not FFlagPlayerScriptsBindAtPriority2 then
2776 self:BindGamepadInputActions()
2777 end
2778 self:AssignActivateGamepad()
2779 self:UpdateMouseBehavior()
2780end
2781
2782function BaseCamera:BindContextActions()
2783 self:BindGamepadInputActions()
2784 self:BindKeyboardInputActions()
2785end
2786
2787function BaseCamera:AssignActivateGamepad()
2788 local connectedGamepads = UserInputService:GetConnectedGamepads()
2789 if #connectedGamepads > 0 then
2790 for i = 1, #connectedGamepads do
2791 if self.activeGamepad == nil then
2792 self.activeGamepad = connectedGamepads[i]
2793 elseif connectedGamepads[i].Value < self.activeGamepad.Value then
2794 self.activeGamepad = connectedGamepads[i]
2795 end
2796 end
2797 end
2798
2799 if self.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1
2800 self.activeGamepad = Enum.UserInputType.Gamepad1
2801 end
2802end
2803
2804function BaseCamera:DisconnectInputEvents()
2805 if self.inputBeganConn then
2806 self.inputBeganConn:Disconnect()
2807 self.inputBeganConn = nil
2808 end
2809 if self.inputChangedConn then
2810 self.inputChangedConn:Disconnect()
2811 self.inputChangedConn = nil
2812 end
2813 if self.inputEndedConn then
2814 self.inputEndedConn:Disconnect()
2815 self.inputEndedConn = nil
2816 end
2817end
2818
2819function BaseCamera:UnbindContextActions()
2820 for i = 1, #self.boundContextActions do
2821 ContextActionService:UnbindAction(self.boundContextActions[i])
2822 end
2823 self.boundContextActions = {}
2824end
2825
2826function BaseCamera:Cleanup()
2827 if self.menuOpenedConn then
2828 self.menuOpenedConn:Disconnect()
2829 self.menuOpenedConn = nil
2830 end
2831 if self.mouseLockToggleConn then
2832 self.mouseLockToggleConn:Disconnect()
2833 self.mouseLockToggleConn = nil
2834 end
2835 if self.gamepadConnectedConn then
2836 self.gamepadConnectedConn:Disconnect()
2837 self.gamepadConnectedConn = nil
2838 end
2839 if self.gamepadDisconnectedConn then
2840 self.gamepadDisconnectedConn:Disconnect()
2841 self.gamepadDisconnectedConn = nil
2842 end
2843 if self.subjectStateChangedConn then
2844 self.subjectStateChangedConn:Disconnect()
2845 self.subjectStateChangedConn = nil
2846 end
2847 if self.viewportSizeChangedConn then
2848 self.viewportSizeChangedConn:Disconnect()
2849 self.viewportSizeChangedConn = nil
2850 end
2851 if self.touchActivateConn then
2852 self.touchActivateConn:Disconnect()
2853 self.touchActivateConn = nil
2854 end
2855
2856 self.turningLeft = false
2857 self.turningRight = false
2858 self.lastCameraTransform = nil
2859 self.lastSubjectCFrame = nil
2860 self.userPanningTheCamera = false
2861 self.rotateInput = Vector2.new()
2862 self.gamepadPanningCamera = Vector2.new(0,0)
2863
2864 -- Reset input states
2865 self.startPos = nil
2866 self.lastPos = nil
2867 self.panBeginLook = nil
2868 self.isRightMouseDown = false
2869 self.isMiddleMouseDown = false
2870
2871 self.fingerTouches = {}
2872 self.numUnsunkTouches = 0
2873
2874 self.startingDiff = nil
2875 self.pinchBeginZoom = nil
2876
2877 -- Unlock mouse for example if right mouse button was being held down
2878 if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then
2879 UserInputService.MouseBehavior = Enum.MouseBehavior.Default
2880 end
2881end
2882
2883-- This is called when settings menu is opened
2884function BaseCamera:ResetInputStates()
2885 self.isRightMouseDown = false
2886 self.isMiddleMouseDown = false
2887 self:OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters
2888
2889 if UserInputService.TouchEnabled then
2890 --[[menu opening was causing serious touch issues
2891 this should disable all active touch events if
2892 they're active when menu opens.]]
2893 for inputObject in pairs(self.fingerTouches) do
2894 self.fingerTouches[inputObject] = nil
2895 end
2896 self.panBeginLook = nil
2897 self.startPos = nil
2898 self.lastPos = nil
2899 self.userPanningTheCamera = false
2900 self.startingDiff = nil
2901 self.pinchBeginZoom = nil
2902 self.numUnsunkTouches = 0
2903 end
2904end
2905
2906function BaseCamera:GetGamepadPan(name, state, input)
2907 if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
2908-- if self.L3ButtonDown then
2909-- -- L3 Thumbstick is depressed, right stick controls dolly in/out
2910-- if (input.Position.Y > THUMBSTICK_DEADZONE) then
2911-- self.currentZoomSpeed = 0.96
2912-- elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
2913-- self.currentZoomSpeed = 1.04
2914-- else
2915-- self.currentZoomSpeed = 1.00
2916-- end
2917-- else
2918 if state == Enum.UserInputState.Cancel then
2919 self.gamepadPanningCamera = ZERO_VECTOR2
2920 return
2921 end
2922
2923 local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
2924 if inputVector.magnitude > THUMBSTICK_DEADZONE then
2925 self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
2926 else
2927 self.gamepadPanningCamera = ZERO_VECTOR2
2928 end
2929 --end
2930 if FFlagPlayerScriptsBindAtPriority2 then
2931 return Enum.ContextActionResult.Sink
2932 end
2933 end
2934 if FFlagPlayerScriptsBindAtPriority2 then
2935 return Enum.ContextActionResult.Pass
2936 end
2937end
2938
2939function BaseCamera:DoKeyboardPanTurn(name, state, input)
2940 if not self.hasGameLoaded and VRService.VREnabled then
2941 return Enum.ContextActionResult.Pass
2942 end
2943
2944 if state == Enum.UserInputState.Cancel then
2945 self.turningLeft = false
2946 self.turningRight = false
2947 return Enum.ContextActionResult.Sink
2948 end
2949
2950 if self.panBeginLook == nil and self.keyPanEnabled then
2951 if input.KeyCode == Enum.KeyCode.Left then
2952 self.turningLeft = state == Enum.UserInputState.Begin
2953 elseif input.KeyCode == Enum.KeyCode.Right then
2954 self.turningRight = state == Enum.UserInputState.Begin
2955 end
2956 return Enum.ContextActionResult.Sink
2957 end
2958 return Enum.ContextActionResult.Pass
2959end
2960
2961function BaseCamera:DoPanRotateCamera(rotateAngle)
2962 local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), rotateAngle, math.pi*0.25)
2963 if angle ~= 0 then
2964 self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
2965 self.lastUserPanCamera = tick()
2966 self.lastCameraTransform = nil
2967 end
2968end
2969
2970function BaseCamera:DoKeyboardPan(name, state, input)
2971 if not self.hasGameLoaded and VRService.VREnabled then
2972 return Enum.ContextActionResult.Pass
2973 end
2974
2975 if state ~= Enum.UserInputState.Begin then
2976 return Enum.ContextActionResult.Pass
2977 end
2978
2979 if self.panBeginLook == nil and self.keyPanEnabled then
2980 if input.KeyCode == Enum.KeyCode.Comma then
2981 self:DoPanRotateCamera(-math.pi*0.1875)
2982 elseif input.KeyCode == Enum.KeyCode.Period then
2983 self:DoPanRotateCamera(math.pi*0.1875)
2984 elseif input.KeyCode == Enum.KeyCode.PageUp then
2985 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(15))
2986 self.lastCameraTransform = nil
2987 elseif input.KeyCode == Enum.KeyCode.PageDown then
2988 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(-15))
2989 self.lastCameraTransform = nil
2990 end
2991 return Enum.ContextActionResult.Sink
2992 end
2993 return Enum.ContextActionResult.Pass
2994end
2995
2996function BaseCamera:DoGamepadZoom(name, state, input)
2997 if input.UserInputType == self.activeGamepad then
2998 if input.KeyCode == Enum.KeyCode.ButtonR3 then
2999 if state == Enum.UserInputState.Begin then
3000 if self.distanceChangeEnabled then
3001 local dist = self:GetCameraToSubjectDistance()
3002 if FFlagUserThirdGamepadZoomStep then
3003 if dist > (GAMEPAD_ZOOM_STEP_2 + GAMEPAD_ZOOM_STEP_3)/2 then
3004 self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_2)
3005 elseif dist > (GAMEPAD_ZOOM_STEP_1 + GAMEPAD_ZOOM_STEP_2)/2 then
3006 self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_1)
3007 else
3008 self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_3)
3009 end
3010 else
3011 if dist > 0.5 then
3012 self:SetCameraToSubjectDistance(0)
3013 else
3014 self:SetCameraToSubjectDistance(10)
3015 end
3016 end
3017 end
3018 end
3019 elseif input.KeyCode == Enum.KeyCode.DPadLeft then
3020 self.dpadLeftDown = (state == Enum.UserInputState.Begin)
3021 elseif input.KeyCode == Enum.KeyCode.DPadRight then
3022 self.dpadRightDown = (state == Enum.UserInputState.Begin)
3023 end
3024
3025 if self.dpadLeftDown then
3026 self.currentZoomSpeed = 1.04
3027 elseif self.dpadRightDown then
3028 self.currentZoomSpeed = 0.96
3029 else
3030 self.currentZoomSpeed = 1.00
3031 end
3032 if FFlagPlayerScriptsBindAtPriority2 then
3033 return Enum.ContextActionResult.Sink
3034 end
3035 end
3036 if FFlagPlayerScriptsBindAtPriority2 then
3037 return Enum.ContextActionResult.Pass
3038 end
3039-- elseif input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonL3 then
3040-- if (state == Enum.UserInputState.Begin) then
3041-- self.L3ButtonDown = true
3042-- elseif (state == Enum.UserInputState.End) then
3043-- self.L3ButtonDown = false
3044-- self.currentZoomSpeed = 1.00
3045-- end
3046-- end
3047end
3048
3049function BaseCamera:DoKeyboardZoom(name, state, input)
3050 if not self.hasGameLoaded and VRService.VREnabled then
3051 return Enum.ContextActionResult.Pass
3052 end
3053
3054 if state ~= Enum.UserInputState.Begin then
3055 return Enum.ContextActionResult.Pass
3056 end
3057
3058 if self.distanceChangeEnabled and Players.LocalPlayer.CameraMode ~= Enum.CameraMode.LockFirstPerson then
3059 if input.KeyCode == Enum.KeyCode.I then
3060 self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 )
3061 elseif input.KeyCode == Enum.KeyCode.O then
3062 self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 )
3063 end
3064 return Enum.ContextActionResult.Sink
3065 end
3066 return Enum.ContextActionResult.Pass
3067end
3068
3069function BaseCamera:BindAction(actionName, actionFunc, createTouchButton, ...)
3070 table.insert(self.boundContextActions, actionName)
3071 ContextActionService:BindActionAtPriority(actionName, actionFunc, createTouchButton,
3072 CAMERA_ACTION_PRIORITY, ...)
3073end
3074
3075function BaseCamera:BindGamepadInputActions()
3076 if FFlagPlayerScriptsBindAtPriority2 then
3077 self:BindAction("BaseCameraGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end,
3078 false, Enum.KeyCode.Thumbstick2)
3079 self:BindAction("BaseCameraGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end,
3080 false, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight, Enum.KeyCode.ButtonR3)
3081 else
3082 ContextActionService:BindAction("RootCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2)
3083 ContextActionService:BindAction("RootCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3)
3084 --ContextActionService:BindAction("RootGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonL3)
3085 ContextActionService:BindAction("RootGamepadZoomOut", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadLeft)
3086 ContextActionService:BindAction("RootGamepadZoomIn", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadRight)
3087 end
3088end
3089
3090function BaseCamera:BindKeyboardInputActions()
3091 self:BindAction("BaseCameraKeyboardPanArrowKeys", function(name, state, input) return self:DoKeyboardPanTurn(name, state, input) end,
3092 false, Enum.KeyCode.Left, Enum.KeyCode.Right)
3093 self:BindAction("BaseCameraKeyboardPan", function(name, state, input) return self:DoKeyboardPan(name, state, input) end,
3094 false, Enum.KeyCode.Comma, Enum.KeyCode.Period, Enum.KeyCode.PageUp, Enum.KeyCode.PageDown)
3095 self:BindAction("BaseCameraKeyboardZoom", function(name, state, input) return self:DoKeyboardZoom(name, state, input) end,
3096 false, Enum.KeyCode.I, Enum.KeyCode.O)
3097end
3098
3099function BaseCamera:OnTouchBegan(input, processed)
3100 local canUseDynamicTouch = self.isDynamicThumbstickEnabled and not processed
3101 if canUseDynamicTouch then
3102 self.fingerTouches[input] = processed
3103 if not processed then
3104 self.inputStartPositions[input] = input.Position
3105 self.inputStartTimes[input] = tick()
3106 self.numUnsunkTouches = self.numUnsunkTouches + 1
3107 end
3108 end
3109end
3110
3111function BaseCamera:OnTouchChanged(input, processed)
3112 if self.fingerTouches[input] == nil then
3113 if self.isDynamicThumbstickEnabled then
3114 return
3115 end
3116 self.fingerTouches[input] = processed
3117 if not processed then
3118 self.numUnsunkTouches = self.numUnsunkTouches + 1
3119 end
3120 elseif self.isDynamicThumbstickEnabled and self.fingerTouches[input] ~= processed then
3121 --This is necessary to allow the dynamic thumbstick to steal touches after passing the InputBegan state.
3122 self.fingerTouches[input] = processed
3123 if processed then
3124 self.numUnsunkTouches = self.numUnsunkTouches - 1
3125 else
3126 self.numUnsunkTouches = self.numUnsunkTouches + 1
3127 end
3128 end
3129
3130 if self.numUnsunkTouches == 1 then
3131 if self.fingerTouches[input] == false then
3132 self.panBeginLook = self.panBeginLook or self:GetCameraLookVector()
3133 self.startPos = self.startPos or input.Position
3134 self.lastPos = self.lastPos or self.startPos
3135 self.userPanningTheCamera = true
3136
3137 local delta = input.Position - self.lastPos
3138 delta = Vector2.new(delta.X, delta.Y * UserGameSettings:GetCameraYInvertValue())
3139 if self.panEnabled then
3140 local desiredXYVector = self:InputTranslationToCameraAngleChange(delta, TOUCH_SENSITIVTY)
3141 self.rotateInput = self.rotateInput + desiredXYVector
3142 end
3143 self.lastPos = input.Position
3144 end
3145 else
3146 self.panBeginLook = nil
3147 self.startPos = nil
3148 self.lastPos = nil
3149 self.userPanningTheCamera = false
3150 end
3151 if self.numUnsunkTouches == 2 then
3152 local unsunkTouches = {}
3153 for touch, wasSunk in pairs(self.fingerTouches) do
3154 if not wasSunk then
3155 table.insert(unsunkTouches, touch)
3156 end
3157 end
3158 if #unsunkTouches == 2 then
3159 local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude
3160 if self.startingDiff and self.pinchBeginZoom then
3161 local scale = difference / math.max(0.01, self.startingDiff)
3162 local clampedScale = Util.Clamp(0.1, 10, scale)
3163 if self.distanceChangeEnabled then
3164 self:SetCameraToSubjectDistance(self.pinchBeginZoom / clampedScale)
3165 end
3166 else
3167 self.startingDiff = difference
3168 self.pinchBeginZoom = self:GetCameraToSubjectDistance()
3169 end
3170 end
3171 else
3172 self.startingDiff = nil
3173 self.pinchBeginZoom = nil
3174 end
3175end
3176
3177function BaseCamera:OnTouchEnded(input, processed)
3178 if self.fingerTouches[input] == false then
3179 if self.numUnsunkTouches == 1 then
3180 self.panBeginLook = nil
3181 self.startPos = nil
3182 self.lastPos = nil
3183 self.userPanningTheCamera = false
3184 elseif self.numUnsunkTouches == 2 then
3185 self.startingDiff = nil
3186 self.pinchBeginZoom = nil
3187 end
3188 end
3189
3190 if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
3191 self.numUnsunkTouches = self.numUnsunkTouches - 1
3192 end
3193 self.fingerTouches[input] = nil
3194 self.inputStartPositions[input] = nil
3195 self.inputStartTimes[input] = nil
3196end
3197
3198function BaseCamera:OnMouse2Down(input, processed)
3199 if processed then return end
3200
3201 self.isRightMouseDown = true
3202 self:OnMousePanButtonPressed(input, processed)
3203end
3204
3205function BaseCamera:OnMouse2Up(input, processed)
3206 self.isRightMouseDown = false
3207 self:OnMousePanButtonReleased(input, processed)
3208end
3209
3210function BaseCamera:OnMouse3Down(input, processed)
3211 if processed then return end
3212
3213 self.isMiddleMouseDown = true
3214 self:OnMousePanButtonPressed(input, processed)
3215end
3216
3217function BaseCamera:OnMouse3Up(input, processed)
3218 self.isMiddleMouseDown = false
3219 self:OnMousePanButtonReleased(input, processed)
3220end
3221
3222function BaseCamera:OnMouseMoved(input, processed)
3223 if not self.hasGameLoaded and VRService.VREnabled then
3224 return
3225 end
3226
3227 local inputDelta = input.Delta
3228 inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * UserGameSettings:GetCameraYInvertValue())
3229
3230 if self.panEnabled and ((self.startPos and self.lastPos and self.panBeginLook) or self.inFirstPerson or self.inMouseLockedMode) then
3231 local desiredXYVector = self:InputTranslationToCameraAngleChange(inputDelta,MOUSE_SENSITIVITY)
3232 self.rotateInput = self.rotateInput + desiredXYVector
3233 end
3234
3235 if self.startPos and self.lastPos and self.panBeginLook then
3236 self.lastPos = self.lastPos + input.Delta
3237 end
3238end
3239
3240function BaseCamera:OnMousePanButtonPressed(input, processed)
3241 if processed then return end
3242 self:UpdateMouseBehavior()
3243 self.panBeginLook = self.panBeginLook or self:GetCameraLookVector()
3244 self.startPos = self.startPos or input.Position
3245 self.lastPos = self.lastPos or self.startPos
3246 self.userPanningTheCamera = true
3247end
3248
3249function BaseCamera:OnMousePanButtonReleased(input, processed)
3250 self:UpdateMouseBehavior()
3251 if not (self.isRightMouseDown or self.isMiddleMouseDown) then
3252 self.panBeginLook = nil
3253 self.startPos = nil
3254 self.lastPos = nil
3255 self.userPanningTheCamera = false
3256 end
3257end
3258
3259function BaseCamera:OnMouseWheel(input, processed)
3260 if not self.hasGameLoaded and VRService.VREnabled then
3261 return
3262 end
3263 if not processed then
3264 if self.distanceChangeEnabled then
3265 local wheelInput = Util.Clamp(-1, 1, -input.Position.Z)
3266
3267 local newDistance
3268 if self.inFirstPerson and wheelInput > 0 then
3269 newDistance = FIRST_PERSON_DISTANCE_THRESHOLD
3270 else
3271 -- The 0.156 and 1.7 values are the slope and intercept of a line that is replacing the old
3272 -- rk4Integrator function which was not being used as an integrator, only to get a delta as a function of distance,
3273 -- which was linear as it was being used. These constants preserve the status quo behavior.
3274 newDistance = self.currentSubjectDistance + 0.156 * self.currentSubjectDistance * wheelInput + 1.7 * math.sign(wheelInput)
3275 end
3276
3277 self:SetCameraToSubjectDistance(newDistance)
3278 end
3279 end
3280end
3281
3282--Remove with FFlagPlayerScriptsBindAtPriority2
3283function BaseCamera:OnKeyDown(input, processed)
3284 if not self.hasGameLoaded and VRService.VREnabled then
3285 return
3286 end
3287
3288 if processed then
3289 return
3290 end
3291
3292 if self.distanceChangeEnabled then
3293 if input.KeyCode == Enum.KeyCode.I then
3294 self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 )
3295 elseif input.KeyCode == Enum.KeyCode.O then
3296 self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 )
3297 end
3298 end
3299
3300 if self.panBeginLook == nil and self.keyPanEnabled then
3301 if input.KeyCode == Enum.KeyCode.Left then
3302 self.turningLeft = true
3303 elseif input.KeyCode == Enum.KeyCode.Right then
3304 self.turningRight = true
3305 elseif input.KeyCode == Enum.KeyCode.Comma then
3306 local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), -math.pi*0.1875, math.pi*0.25)
3307 if angle ~= 0 then
3308 self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
3309 self.lastUserPanCamera = tick()
3310 self.lastCameraTransform = nil
3311 end
3312 elseif input.KeyCode == Enum.KeyCode.Period then
3313 local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), math.pi*0.1875, math.pi*0.25)
3314 if angle ~= 0 then
3315 self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
3316 self.lastUserPanCamera = tick()
3317 self.lastCameraTransform = nil
3318 end
3319 elseif input.KeyCode == Enum.KeyCode.PageUp then
3320 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(15))
3321 self.lastCameraTransform = nil
3322 elseif input.KeyCode == Enum.KeyCode.PageDown then
3323 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(-15))
3324 self.lastCameraTransform = nil
3325 end
3326 end
3327end
3328
3329--Remove with FFlagPlayerScriptsBindAtPriority2
3330function BaseCamera:OnKeyUp(input, processed)
3331 if input.KeyCode == Enum.KeyCode.Left then
3332 self.turningLeft = false
3333 elseif input.KeyCode == Enum.KeyCode.Right then
3334 self.turningRight = false
3335 end
3336end
3337
3338function BaseCamera:UpdateMouseBehavior()
3339 -- first time transition to first person mode or mouse-locked third person
3340 if self.inFirstPerson or self.inMouseLockedMode then
3341 UserGameSettings.RotationType = Enum.RotationType.CameraRelative
3342 UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
3343 else
3344 UserGameSettings.RotationType = Enum.RotationType.MovementRelative
3345 if self.isRightMouseDown or self.isMiddleMouseDown then
3346 UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
3347 else
3348 UserInputService.MouseBehavior = Enum.MouseBehavior.Default
3349 end
3350 end
3351end
3352
3353function BaseCamera:UpdateForDistancePropertyChange()
3354 -- Calling this setter with the current value will force checking that it is still
3355 -- in range after a change to the min/max distance limits
3356 self:SetCameraToSubjectDistance(self.currentSubjectDistance)
3357end
3358
3359function BaseCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
3360 local player = Players.LocalPlayer
3361
3362 local lastSubjectDistance = self.currentSubjectDistance
3363
3364 -- By default, camera modules will respect LockFirstPerson and override the currentSubjectDistance with 0
3365 -- regardless of what Player.CameraMinZoomDistance is set to, so that first person can be made
3366 -- available by the developer without needing to allow players to mousewheel dolly into first person.
3367 -- Some modules will override this function to remove or change first-person capability.
3368 if player.CameraMode == Enum.CameraMode.LockFirstPerson then
3369 self.currentSubjectDistance = 0.5
3370 if not self.inFirstPerson then
3371 self:EnterFirstPerson()
3372 end
3373 else
3374 local newSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, desiredSubjectDistance)
3375 if newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD then
3376 self.currentSubjectDistance = 0.5
3377 if not self.inFirstPerson then
3378 self:EnterFirstPerson()
3379 end
3380 else
3381 self.currentSubjectDistance = newSubjectDistance
3382 if self.inFirstPerson then
3383 self:LeaveFirstPerson()
3384 end
3385 end
3386 end
3387
3388 -- Pass target distance and zoom direction to the zoom controller
3389 ZoomController.SetZoomParameters(self.currentSubjectDistance, math.sign(desiredSubjectDistance - lastSubjectDistance))
3390
3391 -- Returned only for convenience to the caller to know the outcome
3392 return self.currentSubjectDistance
3393end
3394
3395function BaseCamera:SetCameraType( cameraType )
3396 --Used by derived classes
3397 self.cameraType = cameraType
3398end
3399
3400function BaseCamera:GetCameraType()
3401 return self.cameraType
3402end
3403
3404-- Movement mode standardized to Enum.ComputerCameraMovementMode values
3405function BaseCamera:SetCameraMovementMode( cameraMovementMode )
3406 self.cameraMovementMode = cameraMovementMode
3407end
3408
3409function BaseCamera:GetCameraMovementMode()
3410 return self.cameraMovementMode
3411end
3412
3413function BaseCamera:SetIsMouseLocked(mouseLocked)
3414 self.inMouseLockedMode = mouseLocked
3415 self:UpdateMouseBehavior()
3416end
3417
3418function BaseCamera:GetIsMouseLocked()
3419 return self.inMouseLockedMode
3420end
3421
3422function BaseCamera:SetMouseLockOffset(offsetVector)
3423 self.mouseLockOffset = offsetVector
3424end
3425
3426function BaseCamera:GetMouseLockOffset()
3427 return self.mouseLockOffset
3428end
3429
3430function BaseCamera:InFirstPerson()
3431 return self.inFirstPerson
3432end
3433
3434function BaseCamera:EnterFirstPerson()
3435 -- Overridden in ClassicCamera, the only module which supports FirstPerson
3436end
3437
3438function BaseCamera:LeaveFirstPerson()
3439 -- Overridden in ClassicCamera, the only module which supports FirstPerson
3440end
3441
3442-- Nominal distance, set by dollying in and out with the mouse wheel or equivalent, not measured distance
3443function BaseCamera:GetCameraToSubjectDistance()
3444 return self.currentSubjectDistance
3445end
3446
3447-- Actual measured distance to the camera Focus point, which may be needed in special circumstances, but should
3448-- never be used as the starting point for updating the nominal camera-to-subject distance (self.currentSubjectDistance)
3449-- since that is a desired target value set only by mouse wheel (or equivalent) input, PopperCam, and clamped to min max camera distance
3450function BaseCamera:GetMeasuredDistanceToFocus()
3451 local camera = game.Workspace.CurrentCamera
3452 if camera then
3453 return (camera.CoordinateFrame.p - camera.Focus.p).magnitude
3454 end
3455 return nil
3456end
3457
3458function BaseCamera:CalculateNewLookVector(suppliedLookVector)
3459 local newLookCFrame = self:CalculateNewLookCFrame(suppliedLookVector)
3460 return newLookCFrame.lookVector
3461end
3462
3463function BaseCamera:CalculateNewLookVectorVR()
3464 local subjectPosition = self:GetSubjectPosition()
3465 local vecToSubject = (subjectPosition - game.Workspace.CurrentCamera.CFrame.p)
3466 local currLookVector = (vecToSubject * X1_Y0_Z1).unit
3467 local vrRotateInput = Vector2.new(self.rotateInput.x, 0)
3468 local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
3469 local yawRotatedVector = (CFrame.Angles(0, -vrRotateInput.x, 0) * startCFrame * CFrame.Angles(-vrRotateInput.y,0,0)).lookVector
3470 return (yawRotatedVector * X1_Y0_Z1).unit
3471end
3472
3473function BaseCamera:GetHumanoid()
3474 local player = Players.LocalPlayer
3475 local character = player and player.Character
3476 if character then
3477 local resultHumanoid = self.humanoidCache[player]
3478 if resultHumanoid and resultHumanoid.Parent == character then
3479 return resultHumanoid
3480 else
3481 self.humanoidCache[player] = nil -- Bust Old Cache
3482 local humanoid = character:FindFirstChildOfClass("Humanoid")
3483 if humanoid then
3484 self.humanoidCache[player] = humanoid
3485 end
3486 return humanoid
3487 end
3488 end
3489 return nil
3490end
3491
3492function BaseCamera:GetHumanoidPartToFollow(humanoid, humanoidStateType)
3493 if humanoidStateType == Enum.HumanoidStateType.Dead then
3494 local character = humanoid.Parent
3495 if character then
3496 return character:FindFirstChild("Head") or humanoid.Torso
3497 else
3498 return humanoid.Torso
3499 end
3500 else
3501 return humanoid.Torso
3502 end
3503end
3504
3505function BaseCamera:UpdateGamepad()
3506 local gamepadPan = self.gamepadPanningCamera
3507 if gamepadPan and (self.hasGameLoaded or not VRService.VREnabled) then
3508 gamepadPan = Util.GamepadLinearToCurve(gamepadPan)
3509 local currentTime = tick()
3510 if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then
3511 self.userPanningTheCamera = true
3512 elseif gamepadPan == ZERO_VECTOR2 then
3513 self.lastThumbstickRotate = nil
3514 if self.lastThumbstickPos == ZERO_VECTOR2 then
3515 self.currentSpeed = 0
3516 end
3517 end
3518
3519 local finalConstant = 0
3520
3521 if self.lastThumbstickRotate then
3522 if VRService.VREnabled then
3523 self.currentSpeed = self.vrMaxSpeed
3524 else
3525 local elapsedTime = (currentTime - self.lastThumbstickRotate) * 10
3526 self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds))
3527
3528 if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end
3529
3530 if self.lastVelocity then
3531 local velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
3532 local velocityDeltaMag = (velocity - self.lastVelocity).magnitude
3533
3534 if velocityDeltaMag > 12 then
3535 self.currentSpeed = self.currentSpeed * (20/velocityDeltaMag)
3536 if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end
3537 end
3538 end
3539 end
3540
3541 finalConstant = UserGameSettings.GamepadCameraSensitivity * self.currentSpeed
3542 self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
3543 end
3544
3545 self.lastThumbstickPos = gamepadPan
3546 self.lastThumbstickRotate = currentTime
3547
3548 return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:GetCameraYInvertValue())
3549 end
3550
3551 return ZERO_VECTOR2
3552end
3553
3554-- [[ VR Support Section ]] --
3555
3556function BaseCamera:ApplyVRTransform()
3557 if not VRService.VREnabled then
3558 return
3559 end
3560
3561 --we only want this to happen in first person VR
3562 local rootJoint = self.humanoidRootPart and self.humanoidRootPart:FindFirstChild("RootJoint")
3563 if not rootJoint then
3564 return
3565 end
3566
3567 local cameraSubject = game.Workspace.CurrentCamera.CameraSubject
3568 local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat")
3569
3570 if self.inFirstPerson and not isInVehicle then
3571 local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head)
3572 local vrRotation = vrFrame - vrFrame.p
3573 rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0)
3574 else
3575 rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0)
3576 end
3577end
3578
3579function BaseCamera:IsInFirstPerson()
3580 return self.inFirstPerson
3581end
3582
3583function BaseCamera:ShouldUseVRRotation()
3584 if not VRService.VREnabled then
3585 return false
3586 end
3587
3588 if not self.VRRotationIntensityAvailable and tick() - self.lastVRRotationIntensityCheckTime < 1 then
3589 return false
3590 end
3591
3592 local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end)
3593 self.VRRotationIntensityAvailable = success and vrRotationIntensity ~= nil
3594 self.lastVRRotationIntensityCheckTime = tick()
3595
3596 self.shouldUseVRRotation = success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth"
3597
3598 return self.shouldUseVRRotation
3599end
3600
3601function BaseCamera:GetVRRotationInput()
3602 local vrRotateSum = ZERO_VECTOR2
3603 local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end)
3604
3605 if not success then
3606 return
3607 end
3608
3609 local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2
3610 local delayExpired = (tick() - self.lastVRRotationTime) >= self:GetRepeatDelayValue(vrRotationIntensity)
3611
3612 if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then
3613 if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then
3614 local sign = 1
3615 if vrGamepadRotation.x < 0 then
3616 sign = -1
3617 end
3618 vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign
3619 self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true
3620 end
3621 elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then
3622 self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil
3623 end
3624 if self.turningLeft then
3625 if delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Left] then
3626 vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity)
3627 self.vrRotateKeyCooldown[Enum.KeyCode.Left] = true
3628 end
3629 else
3630 self.vrRotateKeyCooldown[Enum.KeyCode.Left] = nil
3631 end
3632 if self.turningRight then
3633 if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Right]) then
3634 vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity)
3635 self.vrRotateKeyCooldown[Enum.KeyCode.Right] = true
3636 end
3637 else
3638 self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil
3639 end
3640
3641 if vrRotateSum ~= ZERO_VECTOR2 then
3642 self.lastVRRotationTime = tick()
3643 end
3644
3645 return vrRotateSum
3646end
3647
3648function BaseCamera:CancelCameraFreeze(keepConstraints)
3649 if not keepConstraints then
3650 self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z)
3651 end
3652 if self.cameraFrozen then
3653 self.trackingHumanoid = nil
3654 self.cameraFrozen = false
3655 end
3656end
3657
3658function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack)
3659 if not self.cameraFrozen then
3660 self.humanoidJumpOrigin = subjectPosition
3661 self.trackingHumanoid = humanoidToTrack
3662 self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z)
3663 self.cameraFrozen = true
3664 end
3665end
3666
3667function BaseCamera:OnNewCameraSubject()
3668 if self.subjectStateChangedConn then
3669 self.subjectStateChangedConn:Disconnect()
3670 self.subjectStateChangedConn = nil
3671 end
3672
3673 local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject
3674 if self.trackingHumanoid ~= humanoid then
3675 self:CancelCameraFreeze()
3676 end
3677 if humanoid and humanoid:IsA("Humanoid") then
3678 self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState)
3679 if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then
3680 self:StartCameraFreeze(self:GetSubjectPosition(), humanoid)
3681 elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then
3682 self:CancelCameraFreeze(true)
3683 end
3684 end)
3685 end
3686end
3687
3688function BaseCamera:GetVRFocus(subjectPosition, timeDelta)
3689 local lastFocus = self.LastCameraFocus or subjectPosition
3690 if not self.cameraFrozen then
3691 self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z)
3692 end
3693
3694 local newFocus
3695 if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then
3696 newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z))
3697 else
3698 newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y))
3699 end
3700
3701 if self.cameraFrozen then
3702 -- No longer in 3rd person
3703 if self.inFirstPerson then -- not VRService.VREnabled
3704 self:CancelCameraFreeze()
3705 end
3706 -- This case you jumped off a cliff and want to keep your character in view
3707 -- 0.5 is to fix floating point error when not jumping off cliffs
3708 if self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then
3709 self:CancelCameraFreeze()
3710 end
3711 end
3712
3713 return newFocus
3714end
3715
3716function BaseCamera:GetRotateAmountValue(vrRotationIntensity)
3717 vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
3718 if vrRotationIntensity then
3719 if vrRotationIntensity == "Low" then
3720 return VR_LOW_INTENSITY_ROTATION
3721 elseif vrRotationIntensity == "High" then
3722 return VR_HIGH_INTENSITY_ROTATION
3723 end
3724 end
3725 return ZERO_VECTOR2
3726end
3727
3728function BaseCamera:GetRepeatDelayValue(vrRotationIntensity)
3729 vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
3730 if vrRotationIntensity then
3731 if vrRotationIntensity == "Low" then
3732 return VR_LOW_INTENSITY_REPEAT
3733 elseif vrRotationIntensity == "High" then
3734 return VR_HIGH_INTENSITY_REPEAT
3735 end
3736 end
3737 return 0
3738end
3739
3740function BaseCamera:Test()
3741 print("BaseCamera:Test()")
3742end
3743
3744function BaseCamera:Update(dt)
3745 warn("BaseCamera:Update() This is a virtual function that should never be getting called.")
3746 return game.Workspace.CurrentCamera.CFrame, game.Workspace.CurrentCamera.Focus
3747end
3748
3749return BaseCamera
3750
3751--[[
3752 BaseOcclusion - Abstract base class for character occlusion control modules
3753 2018 Camera Update - AllYourBlox
3754--]]
3755
3756--[[ The Module ]]--
3757local BaseOcclusion = {}
3758BaseOcclusion.__index = BaseOcclusion
3759setmetatable(BaseOcclusion, { __call = function(_, ...) return BaseOcclusion.new(...) end})
3760
3761function BaseOcclusion.new()
3762 local self = setmetatable({}, BaseOcclusion)
3763 return self
3764end
3765
3766-- Called when character is added
3767function BaseOcclusion:CharacterAdded(char, player)
3768
3769end
3770
3771-- Called when character is about to be removed
3772function BaseOcclusion:CharacterRemoving(char, player)
3773
3774end
3775
3776function BaseOcclusion:OnCameraSubjectChanged(newSubject)
3777
3778end
3779
3780--[[ Derived classes are required to override and implement all of the following functions ]]--
3781function GetOcclusionMode()
3782 -- Must be overridden in derived classes to return an Enum.DevCameraOcclusionMode value
3783 warn("BaseOcclusion GetOcclusionMode must be overridden by derived classes")
3784 return nil
3785end
3786
3787function BaseOcclusion:Enable(enabled)
3788 warn("BaseOcclusion Enable must be overridden by derived classes")
3789end
3790
3791function BaseOcclusion:Update(dt, desiredCameraCFrame, desiredCameraFocus)
3792 warn("BaseOcclusion Update must be overridden by derived classes")
3793 return desiredCameraCFrame, desiredCameraFocus
3794end
3795
3796return BaseOcclusion
3797
3798--[[
3799 CameraUtils - Math utility functions shared by multiple camera scripts
3800 2018 Camera Update - AllYourBlox
3801--]]
3802
3803local CameraUtils = {}
3804
3805local function round(num)
3806 return math.floor(num + 0.5)
3807end
3808
3809-- Note, arguments do not match the new math.clamp
3810-- Eventually we will replace these calls with math.clamp, but right now
3811-- this is safer as math.clamp is not tolerant of min>max
3812function CameraUtils.Clamp(low, high, val)
3813 return math.min(math.max(val, low), high)
3814end
3815
3816-- From TransparencyController
3817function CameraUtils.Round(num, places)
3818 local decimalPivot = 10^places
3819 return math.floor(num * decimalPivot + 0.5) / decimalPivot
3820end
3821
3822function CameraUtils.IsFinite(val)
3823 return val == val and val ~= math.huge and val ~= -math.huge
3824end
3825
3826function CameraUtils.IsFiniteVector3(vec3)
3827 return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z)
3828end
3829
3830-- Legacy implementation renamed
3831function CameraUtils.GetAngleBetweenXZVectors(v1, v2)
3832 return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
3833end
3834
3835function CameraUtils.RotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount)
3836 if camLook.Magnitude > 0 then
3837 camLook = camLook.unit
3838 local currAngle = math.atan2(camLook.z, camLook.x)
3839 local newAngle = round((math.atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount
3840 return newAngle - currAngle
3841 end
3842 return 0
3843end
3844
3845-- K is a tunable parameter that changes the shape of the S-curve
3846-- the larger K is the more straight/linear the curve gets
3847local k = 0.35
3848local lowerK = 0.8
3849local function SCurveTranform(t)
3850 t = CameraUtils.Clamp(-1,1,t)
3851 if t >= 0 then
3852 return (k*t) / (k - t + 1)
3853 end
3854 return -((lowerK*-t) / (lowerK + t + 1))
3855end
3856
3857local DEADZONE = 0.1
3858local function toSCurveSpace(t)
3859 return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE
3860end
3861
3862local function fromSCurveSpace(t)
3863 return t/2 + 0.5
3864end
3865
3866function CameraUtils.GamepadLinearToCurve(thumbstickPosition)
3867 local function onAxis(axisValue)
3868 local sign = 1
3869 if axisValue < 0 then
3870 sign = -1
3871 end
3872 local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue))))
3873 point = point * sign
3874 return CameraUtils.Clamp(-1, 1, point)
3875 end
3876 return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y))
3877end
3878
3879-- This function converts 4 different, redundant enumeration types to one standard so the values can be compared
3880function CameraUtils.ConvertCameraModeEnumToStandard( enumValue )
3881
3882 if enumValue == Enum.TouchCameraMovementMode.Default then
3883 return Enum.ComputerCameraMovementMode.Follow
3884 end
3885
3886 if enumValue == Enum.ComputerCameraMovementMode.Default then
3887 return Enum.ComputerCameraMovementMode.Classic
3888 end
3889
3890 if enumValue == Enum.TouchCameraMovementMode.Classic or
3891 enumValue == Enum.DevTouchCameraMovementMode.Classic or
3892 enumValue == Enum.DevComputerCameraMovementMode.Classic or
3893 enumValue == Enum.ComputerCameraMovementMode.Classic then
3894 return Enum.ComputerCameraMovementMode.Classic
3895 end
3896
3897 if enumValue == Enum.TouchCameraMovementMode.Follow or
3898 enumValue == Enum.DevTouchCameraMovementMode.Follow or
3899 enumValue == Enum.DevComputerCameraMovementMode.Follow or
3900 enumValue == Enum.ComputerCameraMovementMode.Follow then
3901 return Enum.ComputerCameraMovementMode.Follow
3902 end
3903
3904 if enumValue == Enum.TouchCameraMovementMode.Orbital or
3905 enumValue == Enum.DevTouchCameraMovementMode.Orbital or
3906 enumValue == Enum.DevComputerCameraMovementMode.Orbital or
3907 enumValue == Enum.ComputerCameraMovementMode.Orbital then
3908 return Enum.ComputerCameraMovementMode.Orbital
3909 end
3910
3911 -- Note: Only the Dev versions of the Enums have UserChoice as an option
3912 if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or
3913 enumValue == Enum.DevComputerCameraMovementMode.UserChoice then
3914 return Enum.DevComputerCameraMovementMode.UserChoice
3915 end
3916
3917 -- For any unmapped options return Classic camera
3918 return Enum.ComputerCameraMovementMode.Classic
3919end
3920
3921return CameraUtils
3922
3923--[[
3924 ClassicCamera - Classic Roblox camera control module
3925 2018 Camera Update - AllYourBlox
3926
3927 Note: This module also handles camera control types Follow and Track, the
3928 latter of which is currently not distinguished from Classic
3929--]]
3930
3931-- Local private variables and constants
3932local ZERO_VECTOR2 = Vector2.new(0,0)
3933
3934local tweenAcceleration = math.rad(220) --Radians/Second^2
3935local tweenSpeed = math.rad(0) --Radians/Second
3936local tweenMaxSpeed = math.rad(250) --Radians/Second
3937local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
3938
3939local INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0)
3940
3941--[[ Services ]]--
3942local PlayersService = game:GetService('Players')
3943local VRService = game:GetService("VRService")
3944
3945local Util = require(script.Parent:WaitForChild("CameraUtils"))
3946
3947--[[ The Module ]]--
3948local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
3949local ClassicCamera = setmetatable({}, BaseCamera)
3950ClassicCamera.__index = ClassicCamera
3951
3952function ClassicCamera.new()
3953 local self = setmetatable(BaseCamera.new(), ClassicCamera)
3954
3955 self.isFollowCamera = false
3956 self.lastUpdate = tick()
3957
3958 return self
3959end
3960
3961function ClassicCamera:GetModuleName()
3962 return "ClassicCamera"
3963end
3964
3965-- Movement mode standardized to Enum.ComputerCameraMovementMode values
3966function ClassicCamera:SetCameraMovementMode( cameraMovementMode )
3967 BaseCamera.SetCameraMovementMode(self,cameraMovementMode)
3968 self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow
3969end
3970
3971function ClassicCamera:Test()
3972 print("ClassicCamera:Test()")
3973end
3974
3975function ClassicCamera:Update()
3976 local now = tick()
3977 local timeDelta = (now - self.lastUpdate)
3978
3979 local camera = workspace.CurrentCamera
3980 local newCameraCFrame = camera.CFrame
3981 local newCameraFocus = camera.Focus
3982 local overrideCameraLookVector = nil
3983 if self.resetCameraAngle then
3984 local rootPart = self:GetHumanoidRootPart()
3985 if rootPart then
3986 overrideCameraLookVector = (rootPart.CFrame * INITIAL_CAMERA_ANGLE).lookVector
3987 else
3988 overrideCameraLookVector = INITIAL_CAMERA_ANGLE.lookVector
3989 end
3990 self.resetCameraAngle = false
3991 end
3992
3993 local player = PlayersService.LocalPlayer
3994 local humanoid = self:GetHumanoid()
3995 local cameraSubject = camera.CameraSubject
3996 local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
3997 local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
3998 local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
3999
4000 if self.lastUpdate == nil or timeDelta > 1 then
4001 self.lastCameraTransform = nil
4002 end
4003
4004 if self.lastUpdate then
4005 local gamepadRotation = self:UpdateGamepad()
4006
4007 if self:ShouldUseVRRotation() then
4008 self.rotateInput = self.rotateInput + self:GetVRRotationInput()
4009 else
4010 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
4011 local delta = math.min(0.1, timeDelta)
4012
4013 if gamepadRotation ~= ZERO_VECTOR2 then
4014 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
4015 end
4016
4017 local angle = 0
4018 if not (isInVehicle or isOnASkateboard) then
4019 angle = angle + (self.turningLeft and -120 or 0)
4020 angle = angle + (self.turningRight and 120 or 0)
4021 end
4022
4023 if angle ~= 0 then
4024 self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
4025 end
4026 end
4027 end
4028
4029 -- Reset tween speed if user is panning
4030 if self.userPanningTheCamera then
4031 tweenSpeed = 0
4032 self.lastUserPanCamera = tick()
4033 end
4034
4035 local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
4036 local subjectPosition = self:GetSubjectPosition()
4037
4038 if subjectPosition and player and camera then
4039 local zoom = self:GetCameraToSubjectDistance()
4040 if zoom < 0.5 then
4041 zoom = 0.5
4042 end
4043
4044 if self:GetIsMouseLocked() and not self:IsInFirstPerson() then
4045 -- We need to use the right vector of the camera after rotation, not before
4046 local newLookCFrame = self:CalculateNewLookCFrame(overrideCameraLookVector)
4047
4048 local offset = self:GetMouseLockOffset()
4049 local cameraRelativeOffset = offset.X * newLookCFrame.rightVector + offset.Y * newLookCFrame.upVector + offset.Z * newLookCFrame.lookVector
4050
4051 --offset can be NAN, NAN, NAN if newLookVector has only y component
4052 if Util.IsFiniteVector3(cameraRelativeOffset) then
4053 subjectPosition = subjectPosition + cameraRelativeOffset
4054 end
4055 else
4056 if not self.userPanningTheCamera and self.lastCameraTransform then
4057
4058 local isInFirstPerson = self:IsInFirstPerson()
4059
4060 if (isInVehicle or isOnASkateboard or (self.isFollowCamera and isClimbing)) and self.lastUpdate and humanoid and humanoid.Torso then
4061 if isInFirstPerson then
4062 if self.lastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
4063 local y = -Util.GetAngleBetweenXZVectors(self.lastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
4064 if Util.IsFinite(y) then
4065 self.rotateInput = self.rotateInput + Vector2.new(y, 0)
4066 end
4067 tweenSpeed = 0
4068 end
4069 elseif not userRecentlyPannedCamera then
4070 local forwardVector = humanoid.Torso.CFrame.lookVector
4071 if isOnASkateboard then
4072 forwardVector = cameraSubject.CFrame.lookVector
4073 end
4074
4075 tweenSpeed = Util.Clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
4076
4077 local percent = Util.Clamp(0, 1, tweenSpeed * timeDelta)
4078 if self:IsInFirstPerson() and not (self.isFollowCamera and self.isClimbing) then
4079 percent = 1
4080 end
4081
4082 local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
4083 if Util.IsFinite(y) and math.abs(y) > 0.0001 then
4084 self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0)
4085 end
4086 end
4087
4088 elseif self.isFollowCamera and (not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled) then
4089 -- Logic that was unique to the old FollowCamera module
4090 local lastVec = -(self.lastCameraTransform.p - subjectPosition)
4091
4092 local y = Util.GetAngleBetweenXZVectors(lastVec, self:GetCameraLookVector())
4093
4094 -- This cutoff is to decide if the humanoid's angle of movement,
4095 -- relative to the camera's look vector, is enough that
4096 -- we want the camera to be following them. The point is to provide
4097 -- a sizable dead zone to allow more precise forward movements.
4098 local thetaCutoff = 0.4
4099
4100 -- Check for NaNs
4101 if Util.IsFinite(y) and math.abs(y) > 0.0001 and math.abs(y) > thetaCutoff * timeDelta then
4102 self.rotateInput = self.rotateInput + Vector2.new(y, 0)
4103 end
4104 end
4105 end
4106 end
4107
4108 if not self.isFollowCamera then
4109 local VREnabled = VRService.VREnabled
4110
4111 if VREnabled then
4112 newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
4113 else
4114 newCameraFocus = CFrame.new(subjectPosition)
4115 end
4116
4117 local cameraFocusP = newCameraFocus.p
4118 if VREnabled and not self:IsInFirstPerson() then
4119 local cameraHeight = self:GetCameraHeight()
4120 local vecToSubject = (subjectPosition - camera.CFrame.p)
4121 local distToSubject = vecToSubject.magnitude
4122
4123 -- Only move the camera if it exceeded a maximum distance to the subject in VR
4124 if distToSubject > zoom or self.rotateInput.x ~= 0 then
4125 local desiredDist = math.min(distToSubject, zoom)
4126 vecToSubject = self:CalculateNewLookVectorVR() * desiredDist
4127 local newPos = cameraFocusP - vecToSubject
4128 local desiredLookDir = camera.CFrame.lookVector
4129 if self.rotateInput.x ~= 0 then
4130 desiredLookDir = vecToSubject
4131 end
4132 local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
4133 self.rotateInput = ZERO_VECTOR2
4134
4135 newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
4136 end
4137 else
4138 local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
4139 self.rotateInput = ZERO_VECTOR2
4140 newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
4141 end
4142 else -- is FollowCamera
4143 local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
4144 self.rotateInput = ZERO_VECTOR2
4145
4146 if VRService.VREnabled then
4147 newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
4148 else
4149 newCameraFocus = CFrame.new(subjectPosition)
4150 end
4151 newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, self:GetCameraHeight(), 0)
4152 end
4153
4154 self.lastCameraTransform = newCameraCFrame
4155 self.lastCameraFocus = newCameraFocus
4156 if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
4157 self.lastSubjectCFrame = cameraSubject.CFrame
4158 else
4159 self.lastSubjectCFrame = nil
4160 end
4161 end
4162
4163 self.lastUpdate = now
4164 return newCameraCFrame, newCameraFocus
4165end
4166
4167function ClassicCamera:EnterFirstPerson()
4168 self.inFirstPerson = true
4169 self:UpdateMouseBehavior()
4170end
4171
4172function ClassicCamera:LeaveFirstPerson()
4173 self.inFirstPerson = false
4174 self:UpdateMouseBehavior()
4175end
4176
4177return ClassicCamera
4178
4179--[[
4180 Invisicam - Occlusion module that makes objects occluding character view semi-transparent
4181 2018 Camera Update - AllYourBlox
4182--]]
4183
4184--[[ Camera Maths Utilities Library ]]--
4185local Util = require(script.Parent:WaitForChild("CameraUtils"))
4186
4187--[[ Top Level Roblox Services ]]--
4188local PlayersService = game:GetService("Players")
4189local RunService = game:GetService("RunService")
4190
4191--[[ Constants ]]--
4192local ZERO_VECTOR3 = Vector3.new(0,0,0)
4193local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY
4194local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays
4195local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays
4196
4197local MODE = {
4198 --CUSTOM = 1, -- Retired, unused
4199 LIMBS = 2, -- Track limbs
4200 MOVEMENT = 3, -- Track movement
4201 CORNERS = 4, -- Char model corners
4202 CIRCLE1 = 5, -- Circle of casts around character
4203 CIRCLE2 = 6, -- Circle of casts around character, camera relative
4204 LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode
4205 SMART_CIRCLE = 8, -- More sample points on and around character
4206 CHAR_OUTLINE = 9, -- Dynamic outline around the character
4207}
4208
4209local LIMB_TRACKING_SET = {
4210 -- Body parts common to R15 and R6
4211 ['Head'] = true,
4212
4213 -- Body parts unique to R6
4214 ['Left Arm'] = true,
4215 ['Right Arm'] = true,
4216 ['Left Leg'] = true,
4217 ['Right Leg'] = true,
4218
4219 -- Body parts unique to R15
4220 ['LeftLowerArm'] = true,
4221 ['RightLowerArm'] = true,
4222 ['LeftUpperLeg'] = true,
4223 ['RightUpperLeg'] = true
4224}
4225
4226local CORNER_FACTORS = {
4227 Vector3.new(1,1,-1),
4228 Vector3.new(1,-1,-1),
4229 Vector3.new(-1,-1,-1),
4230 Vector3.new(-1,1,-1)
4231}
4232
4233local CIRCLE_CASTS = 10
4234local MOVE_CASTS = 3
4235local SMART_CIRCLE_CASTS = 24
4236local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS
4237local CHAR_OUTLINE_CASTS = 24
4238
4239-- Used to sanitize user-supplied functions
4240local function AssertTypes(param, ...)
4241 local allowedTypes = {}
4242 local typeString = ''
4243 for _, typeName in pairs({...}) do
4244 allowedTypes[typeName] = true
4245 typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName
4246 end
4247 local theType = type(param)
4248 assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType)
4249end
4250
4251-- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons
4252local function Det3x3(a,b,c,d,e,f,g,h,i)
4253 return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g))
4254end
4255
4256-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane
4257-- because they are generated from cross products with a common vector. This function is computing
4258-- that intersection, but it's actually the general solution for the point halfway between where
4259-- two skew lines come nearest to each other, which is more forgiving.
4260local function RayIntersection(p0, v0, p1, v1)
4261 local v2 = v0:Cross(v1)
4262 local d1 = p1.x - p0.x
4263 local d2 = p1.y - p0.y
4264 local d3 = p1.z - p0.z
4265 local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z)
4266
4267 if (denom == 0) then
4268 return ZERO_VECTOR3 -- No solution (rays are parallel)
4269 end
4270
4271 local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom
4272 local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom
4273 local s0 = p0 + t0 * v0
4274 local s1 = p1 + t1 * v1
4275 local s = s0 + 0.5 * ( s1 - s0 )
4276
4277 -- 0.25 studs is a threshold for deciding if the rays are
4278 -- close enough to be considered intersecting, found through testing
4279 if (s1-s0).Magnitude < 0.25 then
4280 return s
4281 else
4282 return ZERO_VECTOR3
4283 end
4284end
4285
4286
4287
4288--[[ The Module ]]--
4289local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
4290local Invisicam = setmetatable({}, BaseOcclusion)
4291Invisicam.__index = Invisicam
4292
4293function Invisicam.new()
4294 local self = setmetatable(BaseOcclusion.new(), Invisicam)
4295
4296 self.char = nil
4297 self.humanoidRootPart = nil
4298 self.torsoPart = nil
4299 self.headPart = nil
4300
4301 self.childAddedConn = nil
4302 self.childRemovedConn = nil
4303
4304 self.behaviors = {} -- Map of modes to behavior fns
4305 self.behaviors[MODE.LIMBS] = self.LimbBehavior
4306 self.behaviors[MODE.MOVEMENT] = self.MoveBehavior
4307 self.behaviors[MODE.CORNERS] = self.CornerBehavior
4308 self.behaviors[MODE.CIRCLE1] = self.CircleBehavior
4309 self.behaviors[MODE.CIRCLE2] = self.CircleBehavior
4310 self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior
4311 self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior
4312 self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior
4313
4314 self.mode = MODE.SMART_CIRCLE
4315 self.behaviorFunction = self.SmartCircleBehavior
4316
4317
4318 self.savedHits = {} -- Objects currently being faded in/out
4319 self.trackedLimbs = {} -- Used in limb-tracking casting modes
4320
4321 self.camera = game.Workspace.CurrentCamera
4322
4323 self.enabled = false
4324 return self
4325end
4326
4327function Invisicam:Enable(enable)
4328 self.enabled = enable
4329
4330 if not enable then
4331 self:Cleanup()
4332 end
4333end
4334
4335function Invisicam:GetOcclusionMode()
4336 return Enum.DevCameraOcclusionMode.Invisicam
4337end
4338
4339--[[ Module functions ]]--
4340function Invisicam:LimbBehavior(castPoints)
4341 for limb, _ in pairs(self.trackedLimbs) do
4342 castPoints[#castPoints + 1] = limb.Position
4343 end
4344end
4345
4346function Invisicam:MoveBehavior(castPoints)
4347 for i = 1, MOVE_CASTS do
4348 local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity
4349 local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2
4350 local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed
4351 castPoints[#castPoints + 1] = position + offsetVector
4352 end
4353end
4354
4355function Invisicam:CornerBehavior(castPoints)
4356 local cframe = self.humanoidRootPart.CFrame
4357 local centerPoint = cframe.p
4358 local rotation = cframe - centerPoint
4359 local halfSize = self.char:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations
4360 castPoints[#castPoints + 1] = centerPoint
4361 for i = 1, #CORNER_FACTORS do
4362 castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i]))
4363 end
4364end
4365
4366function Invisicam:CircleBehavior(castPoints)
4367 local cframe = nil
4368 if self.mode == MODE.CIRCLE1 then
4369 cframe = self.humanoidRootPart.CFrame
4370 else
4371 local camCFrame = self.camera.CoordinateFrame
4372 cframe = camCFrame - camCFrame.p + self.humanoidRootPart.Position
4373 end
4374 castPoints[#castPoints + 1] = cframe.p
4375 for i = 0, CIRCLE_CASTS - 1 do
4376 local angle = (2 * math.pi / CIRCLE_CASTS) * i
4377 local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0)
4378 castPoints[#castPoints + 1] = cframe * offset
4379 end
4380end
4381
4382function Invisicam:LimbMoveBehavior(castPoints)
4383 self:LimbBehavior(castPoints)
4384 self:MoveBehavior(castPoints)
4385end
4386
4387function Invisicam:CharacterOutlineBehavior(castPoints)
4388 local torsoUp = self.torsoPart.CFrame.upVector.unit
4389 local torsoRight = self.torsoPart.CFrame.rightVector.unit
4390
4391 -- Torso cross of points for interior coverage
4392 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
4393 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
4394 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
4395 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
4396 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
4397 if self.headPart then
4398 castPoints[#castPoints + 1] = self.headPart.CFrame.p
4399 end
4400
4401 local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z))
4402 local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
4403
4404 local partsWhitelist = {self.torsoPart}
4405 if self.headPart then
4406 partsWhitelist[#partsWhitelist + 1] = self.headPart
4407 end
4408
4409 for i = 1, CHAR_OUTLINE_CASTS do
4410 local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS)
4411 local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0))
4412
4413 offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z)
4414
4415 local ray = Ray.new(centerPoint + offset, -3 * offset)
4416 local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false)
4417
4418 if hit then
4419 -- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against
4420 -- walls is less likely to cause a transparency glitch
4421 castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit
4422 end
4423 end
4424end
4425
4426function Invisicam:SmartCircleBehavior(castPoints)
4427 local torsoUp = self.torsoPart.CFrame.upVector.unit
4428 local torsoRight = self.torsoPart.CFrame.rightVector.unit
4429
4430 -- SMART_CIRCLE mode includes rays to head and 5 to the torso.
4431 -- Hands, arms, legs and feet are not included since they
4432 -- are not canCollide and can therefore go inside of parts
4433 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
4434 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
4435 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
4436 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
4437 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
4438 if self.headPart then
4439 castPoints[#castPoints + 1] = self.headPart.CFrame.p
4440 end
4441
4442 local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p
4443 local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
4444 local radius = 2.5
4445
4446 -- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the
4447 -- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within
4448 -- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that
4449 -- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the
4450 -- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are
4451 -- behind, below or beside the character and not really obstructing view of the character. This minimizes
4452 -- the undesirable situation where the character walks up to an exterior wall and it is made invisible even
4453 -- though it is behind the character.
4454 for i = 1, SMART_CIRCLE_CASTS do
4455 local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi
4456 local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0)
4457 local circlePoint = torsoPoint + cameraOrientation * offset
4458
4459 -- Vector from camera to point on the circle being tested
4460 local vp = circlePoint - self.camera.CFrame.p
4461
4462 local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
4463 local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
4464 local castPoint = circlePoint
4465
4466 if hit then
4467 local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface
4468 local v0 = hprime - torsoPoint -- Vector from torso to offset hit point
4469 local d0 = v0.magnitude
4470
4471 local perp = (v0:Cross(vp)).unit
4472
4473 -- Vector from the offset hit point, along the hit surface
4474 local v1 = (perp:Cross(hitNormal)).unit
4475
4476 -- Vector from camera to offset hit
4477 local vprime = (hprime - self.camera.CFrame.p).unit
4478
4479 -- This dot product checks to see if the vector along the hit surface would hit the correct
4480 -- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side
4481 if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then
4482 castPoint = RayIntersection(hprime, v1, circlePoint, vp)
4483
4484 if castPoint.Magnitude > 0 then
4485 local ray = Ray.new(hprime, castPoint - hprime)
4486 local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
4487
4488 if hit then
4489 local hprime2 = hitPoint + 0.1 * hitNormal.unit
4490 castPoint = hprime2
4491 end
4492 else
4493 castPoint = hprime
4494 end
4495 else
4496 castPoint = hprime
4497 end
4498
4499 local ray = Ray.new(torsoPoint, (castPoint - torsoPoint))
4500 local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
4501
4502 if hit then
4503 local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
4504 castPoint = castPoint2
4505 end
4506 end
4507
4508 castPoints[#castPoints + 1] = castPoint
4509 end
4510end
4511
4512function Invisicam:CheckTorsoReference()
4513 if self.char then
4514 self.torsoPart = self.char:FindFirstChild("Torso")
4515 if not self.torsoPart then
4516 self.torsoPart = self.char:FindFirstChild("UpperTorso")
4517 if not self.torsoPart then
4518 self.torsoPart = self.char:FindFirstChild("HumanoidRootPart")
4519 end
4520 end
4521
4522 self.headPart = self.char:FindFirstChild("Head")
4523 end
4524end
4525
4526function Invisicam:CharacterAdded(char, player)
4527 -- We only want the LocalPlayer's character
4528 if player~=PlayersService.LocalPlayer then return end
4529
4530 if self.childAddedConn then
4531 self.childAddedConn:Disconnect()
4532 self.childAddedConn = nil
4533 end
4534 if self.childRemovedConn then
4535 self.childRemovedConn:Disconnect()
4536 self.childRemovedConn = nil
4537 end
4538
4539 self.char = char
4540
4541 self.trackedLimbs = {}
4542 local function childAdded(child)
4543 if child:IsA("BasePart") then
4544 if LIMB_TRACKING_SET[child.Name] then
4545 self.trackedLimbs[child] = true
4546 end
4547
4548 if (child.Name == "Torso" or child.Name == "UpperTorso") then
4549 self.torsoPart = child
4550 end
4551
4552 if (child.Name == "Head") then
4553 self.headPart = child
4554 end
4555 end
4556 end
4557
4558 local function childRemoved(child)
4559 self.trackedLimbs[child] = nil
4560
4561 -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
4562 self:CheckTorsoReference()
4563 end
4564
4565 self.childAddedConn = char.ChildAdded:Connect(childAdded)
4566 self.childRemovedConn = char.ChildRemoved:Connect(childRemoved)
4567 for _, child in pairs(self.char:GetChildren()) do
4568 childAdded(child)
4569 end
4570end
4571
4572function Invisicam:SetMode(newMode)
4573 AssertTypes(newMode, 'number')
4574 for modeName, modeNum in pairs(MODE) do
4575 if modeNum == newMode then
4576 self.mode = newMode
4577 self.behaviorFunction = self.behaviors[self.mode]
4578 return
4579 end
4580 end
4581 error("Invalid mode number")
4582end
4583
4584function Invisicam:GetObscuredParts()
4585 return self.savedHits
4586end
4587
4588-- Want to turn off Invisicam? Be sure to call this after.
4589function Invisicam:Cleanup()
4590 for hit, originalFade in pairs(self.savedHits) do
4591 hit.LocalTransparencyModifier = originalFade
4592 end
4593end
4594
4595function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
4596
4597 -- Bail if there is no Character
4598 if not self.enabled or not self.char then
4599 return desiredCameraCFrame, desiredCameraFocus
4600 end
4601
4602 self.camera = game.Workspace.CurrentCamera
4603
4604 -- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference
4605 -- Make sure we still have a HumanoidRootPart
4606 if not self.humanoidRootPart then
4607 local humanoid = self.char:FindFirstChildOfClass("Humanoid")
4608 if humanoid and humanoid.RootPart then
4609 self.humanoidRootPart = humanoid.RootPart
4610 else
4611 -- Not set up with Humanoid? Try and see if there's one in the Character at all:
4612 self.humanoidRootPart = self.char:FindFirstChild("HumanoidRootPart")
4613 if not self.humanoidRootPart then
4614 -- Bail out, since we're relying on HumanoidRootPart existing
4615 return desiredCameraCFrame, desiredCameraFocus
4616 end
4617 end
4618
4619 -- TODO: Replace this with something more sensible
4620 local ancestryChangedConn
4621 ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent)
4622 if child == self.humanoidRootPart and not parent then
4623 self.humanoidRootPart = nil
4624 if ancestryChangedConn and ancestryChangedConn.Connected then
4625 ancestryChangedConn:Disconnect()
4626 ancestryChangedConn = nil
4627 end
4628 end
4629 end)
4630 end
4631
4632 if not self.torsoPart then
4633 self:CheckTorsoReference()
4634 if not self.torsoPart then
4635 -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
4636 return desiredCameraCFrame, desiredCameraFocus
4637 end
4638 end
4639
4640 -- Make a list of world points to raycast to
4641 local castPoints = {}
4642 self.behaviorFunction(self, castPoints)
4643
4644 -- Cast to get a list of objects between the camera and the cast points
4645 local currentHits = {}
4646 local ignoreList = {self.char}
4647 local function add(hit)
4648 currentHits[hit] = true
4649 if not self.savedHits[hit] then
4650 self.savedHits[hit] = hit.LocalTransparencyModifier
4651 end
4652 end
4653
4654 local hitParts
4655 local hitPartCount = 0
4656
4657 -- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays
4658 -- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled
4659 local headTorsoRayHitParts = {}
4660 local partIsTouchingCamera = {}
4661
4662 local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY
4663 local perPartTransparencyOtherHits = TARGET_TRANSPARENCY
4664
4665 if USE_STACKING_TRANSPARENCY then
4666
4667 -- This first call uses head and torso rays to find out how many parts are stacked up
4668 -- for the purpose of calculating required per-part transparency
4669 local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1]
4670 local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2]
4671 hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList)
4672
4673 -- Count how many things the sample rays passed through, including decals. This should only
4674 -- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals,
4675 -- so my compromise for now is to just let any decal increase the part count by 1. Only one
4676 -- decal per part will be considered.
4677 for i = 1, #hitParts do
4678 local hitPart = hitParts[i]
4679 hitPartCount = hitPartCount + 1 -- count the part itself
4680 headTorsoRayHitParts[hitPart] = true
4681 for _, child in pairs(hitPart:GetChildren()) do
4682 if child:IsA('Decal') or child:IsA('Texture') then
4683 hitPartCount = hitPartCount + 1 -- count first decal hit, then break
4684 break
4685 end
4686 end
4687 end
4688
4689 if (hitPartCount > 0) then
4690 perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount )
4691 perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount )
4692 end
4693 end
4694
4695 -- Now get all the parts hit by all the rays
4696 hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList)
4697
4698 local partTargetTransparency = {}
4699
4700 -- Include decals and textures
4701 for i = 1, #hitParts do
4702 local hitPart = hitParts[i]
4703
4704 partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits
4705
4706 -- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of
4707 -- parts to be modified by invisicam
4708 if hitPart.Transparency < partTargetTransparency[hitPart] then
4709 add(hitPart)
4710 end
4711
4712 -- Check all decals and textures on the part
4713 for _, child in pairs(hitPart:GetChildren()) do
4714 if child:IsA('Decal') or child:IsA('Texture') then
4715 if (child.Transparency < partTargetTransparency[hitPart]) then
4716 partTargetTransparency[child] = partTargetTransparency[hitPart]
4717 add(child)
4718 end
4719 end
4720 end
4721 end
4722
4723 -- Invisibilize objects that are in the way, restore those that aren't anymore
4724 for hitPart, originalLTM in pairs(self.savedHits) do
4725 if currentHits[hitPart] then
4726 -- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency
4727 hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0
4728 else -- Restore original pre-invisicam value of LTM
4729 hitPart.LocalTransparencyModifier = originalLTM
4730 self.savedHits[hitPart] = nil
4731 end
4732 end
4733
4734 -- Invisicam does not change the camera values
4735 return desiredCameraCFrame, desiredCameraFocus
4736end
4737
4738return Invisicam
4739
4740--[[
4741 LegacyCamera - Implements legacy controller types: Attach, Fixed, Watch
4742 2018 Camera Update - AllYourBlox
4743--]]
4744
4745-- Local private variables and constants
4746local UNIT_X = Vector3.new(1,0,0)
4747local UNIT_Y = Vector3.new(0,1,0)
4748local UNIT_Z = Vector3.new(0,0,1)
4749local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
4750local ZERO_VECTOR3 = Vector3.new(0,0,0)
4751local ZERO_VECTOR2 = Vector2.new(0,0)
4752
4753local VR_PITCH_FRACTION = 0.25
4754local tweenAcceleration = math.rad(220) --Radians/Second^2
4755local tweenSpeed = math.rad(0) --Radians/Second
4756local tweenMaxSpeed = math.rad(250) --Radians/Second
4757local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
4758local PORTRAIT_OFFSET = Vector3.new(0,-3,0)
4759
4760local Util = require(script.Parent:WaitForChild("CameraUtils"))
4761
4762--[[ Services ]]--
4763local PlayersService = game:GetService('Players')
4764local VRService = game:GetService("VRService")
4765
4766--[[ The Module ]]--
4767local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
4768local LegacyCamera = setmetatable({}, BaseCamera)
4769LegacyCamera.__index = LegacyCamera
4770
4771function LegacyCamera.new()
4772 local self = setmetatable(BaseCamera.new(), LegacyCamera)
4773
4774 self.cameraType = Enum.CameraType.Fixed
4775 self.lastUpdate = tick()
4776 self.lastDistanceToSubject = nil
4777
4778 return self
4779end
4780
4781function LegacyCamera:GetModuleName()
4782 return "LegacyCamera"
4783end
4784
4785function LegacyCamera:Test()
4786 print("LegacyCamera:Test()")
4787end
4788
4789--[[ Functions overridden from BaseCamera ]]--
4790function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
4791 return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance)
4792end
4793
4794function LegacyCamera:Update(dt)
4795
4796 -- Cannot update until cameraType has been set
4797 if not self.cameraType then return end
4798
4799 local now = tick()
4800 local timeDelta = (now - self.lastUpdate)
4801 local camera = workspace.CurrentCamera
4802 local newCameraCFrame = camera.CFrame
4803 local newCameraFocus = camera.Focus
4804 local player = PlayersService.LocalPlayer
4805 local humanoid = self:GetHumanoid()
4806 local cameraSubject = camera and camera.CameraSubject
4807 local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
4808 local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
4809 local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
4810
4811 if self.lastUpdate == nil or timeDelta > 1 then
4812 self.lastDistanceToSubject = nil
4813 end
4814 local subjectPosition = self:GetSubjectPosition()
4815
4816 if self.cameraType == Enum.CameraType.Fixed then
4817 if self.lastUpdate then
4818 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
4819 local delta = math.min(0.1, now - self.lastUpdate)
4820 local gamepadRotation = self:UpdateGamepad()
4821 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
4822 end
4823
4824 if subjectPosition and player and camera then
4825 local distanceToSubject = self:GetCameraToSubjectDistance()
4826 local newLookVector = self:CalculateNewLookVector()
4827 self.rotateInput = ZERO_VECTOR2
4828
4829 newCameraFocus = camera.Focus -- Fixed camera does not change focus
4830 newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector))
4831 end
4832 elseif self.cameraType == Enum.CameraType.Attach then
4833 if subjectPosition and camera then
4834 local distanceToSubject = self:GetCameraToSubjectDistance()
4835 local humanoid = self:GetHumanoid()
4836 if self.lastUpdate and humanoid and humanoid.RootPart then
4837
4838 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
4839 local delta = math.min(0.1, now - self.lastUpdate)
4840 local gamepadRotation = self:UpdateGamepad()
4841 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
4842
4843 local forwardVector = humanoid.RootPart.CFrame.lookVector
4844
4845 local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
4846 if Util.IsFinite(y) then
4847 -- Preserve vertical rotation from user input
4848 self.rotateInput = Vector2.new(y, self.rotateInput.Y)
4849 end
4850 end
4851
4852 local newLookVector = self:CalculateNewLookVector()
4853 self.rotateInput = ZERO_VECTOR2
4854
4855 newCameraFocus = CFrame.new(subjectPosition)
4856 newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
4857 end
4858 elseif self.cameraType == Enum.CameraType.Watch then
4859 if subjectPosition and player and camera then
4860 local cameraLook = nil
4861
4862 local humanoid = self:GetHumanoid()
4863 if humanoid and humanoid.RootPart then
4864 local diffVector = subjectPosition - camera.CFrame.p
4865 cameraLook = diffVector.unit
4866
4867 if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then
4868 -- Don't clobber the zoom if they zoomed the camera
4869 local newDistanceToSubject = diffVector.magnitude
4870 self:SetCameraToSubjectDistance(newDistanceToSubject)
4871 end
4872 end
4873
4874 local distanceToSubject = self:GetCameraToSubjectDistance()
4875 local newLookVector = self:CalculateNewLookVector(cameraLook)
4876 self.rotateInput = ZERO_VECTOR2
4877
4878 newCameraFocus = CFrame.new(subjectPosition)
4879 newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
4880
4881 self.lastDistanceToSubject = distanceToSubject
4882 end
4883 else
4884 -- Unsupported type, return current values unchanged
4885 return camera.CFrame, camera.Focus
4886 end
4887
4888 self.lastUpdate = now
4889 return newCameraCFrame, newCameraFocus
4890end
4891
4892return LegacyCamera
4893
4894--[[
4895 MouseLockController - Replacement for ShiftLockController, manages use of mouse-locked mode
4896 2018 Camera Update - AllYourBlox
4897--]]
4898
4899--[[ Constants ]]--
4900local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png"
4901
4902local CONTEXT_ACTION_NAME = "MouseLockSwitchAction"
4903local MOUSELOCK_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
4904
4905local Util = require(script.Parent:WaitForChild("CameraUtils"))
4906
4907--[[ Services ]]--
4908local PlayersService = game:GetService("Players")
4909local UserInputService = game:GetService("UserInputService")
4910local ContextActionService = game:GetService("ContextActionService")
4911local Settings = UserSettings() -- ignore warning
4912local GameSettings = Settings.GameSettings
4913local Mouse = PlayersService.LocalPlayer:GetMouse()
4914
4915--[[ Variables ]]
4916local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
4917 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority2")
4918end)
4919local FFlagPlayerScriptsBindAtPriority2 = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
4920
4921--[[ The Module ]]--
4922local MouseLockController = {}
4923MouseLockController.__index = MouseLockController
4924
4925function MouseLockController.new()
4926 local self = setmetatable({}, MouseLockController)
4927
4928 self.inputBeganConn = nil -- Remove with FFlagPlayerScriptsBindAtPriority2
4929 self.isMouseLocked = false
4930 self.savedMouseCursor = nil
4931 self.boundKeys = {Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift} -- defaults
4932
4933 self.mouseLockToggledEvent = Instance.new("BindableEvent")
4934
4935 local boundKeysObj = script:FindFirstChild("BoundKeys")
4936 if (not boundKeysObj) or (not boundKeysObj:IsA("StringValue")) then
4937 -- If object with correct name was found, but it's not a StringValue, destroy and replace
4938 if boundKeysObj then
4939 boundKeysObj:Destroy()
4940 end
4941
4942 boundKeysObj = Instance.new("StringValue")
4943 boundKeysObj.Name = "BoundKeys"
4944 boundKeysObj.Value = "LeftShift,RightShift"
4945 boundKeysObj.Parent = script
4946 end
4947
4948 if boundKeysObj then
4949 boundKeysObj.Changed:Connect(function(value)
4950 self:OnBoundKeysObjectChanged(value)
4951 end)
4952 self:OnBoundKeysObjectChanged(boundKeysObj.Value) -- Initial setup call
4953 end
4954
4955 -- Watch for changes to user's ControlMode and ComputerMovementMode settings and update the feature availability accordingly
4956 GameSettings.Changed:Connect(function(property)
4957 if property == "ControlMode" or property == "ComputerMovementMode" then
4958 self:UpdateMouseLockAvailability()
4959 end
4960 end)
4961
4962 -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
4963 PlayersService.LocalPlayer:GetPropertyChangedSignal("DevEnableMouseLock"):Connect(function()
4964 self:UpdateMouseLockAvailability()
4965 end)
4966
4967 -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
4968 PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function()
4969 self:UpdateMouseLockAvailability()
4970 end)
4971
4972 self:UpdateMouseLockAvailability()
4973
4974 return self
4975end
4976
4977function MouseLockController:GetIsMouseLocked()
4978 return self.isMouseLocked
4979end
4980
4981function MouseLockController:GetBindableToggleEvent()
4982 return self.mouseLockToggledEvent.Event
4983end
4984
4985function MouseLockController:GetMouseLockOffset()
4986 local offsetValueObj = script:FindFirstChild("CameraOffset")
4987 if offsetValueObj and offsetValueObj:IsA("Vector3Value") then
4988 return offsetValueObj.Value
4989 else
4990 -- If CameraOffset object was found but not correct type, destroy
4991 if offsetValueObj then
4992 offsetValueObj:Destroy()
4993 end
4994 offsetValueObj = Instance.new("Vector3Value")
4995 offsetValueObj.Name = "CameraOffset"
4996 offsetValueObj.Value = Vector3.new(1.75,0,0) -- Legacy Default Value
4997 offsetValueObj.Parent = script
4998 end
4999
5000 if offsetValueObj and offsetValueObj.Value then
5001 return offsetValueObj.Value
5002 end
5003
5004 return Vector3.new(1.75,0,0)
5005end
5006
5007function MouseLockController:UpdateMouseLockAvailability()
5008 local devAllowsMouseLock = PlayersService.LocalPlayer.DevEnableMouseLock
5009 local devMovementModeIsScriptable = PlayersService.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable
5010 local userHasMouseLockModeEnabled = GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch
5011 local userHasClickToMoveEnabled = GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove
5012 local MouseLockAvailable = devAllowsMouseLock and userHasMouseLockModeEnabled and not userHasClickToMoveEnabled and not devMovementModeIsScriptable
5013
5014 if MouseLockAvailable~=self.enabled then
5015 self:EnableMouseLock(MouseLockAvailable)
5016 end
5017end
5018
5019function MouseLockController:OnBoundKeysObjectChanged(newValue)
5020 self.boundKeys = {} -- Overriding defaults, note: possibly with nothing at all if boundKeysObj.Value is "" or contains invalid values
5021 for token in string.gmatch(newValue,"[^%s,]+") do
5022 for keyCode, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do
5023 if token == keyEnum.Name then
5024 self.boundKeys[#self.boundKeys+1] = keyEnum
5025 break
5026 end
5027 end
5028 end
5029 if FFlagPlayerScriptsBindAtPriority2 then
5030 self:UnbindContextActions()
5031 self:BindContextActions()
5032 end
5033end
5034
5035--[[ Local Functions ]]--
5036function MouseLockController:OnMouseLockToggled()
5037 self.isMouseLocked = not self.isMouseLocked
5038
5039 if self.isMouseLocked then
5040 local cursorImageValueObj = script:FindFirstChild("CursorImage")
5041 if cursorImageValueObj and cursorImageValueObj:IsA("StringValue") and cursorImageValueObj.Value then
5042 self.savedMouseCursor = Mouse.Icon
5043 Mouse.Icon = cursorImageValueObj.Value
5044 else
5045 if cursorImageValueObj then
5046 cursorImageValueObj:Destroy()
5047 end
5048 cursorImageValueObj = Instance.new("StringValue")
5049 cursorImageValueObj.Name = "CursorImage"
5050 cursorImageValueObj.Value = DEFAULT_MOUSE_LOCK_CURSOR
5051 cursorImageValueObj.Parent = script
5052 self.savedMouseCursor = Mouse.Icon
5053 Mouse.Icon = DEFAULT_MOUSE_LOCK_CURSOR
5054 end
5055 else
5056 if self.savedMouseCursor then
5057 Mouse.Icon = self.savedMouseCursor
5058 self.savedMouseCursor = nil
5059 end
5060 end
5061
5062 self.mouseLockToggledEvent:Fire()
5063end
5064
5065-- Remove with FFlagPlayerScriptsBindAtPriority2
5066function MouseLockController:OnInputBegan(input, processed)
5067 if processed then return end
5068
5069 if input.UserInputType == Enum.UserInputType.Keyboard then
5070 for _, keyCode in pairs(self.boundKeys) do
5071 if keyCode == input.KeyCode then
5072 self:OnMouseLockToggled()
5073 return
5074 end
5075 end
5076 end
5077end
5078
5079function MouseLockController:DoMouseLockSwitch(name, state, input)
5080 if state == Enum.UserInputState.Begin then
5081 self:OnMouseLockToggled()
5082 return Enum.ContextActionResult.Sink
5083 end
5084 return Enum.ContextActionResult.Pass
5085end
5086
5087function MouseLockController:BindContextActions()
5088 ContextActionService:BindActionAtPriority(CONTEXT_ACTION_NAME, function(name, state, input) return self:DoMouseLockSwitch(name, state, input) end,
5089 false, MOUSELOCK_ACTION_PRIORITY, unpack(self.boundKeys))
5090end
5091
5092function MouseLockController:UnbindContextActions()
5093 ContextActionService:UnbindAction(CONTEXT_ACTION_NAME)
5094end
5095
5096function MouseLockController:IsMouseLocked()
5097 return self.enabled and self.isMouseLocked
5098end
5099
5100function MouseLockController:EnableMouseLock(enable)
5101 if enable~=self.enabled then
5102
5103 self.enabled = enable
5104
5105 if self.enabled then
5106 -- Enabling the mode
5107 if FFlagPlayerScriptsBindAtPriority2 then
5108 self:BindContextActions()
5109 else
5110 if self.inputBeganConn then
5111 self.inputBeganConn:Disconnect()
5112 end
5113 self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
5114 self:OnInputBegan(input, processed)
5115 end)
5116 end
5117 else
5118 -- Disabling
5119 -- Restore mouse cursor
5120 if Mouse.Icon~="" then
5121 Mouse.Icon = ""
5122 end
5123
5124 if FFlagPlayerScriptsBindAtPriority2 then
5125 self:UnbindContextActions()
5126 else
5127 if self.inputBeganConn then
5128 self.inputBeganConn:Disconnect()
5129 end
5130 self.inputBeganConn = nil
5131 end
5132
5133 -- If the mode is disabled while being used, fire the event to toggle it off
5134 if self.isMouseLocked then
5135 self.mouseLockToggledEvent:Fire()
5136 end
5137
5138 self.isMouseLocked = false
5139 end
5140
5141 end
5142end
5143
5144return MouseLockController
5145
5146--[[
5147 OrbitalCamera - Spherical coordinates control camera for top-down games
5148 2018 Camera Update - AllYourBlox
5149--]]
5150
5151-- Local private variables and constants
5152local UNIT_X = Vector3.new(1,0,0)
5153local UNIT_Y = Vector3.new(0,1,0)
5154local UNIT_Z = Vector3.new(0,0,1)
5155local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
5156local ZERO_VECTOR3 = Vector3.new(0,0,0)
5157local ZERO_VECTOR2 = Vector2.new(0,0)
5158local TAU = 2 * math.pi
5159
5160local VR_PITCH_FRACTION = 0.25
5161local tweenAcceleration = math.rad(220) --Radians/Second^2
5162local tweenSpeed = math.rad(0) --Radians/Second
5163local tweenMaxSpeed = math.rad(250) --Radians/Second
5164local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
5165local PORTRAIT_OFFSET = Vector3.new(0,-3,0)
5166
5167local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
5168 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority2")
5169end)
5170local FFlagPlayerScriptsBindAtPriority2 = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
5171
5172--[[ Gamepad Support ]]--
5173local THUMBSTICK_DEADZONE = 0.2
5174
5175-- Do not edit these values, they are not the developer-set limits, they are limits
5176-- to the values the camera system equations can correctly handle
5177local MIN_ALLOWED_ELEVATION_DEG = -80
5178local MAX_ALLOWED_ELEVATION_DEG = 80
5179
5180local externalProperties = {}
5181externalProperties["InitialDistance"] = 25
5182externalProperties["MinDistance"] = 10
5183externalProperties["MaxDistance"] = 100
5184externalProperties["InitialElevation"] = 35
5185externalProperties["MinElevation"] = 35
5186externalProperties["MaxElevation"] = 35
5187externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
5188externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
5189externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
5190externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
5191
5192local Util = require(script.Parent:WaitForChild("CameraUtils"))
5193
5194--[[ Services ]]--
5195local PlayersService = game:GetService('Players')
5196local VRService = game:GetService("VRService")
5197
5198--[[ Utility functions specific to OrbitalCamera ]]--
5199local function GetValueObject(name, defaultValue)
5200 local valueObj = script:FindFirstChild(name)
5201 if valueObj then
5202 return valueObj.Value
5203 end
5204 return defaultValue
5205end
5206
5207--[[ The Module ]]--
5208local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
5209local OrbitalCamera = setmetatable({}, BaseCamera)
5210OrbitalCamera.__index = OrbitalCamera
5211
5212
5213function OrbitalCamera.new()
5214 local self = setmetatable(BaseCamera.new(), OrbitalCamera)
5215
5216 self.lastUpdate = tick()
5217
5218 -- OrbitalCamera-specific members
5219 self.changedSignalConnections = {}
5220 self.refAzimuthRad = nil
5221 self.curAzimuthRad = nil
5222 self.minAzimuthAbsoluteRad = nil
5223 self.maxAzimuthAbsoluteRad = nil
5224 self.useAzimuthLimits = nil
5225 self.curElevationRad = nil
5226 self.minElevationRad = nil
5227 self.maxElevationRad = nil
5228 self.curDistance = nil
5229 self.minDistance = nil
5230 self.maxDistance = nil
5231
5232 -- Gamepad
5233 self.r3ButtonDown = false
5234 self.l3ButtonDown = false
5235 self.gamepadDollySpeedMultiplier = 1
5236
5237 self.lastUserPanCamera = tick()
5238
5239 self.externalProperties = {}
5240 self.externalProperties["InitialDistance"] = 25
5241 self.externalProperties["MinDistance"] = 10
5242 self.externalProperties["MaxDistance"] = 100
5243 self.externalProperties["InitialElevation"] = 35
5244 self.externalProperties["MinElevation"] = 35
5245 self.externalProperties["MaxElevation"] = 35
5246 self.externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
5247 self.externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
5248 self.externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
5249 self.externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
5250 self:LoadNumberValueParameters()
5251
5252 return self
5253end
5254
5255function OrbitalCamera:LoadOrCreateNumberValueParameter(name, valueType, updateFunction)
5256 local valueObj = script:FindFirstChild(name)
5257
5258 if valueObj and valueObj:isA(valueType) then
5259 -- Value object exists and is the correct type, use its value
5260 self.externalProperties[name] = valueObj.Value
5261 elseif self.externalProperties[name] ~= nil then
5262 -- Create missing (or replace incorrectly-typed) valueObject with default value
5263 valueObj = Instance.new(valueType)
5264 valueObj.Name = name
5265 valueObj.Parent = script
5266 valueObj.Value = self.externalProperties[name]
5267 else
5268 print("externalProperties table has no entry for ",name)
5269 return
5270 end
5271
5272 if updateFunction then
5273 if self.changedSignalConnections[name] then
5274 self.changedSignalConnections[name]:Disconnect()
5275 end
5276 self.changedSignalConnections[name] = valueObj.Changed:Connect(function(newValue)
5277 self.externalProperties[name] = newValue
5278 updateFunction(self)
5279 end)
5280 end
5281end
5282
5283function OrbitalCamera:SetAndBoundsCheckAzimuthValues()
5284 self.minAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(self.externalProperties["CWAzimuthTravel"]))
5285 self.maxAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(self.externalProperties["CCWAzimuthTravel"]))
5286 self.useAzimuthLimits = self.externalProperties["UseAzimuthLimits"]
5287 if self.useAzimuthLimits then
5288 self.curAzimuthRad = math.max(self.curAzimuthRad, self.minAzimuthAbsoluteRad)
5289 self.curAzimuthRad = math.min(self.curAzimuthRad, self.maxAzimuthAbsoluteRad)
5290 end
5291end
5292
5293function OrbitalCamera:SetAndBoundsCheckElevationValues()
5294 -- These degree values are the direct user input values. It is deliberate that they are
5295 -- ranged checked only against the extremes, and not against each other. Any time one
5296 -- is changed, both of the internal values in radians are recalculated. This allows for
5297 -- A developer to change the values in any order and for the end results to be that the
5298 -- internal values adjust to match intent as best as possible.
5299 local minElevationDeg = math.max(self.externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
5300 local maxElevationDeg = math.min(self.externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
5301
5302 -- Set internal values in radians
5303 self.minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
5304 self.maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
5305 self.curElevationRad = math.max(self.curElevationRad, self.minElevationRad)
5306 self.curElevationRad = math.min(self.curElevationRad, self.maxElevationRad)
5307end
5308
5309function OrbitalCamera:SetAndBoundsCheckDistanceValues()
5310 self.minDistance = self.externalProperties["MinDistance"]
5311 self.maxDistance = self.externalProperties["MaxDistance"]
5312 self.curDistance = math.max(self.curDistance, self.minDistance)
5313 self.curDistance = math.min(self.curDistance, self.maxDistance)
5314end
5315
5316-- This loads from, or lazily creates, NumberValue objects for exposed parameters
5317function OrbitalCamera:LoadNumberValueParameters()
5318 -- These initial values do not require change listeners since they are read only once
5319 self:LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
5320 self:LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
5321
5322 -- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
5323 self:LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", self.SetAndBoundsCheckAzimuthValue)
5324 self:LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
5325 self:LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
5326 self:LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
5327 self:LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
5328 self:LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
5329 self:LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
5330 self:LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", self.SetAndBoundsCheckAzimuthValues)
5331
5332 -- Internal values set (in radians, from degrees), plus sanitization
5333 self.curAzimuthRad = math.rad(self.externalProperties["ReferenceAzimuth"])
5334 self.curElevationRad = math.rad(self.externalProperties["InitialElevation"])
5335 self.curDistance = self.externalProperties["InitialDistance"]
5336
5337 self:SetAndBoundsCheckAzimuthValues()
5338 self:SetAndBoundsCheckElevationValues()
5339 self:SetAndBoundsCheckDistanceValues()
5340end
5341
5342function OrbitalCamera:GetModuleName()
5343 return "OrbitalCamera"
5344end
5345
5346function OrbitalCamera:SetInitialOrientation(humanoid)
5347 if not humanoid or not humanoid.RootPart then
5348 warn("OrbitalCamera could not set initial orientation due to missing humanoid")
5349 return
5350 end
5351 local newDesiredLook = (humanoid.RootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
5352 local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, self:GetCameraLookVector())
5353 local vertShift = math.asin(self:GetCameraLookVector().y) - math.asin(newDesiredLook.y)
5354 if not Util.IsFinite(horizontalShift) then
5355 horizontalShift = 0
5356 end
5357 if not Util.IsFinite(vertShift) then
5358 vertShift = 0
5359 end
5360 self.rotateInput = Vector2.new(horizontalShift, vertShift)
5361end
5362
5363--[[ Functions of BaseCamera that are overridden by OrbitalCamera ]]--
5364function OrbitalCamera:GetCameraToSubjectDistance()
5365 return self.curDistance
5366end
5367
5368function OrbitalCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
5369 print("OrbitalCamera SetCameraToSubjectDistance ",desiredSubjectDistance)
5370 local player = PlayersService.LocalPlayer
5371 if player then
5372 self.currentSubjectDistance = Util.Clamp(self.minDistance, self.maxDistance, desiredSubjectDistance)
5373
5374 -- OrbitalCamera is not allowed to go into the first-person range
5375 self.currentSubjectDistance = math.max(self.currentSubjectDistance, self.FIRST_PERSON_DISTANCE_THRESHOLD)
5376 end
5377 self.inFirstPerson = false
5378 self:UpdateMouseBehavior()
5379 return self.currentSubjectDistance
5380end
5381
5382function OrbitalCamera:CalculateNewLookVector(suppliedLookVector, xyRotateVector)
5383 local currLookVector = suppliedLookVector or self:GetCameraLookVector()
5384 local currPitchAngle = math.asin(currLookVector.y)
5385 local yTheta = Util.Clamp(currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG), xyRotateVector.y)
5386 local constrainedRotateInput = Vector2.new(xyRotateVector.x, yTheta)
5387 local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
5388 local newLookVector = (CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)).lookVector
5389 return newLookVector
5390end
5391
5392function OrbitalCamera:GetGamepadPan(name, state, input)
5393 if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
5394 if self.r3ButtonDown or self.l3ButtonDown then
5395 -- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
5396 if (input.Position.Y > THUMBSTICK_DEADZONE) then
5397 self.gamepadDollySpeedMultiplier = 0.96
5398 elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
5399 self.gamepadDollySpeedMultiplier = 1.04
5400 else
5401 self.gamepadDollySpeedMultiplier = 1.00
5402 end
5403 else
5404 if state == Enum.UserInputState.Cancel then
5405 self.gamepadPanningCamera = ZERO_VECTOR2
5406 return
5407 end
5408
5409 local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
5410 if inputVector.magnitude > THUMBSTICK_DEADZONE then
5411 self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
5412 else
5413 self.gamepadPanningCamera = ZERO_VECTOR2
5414 end
5415 end
5416 if FFlagPlayerScriptsBindAtPriority2 then
5417 return Enum.ContextActionResult.Sink
5418 end
5419 end
5420 if FFlagPlayerScriptsBindAtPriority2 then
5421 return Enum.ContextActionResult.Pass
5422 end
5423end
5424
5425function OrbitalCamera:DoGamepadZoom(name, state, input)
5426 if input.UserInputType == self.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
5427 if (state == Enum.UserInputState.Begin) then
5428 self.r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
5429 self.l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
5430 elseif (state == Enum.UserInputState.End) then
5431 if (input.KeyCode == Enum.KeyCode.ButtonR3) then
5432 self.r3ButtonDown = false
5433 elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
5434 self.l3ButtonDown = false
5435 end
5436 if (not self.r3ButtonDown) and (not self.l3ButtonDown) then
5437 self.gamepadDollySpeedMultiplier = 1.00
5438 end
5439 end
5440 if FFlagPlayerScriptsBindAtPriority2 then
5441 return Enum.ContextActionResult.Sink
5442 end
5443 end
5444 if FFlagPlayerScriptsBindAtPriority2 then
5445 return Enum.ContextActionResult.Pass
5446 end
5447end
5448
5449function OrbitalCamera:BindGamepadInputActions()
5450 if FFlagPlayerScriptsBindAtPriority2 then
5451 self:BindAction("OrbitalCamGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end,
5452 false, Enum.KeyCode.Thumbstick2)
5453 self:BindAction("OrbitalCamGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end,
5454 false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonL3)
5455 else
5456 local ContextActionService = game:GetService('ContextActionService')
5457
5458 ContextActionService:BindAction("OrbitalCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2)
5459 ContextActionService:BindAction("OrbitalCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3)
5460 ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonL3)
5461 end
5462end
5463
5464
5465-- [[ Update ]]--
5466function OrbitalCamera:Update(dt)
5467 local now = tick()
5468 local timeDelta = (now - self.lastUpdate)
5469 local userPanningTheCamera = (self.UserPanningTheCamera == true)
5470 local camera = workspace.CurrentCamera
5471 local newCameraCFrame = camera.CFrame
5472 local newCameraFocus = camera.Focus
5473 local player = PlayersService.LocalPlayer
5474 local humanoid = self:GetHumanoid()
5475 local cameraSubject = camera and camera.CameraSubject
5476 local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
5477 local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
5478
5479 if self.lastUpdate == nil or timeDelta > 1 then
5480 self.lastCameraTransform = nil
5481 end
5482
5483 if self.lastUpdate then
5484 local gamepadRotation = self:UpdateGamepad()
5485
5486 if self:ShouldUseVRRotation() then
5487 self.RotateInput = self.RotateInput + self:GetVRRotationInput()
5488 else
5489 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
5490 local delta = math.min(0.1, timeDelta)
5491
5492 if gamepadRotation ~= ZERO_VECTOR2 then
5493 userPanningTheCamera = true
5494 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
5495 end
5496
5497 local angle = 0
5498 if not (isInVehicle or isOnASkateboard) then
5499 angle = angle + (self.TurningLeft and -120 or 0)
5500 angle = angle + (self.TurningRight and 120 or 0)
5501 end
5502
5503 if angle ~= 0 then
5504 self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
5505 userPanningTheCamera = true
5506 end
5507 end
5508 end
5509
5510 -- Reset tween speed if user is panning
5511 if userPanningTheCamera then
5512 tweenSpeed = 0
5513 self.lastUserPanCamera = tick()
5514 end
5515
5516 local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
5517 local subjectPosition = self:GetSubjectPosition()
5518
5519 if subjectPosition and player and camera then
5520
5521 -- Process any dollying being done by gamepad
5522 -- TODO: Move this
5523 if self.gamepadDollySpeedMultiplier ~= 1 then
5524 self:SetCameraToSubjectDistance(self.currentSubjectDistance * self.gamepadDollySpeedMultiplier)
5525 end
5526
5527 local VREnabled = VRService.VREnabled
5528 newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition)
5529
5530 local cameraFocusP = newCameraFocus.p
5531 if VREnabled and not self:IsInFirstPerson() then
5532 local cameraHeight = self:GetCameraHeight()
5533 local vecToSubject = (subjectPosition - camera.CFrame.p)
5534 local distToSubject = vecToSubject.magnitude
5535
5536 -- Only move the camera if it exceeded a maximum distance to the subject in VR
5537 if distToSubject > self.currentSubjectDistance or self.rotateInput.x ~= 0 then
5538 local desiredDist = math.min(distToSubject, self.currentSubjectDistance)
5539
5540 -- Note that CalculateNewLookVector is overridden from BaseCamera
5541 vecToSubject = self:CalculateNewLookVector(vecToSubject.unit * X1_Y0_Z1, Vector2.new(self.rotateInput.x, 0)) * desiredDist
5542
5543 local newPos = cameraFocusP - vecToSubject
5544 local desiredLookDir = camera.CFrame.lookVector
5545 if self.rotateInput.x ~= 0 then
5546 desiredLookDir = vecToSubject
5547 end
5548 local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
5549 self.RotateInput = ZERO_VECTOR2
5550
5551 newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
5552 end
5553 else
5554 -- self.RotateInput is a Vector2 of mouse movement deltas since last update
5555 self.curAzimuthRad = self.curAzimuthRad - self.rotateInput.x
5556
5557 if self.useAzimuthLimits then
5558 self.curAzimuthRad = Util.Clamp(self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad, self.curAzimuthRad)
5559 else
5560 self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0
5561 end
5562
5563 self.curElevationRad = Util.Clamp(self.minElevationRad, self.maxElevationRad, self.curElevationRad + self.rotateInput.y)
5564
5565 local cameraPosVector = self.currentSubjectDistance * ( CFrame.fromEulerAnglesYXZ( -self.curElevationRad, self.curAzimuthRad, 0 ) * UNIT_Z )
5566 local camPos = subjectPosition + cameraPosVector
5567
5568 newCameraCFrame = CFrame.new(camPos, subjectPosition)
5569
5570 self.rotateInput = ZERO_VECTOR2
5571 end
5572
5573 self.lastCameraTransform = newCameraCFrame
5574 self.lastCameraFocus = newCameraFocus
5575 if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
5576 self.lastSubjectCFrame = cameraSubject.CFrame
5577 else
5578 self.lastSubjectCFrame = nil
5579 end
5580 end
5581
5582 self.lastUpdate = now
5583 return newCameraCFrame, newCameraFocus
5584end
5585
5586return OrbitalCamera
5587
5588--[[
5589 Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view.
5590--]]
5591
5592local ZoomController = require(script.Parent:WaitForChild("ZoomController"))
5593
5594local TransformExtrapolator = {} do
5595 TransformExtrapolator.__index = TransformExtrapolator
5596
5597 local CF_IDENTITY = CFrame.new()
5598
5599 local function cframeToAxis(cframe)
5600 local axis, angle = cframe:toAxisAngle()
5601 return axis*angle
5602 end
5603
5604 local function axisToCFrame(axis)
5605 local angle = axis.magnitude
5606 if angle > 1e-5 then
5607 return CFrame.fromAxisAngle(axis, angle)
5608 end
5609 return CF_IDENTITY
5610 end
5611
5612 local function extractRotation(cf)
5613 local _, _, _, xx, yx, zx, xy, yy, zy, xz, yz, zz = cf:components()
5614 return CFrame.new(0, 0, 0, xx, yx, zx, xy, yy, zy, xz, yz, zz)
5615 end
5616
5617 function TransformExtrapolator.new()
5618 return setmetatable({
5619 lastCFrame = nil,
5620 }, TransformExtrapolator)
5621 end
5622
5623 function TransformExtrapolator:Step(dt, currentCFrame)
5624 local lastCFrame = self.lastCFrame or currentCFrame
5625 self.lastCFrame = currentCFrame
5626
5627 local currentPos = currentCFrame.p
5628 local currentRot = extractRotation(currentCFrame)
5629
5630 local lastPos = lastCFrame.p
5631 local lastRot = extractRotation(lastCFrame)
5632
5633 -- Estimate velocities from the delta between now and the last frame
5634 -- This estimation can be a little noisy.
5635 local dp = (currentPos - lastPos)/dt
5636 local dr = cframeToAxis(currentRot*lastRot:inverse())/dt
5637
5638 local function extrapolate(t)
5639 local p = dp*t + currentPos
5640 local r = axisToCFrame(dr*t)*currentRot
5641 return r + p
5642 end
5643
5644 return {
5645 extrapolate = extrapolate,
5646 posVelocity = dp,
5647 rotVelocity = dr,
5648 }
5649 end
5650
5651 function TransformExtrapolator:Reset()
5652 self.lastCFrame = nil
5653 end
5654end
5655
5656--[[ The Module ]]--
5657local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
5658local Poppercam = setmetatable({}, BaseOcclusion)
5659Poppercam.__index = Poppercam
5660
5661function Poppercam.new()
5662 local self = setmetatable(BaseOcclusion.new(), Poppercam)
5663 self.focusExtrapolator = TransformExtrapolator.new()
5664 return self
5665end
5666
5667function Poppercam:GetOcclusionMode()
5668 return Enum.DevCameraOcclusionMode.Zoom
5669end
5670
5671function Poppercam:Enable(enable)
5672 self.focusExtrapolator:Reset()
5673end
5674
5675function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController)
5676 --[[
5677 local rotatedFocus = CFrame.new(desiredCameraFocus.p, desiredCameraCFrame.p)*CFrame.new(
5678 0, 0, 0,
5679 -1, 0, 0,
5680 0, 1, 0,
5681 0, 0, -1
5682 )
5683 --]]
5684 local rotatedFocus = desiredCameraFocus * (desiredCameraCFrame - desiredCameraCFrame.p)
5685 local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus)
5686 local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation)
5687 return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus
5688end
5689
5690-- Called when character is added
5691function Poppercam:CharacterAdded(character, player)
5692end
5693
5694-- Called when character is about to be removed
5695function Poppercam:CharacterRemoving(character, player)
5696end
5697
5698function Poppercam:OnCameraSubjectChanged(newSubject)
5699end
5700
5701return Poppercam
5702
5703--[[
5704 Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view
5705 Refactored for 2018 Camera Update but functionality is unchanged - AllYourBlox
5706--]]
5707
5708--[[ Camera Maths Utilities Library ]]--
5709local Util = require(script.Parent:WaitForChild("CameraUtils"))
5710
5711local PlayersService = game:GetService("Players")
5712local POP_RESTORE_RATE = 0.3
5713local MIN_CAMERA_ZOOM = 0.5
5714local VALID_SUBJECTS = {
5715 'Humanoid',
5716 'VehicleSeat',
5717 'SkateboardPlatform',
5718}
5719
5720local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function()
5721 return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix")
5722end)
5723local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled
5724
5725
5726--[[ The Module ]]--
5727local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
5728local Poppercam = setmetatable({}, BaseOcclusion)
5729Poppercam.__index = Poppercam
5730
5731function Poppercam.new()
5732 local self = setmetatable(BaseOcclusion.new(), Poppercam)
5733
5734 self.camera = nil
5735 self.cameraSubjectChangeConn = nil
5736
5737 self.subjectPart = nil
5738
5739 self.playerCharacters = {} -- For ignoring in raycasts
5740 self.vehicleParts = {} -- Also just for ignoring
5741
5742 self.lastPopAmount = 0
5743 self.lastZoomLevel = 0
5744 self.popperEnabled = false
5745
5746 return self
5747end
5748
5749function Poppercam:GetOcclusionMode()
5750 return Enum.DevCameraOcclusionMode.Zoom
5751end
5752
5753function Poppercam:Enable(enable)
5754
5755end
5756
5757-- Called when character is added
5758function Poppercam:CharacterAdded(char, player)
5759 self.playerCharacters[player] = char
5760end
5761
5762-- Called when character is about to be removed
5763function Poppercam:CharacterRemoving(char, player)
5764 self.playerCharacters[player] = nil
5765end
5766
5767function Poppercam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
5768 if self.popperEnabled then
5769 self.camera = game.Workspace.CurrentCamera
5770 local newCameraCFrame = desiredCameraCFrame
5771 local focusPoint = desiredCameraFocus.p
5772
5773 if FFlagUserPortraitPopperFix and self.subjectPart then
5774 focusPoint = self.subjectPart.CFrame.p
5775 end
5776
5777 local ignoreList = {}
5778 for _, character in pairs(self.playerCharacters) do
5779 ignoreList[#ignoreList + 1] = character
5780 end
5781 for i = 1, #self.vehicleParts do
5782 ignoreList[#ignoreList + 1] = self.vehicleParts[i]
5783 end
5784
5785 -- Get largest cutoff distance
5786 -- Note that the camera CFrame must be set here, because the current implementation of GetLargestCutoffDistance
5787 -- uses the current camera CFrame directly (it cannot yet be passed the desiredCameraCFrame).
5788 local prevCameraCFrame = self.camera.CFrame
5789 self.camera.CFrame = desiredCameraCFrame
5790 self.camera.Focus = desiredCameraFocus
5791 local largest = self.camera:GetLargestCutoffDistance(ignoreList)
5792
5793 -- Then check if the player zoomed since the last frame,
5794 -- and if so, reset our pop history so we stop tweening
5795 local zoomLevel = (desiredCameraCFrame.p - focusPoint).Magnitude
5796 if math.abs(zoomLevel - self.lastZoomLevel) > 0.001 then
5797 self.lastPopAmount = 0
5798 end
5799
5800 -- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more
5801 local popAmount = largest
5802 if self.lastPopAmount > popAmount then
5803 popAmount = self.lastPopAmount
5804 end
5805
5806 if popAmount > 0 then
5807 newCameraCFrame = desiredCameraCFrame + (desiredCameraCFrame.lookVector * popAmount)
5808 self.lastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame
5809 if self.lastPopAmount < 0 then
5810 self.lastPopAmount = 0
5811 end
5812 end
5813
5814 self.lastZoomLevel = zoomLevel
5815
5816 -- Stop shift lock being able to see through walls by manipulating Camera focus inside the wall
5817-- if EnabledCamera and EnabledCamera:GetShiftLock() and not EnabledCamera:IsInFirstPerson() then
5818-- if EnabledCamera:GetCameraActualZoom() < 1 then
5819-- local subjectPosition = EnabledCamera.lastSubjectPosition
5820-- if subjectPosition then
5821-- Camera.Focus = CFrame_new(subjectPosition)
5822-- Camera.CFrame = CFrame_new(subjectPosition - MIN_CAMERA_ZOOM*EnabledCamera:GetCameraLook(), subjectPosition)
5823-- end
5824-- end
5825-- end
5826 return newCameraCFrame, desiredCameraFocus
5827 end
5828
5829 -- Return unchanged values
5830 return desiredCameraCFrame, desiredCameraFocus
5831end
5832
5833function Poppercam:OnCameraSubjectChanged(newSubject)
5834 self.vehicleParts = {}
5835
5836 self.lastPopAmount = 0
5837
5838 if newSubject then
5839 -- Determine if we should be popping at all
5840 self.popperEnabled = false
5841 for _, subjectType in pairs(VALID_SUBJECTS) do
5842 if newSubject:IsA(subjectType) then
5843 self.popperEnabled = true
5844 break
5845 end
5846 end
5847
5848 -- Get all parts of the vehicle the player is controlling
5849 if newSubject:IsA('VehicleSeat') then
5850 self.vehicleParts = newSubject:GetConnectedParts(true)
5851 end
5852
5853 if FFlagUserPortraitPopperFix then
5854 if newSubject:IsA("BasePart") then
5855 self.subjectPart = newSubject
5856 elseif newSubject:IsA("Model") then
5857 if newSubject.PrimaryPart then
5858 self.subjectPart = newSubject.PrimaryPart
5859 else
5860 -- Model has no PrimaryPart set, just use first BasePart
5861 -- we can find as better-than-nothing solution (can still fail)
5862 for _, child in pairs(newSubject:GetChildren()) do
5863 if child:IsA("BasePart") then
5864 self.subjectPart = child
5865 break
5866 end
5867 end
5868 end
5869 elseif newSubject:IsA("Humanoid") then
5870 self.subjectPart = newSubject.RootPart
5871 end
5872 end
5873 end
5874end
5875
5876return Poppercam
5877
5878--[[
5879 TransparencyController - Manages transparency of player character at close camera-to-subject distances
5880 2018 Camera Update - AllYourBlox
5881--]]
5882
5883local MAX_TWEEN_RATE = 2.8 -- per second
5884
5885local Util = require(script.Parent:WaitForChild("CameraUtils"))
5886
5887--[[ The Module ]]--
5888local TransparencyController = {}
5889TransparencyController.__index = TransparencyController
5890
5891function TransparencyController.new()
5892 local self = setmetatable({}, TransparencyController)
5893
5894 self.lastUpdate = tick()
5895 self.transparencyDirty = false
5896 self.enabled = false
5897 self.lastTransparency = nil
5898
5899 self.descendantAddedConn, self.descendantRemovingConn = nil, nil
5900 self.toolDescendantAddedConns = {}
5901 self.toolDescendantRemovingConns = {}
5902 self.cachedParts = {}
5903
5904 return self
5905end
5906
5907
5908function TransparencyController:HasToolAncestor(object)
5909 if object.Parent == nil then return false end
5910 return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent)
5911end
5912
5913function TransparencyController:IsValidPartToModify(part)
5914 if part:IsA('BasePart') or part:IsA('Decal') then
5915 return not self:HasToolAncestor(part)
5916 end
5917 return false
5918end
5919
5920function TransparencyController:CachePartsRecursive(object)
5921 if object then
5922 if self:IsValidPartToModify(object) then
5923 self.cachedParts[object] = true
5924 self.transparencyDirty = true
5925 end
5926 for _, child in pairs(object:GetChildren()) do
5927 self:CachePartsRecursive(child)
5928 end
5929 end
5930end
5931
5932function TransparencyController:TeardownTransparency()
5933 for child, _ in pairs(self.cachedParts) do
5934 child.LocalTransparencyModifier = 0
5935 end
5936 self.cachedParts = {}
5937 self.transparencyDirty = true
5938 self.lastTransparency = nil
5939
5940 if self.descendantAddedConn then
5941 self.descendantAddedConn:disconnect()
5942 self.descendantAddedConn = nil
5943 end
5944 if self.descendantRemovingConn then
5945 self.descendantRemovingConn:disconnect()
5946 self.descendantRemovingConn = nil
5947 end
5948 for object, conn in pairs(self.toolDescendantAddedConns) do
5949 conn:Disconnect()
5950 self.toolDescendantAddedConns[object] = nil
5951 end
5952 for object, conn in pairs(self.toolDescendantRemovingConns) do
5953 conn:Disconnect()
5954 self.toolDescendantRemovingConns[object] = nil
5955 end
5956end
5957
5958function TransparencyController:SetupTransparency(character)
5959 self:TeardownTransparency()
5960
5961 if self.descendantAddedConn then self.descendantAddedConn:disconnect() end
5962 self.descendantAddedConn = character.DescendantAdded:Connect(function(object)
5963 -- This is a part we want to invisify
5964 if self:IsValidPartToModify(object) then
5965 self.cachedParts[object] = true
5966 self.transparencyDirty = true
5967 -- There is now a tool under the character
5968 elseif object:IsA('Tool') then
5969 if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end
5970 self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild)
5971 self.cachedParts[toolChild] = nil
5972 if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
5973 -- Reset the transparency
5974 toolChild.LocalTransparencyModifier = 0
5975 end
5976 end)
5977 if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end
5978 self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild)
5979 wait() -- wait for new parent
5980 if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
5981 if self:IsValidPartToModify(formerToolChild) then
5982 self.cachedParts[formerToolChild] = true
5983 self.transparencyDirty = true
5984 end
5985 end
5986 end)
5987 end
5988 end)
5989 if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end
5990 self.descendantRemovingConn = character.DescendantRemoving:connect(function(object)
5991 if self.cachedParts[object] then
5992 self.cachedParts[object] = nil
5993 -- Reset the transparency
5994 object.LocalTransparencyModifier = 0
5995 end
5996 end)
5997 self:CachePartsRecursive(character)
5998end
5999
6000
6001function TransparencyController:Enable(enable)
6002 if self.enabled ~= enable then
6003 self.enabled = enable
6004 self:Update()
6005 end
6006end
6007
6008function TransparencyController:SetSubject(subject)
6009 local character = nil
6010 if subject and subject:IsA("Humanoid") then
6011 character = subject.Parent
6012 end
6013 if subject and subject:IsA("VehicleSeat") and subject.Occupant then
6014 character = subject.Occupant.Parent
6015 end
6016 if character then
6017 self:SetupTransparency(character)
6018 else
6019 self:TeardownTransparency()
6020 end
6021end
6022
6023function TransparencyController:Update()
6024 local instant = false
6025 local now = tick()
6026 local currentCamera = workspace.CurrentCamera
6027
6028 if currentCamera then
6029 local transparency = 0
6030 if not self.enabled then
6031 instant = true
6032 else
6033 local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
6034 transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 --(7 - distance) / 5
6035 if transparency < 0.5 then
6036 transparency = 0
6037 end
6038
6039 if self.lastTransparency then
6040 local deltaTransparency = transparency - self.lastTransparency
6041
6042 -- Don't tween transparency if it is instant or your character was fully invisible last frame
6043 if not instant and transparency < 1 and self.lastTransparency < 0.95 then
6044 local maxDelta = MAX_TWEEN_RATE * (now - self.lastUpdate)
6045 deltaTransparency = Util.Clamp(-maxDelta, maxDelta, deltaTransparency)
6046 end
6047 transparency = self.lastTransparency + deltaTransparency
6048 else
6049 self.transparencyDirty = true
6050 end
6051
6052 transparency = Util.Clamp(0, 1, Util.Round(transparency, 2))
6053 end
6054
6055 if self.transparencyDirty or self.lastTransparency ~= transparency then
6056 for child, _ in pairs(self.cachedParts) do
6057 child.LocalTransparencyModifier = transparency
6058 end
6059 self.transparencyDirty = false
6060 self.lastTransparency = transparency
6061 end
6062 end
6063 self.lastUpdate = now
6064end
6065
6066return TransparencyController
6067
6068-- Zoom
6069-- Controls the distance between the focus and the camera.
6070
6071local ZOOM_STIFFNESS = 4.5
6072local ZOOM_DEFAULT = 12.5
6073local ZOOM_ACCELERATION = 0.0375
6074
6075local MIN_FOCUS_DIST = 0.5
6076local DIST_OPAQUE = 1
6077
6078local Popper = require(script:WaitForChild("Popper"))
6079
6080local clamp = math.clamp
6081local exp = math.exp
6082local min = math.min
6083local max = math.max
6084local pi = math.pi
6085
6086local cameraMinZoomDistance, cameraMaxZoomDistance do
6087 local Player = game:GetService("Players").LocalPlayer
6088
6089 local function updateBounds()
6090 cameraMinZoomDistance = Player.CameraMinZoomDistance
6091 cameraMaxZoomDistance = Player.CameraMaxZoomDistance
6092 end
6093
6094 updateBounds()
6095
6096 Player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(updateBounds)
6097 Player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(updateBounds)
6098end
6099
6100local ConstrainedSpring = {} do
6101 ConstrainedSpring.__index = ConstrainedSpring
6102
6103 function ConstrainedSpring.new(freq, x, minValue, maxValue)
6104 x = clamp(x, minValue, maxValue)
6105 return setmetatable({
6106 freq = freq, -- Undamped frequency (Hz)
6107 x = x, -- Current position
6108 v = 0, -- Current velocity
6109 minValue = minValue, -- Minimum bound
6110 maxValue = maxValue, -- Maximum bound
6111 goal = x, -- Goal position
6112 }, ConstrainedSpring)
6113 end
6114
6115 function ConstrainedSpring:Step(dt)
6116 local freq = self.freq*2*pi -- Convert from Hz to rad/s
6117 local x = self.x
6118 local v = self.v
6119 local minValue = self.minValue
6120 local maxValue = self.maxValue
6121 local goal = self.goal
6122
6123 -- Solve the spring ODE for position and velocity after time t, assuming critical damping:
6124 -- 2*f*x'[t] + x''[t] = f^2*(g - x[t])
6125 -- Knowns are x[0] and x'[0].
6126 -- Solve for x[t] and x'[t].
6127
6128 local offset = goal - x
6129 local step = freq*dt
6130 local decay = exp(-step)
6131
6132 local x1 = goal + (v*dt - offset*(step + 1))*decay
6133 local v1 = ((offset*freq - v)*step + v)*decay
6134
6135 -- Constrain
6136 if x1 < minValue then
6137 x1 = minValue
6138 v1 = 0
6139 elseif x1 > maxValue then
6140 x1 = maxValue
6141 v1 = 0
6142 end
6143
6144 self.x = x1
6145 self.v = v1
6146
6147 return x1
6148 end
6149end
6150
6151local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, MIN_FOCUS_DIST, cameraMaxZoomDistance)
6152
6153local function stepTargetZoom(z, dz, zoomMin, zoomMax)
6154 z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax)
6155 if z < DIST_OPAQUE then
6156 z = dz <= 0 and zoomMin or DIST_OPAQUE
6157 end
6158 return z
6159end
6160
6161local zoomDelta = 0
6162
6163local Zoom = {} do
6164 function Zoom.Update(renderDt, focus, extrapolation)
6165 local poppedZoom = math.huge
6166
6167 if zoomSpring.goal > DIST_OPAQUE then
6168 -- Make a pessimistic estimate of zoom distance for this step without accounting for poppercam
6169 local maxPossibleZoom = max(
6170 zoomSpring.x,
6171 stepTargetZoom(zoomSpring.goal, zoomDelta, cameraMinZoomDistance, cameraMaxZoomDistance)
6172 )
6173
6174 -- Run the Popper algorithm on the feasible zoom range, [MIN_FOCUS_DIST, maxPossibleZoom]
6175 poppedZoom = Popper(
6176 focus*CFrame.new(0, 0, MIN_FOCUS_DIST),
6177 maxPossibleZoom - MIN_FOCUS_DIST,
6178 extrapolation
6179 ) + MIN_FOCUS_DIST
6180 end
6181
6182 zoomSpring.minValue = MIN_FOCUS_DIST
6183 zoomSpring.maxValue = min(cameraMaxZoomDistance, poppedZoom)
6184
6185 return zoomSpring:Step(renderDt)
6186 end
6187
6188 function Zoom.SetZoomParameters(targetZoom, newZoomDelta)
6189 zoomSpring.goal = targetZoom
6190 zoomDelta = newZoomDelta
6191 end
6192end
6193
6194return Zoom
6195
6196--------------------------------------------------------------------------------
6197-- Popper.lua
6198-- Prevents your camera from clipping through walls.
6199--------------------------------------------------------------------------------
6200
6201local Players = game:GetService('Players')
6202
6203local FFlagUserPoppercamLooseOpacityThreshold do
6204 local success, enabled = pcall(function()
6205 return UserSettings():IsUserFeatureEnabled("UserPoppercamLooseOpacityThreshold")
6206 end)
6207 FFlagUserPoppercamLooseOpacityThreshold = success and enabled
6208end
6209
6210local camera = game.Workspace.CurrentCamera
6211
6212local min = math.min
6213local tan = math.tan
6214local rad = math.rad
6215local inf = math.huge
6216local ray = Ray.new
6217
6218local function eraseFromEnd(t, toSize)
6219 for i = #t, toSize + 1, -1 do
6220 t[i] = nil
6221 end
6222end
6223
6224local nearPlaneZ, projX, projY do
6225 local function updateProjection()
6226 local fov = rad(camera.FieldOfView)
6227 local view = camera.ViewportSize
6228 local ar = view.X/view.Y
6229
6230 projY = 2*tan(fov/2)
6231 projX = ar*projY
6232 end
6233
6234 camera:GetPropertyChangedSignal('FieldOfView'):Connect(updateProjection)
6235 camera:GetPropertyChangedSignal('ViewportSize'):Connect(updateProjection)
6236
6237 updateProjection()
6238
6239 nearPlaneZ = camera.NearPlaneZ
6240 camera:GetPropertyChangedSignal('NearPlaneZ'):Connect(function()
6241 nearPlaneZ = camera.NearPlaneZ
6242 end)
6243end
6244
6245local blacklist = {} do
6246 local charMap = {}
6247
6248 local function refreshIgnoreList()
6249 local n = 1
6250 blacklist = {}
6251 for _, character in pairs(charMap) do
6252 blacklist[n] = character
6253 n = n + 1
6254 end
6255 end
6256
6257 local function playerAdded(player)
6258 local function characterAdded(character)
6259 charMap[player] = character
6260 refreshIgnoreList()
6261 end
6262 local function characterRemoving()
6263 charMap[player] = nil
6264 refreshIgnoreList()
6265 end
6266
6267 player.CharacterAdded:Connect(characterAdded)
6268 player.CharacterRemoving:Connect(characterRemoving)
6269 if player.Character then
6270 characterAdded(player.Character)
6271 end
6272 end
6273
6274 local function playerRemoving(player)
6275 charMap[player] = nil
6276 refreshIgnoreList()
6277 end
6278
6279 Players.PlayerAdded:Connect(playerAdded)
6280 Players.PlayerRemoving:Connect(playerRemoving)
6281
6282 for _, player in ipairs(Players:GetPlayers()) do
6283 playerAdded(player)
6284 end
6285 refreshIgnoreList()
6286end
6287
6288--------------------------------------------------------------------------------------------
6289-- Popper uses the level geometry find an upper bound on subject-to-camera distance.
6290--
6291-- Hard limits are applied immediately and unconditionally. They're generally caused
6292-- when level geometry intersects with the near plane (with exceptions, see below).
6293--
6294-- Soft limits are only applied under certain conditions.
6295-- They're caused when level geometry occludes the subject without actually intersecting
6296-- with the near plane at the target distance.
6297--
6298-- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits.
6299-- We usually don't want the latter to happen.
6300--
6301-- A soft limit will be promoted to a hard limit if an obstruction
6302-- lies between the current and target camera positions.
6303--------------------------------------------------------------------------------------------
6304
6305local subjectRoot
6306local subjectPart
6307
6308camera:GetPropertyChangedSignal('CameraSubject'):Connect(function()
6309 local subject = camera.CameraSubject
6310 if subject:IsA('Humanoid') then
6311 subjectPart = subject.RootPart
6312 elseif subject:IsA('BasePart') then
6313 subjectPart = subject
6314 else
6315 subjectPart = nil
6316 end
6317end)
6318
6319local function canOcclude(part)
6320 -- Occluders must be:
6321 -- 1. Opaque
6322 -- 2. Interactable
6323 -- 3. Not in the same assembly as the subject
6324
6325 if FFlagUserPoppercamLooseOpacityThreshold then
6326 return
6327 part.Transparency < 0.25 and
6328 part.CanCollide and
6329 subjectRoot ~= (part:GetRootPart() or part) and
6330 not part:IsA('TrussPart')
6331 else
6332 return
6333 part.Transparency < 0.95 and
6334 part.CanCollide and
6335 subjectRoot ~= (part:GetRootPart() or part)
6336 end
6337end
6338
6339-- Offsets for the volume visibility test
6340local SCAN_SAMPLE_OFFSETS = {
6341 Vector2.new( 0.4, 0.0),
6342 Vector2.new(-0.4, 0.0),
6343 Vector2.new( 0.0,-0.4),
6344 Vector2.new( 0.0, 0.4),
6345 Vector2.new( 0.0, 0.2),
6346}
6347
6348--------------------------------------------------------------------------------
6349-- Piercing raycasts
6350
6351local function getCollisionPoint(origin, dir)
6352 local originalSize = #blacklist
6353 repeat
6354 local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList(
6355 ray(origin, dir), blacklist, false, true
6356 )
6357
6358 if hitPart then
6359 if hitPart.CanCollide then
6360 eraseFromEnd(blacklist, originalSize)
6361 return hitPoint, true
6362 end
6363 blacklist[#blacklist + 1] = hitPart
6364 end
6365 until not hitPart
6366
6367 eraseFromEnd(blacklist, originalSize)
6368 return origin + dir, false
6369end
6370
6371--------------------------------------------------------------------------------
6372
6373local function queryPoint(origin, unitDir, dist, lastPos)
6374 debug.profilebegin('queryPoint')
6375
6376 local originalSize = #blacklist
6377
6378 dist = dist + nearPlaneZ
6379 local target = origin + unitDir*dist
6380
6381 local softLimit = inf
6382 local hardLimit = inf
6383 local movingOrigin = origin
6384
6385 repeat
6386 local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
6387
6388 if entryPart then
6389 if canOcclude(entryPart) then
6390 local wl = {entryPart}
6391 local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)
6392
6393 local lim = (entryPos - origin).Magnitude
6394
6395 if exitPart then
6396 local promote = false
6397 if lastPos then
6398 promote =
6399 workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or
6400 workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
6401 end
6402
6403 if promote then
6404 -- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
6405 hardLimit = lim
6406 elseif dist < softLimit then
6407 -- Trivial soft limit
6408 softLimit = lim
6409 end
6410 else
6411 -- Trivial hard limit
6412 hardLimit = lim
6413 end
6414 end
6415
6416 blacklist[#blacklist + 1] = entryPart
6417 movingOrigin = entryPos - unitDir*1e-3
6418 end
6419 until hardLimit < inf or not entryPart
6420
6421 eraseFromEnd(blacklist, originalSize)
6422
6423 debug.profileend()
6424 return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
6425end
6426
6427local function queryViewport(focus, dist)
6428 debug.profilebegin('queryViewport')
6429
6430 local fP = focus.p
6431 local fX = focus.rightVector
6432 local fY = focus.upVector
6433 local fZ = -focus.lookVector
6434
6435 local viewport = camera.ViewportSize
6436
6437 local hardBoxLimit = inf
6438 local softBoxLimit = inf
6439
6440 -- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
6441 for viewX = 0, 1 do
6442 local worldX = fX*((viewX - 0.5)*projX)
6443
6444 for viewY = 0, 1 do
6445 local worldY = fY*((viewY - 0.5)*projY)
6446
6447 local origin = fP + nearPlaneZ*(worldX + worldY)
6448 local lastPos = camera:ViewportPointToRay(
6449 viewport.x*viewX,
6450 viewport.y*viewY
6451 ).Origin
6452
6453 local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)
6454
6455 if hardPointLimit < hardBoxLimit then
6456 hardBoxLimit = hardPointLimit
6457 end
6458 if softPointLimit < softBoxLimit then
6459 softBoxLimit = softPointLimit
6460 end
6461 end
6462 end
6463 debug.profileend()
6464
6465 return softBoxLimit, hardBoxLimit
6466end
6467
6468local function testPromotion(focus, dist, focusExtrapolation)
6469 debug.profilebegin('testPromotion')
6470
6471 local fP = focus.p
6472 local fX = focus.rightVector
6473 local fY = focus.upVector
6474 local fZ = -focus.lookVector
6475
6476 do
6477 -- Dead reckoning the camera rotation and focus
6478 debug.profilebegin('extrapolate')
6479
6480 local SAMPLE_DT = 0.0625
6481 local SAMPLE_MAX_T = 1.25
6482
6483 local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude
6484 -- Metric that decides how many samples to take
6485 local combinedSpeed = focusExtrapolation.posVelocity.magnitude
6486
6487 for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do
6488 local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt
6489
6490 if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
6491 return false
6492 end
6493 end
6494
6495 debug.profileend()
6496 end
6497
6498 do
6499 -- Test screen-space offsets from the focus for the presence of soft limits
6500 debug.profilebegin('testOffsets')
6501
6502 for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
6503 local scaledOffset = offset
6504 local pos, isHit = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
6505 if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
6506 return false
6507 end
6508 end
6509
6510 debug.profileend()
6511 end
6512
6513 debug.profileend()
6514 return true
6515end
6516
6517local function Popper(focus, targetDist, focusExtrapolation)
6518 debug.profilebegin('popper')
6519
6520 subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart
6521
6522 local dist = targetDist
6523 local soft, hard = queryViewport(focus, targetDist)
6524 if hard < dist then
6525 dist = hard
6526 end
6527 if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
6528 dist = soft
6529 end
6530
6531 subjectRoot = nil
6532
6533 debug.profileend()
6534 return dist
6535end
6536
6537return Popper
6538
6539--[[
6540 ControlModule - This ModuleScript implements a singleton class to manage the
6541 selection, activation, and deactivation of the current character movement controller.
6542 This script binds to RenderStepped at Input priority and calls the Update() methods
6543 on the active controller instances.
6544
6545 The character controller ModuleScripts implement classes which are instantiated and
6546 activated as-needed, they are no longer all instantiated up front as they were in
6547 the previous generation of PlayerScripts.
6548
6549 2018 PlayerScripts Update - AllYourBlox
6550--]]
6551local ControlModule = {}
6552ControlModule.__index = ControlModule
6553
6554--[[ Roblox Services ]]--
6555local Players = game:GetService("Players")
6556local RunService = game:GetService("RunService")
6557local UserInputService = game:GetService("UserInputService")
6558local Workspace = game:GetService("Workspace")
6559local UserGameSettings = UserSettings():GetService("UserGameSettings")
6560
6561-- Roblox User Input Control Modules - each returns a new() constructor function used to create controllers as needed
6562local Keyboard = require(script:WaitForChild("Keyboard"))
6563local Gamepad = require(script:WaitForChild("Gamepad"))
6564local TouchDPad = require(script:WaitForChild("TouchDPad"))
6565local DynamicThumbstick = require(script:WaitForChild("DynamicThumbstick"))
6566
6567-- These controllers handle only walk/run movement, jumping is handled by the
6568-- TouchJump controller if any of these are active
6569local ClickToMove = require(script:WaitForChild("ClickToMoveController"))
6570local TouchThumbstick = require(script:WaitForChild("TouchThumbstick"))
6571local TouchThumbpad = require(script:WaitForChild("TouchThumbpad"))
6572local TouchJump = require(script:WaitForChild("TouchJump"))
6573
6574local VehicleController = require(script:WaitForChild("VehicleController"))
6575
6576local CONTROL_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
6577
6578local FFlagUserFixMovementCameraStraightDownSuccess, FFlagUserFixMovementCameraStraightDownResult = pcall(function()
6579 return UserSettings():IsUserFeatureEnabled("UserFixMovementCameraStraightDown")
6580end)
6581local FFlagUserFixMovementCameraStraightDown = FFlagUserFixMovementCameraStraightDownSuccess and FFlagUserFixMovementCameraStraightDownResult
6582
6583local FFlagUserClickToMoveBetterCleanupSuccess, FFlagUserClickToMoveBetterCleanupResult = pcall(function()
6584 return UserSettings():IsUserFeatureEnabled("UserClickToMoveBetterCleanup")
6585end)
6586local FFlagUserClickToMoveBetterCleanup = FFlagUserClickToMoveBetterCleanupSuccess and FFlagUserClickToMoveBetterCleanupResult
6587
6588local FFlagUserFixTouchGuiWhenNoCharacterSuccess, FFlagUserFixTouchGuiWhenNoCharacterResult = pcall(function()
6589 return UserSettings():IsUserFeatureEnabled("UserFixTouchGuiWhenNoCharacter")
6590end)
6591local FFlagUserFixTouchGuiWhenNoCharacter = FFlagUserFixTouchGuiWhenNoCharacterSuccess and FFlagUserFixTouchGuiWhenNoCharacterResult
6592
6593-- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching
6594local movementEnumToModuleMap = {
6595 [Enum.TouchMovementMode.DPad] = TouchDPad,
6596 [Enum.DevTouchMovementMode.DPad] = TouchDPad,
6597 [Enum.TouchMovementMode.Thumbpad] = TouchThumbpad,
6598 [Enum.DevTouchMovementMode.Thumbpad] = TouchThumbpad,
6599 [Enum.TouchMovementMode.Thumbstick] = TouchThumbstick,
6600 [Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick,
6601 [Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
6602 [Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
6603 [Enum.TouchMovementMode.ClickToMove] = ClickToMove,
6604 [Enum.DevTouchMovementMode.ClickToMove] = ClickToMove,
6605
6606 -- Current default
6607 [Enum.TouchMovementMode.Default] = DynamicThumbstick,
6608
6609 [Enum.ComputerMovementMode.Default] = Keyboard,
6610 [Enum.ComputerMovementMode.KeyboardMouse] = Keyboard,
6611 [Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard,
6612 [Enum.DevComputerMovementMode.Scriptable] = nil,
6613 [Enum.ComputerMovementMode.ClickToMove] = ClickToMove,
6614 [Enum.DevComputerMovementMode.ClickToMove] = ClickToMove,
6615}
6616
6617-- Keyboard controller is really keyboard and mouse controller
6618local computerInputTypeToModuleMap = {
6619 [Enum.UserInputType.Keyboard] = Keyboard,
6620 [Enum.UserInputType.MouseButton1] = Keyboard,
6621 [Enum.UserInputType.MouseButton2] = Keyboard,
6622 [Enum.UserInputType.MouseButton3] = Keyboard,
6623 [Enum.UserInputType.MouseWheel] = Keyboard,
6624 [Enum.UserInputType.MouseMovement] = Keyboard,
6625 [Enum.UserInputType.Gamepad1] = Gamepad,
6626 [Enum.UserInputType.Gamepad2] = Gamepad,
6627 [Enum.UserInputType.Gamepad3] = Gamepad,
6628 [Enum.UserInputType.Gamepad4] = Gamepad,
6629}
6630
6631function ControlModule.new()
6632 local self = setmetatable({},ControlModule)
6633
6634 -- The Modules above are used to construct controller instances as-needed, and this
6635 -- table is a map from Module to the instance created from it
6636 self.controllers = {}
6637
6638 self.activeControlModule = nil -- Used to prevent unnecessarily expensive checks on each input event
6639 self.activeController = nil
6640 self.touchJumpController = nil
6641 self.moveFunction = Players.LocalPlayer.Move
6642 self.humanoid = nil
6643 self.lastInputType = Enum.UserInputType.None
6644
6645 -- For Roblox self.vehicleController
6646 self.humanoidSeatedConn = nil
6647 self.vehicleController = nil
6648
6649 self.touchControlFrame = nil
6650
6651 self.vehicleController = VehicleController.new(CONTROL_ACTION_PRIORITY)
6652
6653 Players.LocalPlayer.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end)
6654 Players.LocalPlayer.CharacterRemoving:Connect(function(char) self:OnCharacterAdded(char) end)
6655 if Players.LocalPlayer.Character then
6656 self:OnCharacterAdded(Players.LocalPlayer.Character)
6657 end
6658
6659 RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, function(dt) self:OnRenderStepped(dt) end)
6660
6661 UserInputService.LastInputTypeChanged:Connect(function(newLastInputType) self:OnLastInputTypeChanged(newLastInputType) end)
6662
6663 local propertyChangeListeners = {
6664 UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function() self:OnTouchMovementModeChange() end),
6665 Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function() self:OnTouchMovementModeChange() end),
6666
6667 UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(function() self:OnComputerMovementModeChange() end),
6668 Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function() self:OnComputerMovementModeChange() end),
6669 }
6670
6671 --[[ Touch Device UI ]]--
6672 self.playerGui = nil
6673 self.touchGui = nil
6674 self.playerGuiAddedConn = nil
6675
6676 if UserInputService.TouchEnabled then
6677 self.playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
6678 if self.playerGui then
6679 self:CreateTouchGuiContainer()
6680 self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
6681 else
6682 self.playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child)
6683 if child:IsA("PlayerGui") then
6684 self.playerGui = child
6685 self:CreateTouchGuiContainer()
6686 self.playerGuiAddedConn:Disconnect()
6687 self.playerGuiAddedConn = nil
6688 self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
6689 end
6690 end)
6691 end
6692 else
6693 self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
6694 end
6695
6696 return self
6697end
6698
6699-- Convenience function so that calling code does not have to first get the activeController
6700-- and then call GetMoveVector on it. When there is no active controller, this function returns
6701-- nil so that this case can be distinguished from no current movement (which returns zero vector).
6702function ControlModule:GetMoveVector()
6703 if self.activeController then
6704 return self.activeController:GetMoveVector()
6705 end
6706 return Vector3.new(0,0,0)
6707end
6708
6709function ControlModule:GetActiveController()
6710 return self.activeController
6711end
6712
6713function ControlModule:EnableActiveControlModule()
6714 if self.activeControlModule == ClickToMove then
6715 -- For ClickToMove, when it is the player's choice, we also enable the full keyboard controls.
6716 -- When the developer is forcing click to move, the most keyboard controls (WASD) are not available, only jump.
6717 self.activeController:Enable(
6718 true,
6719 Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice,
6720 self.touchJumpController
6721 )
6722 elseif self.touchControlFrame then
6723 self.activeController:Enable(true, self.touchControlFrame)
6724 else
6725 self.activeController:Enable(true)
6726 end
6727end
6728
6729function ControlModule:Enable(enable)
6730 if not self.activeController then
6731 return
6732 end
6733
6734 if enable == nil then
6735 enable = true
6736 end
6737 if enable then
6738 self:EnableActiveControlModule()
6739 else
6740 self:Disable()
6741 end
6742end
6743
6744-- For those who prefer distinct functions
6745function ControlModule:Disable()
6746 if self.activeController then
6747 self.activeController:Enable(false)
6748
6749 if self.moveFunction then
6750 self.moveFunction(Players.LocalPlayer, Vector3.new(0,0,0), true)
6751 end
6752 end
6753end
6754
6755
6756-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
6757function ControlModule:SelectComputerMovementModule()
6758 if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then
6759 return nil, false
6760 end
6761
6762 local computerModule = nil
6763 local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode
6764
6765 if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then
6766 computerModule = computerInputTypeToModuleMap[lastInputType]
6767 if UserGameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove and computerModule == Keyboard then
6768 -- User has ClickToMove set in Settings, prefer ClickToMove controller for keyboard and mouse lastInputTypes
6769 computerModule = ClickToMove
6770 end
6771 else
6772 -- Developer has selected a mode that must be used.
6773 computerModule = movementEnumToModuleMap[DevMovementMode]
6774
6775 -- computerModule is expected to be nil here only when developer has selected Scriptable
6776 if (not computerModule) and DevMovementMode ~= Enum.DevComputerMovementMode.Scriptable then
6777 warn("No character control module is associated with DevComputerMovementMode ", DevMovementMode)
6778 end
6779 end
6780
6781 if computerModule then
6782 return computerModule, true
6783 elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then
6784 -- Special case where nil is returned and we actually want to set self.activeController to nil for Scriptable
6785 return nil, true
6786 else
6787 -- This case is for when computerModule is nil because of an error and no suitable control module could
6788 -- be found.
6789 return nil, false
6790 end
6791end
6792
6793-- Choose current Touch control module based on settings (user, dev)
6794-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
6795function ControlModule:SelectTouchModule()
6796 if not UserInputService.TouchEnabled then
6797 return nil, false
6798 end
6799 local touchModule = nil
6800 local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode
6801 if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then
6802 touchModule = movementEnumToModuleMap[UserGameSettings.TouchMovementMode]
6803 elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
6804 return nil, true
6805 else
6806 touchModule = movementEnumToModuleMap[DevMovementMode]
6807 end
6808 return touchModule, true
6809end
6810
6811local function calculateRawMoveVector(humanoid, cameraRelativeMoveVector)
6812 local camera = Workspace.CurrentCamera
6813 if not camera then
6814 return cameraRelativeMoveVector
6815 end
6816
6817 if humanoid:GetState() == Enum.HumanoidStateType.Swimming then
6818 return camera.CFrame:VectorToWorldSpace(cameraRelativeMoveVector)
6819 end
6820
6821 local c, s
6822 local _, _, _, R00, R01, R02, _, _, R12, _, _, R22 = camera.CFrame:GetComponents()
6823 if R12 < 1 and R12 > -1 then
6824 -- X and Z components from back vector.
6825 c = R22
6826 s = R02
6827 else
6828 -- In this case the camera is looking straight up or straight down.
6829 -- Use X components from right and up vectors.
6830 c = R00
6831 s = -R01*math.sign(R12)
6832 end
6833 local norm = math.sqrt(c*c + s*s)
6834 return Vector3.new(
6835 (c*cameraRelativeMoveVector.x + s*cameraRelativeMoveVector.z)/norm,
6836 0,
6837 (c*cameraRelativeMoveVector.z - s*cameraRelativeMoveVector.x)/norm
6838 )
6839end
6840
6841function ControlModule:OnRenderStepped(dt)
6842 if self.activeController and self.activeController.enabled and self.humanoid then
6843 -- Give the controller a chance to adjust its state
6844 self.activeController:OnRenderStepped(dt)
6845
6846 -- Now retrieve info from the controller
6847 local moveVector = self.activeController:GetMoveVector()
6848 local cameraRelative = self.activeController:IsMoveVectorCameraRelative()
6849
6850 local clickToMoveController = self:GetClickToMoveController()
6851 if FFlagUserClickToMoveBetterCleanup and self.activeController ~= clickToMoveController then
6852 if moveVector.magnitude > 0 then
6853 -- Clean up any developer started MoveTo path
6854 clickToMoveController:CleanupPath()
6855 else
6856 -- Get move vector for developer started MoveTo
6857 clickToMoveController:OnRenderStepped(dt)
6858 moveVector = clickToMoveController:GetMoveVector()
6859 cameraRelative = clickToMoveController:IsMoveVectorCameraRelative()
6860 end
6861 end
6862
6863 -- Are we driving a vehicle ?
6864 local vehicleConsumedInput = false
6865 if self.vehicleController then
6866 moveVector, vehicleConsumedInput = self.vehicleController:Update(moveVector, cameraRelative, self.activeControlModule==Gamepad)
6867 end
6868
6869 -- If not, move the player
6870 -- Verification of vehicleConsumedInput is commented out to preserve legacy behavior, in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat
6871 --if not vehicleConsumedInput then
6872 if FFlagUserFixMovementCameraStraightDown then
6873 if cameraRelative then
6874 moveVector = calculateRawMoveVector(self.humanoid, moveVector)
6875 end
6876 self.moveFunction(Players.LocalPlayer, moveVector, false)
6877 else
6878 self.moveFunction(Players.LocalPlayer, moveVector, cameraRelative)
6879 end
6880 --end
6881
6882 -- And make them jump if needed
6883 self.humanoid.Jump = self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping())
6884 end
6885end
6886
6887function ControlModule:OnHumanoidSeated(active, currentSeatPart)
6888 if active then
6889 if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then
6890 if not self.vehicleController then
6891 self.vehicleController = self.vehicleController.new(CONTROL_ACTION_PRIORITY)
6892 end
6893 self.vehicleController:Enable(true, currentSeatPart)
6894 end
6895 else
6896 if self.vehicleController then
6897 self.vehicleController:Enable(false, currentSeatPart)
6898 end
6899 end
6900end
6901
6902function ControlModule:OnCharacterAdded(char)
6903 self.humanoid = char:FindFirstChildOfClass("Humanoid")
6904 while not self.humanoid do
6905 char.ChildAdded:wait()
6906 self.humanoid = char:FindFirstChildOfClass("Humanoid")
6907 end
6908
6909 if FFlagUserFixTouchGuiWhenNoCharacter then
6910 if self.touchGui then
6911 self.touchGui.Enabled = true
6912 end
6913 end
6914
6915 if self.humanoidSeatedConn then
6916 self.humanoidSeatedConn:Disconnect()
6917 self.humanoidSeatedConn = nil
6918 end
6919 self.humanoidSeatedConn = self.humanoid.Seated:Connect(function(active, currentSeatPart) self:OnHumanoidSeated(active, currentSeatPart) end)
6920end
6921
6922function ControlModule:OnCharacterRemoving(char)
6923 self.humanoid = nil
6924
6925 if FFlagUserFixTouchGuiWhenNoCharacter then
6926 if self.touchGui then
6927 self.touchGui.Enabled = false
6928 end
6929 end
6930end
6931
6932-- Helper function to lazily instantiate a controller if it does not yet exist,
6933-- disable the active controller if it is different from the on being switched to,
6934-- and then enable the requested controller. The argument to this function must be
6935-- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc.
6936function ControlModule:SwitchToController(controlModule)
6937 if not controlModule then
6938 if self.activeController then
6939 self.activeController:Enable(false)
6940 end
6941 self.activeController = nil
6942 self.activeControlModule = nil
6943 else
6944 if not self.controllers[controlModule] then
6945 self.controllers[controlModule] = controlModule.new(CONTROL_ACTION_PRIORITY)
6946 end
6947
6948 if self.activeController ~= self.controllers[controlModule] then
6949 if self.activeController then
6950 self.activeController:Enable(false)
6951 end
6952 self.activeController = self.controllers[controlModule]
6953 self.activeControlModule = controlModule -- Only used to check if controller switch is necessary
6954
6955 if self.touchControlFrame and (self.activeControlModule == TouchThumbpad
6956 or self.activeControlModule == TouchThumbstick
6957 or self.activeControlModule == ClickToMove
6958 or self.activeControlModule == DynamicThumbstick) then
6959 if not self.controllers[TouchJump] then
6960 self.controllers[TouchJump] = TouchJump.new()
6961 end
6962 self.touchJumpController = self.controllers[TouchJump]
6963 self.touchJumpController:Enable(true, self.touchControlFrame)
6964 else
6965 if self.touchJumpController then
6966 self.touchJumpController:Enable(false)
6967 end
6968 end
6969
6970 self:EnableActiveControlModule()
6971 end
6972 end
6973end
6974
6975function ControlModule:OnLastInputTypeChanged(newLastInputType)
6976 if lastInputType == newLastInputType then
6977 warn("LastInputType Change listener called with current type.")
6978 end
6979 lastInputType = newLastInputType
6980
6981 if lastInputType == Enum.UserInputType.Touch then
6982 -- TODO: Check if touch module already active
6983 local touchModule, success = self:SelectTouchModule()
6984 if success then
6985 while not self.touchControlFrame do
6986 wait()
6987 end
6988 self:SwitchToController(touchModule)
6989 end
6990 elseif computerInputTypeToModuleMap[lastInputType] ~= nil then
6991 local computerModule = self:SelectComputerMovementModule()
6992 if computerModule then
6993 self:SwitchToController(computerModule)
6994 end
6995 end
6996end
6997
6998-- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of
6999-- current control scheme
7000function ControlModule:OnComputerMovementModeChange()
7001 local controlModule, success = self:SelectComputerMovementModule()
7002 if success then
7003 self:SwitchToController(controlModule)
7004 end
7005end
7006
7007function ControlModule:OnTouchMovementModeChange()
7008 local touchModule, success = self:SelectTouchModule()
7009 if success then
7010 while not self.touchControlFrame do
7011 wait()
7012 end
7013 self:SwitchToController(touchModule)
7014 end
7015end
7016
7017function ControlModule:CreateTouchGuiContainer()
7018 if self.touchGui then self.touchGui:Destroy() end
7019
7020 -- Container for all touch device guis
7021 self.touchGui = Instance.new('ScreenGui')
7022 self.touchGui.Name = "TouchGui"
7023 self.touchGui.ResetOnSpawn = false
7024 if FFlagUserFixTouchGuiWhenNoCharacter then
7025 self.touchGui.Enabled = self.humanoid ~= nil
7026 end
7027
7028 self.touchControlFrame = Instance.new("Frame")
7029 self.touchControlFrame.Name = "TouchControlFrame"
7030 self.touchControlFrame.Size = UDim2.new(1, 0, 1, 0)
7031 self.touchControlFrame.BackgroundTransparency = 1
7032 self.touchControlFrame.Parent = self.touchGui
7033
7034 self.touchGui.Parent = self.playerGui
7035end
7036
7037function ControlModule:GetClickToMoveController()
7038 if not self.controllers[ClickToMove] then
7039 self.controllers[ClickToMove] = ClickToMove.new(CONTROL_ACTION_PRIORITY)
7040 end
7041 return self.controllers[ClickToMove]
7042end
7043
7044return ControlModule.new()
7045
7046--[[
7047 BaseCharacterController - Abstract base class for character controllers, not intended to be
7048 directly instantiated.
7049
7050 2018 PlayerScripts Update - AllYourBlox
7051--]]
7052
7053local ZERO_VECTOR3 = Vector3.new(0,0,0)
7054
7055--[[ Roblox Services ]]--
7056local Players = game:GetService("Players")
7057
7058--[[ The Module ]]--
7059local BaseCharacterController = {}
7060BaseCharacterController.__index = BaseCharacterController
7061
7062function BaseCharacterController.new()
7063 local self = setmetatable({}, BaseCharacterController)
7064 self.enabled = false
7065 self.moveVector = ZERO_VECTOR3
7066 self.moveVectorIsCameraRelative = true
7067 self.isJumping = false
7068 return self
7069end
7070
7071function BaseCharacterController:OnRenderStepped(dt)
7072 -- By default, nothing to do
7073end
7074
7075function BaseCharacterController:GetMoveVector()
7076 return self.moveVector
7077end
7078
7079function BaseCharacterController:IsMoveVectorCameraRelative()
7080 return self.moveVectorIsCameraRelative
7081end
7082
7083function BaseCharacterController:GetIsJumping()
7084 return self.isJumping
7085end
7086
7087-- Override in derived classes to set self.enabled and return boolean indicating
7088-- whether Enable/Disable was successful. Return true if controller is already in the requested state.
7089function BaseCharacterController:Enable(enable)
7090 error("BaseCharacterController:Enable must be overridden in derived classes and should not be called.")
7091 return false
7092end
7093
7094return BaseCharacterController
7095
7096--[[
7097 -- Original By Kip Turner, Copyright Roblox 2014
7098 -- Updated by Garnold to utilize the new PathfindingService API, 2017
7099 -- 2018 PlayerScripts Update - AllYourBlox
7100--]]
7101
7102--[[ Roblox Services ]]--
7103local UserInputService = game:GetService("UserInputService")
7104local PathfindingService = game:GetService("PathfindingService")
7105local Players = game:GetService("Players")
7106local DebrisService = game:GetService('Debris')
7107local StarterGui = game:GetService("StarterGui")
7108local Workspace = game:GetService("Workspace")
7109local CollectionService = game:GetService("CollectionService")
7110local GuiService = game:GetService("GuiService")
7111
7112--[[ Configuration ]]
7113local ShowPath = true
7114local PlayFailureAnimation = true
7115local UseDirectPath = false
7116local UseDirectPathForVehicle = true
7117local AgentSizeIncreaseFactor = 1.0
7118local UnreachableWaypointTimeout = 8
7119
7120--[[ Constants ]]--
7121local movementKeys = {
7122 [Enum.KeyCode.W] = true;
7123 [Enum.KeyCode.A] = true;
7124 [Enum.KeyCode.S] = true;
7125 [Enum.KeyCode.D] = true;
7126 [Enum.KeyCode.Up] = true;
7127 [Enum.KeyCode.Down] = true;
7128}
7129
7130local FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess, FFlagUserNavigationClickToMoveSkipPassedWaypointsResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveSkipPassedWaypoints") end)
7131local FFlagUserNavigationClickToMoveSkipPassedWaypoints = FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess and FFlagUserNavigationClickToMoveSkipPassedWaypointsResult
7132local FFlagUserClickToMoveFollowPathRefactorSuccess, FFlagUserClickToMoveFollowPathRefactorResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserClickToMoveFollowPathRefactor") end)
7133local FFlagUserClickToMoveFollowPathRefactor = FFlagUserClickToMoveFollowPathRefactorSuccess and FFlagUserClickToMoveFollowPathRefactorResult
7134
7135local FFlagUserClickToMoveCancelOnMenuOpenedSuccess, FFlagUserClickToMoveCancelOnMenuOpenedResult = pcall(function()
7136 return UserSettings():IsUserFeatureEnabled("UserClickToMoveCancelOnMenuOpened")
7137end)
7138local FFlagUserClickToMoveCancelOnMenuOpened = FFlagUserClickToMoveCancelOnMenuOpenedSuccess and FFlagUserClickToMoveCancelOnMenuOpenedResult
7139
7140local Player = Players.LocalPlayer
7141
7142local ClickToMoveDisplay = require(script.Parent:WaitForChild("ClickToMoveDisplay"))
7143
7144local ZERO_VECTOR3 = Vector3.new(0,0,0)
7145local ALMOST_ZERO = 0.000001
7146
7147
7148--------------------------UTIL LIBRARY-------------------------------
7149local Utility = {}
7150do
7151 local function FindCharacterAncestor(part)
7152 if part then
7153 local humanoid = part:FindFirstChildOfClass("Humanoid")
7154 if humanoid then
7155 return part, humanoid
7156 else
7157 return FindCharacterAncestor(part.Parent)
7158 end
7159 end
7160 end
7161 Utility.FindCharacterAncestor = FindCharacterAncestor
7162
7163 local function Raycast(ray, ignoreNonCollidable, ignoreList)
7164 ignoreList = ignoreList or {}
7165 local hitPart, hitPos, hitNorm, hitMat = Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList)
7166 if hitPart then
7167 if ignoreNonCollidable and hitPart.CanCollide == false then
7168 -- We always include character parts so a user can click on another character
7169 -- to walk to them.
7170 local _, humanoid = FindCharacterAncestor(hitPart)
7171 if humanoid == nil then
7172 table.insert(ignoreList, hitPart)
7173 return Raycast(ray, ignoreNonCollidable, ignoreList)
7174 end
7175 end
7176 return hitPart, hitPos, hitNorm, hitMat
7177 end
7178 return nil, nil
7179 end
7180 Utility.Raycast = Raycast
7181end
7182
7183local humanoidCache = {}
7184local function findPlayerHumanoid(player)
7185 local character = player and player.Character
7186 if character then
7187 local resultHumanoid = humanoidCache[player]
7188 if resultHumanoid and resultHumanoid.Parent == character then
7189 return resultHumanoid
7190 else
7191 humanoidCache[player] = nil -- Bust Old Cache
7192 local humanoid = character:FindFirstChildOfClass("Humanoid")
7193 if humanoid then
7194 humanoidCache[player] = humanoid
7195 end
7196 return humanoid
7197 end
7198 end
7199end
7200
7201--------------------------CHARACTER CONTROL-------------------------------
7202local CurrentIgnoreList
7203local CurrentIgnoreTag = nil
7204
7205local TaggedInstanceAddedConnection = nil
7206local TaggedInstanceRemovedConnection = nil
7207
7208local function GetCharacter()
7209 return Player and Player.Character
7210end
7211
7212local function UpdateIgnoreTag(newIgnoreTag)
7213 if newIgnoreTag == CurrentIgnoreTag then
7214 return
7215 end
7216 if TaggedInstanceAddedConnection then
7217 TaggedInstanceAddedConnection:Disconnect()
7218 TaggedInstanceAddedConnection = nil
7219 end
7220 if TaggedInstanceRemovedConnection then
7221 TaggedInstanceRemovedConnection:Disconnect()
7222 TaggedInstanceRemovedConnection = nil
7223 end
7224 CurrentIgnoreTag = newIgnoreTag
7225 CurrentIgnoreList = {GetCharacter()}
7226 if CurrentIgnoreTag ~= nil then
7227 local ignoreParts = CollectionService:GetTagged(CurrentIgnoreTag)
7228 for _, ignorePart in ipairs(ignoreParts) do
7229 table.insert(CurrentIgnoreList, ignorePart)
7230 end
7231 TaggedInstanceAddedConnection = CollectionService:GetInstanceAddedSignal(
7232 CurrentIgnoreTag):Connect(function(ignorePart)
7233 table.insert(CurrentIgnoreList, ignorePart)
7234 end)
7235 TaggedInstanceRemovedConnection = CollectionService:GetInstanceRemovedSignal(
7236 CurrentIgnoreTag):Connect(function(ignorePart)
7237 for i = 1, #CurrentIgnoreList do
7238 if CurrentIgnoreList[i] == ignorePart then
7239 CurrentIgnoreList[i] = CurrentIgnoreList[#CurrentIgnoreList]
7240 table.remove(CurrentIgnoreList)
7241 break
7242 end
7243 end
7244 end)
7245 end
7246end
7247
7248local function getIgnoreList()
7249 if CurrentIgnoreList then
7250 return CurrentIgnoreList
7251 end
7252 CurrentIgnoreList = {}
7253 table.insert(CurrentIgnoreList, GetCharacter())
7254 return CurrentIgnoreList
7255end
7256
7257-----------------------------------PATHER--------------------------------------
7258
7259local function Pather(endPoint, surfaceNormal, overrideUseDirectPath)
7260 local this = {}
7261
7262 local directPathForHumanoid
7263 local directPathForVehicle
7264 if overrideUseDirectPath ~= nil then
7265 directPathForHumanoid = overrideUseDirectPath
7266 directPathForVehicle = overrideUseDirectPath
7267 else
7268 directPathForHumanoid = UseDirectPath
7269 directPathForVehicle = UseDirectPathForVehicle
7270 end
7271
7272 this.Cancelled = false
7273 this.Started = false
7274
7275 this.Finished = Instance.new("BindableEvent")
7276 this.PathFailed = Instance.new("BindableEvent")
7277
7278 this.PathComputing = false
7279 this.PathComputed = false
7280
7281 this.OriginalTargetPoint = endPoint
7282 this.TargetPoint = endPoint
7283 this.TargetSurfaceNormal = surfaceNormal
7284
7285 this.DiedConn = nil
7286 this.SeatedConn = nil
7287 this.MoveToConn = nil -- To be removd with FFlagUserClickToMoveFollowPathRefactor
7288 this.BlockedConn = nil
7289 this.TeleportedConn = nil
7290
7291 this.CurrentPoint = 0
7292
7293 this.HumanoidOffsetFromPath = ZERO_VECTOR3
7294
7295 this.CurrentWaypointPosition = nil
7296 this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
7297 this.CurrentWaypointPlaneDistance = 0
7298 this.CurrentWaypointNeedsJump = false;
7299
7300 this.CurrentHumanoidPosition = ZERO_VECTOR3
7301 this.CurrentHumanoidVelocity = 0
7302
7303 this.NextActionMoveDirection = ZERO_VECTOR3
7304 this.NextActionJump = false
7305
7306 this.Timeout = 0
7307
7308 this.Humanoid = findPlayerHumanoid(Player)
7309 this.OriginPoint = nil
7310 this.AgentCanFollowPath = false
7311 this.DirectPath = false
7312 this.DirectPathRiseFirst = false
7313
7314 if FFlagUserClickToMoveFollowPathRefactor then
7315 local rootPart = this.Humanoid and this.Humanoid.RootPart
7316 if rootPart then
7317 -- Setup origin
7318 this.OriginPoint = rootPart.CFrame.p
7319
7320 -- Setup agent
7321 local agentRadius = 2
7322 local agentHeight = 5
7323 local agentCanJump = true
7324
7325 local seat = this.Humanoid.SeatPart
7326 if seat and seat:IsA("VehicleSeat") then
7327 -- Humanoid is seated on a vehicle
7328 local vehicle = seat:FindFirstAncestorOfClass("Model")
7329 if vehicle then
7330 -- Make sure the PrimaryPart is set to the vehicle seat while we compute the extends.
7331 local tempPrimaryPart = vehicle.PrimaryPart
7332 vehicle.PrimaryPart = seat
7333
7334 -- For now, only direct path
7335 if directPathForVehicle then
7336 local extents = vehicle:GetExtentsSize()
7337 agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z)
7338 agentHeight = AgentSizeIncreaseFactor * extents.Y
7339 agentCanJump = false
7340 this.AgentCanFollowPath = true
7341 this.DirectPath = directPathForVehicle
7342 end
7343
7344 -- Reset PrimaryPart
7345 vehicle.PrimaryPart = tempPrimaryPart
7346 end
7347 else
7348 local extents = GetCharacter():GetExtentsSize()
7349 agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z)
7350 agentHeight = AgentSizeIncreaseFactor * extents.Y
7351 agentCanJump = (this.Humanoid.JumpPower > 0)
7352 this.AgentCanFollowPath = true
7353 this.DirectPath = directPathForHumanoid
7354 this.DirectPathRiseFirst = this.Humanoid.Sit
7355 end
7356
7357 -- Build path object
7358 this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump})
7359 end
7360 else
7361 if this.Humanoid then
7362 local torso = this.Humanoid.Torso
7363 if torso then
7364 this.OriginPoint = torso.CFrame.p
7365 this.AgentCanFollowPath = true
7366 this.DirectPath = directPathForHumanoid
7367 end
7368 end
7369 end
7370
7371 function this:Cleanup()
7372 if this.stopTraverseFunc then
7373 this.stopTraverseFunc()
7374 this.stopTraverseFunc = nil
7375 end
7376
7377 if this.MoveToConn then
7378 this.MoveToConn:Disconnect()
7379 this.MoveToConn = nil
7380 end
7381
7382 if this.BlockedConn then
7383 this.BlockedConn:Disconnect()
7384 this.BlockedConn = nil
7385 end
7386
7387 if this.DiedConn then
7388 this.DiedConn:Disconnect()
7389 this.DiedConn = nil
7390 end
7391
7392 if this.SeatedConn then
7393 this.SeatedConn:Disconnect()
7394 this.SeatedConn = nil
7395 end
7396
7397 if this.TeleportedConn then
7398 this.TeleportedConn:Disconnect()
7399 this.TeleportedConn = nil
7400 end
7401
7402 this.Started = false
7403 end
7404
7405 function this:Cancel()
7406 this.Cancelled = true
7407 this:Cleanup()
7408 end
7409
7410 function this:IsActive()
7411 return this.AgentCanFollowPath and this.Started and not this.Cancelled
7412 end
7413
7414 function this:OnPathInterrupted()
7415 -- Stop moving
7416 this.Cancelled = true
7417 this:OnPointReached(false)
7418 end
7419
7420 function this:ComputePath()
7421 if this.OriginPoint then
7422 if this.PathComputed or this.PathComputing then return end
7423 this.PathComputing = true
7424 if FFlagUserClickToMoveFollowPathRefactor then
7425 if this.AgentCanFollowPath then
7426 if this.DirectPath then
7427 this.pointList = {
7428 PathWaypoint.new(this.OriginPoint, Enum.PathWaypointAction.Walk),
7429 PathWaypoint.new(this.TargetPoint, this.DirectPathRiseFirst and Enum.PathWaypointAction.Jump or Enum.PathWaypointAction.Walk)
7430 }
7431 this.PathComputed = true
7432 else
7433 this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint)
7434 this.pointList = this.pathResult:GetWaypoints()
7435 this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
7436 this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success
7437 end
7438 end
7439 else
7440 pcall(function()
7441 this.pathResult = PathfindingService:FindPathAsync(this.OriginPoint, this.TargetPoint)
7442 end)
7443 this.pointList = this.pathResult and this.pathResult:GetWaypoints()
7444 if this.pathResult then
7445 this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
7446 end
7447 this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false
7448 end
7449 this.PathComputing = false
7450 end
7451 end
7452
7453 function this:IsValidPath()
7454 if FFlagUserClickToMoveFollowPathRefactor then
7455 this:ComputePath()
7456 return this.PathComputed and this.AgentCanFollowPath
7457 else
7458 if not this.pathResult then
7459 this:ComputePath()
7460 end
7461 return this.pathResult.Status == Enum.PathStatus.Success
7462 end
7463 end
7464
7465 this.Recomputing = false
7466 function this:OnPathBlocked(blockedWaypointIdx)
7467 local pathBlocked = blockedWaypointIdx >= this.CurrentPoint
7468 if not pathBlocked or this.Recomputing then
7469 return
7470 end
7471
7472 this.Recomputing = true
7473
7474 if this.stopTraverseFunc then
7475 this.stopTraverseFunc()
7476 this.stopTraverseFunc = nil
7477 end
7478
7479 if FFlagUserClickToMoveFollowPathRefactor then
7480 this.OriginPoint = this.Humanoid.RootPart.CFrame.p
7481 else
7482 this.OriginPoint = this.Humanoid.Torso.CFrame.p
7483 end
7484
7485 this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint)
7486 this.pointList = this.pathResult:GetWaypoints()
7487 if #this.pointList > 0 then
7488 if FFlagUserClickToMoveFollowPathRefactor then
7489 this.HumanoidOffsetFromPath = this.pointList[1].Position - this.OriginPoint
7490 else
7491 -- Nothing to do : offset did not change
7492 end
7493 end
7494 this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success
7495
7496 if ShowPath then
7497 this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList)
7498 end
7499 if this.PathComputed then
7500 this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
7501 this:OnPointReached(true) -- Move to first point
7502 else
7503 this.PathFailed:Fire()
7504 this:Cleanup()
7505 end
7506
7507 this.Recomputing = false
7508 end
7509
7510 function this:OnRenderStepped(dt)
7511 if this.Started and not this.Cancelled then
7512 -- Check for Timeout (if a waypoint is not reached within the delay, we fail)
7513 this.Timeout = this.Timeout + dt
7514 if this.Timeout > UnreachableWaypointTimeout then
7515 this:OnPointReached(false)
7516 return
7517 end
7518
7519 -- Get Humanoid position and velocity
7520 this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath
7521 this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity
7522
7523 -- Check if it has reached some waypoints
7524 while this.Started and this:IsCurrentWaypointReached() do
7525 this:OnPointReached(true)
7526 end
7527
7528 -- If still started, update actions
7529 if this.Started then
7530 -- Move action
7531 this.NextActionMoveDirection = this.CurrentWaypointPosition - this.CurrentHumanoidPosition
7532 if this.NextActionMoveDirection.Magnitude > ALMOST_ZERO then
7533 this.NextActionMoveDirection = this.NextActionMoveDirection.Unit
7534 else
7535 this.NextActionMoveDirection = ZERO_VECTOR3
7536 end
7537 -- Jump action
7538 if this.CurrentWaypointNeedsJump then
7539 this.NextActionJump = true
7540 this.CurrentWaypointNeedsJump = false -- Request jump only once
7541 else
7542 this.NextActionJump = false
7543 end
7544 end
7545 end
7546 end
7547
7548 function this:IsCurrentWaypointReached()
7549 local reached = false
7550
7551 -- Check we do have a plane, if not, we consider the waypoint reached
7552 if this.CurrentWaypointPlaneNormal ~= ZERO_VECTOR3 then
7553 -- Compute distance of Humanoid from destination plane
7554 local dist = this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidPosition) - this.CurrentWaypointPlaneDistance
7555 -- Compute the component of the Humanoid velocity that is towards the plane
7556 local velocity = -this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidVelocity)
7557 -- Compute the threshold from the destination plane based on Humanoid velocity
7558 local threshold = math.max(1.0, 0.0625 * velocity)
7559 -- If we are less then threshold in front of the plane (between 0 and threshold) or if we are behing the plane (less then 0), we consider we reached it
7560 reached = dist < threshold
7561 else
7562 reached = true
7563 end
7564
7565 if reached then
7566 this.CurrentWaypointPosition = nil
7567 this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
7568 this.CurrentWaypointPlaneDistance = 0
7569 end
7570
7571 return reached
7572 end
7573
7574 function this:OnPointReached(reached)
7575
7576 if reached and not this.Cancelled then
7577
7578 if FFlagUserClickToMoveFollowPathRefactor then
7579 -- First, destroyed the current displayed waypoint
7580 if this.setPointFunc then
7581 this.setPointFunc(this.CurrentPoint)
7582 end
7583 end
7584
7585 local nextWaypointIdx = this.CurrentPoint + 1
7586
7587 if nextWaypointIdx > #this.pointList then
7588 -- End of path reached
7589 if this.stopTraverseFunc then
7590 this.stopTraverseFunc()
7591 end
7592 this.Finished:Fire()
7593 this:Cleanup()
7594 else
7595 local currentWaypoint = this.pointList[this.CurrentPoint]
7596 local nextWaypoint = this.pointList[nextWaypointIdx]
7597
7598 -- If airborne, only allow to keep moving
7599 -- if nextWaypoint.Action ~= Jump, or path mantains a direction
7600 -- Otherwise, wait until the humanoid gets to the ground
7601 local currentState = this.Humanoid:GetState()
7602 local isInAir = currentState == Enum.HumanoidStateType.FallingDown
7603 or currentState == Enum.HumanoidStateType.Freefall
7604 or currentState == Enum.HumanoidStateType.Jumping
7605
7606 if isInAir then
7607 local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump
7608 if not shouldWaitForGround and this.CurrentPoint > 1 then
7609 local prevWaypoint = this.pointList[this.CurrentPoint - 1]
7610
7611 local prevDir = currentWaypoint.Position - prevWaypoint.Position
7612 local currDir = nextWaypoint.Position - currentWaypoint.Position
7613
7614 local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit
7615 local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit
7616
7617 local THRESHOLD_COS = 0.996 -- ~cos(5 degrees)
7618 shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS
7619 end
7620
7621 if shouldWaitForGround then
7622 this.Humanoid.FreeFalling:Wait()
7623
7624 -- Give time to the humanoid's state to change
7625 -- Otherwise, the jump flag in Humanoid
7626 -- will be reset by the state change
7627 wait(0.1)
7628 end
7629 end
7630
7631 -- Move to the next point
7632 if FFlagUserNavigationClickToMoveSkipPassedWaypoints then
7633 if FFlagUserClickToMoveFollowPathRefactor then
7634 this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx)
7635 else
7636 -- First, check if we already passed the next point
7637 local nextWaypointAlreadyReached
7638 -- 1) Build plane (normal is from next waypoint towards current one
7639 -- (provided the two waypoints are not at the same location); location is at next waypoint)
7640 local planeNormal = currentWaypoint.Position - nextWaypoint.Position
7641 if planeNormal.Magnitude > ALMOST_ZERO then
7642 planeNormal = planeNormal.Unit
7643 local planeDistance = planeNormal:Dot(nextWaypoint.Position)
7644 -- 2) Find current Humanoid position
7645 local humanoidPosition = this.Humanoid.RootPart.Position - Vector3.new(
7646 0, 0.5 * this.Humanoid.RootPart.Size.y + this.Humanoid.HipHeight, 0)
7647 -- 3) Compute distance from plane
7648 local dist = planeNormal:Dot(humanoidPosition) - planeDistance
7649 -- 4) If we are less then a stud in front of the plane or if we are behing the plane, we consider we reached it
7650 nextWaypointAlreadyReached = dist < 1.0
7651 else
7652 -- Next waypoint is the same as current waypoint so we reached it as well
7653 nextWaypointAlreadyReached = true
7654 end
7655
7656 -- Prepare for next point
7657 if not FFlagUserClickToMoveFollowPathRefactor then
7658 if this.setPointFunc then
7659 this.setPointFunc(nextWaypointIdx)
7660 end
7661 end
7662 this.CurrentPoint = nextWaypointIdx
7663
7664 -- Either callback here right away if next waypoint is already passed
7665 -- Otherwise, ask the Humanoid to MoveTo
7666 if nextWaypointAlreadyReached then
7667 this:OnPointReached(true)
7668 else
7669 if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
7670 this.Humanoid.Jump = true
7671 end
7672 this.Humanoid:MoveTo(nextWaypoint.Position)
7673 end
7674 end
7675 else
7676 if this.setPointFunc then
7677 this.setPointFunc(nextWaypointIdx)
7678 end
7679 if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
7680 this.Humanoid.Jump = true
7681 end
7682 this.Humanoid:MoveTo(nextWaypoint.Position)
7683
7684 this.CurrentPoint = nextWaypointIdx
7685 end
7686 end
7687 else
7688 this.PathFailed:Fire()
7689 this:Cleanup()
7690 end
7691 end
7692
7693 function this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx)
7694 -- Build next destination plane
7695 -- (plane normal is perpendicular to the y plane and is from next waypoint towards current one (provided the two waypoints are not at the same location))
7696 -- (plane location is at next waypoint)
7697 this.CurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position
7698 this.CurrentWaypointPlaneNormal = Vector3.new(this.CurrentWaypointPlaneNormal.X, 0, this.CurrentWaypointPlaneNormal.Z)
7699 if this.CurrentWaypointPlaneNormal.Magnitude > ALMOST_ZERO then
7700 this.CurrentWaypointPlaneNormal = this.CurrentWaypointPlaneNormal.Unit
7701 this.CurrentWaypointPlaneDistance = this.CurrentWaypointPlaneNormal:Dot(nextWaypoint.Position)
7702 else
7703 -- Next waypoint is the same as current waypoint so no plane
7704 this.CurrentWaypointPlaneNormal = ZERO_VECTOR3
7705 this.CurrentWaypointPlaneDistance = 0
7706 end
7707
7708 -- Should we jump
7709 this.CurrentWaypointNeedsJump = nextWaypoint.Action == Enum.PathWaypointAction.Jump;
7710
7711 -- Remember next waypoint position
7712 this.CurrentWaypointPosition = nextWaypoint.Position
7713
7714 -- Move to next point
7715 this.CurrentPoint = nextWaypointIdx
7716
7717 -- Finally reset Timeout
7718 this.Timeout = 0
7719 end
7720
7721 function this:Start(overrideShowPath)
7722 if not this.AgentCanFollowPath then
7723 this.PathFailed:Fire()
7724 return
7725 end
7726
7727 if this.Started then return end
7728 this.Started = true
7729
7730 ClickToMoveDisplay.CancelFailureAnimation()
7731
7732 if ShowPath then
7733 if overrideShowPath == nil or overrideShowPath then
7734 this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList, this.OriginalTargetPoint)
7735 end
7736 end
7737
7738 if #this.pointList > 0 then
7739 -- Determine the humanoid offset from the path's first point
7740 if FFlagUserClickToMoveFollowPathRefactor then
7741 -- Offset of the first waypoint from the path's origin point
7742 this.HumanoidOffsetFromPath = Vector3.new(0, this.pointList[1].Position.Y - this.OriginPoint.Y, 0)
7743 else
7744 -- Humanoid height above ground
7745 this.HumanoidOffsetFromPath = Vector3.new(0, -(0.5 * this.Humanoid.RootPart.Size.y + this.Humanoid.HipHeight), 0)
7746 end
7747
7748 -- As well as its current position and velocity
7749 this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath
7750 this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity
7751
7752 -- Connect to events
7753 this.SeatedConn = this.Humanoid.Seated:Connect(function(isSeated, seat) this:OnPathInterrupted() end)
7754 this.DiedConn = this.Humanoid.Died:Connect(function() this:OnPathInterrupted() end)
7755 if not FFlagUserClickToMoveFollowPathRefactor then
7756 this.MoveToConn = this.Humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end)
7757 end
7758 if FFlagUserClickToMoveFollowPathRefactor then
7759 this.TeleportedConn = this.Humanoid.RootPart:GetPropertyChangedSignal("CFrame"):Connect(function() this:OnPathInterrupted() end)
7760 end
7761
7762 -- Actually start
7763 this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
7764 this:OnPointReached(true) -- Move to first point
7765 else
7766 this.PathFailed:Fire()
7767 if this.stopTraverseFunc then
7768 this.stopTraverseFunc()
7769 end
7770 end
7771 end
7772
7773 --We always raycast to the ground in the case that the user clicked a wall.
7774 local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5
7775 local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50)
7776 local newHitPart, newHitPos = Workspace:FindPartOnRayWithIgnoreList(ray, getIgnoreList())
7777 if newHitPart then
7778 this.TargetPoint = newHitPos
7779 end
7780 this:ComputePath()
7781
7782 return this
7783end
7784
7785-------------------------------------------------------------------------
7786
7787local function CheckAlive()
7788 local humanoid = findPlayerHumanoid(Player)
7789 return humanoid ~= nil and humanoid.Health > 0
7790end
7791
7792local function GetEquippedTool(character)
7793 if character ~= nil then
7794 for _, child in pairs(character:GetChildren()) do
7795 if child:IsA('Tool') then
7796 return child
7797 end
7798 end
7799 end
7800end
7801
7802local ExistingPather = nil
7803local ExistingIndicator = nil
7804local PathCompleteListener = nil
7805local PathFailedListener = nil
7806
7807local function CleanupPath()
7808 if ExistingPather then
7809 ExistingPather:Cancel()
7810 ExistingPather = nil
7811 end
7812 if PathCompleteListener then
7813 PathCompleteListener:Disconnect()
7814 PathCompleteListener = nil
7815 end
7816 if PathFailedListener then
7817 PathFailedListener:Disconnect()
7818 PathFailedListener = nil
7819 end
7820 if ExistingIndicator then
7821 ExistingIndicator:Destroy()
7822 end
7823end
7824
7825-- Remove with FFlagUserClickToMoveFollowPathRefactor
7826local function HandleDirectMoveTo(hitPt, myHumanoid)
7827 if myHumanoid then
7828 if myHumanoid.Sit then
7829 myHumanoid.Jump = true
7830 end
7831 myHumanoid:MoveTo(hitPt)
7832 end
7833
7834 local endWaypoint = ClickToMoveDisplay.CreateEndWaypoint(hitPt)
7835 ExistingIndicator = endWaypoint
7836 coroutine.wrap(function()
7837 myHumanoid.MoveToFinished:wait()
7838 endWaypoint:Destroy()
7839 end)()
7840end
7841
7842local function HandleMoveTo(thisPather, hitPt, hitChar, character, overrideShowPath)
7843 if FFlagUserClickToMoveFollowPathRefactor then
7844 ExistingPather = thisPather
7845 end
7846
7847 thisPather:Start(overrideShowPath)
7848
7849 if not FFlagUserClickToMoveFollowPathRefactor then
7850 CleanupPath()
7851 end
7852
7853 if not FFlagUserClickToMoveFollowPathRefactor then
7854 ExistingPather = thisPather
7855 end
7856
7857 PathCompleteListener = thisPather.Finished.Event:Connect(function()
7858 if FFlagUserClickToMoveFollowPathRefactor then
7859 CleanupPath()
7860 end
7861 if hitChar then
7862 local currentWeapon = GetEquippedTool(character)
7863 if currentWeapon then
7864 currentWeapon:Activate()
7865 end
7866 end
7867 end)
7868 PathFailedListener = thisPather.PathFailed.Event:Connect(function()
7869 CleanupPath()
7870 if overrideShowPath == nil or overrideShowPath then
7871 local shouldPlayFailureAnim = PlayFailureAnimation and not (ExistingPather and ExistingPather:IsActive())
7872 if shouldPlayFailureAnim then
7873 ClickToMoveDisplay.PlayFailureAnimation()
7874 end
7875 ClickToMoveDisplay.DisplayFailureWaypoint(hitPt)
7876 end
7877 end)
7878end
7879
7880local function ShowPathFailedFeedback(hitPt)
7881 if ExistingPather and ExistingPather:IsActive() then
7882 ExistingPather:Cancel()
7883 end
7884 if PlayFailureAnimation then
7885 ClickToMoveDisplay.PlayFailureAnimation()
7886 end
7887 ClickToMoveDisplay.DisplayFailureWaypoint(hitPt)
7888end
7889
7890function OnTap(tapPositions, goToPoint, wasTouchTap)
7891 -- Good to remember if this is the latest tap event
7892 local camera = Workspace.CurrentCamera
7893 local character = Player.Character
7894
7895 if not CheckAlive() then return end
7896
7897 -- This is a path tap position
7898 if #tapPositions == 1 or goToPoint then
7899 if camera then
7900 local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y)
7901 local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)
7902
7903 local myHumanoid = findPlayerHumanoid(Player)
7904 local hitPart, hitPt, hitNormal = Utility.Raycast(ray, true, getIgnoreList())
7905
7906 local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart)
7907 if wasTouchTap and hitHumanoid and StarterGui:GetCore("AvatarContextMenuEnabled") then
7908 local clickedPlayer = Players:GetPlayerFromCharacter(hitHumanoid.Parent)
7909 if clickedPlayer then
7910 CleanupPath()
7911 return
7912 end
7913 end
7914 if goToPoint then
7915 hitPt = goToPoint
7916 hitChar = nil
7917 end
7918 if FFlagUserClickToMoveFollowPathRefactor then
7919 if hitPt and character then
7920 -- Clean up current path
7921 CleanupPath()
7922 local thisPather = Pather(hitPt, hitNormal)
7923 if thisPather:IsValidPath() then
7924 HandleMoveTo(thisPather, hitPt, hitChar, character)
7925 else
7926 -- Clean up
7927 thisPather:Cleanup()
7928 -- Feedback here for when we don't have a good path
7929 ShowPathFailedFeedback(hitPt)
7930 end
7931 end
7932 else
7933 if UseDirectPath and hitPt and character then
7934 HandleDirectMoveTo(hitPt, myHumanoid)
7935 elseif hitPt and character then
7936 local thisPather = Pather(hitPt, hitNormal)
7937 if thisPather:IsValidPath() then
7938 HandleMoveTo(thisPather, hitPt, hitChar, character)
7939 else
7940 if hitPt then
7941 -- Feedback here for when we don't have a good path
7942 ShowPathFailedFeedback(hitPt)
7943 end
7944 end
7945 end
7946 end
7947 end
7948 elseif #tapPositions >= 2 then
7949 if camera then
7950 -- Do shoot
7951 local currentWeapon = GetEquippedTool(character)
7952 if currentWeapon then
7953 currentWeapon:Activate()
7954 end
7955 end
7956 end
7957end
7958
7959local function DisconnectEvent(event)
7960 if event then
7961 event:Disconnect()
7962 end
7963end
7964
7965--[[ The ClickToMove Controller Class ]]--
7966local KeyboardController = require(script.Parent:WaitForChild("Keyboard"))
7967local ClickToMove = setmetatable({}, KeyboardController)
7968ClickToMove.__index = ClickToMove
7969
7970function ClickToMove.new(CONTROL_ACTION_PRIORITY)
7971 local self = setmetatable(KeyboardController.new(CONTROL_ACTION_PRIORITY), ClickToMove)
7972
7973 self.fingerTouches = {}
7974 self.numUnsunkTouches = 0
7975 -- PC simulation
7976 self.mouse1Down = tick()
7977 self.mouse1DownPos = Vector2.new()
7978 self.mouse2DownTime = tick()
7979 self.mouse2DownPos = Vector2.new()
7980 self.mouse2UpTime = tick()
7981
7982 self.keyboardMoveVector = ZERO_VECTOR3
7983
7984 self.tapConn = nil
7985 self.inputBeganConn = nil
7986 self.inputChangedConn = nil
7987 self.inputEndedConn = nil
7988 self.humanoidDiedConn = nil
7989 self.characterChildAddedConn = nil
7990 self.onCharacterAddedConn = nil
7991 self.characterChildRemovedConn = nil
7992 self.renderSteppedConn = nil
7993 self.menuOpenedConnection = nil
7994
7995 self.running = false
7996
7997 self.wasdEnabled = false
7998
7999 return self
8000end
8001
8002function ClickToMove:DisconnectEvents()
8003 DisconnectEvent(self.tapConn)
8004 DisconnectEvent(self.inputBeganConn)
8005 DisconnectEvent(self.inputChangedConn)
8006 DisconnectEvent(self.inputEndedConn)
8007 DisconnectEvent(self.humanoidDiedConn)
8008 DisconnectEvent(self.characterChildAddedConn)
8009 DisconnectEvent(self.onCharacterAddedConn)
8010 DisconnectEvent(self.renderSteppedConn)
8011 DisconnectEvent(self.characterChildRemovedConn)
8012 if FFlagUserClickToMoveCancelOnMenuOpened then
8013 DisconnectEvent(self.menuOpenedConnection)
8014 end
8015end
8016
8017function ClickToMove:OnTouchBegan(input, processed)
8018 if self.fingerTouches[input] == nil and not processed then
8019 self.numUnsunkTouches = self.numUnsunkTouches + 1
8020 end
8021 self.fingerTouches[input] = processed
8022end
8023
8024function ClickToMove:OnTouchChanged(input, processed)
8025 if self.fingerTouches[input] == nil then
8026 self.fingerTouches[input] = processed
8027 if not processed then
8028 self.numUnsunkTouches = self.numUnsunkTouches + 1
8029 end
8030 end
8031end
8032
8033function ClickToMove:OnTouchEnded(input, processed)
8034 if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
8035 self.numUnsunkTouches = self.numUnsunkTouches - 1
8036 end
8037 self.fingerTouches[input] = nil
8038end
8039
8040
8041function ClickToMove:OnCharacterAdded(character)
8042 self:DisconnectEvents()
8043
8044 self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
8045 if input.UserInputType == Enum.UserInputType.Touch then
8046 self:OnTouchBegan(input, processed)
8047 end
8048
8049 -- Cancel path when you use the keyboard controls.
8050 if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then
8051 CleanupPath()
8052 if FFlagUserClickToMoveFollowPathRefactor then
8053 ClickToMoveDisplay.CancelFailureAnimation()
8054 end
8055 end
8056 if input.UserInputType == Enum.UserInputType.MouseButton1 then
8057 self.mouse1DownTime = tick()
8058 self.mouse1DownPos = input.Position
8059 end
8060 if input.UserInputType == Enum.UserInputType.MouseButton2 then
8061 self.mouse2DownTime = tick()
8062 self.mouse2DownPos = input.Position
8063 end
8064 end)
8065
8066 self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
8067 if input.UserInputType == Enum.UserInputType.Touch then
8068 self:OnTouchChanged(input, processed)
8069 end
8070 end)
8071
8072 self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
8073 if input.UserInputType == Enum.UserInputType.Touch then
8074 self:OnTouchEnded(input, processed)
8075 end
8076
8077 if input.UserInputType == Enum.UserInputType.MouseButton2 then
8078 self.mouse2UpTime = tick()
8079 local currPos = input.Position
8080 -- We allow click to move during path following or if there is no keyboard movement
8081 local allowed
8082 if FFlagUserClickToMoveFollowPathRefactor then
8083 allowed = ExistingPather or self.keyboardMoveVector.Magnitude <= 0
8084 else
8085 allowed = self.moveVector.Magnitude <= 0
8086 end
8087 if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 and allowed then
8088 local positions = {currPos}
8089 OnTap(positions)
8090 end
8091 end
8092 end)
8093
8094 self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed)
8095 if not processed then
8096 OnTap(touchPositions, nil, true)
8097 end
8098 end)
8099
8100 if FFlagUserClickToMoveCancelOnMenuOpened then
8101 self.menuOpenedConnection = GuiService.MenuOpened:Connect(function()
8102 CleanupPath()
8103 end)
8104 end
8105
8106 local function OnCharacterChildAdded(child)
8107 if UserInputService.TouchEnabled then
8108 if child:IsA('Tool') then
8109 child.ManualActivationOnly = true
8110 end
8111 end
8112 if child:IsA('Humanoid') then
8113 DisconnectEvent(self.humanoidDiedConn)
8114 self.humanoidDiedConn = child.Died:Connect(function()
8115 if ExistingIndicator then
8116 DebrisService:AddItem(ExistingIndicator.Model, 1)
8117 end
8118 end)
8119 end
8120 end
8121
8122 self.characterChildAddedConn = character.ChildAdded:Connect(function(child)
8123 OnCharacterChildAdded(child)
8124 end)
8125 self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child)
8126 if UserInputService.TouchEnabled then
8127 if child:IsA('Tool') then
8128 child.ManualActivationOnly = false
8129 end
8130 end
8131 end)
8132 for _, child in pairs(character:GetChildren()) do
8133 OnCharacterChildAdded(child)
8134 end
8135end
8136
8137function ClickToMove:Start()
8138 self:Enable(true)
8139end
8140
8141function ClickToMove:Stop()
8142 self:Enable(false)
8143end
8144
8145function ClickToMove:CleanupPath()
8146 CleanupPath()
8147end
8148
8149function ClickToMove:Enable(enable, enableWASD, touchJumpController)
8150 if enable then
8151 if not self.running then
8152 if Player.Character then -- retro-listen
8153 self:OnCharacterAdded(Player.Character)
8154 end
8155 self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char)
8156 self:OnCharacterAdded(char)
8157 end)
8158 self.running = true
8159 end
8160 self.touchJumpController = touchJumpController
8161 if self.touchJumpController then
8162 self.touchJumpController:Enable(self.jumpEnabled)
8163 end
8164 else
8165 if self.running then
8166 self:DisconnectEvents()
8167 CleanupPath()
8168 -- Restore tool activation on shutdown
8169 if UserInputService.TouchEnabled then
8170 local character = Player.Character
8171 if character then
8172 for _, child in pairs(character:GetChildren()) do
8173 if child:IsA('Tool') then
8174 child.ManualActivationOnly = false
8175 end
8176 end
8177 end
8178 end
8179 self.running = false
8180 end
8181 if self.touchJumpController and not self.jumpEnabled then
8182 self.touchJumpController:Enable(true)
8183 end
8184 self.touchJumpController = nil
8185 end
8186
8187 -- Extension for initializing Keyboard input as this class now derives from Keyboard
8188 if UserInputService.KeyboardEnabled and enable ~= self.enabled then
8189
8190 self.forwardValue = 0
8191 self.backwardValue = 0
8192 self.leftValue = 0
8193 self.rightValue = 0
8194
8195 self.moveVector = ZERO_VECTOR3
8196
8197 if enable then
8198 self:BindContextActions()
8199 self:ConnectFocusEventListeners()
8200 else
8201 self:UnbindContextActions()
8202 self:DisconnectFocusEventListeners()
8203 end
8204 end
8205
8206 self.wasdEnabled = enable and enableWASD or false
8207 self.enabled = enable
8208end
8209
8210function ClickToMove:OnRenderStepped(dt)
8211 if not FFlagUserClickToMoveFollowPathRefactor then
8212 return
8213 end
8214
8215 -- Reset jump
8216 self.isJumping = false
8217
8218 -- Handle Pather
8219 if ExistingPather then
8220 -- Let the Pather update
8221 ExistingPather:OnRenderStepped(dt)
8222
8223 -- If we still have a Pather, set the resulting actions
8224 if ExistingPather then
8225 -- Setup move (NOT relative to camera)
8226 self.moveVector = ExistingPather.NextActionMoveDirection
8227 self.moveVectorIsCameraRelative = false
8228
8229 -- Setup jump (but do NOT prevent the base Keayboard class from requesting jumps as well)
8230 if ExistingPather.NextActionJump then
8231 self.isJumping = true
8232 end
8233 else
8234 self.moveVector = self.keyboardMoveVector
8235 self.moveVectorIsCameraRelative = true
8236 end
8237 else
8238 self.moveVector = self.keyboardMoveVector
8239 self.moveVectorIsCameraRelative = true
8240 end
8241
8242 -- Handle Keyboard's jump
8243 if self.jumpRequested then
8244 self.isJumping = true
8245 end
8246end
8247
8248-- Overrides Keyboard:UpdateMovement(inputState) to conditionally consider self.wasdEnabled and let OnRenderStepped handle the movement
8249function ClickToMove:UpdateMovement(inputState)
8250 if FFlagUserClickToMoveFollowPathRefactor then
8251 if inputState == Enum.UserInputState.Cancel then
8252 self.keyboardMoveVector = ZERO_VECTOR3
8253 elseif self.wasdEnabled then
8254 self.keyboardMoveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
8255 end
8256 else
8257 if inputState == Enum.UserInputState.Cancel then
8258 self.moveVector = ZERO_VECTOR3
8259 elseif self.wasdEnabled then
8260 self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
8261 if self.moveVector.magnitude > 0 then
8262 CleanupPath()
8263 ClickToMoveDisplay.CancelFailureAnimation()
8264 end
8265 end
8266 end
8267end
8268
8269-- Overrides Keyboard:UpdateJump() because jump is handled in OnRenderStepped
8270function ClickToMove:UpdateJump()
8271 if FFlagUserClickToMoveFollowPathRefactor then
8272 -- Nothing to do (handled in OnRenderStepped)
8273 else
8274 self.isJumping = self.jumpRequested
8275 end
8276end
8277
8278--Public developer facing functions
8279function ClickToMove:SetShowPath(value)
8280 ShowPath = value
8281end
8282
8283function ClickToMove:GetShowPath()
8284 return ShowPath
8285end
8286
8287function ClickToMove:SetWaypointTexture(texture)
8288 ClickToMoveDisplay.SetWaypointTexture(texture)
8289end
8290
8291function ClickToMove:GetWaypointTexture()
8292 return ClickToMoveDisplay.GetWaypointTexture()
8293end
8294
8295function ClickToMove:SetWaypointRadius(radius)
8296 ClickToMoveDisplay.SetWaypointRadius(radius)
8297end
8298
8299function ClickToMove:GetWaypointRadius()
8300 return ClickToMoveDisplay.GetWaypointRadius()
8301end
8302
8303function ClickToMove:SetEndWaypointTexture(texture)
8304 ClickToMoveDisplay.SetEndWaypointTexture(texture)
8305end
8306
8307function ClickToMove:GetEndWaypointTexture()
8308 return ClickToMoveDisplay.GetEndWaypointTexture()
8309end
8310
8311function ClickToMove:SetWaypointsAlwaysOnTop(alwaysOnTop)
8312 ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
8313end
8314
8315function ClickToMove:GetWaypointsAlwaysOnTop()
8316 return ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
8317end
8318
8319function ClickToMove:SetFailureAnimationEnabled(enabled)
8320 PlayFailureAnimation = enabled
8321end
8322
8323function ClickToMove:GetFailureAnimationEnabled()
8324 return PlayFailureAnimation
8325end
8326
8327function ClickToMove:SetIgnoredPartsTag(tag)
8328 UpdateIgnoreTag(tag)
8329end
8330
8331function ClickToMove:GetIgnoredPartsTag()
8332 return CurrentIgnoreTag
8333end
8334
8335function ClickToMove:SetUseDirectPath(directPath)
8336 UseDirectPath = directPath
8337end
8338
8339function ClickToMove:GetUseDirectPath()
8340 return UseDirectPath
8341end
8342
8343function ClickToMove:SetAgentSizeIncreaseFactor(increaseFactorPercent)
8344 AgentSizeIncreaseFactor = 1.0 + (increaseFactorPercent / 100.0)
8345end
8346
8347function ClickToMove:GetAgentSizeIncreaseFactor()
8348 return (AgentSizeIncreaseFactor - 1.0) * 100.0
8349end
8350
8351function ClickToMove:SetUnreachableWaypointTimeout(timeoutInSec)
8352 UnreachableWaypointTimeout = timeoutInSec
8353end
8354
8355function ClickToMove:GetUnreachableWaypointTimeout()
8356 return UnreachableWaypointTimeout
8357end
8358
8359function ClickToMove:SetUserJumpEnabled(jumpEnabled)
8360 self.jumpEnabled = jumpEnabled
8361 if self.touchJumpController then
8362 self.touchJumpController:Enable(jumpEnabled)
8363 end
8364end
8365
8366function ClickToMove:GetUserJumpEnabled()
8367 return self.jumpEnabled
8368end
8369
8370function ClickToMove:MoveTo(position, showPath, useDirectPath)
8371 local character = Player.Character
8372 if character == nil then
8373 return false
8374 end
8375 local thisPather = Pather(position, Vector3.new(0, 1, 0), useDirectPath)
8376 if thisPather and thisPather:IsValidPath() then
8377 if FFlagUserClickToMoveFollowPathRefactor then
8378 -- Clean up previous path
8379 CleanupPath()
8380 end
8381 HandleMoveTo(thisPather, position, nil, character, showPath)
8382 return true
8383 end
8384 return false
8385end
8386
8387return ClickToMove
8388
8389local ClickToMoveDisplay = {}
8390
8391local FAILURE_ANIMATION_ID = "rbxassetid://2874840706"
8392
8393local TrailDotIcon = "rbxasset://textures/ui/traildot.png"
8394local EndWaypointIcon = "rbxasset://textures/ui/waypoint.png"
8395
8396local WaypointsAlwaysOnTop = false
8397
8398local WAYPOINT_INCLUDE_FACTOR = 2
8399local LAST_DOT_DISTANCE = 3
8400
8401local WAYPOINT_BILLBOARD_SIZE = UDim2.new(0, 1.68 * 25, 0, 2 * 25)
8402
8403local ENDWAYPOINT_SIZE_OFFSET_MIN = Vector2.new(0, 0.5)
8404local ENDWAYPOINT_SIZE_OFFSET_MAX = Vector2.new(0, 1)
8405
8406local FAIL_WAYPOINT_SIZE_OFFSET_CENTER = Vector2.new(0, 0.5)
8407local FAIL_WAYPOINT_SIZE_OFFSET_LEFT = Vector2.new(0.1, 0.5)
8408local FAIL_WAYPOINT_SIZE_OFFSET_RIGHT = Vector2.new(-0.1, 0.5)
8409
8410local FAILURE_TWEEN_LENGTH = 0.125
8411local FAILURE_TWEEN_COUNT = 4
8412
8413local TWEEN_WAYPOINT_THRESHOLD = 5
8414
8415local TRAIL_DOT_PARENT_NAME = "ClickToMoveDisplay"
8416
8417local TrailDotSize = Vector2.new(1.5, 1.5)
8418
8419local TRAIL_DOT_MIN_SCALE = 1
8420local TRAIL_DOT_MIN_DISTANCE = 10
8421local TRAIL_DOT_MAX_SCALE = 2.5
8422local TRAIL_DOT_MAX_DISTANCE = 100
8423
8424local PlayersService = game:GetService("Players")
8425local TweenService = game:GetService("TweenService")
8426local RunService = game:GetService("RunService")
8427local Workspace = game:GetService("Workspace")
8428
8429local LocalPlayer = PlayersService.LocalPlayer
8430
8431local function CreateWaypointTemplates()
8432 local TrailDotTemplate = Instance.new("Part")
8433 TrailDotTemplate.Size = Vector3.new(1, 1, 1)
8434 TrailDotTemplate.Anchored = true
8435 TrailDotTemplate.CanCollide = false
8436 TrailDotTemplate.Name = "TrailDot"
8437 TrailDotTemplate.Transparency = 1
8438 local TrailDotImage = Instance.new("ImageHandleAdornment")
8439 TrailDotImage.Name = "TrailDotImage"
8440 TrailDotImage.Size = TrailDotSize
8441 TrailDotImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
8442 TrailDotImage.AlwaysOnTop = WaypointsAlwaysOnTop
8443 TrailDotImage.Image = TrailDotIcon
8444 TrailDotImage.Adornee = TrailDotTemplate
8445 TrailDotImage.Parent = TrailDotTemplate
8446
8447 local EndWaypointTemplate = Instance.new("Part")
8448 EndWaypointTemplate.Size = Vector3.new(2, 2, 2)
8449 EndWaypointTemplate.Anchored = true
8450 EndWaypointTemplate.CanCollide = false
8451 EndWaypointTemplate.Name = "EndWaypoint"
8452 EndWaypointTemplate.Transparency = 1
8453 local EndWaypointImage = Instance.new("ImageHandleAdornment")
8454 EndWaypointImage.Name = "TrailDotImage"
8455 EndWaypointImage.Size = TrailDotSize
8456 EndWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
8457 EndWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
8458 EndWaypointImage.Image = TrailDotIcon
8459 EndWaypointImage.Adornee = EndWaypointTemplate
8460 EndWaypointImage.Parent = EndWaypointTemplate
8461 local EndWaypointBillboard = Instance.new("BillboardGui")
8462 EndWaypointBillboard.Name = "EndWaypointBillboard"
8463 EndWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
8464 EndWaypointBillboard.LightInfluence = 0
8465 EndWaypointBillboard.SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MIN
8466 EndWaypointBillboard.AlwaysOnTop = true
8467 EndWaypointBillboard.Adornee = EndWaypointTemplate
8468 EndWaypointBillboard.Parent = EndWaypointTemplate
8469 local EndWaypointImageLabel = Instance.new("ImageLabel")
8470 EndWaypointImageLabel.Image = EndWaypointIcon
8471 EndWaypointImageLabel.BackgroundTransparency = 1
8472 EndWaypointImageLabel.Size = UDim2.new(1, 0, 1, 0)
8473 EndWaypointImageLabel.Parent = EndWaypointBillboard
8474
8475
8476 local FailureWaypointTemplate = Instance.new("Part")
8477 FailureWaypointTemplate.Size = Vector3.new(2, 2, 2)
8478 FailureWaypointTemplate.Anchored = true
8479 FailureWaypointTemplate.CanCollide = false
8480 FailureWaypointTemplate.Name = "FailureWaypoint"
8481 FailureWaypointTemplate.Transparency = 1
8482 local FailureWaypointImage = Instance.new("ImageHandleAdornment")
8483 FailureWaypointImage.Name = "TrailDotImage"
8484 FailureWaypointImage.Size = TrailDotSize
8485 FailureWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
8486 FailureWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
8487 FailureWaypointImage.Image = TrailDotIcon
8488 FailureWaypointImage.Adornee = FailureWaypointTemplate
8489 FailureWaypointImage.Parent = FailureWaypointTemplate
8490 local FailureWaypointBillboard = Instance.new("BillboardGui")
8491 FailureWaypointBillboard.Name = "FailureWaypointBillboard"
8492 FailureWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
8493 FailureWaypointBillboard.LightInfluence = 0
8494 FailureWaypointBillboard.SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER
8495 FailureWaypointBillboard.AlwaysOnTop = true
8496 FailureWaypointBillboard.Adornee = FailureWaypointTemplate
8497 FailureWaypointBillboard.Parent = FailureWaypointTemplate
8498 local FailureWaypointFrame = Instance.new("Frame")
8499 FailureWaypointFrame.BackgroundTransparency = 1
8500 FailureWaypointFrame.Size = UDim2.new(0, 0, 0, 0)
8501 FailureWaypointFrame.Position = UDim2.new(0.5, 0, 1, 0)
8502 FailureWaypointFrame.Parent = FailureWaypointBillboard
8503 local FailureWaypointImageLabel = Instance.new("ImageLabel")
8504 FailureWaypointImageLabel.Image = EndWaypointIcon
8505 FailureWaypointImageLabel.BackgroundTransparency = 1
8506 FailureWaypointImageLabel.Position = UDim2.new(
8507 0, -WAYPOINT_BILLBOARD_SIZE.X.Offset/2, 0, -WAYPOINT_BILLBOARD_SIZE.Y.Offset
8508 )
8509 FailureWaypointImageLabel.Size = WAYPOINT_BILLBOARD_SIZE
8510 FailureWaypointImageLabel.Parent = FailureWaypointFrame
8511
8512 return TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate
8513end
8514
8515local TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
8516
8517local function getTrailDotParent()
8518 local camera = Workspace.CurrentCamera
8519 local trailParent = camera:FindFirstChild(TRAIL_DOT_PARENT_NAME)
8520 if not trailParent then
8521 trailParent = Instance.new("Model")
8522 trailParent.Name = TRAIL_DOT_PARENT_NAME
8523 trailParent.Parent = camera
8524 end
8525 return trailParent
8526end
8527
8528local function placePathWaypoint(waypointModel, position)
8529 local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
8530 local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
8531 ray,
8532 { Workspace.CurrentCamera, LocalPlayer.Character }
8533 )
8534 if hitPart then
8535 waypointModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
8536 waypointModel.Parent = getTrailDotParent()
8537 end
8538end
8539
8540local TrailDot = {}
8541TrailDot.__index = TrailDot
8542
8543function TrailDot:Destroy()
8544 self.DisplayModel:Destroy()
8545end
8546
8547function TrailDot:NewDisplayModel(position)
8548 local newDisplayModel = TrailDotTemplate:Clone()
8549 placePathWaypoint(newDisplayModel, position)
8550 return newDisplayModel
8551end
8552
8553function TrailDot.new(position, closestWaypoint)
8554 local self = setmetatable({}, TrailDot)
8555
8556 self.DisplayModel = self:NewDisplayModel(position)
8557 self.ClosestWayPoint = closestWaypoint
8558
8559 return self
8560end
8561
8562local EndWaypoint = {}
8563EndWaypoint.__index = EndWaypoint
8564
8565function EndWaypoint:Destroy()
8566 self.Destroyed = true
8567 self.Tween:Cancel()
8568 self.DisplayModel:Destroy()
8569end
8570
8571function EndWaypoint:NewDisplayModel(position)
8572 local newDisplayModel = EndWaypointTemplate:Clone()
8573 placePathWaypoint(newDisplayModel, position)
8574 return newDisplayModel
8575end
8576
8577function EndWaypoint:CreateTween()
8578 local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, -1, true)
8579 local tween = TweenService:Create(
8580 self.DisplayModel.EndWaypointBillboard,
8581 tweenInfo,
8582 { SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MAX }
8583 )
8584 tween:Play()
8585 return tween
8586end
8587
8588function EndWaypoint:TweenInFrom(originalPosition)
8589 local currentPositon = self.DisplayModel.Position
8590 local studsOffset = originalPosition - currentPositon
8591 self.DisplayModel.EndWaypointBillboard.StudsOffset = Vector3.new(0, studsOffset.Y, 0)
8592 local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
8593 local tween = TweenService:Create(
8594 self.DisplayModel.EndWaypointBillboard,
8595 tweenInfo,
8596 { StudsOffset = Vector3.new(0, 0, 0) }
8597 )
8598 tween:Play()
8599 return tween
8600end
8601
8602function EndWaypoint.new(position, closestWaypoint, originalPosition)
8603 local self = setmetatable({}, EndWaypoint)
8604
8605 self.DisplayModel = self:NewDisplayModel(position)
8606 self.Destroyed = false
8607 if originalPosition and (originalPosition - position).magnitude > TWEEN_WAYPOINT_THRESHOLD then
8608 self.Tween = self:TweenInFrom(originalPosition)
8609 coroutine.wrap(function()
8610 self.Tween.Completed:Wait()
8611 if not self.Destroyed then
8612 self.Tween = self:CreateTween()
8613 end
8614 end)()
8615 else
8616 self.Tween = self:CreateTween()
8617 end
8618 self.ClosestWayPoint = closestWaypoint
8619
8620 return self
8621end
8622
8623local FailureWaypoint = {}
8624FailureWaypoint.__index = FailureWaypoint
8625
8626function FailureWaypoint:Hide()
8627 self.DisplayModel.Parent = nil
8628end
8629
8630function FailureWaypoint:Destroy()
8631 self.DisplayModel:Destroy()
8632end
8633
8634function FailureWaypoint:NewDisplayModel(position)
8635 local newDisplayModel = FailureWaypointTemplate:Clone()
8636 placePathWaypoint(newDisplayModel, position)
8637 local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
8638 local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
8639 ray, { Workspace.CurrentCamera, LocalPlayer.Character }
8640 )
8641 if hitPart then
8642 newDisplayModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
8643 newDisplayModel.Parent = getTrailDotParent()
8644 end
8645 return newDisplayModel
8646end
8647
8648function FailureWaypoint:RunFailureTween()
8649 wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore starting tweening
8650 -- Tween out from center
8651 local tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
8652 local tweenLeft = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
8653 { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_LEFT })
8654 tweenLeft:Play()
8655
8656 local tweenLeftRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
8657 { Rotation = 10 })
8658 tweenLeftRoation:Play()
8659
8660 tweenLeft.Completed:wait()
8661
8662 -- Tween back and forth
8663 tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
8664 FAILURE_TWEEN_COUNT - 1, true)
8665 local tweenSideToSide = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
8666 { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_RIGHT})
8667 tweenSideToSide:Play()
8668
8669 -- Tween flash dark and roate left and right
8670 tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
8671 FAILURE_TWEEN_COUNT - 1, true)
8672 local tweenFlash = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame.ImageLabel, tweenInfo,
8673 { ImageColor3 = Color3.new(0.75, 0.75, 0.75)})
8674 tweenFlash:Play()
8675
8676 local tweenRotate = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
8677 { Rotation = -10 })
8678 tweenRotate:Play()
8679
8680 tweenSideToSide.Completed:wait()
8681
8682 -- Tween back to center
8683 tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
8684 local tweenCenter = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
8685 { SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER })
8686 tweenCenter:Play()
8687
8688 local tweenRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
8689 { Rotation = 0 })
8690 tweenRoation:Play()
8691
8692 tweenCenter.Completed:wait()
8693
8694 wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore removing
8695end
8696
8697function FailureWaypoint.new(position)
8698 local self = setmetatable({}, FailureWaypoint)
8699
8700 self.DisplayModel = self:NewDisplayModel(position)
8701
8702 return self
8703end
8704
8705local failureAnimation = Instance.new("Animation")
8706failureAnimation.AnimationId = FAILURE_ANIMATION_ID
8707
8708local lastHumanoid = nil
8709local lastFailureAnimationTrack = nil
8710
8711local function getFailureAnimationTrack(myHumanoid)
8712 if myHumanoid == lastHumanoid then
8713 return lastFailureAnimationTrack
8714 end
8715 lastFailureAnimationTrack = myHumanoid:LoadAnimation(failureAnimation)
8716 lastFailureAnimationTrack.Priority = Enum.AnimationPriority.Action
8717 lastFailureAnimationTrack.Looped = false
8718 return lastFailureAnimationTrack
8719end
8720
8721local function findPlayerHumanoid()
8722 local character = LocalPlayer.Character
8723 if character then
8724 return character:FindFirstChildOfClass("Humanoid")
8725 end
8726end
8727
8728local function createTrailDots(wayPoints, originalEndWaypoint)
8729 local newTrailDots = {}
8730 local count = 1
8731 for i = 1, #wayPoints - 1 do
8732 local closeToEnd = (wayPoints[i].Position - wayPoints[#wayPoints].Position).magnitude < LAST_DOT_DISTANCE
8733 local includeWaypoint = i % WAYPOINT_INCLUDE_FACTOR == 0 and not closeToEnd
8734 if includeWaypoint then
8735 local trailDot = TrailDot.new(wayPoints[i].Position, i)
8736 newTrailDots[count] = trailDot
8737 count = count + 1
8738 end
8739 end
8740
8741 local newEndWaypoint = EndWaypoint.new(wayPoints[#wayPoints].Position, #wayPoints, originalEndWaypoint)
8742 table.insert(newTrailDots, newEndWaypoint)
8743
8744 local reversedTrailDots = {}
8745 count = 1
8746 for i = #newTrailDots, 1, -1 do
8747 reversedTrailDots[count] = newTrailDots[i]
8748 count = count + 1
8749 end
8750 return reversedTrailDots
8751end
8752
8753local function getTrailDotScale(distanceToCamera, defaultSize)
8754 local rangeLength = TRAIL_DOT_MAX_DISTANCE - TRAIL_DOT_MIN_DISTANCE
8755 local inRangePoint = math.clamp(distanceToCamera - TRAIL_DOT_MIN_DISTANCE, 0, rangeLength)/rangeLength
8756 local scale = TRAIL_DOT_MIN_SCALE + (TRAIL_DOT_MAX_SCALE - TRAIL_DOT_MIN_SCALE)*inRangePoint
8757 return defaultSize * scale
8758end
8759
8760local createPathCount = 0
8761-- originalEndWaypoint is optional, causes the waypoint to tween from that position.
8762function ClickToMoveDisplay.CreatePathDisplay(wayPoints, originalEndWaypoint)
8763 createPathCount = createPathCount + 1
8764 local trailDots = createTrailDots(wayPoints, originalEndWaypoint)
8765
8766 local function removePathBeforePoint(wayPointNumber)
8767 -- kill all trailDots before and at wayPointNumber
8768 for i = #trailDots, 1, -1 do
8769 local trailDot = trailDots[i]
8770 if trailDot.ClosestWayPoint <= wayPointNumber then
8771 trailDot:Destroy()
8772 trailDots[i] = nil
8773 else
8774 break
8775 end
8776 end
8777 end
8778
8779 local reiszeTrailDotsUpdateName = "ClickToMoveResizeTrail" ..createPathCount
8780 local function resizeTrailDots()
8781 if #trailDots == 0 then
8782 RunService:UnbindFromRenderStep(reiszeTrailDotsUpdateName)
8783 return
8784 end
8785 local cameraPos = Workspace.CurrentCamera.CFrame.p
8786 for i = 1, #trailDots do
8787 local trailDotImage = trailDots[i].DisplayModel:FindFirstChild("TrailDotImage")
8788 if trailDotImage then
8789 local distanceToCamera = (trailDots[i].DisplayModel.Position - cameraPos).magnitude
8790 trailDotImage.Size = getTrailDotScale(distanceToCamera, TrailDotSize)
8791 end
8792 end
8793 end
8794 RunService:BindToRenderStep(reiszeTrailDotsUpdateName, Enum.RenderPriority.Camera.Value - 1, resizeTrailDots)
8795
8796 local function removePath()
8797 removePathBeforePoint(#wayPoints)
8798 end
8799
8800 return removePath, removePathBeforePoint
8801end
8802
8803local lastFailureWaypoint = nil
8804function ClickToMoveDisplay.DisplayFailureWaypoint(position)
8805 if lastFailureWaypoint then
8806 lastFailureWaypoint:Hide()
8807 end
8808 local failureWaypoint = FailureWaypoint.new(position)
8809 lastFailureWaypoint = failureWaypoint
8810 coroutine.wrap(function()
8811 failureWaypoint:RunFailureTween()
8812 failureWaypoint:Destroy()
8813 failureWaypoint = nil
8814 end)()
8815end
8816
8817function ClickToMoveDisplay.CreateEndWaypoint(position)
8818 return EndWaypoint.new(position)
8819end
8820
8821function ClickToMoveDisplay.PlayFailureAnimation()
8822 local myHumanoid = findPlayerHumanoid()
8823 if myHumanoid then
8824 local animationTrack = getFailureAnimationTrack(myHumanoid)
8825 animationTrack:Play()
8826 end
8827end
8828
8829function ClickToMoveDisplay.CancelFailureAnimation()
8830 if lastFailureAnimationTrack ~= nil and lastFailureAnimationTrack.IsPlaying then
8831 lastFailureAnimationTrack:Stop()
8832 end
8833end
8834
8835function ClickToMoveDisplay.SetWaypointTexture(texture)
8836 TrailDotIcon = texture
8837 TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
8838end
8839
8840function ClickToMoveDisplay.GetWaypointTexture()
8841 return TrailDotIcon
8842end
8843
8844function ClickToMoveDisplay.SetWaypointRadius(radius)
8845 TrailDotSize = Vector2.new(radius, radius)
8846 TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
8847end
8848
8849function ClickToMoveDisplay.GetWaypointRadius()
8850 return TrailDotSize.X
8851end
8852
8853function ClickToMoveDisplay.SetEndWaypointTexture(texture)
8854 EndWaypointIcon = texture
8855 TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
8856end
8857
8858function ClickToMoveDisplay.GetEndWaypointTexture()
8859 return EndWaypointIcon
8860end
8861
8862function ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
8863 WaypointsAlwaysOnTop = alwaysOnTop
8864 TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
8865end
8866
8867function ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
8868 return WaypointsAlwaysOnTop
8869end
8870
8871return ClickToMoveDisplay
8872
8873--[[ Constants ]]--
8874local ZERO_VECTOR3 = Vector3.new(0,0,0)
8875local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
8876
8877local DYNAMIC_THUMBSTICK_ACTION_NAME = "DynamicThumbstickAction"
8878local DYNAMIC_THUMBSTICK_ACTION_PRIORITY = Enum.ContextActionPriority.High.Value
8879
8880local MIDDLE_TRANSPARENCIES = {
8881 1 - 0.89,
8882 1 - 0.70,
8883 1 - 0.60,
8884 1 - 0.50,
8885 1 - 0.40,
8886 1 - 0.30,
8887 1 - 0.25
8888}
8889local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES
8890
8891local FADE_IN_OUT_BACKGROUND = true
8892local FADE_IN_OUT_MAX_ALPHA = 0.35
8893
8894local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3
8895local FADE_IN_OUT_BALANCE_DEFAULT = 0.5
8896local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
8897
8898local Players = game:GetService("Players")
8899local GuiService = game:GetService("GuiService")
8900local UserInputService = game:GetService("UserInputService")
8901local ContextActionService = game:GetService("ContextActionService")
8902local RunService = game:GetService("RunService")
8903local TweenService = game:GetService("TweenService")
8904
8905local thumstickIsFrameSuccess, thumstickIsFrameValue = pcall(function()
8906 return UserSettings():IsUserFeatureEnabled("UserThumbstickFrameIsActuallyFrame")
8907end)
8908local FFlagUserThumbstickFrameIsActuallyFrame = thumstickIsFrameSuccess and thumstickIsFrameValue
8909
8910--[[ The Module ]]--
8911local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
8912local DynamicThumbstick = setmetatable({}, BaseCharacterController)
8913DynamicThumbstick.__index = DynamicThumbstick
8914
8915function DynamicThumbstick.new()
8916 local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick)
8917
8918 self.humanoid = nil
8919
8920 self.tools = {}
8921 self.toolEquipped = nil
8922
8923 self.revertAutoJumpEnabledToFalse = false
8924
8925 self.moveTouchObject = nil
8926 self.moveTouchLockedIn = false
8927 self.moveTouchFirstChanged = false
8928 self.moveTouchStartPosition = nil
8929
8930 self.startImage = nil
8931 self.endImage = nil
8932 self.middleImages = {}
8933
8934 self.startImageFadeTween = nil
8935 self.endImageFadeTween = nil
8936 self.middleImageFadeTweens = {}
8937
8938 self.isFirstTouch = true
8939
8940 self.isFollowStick = false
8941 self.thumbstickFrame = nil
8942
8943 self.onRenderSteppedConn = nil
8944
8945 self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
8946 self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
8947 self.hasFadedBackgroundInPortrait = false
8948 self.hasFadedBackgroundInLandscape = false
8949
8950 self.tweenInAlphaStart = nil
8951 self.tweenOutAlphaStart = nil
8952
8953 -- If this module changes a player's humanoid's AutoJumpEnabled, it saves
8954 -- the previous state in this variable to revert to
8955 self.shouldRevertAutoJumpOnDisable = false
8956
8957 return self
8958end
8959
8960-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump
8961-- rather than sustained jumping. This is only to preserve the current behavior through the refactor.
8962function DynamicThumbstick:GetIsJumping()
8963 local wasJumping = self.isJumping
8964 self.isJumping = false
8965 return wasJumping
8966end
8967
8968function DynamicThumbstick:EnableAutoJump(enable)
8969 local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
8970 if humanoid then
8971 if enable then
8972 self.shouldRevertAutoJumpOnDisable = (humanoid.AutoJumpEnabled == false) and (Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice)
8973 humanoid.AutoJumpEnabled = true
8974 elseif self.shouldRevertAutoJumpOnDisable then
8975 humanoid.AutoJumpEnabled = false
8976 end
8977 end
8978end
8979
8980function DynamicThumbstick:Enable(enable, uiParentFrame)
8981 if enable == nil then return false end -- If nil, return false (invalid argument)
8982 enable = enable and true or false -- Force anything non-nil to boolean before comparison
8983 if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
8984
8985 if enable then
8986 -- Enable
8987 if not self.thumbstickFrame then
8988 self:Create(uiParentFrame)
8989 end
8990
8991 self:BindContextActions()
8992
8993 if Players.LocalPlayer.Character then
8994 self:OnCharacterAdded(Players.LocalPlayer.Character)
8995 else
8996 Players.LocalPlayer.CharacterAdded:Connect(function(char)
8997 self:OnCharacterAdded(char)
8998 end)
8999 end
9000 else
9001 ContextActionService:UnbindAction(DYNAMIC_THUMBSTICK_ACTION_NAME)
9002 -- Disable
9003 self:OnInputEnded() -- Cleanup
9004 end
9005
9006 self.enabled = enable
9007 self.thumbstickFrame.Visible = enable
9008end
9009
9010function DynamicThumbstick:OnCharacterAdded(char)
9011
9012 for _, child in ipairs(char:GetChildren()) do
9013 if child:IsA("Tool") then
9014 self.toolEquipped = child
9015 end
9016 end
9017
9018 char.ChildAdded:Connect(function(child)
9019 if child:IsA("Tool") then
9020 self.toolEquipped = child
9021 elseif child:IsA("Humanoid") then
9022 self:EnableAutoJump(true)
9023 end
9024
9025 end)
9026 char.ChildRemoved:Connect(function(child)
9027 if child == self.toolEquipped then
9028 self.toolEquipped = nil
9029 end
9030 end)
9031
9032 self.humanoid = char:FindFirstChildOfClass("Humanoid")
9033 if self.humanoid then
9034 self:EnableAutoJump(true)
9035 end
9036end
9037
9038-- Was called OnMoveTouchEnded in previous version
9039function DynamicThumbstick:OnInputEnded()
9040 self.moveTouchObject = nil
9041 self.moveVector = ZERO_VECTOR3
9042 self:FadeThumbstick(false)
9043end
9044
9045function DynamicThumbstick:FadeThumbstick(visible)
9046 if not visible and self.moveTouchObject then
9047 return
9048 end
9049 if self.isFirstTouch then return end
9050
9051 if self.startImageFadeTween then
9052 self.startImageFadeTween:Cancel()
9053 end
9054 if self.endImageFadeTween then
9055 self.endImageFadeTween:Cancel()
9056 end
9057 for i = 1, #self.middleImages do
9058 if self. middleImageFadeTweens[i] then
9059 self.middleImageFadeTweens[i]:Cancel()
9060 end
9061 end
9062
9063 if visible then
9064 self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 })
9065 self.startImageFadeTween:Play()
9066
9067 self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 })
9068 self.endImageFadeTween:Play()
9069
9070 for i = 1, #self.middleImages do
9071 self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] })
9072 self.middleImageFadeTweens[i]:Play()
9073 end
9074 else
9075 self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
9076 self.startImageFadeTween:Play()
9077
9078 self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
9079 self.endImageFadeTween:Play()
9080
9081 for i = 1, #self.middleImages do
9082 self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
9083 self.middleImageFadeTweens[i]:Play()
9084 end
9085 end
9086end
9087
9088function DynamicThumbstick:FadeThumbstickFrame(fadeDuration, fadeRatio)
9089 self.fadeInAndOutHalfDuration = fadeDuration * 0.5
9090 self.fadeInAndOutBalance = fadeRatio
9091 self.tweenInAlphaStart = tick()
9092end
9093
9094function DynamicThumbstick:InputInFrame(inputObject)
9095 local frameCornerTopLeft = self.thumbstickFrame.AbsolutePosition
9096 local frameCornerBottomRight = frameCornerTopLeft + self.thumbstickFrame.AbsoluteSize
9097 local inputPosition = inputObject.Position
9098 if inputPosition.X >= frameCornerTopLeft.X and inputPosition.Y >= frameCornerTopLeft.Y then
9099 if inputPosition.X <= frameCornerBottomRight.X and inputPosition.Y <= frameCornerBottomRight.Y then
9100 return true
9101 end
9102 end
9103 return false
9104end
9105
9106function DynamicThumbstick:DoFadeInBackground()
9107 local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
9108 local hasFadedBackgroundInOrientation = false
9109
9110 -- only fade in/out the background once per orientation
9111 if playerGui then
9112 if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
9113 playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then
9114 hasFadedBackgroundInOrientation = self.hasFadedBackgroundInLandscape
9115 self.hasFadedBackgroundInLandscape = true
9116 elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then
9117 hasFadedBackgroundInOrientation = self.hasFadedBackgroundInPortrait
9118 self.hasFadedBackgroundInPortrait = true
9119 end
9120 end
9121
9122 if not hasFadedBackgroundInOrientation then
9123 self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
9124 self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
9125 self.tweenInAlphaStart = tick()
9126 end
9127end
9128
9129function DynamicThumbstick:DoMove(direction)
9130 local currentMoveVector = direction
9131
9132 -- Scaled Radial Dead Zone
9133 local inputAxisMagnitude = currentMoveVector.magnitude
9134 if inputAxisMagnitude < self.radiusOfDeadZone then
9135 currentMoveVector = Vector3.new()
9136 else
9137 currentMoveVector = currentMoveVector.unit*(
9138 1 - math.max(0, (self.radiusOfMaxSpeed - currentMoveVector.magnitude)/self.radiusOfMaxSpeed)
9139 )
9140 currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
9141 end
9142
9143 self.moveVector = currentMoveVector
9144end
9145
9146
9147function DynamicThumbstick:LayoutMiddleImages(startPos, endPos)
9148 local startDist = (self.thumbstickSize / 2) + self.middleSize
9149 local vector = endPos - startPos
9150 local distAvailable = vector.magnitude - (self.thumbstickRingSize / 2) - self.middleSize
9151 local direction = vector.unit
9152
9153 local distNeeded = self.middleSpacing * NUM_MIDDLE_IMAGES
9154 local spacing = self.middleSpacing
9155
9156 if distNeeded < distAvailable then
9157 spacing = distAvailable / NUM_MIDDLE_IMAGES
9158 end
9159
9160 for i = 1, NUM_MIDDLE_IMAGES do
9161 local image = self.middleImages[i]
9162 local distWithout = startDist + (spacing * (i - 2))
9163 local currentDist = startDist + (spacing * (i - 1))
9164
9165 if distWithout < distAvailable then
9166 local pos = endPos - direction * currentDist
9167 local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1)
9168
9169 image.Visible = true
9170 image.Position = UDim2.new(0, pos.X, 0, pos.Y)
9171 image.Size = UDim2.new(0, self.middleSize * exposedFraction, 0, self.middleSize * exposedFraction)
9172 else
9173 image.Visible = false
9174 end
9175 end
9176end
9177
9178function DynamicThumbstick:MoveStick(pos)
9179 local vector2StartPosition = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y)
9180 local startPos = vector2StartPosition - self.thumbstickFrame.AbsolutePosition
9181 local endPos = Vector2.new(pos.X, pos.Y) - self.thumbstickFrame.AbsolutePosition
9182 self.endImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y)
9183 self:LayoutMiddleImages(startPos, endPos)
9184end
9185
9186function DynamicThumbstick:BindContextActions()
9187 local function inputBegan(inputObject)
9188 if self.moveTouchObject then
9189 return Enum.ContextActionResult.Pass
9190 end
9191
9192 if not self:InputInFrame(inputObject) then
9193 return Enum.ContextActionResult.Pass
9194 end
9195
9196 if self.isFirstTouch then
9197 self.isFirstTouch = false
9198 local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0)
9199 TweenService:Create(self.startImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play()
9200 TweenService:Create(
9201 self.endImage,
9202 tweenInfo,
9203 {Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize), ImageColor3 = Color3.new(0,0,0)}
9204 ):Play()
9205 end
9206
9207 if FFlagUserThumbstickFrameIsActuallyFrame then
9208 self.moveTouchLockedIn = false
9209 end
9210 self.moveTouchObject = inputObject
9211 self.moveTouchStartPosition = inputObject.Position
9212 self.moveTouchFirstChanged = true
9213
9214 if FADE_IN_OUT_BACKGROUND then
9215 self:DoFadeInBackground()
9216 end
9217
9218 return Enum.ContextActionResult.Pass
9219 end
9220
9221 local function inputChanged(inputObject)
9222 if inputObject == self.moveTouchObject then
9223 if self.moveTouchFirstChanged then
9224 self.moveTouchFirstChanged = false
9225
9226 local startPosVec2 = Vector2.new(
9227 inputObject.Position.X - self.thumbstickFrame.AbsolutePosition.X,
9228 inputObject.Position.Y - self.thumbstickFrame.AbsolutePosition.Y
9229 )
9230 self.startImage.Visible = true
9231 self.startImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y)
9232 self.endImage.Visible = true
9233 self.endImage.Position = self.startImage.Position
9234
9235 self:FadeThumbstick(true)
9236 self:MoveStick(inputObject.Position)
9237 end
9238
9239 if FFlagUserThumbstickFrameIsActuallyFrame then
9240 self.moveTouchLockedIn = true
9241 end
9242
9243 local direction = Vector2.new(
9244 inputObject.Position.x - self.moveTouchStartPosition.x,
9245 inputObject.Position.y - self.moveTouchStartPosition.y
9246 )
9247 if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then
9248 self:DoMove(direction)
9249 self:MoveStick(inputObject.Position)
9250 end
9251 return Enum.ContextActionResult.Sink
9252 end
9253 return Enum.ContextActionResult.Pass
9254 end
9255
9256 local function inputEnded(inputObject)
9257 if inputObject == self.moveTouchObject then
9258 self:OnInputEnded()
9259 if self.moveTouchLockedIn then
9260 return Enum.ContextActionResult.Sink
9261 end
9262 end
9263 return Enum.ContextActionResult.Pass
9264 end
9265
9266 local function handleInput(actionName, inputState, inputObject)
9267 if inputState == Enum.UserInputState.Begin then
9268 return inputBegan(inputObject)
9269 elseif inputState == Enum.UserInputState.Change then
9270 return inputChanged(inputObject)
9271 elseif inputState == Enum.UserInputState.End then
9272 return inputEnded(inputObject)
9273 elseif inputState == Enum.UserInputState.Cancel then
9274 self:OnInputEnded()
9275 end
9276 end
9277
9278 ContextActionService:BindActionAtPriority(
9279 DYNAMIC_THUMBSTICK_ACTION_NAME,
9280 handleInput,
9281 false,
9282 DYNAMIC_THUMBSTICK_ACTION_PRIORITY,
9283 Enum.UserInputType.Touch)
9284end
9285
9286function DynamicThumbstick:Create(parentFrame)
9287 if self.thumbstickFrame then
9288 self.thumbstickFrame:Destroy()
9289 self.thumbstickFrame = nil
9290 if self.onRenderSteppedConn then
9291 self.onRenderSteppedConn:Disconnect()
9292 self.onRenderSteppedConn = nil
9293 end
9294 end
9295
9296 self.thumbstickSize = 45
9297 self.thumbstickRingSize = 20
9298 self.middleSize = 10
9299 self.middleSpacing = self.middleSize + 4
9300 self.radiusOfDeadZone = 2
9301 self.radiusOfMaxSpeed = 20
9302
9303 local screenSize = parentFrame.AbsoluteSize
9304 local isBigScreen = math.min(screenSize.x, screenSize.y) > 500
9305 if isBigScreen then
9306 self.thumbstickSize = self.thumbstickSize * 2
9307 self.thumbstickRingSize = self.thumbstickRingSize * 2
9308 self.middleSize = self.middleSize * 2
9309 self.middleSpacing = self.middleSpacing * 2
9310 self.radiusOfDeadZone = self.radiusOfDeadZone * 2
9311 self.radiusOfMaxSpeed = self.radiusOfMaxSpeed * 2
9312 end
9313
9314 local function layoutThumbstickFrame(portraitMode)
9315 if portraitMode then
9316 self.thumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0)
9317 self.thumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0)
9318 else
9319 self.thumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0)
9320 self.thumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0)
9321 end
9322 end
9323
9324 if FFlagUserThumbstickFrameIsActuallyFrame then
9325 self.thumbstickFrame = Instance.new("Frame")
9326 self.thumbstickFrame.BorderSizePixel = 0
9327 else
9328 self.thumbstickFrame = Instance.new("TextButton")
9329 self.thumbstickFrame.Text = ""
9330 end
9331 self.thumbstickFrame.Name = "DynamicThumbstickFrame"
9332 self.thumbstickFrame.Visible = false
9333 self.thumbstickFrame.BackgroundTransparency = 1.0
9334 self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
9335 self.thumbstickFrame.Active = false
9336 layoutThumbstickFrame(false)
9337
9338 self.startImage = Instance.new("ImageLabel")
9339 self.startImage.Name = "ThumbstickStart"
9340 self.startImage.Visible = true
9341 self.startImage.BackgroundTransparency = 1
9342 self.startImage.Image = TOUCH_CONTROLS_SHEET
9343 self.startImage.ImageRectOffset = Vector2.new(1,1)
9344 self.startImage.ImageRectSize = Vector2.new(144, 144)
9345 self.startImage.ImageColor3 = Color3.new(0, 0, 0)
9346 self.startImage.AnchorPoint = Vector2.new(0.5, 0.5)
9347 self.startImage.Position = UDim2.new(0, self.thumbstickRingSize * 3.3, 1, -self.thumbstickRingSize * 2.8)
9348 self.startImage.Size = UDim2.new(0, self.thumbstickRingSize * 3.7, 0, self.thumbstickRingSize * 3.7)
9349 self.startImage.ZIndex = 10
9350 self.startImage.Parent = self.thumbstickFrame
9351
9352 self.endImage = Instance.new("ImageLabel")
9353 self.endImage.Name = "ThumbstickEnd"
9354 self.endImage.Visible = true
9355 self.endImage.BackgroundTransparency = 1
9356 self.endImage.Image = TOUCH_CONTROLS_SHEET
9357 self.endImage.ImageRectOffset = Vector2.new(1,1)
9358 self.endImage.ImageRectSize = Vector2.new(144, 144)
9359 self.endImage.AnchorPoint = Vector2.new(0.5, 0.5)
9360 self.endImage.Position = self.startImage.Position
9361 self.endImage.Size = UDim2.new(0, self.thumbstickSize * 0.8, 0, self.thumbstickSize * 0.8)
9362 self.endImage.ZIndex = 10
9363 self.endImage.Parent = self.thumbstickFrame
9364
9365 for i = 1, NUM_MIDDLE_IMAGES do
9366 self.middleImages[i] = Instance.new("ImageLabel")
9367 self.middleImages[i].Name = "ThumbstickMiddle"
9368 self.middleImages[i].Visible = false
9369 self.middleImages[i].BackgroundTransparency = 1
9370 self.middleImages[i].Image = TOUCH_CONTROLS_SHEET
9371 self.middleImages[i].ImageRectOffset = Vector2.new(1,1)
9372 self.middleImages[i].ImageRectSize = Vector2.new(144, 144)
9373 self.middleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i]
9374 self.middleImages[i].AnchorPoint = Vector2.new(0.5, 0.5)
9375 self.middleImages[i].ZIndex = 9
9376 self.middleImages[i].Parent = self.thumbstickFrame
9377 end
9378
9379 local CameraChangedConn = nil
9380 local function onCurrentCameraChanged()
9381 if CameraChangedConn then
9382 CameraChangedConn:Disconnect()
9383 CameraChangedConn = nil
9384 end
9385 local newCamera = workspace.CurrentCamera
9386 if newCamera then
9387 local function onViewportSizeChanged()
9388 local size = newCamera.ViewportSize
9389 local portraitMode = size.X < size.Y
9390 layoutThumbstickFrame(portraitMode)
9391 end
9392 CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged)
9393 onViewportSizeChanged()
9394 end
9395 end
9396 workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged)
9397 if workspace.CurrentCamera then
9398 onCurrentCameraChanged()
9399 end
9400
9401 self.moveTouchStartPosition = nil
9402
9403 self.startImageFadeTween = nil
9404 self.endImageFadeTween = nil
9405 self.middleImageFadeTweens = {}
9406
9407 self.onRenderSteppedConn = RunService.RenderStepped:Connect(function()
9408 if self.tweenInAlphaStart ~= nil then
9409 local delta = tick() - self.tweenInAlphaStart
9410 local fadeInTime = (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
9411 self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeInTime, 1)
9412 if delta > fadeInTime then
9413 self.tweenOutAlphaStart = tick()
9414 self.tweenInAlphaStart = nil
9415 end
9416 elseif self.tweenOutAlphaStart ~= nil then
9417 local delta = tick() - self.tweenOutAlphaStart
9418 local fadeOutTime = (self.fadeInAndOutHalfDuration * 2) - (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
9419 self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA + FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeOutTime, 1)
9420 if delta > fadeOutTime then
9421 self.tweenOutAlphaStart = nil
9422 end
9423 end
9424 end)
9425
9426 self.onTouchEndedConn = UserInputService.TouchEnded:connect(function(inputObject)
9427 if inputObject == self.moveTouchObject then
9428 self:OnInputEnded()
9429 end
9430 end)
9431
9432 GuiService.MenuOpened:connect(function()
9433 if self.moveTouchObject then
9434 self:OnInputEnded()
9435 end
9436 end)
9437
9438 local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
9439 while not playerGui do
9440 Players.LocalPlayer.ChildAdded:wait()
9441 playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
9442 end
9443
9444 local playerGuiChangedConn = nil
9445 local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
9446 playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight
9447
9448 local function longShowBackground()
9449 self.fadeInAndOutHalfDuration = 2.5
9450 self.fadeInAndOutBalance = 0.05
9451 self.tweenInAlphaStart = tick()
9452 end
9453
9454 playerGuiChangedConn = playerGui.Changed:connect(function(prop)
9455 if prop == "CurrentScreenOrientation" then
9456 if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or
9457 (not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then
9458
9459 playerGuiChangedConn:disconnect()
9460 longShowBackground()
9461
9462 if originalScreenOrientationWasLandscape then
9463 self.hasFadedBackgroundInPortrait = true
9464 else
9465 self.hasFadedBackgroundInLandscape = true
9466 end
9467 end
9468 end
9469 end)
9470
9471 self.thumbstickFrame.Parent = parentFrame
9472
9473 spawn(function()
9474 if game:IsLoaded() then
9475 longShowBackground()
9476 else
9477 game.Loaded:wait()
9478 longShowBackground()
9479 end
9480 end)
9481end
9482
9483return DynamicThumbstick
9484
9485--[[
9486 Gamepad Character Control - This module handles controlling your avatar using a game console-style controller
9487
9488 2018 PlayerScripts Update - AllYourBlox
9489--]]
9490
9491local UserInputService = game:GetService("UserInputService")
9492local ContextActionService = game:GetService("ContextActionService")
9493
9494--[[ Constants ]]--
9495local ZERO_VECTOR3 = Vector3.new(0,0,0)
9496local NONE = Enum.UserInputType.None
9497local thumbstickDeadzone = 0.2
9498
9499--[[ The Module ]]--
9500local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
9501local Gamepad = setmetatable({}, BaseCharacterController)
9502Gamepad.__index = Gamepad
9503
9504local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
9505 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority2")
9506end)
9507local FFlagPlayerScriptsBindAtPriority2 = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
9508
9509function Gamepad.new(CONTROL_ACTION_PRIORITY)
9510 local self = setmetatable(BaseCharacterController.new(), Gamepad)
9511
9512 self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
9513
9514 self.forwardValue = 0
9515 self.backwardValue = 0
9516 self.leftValue = 0
9517 self.rightValue = 0
9518
9519 self.activeGamepad = NONE -- Enum.UserInputType.Gamepad1, 2, 3...
9520 self.gamepadConnectedConn = nil
9521 self.gamepadDisconnectedConn = nil
9522 return self
9523end
9524
9525function Gamepad:Enable(enable)
9526 if not UserInputService.GamepadEnabled then
9527 return false
9528 end
9529
9530 if enable == self.enabled then
9531 -- Module is already in the state being requested. True is returned here since the module will be in the state
9532 -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
9533 -- no action was necessary. False indicates failure to be in requested/expected state.
9534 return true
9535 end
9536
9537 self.forwardValue = 0
9538 self.backwardValue = 0
9539 self.leftValue = 0
9540 self.rightValue = 0
9541 self.moveVector = ZERO_VECTOR3
9542 self.isJumping = false
9543
9544 if enable then
9545 self.activeGamepad = self:GetHighestPriorityGamepad()
9546 if self.activeGamepad ~= NONE then
9547 self:BindContextActions()
9548 self:ConnectGamepadConnectionListeners()
9549 else
9550 -- No connected gamepads, failure to enable
9551 return false
9552 end
9553 else
9554 self:UnbindContextActions()
9555 self:DisconnectGamepadConnectionListeners()
9556 self.activeGamepad = NONE
9557 end
9558
9559 self.enabled = enable
9560 return true
9561end
9562
9563-- This function selects the lowest number gamepad from the currently-connected gamepad
9564-- and sets it as the active gamepad
9565function Gamepad:GetHighestPriorityGamepad()
9566 local connectedGamepads = UserInputService:GetConnectedGamepads()
9567 local bestGamepad = NONE -- Note that this value is higher than all valid gamepad values
9568 for _, gamepad in pairs(connectedGamepads) do
9569 if gamepad.Value < bestGamepad.Value then
9570 bestGamepad = gamepad
9571 end
9572 end
9573 return bestGamepad
9574end
9575
9576function Gamepad:BindContextActions()
9577
9578 if self.activeGamepad == NONE then
9579 -- There must be an active gamepad to set up bindings
9580 return false
9581 end
9582
9583
9584 local updateMovement = function(inputState)
9585 if inputState == Enum.UserInputState.Cancel then
9586 self.moveVector = ZERO_VECTOR3
9587 else
9588 self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
9589 end
9590 end
9591
9592 -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
9593 -- which fixes them from getting stuck on.
9594 local handleMoveForward = function(actionName, inputState, inputObject)
9595 self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
9596 updateMovement(inputState)
9597 end
9598
9599 local handleMoveBackward = function(actionName, inputState, inputObject)
9600 self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
9601 updateMovement(inputState)
9602 end
9603
9604 local handleMoveLeft = function(actionName, inputState, inputObject)
9605 self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
9606 updateMovement(inputState)
9607 end
9608
9609 local handleMoveRight = function(actionName, inputState, inputObject)
9610 self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
9611 updateMovement(inputState)
9612 end
9613
9614 local handleJumpAction = function(actionName, inputState, inputObject)
9615 self.isJumping = (inputState == Enum.UserInputState.Begin)
9616 if FFlagPlayerScriptsBindAtPriority2 then
9617 return Enum.ContextActionResult.Sink
9618 end
9619 end
9620
9621 local handleThumbstickInput = function(actionName, inputState, inputObject)
9622
9623 if inputState == Enum.UserInputState.Cancel then
9624 self.moveVector = ZERO_VECTOR3
9625 return FFlagPlayerScriptsBindAtPriority2 and Enum.ContextActionResult.Sink or nil
9626 end
9627
9628 if self.activeGamepad ~= inputObject.UserInputType then
9629 return FFlagPlayerScriptsBindAtPriority2 and Enum.ContextActionResult.Pass or nil
9630 end
9631 if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
9632
9633 if inputObject.Position.magnitude > thumbstickDeadzone then
9634 self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
9635 else
9636 self.moveVector = ZERO_VECTOR3
9637 end
9638 if FFlagPlayerScriptsBindAtPriority2 then
9639 return Enum.ContextActionResult.Sink
9640 end
9641 end
9642
9643 ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
9644 if FFlagPlayerScriptsBindAtPriority2 then
9645 ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
9646 self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA)
9647 ContextActionService:BindActionAtPriority("moveThumbstick", handleThumbstickInput, false,
9648 self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1)
9649 else
9650 ContextActionService:BindAction("jumpAction",handleJumpAction, false, Enum.KeyCode.ButtonA)
9651 ContextActionService:BindAction("moveThumbstick",handleThumbstickInput, false, Enum.KeyCode.Thumbstick1)
9652 end
9653
9654 return true
9655end
9656
9657function Gamepad:UnbindContextActions()
9658 if self.activeGamepad ~= NONE then
9659 ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
9660 end
9661 ContextActionService:UnbindAction("moveThumbstick")
9662 ContextActionService:UnbindAction("jumpAction")
9663end
9664
9665function Gamepad:OnNewGamepadConnected()
9666 -- A new gamepad has been connected.
9667 local bestGamepad = self:GetHighestPriorityGamepad()
9668
9669 if bestGamepad == self.activeGamepad then
9670 -- A new gamepad was connected, but our active gamepad is not changing
9671 return
9672 end
9673
9674 if bestGamepad == NONE then
9675 -- There should be an active gamepad when GamepadConnected fires, so this should not
9676 -- normally be hit. If there is no active gamepad, unbind actions but leave
9677 -- the module enabled and continue to listen for a new gamepad connection.
9678 warn("Gamepad:OnNewGamepadConnected found no connected gamepads")
9679 self:UnbindContextActions()
9680 return
9681 end
9682
9683 if self.activeGamepad ~= NONE then
9684 -- Switching from one active gamepad to another
9685 self:UnbindContextActions()
9686 end
9687
9688 self.activeGamepad = bestGamepad
9689 self:BindContextActions()
9690end
9691
9692function Gamepad:OnCurrentGamepadDisconnected()
9693 if self.activeGamepad ~= NONE then
9694 ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
9695 end
9696
9697 local bestGamepad = self:GetHighestPriorityGamepad()
9698
9699 if self.activeGamepad ~= NONE and bestGamepad == self.activeGamepad then
9700 warn("Gamepad:OnCurrentGamepadDisconnected found the supposedly disconnected gamepad in connectedGamepads.")
9701 self:UnbindContextActions()
9702 self.activeGamepad = NONE
9703 return
9704 end
9705
9706 if bestGamepad == NONE then
9707 -- No active gamepad, unbinding actions but leaving gamepad connection listener active
9708 self:UnbindContextActions()
9709 self.activeGamepad = NONE
9710 else
9711 -- Set new gamepad as active and bind to tool activation
9712 self.activeGamepad = bestGamepad
9713 ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
9714 end
9715end
9716
9717function Gamepad:ConnectGamepadConnectionListeners()
9718 self.gamepadConnectedConn = UserInputService.GamepadConnected:Connect(function(gamepadEnum)
9719 self:OnNewGamepadConnected()
9720 end)
9721
9722 self.gamepadDisconnectedConn = UserInputService.GamepadDisconnected:Connect(function(gamepadEnum)
9723 if self.activeGamepad == gamepadEnum then
9724 self:OnCurrentGamepadDisconnected()
9725 end
9726 end)
9727
9728end
9729
9730function Gamepad:DisconnectGamepadConnectionListeners()
9731 if self.gamepadConnectedConn then
9732 self.gamepadConnectedConn:Disconnect()
9733 self.gamepadConnectedConn = nil
9734 end
9735
9736 if self.gamepadDisconnectedConn then
9737 self.gamepadDisconnectedConn:Disconnect()
9738 self.gamepadDisconnectedConn = nil
9739 end
9740end
9741
9742return Gamepad
9743
9744--[[
9745 Keyboard Character Control - This module handles controlling your avatar from a keyboard
9746
9747 2018 PlayerScripts Update - AllYourBlox
9748--]]
9749
9750--[[ Roblox Services ]]--
9751local UserInputService = game:GetService("UserInputService")
9752local ContextActionService = game:GetService("ContextActionService")
9753
9754--[[ Constants ]]--
9755local ZERO_VECTOR3 = Vector3.new(0,0,0)
9756
9757--[[ The Module ]]--
9758local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
9759local Keyboard = setmetatable({}, BaseCharacterController)
9760Keyboard.__index = Keyboard
9761
9762local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
9763 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority2")
9764end)
9765local FFlagPlayerScriptsBindAtPriority2 = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
9766
9767function Keyboard.new(CONTROL_ACTION_PRIORITY)
9768 local self = setmetatable(BaseCharacterController.new(), Keyboard)
9769
9770 self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
9771
9772 self.textFocusReleasedConn = nil
9773 self.textFocusGainedConn = nil
9774 self.windowFocusReleasedConn = nil
9775
9776 self.forwardValue = 0
9777 self.backwardValue = 0
9778 self.leftValue = 0
9779 self.rightValue = 0
9780
9781 self.jumpEnabled = true
9782
9783 return self
9784end
9785
9786function Keyboard:Enable(enable)
9787 if not UserInputService.KeyboardEnabled then
9788 return false
9789 end
9790
9791 if enable == self.enabled then
9792 -- Module is already in the state being requested. True is returned here since the module will be in the state
9793 -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
9794 -- no action was necessary. False indicates failure to be in requested/expected state.
9795 return true
9796 end
9797
9798 self.forwardValue = 0
9799 self.backwardValue = 0
9800 self.leftValue = 0
9801 self.rightValue = 0
9802 self.moveVector = ZERO_VECTOR3
9803 self.jumpRequested = false
9804 self:UpdateJump()
9805
9806 if enable then
9807 self:BindContextActions()
9808 self:ConnectFocusEventListeners()
9809 else
9810 self:UnbindContextActions()
9811 self:DisconnectFocusEventListeners()
9812 end
9813
9814 self.enabled = enable
9815 return true
9816end
9817
9818function Keyboard:UpdateMovement(inputState)
9819 if inputState == Enum.UserInputState.Cancel then
9820 self.moveVector = ZERO_VECTOR3
9821 else
9822 self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
9823 end
9824end
9825
9826function Keyboard:UpdateJump()
9827 self.isJumping = self.jumpRequested
9828end
9829
9830function Keyboard:BindContextActions()
9831
9832 -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
9833 -- which fixes them from getting stuck on.
9834 -- We return ContextActionResult.Pass here for legacy reasons.
9835 -- Many games rely on gameProcessedEvent being false on UserInputService.InputBegan for these control actions.
9836 local handleMoveForward = function(actionName, inputState, inputObject)
9837 self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
9838 self:UpdateMovement(inputState)
9839 if FFlagPlayerScriptsBindAtPriority2 then
9840 return Enum.ContextActionResult.Pass
9841 end
9842 end
9843
9844 local handleMoveBackward = function(actionName, inputState, inputObject)
9845 self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
9846 self:UpdateMovement(inputState)
9847 if FFlagPlayerScriptsBindAtPriority2 then
9848 return Enum.ContextActionResult.Pass
9849 end
9850 end
9851
9852 local handleMoveLeft = function(actionName, inputState, inputObject)
9853 self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
9854 self:UpdateMovement(inputState)
9855 if FFlagPlayerScriptsBindAtPriority2 then
9856 return Enum.ContextActionResult.Pass
9857 end
9858 end
9859
9860 local handleMoveRight = function(actionName, inputState, inputObject)
9861 self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
9862 self:UpdateMovement(inputState)
9863 if FFlagPlayerScriptsBindAtPriority2 then
9864 return Enum.ContextActionResult.Pass
9865 end
9866 end
9867
9868 local handleJumpAction = function(actionName, inputState, inputObject)
9869 self.jumpRequested = self.jumpEnabled and (inputState == Enum.UserInputState.Begin)
9870 self:UpdateJump()
9871 if FFlagPlayerScriptsBindAtPriority2 then
9872 return Enum.ContextActionResult.Pass
9873 end
9874 end
9875
9876 -- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to
9877 -- movement direction is done in Lua
9878 if FFlagPlayerScriptsBindAtPriority2 then
9879 ContextActionService:BindActionAtPriority("moveForwardAction", handleMoveForward, false,
9880 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterForward)
9881 ContextActionService:BindActionAtPriority("moveBackwardAction", handleMoveBackward, false,
9882 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterBackward)
9883 ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false,
9884 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft)
9885 ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false,
9886 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight)
9887 ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
9888 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterJump)
9889 else
9890 ContextActionService:BindAction("moveForwardAction", handleMoveForward, false, Enum.PlayerActions.CharacterForward)
9891 ContextActionService:BindAction("moveBackwardAction", handleMoveBackward, false, Enum.PlayerActions.CharacterBackward)
9892 ContextActionService:BindAction("moveLeftAction", handleMoveLeft, false, Enum.PlayerActions.CharacterLeft)
9893 ContextActionService:BindAction("moveRightAction", handleMoveRight, false, Enum.PlayerActions.CharacterRight)
9894 ContextActionService:BindAction("jumpAction",handleJumpAction,false,Enum.PlayerActions.CharacterJump)
9895 end
9896end
9897
9898function Keyboard:UnbindContextActions()
9899 ContextActionService:UnbindAction("moveForwardAction")
9900 ContextActionService:UnbindAction("moveBackwardAction")
9901 ContextActionService:UnbindAction("moveLeftAction")
9902 ContextActionService:UnbindAction("moveRightAction")
9903 ContextActionService:UnbindAction("jumpAction")
9904end
9905
9906function Keyboard:ConnectFocusEventListeners()
9907 local function onFocusReleased()
9908 self.moveVector = ZERO_VECTOR3
9909 self.forwardValue = 0
9910 self.backwardValue = 0
9911 self.leftValue = 0
9912 self.rightValue = 0
9913 self.jumpRequested = false
9914 self:UpdateJump()
9915 end
9916
9917 local function onTextFocusGained(textboxFocused)
9918 self.jumpRequested = false
9919 self:UpdateJump()
9920 end
9921
9922 self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased)
9923 self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained)
9924 self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased)
9925end
9926
9927function Keyboard:DisconnectFocusEventListeners()
9928 if self.textFocusReleasedCon then
9929 self.textFocusReleasedCon:Disconnect()
9930 self.textFocusReleasedCon = nil
9931 end
9932 if self.textFocusGainedConn then
9933 self.textFocusGainedConn:Disconnect()
9934 self.textFocusGainedConn = nil
9935 end
9936 if self.windowFocusReleasedConn then
9937 self.windowFocusReleasedConn:Disconnect()
9938 self.windowFocusReleasedConn = nil
9939 end
9940end
9941
9942return Keyboard
9943
9944
9945
9946local PathDisplay = {}
9947PathDisplay.spacing = 8
9948PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
9949PathDisplay.imageSize = Vector2.new(2, 2)
9950
9951local currentPoints = {}
9952local renderedPoints = {}
9953
9954local pointModel = Instance.new("Model")
9955pointModel.Name = "PathDisplayPoints"
9956
9957local adorneePart = Instance.new("Part")
9958adorneePart.Anchored = true
9959adorneePart.CanCollide = false
9960adorneePart.Transparency = 1
9961adorneePart.Name = "PathDisplayAdornee"
9962adorneePart.CFrame = CFrame.new(0, 0, 0)
9963adorneePart.Parent = pointModel
9964
9965local pointPool = {}
9966local poolTop = 30
9967for i = 1, poolTop do
9968 local point = Instance.new("ImageHandleAdornment")
9969 point.Archivable = false
9970 point.Adornee = adorneePart
9971 point.Image = PathDisplay.image
9972 point.Size = PathDisplay.imageSize
9973 pointPool[i] = point
9974end
9975
9976local function retrieveFromPool()
9977 local point = pointPool[1]
9978 if not point then
9979 return
9980 end
9981
9982 pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil
9983 poolTop = poolTop - 1
9984 return point
9985end
9986
9987local function returnToPool(point)
9988 poolTop = poolTop + 1
9989 pointPool[poolTop] = point
9990end
9991
9992local function renderPoint(point, isLast)
9993 if poolTop == 0 then
9994 return
9995 end
9996
9997 local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0))
9998 local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { game.Players.LocalPlayer.Character, workspace.CurrentCamera })
9999 if not hitPart then
10000 return
10001 end
10002
10003 local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
10004
10005 local point = retrieveFromPool()
10006 point.CFrame = pointCFrame
10007 point.Parent = pointModel
10008 return point
10009end
10010
10011function PathDisplay.setCurrentPoints(points)
10012 if typeof(points) == 'table' then
10013 currentPoints = points
10014 else
10015 currentPoints = {}
10016 end
10017end
10018
10019function PathDisplay.clearRenderedPath()
10020 for _, oldPoint in ipairs(renderedPoints) do
10021 oldPoint.Parent = nil
10022 returnToPool(oldPoint)
10023 end
10024 renderedPoints = {}
10025 pointModel.Parent = nil
10026end
10027
10028function PathDisplay.renderPath()
10029 PathDisplay.clearRenderedPath()
10030 if not currentPoints or #currentPoints == 0 then
10031 return
10032 end
10033
10034 local currentIdx = #currentPoints
10035 local lastPos = currentPoints[currentIdx]
10036 local distanceBudget = 0
10037
10038 renderedPoints[1] = renderPoint(lastPos, true)
10039 if not renderedPoints[1] then
10040 return
10041 end
10042
10043 while true do
10044 local currentPoint = currentPoints[currentIdx]
10045 local nextPoint = currentPoints[currentIdx - 1]
10046
10047 if currentIdx < 2 then
10048 break
10049 else
10050
10051 local toNextPoint = nextPoint - currentPoint
10052 local distToNextPoint = toNextPoint.magnitude
10053
10054 if distanceBudget > distToNextPoint then
10055 distanceBudget = distanceBudget - distToNextPoint
10056 currentIdx = currentIdx - 1
10057 else
10058 local dirToNextPoint = toNextPoint.unit
10059 local pointPos = currentPoint + (dirToNextPoint * distanceBudget)
10060 local point = renderPoint(pointPos, false)
10061
10062 if point then
10063 renderedPoints[#renderedPoints + 1] = point
10064 end
10065
10066 distanceBudget = distanceBudget + PathDisplay.spacing
10067 end
10068 end
10069 end
10070
10071 pointModel.Parent = workspace.CurrentCamera
10072end
10073
10074return PathDisplay