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