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