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