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