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