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