· 7 years ago · Feb 08, 2019, 10:58 PM
1
2 end
3 elseif computerInputTypeToModuleMap[lastInputType] ~= nil then
4 local computerModule = self:SelectComputerMovementModule()
5 if computerModule then
6 self:SwitchToController(computerModule)
7 end
8 end
9end
10
11-- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of
12-- current control scheme
13function ControlModule:OnComputerMovementModeChange()
14 local controlModule, success = self:SelectComputerMovementModule()
15 if success then
16 self:SwitchToController(controlModule)
17 end
18end
19
20function ControlModule:OnTouchMovementModeChange()
21 local touchModule, success = self:SelectTouchModule()
22 if success then
23 while not self.touchControlFrame do
24 wait()
25 end
26 self:SwitchToController(touchModule)
27 end
28end
29
30function ControlModule:CreateTouchGuiContainer()
31 if self.touchGui then self.touchGui:Destroy() end
32
33 -- Container for all touch device guis
34 self.touchGui = Instance.new('ScreenGui')
35 self.touchGui.Name = "TouchGui"
36 self.touchGui.ResetOnSpawn = false
37
38 self.touchControlFrame = Instance.new("Frame")
39 self.touchControlFrame.Name = "TouchControlFrame"
40 self.touchControlFrame.Size = UDim2.new(1, 0, 1, 0)
41 self.touchControlFrame.BackgroundTransparency = 1
42 self.touchControlFrame.Parent = self.touchGui
43
44 self.touchGui.Parent = self.playerGui
45end
46
47return ControlModule.new()
48
49
50
51--[[
52 VRNavigation
53--]]
54
55local VRService = game:GetService("VRService")
56local UserInputService = game:GetService("UserInputService")
57local RunService = game:GetService("RunService")
58local Players = game:GetService("Players")
59local PathfindingService = game:GetService("PathfindingService")
60local ContextActionService = game:GetService("ContextActionService")
61local StarterGui = game:GetService("StarterGui")
62
63--local MasterControl = require(script.Parent)
64local PathDisplay = nil
65local LocalPlayer = Players.LocalPlayer
66
67local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
68 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
69end)
70local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
71
72--[[ Constants ]]--
73local RECALCULATE_PATH_THRESHOLD = 4
74local NO_PATH_THRESHOLD = 12
75local MAX_PATHING_DISTANCE = 200
76local POINT_REACHED_THRESHOLD = 1
77local OFFTRACK_TIME_THRESHOLD = 2
78local THUMBSTICK_DEADZONE = 0.22
79
80local ZERO_VECTOR3 = Vector3.new(0,0,0)
81local XZ_VECTOR3 = Vector3.new(1,0,1)
82
83--[[ Utility Functions ]]--
84local function IsFinite(num)
85 return num == num and num ~= 1/0 and num ~= -1/0
86end
87
88local function IsFiniteVector3(vec3)
89 return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
90end
91
92local movementUpdateEvent = Instance.new("BindableEvent")
93movementUpdateEvent.Name = "MovementUpdate"
94movementUpdateEvent.Parent = script
95
96coroutine.wrap(function()
97 local PathDisplayModule = script.Parent:WaitForChild("PathDisplay")
98 if PathDisplayModule then
99 PathDisplay = require(PathDisplayModule)
100 end
101end)()
102
103
104--[[ The Class ]]--
105local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
106local VRNavigation = setmetatable({}, BaseCharacterController)
107VRNavigation.__index = VRNavigation
108
109function VRNavigation.new(CONTROL_ACTION_PRIORITY)
110 local self = setmetatable(BaseCharacterController.new(), VRNavigation)
111
112 self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
113
114 self.navigationRequestedConn = nil
115 self.heartbeatConn = nil
116
117 self.currentDestination = nil
118 self.currentPath = nil
119 self.currentPoints = nil
120 self.currentPointIdx = 0
121
122 self.expectedTimeToNextPoint = 0
123 self.timeReachedLastPoint = tick()
124 self.moving = false
125
126 self.isJumpBound = false
127 self.moveLatch = false
128
129 self.userCFrameEnabledConn = nil
130
131 return self
132end
133
134function VRNavigation:SetLaserPointerMode(mode)
135 pcall(function()
136 StarterGui:SetCore("VRLaserPointerMode", mode)
137 end)
138end
139
140function VRNavigation:GetLocalHumanoid()
141 local character = LocalPlayer.Character
142 if not character then
143 return
144 end
145
146 for _, child in pairs(character:GetChildren()) do
147 if child:IsA("Humanoid") then
148 return child
149 end
150 end
151 return nil
152end
153
154function VRNavigation:HasBothHandControllers()
155 return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
156end
157
158function VRNavigation:HasAnyHandControllers()
159 return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
160end
161
162function VRNavigation:IsMobileVR()
163 return UserInputService.TouchEnabled
164end
165
166function VRNavigation:HasGamepad()
167 return UserInputService.GamepadEnabled
168end
169
170function VRNavigation:ShouldUseNavigationLaser()
171 --Places where we use the navigation laser:
172 -- mobile VR with any number of hands tracked
173 -- desktop VR with only one hand tracked
174 -- desktop VR with no hands and no gamepad (i.e. with Oculus remote?)
175 --using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick.
176 --in the future, we should query thumbstick presence with a features API
177 if self:IsMobileVR() then
178 return true
179 else
180 if self:HasBothHandControllers() then
181 return false
182 end
183 if not self:HasAnyHandControllers() then
184 return not self:HasGamepad()
185 end
186 return true
187 end
188end
189
190
191
192function VRNavigation:StartFollowingPath(newPath)
193 currentPath = newPath
194 currentPoints = currentPath:GetPointCoordinates()
195 currentPointIdx = 1
196 moving = true
197
198 timeReachedLastPoint = tick()
199
200 local humanoid = self:GetLocalHumanoid()
201 if humanoid and humanoid.Torso and #currentPoints >= 1 then
202 local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude
203 expectedTimeToNextPoint = dist / humanoid.WalkSpeed
204 end
205
206 movementUpdateEvent:Fire("targetPoint", self.currentDestination)
207end
208
209function VRNavigation:GoToPoint(point)
210 currentPath = true
211 currentPoints = { point }
212 currentPointIdx = 1
213 moving = true
214
215 local humanoid = self:GetLocalHumanoid()
216 local distance = (humanoid.Torso.Position - point).magnitude
217 local estimatedTimeRemaining = distance / humanoid.WalkSpeed
218
219 timeReachedLastPoint = tick()
220 expectedTimeToNextPoint = estimatedTimeRemaining
221
222 movementUpdateEvent:Fire("targetPoint", point)
223end
224
225function VRNavigation:StopFollowingPath()
226 currentPath = nil
227 currentPoints = nil
228 currentPointIdx = 0
229 moving = false
230 self.moveVector = ZERO_VECTOR3
231end
232
233function VRNavigation:TryComputePath(startPos, destination)
234 local numAttempts = 0
235 local newPath = nil
236
237 while not newPath and numAttempts < 5 do
238 newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE)
239 numAttempts = numAttempts + 1
240
241 if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then
242 newPath = nil
243 break
244 end
245
246 if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then
247 startPos = startPos + (destination - startPos).unit
248 newPath = nil
249 end
250
251 if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then
252 destination = destination + Vector3.new(0, 1, 0)
253 newPath = nil
254 end
255 end
256
257 return newPath
258end
259
260function VRNavigation:OnNavigationRequest(destinationCFrame, inputUserCFrame )
261 local destinationPosition = destinationCFrame.p
262 local lastDestination = self.currentDestination
263
264 if not IsFiniteVector3(destinationPosition) then
265 return
266 end
267
268 self.currentDestination = destinationPosition
269
270 local humanoid = self:GetLocalHumanoid()
271 if not humanoid or not humanoid.Torso then
272 return
273 end
274
275 local currentPosition = humanoid.Torso.Position
276 local distanceToDestination = (self.currentDestination - currentPosition).magnitude
277
278 if distanceToDestination < NO_PATH_THRESHOLD then
279 self:GoToPoint(self.currentDestination)
280 return
281 end
282
283 if not lastDestination or (self.currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then
284 local newPath = self:TryComputePath(currentPosition, self.currentDestination)
285 if newPath then
286 self:StartFollowingPath(newPath)
287 if PathDisplay then
288 PathDisplay.setCurrentPoints(self.currentPoints)
289 PathDisplay.renderPath()
290 end
291 else
292 self:StopFollowingPath()
293 if PathDisplay then
294 PathDisplay.clearRenderedPath()
295 end
296 end
297 else
298 if moving then
299 self.currentPoints[#currentPoints] = self.currentDestination
300 else
301 self:GoToPoint(self.currentDestination)
302 end
303 end
304end
305
306function VRNavigation:OnJumpAction(actionName, inputState, inputObj)
307 if inputState == Enum.UserInputState.Begin then
308 self.isJumping = true
309 end
310 if FFlagPlayerScriptsBindAtPriority then
311 return Enum.ContextActionResult.Sink
312 end
313end
314function VRNavigation:BindJumpAction(active)
315 if active then
316 if not self.isJumpBound then
317 self.isJumpBound = true
318 if FFlagPlayerScriptsBindAtPriority then
319 ContextActionService:BindActionAtPriority("VRJumpAction", (function() return self:OnJumpAction() end), false,
320 self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA)
321 else
322 ContextActionService:BindAction("VRJumpAction", (function() self:OnJumpAction() end), false, Enum.KeyCode.ButtonA)
323 end
324 end
325 else
326 if self.isJumpBound then
327 self.isJumpBound = false
328 ContextActionService:UnbindAction("VRJumpAction")
329 end
330 end
331end
332
333function VRNavigation:ControlCharacterGamepad(actionName, inputState, inputObject)
334 if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
335
336 if inputState == Enum.UserInputState.Cancel then
337 self.moveVector = ZERO_VECTOR3
338 return
339 end
340
341 if inputState ~= Enum.UserInputState.End then
342 self:StopFollowingPath()
343 if PathDisplay then
344 PathDisplay.clearRenderedPath()
345 end
346
347 if self:ShouldUseNavigationLaser() then
348 self:BindJumpAction(true)
349 self:SetLaserPointerMode("Hidden")
350 end
351
352 if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then
353 self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
354 if self.moveVector.magnitude > 0 then
355 self.moveVector = self.moveVector.unit * math.min(1, inputObject.Position.magnitude)
356 end
357
358 self.moveLatch = true
359 end
360 else
361 self.moveVector = ZERO_VECTOR3
362
363 if self:ShouldUseNavigationLaser() then
364 self:BindJumpAction(false)
365 self:SetLaserPointerMode("Navigation")
366 end
367
368 if self.moveLatch then
369 self.moveLatch = false
370 movementUpdateEvent:Fire("offtrack")
371 end
372 end
373 if FFlagPlayerScriptsBindAtPriority then
374 return Enum.ContextActionResult.Sink
375 end
376end
377
378function VRNavigation:OnHeartbeat(dt)
379 local newMoveVector = self.moveVector
380 local humanoid = self:GetLocalHumanoid()
381 if not humanoid or not humanoid.Torso then
382 return
383 end
384
385 if self.moving and self.currentPoints then
386 local currentPosition = humanoid.Torso.Position
387 local goalPosition = currentPoints[1]
388 local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3
389 local moveDist = vectorToGoal.magnitude
390 local moveDir = vectorToGoal / moveDist
391
392 if moveDist < POINT_REACHED_THRESHOLD then
393 local estimatedTimeRemaining = 0
394 local prevPoint = currentPoints[1]
395 for i, point in pairs(currentPoints) do
396 if i ~= 1 then
397 local dist = (point - prevPoint).magnitude
398 prevPoint = point
399 estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed)
400 end
401 end
402
403 table.remove(currentPoints, 1)
404 currentPointIdx = currentPointIdx + 1
405
406 if #currentPoints == 0 then
407 self:StopFollowingPath()
408 if PathDisplay then
409 PathDisplay.clearRenderedPath()
410 end
411 return
412 else
413 if PathDisplay then
414 PathDisplay.setCurrentPoints(currentPoints)
415 PathDisplay.renderPath()
416 end
417
418 local newGoal = currentPoints[1]
419 local distanceToGoal = (newGoal - currentPosition).magnitude
420 expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed
421 timeReachedLastPoint = tick()
422 end
423 else
424 local ignoreTable = {
425 game.Players.LocalPlayer.Character,
426 workspace.CurrentCamera
427 }
428 local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3)
429 local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable)
430
431 if obstructPart then
432 local heightOffset = Vector3.new(0, 100, 0)
433 local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset)
434 local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable)
435
436 local heightDifference = jumpCheckPoint.Y - currentPosition.Y
437 if heightDifference < 6 and heightDifference > -2 then
438 humanoid.Jump = true
439 end
440 end
441
442 local timeSinceLastPoint = tick() - timeReachedLastPoint
443 if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then
444 self:StopFollowingPath()
445 if PathDisplay then
446 PathDisplay.clearRenderedPath()
447 end
448
449 movementUpdateEvent:Fire("offtrack")
450 end
451
452 newMoveVector = self.moveVector:Lerp(moveDir, dt * 10)
453 end
454 end
455
456 if IsFiniteVector3(newMoveVector) then
457 self.moveVector = newMoveVector
458 end
459end
460
461
462function VRNavigation:OnUserCFrameEnabled()
463 if self:ShouldUseNavigationLaser() then
464 self:BindJumpAction(false)
465 self:SetLaserPointerMode("Navigation")
466 else
467 self:BindJumpAction(true)
468 self:SetLaserPointerMode("Hidden")
469 end
470end
471
472function VRNavigation:Enable(enable)
473
474 self.moveVector = ZERO_VECTOR3
475 self.isJumping = false
476
477 if enable then
478 self.navigationRequestedConn = VRService.NavigationRequested:Connect(function(destinationCFrame, inputUserCFrame) self:OnNavigationRequest(destinationCFrame, inputUserCFrame) end)
479 self.heartbeatConn = RunService.Heartbeat:Connect(function(dt) self:OnHeartbeat(dt) end)
480
481 if FFlagPlayerScriptsBindAtPriority then
482 ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) return self:ControlCharacterGamepad(actionName, inputState, inputObject) end),
483 false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1)
484 else
485 ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) self:ControlCharacterGamepad(actionName, inputState, inputObject) end), false, Enum.KeyCode.Thumbstick1)
486 end
487 ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
488
489 self.userCFrameEnabledConn = VRService.UserCFrameEnabled:Connect(function() self:OnUserCFrameEnabled() end)
490 self:OnUserCFrameEnabled()
491
492 VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick)
493 VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY)
494
495 self.enabled = true
496 else
497 -- Disable
498 self:StopFollowingPath()
499
500 ContextActionService:UnbindAction("MoveThumbstick")
501 ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
502
503 self:BindJumpAction(false)
504 self:SetLaserPointerMode("Disabled")
505
506 if self.navigationRequestedConn then
507 self.navigationRequestedConn:Disconnect()
508 self.navigationRequestedConn = nil
509 end
510 if self.heartbeatConn then
511 self.heartbeatConn:Disconnect()
512 self.heartbeatConn = nil
513 end
514 if self.userCFrameEnabledConn then
515 self.userCFrameEnabledConn:Disconnect()
516 self.userCFrameEnabledConn = nil
517 end
518 self.enabled = false
519 end
520end
521
522return VRNavigation
523
524
525--[[
526 // FileName: VehicleControl
527 // Version 1.0
528 // Written by: jmargh
529 // Description: Implements in-game vehicle controls for all input devices
530
531 // NOTE: This works for basic vehicles (single vehicle seat). If you use custom VehicleSeat code,
532 // multiple VehicleSeats or your own implementation of a VehicleSeat this will not work.
533--]]
534local ContextActionService = game:GetService("ContextActionService")
535local Players = game:GetService("Players")
536local RunService = game:GetService("RunService")
537
538--[[ Constants ]]--
539-- Set this to true if you want to instead use the triggers for the throttle
540local useTriggersForThrottle = true
541-- Also set this to true if you want the thumbstick to not affect throttle, only triggers when a gamepad is conected
542local onlyTriggersForThrottle = false
543local ZERO_VECTOR3 = Vector3.new(0,0,0)
544
545local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
546 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
547end)
548local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
549
550-- Note that VehicleController does not derive from BaseCharacterController, it is a special case
551local VehicleController = {}
552VehicleController.__index = VehicleController
553
554function VehicleController.new(CONTROL_ACTION_PRIORITY)
555 local self = setmetatable({}, VehicleController)
556
557 self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
558
559 self.enabled = false
560 self.vehicleSeat = nil
561 self.throttle = 0
562 self.steer = 0
563
564 self.acceleration = 0
565 self.decceleration = 0
566 self.turningRight = 0
567 self.turningLeft = 0
568
569 self.vehicleMoveVector = ZERO_VECTOR3
570
571 return self
572end
573
574function VehicleController:BindContextActions()
575 if useTriggersForThrottle then
576 ContextActionService:BindActionAtPriority("throttleAccel", (function(actionName, inputState, inputObject)
577 self:OnThrottleAccel(actionName, inputState, inputObject)
578 return Enum.ContextActionResult.Pass
579 end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonR2)
580 ContextActionService:BindActionAtPriority("throttleDeccel", (function(actionName, inputState, inputObject)
581 self:OnThrottleDeccel(actionName, inputState, inputObject)
582 return Enum.ContextActionResult.Pass
583 end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonL2)
584 end
585 ContextActionService:BindActionAtPriority("arrowSteerRight", (function(actionName, inputState, inputObject)
586 self:OnSteerRight(actionName, inputState, inputObject)
587 return Enum.ContextActionResult.Pass
588 end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Right)
589 ContextActionService:BindActionAtPriority("arrowSteerLeft", (function(actionName, inputState, inputObject)
590 self:OnSteerLeft(actionName, inputState, inputObject)
591 return Enum.ContextActionResult.Pass
592 end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Left)
593end
594
595function VehicleController:Enable(enable, vehicleSeat)
596 if enable == self.enabled and vehicleSeat == self.vehicleSeat then
597 return
598 end
599
600 self.vehicleMoveVector = ZERO_VECTOR3
601
602 if enable then
603 if vehicleSeat then
604 self.vehicleSeat = vehicleSeat
605 if FFlagPlayerScriptsBindAtPriority then
606 self:BindContextActions()
607 else
608 if useTriggersForThrottle then
609 ContextActionService:BindAction("throttleAccel", (function(actionName, inputState, inputObject)
610 self:OnThrottleAccel(actionName, inputState, inputObject)
611 return Enum.ContextActionResult.Pass
612 end), false, Enum.KeyCode.ButtonR2)
613 ContextActionService:BindAction("throttleDeccel", (function(actionName, inputState, inputObject)
614 self:OnThrottleDeccel(actionName, inputState, inputObject)
615 return Enum.ContextActionResult.Pass
616 end), false, Enum.KeyCode.ButtonL2)
617 end
618 ContextActionService:BindAction("arrowSteerRight", (function(actionName, inputState, inputObject)
619 self:OnSteerRight(actionName, inputState, inputObject)
620 return Enum.ContextActionResult.Pass
621 end), false, Enum.KeyCode.Right)
622 ContextActionService:BindAction("arrowSteerLeft", (function(actionName, inputState, inputObject)
623 self:OnSteerLeft(actionName, inputState, inputObject)
624 return Enum.ContextActionResult.Pass
625 end), false, Enum.KeyCode.Left)
626 end
627 end
628 else
629 if useTriggersForThrottle then
630 ContextActionService:UnbindAction("throttleAccel")
631 ContextActionService:UnbindAction("throttleDeccel")
632 end
633 ContextActionService:UnbindAction("arrowSteerRight")
634 ContextActionService:UnbindAction("arrowSteerLeft")
635 self.vehicleSeat = nil
636 end
637end
638
639function VehicleController:OnThrottleAccel(actionName, inputState, inputObject)
640 self.acceleration = (inputState ~= Enum.UserInputState.End) and -1 or 0
641 self.throttle = self.acceleration + self.decceleration
642end
643
644function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject)
645 self.decceleration = (inputState ~= Enum.UserInputState.End) and 1 or 0
646 self.throttle = self.acceleration + self.decceleration
647end
648
649function VehicleController:OnSteerRight(actionName, inputState, inputObject)
650 self.turningRight = (inputState ~= Enum.UserInputState.End) and 1 or 0
651 self.steer = self.turningRight + self.turningLeft
652end
653
654function VehicleController:OnSteerLeft(actionName, inputState, inputObject)
655 self.turningLeft = (inputState ~= Enum.UserInputState.End) and -1 or 0
656 self.steer = self.turningRight + self.turningLeft
657end
658
659-- Call this from a function bound to Renderstep with Input Priority
660function VehicleController:Update(moveVector, usingGamepad)
661 if self.vehicleSeat then
662 moveVector = moveVector + Vector3.new(self.steer, 0, self.throttle)
663 if usingGamepad and onlyTriggersForThrottle and useTriggersForThrottle then
664 self.vehicleSeat.ThrottleFloat = -self.throttle
665 else
666 self.vehicleSeat.ThrottleFloat = -moveVector.Z
667 end
668 self.vehicleSeat.SteerFloat = moveVector.X
669
670 return moveVector, true
671 end
672 return moveVector, false
673end
674
675return VehicleController
676
677
678--[[
679 // FileName: TouchJump
680 // Version 1.0
681 // Written by: jmargh
682 // Description: Implements jump controls for touch devices. Use with Thumbstick and Thumbpad
683--]]
684
685local Players = game:GetService("Players")
686local GuiService = game:GetService("GuiService")
687
688--[[ Constants ]]--
689local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
690
691--[[ The Module ]]--
692local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
693local TouchJump = setmetatable({}, BaseCharacterController)
694TouchJump.__index = TouchJump
695
696function TouchJump.new()
697 local self = setmetatable(BaseCharacterController.new(), TouchJump)
698
699 self.parentUIFrame = nil
700 self.jumpButton = nil
701 self.characterAddedConn = nil
702 self.humanoidStateEnabledChangedConn = nil
703 self.humanoidJumpPowerConn = nil
704 self.humanoidParentConn = nil
705 self.externallyEnabled = false
706 self.jumpPower = 0
707 self.jumpStateEnabled = true
708 self.isJumping = false
709 self.humanoid = nil -- saved reference because property change connections are made using it
710
711 return self
712end
713
714function TouchJump:EnableButton(enable)
715 if enable then
716 if not self.jumpButton then
717 self:Create()
718 end
719 local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
720 if humanoid and self.externallyEnabled then
721 if self.externallyEnabled then
722 if humanoid.JumpPower > 0 then
723 self.jumpButton.Visible = true
724 end
725 end
726 end
727 else
728 self.jumpButton.Visible = false
729 self.isJumping = false
730 self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
731 end
732end
733
734function TouchJump:UpdateEnabled()
735 if self.jumpPower > 0 and self.jumpStateEnabled then
736 self:EnableButton(true)
737 else
738 self:EnableButton(false)
739 end
740end
741
742function TouchJump:HumanoidChanged(prop)
743 local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
744 if humanoid then
745 if prop == "JumpPower" then
746 self.jumpPower = humanoid.JumpPower
747 self:UpdateEnabled()
748 elseif prop == "Parent" then
749 if not humanoid.Parent then
750 self.humanoidChangeConn:Disconnect()
751 end
752 end
753 end
754end
755
756function TouchJump:HumanoidStateEnabledChanged(state, isEnabled)
757 if state == Enum.HumanoidStateType.Jumping then
758 self.jumpStateEnabled = isEnabled
759 self:UpdateEnabled()
760 end
761end
762
763function TouchJump:CharacterAdded(char)
764 if self.humanoidChangeConn then
765 self.humanoidChangeConn:Disconnect()
766 self.humanoidChangeConn = nil
767 end
768
769 self.humanoid = char:FindFirstChildOfClass("Humanoid")
770 while not self.humanoid do
771 char.ChildAdded:wait()
772 self.humanoid = char:FindFirstChildOfClass("Humanoid")
773 end
774
775 self.humanoidJumpPowerConn = self.humanoid:GetPropertyChangedSignal("JumpPower"):Connect(function()
776 self.jumpPower = self.humanoid.JumpPower
777 self:UpdateEnabled()
778 end)
779
780 self.humanoidParentConn = self.humanoid:GetPropertyChangedSignal("Parent"):Connect(function()
781 if not self.humanoid.Parent then
782 self.humanoidJumpPowerConn:Disconnect()
783 self.humanoidJumpPowerConn = nil
784 self.humanoidParentConn:Disconnect()
785 self.humanoidParentConn = nil
786 end
787 end)
788
789 self.humanoidStateEnabledChangedConn = self.humanoid.StateEnabledChanged:Connect(function(state, enabled)
790 self:HumanoidStateEnabledChanged(state, enabled)
791 end)
792
793 self.jumpPower = self.humanoid.JumpPower
794 self.jumpStateEnabled = self.humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping)
795 self:UpdateEnabled()
796end
797
798function TouchJump:SetupCharacterAddedFunction()
799 self.characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(function(char)
800 self:CharacterAdded(char)
801 end)
802 if Players.LocalPlayer.Character then
803 self:CharacterAdded(Players.LocalPlayer.Character)
804 end
805end
806
807function TouchJump:Enable(enable, parentFrame)
808 self.parentUIFrame = parentFrame
809 self.externallyEnabled = enable
810 self:EnableButton(enable)
811end
812
813function TouchJump:Create()
814 if not self.parentUIFrame then
815 return
816 end
817
818 if self.jumpButton then
819 self.jumpButton:Destroy()
820 self.jumpButton = nil
821 end
822
823 local minAxis = math.min(self.parentUIFrame.AbsoluteSize.x, self.parentUIFrame.AbsoluteSize.y)
824 local isSmallScreen = minAxis <= 500
825 local jumpButtonSize = isSmallScreen and 70 or 120
826
827 self.jumpButton = Instance.new("ImageButton")
828 self.jumpButton.Name = "JumpButton"
829 self.jumpButton.Visible = false
830 self.jumpButton.BackgroundTransparency = 1
831 self.jumpButton.Image = TOUCH_CONTROL_SHEET
832 self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
833 self.jumpButton.ImageRectSize = Vector2.new(144, 144)
834 self.jumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize)
835
836 self.jumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or
837 UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75)
838
839 local touchObject = nil
840 self.jumpButton.InputBegan:connect(function(inputObject)
841 --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
842 --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
843 if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
844 or inputObject.UserInputState ~= Enum.UserInputState.Begin then
845 return
846 end
847
848 touchObject = inputObject
849 self.jumpButton.ImageRectOffset = Vector2.new(146, 146)
850 self.isJumping = true
851 end)
852
853 local OnInputEnded = function()
854 touchObject = nil
855 self.isJumping = false
856 self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
857 end
858
859 self.jumpButton.InputEnded:connect(function(inputObject)
860 if inputObject == touchObject then
861 OnInputEnded()
862 end
863 end)
864
865 GuiService.MenuOpened:connect(function()
866 if touchObject then
867 OnInputEnded()
868 end
869 end)
870
871 if not self.characterAddedConn then
872 self:SetupCharacterAddedFunction()
873 end
874
875 self.jumpButton.Parent = self.parentUIFrame
876end
877
878return TouchJump
879
880
881
882
883local PathDisplay = {}
884PathDisplay.spacing = 8
885PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
886PathDisplay.imageSize = Vector2.new(2, 2)
887
888local currentPoints = {}
889local renderedPoints = {}
890
891local pointModel = Instance.new("Model")
892pointModel.Name = "PathDisplayPoints"
893
894local adorneePart = Instance.new("Part")
895adorneePart.Anchored = true
896adorneePart.CanCollide = false
897adorneePart.Transparency = 1
898adorneePart.Name = "PathDisplayAdornee"
899adorneePart.CFrame = CFrame.new(0, 0, 0)
900adorneePart.Parent = pointModel
901
902local pointPool = {}
903local poolTop = 30
904for i = 1, poolTop do
905 local point = Instance.new("ImageHandleAdornment")
906 point.Archivable = false
907 point.Adornee = adorneePart
908 point.Image = PathDisplay.image
909 point.Size = PathDisplay.imageSize
910 pointPool[i] = point
911end
912
913local function retrieveFromPool()
914 local point = pointPool[1]
915 if not point then
916 return
917 end
918
919 pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil
920 poolTop = poolTop - 1
921 return point
922end
923
924local function returnToPool(point)
925 poolTop = poolTop + 1
926 pointPool[poolTop] = point
927end
928
929local function renderPoint(point, isLast)
930 if poolTop == 0 then
931 return
932 end
933
934 local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0))
935 local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { game.Players.LocalPlayer.Character, workspace.CurrentCamera })
936 if not hitPart then
937 return
938 end
939
940 local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
941
942 local point = retrieveFromPool()
943 point.CFrame = pointCFrame
944 point.Parent = pointModel
945 return point
946end
947
948function PathDisplay.setCurrentPoints(points)
949 if typeof(points) == 'table' then
950 currentPoints = points
951 else
952 currentPoints = {}
953 end
954end
955
956function PathDisplay.clearRenderedPath()
957 for _, oldPoint in ipairs(renderedPoints) do
958 oldPoint.Parent = nil
959 returnToPool(oldPoint)
960 end
961 renderedPoints = {}
962 pointModel.Parent = nil
963end
964
965function PathDisplay.renderPath()
966 PathDisplay.clearRenderedPath()
967 if not currentPoints or #currentPoints == 0 then
968 return
969 end
970
971 local currentIdx = #currentPoints
972 local lastPos = currentPoints[currentIdx]
973 local distanceBudget = 0
974
975 renderedPoints[1] = renderPoint(lastPos, true)
976 if not renderedPoints[1] then
977 return
978 end
979
980 while true do
981 local currentPoint = currentPoints[currentIdx]
982 local nextPoint = currentPoints[currentIdx - 1]
983
984 if currentIdx < 2 then
985 break
986 else
987
988 local toNextPoint = nextPoint - currentPoint
989 local distToNextPoint = toNextPoint.magnitude
990
991 if distanceBudget > distToNextPoint then
992 distanceBudget = distanceBudget - distToNextPoint
993 currentIdx = currentIdx - 1
994 else
995 local dirToNextPoint = toNextPoint.unit
996 local pointPos = currentPoint + (dirToNextPoint * distanceBudget)
997 local point = renderPoint(pointPos, false)
998
999 if point then
1000 renderedPoints[#renderedPoints + 1] = point
1001 end
1002
1003 distanceBudget = distanceBudget + PathDisplay.spacing
1004 end
1005 end
1006 end
1007
1008 pointModel.Parent = workspace.CurrentCamera
1009end
1010
1011return PathDisplay
1012
1013
1014--[[ Constants ]]--
1015local ZERO_VECTOR3 = Vector3.new(0,0,0)
1016local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
1017
1018local MIDDLE_TRANSPARENCIES = {
1019 1 - 0.89,
1020 1 - 0.70,
1021 1 - 0.60,
1022 1 - 0.50,
1023 1 - 0.40,
1024 1 - 0.30,
1025 1 - 0.25
1026}
1027local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES
1028
1029local FADE_IN_OUT_BACKGROUND = true
1030local FADE_IN_OUT_MAX_ALPHA = 0.35
1031
1032local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3
1033local FADE_IN_OUT_BALANCE_DEFAULT = 0.5
1034local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
1035
1036local Players = game:GetService("Players")
1037local GuiService = game:GetService("GuiService")
1038local UserInputService = game:GetService("UserInputService")
1039local RunService = game:GetService("RunService")
1040local TweenService = game:GetService("TweenService")
1041
1042--[[ The Module ]]--
1043local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
1044local DynamicThumbstick = setmetatable({}, BaseCharacterController)
1045DynamicThumbstick.__index = DynamicThumbstick
1046
1047function DynamicThumbstick.new()
1048 local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick)
1049
1050 self.humanoid = nil
1051
1052 self.tools = {}
1053 self.toolEquipped = nil
1054
1055 self.revertAutoJumpEnabledToFalse = false
1056
1057 self.moveTouchObject = nil
1058 self.moveTouchStartPosition = nil
1059
1060 self.startImage = nil
1061 self.endImage = nil
1062 self.middleImages = {}
1063
1064 self.startImageFadeTween = nil
1065 self.endImageFadeTween = nil
1066 self.middleImageFadeTweens = {}
1067
1068 self.isFirstTouch = true
1069
1070 self.isFollowStick = false
1071 self.thumbstickFrame = nil
1072 self.onTouchMovedConn = nil
1073 self.onTouchEndedConn = nil
1074 self.onTouchActivateConn = nil
1075 self.onRenderSteppedConn = nil
1076
1077 self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
1078 self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
1079 self.hasFadedBackgroundInPortrait = false
1080 self.hasFadedBackgroundInLandscape = false
1081
1082 self.tweenInAlphaStart = nil
1083 self.tweenOutAlphaStart = nil
1084
1085 -- If this module changes a player's humanoid's AutoJumpEnabled, it saves
1086 -- the previous state in this variable to revert to
1087 self.shouldRevertAutoJumpOnDisable = false
1088
1089 return self
1090end
1091
1092-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump
1093-- rather than sustained jumping. This is only to preserve the current behavior through the refactor.
1094function DynamicThumbstick:GetIsJumping()
1095 local wasJumping = self.isJumping
1096 self.isJumping = false
1097 return wasJumping
1098end
1099
1100function DynamicThumbstick:EnableAutoJump(enable)
1101 local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
1102 if humanoid then
1103 if enable then
1104 self.shouldRevertAutoJumpOnDisable = (humanoid.AutoJumpEnabled == false) and (Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice)
1105 humanoid.AutoJumpEnabled = true
1106 elseif self.shouldRevertAutoJumpOnDisable then
1107 humanoid.AutoJumpEnabled = false
1108 end
1109 end
1110end
1111
1112function DynamicThumbstick:Enable(enable, uiParentFrame)
1113 if enable == nil then return false end -- If nil, return false (invalid argument)
1114 enable = enable and true or false -- Force anything non-nil to boolean before comparison
1115 if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
1116
1117 if enable then
1118 -- Enable
1119 if not self.thumbstickFrame then
1120 self:Create(uiParentFrame)
1121 end
1122
1123 if Players.LocalPlayer.Character then
1124 self:OnCharacterAdded(Players.LocalPlayer.Character)
1125 else
1126 Players.LocalPlayer.CharacterAdded:Connect(function(char)
1127 self:OnCharacterAdded(char)
1128 end)
1129 end
1130 else
1131 -- Disable
1132 self:OnInputEnded() -- Cleanup
1133 end
1134
1135 self.enabled = enable
1136 self.thumbstickFrame.Visible = enable
1137end
1138
1139function DynamicThumbstick:OnCharacterAdded(char)
1140
1141 for _, child in ipairs(char:GetChildren()) do
1142 if child:IsA("Tool") then
1143 self.toolEquipped = child
1144 end
1145 end
1146
1147 char.ChildAdded:Connect(function(child)
1148 if child:IsA("Tool") then
1149 self.toolEquipped = child
1150 elseif child:IsA("Humanoid") then
1151 self:EnableAutoJump(true)
1152 end
1153
1154 end)
1155 char.ChildRemoved:Connect(function(child)
1156 if child == self.toolEquipped then
1157 self.toolEquipped = nil
1158 end
1159 end)
1160
1161 self.humanoid = char:FindFirstChildOfClass("Humanoid")
1162 if self.humanoid then
1163 self:EnableAutoJump(true)
1164 end
1165end
1166
1167-- Was called OnMoveTouchEnded in previous version
1168function DynamicThumbstick:OnInputEnded()
1169 self.moveTouchObject = nil
1170 self.moveVector = ZERO_VECTOR3
1171 self:FadeThumbstick(false)
1172 self.thumbstickFrame.Active = true
1173end
1174
1175function DynamicThumbstick:FadeThumbstick(visible)
1176 if not visible and self.moveTouchObject then
1177 return
1178 end
1179 if self.isFirstTouch then return end
1180
1181 if self.startImageFadeTween then
1182 self.startImageFadeTween:Cancel()
1183 end
1184 if self.endImageFadeTween then
1185 self.endImageFadeTween:Cancel()
1186 end
1187 for i = 1, #self.middleImages do
1188 if self. middleImageFadeTweens[i] then
1189 self.middleImageFadeTweens[i]:Cancel()
1190 end
1191 end
1192
1193 if visible then
1194 self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 })
1195 self.startImageFadeTween:Play()
1196
1197 self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 })
1198 self.endImageFadeTween:Play()
1199
1200 for i = 1, #self.middleImages do
1201 self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] })
1202 self.middleImageFadeTweens[i]:Play()
1203 end
1204 else
1205 self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
1206 self.startImageFadeTween:Play()
1207
1208 self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
1209 self.endImageFadeTween:Play()
1210
1211 for i = 1, #self.middleImages do
1212 self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
1213 self.middleImageFadeTweens[i]:Play()
1214 end
1215 end
1216end
1217
1218function DynamicThumbstick:FadeThumbstickFrame(fadeDuration, fadeRatio)
1219 self.fadeInAndOutHalfDuration = fadeDuration * 0.5
1220 self.fadeInAndOutBalance = fadeRatio
1221 self.tweenInAlphaStart = tick()
1222end
1223
1224function DynamicThumbstick:Create(parentFrame)
1225 if self.thumbstickFrame then
1226 self.thumbstickFrame:Destroy()
1227 self.thumbstickFrame = nil
1228 if self.onTouchMovedConn then
1229 self.onTouchMovedConn:Disconnect()
1230 self.onTouchMovedConn = nil
1231 end
1232 if self.onTouchEndedConn then
1233 self.onTouchEndedCon:Disconnect()
1234 self.onTouchEndedCon = nil
1235 end
1236 if self.onRenderSteppedConn then
1237 self.onRenderSteppedConn:Disconnect()
1238 self.onRenderSteppedConn = nil
1239 end
1240 if self.onTouchActivateConn then
1241 self.onTouchActivateConn:Disconnect()
1242 self.onTouchActivateConn = nil
1243 end
1244 end
1245
1246 local ThumbstickSize = 45
1247 local ThumbstickRingSize = 20
1248 local MiddleSize = 10
1249 local MiddleSpacing = MiddleSize + 4
1250 local RadiusOfDeadZone = 2
1251 local RadiusOfMaxSpeed = 20
1252
1253 local screenSize = parentFrame.AbsoluteSize
1254 local isBigScreen = math.min(screenSize.x, screenSize.y) > 500
1255 if isBigScreen then
1256 ThumbstickSize = ThumbstickSize * 2
1257 ThumbstickRingSize = ThumbstickRingSize * 2
1258 MiddleSize = MiddleSize * 2
1259 MiddleSpacing = MiddleSpacing * 2
1260 RadiusOfDeadZone = RadiusOfDeadZone * 2
1261 RadiusOfMaxSpeed = RadiusOfMaxSpeed * 2
1262 end
1263
1264 local function layoutThumbstickFrame(portraitMode)
1265 if portraitMode then
1266 self.thumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0)
1267 self.thumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0)
1268 else
1269 self.thumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0)
1270 self.thumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0)
1271 end
1272 end
1273
1274 self.thumbstickFrame = Instance.new("TextButton")
1275 self.thumbstickFrame.Text = ""
1276 self.thumbstickFrame.Name = "DynamicThumbstickFrame"
1277 self.thumbstickFrame.Visible = false
1278 self.thumbstickFrame.BackgroundTransparency = 1.0
1279 self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
1280 layoutThumbstickFrame(false)
1281
1282 self.startImage = Instance.new("ImageLabel")
1283 self.startImage.Name = "ThumbstickStart"
1284 self.startImage.Visible = true
1285 self.startImage.BackgroundTransparency = 1
1286 self.startImage.Image = TOUCH_CONTROLS_SHEET
1287 self.startImage.ImageRectOffset = Vector2.new(1,1)
1288 self.startImage.ImageRectSize = Vector2.new(144, 144)
1289 self.startImage.ImageColor3 = Color3.new(0, 0, 0)
1290 self.startImage.AnchorPoint = Vector2.new(0.5, 0.5)
1291 self.startImage.Position = UDim2.new(0, ThumbstickRingSize * 3.3, 1, -ThumbstickRingSize * 2.8)
1292 self.startImage.Size = UDim2.new(0, ThumbstickRingSize * 3.7, 0, ThumbstickRingSize * 3.7)
1293 self.startImage.ZIndex = 10
1294 self.startImage.Parent = self.thumbstickFrame
1295
1296 self.endImage = Instance.new("ImageLabel")
1297 self.endImage.Name = "ThumbstickEnd"
1298 self.endImage.Visible = true
1299 self.endImage.BackgroundTransparency = 1
1300 self.endImage.Image = TOUCH_CONTROLS_SHEET
1301 self.endImage.ImageRectOffset = Vector2.new(1,1)
1302 self.endImage.ImageRectSize = Vector2.new(144, 144)
1303 self.endImage.AnchorPoint = Vector2.new(0.5, 0.5)
1304 self.endImage.Position = self.startImage.Position
1305 self.endImage.Size = UDim2.new(0, ThumbstickSize * 0.8, 0, ThumbstickSize * 0.8)
1306 self.endImage.ZIndex = 10
1307 self.endImage.Parent = self.thumbstickFrame
1308
1309 for i = 1, NUM_MIDDLE_IMAGES do
1310 self.middleImages[i] = Instance.new("ImageLabel")
1311 self.middleImages[i].Name = "ThumbstickMiddle"
1312 self.middleImages[i].Visible = false
1313 self.middleImages[i].BackgroundTransparency = 1
1314 self.middleImages[i].Image = TOUCH_CONTROLS_SHEET
1315 self.middleImages[i].ImageRectOffset = Vector2.new(1,1)
1316 self.middleImages[i].ImageRectSize = Vector2.new(144, 144)
1317 self.middleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i]
1318 self.middleImages[i].AnchorPoint = Vector2.new(0.5, 0.5)
1319 self.middleImages[i].ZIndex = 9
1320 self.middleImages[i].Parent = self.thumbstickFrame
1321 end
1322
1323 local CameraChangedConn = nil
1324 local function onCurrentCameraChanged()
1325 if CameraChangedConn then
1326 CameraChangedConn:Disconnect()
1327 CameraChangedConn = nil
1328 end
1329 local newCamera = workspace.CurrentCamera
1330 if newCamera then
1331 local function onViewportSizeChanged()
1332 local size = newCamera.ViewportSize
1333 local portraitMode = size.X < size.Y
1334 layoutThumbstickFrame(portraitMode)
1335 end
1336 CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged)
1337 onViewportSizeChanged()
1338 end
1339 end
1340 workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged)
1341 if workspace.CurrentCamera then
1342 onCurrentCameraChanged()
1343 end
1344
1345 self.moveTouchStartPosition = nil
1346
1347 self.startImageFadeTween = nil
1348 self.endImageFadeTween = nil
1349 self.middleImageFadeTweens = {}
1350
1351 local function doMove(direction)
1352 local currentMoveVector = direction
1353
1354 -- Scaled Radial Dead Zone
1355 local inputAxisMagnitude = currentMoveVector.magnitude
1356 if inputAxisMagnitude < RadiusOfDeadZone then
1357 currentMoveVector = Vector3.new()
1358 else
1359 currentMoveVector = currentMoveVector.unit*(1 - math.max(0, (RadiusOfMaxSpeed - currentMoveVector.magnitude)/RadiusOfMaxSpeed))
1360 currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
1361 end
1362
1363 self.moveVector = currentMoveVector
1364 end
1365
1366 local function layoutMiddleImages(startPos, endPos)
1367 local startDist = (ThumbstickSize / 2) + MiddleSize
1368 local vector = endPos - startPos
1369 local distAvailable = vector.magnitude - (ThumbstickRingSize / 2) - MiddleSize
1370 local direction = vector.unit
1371
1372 local distNeeded = MiddleSpacing * NUM_MIDDLE_IMAGES
1373 local spacing = MiddleSpacing
1374
1375 if distNeeded < distAvailable then
1376 spacing = distAvailable / NUM_MIDDLE_IMAGES
1377 end
1378
1379 for i = 1, NUM_MIDDLE_IMAGES do
1380 local image = self.middleImages[i]
1381 local distWithout = startDist + (spacing * (i - 2))
1382 local currentDist = startDist + (spacing * (i - 1))
1383
1384 if distWithout < distAvailable then
1385 local pos = endPos - direction * currentDist
1386 local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1)
1387
1388 image.Visible = true
1389 image.Position = UDim2.new(0, pos.X, 0, pos.Y)
1390 image.Size = UDim2.new(0, MiddleSize * exposedFraction, 0, MiddleSize * exposedFraction)
1391 else
1392 image.Visible = false
1393 end
1394 end
1395 end
1396
1397 local function moveStick(pos)
1398 local startPos = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y) - self.thumbstickFrame.AbsolutePosition
1399 local endPos = Vector2.new(pos.X, pos.Y) - self.thumbstickFrame.AbsolutePosition
1400 self.endImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y)
1401 layoutMiddleImages(startPos, endPos)
1402 end
1403
1404 -- input connections
1405 self.thumbstickFrame.InputBegan:Connect(function(inputObject)
1406 if inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then
1407 return
1408 end
1409 if self.moveTouchObject then
1410 return
1411 end
1412
1413 if self.isFirstTouch then
1414 self.isFirstTouch = false
1415 local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0)
1416 TweenService:Create(self.startImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play()
1417 TweenService:Create(self.endImage, tweenInfo, {Size = UDim2.new(0, ThumbstickSize, 0, ThumbstickSize), ImageColor3 = Color3.new(0,0,0)}):Play()
1418 end
1419
1420 self.moveTouchObject = inputObject
1421 self.moveTouchStartPosition = inputObject.Position
1422 local startPosVec2 = Vector2.new(inputObject.Position.X - self.thumbstickFrame.AbsolutePosition.X, inputObject.Position.Y - self.thumbstickFrame.AbsolutePosition.Y)
1423
1424 self.startImage.Visible = true
1425 self.startImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y)
1426 self.endImage.Visible = true
1427 self.endImage.Position = self.startImage.Position
1428
1429 self:FadeThumbstick(true)
1430 moveStick(inputObject.Position)
1431
1432 if FADE_IN_OUT_BACKGROUND then
1433 local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
1434 local hasFadedBackgroundInOrientation = false
1435
1436 -- only fade in/out the background once per orientation
1437 if playerGui then
1438 if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
1439 playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then
1440 hasFadedBackgroundInOrientation = self.hasFadedBackgroundInLandscape
1441 self.hasFadedBackgroundInLandscape = true
1442 elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then
1443 hasFadedBackgroundInOrientation = self.hasFadedBackgroundInPortrait
1444 self.hasFadedBackgroundInPortrait = true
1445 end
1446 end
1447
1448 if not hasFadedBackgroundInOrientation then
1449 self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
1450 self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
1451 self.tweenInAlphaStart = tick()
1452 end
1453 end
1454 end)
1455
1456 self.onTouchMovedConn = UserInputService.TouchMoved:connect(function(inputObject)
1457 if inputObject == self.moveTouchObject then
1458 self.thumbstickFrame.Active = false
1459 local direction = Vector2.new(inputObject.Position.x - self.moveTouchStartPosition.x, inputObject.Position.y - self.moveTouchStartPosition.y)
1460 if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then
1461 doMove(direction)
1462 moveStick(inputObject.Position)
1463 end
1464 end
1465 end)
1466
1467 self.onRenderSteppedConn = RunService.RenderStepped:Connect(function()
1468 if self.tweenInAlphaStart ~= nil then
1469 local delta = tick() - self.tweenInAlphaStart
1470 local fadeInTime = (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
1471 self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeInTime, 1)
1472 if delta > fadeInTime then
1473 self.tweenOutAlphaStart = tick()
1474 self.tweenInAlphaStart = nil
1475 end
1476 elseif self.tweenOutAlphaStart ~= nil then
1477 local delta = tick() - self.tweenOutAlphaStart
1478 local fadeOutTime = (self.fadeInAndOutHalfDuration * 2) - (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
1479 self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA + FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeOutTime, 1)
1480 if delta > fadeOutTime then
1481 self.tweenOutAlphaStart = nil
1482 end
1483 end
1484 end)
1485
1486 self.onTouchEndedConn = UserInputService.TouchEnded:connect(function(inputObject)
1487 if inputObject == self.moveTouchObject then
1488 self:OnInputEnded()
1489 end
1490 end)
1491
1492 GuiService.MenuOpened:connect(function()
1493 if self.moveTouchObject then
1494 self:OnInputEnded()
1495 end
1496 end)
1497
1498 local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
1499 while not playerGui do
1500 Players.LocalPlayer.ChildAdded:wait()
1501 playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
1502 end
1503
1504 local playerGuiChangedConn = nil
1505 local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
1506 playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight
1507
1508 local function longShowBackground()
1509 self.fadeInAndOutHalfDuration = 2.5
1510 self.fadeInAndOutBalance = 0.05
1511 self.tweenInAlphaStart = tick()
1512 end
1513
1514 playerGuiChangedConn = playerGui.Changed:connect(function(prop)
1515 if prop == "CurrentScreenOrientation" then
1516 if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or
1517 (not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then
1518
1519 playerGuiChangedConn:disconnect()
1520 longShowBackground()
1521
1522 if originalScreenOrientationWasLandscape then
1523 self.hasFadedBackgroundInPortrait = true
1524 else
1525 self.hasFadedBackgroundInLandscape = true
1526 end
1527 end
1528 end
1529 end)
1530
1531 self.thumbstickFrame.Parent = parentFrame
1532
1533 spawn(function()
1534 if game:IsLoaded() then
1535 longShowBackground()
1536 else
1537 game.Loaded:wait()
1538 longShowBackground()
1539 end
1540 end)
1541end
1542
1543return DynamicThumbstick
1544
1545
1546--[[
1547 -- Original By Kip Turner, Copyright Roblox 2014
1548 -- Updated by Garnold to utilize the new PathfindingService API, 2017
1549 -- 2018 PlayerScripts Update - AllYourBlox
1550--]]
1551
1552--[[ Roblox Services ]]--
1553local UserInputService = game:GetService("UserInputService")
1554local ContextActionService = game:GetService("ContextActionService")
1555local PathfindingService = game:GetService("PathfindingService")
1556local Players = game:GetService("Players")
1557local RunService = game:GetService("RunService")
1558local DebrisService = game:GetService('Debris')
1559local ReplicatedStorage = game:GetService('ReplicatedStorage')
1560local TweenService = game:GetService("TweenService")
1561
1562--[[ Constants ]]--
1563local ZERO_VECTOR3 = Vector3.new(0,0,0)
1564 local movementKeys = {
1565 [Enum.KeyCode.W] = true;
1566 [Enum.KeyCode.A] = true;
1567 [Enum.KeyCode.S] = true;
1568 [Enum.KeyCode.D] = true;
1569 [Enum.KeyCode.Up] = true;
1570 [Enum.KeyCode.Down] = true;
1571 }
1572
1573local FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess, FFlagUserNavigationClickToMoveSkipPassedWaypointsResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveSkipPassedWaypoints") end)
1574local FFlagUserNavigationClickToMoveSkipPassedWaypoints = FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess and FFlagUserNavigationClickToMoveSkipPassedWaypointsResult
1575
1576local Player = Players.LocalPlayer
1577local PlayerScripts = Player.PlayerScripts
1578
1579local TouchJump = nil
1580
1581local SHOW_PATH = true
1582
1583local RayCastIgnoreList = workspace.FindPartOnRayWithIgnoreList
1584
1585local CurrentSeatPart = nil
1586local DrivingTo = nil
1587
1588local XZ_VECTOR3 = Vector3.new(1,0,1)
1589local ZERO_VECTOR3 = Vector3.new(0,0,0)
1590local ZERO_VECTOR2 = Vector2.new(0,0)
1591
1592local BindableEvent_OnFailStateChanged = nil
1593if UserInputService.TouchEnabled then
1594-- BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged()
1595end
1596
1597--------------------------UTIL LIBRARY-------------------------------
1598local Utility = {}
1599do
1600 local function ViewSizeX()
1601 local camera = workspace.CurrentCamera
1602 local x = camera and camera.ViewportSize.X or 0
1603 local y = camera and camera.ViewportSize.Y or 0
1604 if x == 0 then
1605 return 1024
1606 else
1607 if x > y then
1608 return x
1609 else
1610 return y
1611 end
1612 end
1613 end
1614 Utility.ViewSizeX = ViewSizeX
1615
1616 local function ViewSizeY()
1617 local camera = workspace.CurrentCamera
1618 local x = camera and camera.ViewportSize.X or 0
1619 local y = camera and camera.ViewportSize.Y or 0
1620 if y == 0 then
1621 return 768
1622 else
1623 if x > y then
1624 return y
1625 else
1626 return x
1627 end
1628 end
1629 end
1630 Utility.ViewSizeY = ViewSizeY
1631
1632 local function FindCharacterAncestor(part)
1633 if part then
1634 local humanoid = part:FindFirstChild("Humanoid")
1635 if humanoid then
1636 return part, humanoid
1637 else
1638 return FindCharacterAncestor(part.Parent)
1639 end
1640 end
1641 end
1642 Utility.FindCharacterAncestor = FindCharacterAncestor
1643
1644 local function Raycast(ray, ignoreNonCollidable, ignoreList)
1645 local ignoreList = ignoreList or {}
1646 local hitPart, hitPos, hitNorm, hitMat = RayCastIgnoreList(workspace, ray, ignoreList)
1647 if hitPart then
1648 if ignoreNonCollidable and hitPart.CanCollide == false then
1649 table.insert(ignoreList, hitPart)
1650 return Raycast(ray, ignoreNonCollidable, ignoreList)
1651 end
1652 return hitPart, hitPos, hitNorm, hitMat
1653 end
1654 return nil, nil
1655 end
1656 Utility.Raycast = Raycast
1657
1658 local function AveragePoints(positions)
1659 local avgPos = ZERO_VECTOR2
1660 if #positions > 0 then
1661 for i = 1, #positions do
1662 avgPos = avgPos + positions[i]
1663 end
1664 avgPos = avgPos / #positions
1665 end
1666 return avgPos
1667 end
1668 Utility.AveragePoints = AveragePoints
1669end
1670
1671local humanoidCache = {}
1672local function findPlayerHumanoid(player)
1673 local character = player and player.Character
1674 if character then
1675 local resultHumanoid = humanoidCache[player]
1676 if resultHumanoid and resultHumanoid.Parent == character then
1677 return resultHumanoid
1678 else
1679 humanoidCache[player] = nil -- Bust Old Cache
1680 local humanoid = character:FindFirstChildOfClass("Humanoid")
1681 if humanoid then
1682 humanoidCache[player] = humanoid
1683 end
1684 return humanoid
1685 end
1686 end
1687end
1688
1689--------------------------CHARACTER CONTROL-------------------------------
1690local CurrentIgnoreList
1691
1692local function GetCharacter()
1693 return Player and Player.Character
1694end
1695
1696local function GetTorso()
1697 local humanoid = findPlayerHumanoid(Player)
1698 return humanoid and humanoid.RootPart
1699end
1700
1701local function getIgnoreList()
1702 if CurrentIgnoreList then
1703 return CurrentIgnoreList
1704 end
1705 CurrentIgnoreList = {}
1706 table.insert(CurrentIgnoreList, GetCharacter())
1707 return CurrentIgnoreList
1708end
1709
1710-----------------------------------PATHER--------------------------------------
1711
1712local popupAdornee
1713local function getPopupAdorneePart()
1714 --Handle the case of the adornee part getting deleted (camera changed, maybe)
1715 if popupAdornee and not popupAdornee.Parent then
1716 popupAdornee = nil
1717 end
1718
1719 --If the adornee doesn't exist yet, create it
1720 if not popupAdornee then
1721 popupAdornee = Instance.new("Part")
1722 popupAdornee.Name = "ClickToMovePopupAdornee"
1723 popupAdornee.Transparency = 1
1724 popupAdornee.CanCollide = false
1725 popupAdornee.Anchored = true
1726 popupAdornee.Size = Vector3.new(2, 2, 2)
1727 popupAdornee.CFrame = CFrame.new()
1728
1729 popupAdornee.Parent = workspace.CurrentCamera
1730 end
1731
1732 return popupAdornee
1733end
1734
1735local activePopups = {}
1736local function createNewPopup(popupType)
1737 local newModel = Instance.new("ImageHandleAdornment")
1738
1739 newModel.AlwaysOnTop = false
1740 newModel.Transparency = 1
1741 newModel.Size = ZERO_VECTOR2
1742 newModel.SizeRelativeOffset = ZERO_VECTOR3
1743 newModel.Image = "rbxasset://textures/ui/move.png"
1744 newModel.ZIndex = 20
1745
1746 local radius = 0
1747 if popupType == "DestinationPopup" then
1748 newModel.Color3 = Color3.fromRGB(0, 175, 255)
1749 radius = 1.25
1750 elseif popupType == "DirectWalkPopup" then
1751 newModel.Color3 = Color3.fromRGB(0, 175, 255)
1752 radius = 1.25
1753 elseif popupType == "FailurePopup" then
1754 newModel.Color3 = Color3.fromRGB(255, 100, 100)
1755 radius = 1.25
1756 elseif popupType == "PatherPopup" then
1757 newModel.Color3 = Color3.fromRGB(255, 255, 255)
1758 radius = 1
1759 newModel.ZIndex = 10
1760 end
1761 newModel.Size = Vector2.new(5, 0.1) * radius
1762
1763 local dataStructure = {}
1764 dataStructure.Model = newModel
1765
1766 activePopups[#activePopups + 1] = newModel
1767
1768 function dataStructure:TweenIn()
1769 local tweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out)
1770 local tween1 = TweenService:Create(newModel, tweenInfo, { Size = Vector2.new(2,2) * radius })
1771 tween1:Play()
1772 TweenService:Create(newModel, TweenInfo.new(0.25, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 0, false, 0.1), { Transparency = 0, SizeRelativeOffset = Vector3.new(0, radius * 1.5, 0) }):Play()
1773 return tween1
1774 end
1775
1776 function dataStructure:TweenOut()
1777 local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
1778 local tween1 = TweenService:Create(newModel, tweenInfo, { Size = ZERO_VECTOR2 })
1779 tween1:Play()
1780
1781 coroutine.wrap(function()
1782 tween1.Completed:Wait()
1783
1784 for i = 1, #activePopups do
1785 if activePopups[i] == newModel then
1786 table.remove(activePopups, i)
1787 break
1788 end
1789 end
1790 end)()
1791 return tween1
1792 end
1793
1794 function dataStructure:Place(position, dest)
1795 -- place the model at position
1796 if not self.Model.Parent then
1797 local popupAdorneePart = getPopupAdorneePart()
1798 self.Model.Parent = popupAdorneePart
1799 self.Model.Adornee = popupAdorneePart
1800
1801 --Start the 10-stud long ray 2.5 studs above where the tap happened and point straight down to try to find
1802 --the actual ground position.
1803 local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
1804 local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, { workspace.CurrentCamera, Player.Character })
1805
1806 self.Model.CFrame = CFrame.new(hitPoint) + Vector3.new(0, -radius,0)
1807 end
1808 end
1809
1810 return dataStructure
1811end
1812
1813local function createPopupPath(points, numCircles)
1814 -- creates a path with the provided points, using the path and number of circles provided
1815 local popups = {}
1816 local stopTraversing = false
1817
1818 local function killPopup(i)
1819 -- kill all popups before and at i
1820 for iter, v in pairs(popups) do
1821 if iter <= i then
1822 local tween = v:TweenOut()
1823 spawn(function()
1824 tween.Completed:Wait()
1825 v.Model:Destroy()
1826 end)
1827 popups[iter] = nil
1828 end
1829 end
1830 end
1831
1832 local function stopFunction()
1833 stopTraversing = true
1834 killPopup(#points)
1835 end
1836
1837 spawn(function()
1838 for i = 1, #points do
1839 if stopTraversing then
1840 break
1841 end
1842
1843 local includeWaypoint = i % numCircles == 0
1844 and i < #points
1845 and (points[#points].Position - points[i].Position).magnitude > 4
1846 if includeWaypoint then
1847 local popup = createNewPopup("PatherPopup")
1848 popups[i] = popup
1849 local nextPopup = points[i+1]
1850 popup:Place(points[i].Position, nextPopup and nextPopup.Position or points[#points].Position)
1851 local tween = popup:TweenIn()
1852 wait(0.2)
1853 end
1854 end
1855 end)
1856
1857 return stopFunction, killPopup
1858end
1859
1860local function Pather(character, endPoint, surfaceNormal)
1861 local this = {}
1862
1863 this.Cancelled = false
1864 this.Started = false
1865
1866 this.Finished = Instance.new("BindableEvent")
1867 this.PathFailed = Instance.new("BindableEvent")
1868
1869 this.PathComputing = false
1870 this.PathComputed = false
1871
1872 this.TargetPoint = endPoint
1873 this.TargetSurfaceNormal = surfaceNormal
1874
1875 this.DiedConn = nil
1876 this.SeatedConn = nil
1877 this.MoveToConn = nil
1878 this.BlockedConn = nil
1879 this.CurrentPoint = 0
1880
1881 function this:Cleanup()
1882 if this.stopTraverseFunc then
1883 this.stopTraverseFunc()
1884 this.stopTraverseFunc = nil
1885 end
1886
1887 if this.MoveToConn then
1888 this.MoveToConn:Disconnect()
1889 this.MoveToConn = nil
1890 end
1891
1892 if this.BlockedConn then
1893 this.BlockedConn:Disconnect()
1894 this.BlockedConn = nil
1895 end
1896
1897 if this.DiedConn then
1898 this.DiedConn:Disconnect()
1899 this.DiedConn = nil
1900 end
1901
1902 if this.SeatedConn then
1903 this.SeatedConn:Disconnect()
1904 this.SeatedConn = nil
1905 end
1906
1907 this.humanoid = nil
1908 end
1909
1910 function this:Cancel()
1911 this.Cancelled = true
1912 this:Cleanup()
1913 end
1914
1915 function this:OnPathInterrupted()
1916 -- Stop moving
1917 this.Cancelled = true
1918 this:OnPointReached(false)
1919 end
1920
1921 function this:ComputePath()
1922 local humanoid = findPlayerHumanoid(Player)
1923 local torso = humanoid and humanoid.Torso
1924 local success = false
1925 if torso then
1926 if this.PathComputed or this.PathComputing then return end
1927 this.PathComputing = true
1928 success = pcall(function()
1929 this.pathResult = PathfindingService:FindPathAsync(torso.CFrame.p, this.TargetPoint)
1930 end)
1931 this.pointList = this.pathResult and this.pathResult:GetWaypoints()
1932 if this.pathResult then
1933 this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end)
1934 end
1935 this.PathComputing = false
1936 this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false
1937 end
1938 return true
1939 end
1940
1941 function this:IsValidPath()
1942 if not this.pathResult then
1943 this:ComputePath()
1944 end
1945 return this.pathResult.Status == Enum.PathStatus.Success
1946 end
1947
1948 this.Recomputing = false
1949 function this:OnPathBlocked(blockedWaypointIdx)
1950 local pathBlocked = blockedWaypointIdx >= this.CurrentPoint
1951 if not pathBlocked or this.Recomputing then
1952 return
1953 end
1954
1955 this.Recomputing = true
1956
1957 if this.stopTraverseFunc then
1958 this.stopTraverseFunc()
1959 this.stopTraverseFunc = nil
1960 end
1961
1962 this.pathResult:ComputeAsync(this.humanoid.Torso.CFrame.p, this.TargetPoint)
1963 this.pointList = this.pathResult:GetWaypoints()
1964 this.PathComputed = this.pathResult and this.pathResult.Status == Enum.PathStatus.Success or false
1965
1966 if SHOW_PATH then
1967 this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4, true)
1968 end
1969 if this.PathComputed then
1970 this.humanoid = findPlayerHumanoid(Player)
1971 this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
1972 this:OnPointReached(true) -- Move to first point
1973 else
1974 this.PathFailed:Fire()
1975 this:Cleanup()
1976 end
1977
1978 this.Recomputing = false
1979 end
1980
1981 function this:OnPointReached(reached)
1982
1983 if reached and not this.Cancelled then
1984
1985 local nextWaypointIdx = this.CurrentPoint + 1
1986
1987 if nextWaypointIdx > #this.pointList then
1988 -- End of path reached
1989 if this.stopTraverseFunc then
1990 this.stopTraverseFunc()
1991 end
1992 this.Finished:Fire()
1993 this:Cleanup()
1994 else
1995 local currentWaypoint = this.pointList[this.CurrentPoint]
1996 local nextWaypoint = this.pointList[nextWaypointIdx]
1997
1998 -- If airborne, only allow to keep moving
1999 -- if nextWaypoint.Action ~= Jump, or path mantains a direction
2000 -- Otherwise, wait until the humanoid gets to the ground
2001 local currentState = this.humanoid:GetState()
2002 local isInAir = currentState == Enum.HumanoidStateType.FallingDown
2003 or currentState == Enum.HumanoidStateType.Freefall
2004 or currentState == Enum.HumanoidStateType.Jumping
2005
2006 if isInAir then
2007 local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump
2008 if not shouldWaitForGround and this.CurrentPoint > 1 then
2009 local prevWaypoint = this.pointList[this.CurrentPoint - 1]
2010
2011 local prevDir = currentWaypoint.Position - prevWaypoint.Position
2012 local currDir = nextWaypoint.Position - currentWaypoint.Position
2013
2014 local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit
2015 local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit
2016
2017 local THRESHOLD_COS = 0.996 -- ~cos(5 degrees)
2018 shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS
2019 end
2020
2021 if shouldWaitForGround then
2022 this.humanoid.FreeFalling:Wait()
2023
2024 -- Give time to the humanoid's state to change
2025 -- Otherwise, the jump flag in Humanoid
2026 -- will be reset by the state change
2027 wait(0.1)
2028 end
2029 end
2030
2031 -- Move to the next point
2032 if FFlagUserNavigationClickToMoveSkipPassedWaypoints then
2033 -- First, check if we already passed the next point
2034 local nextWaypointAlreadyReached
2035 -- 1) Build plane (normal is from next waypoint towards current one (provided the two waypoints are not at the same location); location is at next waypoint)
2036 local planeNormal = currentWaypoint.Position - nextWaypoint.Position
2037 if planeNormal.Magnitude > 0.000001 then
2038 planeNormal = planeNormal.Unit
2039 local planeDistance = planeNormal:Dot(nextWaypoint.Position)
2040 -- 2) Find current Humanoid position
2041 local humanoidPosition = this.humanoid.RootPart.Position - Vector3.new(0, 0.5 * this.humanoid.RootPart.Size.y + this.humanoid.HipHeight, 0)
2042 -- 3) Compute distance from plane
2043 local dist = planeNormal:Dot(humanoidPosition) - planeDistance
2044 -- 4) If we are less then a stud in front of the plane or if we are behing the plane, we consider we reached it
2045 nextWaypointAlreadyReached = dist < 1.0
2046 else
2047 -- Next waypoint is the same as current waypoint so we reached it as well
2048 nextWaypointAlreadyReached = true
2049 end
2050
2051 -- Prepare for next point
2052 if this.setPointFunc then
2053 this.setPointFunc(nextWaypointIdx)
2054 end
2055 this.CurrentPoint = nextWaypointIdx
2056
2057 -- Either callback here right away if next waypoint is already passed
2058 -- Otherwise, ask the Humanoid to MoveTo
2059 if nextWaypointAlreadyReached then
2060 this:OnPointReached(true)
2061 else
2062 if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
2063 this.humanoid.Jump = true
2064 end
2065 this.humanoid:MoveTo(nextWaypoint.Position)
2066 end
2067 else
2068 if this.setPointFunc then
2069 this.setPointFunc(nextWaypointIdx)
2070 end
2071
2072 if nextWaypoint.Action == Enum.PathWaypointAction.Jump then
2073 this.humanoid.Jump = true
2074 end
2075 this.humanoid:MoveTo(nextWaypoint.Position)
2076
2077 this.CurrentPoint = nextWaypointIdx
2078 end
2079 end
2080 else
2081 this.PathFailed:Fire()
2082 this:Cleanup()
2083 end
2084 end
2085
2086 function this:Start()
2087 if CurrentSeatPart then
2088 return
2089 end
2090
2091 this.humanoid = findPlayerHumanoid(Player)
2092 if not this.humanoid then
2093 this.PathFailed:Fire()
2094 return
2095 end
2096
2097 if this.Started then return end
2098 this.Started = true
2099
2100 if SHOW_PATH then
2101 -- choose whichever one Mike likes best
2102 this.stopTraverseFunc, this.setPointFunc = createPopupPath(this.pointList, 4)
2103 end
2104
2105 if #this.pointList > 0 then
2106 this.SeatedConn = this.humanoid.Seated:Connect(function(reached) this:OnPathInterrupted() end)
2107 this.DiedConn = this.humanoid.Died:Connect(function(reached) this:OnPathInterrupted() end)
2108 this.MoveToConn = this.humanoid.MoveToFinished:Connect(function(reached) this:OnPointReached(reached) end)
2109
2110 this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it.
2111 this:OnPointReached(true) -- Move to first point
2112 else
2113 this.PathFailed:Fire()
2114 if this.stopTraverseFunc then
2115 this.stopTraverseFunc()
2116 end
2117 end
2118 end
2119
2120 this:ComputePath()
2121 if not this.PathComputed then
2122 -- set the end point towards the camera and raycasted towards the ground in case we hit a wall
2123 local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5
2124 local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50)
2125 local newHitPart, newHitPos = RayCastIgnoreList(workspace, ray, getIgnoreList())
2126 if newHitPart then
2127 this.TargetPoint = newHitPos
2128 end
2129 -- try again
2130 this:ComputePath()
2131 end
2132
2133 return this
2134end
2135
2136-------------------------------------------------------------------------
2137
2138local function IsInBottomLeft(pt)
2139 local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250)
2140 local joystickWidth = joystickHeight
2141 return pt.X <= joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight
2142end
2143
2144local function IsInBottomRight(pt)
2145 local joystickHeight = math.min(Utility.ViewSizeY() * 0.33, 250)
2146 local joystickWidth = joystickHeight
2147 return pt.X >= Utility.ViewSizeX() - joystickWidth and pt.Y > Utility.ViewSizeY() - joystickHeight
2148end
2149
2150local function CheckAlive(character)
2151 local humanoid = findPlayerHumanoid(Player)
2152 return humanoid ~= nil and humanoid.Health > 0
2153end
2154
2155local function GetEquippedTool(character)
2156 if character ~= nil then
2157 for _, child in pairs(character:GetChildren()) do
2158 if child:IsA('Tool') then
2159 return child
2160 end
2161 end
2162 end
2163end
2164
2165local ExistingPather = nil
2166local ExistingIndicator = nil
2167local PathCompleteListener = nil
2168local PathFailedListener = nil
2169
2170local function CleanupPath()
2171 DrivingTo = nil
2172 if ExistingPather then
2173 ExistingPather:Cancel()
2174 end
2175 if PathCompleteListener then
2176 PathCompleteListener:Disconnect()
2177 PathCompleteListener = nil
2178 end
2179 if PathFailedListener then
2180 PathFailedListener:Disconnect()
2181 PathFailedListener = nil
2182 end
2183 if ExistingIndicator then
2184 local obj = ExistingIndicator
2185 local tween = obj:TweenOut()
2186 local tweenCompleteEvent = nil
2187 tweenCompleteEvent = tween.Completed:connect(function()
2188 tweenCompleteEvent:Disconnect()
2189 obj.Model:Destroy()
2190 end)
2191 ExistingIndicator = nil
2192 end
2193end
2194
2195local function getExtentsSize(Parts)
2196 local maxX,maxY,maxZ = -math.huge,-math.huge,-math.huge
2197 local minX,minY,minZ = math.huge,math.huge,math.huge
2198 for i = 1, #Parts do
2199 maxX,maxY,maxZ = math.max(maxX, Parts[i].Position.X), math.max(maxY, Parts[i].Position.Y), math.max(maxZ, Parts[i].Position.Z)
2200 minX,minY,minZ = math.min(minX, Parts[i].Position.X), math.min(minY, Parts[i].Position.Y), math.min(minZ, Parts[i].Position.Z)
2201 end
2202 return Region3.new(Vector3.new(minX, minY, minZ), Vector3.new(maxX, maxY, maxZ))
2203end
2204
2205local function inExtents(Extents, Position)
2206 if Position.X < (Extents.CFrame.p.X - Extents.Size.X/2) or Position.X > (Extents.CFrame.p.X + Extents.Size.X/2) then
2207 return false
2208 end
2209 if Position.Z < (Extents.CFrame.p.Z - Extents.Size.Z/2) or Position.Z > (Extents.CFrame.p.Z + Extents.Size.Z/2) then
2210 return false
2211 end
2212 --ignoring Y for now
2213 return true
2214end
2215
2216local function showQuickPopupAsync(position, popupType)
2217 local popup = createNewPopup(popupType)
2218 popup:Place(position, Vector3.new(0,position.y,0))
2219 local tweenIn = popup:TweenIn()
2220 tweenIn.Completed:Wait()
2221 local tweenOut = popup:TweenOut()
2222 tweenOut.Completed:Wait()
2223 popup.Model:Destroy()
2224 popup = nil
2225end
2226
2227local FailCount = 0
2228local function OnTap(tapPositions, goToPoint)
2229 -- Good to remember if this is the latest tap event
2230 local camera = workspace.CurrentCamera
2231 local character = Player.Character
2232
2233 if not CheckAlive(character) then return end
2234
2235 -- This is a path tap position
2236 if #tapPositions == 1 or goToPoint then
2237 if camera then
2238 local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y)
2239 local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000)
2240
2241 -- inivisicam stuff
2242 local initIgnore = getIgnoreList()
2243 local invisicamParts = {} --InvisicamModule and InvisicamModule:GetObscuredParts() or {}
2244 local ignoreTab = {}
2245
2246 -- add to the ignore list
2247 for i, v in pairs(invisicamParts) do
2248 ignoreTab[#ignoreTab+1] = i
2249 end
2250 for i = 1, #initIgnore do
2251 ignoreTab[#ignoreTab+1] = initIgnore[i]
2252 end
2253 --
2254 local myHumanoid = findPlayerHumanoid(Player)
2255 local hitPart, hitPt, hitNormal, hitMat = Utility.Raycast(ray, true, ignoreTab)
2256
2257 local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart)
2258 local torso = GetTorso()
2259 local startPos = torso.CFrame.p
2260 if goToPoint then
2261 hitPt = goToPoint
2262 hitChar = nil
2263 end
2264 if hitChar and hitHumanoid and hitHumanoid.RootPart and (hitHumanoid.Torso.CFrame.p - torso.CFrame.p).magnitude < 7 then
2265 CleanupPath()
2266
2267 if myHumanoid then
2268 myHumanoid:MoveTo(hitPt)
2269 end
2270 -- Do shoot
2271 local currentWeapon = GetEquippedTool(character)
2272 if currentWeapon then
2273 currentWeapon:Activate()
2274 LastFired = tick()
2275 end
2276 elseif hitPt and character and not CurrentSeatPart then
2277 local thisPather = Pather(character, hitPt, hitNormal)
2278 if thisPather:IsValidPath() then
2279 FailCount = 0
2280
2281 thisPather:Start()
2282 if BindableEvent_OnFailStateChanged then
2283 BindableEvent_OnFailStateChanged:Fire(false)
2284 end
2285 CleanupPath()
2286
2287 local destinationPopup = createNewPopup("DestinationPopup")
2288 destinationPopup:Place(hitPt, Vector3.new(0,hitPt.y,0))
2289 local failurePopup = createNewPopup("FailurePopup")
2290 local currentTween = destinationPopup:TweenIn()
2291
2292
2293 ExistingPather = thisPather
2294 ExistingIndicator = destinationPopup
2295
2296 PathCompleteListener = thisPather.Finished.Event:Connect(function()
2297 if destinationPopup then
2298 if ExistingIndicator == destinationPopup then
2299 ExistingIndicator = nil
2300 end
2301 local tween = destinationPopup:TweenOut()
2302 local tweenCompleteEvent = nil
2303 tweenCompleteEvent = tween.Completed:Connect(function()
2304 tweenCompleteEvent:Disconnect()
2305 destinationPopup.Model:Destroy()
2306 destinationPopup = nil
2307 end)
2308 end
2309 if hitChar then
2310 local humanoid = findPlayerHumanoid(Player)
2311 local currentWeapon = GetEquippedTool(character)
2312 if currentWeapon then
2313 currentWeapon:Activate()
2314 LastFired = tick()
2315 end
2316 if humanoid then
2317 humanoid:MoveTo(hitPt)
2318 end
2319 end
2320 end)
2321 PathFailedListener = thisPather.PathFailed.Event:Connect(function()
2322 CleanupPath()
2323 if failurePopup then
2324 failurePopup:Place(hitPt, Vector3.new(0,hitPt.y,0))
2325 local failTweenIn = failurePopup:TweenIn()
2326 failTweenIn.Completed:Wait()
2327 local failTweenOut = failurePopup:TweenOut()
2328 failTweenOut.Completed:Wait()
2329 failurePopup.Model:Destroy()
2330 failurePopup = nil
2331 end
2332 end)
2333 else
2334 if hitPt then
2335 -- Feedback here for when we don't have a good path
2336 local foundDirectPath = false
2337 if (hitPt-startPos).Magnitude < 25 and (startPos.y-hitPt.y > -3) then
2338 -- move directly here
2339 if myHumanoid then
2340 if myHumanoid.Sit then
2341 myHumanoid.Jump = true
2342 end
2343 myHumanoid:MoveTo(hitPt)
2344 foundDirectPath = true
2345 end
2346 end
2347
2348 coroutine.wrap(showQuickPopupAsync)(hitPt, foundDirectPath and "DirectWalkPopup" or "FailurePopup")
2349 end
2350 end
2351 elseif hitPt and character and CurrentSeatPart then
2352 local destinationPopup = createNewPopup("DestinationPopup")
2353 ExistingIndicator = destinationPopup
2354 destinationPopup:Place(hitPt, Vector3.new(0,hitPt.y,0))
2355 destinationPopup:TweenIn()
2356
2357 DrivingTo = hitPt
2358 local ConnectedParts = CurrentSeatPart:GetConnectedParts(true)
2359
2360 while wait() do
2361 if CurrentSeatPart and ExistingIndicator == destinationPopup then
2362 local ExtentsSize = getExtentsSize(ConnectedParts)
2363 if inExtents(ExtentsSize, hitPt) then
2364 local popup = destinationPopup
2365 spawn(function()
2366 local tweenOut = popup:TweenOut()
2367 tweenOut.Completed:Wait()
2368 popup.Model:Destroy()
2369 end)
2370 destinationPopup = nil
2371 DrivingTo = nil
2372 break
2373 end
2374 else
2375 if CurrentSeatPart == nil and destinationPopup == ExistingIndicator then
2376 DrivingTo = nil
2377 OnTap(tapPositions, hitPt)
2378 end
2379 local popup = destinationPopup
2380 spawn(function()
2381 local tweenOut = popup:TweenOut()
2382 tweenOut.Completed:Wait()
2383 popup.Model:Destroy()
2384 end)
2385 destinationPopup = nil
2386 break
2387 end
2388 end
2389 end
2390 end
2391 elseif #tapPositions >= 2 then
2392 if camera then
2393 -- Do shoot
2394 local avgPoint = Utility.AveragePoints(tapPositions)
2395 local unitRay = camera:ScreenPointToRay(avgPoint.x, avgPoint.y)
2396 local currentWeapon = GetEquippedTool(character)
2397 if currentWeapon then
2398 currentWeapon:Activate()
2399 LastFired = tick()
2400 end
2401 end
2402 end
2403end
2404
2405local function IsFinite(num)
2406 return num == num and num ~= 1/0 and num ~= -1/0
2407end
2408
2409local function findAngleBetweenXZVectors(vec2, vec1)
2410 return math.atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
2411end
2412
2413local function DisconnectEvent(event)
2414 if event then
2415 event:Disconnect()
2416 end
2417end
2418
2419--[[ The ClickToMove Controller Class ]]--
2420local KeyboardController = require(script.Parent:WaitForChild("Keyboard"))
2421local ClickToMove = setmetatable({}, KeyboardController)
2422ClickToMove.__index = ClickToMove
2423
2424function ClickToMove.new(CONTROL_ACTION_PRIORITY)
2425 local self = setmetatable(KeyboardController.new(CONTROL_ACTION_PRIORITY), ClickToMove)
2426
2427 self.fingerTouches = {}
2428 self.numUnsunkTouches = 0
2429 -- PC simulation
2430 self.mouse1Down = tick()
2431 self.mouse1DownPos = Vector2.new()
2432 self.mouse2DownTime = tick()
2433 self.mouse2DownPos = Vector2.new()
2434 self.mouse2UpTime = tick()
2435
2436
2437
2438 self.tapConn = nil
2439 self.inputBeganConn = nil
2440 self.inputChangedConn = nil
2441 self.inputEndedConn = nil
2442 self.humanoidDiedConn = nil
2443 self.characterChildAddedConn = nil
2444 self.onCharacterAddedConn = nil
2445 self.characterChildRemovedConn = nil
2446 self.renderSteppedConn = nil
2447 self.humanoidSeatedConn = nil
2448
2449 self.running = false
2450
2451 self.wasdEnabled = false
2452
2453 return self
2454end
2455
2456function ClickToMove:DisconnectEvents()
2457 DisconnectEvent(self.tapConn)
2458 DisconnectEvent(self.inputBeganConn)
2459 DisconnectEvent(self.inputChangedConn)
2460 DisconnectEvent(self.inputEndedConn)
2461 DisconnectEvent(self.humanoidDiedConn)
2462 DisconnectEvent(self.characterChildAddedConn)
2463 DisconnectEvent(self.onCharacterAddedConn)
2464 DisconnectEvent(self.renderSteppedConn)
2465 DisconnectEvent(self.characterChildRemovedConn)
2466
2467 -- TODO: Resolve with ControlScript handling of seating for vehicles
2468 DisconnectEvent(self.humanoidSeatedConn)
2469
2470 RunService:UnbindFromRenderStep("ClickToMoveRenderUpdate")
2471end
2472
2473function ClickToMove:OnTouchBegan(input, processed)
2474 if self.fingerTouches[input] == nil and not processed then
2475 self.numUnsunkTouches = self.numUnsunkTouches + 1
2476 end
2477 self.fingerTouches[input] = processed
2478end
2479
2480function ClickToMove:OnTouchChanged(input, processed)
2481 if self.fingerTouches[input] == nil then
2482 self.fingerTouches[input] = processed
2483 if not processed then
2484 self.numUnsunkTouches = self.numUnsunkTouches + 1
2485 end
2486 end
2487end
2488
2489function ClickToMove:OnTouchEnded(input, processed)
2490 if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
2491 self.numUnsunkTouches = self.numUnsunkTouches - 1
2492 end
2493 self.fingerTouches[input] = nil
2494end
2495
2496
2497function ClickToMove:OnCharacterAdded(character)
2498 self:DisconnectEvents()
2499
2500 self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
2501 if input.UserInputType == Enum.UserInputType.Touch then
2502 self:OnTouchBegan(input, processed)
2503
2504 -- Give back controls when they tap both sticks
2505 local wasInBottomLeft = IsInBottomLeft(input.Position)
2506 local wasInBottomRight = IsInBottomRight(input.Position)
2507 if wasInBottomRight or wasInBottomLeft then
2508 for otherInput, _ in pairs(self.fingerTouches) do
2509 if otherInput ~= input then
2510 local otherInputInLeft = IsInBottomLeft(otherInput.Position)
2511 local otherInputInRight = IsInBottomRight(otherInput.Position)
2512 if otherInput.UserInputState ~= Enum.UserInputState.End and ((wasInBottomLeft and otherInputInRight) or (wasInBottomRight and otherInputInLeft)) then
2513 if BindableEvent_OnFailStateChanged then
2514 BindableEvent_OnFailStateChanged:Fire(true)
2515 end
2516 return
2517 end
2518 end
2519 end
2520 end
2521 end
2522
2523 -- Cancel path when you use the keyboard controls.
2524 if processed == false and input.UserInputType == Enum.UserInputType.Keyboard and movementKeys[input.KeyCode] then
2525 CleanupPath()
2526 end
2527 if input.UserInputType == Enum.UserInputType.MouseButton1 then
2528 self.mouse1DownTime = tick()
2529 self.mouse1DownPos = input.Position
2530 end
2531 if input.UserInputType == Enum.UserInputType.MouseButton2 then
2532 self.mouse2DownTime = tick()
2533 self.mouse2DownPos = input.Position
2534 end
2535 end)
2536
2537 self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
2538 if input.UserInputType == Enum.UserInputType.Touch then
2539 self:OnTouchChanged(input, processed)
2540 end
2541 end)
2542
2543 self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
2544 if input.UserInputType == Enum.UserInputType.Touch then
2545 self:OnTouchEnded(input, processed)
2546 end
2547
2548 if input.UserInputType == Enum.UserInputType.MouseButton2 then
2549 self.mouse2UpTime = tick()
2550 local currPos = input.Position
2551 if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 and self.moveVector.Magnitude<=0 then
2552 local positions = {currPos}
2553 OnTap(positions)
2554 end
2555 end
2556 end)
2557
2558 self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed)
2559 if not processed then
2560 OnTap(touchPositions)
2561 end
2562 end)
2563
2564 local function computeThrottle(dist)
2565 if dist > .2 then
2566 return 0.5+(dist^2)/2
2567 else
2568 return 0
2569 end
2570 end
2571
2572 local lastSteer = 0
2573
2574 --kP = how much the steering corrects for the current error in driving angle
2575 --kD = how much the steering corrects for how quickly the error in driving angle is changing
2576 local kP = 1
2577 local kD = 0.5
2578 local function getThrottleAndSteer(object, point)
2579 local throttle, steer = 0, 0
2580 local oCF = object.CFrame
2581
2582 local relativePosition = oCF:pointToObjectSpace(point)
2583 local relativeZDirection = -relativePosition.z
2584 local relativeDistance = relativePosition.magnitude
2585
2586 -- throttle quadratically increases from 0-1 as distance from the selected point goes from 0-50, after 50, throttle is 1.
2587 -- this allows shorter distance travel to have more fine-tuned control.
2588 throttle = computeThrottle(math.min(1,relativeDistance/50))*math.sign(relativeZDirection)
2589
2590 local steerAngle = -math.atan2(-relativePosition.x, -relativePosition.z)
2591 steer = steerAngle/(math.pi/4)
2592
2593 local steerDelta = steer - lastSteer
2594 lastSteer = steer
2595 local pdSteer = kP * steer + kD * steer
2596 return throttle, pdSteer
2597 end
2598
2599 local function Update()
2600 if CurrentSeatPart then
2601 if DrivingTo then
2602 local throttle, steer = getThrottleAndSteer(CurrentSeatPart, DrivingTo)
2603 CurrentSeatPart.ThrottleFloat = throttle
2604 CurrentSeatPart.SteerFloat = steer
2605 else
2606 CurrentSeatPart.ThrottleFloat = 0
2607 CurrentSeatPart.SteerFloat = 0
2608 end
2609 end
2610
2611 local cameraPos = workspace.CurrentCamera.CFrame.p
2612 for i = 1, #activePopups do
2613 local popup = activePopups[i]
2614 popup.CFrame = CFrame.new(popup.CFrame.p, cameraPos)
2615 end
2616 end
2617
2618 RunService:BindToRenderStep("ClickToMoveRenderUpdate",Enum.RenderPriority.Camera.Value - 1,Update)
2619
2620 local function OnCharacterChildAdded(child)
2621 if UserInputService.TouchEnabled then
2622 if child:IsA('Tool') then
2623 child.ManualActivationOnly = true
2624 end
2625 end
2626 if child:IsA('Humanoid') then
2627 DisconnectEvent(self.humanoidDiedConn)
2628 self.humanoidDiedConn = child.Died:Connect(function()
2629 if ExistingIndicator then
2630 DebrisService:AddItem(ExistingIndicator.Model, 1)
2631 end
2632 end)
2633 end
2634 end
2635
2636 self.characterChildAddedConn = character.ChildAdded:Connect(function(child)
2637 OnCharacterChildAdded(child)
2638 end)
2639 self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child)
2640 if UserInputService.TouchEnabled then
2641 if child:IsA('Tool') then
2642 child.ManualActivationOnly = false
2643 end
2644 end
2645 end)
2646 for _, child in pairs(character:GetChildren()) do
2647 OnCharacterChildAdded(child)
2648 end
2649end
2650
2651function ClickToMove:Start()
2652 self:Enable(true)
2653end
2654
2655function ClickToMove:Stop()
2656 self:Enable(false)
2657end
2658
2659function ClickToMove:Enable(enable, enableWASD)
2660 if enable then
2661 if not self.running then
2662 if Player.Character then -- retro-listen
2663 self:OnCharacterAdded(Player.Character)
2664 end
2665 self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char)
2666 self:OnCharacterAdded(char)
2667 end)
2668 self.running = true
2669 end
2670 else
2671 if self.running then
2672 self:DisconnectEvents()
2673 CleanupPath()
2674 -- Restore tool activation on shutdown
2675 if UserInputService.TouchEnabled then
2676 local character = Player.Character
2677 if character then
2678 for _, child in pairs(character:GetChildren()) do
2679 if child:IsA('Tool') then
2680 child.ManualActivationOnly = false
2681 end
2682 end
2683 end
2684 end
2685 DrivingTo = nil
2686 self.running = false
2687 end
2688 end
2689
2690 -- Extension for initializing Keyboard input as this class now derives from Keyboard
2691 if UserInputService.KeyboardEnabled and enable ~= self.enabled then
2692
2693 self.forwardValue = 0
2694 self.backwardValue = 0
2695 self.leftValue = 0
2696 self.rightValue = 0
2697
2698 self.moveVector = ZERO_VECTOR3
2699
2700 if enable then
2701 self:BindContextActions()
2702 self:ConnectFocusEventListeners()
2703 else
2704 self:UnbindContextActions()
2705 self:DisconnectFocusEventListeners()
2706 end
2707 end
2708
2709 self.wasdEnabled = enable and enableWASD or false
2710 self.enabled = enable
2711end
2712
2713-- Overrides Keyboard:UpdateMovement(inputState) to conditionally consider self.wasdEnabled
2714function ClickToMove:UpdateMovement(inputState)
2715 if inputState == Enum.UserInputState.Cancel then
2716 self.moveVector = ZERO_VECTOR3
2717 elseif self.wasdEnabled then
2718 self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
2719 end
2720end
2721
2722return ClickToMove
2723
2724
2725--[[
2726 BaseCharacterController - Abstract base class for character controllers, not intended to be
2727 directly instantiated.
2728
2729 2018 PlayerScripts Update - AllYourBlox
2730--]]
2731
2732local ZERO_VECTOR3 = Vector3.new(0,0,0)
2733
2734--[[ Roblox Services ]]--
2735local Players = game:GetService("Players")
2736
2737--[[ The Module ]]--
2738local BaseCharacterController = {}
2739BaseCharacterController.__index = BaseCharacterController
2740
2741function BaseCharacterController.new()
2742 local self = setmetatable({}, BaseCharacterController)
2743 self.enabled = false
2744 self.moveVector = ZERO_VECTOR3
2745 self.isJumping = false
2746 return self
2747end
2748
2749function BaseCharacterController:GetMoveVector()
2750 return self.moveVector
2751end
2752
2753function BaseCharacterController:GetIsJumping()
2754 return self.isJumping
2755end
2756
2757-- Override in derived classes to set self.enabled and return boolean indicating
2758-- whether Enable/Disable was successful. Return true if controller is already in the requested state.
2759function BaseCharacterController:Enable(enable)
2760 error("BaseCharacterController:Enable must be overridden in derived classes and should not be called.")
2761 return false
2762end
2763
2764return BaseCharacterController
2765
2766--[[
2767
2768 TouchThumbpad
2769
2770--]]
2771
2772local Players = game:GetService("Players")
2773local UserInputService = game:GetService("UserInputService")
2774local GuiService = game:GetService("GuiService")
2775
2776--[[ Constants ]]--
2777local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png"
2778local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
2779local ZERO_VECTOR3 = Vector3.new(0,0,0)
2780local UNIT_Z = Vector3.new(0,0,1)
2781local UNIT_X = Vector3.new(1,0,0)
2782
2783--[[ The Module ]]--
2784local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
2785local TouchThumbpad = setmetatable({}, BaseCharacterController)
2786TouchThumbpad.__index = TouchThumbpad
2787
2788function TouchThumbpad.new()
2789 local self = setmetatable(BaseCharacterController.new(), TouchThumbpad)
2790
2791 self.thumbpadFrame = nil
2792 self.touchChangedConn = nil
2793 self.touchEndedConn = nil
2794 self.menuOpenedConn = nil
2795 self.screenPos = nil
2796 self.isRight, self.isLeft, self.isUp, self.isDown = false, false, false, false
2797 self.smArrowSize = nil
2798 self.lgArrowSize = nil
2799 self.smImgOffset = nil
2800 self.lgImgOffset = nil
2801 return self
2802end
2803
2804--[[ Local Helper Functions ]]--
2805local function doTween(guiObject, endSize, endPosition)
2806 guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true)
2807end
2808
2809local function CreateArrowLabel(name, position, size, rectOffset, rectSize, parent)
2810 local image = Instance.new("ImageLabel")
2811 image.Name = name
2812 image.Image = DPAD_SHEET
2813 image.ImageRectOffset = rectOffset
2814 image.ImageRectSize = rectSize
2815 image.BackgroundTransparency = 1
2816 image.ImageColor3 = Color3.fromRGB(190, 190, 190)
2817 image.Size = size
2818 image.Position = position
2819 image.Parent = parent
2820 return image
2821end
2822
2823function TouchThumbpad:Enable(enable, uiParentFrame)
2824 if enable == nil then return false end -- If nil, return false (invalid argument)
2825 enable = enable and true or false -- Force anything non-nil to boolean before comparison
2826 if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
2827
2828 self.moveVector = ZERO_VECTOR3
2829 self.isJumping = false
2830
2831 if enable then
2832 -- Enable
2833 if not self.thumbpadFrame then
2834 self:Create(uiParentFrame)
2835 end
2836 self.thumbpadFrame.Visible = true
2837 else
2838 -- Disable
2839 self.thumbpadFrame.Visible = false
2840 self:OnInputEnded()
2841 end
2842 self.enabled = enable
2843end
2844
2845function TouchThumbpad:OnInputEnded()
2846 self.moveVector = ZERO_VECTOR3
2847 self.isJumping = false
2848
2849 self.thumbpadFrame.Position = self.screenPos
2850 self.touchObject = nil
2851 self.isUp, self.isDown, self.isLeft, self.isRight = false, false, false, false
2852 doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset))
2853 doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset))
2854 doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
2855 doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
2856end
2857
2858function TouchThumbpad:Create(parentFrame)
2859 if self.thumbpadFrame then
2860 self.thumbpadFrame:Destroy()
2861 self.thumbpadFrame = nil
2862 end
2863 if self.touchChangedConn then
2864 self.touchChangedConn:Disconnect()
2865 self.touchChangedConn = nil
2866 end
2867 if self.touchEndedConn then
2868 self.touchEndedConn:Disconnect()
2869 self.touchEndedConn = nil
2870 end
2871 if self.menuOpenedConn then
2872 self.menuOpenedConn:Disconnect()
2873 self.menuOpenedConn = nil
2874 end
2875
2876 local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
2877 local isSmallScreen = minAxis <= 500
2878 local thumbpadSize = isSmallScreen and 70 or 120
2879 self.screenPos = isSmallScreen and UDim2.new(0, thumbpadSize * 1.25, 1, -thumbpadSize - 20) or
2880 UDim2.new(0, thumbpadSize * 0.5 - 10, 1, -thumbpadSize * 1.75 - 10)
2881
2882 self.thumbpadFrame = Instance.new("Frame")
2883 self.thumbpadFrame.Name = "ThumbpadFrame"
2884 self.thumbpadFrame.Visible = false
2885 self.thumbpadFrame.Active = true
2886 self.thumbpadFrame.Size = UDim2.new(0, thumbpadSize + 20, 0, thumbpadSize + 20)
2887 self.thumbpadFrame.Position = self.screenPos
2888 self.thumbpadFrame.BackgroundTransparency = 1
2889
2890 local outerImage = Instance.new("ImageLabel")
2891 outerImage.Name = "OuterImage"
2892 outerImage.Image = TOUCH_CONTROL_SHEET
2893 outerImage.ImageRectOffset = Vector2.new(0, 0)
2894 outerImage.ImageRectSize = Vector2.new(220, 220)
2895 outerImage.BackgroundTransparency = 1
2896 outerImage.Size = UDim2.new(0, thumbpadSize, 0, thumbpadSize)
2897 outerImage.Position = UDim2.new(0, 10, 0, 10)
2898 outerImage.Parent = self.thumbpadFrame
2899
2900 self.smArrowSize = isSmallScreen and UDim2.new(0, 32, 0, 32) or UDim2.new(0, 64, 0, 64)
2901 self.lgArrowSize = UDim2.new(0, self.smArrowSize.X.Offset * 2, 0, self.smArrowSize.Y.Offset * 2)
2902 local imgRectSize = Vector2.new(110, 110)
2903 self.smImgOffset = isSmallScreen and -4 or -9
2904 self.lgImgOffset = isSmallScreen and -28 or -55
2905
2906 self.dArrow = CreateArrowLabel("DownArrow", UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset), self.smArrowSize, Vector2.new(8, 8), imgRectSize, outerImage)
2907 self.uArrow = CreateArrowLabel("UpArrow", UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset), self.smArrowSize, Vector2.new(8, 266), imgRectSize, outerImage)
2908 self.lArrow = CreateArrowLabel("LeftArrow", UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset), self.smArrowSize, Vector2.new(137, 137), imgRectSize, outerImage)
2909 self.rArrow = CreateArrowLabel("RightArrow", UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset), self.smArrowSize, Vector2.new(8, 137), imgRectSize, outerImage)
2910
2911 local function doTween(guiObject, endSize, endPosition)
2912 guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true)
2913 end
2914
2915 local padOrigin = nil
2916 local deadZone = 0.1
2917 self.isRight, self.isLeft, self.isUp, self.isDown = false, false, false, false
2918
2919 local function doMove(pos)
2920
2921 local moveDelta = pos - padOrigin
2922 local moveVector2 = 2 * moveDelta / thumbpadSize
2923
2924 -- Scaled Radial Dead Zone
2925 if moveVector2.Magnitude < deadZone then
2926 self.moveVector = ZERO_VECTOR3
2927 else
2928 moveVector2 = moveVector2.unit * ((moveVector2.Magnitude - deadZone) / (1 - deadZone))
2929
2930 -- prevent NAN Vector from trying to do zerovector.Unit
2931 if moveVector2.Magnitude == 0 then
2932 self.moveVector = ZERO_VECTOR3
2933 else
2934 self.moveVector = Vector3.new(moveVector2.x, 0, moveVector2.y).Unit
2935 end
2936 end
2937
2938 local forwardDot = self.moveVector:Dot(-UNIT_Z)
2939 local rightDot = self.moveVector:Dot(UNIT_X)
2940
2941 if forwardDot > 0.5 then -- UP
2942 if not self.isUp then
2943 self.isUp, self.isDown = true, false
2944 doTween(self.uArrow, self.lgArrowSize, UDim2.new(0.5, -self.smArrowSize.X.Offset, 0, self.smImgOffset - 1.5*self.smArrowSize.Y.Offset))
2945 doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset))
2946 end
2947 elseif forwardDot < -0.5 then -- DOWN
2948 if not self.isDown then
2949 self.isDown, self.isUp = true, false
2950 doTween(self.dArrow, self.lgArrowSize, UDim2.new(0.5, -self.smArrowSize.X.Offset, 1, self.lgImgOffset + 0.5*self.smArrowSize.Y.Offset))
2951 doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset))
2952 end
2953 else
2954 self.isUp, self.isDown = false, false
2955 doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset))
2956 doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset))
2957 end
2958
2959 if rightDot > 0.5 then
2960 if not self.isRight then
2961 self.isRight, self.isLeft = true, false
2962 doTween(self.rArrow, self.lgArrowSize, UDim2.new(1, self.lgImgOffset + 0.5*self.smArrowSize.X.Offset, 0.5, -self.smArrowSize.Y.Offset))
2963 doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
2964 end
2965 elseif rightDot < -0.5 then
2966 if not self.isLeft then
2967 self.isLeft, self.isRight = true, false
2968 doTween(self.lArrow, self.lgArrowSize, UDim2.new(0, self.smImgOffset - 1.5*self.smArrowSize.X.Offset, 0.5, -self.smArrowSize.Y.Offset))
2969 doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
2970 end
2971 else
2972 self.isRight, self.isLeft = false, false
2973 doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
2974 doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
2975 end
2976 end
2977
2978 --input connections
2979 self.thumbpadFrame.InputBegan:connect(function(inputObject)
2980 --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
2981 --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
2982 if self.touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
2983 or inputObject.UserInputState ~= Enum.UserInputState.Begin then
2984 return
2985 end
2986
2987 self.thumbpadFrame.Position = UDim2.new(0, inputObject.Position.x - 0.5*self.thumbpadFrame.AbsoluteSize.x, 0, inputObject.Position.y - 0.5*self.thumbpadFrame.Size.Y.Offset)
2988 padOrigin = Vector3.new(self.thumbpadFrame.AbsolutePosition.x +0.5* self.thumbpadFrame.AbsoluteSize.x,
2989 self.thumbpadFrame.AbsolutePosition.y + 0.5*self.thumbpadFrame.AbsoluteSize.y, 0)
2990 doMove(inputObject.Position)
2991 self.touchObject = inputObject
2992 end)
2993
2994 self.touchChangedConn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed)
2995 if inputObject == self.touchObject then
2996 doMove(self.touchObject.Position)
2997 end
2998 end)
2999
3000 self.touchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject)
3001 if inputObject == self.touchObject then
3002 self:OnInputEnded()
3003 end
3004 end)
3005
3006 self.menuOpenedConn = GuiService.MenuOpened:Connect(function()
3007 if self.touchObject then
3008 self:OnInputEnded()
3009 end
3010 end)
3011
3012 self.thumbpadFrame.Parent = parentFrame
3013end
3014
3015return TouchThumbpad
3016
3017
3018--[[
3019
3020
3021
3022--]]
3023
3024local Players = game:GetService("Players")
3025local GuiService = game:GetService("GuiService")
3026
3027--[[ Constants ]]--
3028local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png"
3029local ZERO_VECTOR3 = Vector3.new(0,0,0)
3030local COMPASS_DIR = {
3031 Vector3.new(1, 0, 0), -- E
3032 Vector3.new(1, 0, 1).unit, -- SE
3033 Vector3.new(0, 0, 1), -- S
3034 Vector3.new(-1, 0, 1).unit, -- SW
3035 Vector3.new(-1, 0, 0), -- W
3036 Vector3.new(-1, 0, -1).unit, -- NW
3037 Vector3.new(0, 0, -1), -- N
3038 Vector3.new(1, 0, -1).unit, -- NE
3039}
3040
3041
3042--[[ The Module ]]--
3043local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
3044local TouchDPad = setmetatable({}, BaseCharacterController)
3045TouchDPad.__index = TouchDPad
3046
3047function TouchDPad.new()
3048 local self = setmetatable(BaseCharacterController.new(), TouchDPad)
3049
3050 self.DPadFrame = nil
3051 self.touchObject = nil
3052 self.flBtn = nil
3053 self.frBtn = nil
3054
3055 return self
3056end
3057
3058--[[ Local Functions ]]--
3059local function CreateArrowLabel(name, position, size, rectOffset, rectSize, parent)
3060 local image = Instance.new("ImageLabel")
3061 image.Name = name
3062 image.Image = DPAD_SHEET
3063 image.ImageRectOffset = rectOffset
3064 image.ImageRectSize = rectSize
3065 image.BackgroundTransparency = 1
3066 image.Size = size
3067 image.Position = position
3068 image.Parent = parent
3069 return image
3070end
3071
3072function TouchDPad:GetCenterPosition()
3073 return Vector2.new(self.DPadFrame.AbsolutePosition.x + self.DPadFrame.AbsoluteSize.x * 0.5, self.DPadFrame.AbsolutePosition.y + self.DPadFrame.AbsoluteSize.y * 0.5)
3074end
3075
3076function TouchDPad:Enable(enable, uiParentFrame)
3077 if enable == nil then return false end -- If nil, return false (invalid argument)
3078 enable = enable and true or false -- Force anything non-nil to boolean before comparison
3079 if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
3080
3081 self.moveVector = ZERO_VECTOR3
3082 self.isJumping = false
3083
3084 if enable then
3085 -- Enable
3086 if not self.DPadFrame then
3087 self:Create(uiParentFrame)
3088 end
3089 self.DPadFrame.Visible = true
3090 else
3091 -- Disable
3092 self.DPadFrame.Visible = false
3093 self:OnInputEnded()
3094 end
3095 self.enabled = enable
3096end
3097
3098-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump
3099-- rather than sustained jumping. This is only to preserve the current behavior through the refactor.
3100function TouchDPad:GetIsJumping()
3101 local wasJumping = self.isJumping
3102 self.isJumping = false
3103 return wasJumping
3104end
3105
3106function TouchDPad:OnInputEnded()
3107 self.touchObject =nil
3108 if self.flBtn then self.flBtn.Visible = false end
3109 if self.frBtn then self.frBtn.Visible = false end
3110 self.moveVector = ZERO_VECTOR3
3111end
3112
3113function TouchDPad:Create(parentFrame)
3114 if self.DPadFrame then
3115 self.DPadFrame:Destroy()
3116 self.DPadFrame = nil
3117 end
3118
3119 local position = UDim2.new(0, 10, 1, -230)
3120 self.DPadFrame = Instance.new("Frame")
3121 self.DPadFrame.Name = "DPadFrame"
3122 self.DPadFrame.Active = true
3123 self.DPadFrame.Visible = false
3124 self.DPadFrame.Size = UDim2.new(0, 192, 0, 192)
3125 self.DPadFrame.Position = position
3126 self.DPadFrame.BackgroundTransparency = 1
3127
3128 local smArrowSize = UDim2.new(0, 23, 0, 23)
3129 local lgArrowSize = UDim2.new(0, 64, 0, 64)
3130 local smImgOffset = Vector2.new(46, 46)
3131 local lgImgOffset = Vector2.new(128, 128)
3132
3133 local bBtn = CreateArrowLabel("BackButton", UDim2.new(0.5, -32, 1, -64), lgArrowSize, Vector2.new(0, 0), lgImgOffset, self.DPadFrame)
3134 local fBtn = CreateArrowLabel("ForwardButton", UDim2.new(0.5, -32, 0, 0), lgArrowSize, Vector2.new(0, 258), lgImgOffset, self.DPadFrame)
3135 local lBtn = CreateArrowLabel("LeftButton", UDim2.new(0, 0, 0.5, -32), lgArrowSize, Vector2.new(129, 129), lgImgOffset, self.DPadFrame)
3136 local rBtn = CreateArrowLabel("RightButton", UDim2.new(1, -64, 0.5, -32), lgArrowSize, Vector2.new(0, 129), lgImgOffset, self.DPadFrame)
3137 local jumpBtn = CreateArrowLabel("JumpButton", UDim2.new(0.5, -32, 0.5, -32), lgArrowSize, Vector2.new(129, 0), lgImgOffset, self.DPadFrame)
3138 self.flBtn = CreateArrowLabel("ForwardLeftButton", UDim2.new(0, 35, 0, 35), smArrowSize, Vector2.new(129, 258), smImgOffset, self.DPadFrame)
3139 self.frBtn = CreateArrowLabel("ForwardRightButton", UDim2.new(1, -55, 0, 35), smArrowSize, Vector2.new(176, 258), smImgOffset, self.DPadFrame)
3140 self.flBtn.Visible = false
3141 self.frBtn.Visible = false
3142
3143 -- input connections
3144 jumpBtn.InputBegan:Connect(function(inputObject)
3145 self.isJumping = true
3146 end)
3147
3148 local function normalizeDirection(inputPosition)
3149 local jumpRadius = jumpBtn.AbsoluteSize.x*0.5
3150 local centerPosition = self:GetCenterPosition()
3151 local direction = Vector2.new(inputPosition.x - centerPosition.x, inputPosition.y - centerPosition.y)
3152
3153 if direction.magnitude > jumpRadius then
3154 local angle = math.atan2(direction.y, direction.x)
3155 local octant = (math.floor(8 * angle / (2 * math.pi) + 8.5)%8) + 1
3156 self.moveVector = COMPASS_DIR[octant]
3157 end
3158
3159 if not self.flBtn.Visible and self.moveVector == COMPASS_DIR[7] then
3160 self.flBtn.Visible = true
3161 self.frBtn.Visible = true
3162 end
3163 end
3164
3165 self.DPadFrame.InputBegan:Connect(function(inputObject)
3166 if self.touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then
3167 return
3168 end
3169
3170 self.touchObject = inputObject
3171 normalizeDirection(self.touchObject.Position)
3172 end)
3173
3174 self.DPadFrame.InputChanged:Connect(function(inputObject)
3175 if inputObject == self.touchObject then
3176 normalizeDirection(self.touchObject.Position)
3177 self.isJumping = false
3178 end
3179 end)
3180
3181 self.DPadFrame.InputEnded:connect(function(inputObject)
3182 if inputObject == self.touchObject then
3183 self:OnInputEnded()
3184 end
3185 end)
3186
3187 GuiService.MenuOpened:Connect(function()
3188 if self.touchObject then
3189 self:OnInputEnded()
3190 end
3191 end)
3192
3193 self.DPadFrame.Parent = parentFrame
3194end
3195
3196return TouchDPad
3197
3198
3199--[[
3200
3201 TouchThumbstick
3202
3203--]]
3204
3205local Players = game:GetService("Players")
3206local GuiService = game:GetService("GuiService")
3207local UserInputService = game:GetService("UserInputService")
3208
3209--[[ Constants ]]--
3210local ZERO_VECTOR3 = Vector3.new(0,0,0)
3211local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
3212
3213--[[ The Module ]]--
3214local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
3215local TouchThumbstick = setmetatable({}, BaseCharacterController)
3216TouchThumbstick.__index = TouchThumbstick
3217
3218function TouchThumbstick.new()
3219 local self = setmetatable(BaseCharacterController.new(), TouchThumbstick)
3220
3221 self.isFollowStick = false
3222
3223 self.thumbstickFrame = nil
3224 self.moveTouchObject = nil
3225 self.onTouchMovedConn = nil
3226 self.onTouchEndedConn = nil
3227 self.screenPos = nil
3228 self.stickImage = nil
3229 self.thumbstickSize = nil -- Float
3230
3231 return self
3232end
3233
3234function TouchThumbstick:Enable(enable, uiParentFrame)
3235 if enable == nil then return false end -- If nil, return false (invalid argument)
3236 enable = enable and true or false -- Force anything non-nil to boolean before comparison
3237 if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
3238
3239 self.moveVector = ZERO_VECTOR3
3240 self.isJumping = false
3241
3242 if enable then
3243 -- Enable
3244 if not self.thumbstickFrame then
3245 self:Create(uiParentFrame)
3246 end
3247 self.thumbstickFrame.Visible = true
3248 else
3249 -- Disable
3250 self.thumbstickFrame.Visible = false
3251 self:OnInputEnded()
3252 end
3253 self.enabled = enable
3254end
3255
3256function TouchThumbstick:OnInputEnded()
3257 self.thumbstickFrame.Position = self.screenPos
3258 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)
3259
3260 self.moveVector = ZERO_VECTOR3
3261 self.isJumping = false
3262
3263 self.thumbstickFrame.Position = self.screenPos
3264 self.moveTouchObject = nil
3265end
3266
3267function TouchThumbstick:Create(parentFrame)
3268
3269 if self.thumbstickFrame then
3270 self.thumbstickFrame:Destroy()
3271 self.thumbstickFrame = nil
3272 if self.onTouchMovedConn then
3273 self.onTouchMovedConn:Disconnect()
3274 self.onTouchMovedConn = nil
3275 end
3276 if self.onTouchEndedConn then
3277 self.onTouchEndedConn:Disconnect()
3278 self.onTouchEndedConn = nil
3279 end
3280 end
3281
3282 local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
3283 local isSmallScreen = minAxis <= 500
3284 self.thumbstickSize = isSmallScreen and 70 or 120
3285 self.screenPos = isSmallScreen and UDim2.new(0, (self.thumbstickSize/2) - 10, 1, -self.thumbstickSize - 20) or
3286 UDim2.new(0, self.thumbstickSize/2, 1, -self.thumbstickSize * 1.75)
3287
3288 self.thumbstickFrame = Instance.new("Frame")
3289 self.thumbstickFrame.Name = "ThumbstickFrame"
3290 self.thumbstickFrame.Active = true
3291 self.thumbstickFrame.Visible = false
3292 self.thumbstickFrame.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
3293 self.thumbstickFrame.Position = self.screenPos
3294 self.thumbstickFrame.BackgroundTransparency = 1
3295
3296 local outerImage = Instance.new("ImageLabel")
3297 outerImage.Name = "OuterImage"
3298 outerImage.Image = TOUCH_CONTROL_SHEET
3299 outerImage.ImageRectOffset = Vector2.new()
3300 outerImage.ImageRectSize = Vector2.new(220, 220)
3301 outerImage.BackgroundTransparency = 1
3302 outerImage.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
3303 outerImage.Position = UDim2.new(0, 0, 0, 0)
3304 outerImage.Parent = self.thumbstickFrame
3305
3306 self.stickImage = Instance.new("ImageLabel")
3307 self.stickImage.Name = "StickImage"
3308 self.stickImage.Image = TOUCH_CONTROL_SHEET
3309 self.stickImage.ImageRectOffset = Vector2.new(220, 0)
3310 self.stickImage.ImageRectSize = Vector2.new(111, 111)
3311 self.stickImage.BackgroundTransparency = 1
3312 self.stickImage.Size = UDim2.new(0, self.thumbstickSize/2, 0, self.thumbstickSize/2)
3313 self.stickImage.Position = UDim2.new(0, self.thumbstickSize/2 - self.thumbstickSize/4, 0, self.thumbstickSize/2 - self.thumbstickSize/4)
3314 self.stickImage.ZIndex = 2
3315 self.stickImage.Parent = self.thumbstickFrame
3316
3317 local centerPosition = nil
3318 local deadZone = 0.05
3319
3320 local function DoMove(direction)
3321
3322 local currentMoveVector = direction / (self.thumbstickSize/2)
3323
3324 -- Scaled Radial Dead Zone
3325 local inputAxisMagnitude = currentMoveVector.magnitude
3326 if inputAxisMagnitude < deadZone then
3327 currentMoveVector = Vector3.new()
3328 else
3329 currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone))
3330 -- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed
3331 -- must check for zero length vector is using unit
3332 currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
3333 end
3334
3335 self.moveVector = currentMoveVector
3336 end
3337
3338 local function MoveStick(pos)
3339 local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y)
3340 local length = relativePosition.magnitude
3341 local maxLength = self.thumbstickFrame.AbsoluteSize.x/2
3342 if self.isFollowStick and length > maxLength then
3343 local offset = relativePosition.unit * maxLength
3344 self.thumbstickFrame.Position = UDim2.new(
3345 0, pos.x - self.thumbstickFrame.AbsoluteSize.x/2 - offset.x,
3346 0, pos.y - self.thumbstickFrame.AbsoluteSize.y/2 - offset.y)
3347 else
3348 length = math.min(length, maxLength)
3349 relativePosition = relativePosition.unit * length
3350 end
3351 self.stickImage.Position = UDim2.new(0, relativePosition.x + self.stickImage.AbsoluteSize.x/2, 0, relativePosition.y + self.stickImage.AbsoluteSize.y/2)
3352 end
3353
3354 -- input connections
3355 self.thumbstickFrame.InputBegan:Connect(function(inputObject)
3356 --A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
3357 --if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
3358 if self.moveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
3359 or inputObject.UserInputState ~= Enum.UserInputState.Begin then
3360 return
3361 end
3362
3363 self.moveTouchObject = inputObject
3364 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)
3365 centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2,
3366 self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2)
3367 local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
3368 end)
3369
3370 self.onTouchMovedConn = UserInputService.TouchMoved:Connect(function(inputObject, isProcessed)
3371 if inputObject == self.moveTouchObject then
3372 centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2,
3373 self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2)
3374 local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
3375 DoMove(direction)
3376 MoveStick(inputObject.Position)
3377 end
3378 end)
3379
3380 self.onTouchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject, isProcessed)
3381 if inputObject == self.moveTouchObject then
3382 self:OnInputEnded()
3383 end
3384 end)
3385
3386 GuiService.MenuOpened:Connect(function()
3387 if self.moveTouchObject then
3388 self:OnInputEnded()
3389 end
3390 end)
3391
3392 self.thumbstickFrame.Parent = parentFrame
3393end
3394
3395return TouchThumbstick
3396
3397
3398--[[
3399 Gamepad Character Control - This module handles controlling your avatar using a game console-style controller
3400
3401 2018 PlayerScripts Update - AllYourBlox
3402--]]
3403
3404local UserInputService = game:GetService("UserInputService")
3405local ContextActionService = game:GetService("ContextActionService")
3406
3407--[[ Constants ]]--
3408local ZERO_VECTOR3 = Vector3.new(0,0,0)
3409local NONE = Enum.UserInputType.None
3410local thumbstickDeadzone = 0.2
3411
3412--[[ The Module ]]--
3413local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
3414local Gamepad = setmetatable({}, BaseCharacterController)
3415Gamepad.__index = Gamepad
3416
3417local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
3418 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
3419end)
3420local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
3421
3422function Gamepad.new(CONTROL_ACTION_PRIORITY)
3423 local self = setmetatable(BaseCharacterController.new(), Gamepad)
3424
3425 self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
3426
3427 self.forwardValue = 0
3428 self.backwardValue = 0
3429 self.leftValue = 0
3430 self.rightValue = 0
3431
3432 self.activeGamepad = NONE -- Enum.UserInputType.Gamepad1, 2, 3...
3433 self.gamepadConnectedConn = nil
3434 self.gamepadDisconnectedConn = nil
3435 return self
3436end
3437
3438function Gamepad:Enable(enable)
3439 if not UserInputService.GamepadEnabled then
3440 return false
3441 end
3442
3443 if enable == self.enabled then
3444 -- Module is already in the state being requested. True is returned here since the module will be in the state
3445 -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
3446 -- no action was necessary. False indicates failure to be in requested/expected state.
3447 return true
3448 end
3449
3450 self.forwardValue = 0
3451 self.backwardValue = 0
3452 self.leftValue = 0
3453 self.rightValue = 0
3454 self.moveVector = ZERO_VECTOR3
3455 self.isJumping = false
3456
3457 if enable then
3458 self.activeGamepad = self:GetHighestPriorityGamepad()
3459 if self.activeGamepad ~= NONE then
3460 self:BindContextActions()
3461 self:ConnectGamepadConnectionListeners()
3462 else
3463 -- No connected gamepads, failure to enable
3464 return false
3465 end
3466 else
3467 self:UnbindContextActions()
3468 self:DisconnectGamepadConnectionListeners()
3469 self.activeGamepad = NONE
3470 end
3471
3472 self.enabled = enable
3473 return true
3474end
3475
3476-- This function selects the lowest number gamepad from the currently-connected gamepad
3477-- and sets it as the active gamepad
3478function Gamepad:GetHighestPriorityGamepad()
3479 local connectedGamepads = UserInputService:GetConnectedGamepads()
3480 local bestGamepad = NONE -- Note that this value is higher than all valid gamepad values
3481 for _, gamepad in pairs(connectedGamepads) do
3482 if gamepad.Value < bestGamepad.Value then
3483 bestGamepad = gamepad
3484 end
3485 end
3486 return bestGamepad
3487end
3488
3489function Gamepad:BindContextActions()
3490
3491 if self.activeGamepad == NONE then
3492 -- There must be an active gamepad to set up bindings
3493 return false
3494 end
3495
3496
3497 local updateMovement = function(inputState)
3498 if inputState == Enum.UserInputState.Cancel then
3499 self.moveVector = ZERO_VECTOR3
3500 else
3501 self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
3502 end
3503 end
3504
3505 -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
3506 -- which fixes them from getting stuck on.
3507 local handleMoveForward = function(actionName, inputState, inputObject)
3508 self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
3509 updateMovement(inputState)
3510 end
3511
3512 local handleMoveBackward = function(actionName, inputState, inputObject)
3513 self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
3514 updateMovement(inputState)
3515 end
3516
3517 local handleMoveLeft = function(actionName, inputState, inputObject)
3518 self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
3519 updateMovement(inputState)
3520 end
3521
3522 local handleMoveRight = function(actionName, inputState, inputObject)
3523 self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
3524 updateMovement(inputState)
3525 end
3526
3527 local handleJumpAction = function(actionName, inputState, inputObject)
3528 self.isJumping = (inputState == Enum.UserInputState.Begin)
3529 if FFlagPlayerScriptsBindAtPriority then
3530 return Enum.ContextActionResult.Sink
3531 end
3532 end
3533
3534 local handleThumbstickInput = function(actionName, inputState, inputObject)
3535
3536 if inputState == Enum.UserInputState.Cancel then
3537 self.moveVector = ZERO_VECTOR3
3538 return FFlagPlayerScriptsBindAtPriority and Enum.ContextActionResult.Sink or nil
3539 end
3540
3541 if self.activeGamepad ~= inputObject.UserInputType then
3542 return FFlagPlayerScriptsBindAtPriority and Enum.ContextActionResult.Pass or nil
3543 end
3544 if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
3545
3546 if inputObject.Position.magnitude > thumbstickDeadzone then
3547 self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
3548 else
3549 self.moveVector = ZERO_VECTOR3
3550 end
3551 if FFlagPlayerScriptsBindAtPriority then
3552 return Enum.ContextActionResult.Sink
3553 end
3554 end
3555
3556 ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
3557 if FFlagPlayerScriptsBindAtPriority then
3558 ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
3559 self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA)
3560 ContextActionService:BindActionAtPriority("moveThumbstick", handleThumbstickInput, false,
3561 self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Thumbstick1)
3562 else
3563 ContextActionService:BindAction("jumpAction",handleJumpAction, false, Enum.KeyCode.ButtonA)
3564 ContextActionService:BindAction("moveThumbstick",handleThumbstickInput, false, Enum.KeyCode.Thumbstick1)
3565 end
3566
3567 return true
3568end
3569
3570function Gamepad:UnbindContextActions()
3571 if self.activeGamepad ~= NONE then
3572 ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
3573 end
3574 ContextActionService:UnbindAction("moveThumbstick")
3575 ContextActionService:UnbindAction("jumpAction")
3576end
3577
3578function Gamepad:OnNewGamepadConnected()
3579 -- A new gamepad has been connected.
3580 local bestGamepad = self:GetHighestPriorityGamepad()
3581
3582 if bestGamepad == self.activeGamepad then
3583 -- A new gamepad was connected, but our active gamepad is not changing
3584 return
3585 end
3586
3587 if bestGamepad == NONE then
3588 -- There should be an active gamepad when GamepadConnected fires, so this should not
3589 -- normally be hit. If there is no active gamepad, unbind actions but leave
3590 -- the module enabled and continue to listen for a new gamepad connection.
3591 warn("Gamepad:OnNewGamepadConnected found no connected gamepads")
3592 self:UnbindContextActions()
3593 return
3594 end
3595
3596 if self.activeGamepad ~= NONE then
3597 -- Switching from one active gamepad to another
3598 self:UnbindContextActions()
3599 end
3600
3601 self.activeGamepad = bestGamepad
3602 self:BindContextActions()
3603end
3604
3605function Gamepad:OnCurrentGamepadDisconnected()
3606 if self.activeGamepad ~= NONE then
3607 ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
3608 end
3609
3610 local bestGamepad = self:GetHighestPriorityGamepad()
3611
3612 if self.activeGamepad ~= NONE and bestGamepad == self.activeGamepad then
3613 warn("Gamepad:OnCurrentGamepadDisconnected found the supposedly disconnected gamepad in connectedGamepads.")
3614 self:UnbindContextActions()
3615 self.activeGamepad = NONE
3616 return
3617 end
3618
3619 if bestGamepad == NONE then
3620 -- No active gamepad, unbinding actions but leaving gamepad connection listener active
3621 self:UnbindContextActions()
3622 self.activeGamepad = NONE
3623 else
3624 -- Set new gamepad as active and bind to tool activation
3625 self.activeGamepad = bestGamepad
3626 ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
3627 end
3628end
3629
3630function Gamepad:ConnectGamepadConnectionListeners()
3631 self.gamepadConnectedConn = UserInputService.GamepadConnected:Connect(function(gamepadEnum)
3632 self:OnNewGamepadConnected()
3633 end)
3634
3635 self.gamepadDisconnectedConn = UserInputService.GamepadDisconnected:Connect(function(gamepadEnum)
3636 if self.activeGamepad == gamepadEnum then
3637 self:OnCurrentGamepadDisconnected()
3638 end
3639 end)
3640
3641end
3642
3643function Gamepad:DisconnectGamepadConnectionListeners()
3644 if self.gamepadConnectedConn then
3645 self.gamepadConnectedConn:Disconnect()
3646 self.gamepadConnectedConn = nil
3647 end
3648
3649 if self.gamepadDisconnectedConn then
3650 self.gamepadDisconnectedConn:Disconnect()
3651 self.gamepadDisconnectedConn = nil
3652 end
3653end
3654
3655return Gamepad
3656
3657
3658--[[
3659 Keyboard Character Control - This module handles controlling your avatar from a keyboard
3660
3661 2018 PlayerScripts Update - AllYourBlox
3662--]]
3663
3664--[[ Roblox Services ]]--
3665local UserInputService = game:GetService("UserInputService")
3666local ContextActionService = game:GetService("ContextActionService")
3667
3668--[[ Constants ]]--
3669local ZERO_VECTOR3 = Vector3.new(0,0,0)
3670
3671--[[ The Module ]]--
3672local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
3673local Keyboard = setmetatable({}, BaseCharacterController)
3674Keyboard.__index = Keyboard
3675
3676local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
3677 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
3678end)
3679local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
3680
3681function Keyboard.new(CONTROL_ACTION_PRIORITY)
3682 local self = setmetatable(BaseCharacterController.new(), Keyboard)
3683
3684 self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
3685
3686 self.textFocusReleasedConn = nil
3687 self.textFocusGainedConn = nil
3688 self.windowFocusReleasedConn = nil
3689
3690 self.forwardValue = 0
3691 self.backwardValue = 0
3692 self.leftValue = 0
3693 self.rightValue = 0
3694 return self
3695end
3696
3697function Keyboard:Enable(enable)
3698 if not UserInputService.KeyboardEnabled then
3699 return false
3700 end
3701
3702 if enable == self.enabled then
3703 -- Module is already in the state being requested. True is returned here since the module will be in the state
3704 -- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
3705 -- no action was necessary. False indicates failure to be in requested/expected state.
3706 return true
3707 end
3708
3709 self.forwardValue = 0
3710 self.backwardValue = 0
3711 self.leftValue = 0
3712 self.rightValue = 0
3713 self.moveVector = ZERO_VECTOR3
3714 self.isJumping = false
3715
3716 if enable then
3717 self:BindContextActions()
3718 self:ConnectFocusEventListeners()
3719 else
3720 self:UnbindContextActions()
3721 self:DisconnectFocusEventListeners()
3722 end
3723
3724 self.enabled = enable
3725 return true
3726end
3727
3728function Keyboard:UpdateMovement(inputState)
3729 if inputState == Enum.UserInputState.Cancel then
3730 self.moveVector = ZERO_VECTOR3
3731 else
3732 self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
3733 end
3734end
3735
3736function Keyboard:BindContextActions()
3737
3738 -- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
3739 -- which fixes them from getting stuck on.
3740 local handleMoveForward = function(actionName, inputState, inputObject)
3741 self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
3742 self:UpdateMovement(inputState)
3743 if FFlagPlayerScriptsBindAtPriority then
3744 return Enum.ContextActionResult.Sink
3745 end
3746 end
3747
3748 local handleMoveBackward = function(actionName, inputState, inputObject)
3749 self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
3750 self:UpdateMovement(inputState)
3751 if FFlagPlayerScriptsBindAtPriority then
3752 return Enum.ContextActionResult.Sink
3753 end
3754 end
3755
3756 local handleMoveLeft = function(actionName, inputState, inputObject)
3757 self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
3758 self:UpdateMovement(inputState)
3759 if FFlagPlayerScriptsBindAtPriority then
3760 return Enum.ContextActionResult.Sink
3761 end
3762 end
3763
3764 local handleMoveRight = function(actionName, inputState, inputObject)
3765 self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
3766 self:UpdateMovement(inputState)
3767 if FFlagPlayerScriptsBindAtPriority then
3768 return Enum.ContextActionResult.Sink
3769 end
3770 end
3771
3772 local handleJumpAction = function(actionName, inputState, inputObject)
3773 self.isJumping = (inputState == Enum.UserInputState.Begin)
3774 if FFlagPlayerScriptsBindAtPriority then
3775 return Enum.ContextActionResult.Sink
3776 end
3777 end
3778
3779 -- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to
3780 -- movement direction is done in Lua
3781 if FFlagPlayerScriptsBindAtPriority then
3782 ContextActionService:BindActionAtPriority("moveForwardAction", handleMoveForward, false,
3783 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterForward)
3784 ContextActionService:BindActionAtPriority("moveBackwardAction", handleMoveBackward, false,
3785 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterBackward)
3786 ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false,
3787 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft)
3788 ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false,
3789 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight)
3790 ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
3791 self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterJump)
3792 else
3793 ContextActionService:BindAction("moveForwardAction", handleMoveForward, false, Enum.PlayerActions.CharacterForward)
3794 ContextActionService:BindAction("moveBackwardAction", handleMoveBackward, false, Enum.PlayerActions.CharacterBackward)
3795 ContextActionService:BindAction("moveLeftAction", handleMoveLeft, false, Enum.PlayerActions.CharacterLeft)
3796 ContextActionService:BindAction("moveRightAction", handleMoveRight, false, Enum.PlayerActions.CharacterRight)
3797 ContextActionService:BindAction("jumpAction",handleJumpAction,false,Enum.PlayerActions.CharacterJump)
3798 end
3799end
3800
3801function Keyboard:UnbindContextActions()
3802 ContextActionService:UnbindAction("moveForwardAction")
3803 ContextActionService:UnbindAction("moveBackwardAction")
3804 ContextActionService:UnbindAction("moveLeftAction")
3805 ContextActionService:UnbindAction("moveRightAction")
3806 ContextActionService:UnbindAction("jumpAction")
3807end
3808
3809function Keyboard:ConnectFocusEventListeners()
3810 local function onFocusReleased()
3811 self.moveVector = ZERO_VECTOR3
3812 self.forwardValue = 0
3813 self.backwardValue = 0
3814 self.leftValue = 0
3815 self.rightValue = 0
3816 self.isJumping = false
3817 end
3818
3819 local function onTextFocusGained(textboxFocused)
3820 self.isJumping = false
3821 end
3822
3823 self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased)
3824 self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained)
3825 self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased)
3826end
3827
3828function Keyboard:DisconnectFocusEventListeners()
3829 if self.textFocusReleasedCon then
3830 self.textFocusReleasedCon:Disconnect()
3831 self.textFocusReleasedCon = nil
3832 end
3833 if self.textFocusGainedConn then
3834 self.textFocusGainedConn:Disconnect()
3835 self.textFocusGainedConn = nil
3836 end
3837 if self.windowFocusReleasedConn then
3838 self.windowFocusReleasedConn:Disconnect()
3839 self.windowFocusReleasedConn = nil
3840 end
3841end
3842
3843return Keyboard
3844
3845
3846--[[
3847 CameraModule - This ModuleScript implements a singleton class to manage the
3848 selection, activation, and deactivation of the current camera controller,
3849 character occlusion controller, and transparency controller. This script binds to
3850 RenderStepped at Camera priority and calls the Update() methods on the active
3851 controller instances.
3852
3853 The camera controller ModuleScripts implement classes which are instantiated and
3854 activated as-needed, they are no longer all instantiated up front as they were in
3855 the previous generation of PlayerScripts.
3856
3857 2018 PlayerScripts Update - AllYourBlox
3858--]]
3859
3860local CameraModule = {}
3861CameraModule.__index = CameraModule
3862
3863-- NOTICE: Player property names do not all match their StarterPlayer equivalents,
3864-- with the differences noted in the comments on the right
3865local PLAYER_CAMERA_PROPERTIES =
3866{
3867 "CameraMinZoomDistance",
3868 "CameraMaxZoomDistance",
3869 "CameraMode",
3870 "DevCameraOcclusionMode",
3871 "DevComputerCameraMode", -- Corresponds to StarterPlayer.DevComputerCameraMovementMode
3872 "DevTouchCameraMode", -- Corresponds to StarterPlayer.DevTouchCameraMovementMode
3873
3874 -- Character movement mode
3875 "DevComputerMovementMode",
3876 "DevTouchMovementMode",
3877 "DevEnableMouseLock", -- Corresponds to StarterPlayer.EnableMouseLockOption
3878}
3879
3880local USER_GAME_SETTINGS_PROPERTIES =
3881{
3882 "ComputerCameraMovementMode",
3883 "ComputerMovementMode",
3884 "ControlMode",
3885 "GamepadCameraSensitivity",
3886 "MouseSensitivity",
3887 "RotationType",
3888 "TouchCameraMovementMode",
3889 "TouchMovementMode",
3890}
3891
3892--[[ Roblox Services ]]--
3893local Players = game:GetService("Players")
3894local RunService = game:GetService("RunService")
3895local UserInputService = game:GetService("UserInputService")
3896local StarterPlayer = game:GetService("StarterPlayer")
3897local UserGameSettings = UserSettings():GetService("UserGameSettings")
3898
3899-- Camera math utility library
3900local CameraUtils = require(script:WaitForChild("CameraUtils"))
3901
3902-- Load Roblox Camera Controller Modules
3903local ClassicCamera = require(script:WaitForChild("ClassicCamera"))
3904local OrbitalCamera = require(script:WaitForChild("OrbitalCamera"))
3905local LegacyCamera = require(script:WaitForChild("LegacyCamera"))
3906
3907-- Load Roblox Occlusion Modules
3908local Invisicam = require(script:WaitForChild("Invisicam"))
3909local Poppercam do
3910 local success, useNewPoppercam = pcall(UserSettings().IsUserFeatureEnabled, UserSettings(), "UserNewPoppercam4")
3911 if success and useNewPoppercam then
3912 Poppercam = require(script:WaitForChild("Poppercam"))
3913 else
3914 Poppercam = require(script:WaitForChild("Poppercam_Classic"))
3915 end
3916end
3917
3918-- Load the near-field character transparency controller and the mouse lock "shift lock" controller
3919local TransparencyController = require(script:WaitForChild("TransparencyController"))
3920local MouseLockController = require(script:WaitForChild("MouseLockController"))
3921
3922-- Table of camera controllers that have been instantiated. They are instantiated as they are used.
3923local instantiatedCameraControllers = {}
3924local instantiatedOcclusionModules = {}
3925
3926-- Management of which options appear on the Roblox User Settings screen
3927do
3928 local PlayerScripts = Players.LocalPlayer:WaitForChild("PlayerScripts")
3929
3930 PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default)
3931 PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow)
3932 PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic)
3933
3934 PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default)
3935 PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow)
3936 PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic)
3937end
3938
3939
3940function CameraModule.new()
3941 local self = setmetatable({},CameraModule)
3942
3943 -- Current active controller instances
3944 self.activeCameraController = nil
3945 self.activeOcclusionModule = nil
3946 self.activeTransparencyController = nil
3947 self.activeMouseLockController = nil
3948
3949 self.currentComputerCameraMovementMode = nil
3950
3951 -- Connections to events
3952 self.cameraSubjectChangedConn = nil
3953 self.cameraTypeChangedConn = nil
3954
3955 -- Adds CharacterAdded and CharacterRemoving event handlers for all current players
3956 for _,player in pairs(Players:GetPlayers()) do
3957 self:OnPlayerAdded(player)
3958 end
3959
3960 -- Adds CharacterAdded and CharacterRemoving event handlers for all players who join in the future
3961 Players.PlayerAdded:Connect(function(player)
3962 self:OnPlayerAdded(player)
3963 end)
3964
3965 self.activeTransparencyController = TransparencyController.new()
3966 self.activeTransparencyController:Enable(true)
3967
3968 if not UserInputService.TouchEnabled then
3969 self.activeMouseLockController = MouseLockController.new()
3970 local toggleEvent = self.activeMouseLockController:GetBindableToggleEvent()
3971 if toggleEvent then
3972 toggleEvent:Connect(function()
3973 self:OnMouseLockToggled()
3974 end)
3975 end
3976 end
3977
3978 self:ActivateCameraController(self:GetCameraControlChoice())
3979 self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
3980 self:OnCurrentCameraChanged() -- Does initializations and makes first camera controller
3981 RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, function(dt) self:Update(dt) end)
3982
3983 -- Connect listeners to camera-related properties
3984 for _, propertyName in pairs(PLAYER_CAMERA_PROPERTIES) do
3985 Players.LocalPlayer:GetPropertyChangedSignal(propertyName):Connect(function()
3986 self:OnLocalPlayerCameraPropertyChanged(propertyName)
3987 end)
3988 end
3989
3990 for _, propertyName in pairs(USER_GAME_SETTINGS_PROPERTIES) do
3991 UserGameSettings:GetPropertyChangedSignal(propertyName):Connect(function()
3992 self:OnUserGameSettingsPropertyChanged(propertyName)
3993 end)
3994 end
3995 game.Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
3996 self:OnCurrentCameraChanged()
3997 end)
3998
3999 self.lastInputType = UserInputService:GetLastInputType()
4000 UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
4001 self.lastInputType = newLastInputType
4002 end)
4003
4004 return self
4005end
4006
4007
4008
4009
4010
4011function CameraModule:GetCameraMovementModeFromSettings()
4012 local cameraMode = Players.LocalPlayer.CameraMode
4013
4014 -- Lock First Person trumps all other settings and forces ClassicCamera
4015 if cameraMode == Enum.CameraMode.LockFirstPerson then
4016 return CameraUtils.ConvertCameraModeEnumToStandard(Enum.ComputerCameraMovementMode.Classic)
4017 end
4018
4019 local devMode, userMode
4020 if UserInputService.TouchEnabled then
4021 devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevTouchCameraMode)
4022 userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.TouchCameraMovementMode)
4023 else
4024 devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMode)
4025 userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
4026 end
4027
4028 if devMode == Enum.DevComputerCameraMovementMode.UserChoice then
4029 -- Developer is allowing user choice, so user setting is respected
4030 return userMode
4031 end
4032
4033 return devMode
4034end
4035
4036function CameraModule:ActivateOcclusionModule( occlusionMode )
4037 local newModuleCreator = nil
4038 if occlusionMode == Enum.DevCameraOcclusionMode.Zoom then
4039 newModuleCreator = Poppercam
4040 elseif occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
4041 newModuleCreator = Invisicam
4042 else
4043 warn("CameraScript ActivateOcclusionModule called with unsupported mode")
4044 return
4045 end
4046
4047 -- First check to see if there is actually a change. If the module being requested is already
4048 -- the currently-active solution then just make sure it's enabled and exit early
4049 if self.activeOcclusionModule and self.activeOcclusionModule:GetOcclusionMode() == occlusionMode then
4050 if not self.activeOcclusionModule:GetEnabled() then
4051 self.activeOcclusionModule:Enable(true)
4052 end
4053 return
4054 end
4055
4056 -- Save a reference to the current active module (may be nil) so that we can disable it if
4057 -- we are successful in activating its replacement
4058 local prevOcclusionModule = self.activeOcclusionModule
4059
4060 -- If there is no active module, see if the one we need has already been instantiated
4061 self.activeOcclusionModule = instantiatedOcclusionModules[newModuleCreator]
4062
4063 -- If the module was not already instantiated and selected above, instantiate it
4064 if not self.activeOcclusionModule then
4065 self.activeOcclusionModule = newModuleCreator.new()
4066 if self.activeOcclusionModule then
4067 instantiatedOcclusionModules[newModuleCreator] = self.activeOcclusionModule
4068 end
4069 end
4070
4071 -- If we were successful in either selecting or instantiating the module,
4072 -- enable it if it's not already the currently-active enabled module
4073 if self.activeOcclusionModule then
4074 local newModuleOcclusionMode = self.activeOcclusionModule:GetOcclusionMode()
4075 -- Sanity check that the module we selected or instantiated actually supports the desired occlusionMode
4076 if newModuleOcclusionMode ~= occlusionMode then
4077 warn("CameraScript ActivateOcclusionModule mismatch: ",self.activeOcclusionModule:GetOcclusionMode(),"~=",occlusionMode)
4078 end
4079
4080 -- Deactivate current module if there is one
4081 if prevOcclusionModule then
4082 -- Sanity check that current module is not being replaced by itself (that should have been handled above)
4083 if prevOcclusionModule ~= self.activeOcclusionModule then
4084 prevOcclusionModule:Enable(false)
4085 else
4086 warn("CameraScript ActivateOcclusionModule failure to detect already running correct module")
4087 end
4088 end
4089
4090 -- Occlusion modules need to be initialized with information about characters and cameraSubject
4091 -- Invisicam needs the LocalPlayer's character
4092 -- Poppercam needs all player characters and the camera subject
4093 if occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
4094 -- Optimization to only send Invisicam what we know it needs
4095 if Players.LocalPlayer.Character then
4096 self.activeOcclusionModule:CharacterAdded(Players.LocalPlayer.Character, Players.LocalPlayer )
4097 end
4098 else
4099 -- When Poppercam is enabled, we send it all existing player characters for its raycast ignore list
4100 for _, player in pairs(Players:GetPlayers()) do
4101 if player and player.Character then
4102 self.activeOcclusionModule:CharacterAdded(player.Character, player)
4103 end
4104 end
4105 self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
4106 end
4107
4108 -- Activate new choice
4109 self.activeOcclusionModule:Enable(true)
4110 end
4111end
4112
4113-- When supplied, legacyCameraType is used and cameraMovementMode is ignored (should be nil anyways)
4114-- Next, if userCameraCreator is passed in, that is used as the cameraCreator
4115function CameraModule:ActivateCameraController( cameraMovementMode, legacyCameraType )
4116 local newCameraCreator = nil
4117
4118 if legacyCameraType~=nil then
4119 --[[
4120 This function has been passed a CameraType enum value. Some of these map to the use of
4121 the LegacyCamera module, the value "Custom" will be translated to a movementMode enum
4122 value based on Dev and User settings, and "Scriptable" will disable the camera controller.
4123 --]]
4124
4125 if legacyCameraType == Enum.CameraType.Scriptable then
4126 if self.activeCameraController then
4127 self.activeCameraController:Enable(false)
4128 self.activeCameraController = nil
4129 return
4130 end
4131 elseif legacyCameraType == Enum.CameraType.Custom then
4132 cameraMovementMode = self:GetCameraMovementModeFromSettings()
4133
4134 elseif legacyCameraType == Enum.CameraType.Track then
4135 -- Note: The TrackCamera module was basically an older, less fully-featured
4136 -- version of ClassicCamera, no longer actively maintained, but it is re-implemented in
4137 -- case a game was dependent on its lack of ClassicCamera's extra functionality.
4138 cameraMovementMode = Enum.ComputerCameraMovementMode.Classic
4139
4140 elseif legacyCameraType == Enum.CameraType.Follow then
4141 cameraMovementMode = Enum.ComputerCameraMovementMode.Follow
4142
4143 elseif legacyCameraType == Enum.CameraType.Orbital then
4144 cameraMovementMode = Enum.ComputerCameraMovementMode.Orbital
4145
4146 elseif legacyCameraType == Enum.CameraType.Attach or
4147 legacyCameraType == Enum.CameraType.Watch or
4148 legacyCameraType == Enum.CameraType.Fixed then
4149 newCameraCreator = LegacyCamera
4150 else
4151 warn("CameraScript encountered an unhandled Camera.CameraType value: ",legacyCameraType)
4152 end
4153 end
4154
4155 if not newCameraCreator then
4156 if cameraMovementMode == Enum.ComputerCameraMovementMode.Classic or
4157 cameraMovementMode == Enum.ComputerCameraMovementMode.Follow or
4158 cameraMovementMode == Enum.ComputerCameraMovementMode.Default then
4159 newCameraCreator = ClassicCamera
4160 elseif cameraMovementMode == Enum.ComputerCameraMovementMode.Orbital then
4161 newCameraCreator = OrbitalCamera
4162 else
4163 warn("ActivateCameraController did not select a module.")
4164 return
4165 end
4166 end
4167
4168 -- Create the camera control module we need if it does not already exist in instantiatedCameraControllers
4169 local newCameraController = nil
4170 if not instantiatedCameraControllers[newCameraCreator] then
4171 newCameraController = newCameraCreator.new()
4172 instantiatedCameraControllers[newCameraCreator] = newCameraController
4173 else
4174 newCameraController = instantiatedCameraControllers[newCameraCreator]
4175 end
4176
4177 -- If there is a controller active and it's not the one we need, disable it,
4178 -- if it is the one we need, make sure it's enabled
4179 if self.activeCameraController then
4180 if self.activeCameraController ~= newCameraController then
4181 self.activeCameraController:Enable(false)
4182 self.activeCameraController = newCameraController
4183 self.activeCameraController:Enable(true)
4184 elseif not self.activeCameraController:GetEnabled() then
4185 self.activeCameraController:Enable(true)
4186 end
4187 elseif newCameraController ~= nil then
4188 self.activeCameraController = newCameraController
4189 self.activeCameraController:Enable(true)
4190 end
4191
4192 if self.activeCameraController then
4193 if cameraMovementMode~=nil then
4194 self.activeCameraController:SetCameraMovementMode(cameraMovementMode)
4195 elseif legacyCameraType~=nil then
4196 -- Note that this is only called when legacyCameraType is not a type that
4197 -- was convertible to a ComputerCameraMovementMode value, i.e. really only applies to LegacyCamera
4198 self.activeCameraController:SetCameraType(legacyCameraType)
4199 end
4200 end
4201end
4202
4203-- Note: The active transparency controller could be made to listen for this event itself.
4204function CameraModule:OnCameraSubjectChanged()
4205 if self.activeTransparencyController then
4206 self.activeTransparencyController:SetSubject(game.Workspace.CurrentCamera.CameraSubject)
4207 end
4208
4209 if self.activeOcclusionModule then
4210 self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
4211 end
4212end
4213
4214function CameraModule:OnCameraTypeChanged(newCameraType)
4215 if newCameraType == Enum.CameraType.Scriptable then
4216 if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then
4217 UserInputService.MouseBehavior = Enum.MouseBehavior.Default
4218 end
4219 end
4220
4221 -- Forward the change to ActivateCameraController to handle
4222 self:ActivateCameraController(nil, newCameraType)
4223end
4224
4225-- Note: Called whenever workspace.CurrentCamera changes, but also on initialization of this script
4226function CameraModule:OnCurrentCameraChanged()
4227 local currentCamera = game.Workspace.CurrentCamera
4228 if not currentCamera then return end
4229
4230 if self.cameraSubjectChangedConn then
4231 self.cameraSubjectChangedConn:Disconnect()
4232 end
4233
4234 if self.cameraTypeChangedConn then
4235 self.cameraTypeChangedConn:Disconnect()
4236 end
4237
4238 self.cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
4239 self:OnCameraSubjectChanged(currentCamera.CameraSubject)
4240 end)
4241
4242 self.cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):Connect(function()
4243 self:OnCameraTypeChanged(currentCamera.CameraType)
4244 end)
4245
4246 self:OnCameraSubjectChanged(currentCamera.CameraSubject)
4247 self:OnCameraTypeChanged(currentCamera.CameraType)
4248end
4249
4250function CameraModule:OnLocalPlayerCameraPropertyChanged(propertyName)
4251 if propertyName == "CameraMode" then
4252 -- CameraMode is only used to turn on/off forcing the player into first person view. The
4253 -- Note: The case "Classic" is used for all other views and does not correspond only to the ClassicCamera module
4254 if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then
4255 -- Locked in first person, use ClassicCamera which supports this
4256 if not self.activeCameraController or self.activeCameraController:GetModuleName() ~= "ClassicCamera" then
4257 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(Enum.DevComputerCameraMovementMode.Classic))
4258 end
4259
4260 if self.activeCameraController then
4261 self.activeCameraController:UpdateForDistancePropertyChange()
4262 end
4263 elseif Players.LocalPlayer.CameraMode == Enum.CameraMode.Classic then
4264 -- Not locked in first person view
4265 local cameraMovementMode =self: GetCameraMovementModeFromSettings()
4266 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
4267 else
4268 warn("Unhandled value for property player.CameraMode: ",Players.LocalPlayer.CameraMode)
4269 end
4270
4271 elseif propertyName == "DevComputerCameraMode" or
4272 propertyName == "DevTouchCameraMode" then
4273 local cameraMovementMode = self:GetCameraMovementModeFromSettings()
4274 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
4275
4276 elseif propertyName == "DevCameraOcclusionMode" then
4277 self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
4278
4279 elseif propertyName == "CameraMinZoomDistance" or propertyName == "CameraMaxZoomDistance" then
4280 if self.activeCameraController then
4281 self.activeCameraController:UpdateForDistancePropertyChange()
4282 end
4283 elseif propertyName == "DevTouchMovementMode" then
4284
4285 elseif propertyName == "DevComputerMovementMode" then
4286
4287 elseif propertyName == "DevEnableMouseLock" then
4288 -- This is the enabling/disabling of "Shift Lock" mode, not LockFirstPerson (which is a CameraMode)
4289
4290 -- Note: Enabling and disabling of MouseLock mode is normally only a publish-time choice made via
4291 -- the corresponding EnableMouseLockOption checkbox of StarterPlayer, and this script does not have
4292 -- support for changing the availability of MouseLock at runtime (this would require listening to
4293 -- Player.DevEnableMouseLock changes)
4294 end
4295end
4296
4297function CameraModule:OnUserGameSettingsPropertyChanged(propertyName)
4298
4299 if propertyName == "ComputerCameraMovementMode" then
4300 local cameraMovementMode = self:GetCameraMovementModeFromSettings()
4301 self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
4302 end
4303end
4304
4305
4306
4307--[[
4308 Main RenderStep Update. The camera controller and occlusion module both have opportunities
4309 to set and modify (respectively) the CFrame and Focus before it is set once on CurrentCamera.
4310 The camera and occlusion modules should only return CFrames, not set the CFrame property of
4311 CurrentCamera directly.
4312--]]
4313function CameraModule:Update(dt)
4314 if self.activeCameraController then
4315 local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
4316 self.activeCameraController:ApplyVRTransform()
4317 if self.activeOcclusionModule then
4318 newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus)
4319 end
4320
4321 -- Here is where the new CFrame and Focus are set for this render frame
4322 game.Workspace.CurrentCamera.CFrame = newCameraCFrame
4323 game.Workspace.CurrentCamera.Focus = newCameraFocus
4324
4325 -- Update to character local transparency as needed based on camera-to-subject distance
4326 if self.activeTransparencyController then
4327 self.activeTransparencyController:Update()
4328 end
4329 end
4330end
4331
4332-- Formerly getCurrentCameraMode, this function resolves developer and user camera control settings to
4333-- decide which camera control module should be instantiated. The old method of converting redundant enum types
4334function CameraModule:GetCameraControlChoice()
4335 local player = Players.LocalPlayer
4336
4337 if player then
4338 if self.lastInputType == Enum.UserInputType.Touch or UserInputService.TouchEnabled then
4339 -- Touch
4340 if player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then
4341 return CameraUtils.ConvertCameraModeEnumToStandard( UserGameSettings.TouchCameraMovementMode )
4342 else
4343 return CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode )
4344 end
4345 else
4346 -- Computer
4347 if player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then
4348 local computerMovementMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
4349 return CameraUtils.ConvertCameraModeEnumToStandard(computerMovementMode)
4350 else
4351 return CameraUtils.ConvertCameraModeEnumToStandard(player.DevComputerCameraMode)
4352 end
4353 end
4354 end
4355end
4356
4357
4358function CameraModule:OnCharacterAdded(char, player)
4359 if self.activeOcclusionModule then
4360 self.activeOcclusionModule:CharacterAdded(char, player)
4361 end
4362end
4363
4364function CameraModule:OnCharacterRemoving(char, player)
4365 if self.activeOcclusionModule then
4366 self.activeOcclusionModule:CharacterRemoving(char, player)
4367 end
4368end
4369
4370function CameraModule:OnPlayerAdded(player)
4371 player.CharacterAdded:Connect(function(char)
4372 self:OnCharacterAdded(char, player)
4373 end)
4374 player.CharacterRemoving:Connect(function(char)
4375 self:OnCharacterRemoving(char, player)
4376 end)
4377end
4378
4379function CameraModule:OnMouseLockToggled()
4380 if self.activeMouseLockController then
4381 local mouseLocked = self.activeMouseLockController:GetIsMouseLocked()
4382 local mouseLockOffset = self.activeMouseLockController:GetMouseLockOffset()
4383 if self.activeCameraController then
4384 self.activeCameraController:SetIsMouseLocked(mouseLocked)
4385 self.activeCameraController:SetMouseLockOffset(mouseLockOffset)
4386 end
4387 end
4388end
4389
4390
4391
4392return CameraModule.new()
4393
4394
4395--[[
4396 LegacyCamera - Implements legacy controller types: Attach, Fixed, Watch
4397 2018 Camera Update - AllYourBlox
4398--]]
4399
4400-- Local private variables and constants
4401local UNIT_X = Vector3.new(1,0,0)
4402local UNIT_Y = Vector3.new(0,1,0)
4403local UNIT_Z = Vector3.new(0,0,1)
4404local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
4405local ZERO_VECTOR3 = Vector3.new(0,0,0)
4406local ZERO_VECTOR2 = Vector2.new(0,0)
4407
4408local VR_PITCH_FRACTION = 0.25
4409local tweenAcceleration = math.rad(220) --Radians/Second^2
4410local tweenSpeed = math.rad(0) --Radians/Second
4411local tweenMaxSpeed = math.rad(250) --Radians/Second
4412local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
4413local PORTRAIT_OFFSET = Vector3.new(0,-3,0)
4414
4415local Util = require(script.Parent:WaitForChild("CameraUtils"))
4416
4417--[[ Services ]]--
4418local PlayersService = game:GetService('Players')
4419local VRService = game:GetService("VRService")
4420
4421--[[ The Module ]]--
4422local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
4423local LegacyCamera = setmetatable({}, BaseCamera)
4424LegacyCamera.__index = LegacyCamera
4425
4426function LegacyCamera.new()
4427 local self = setmetatable(BaseCamera.new(), LegacyCamera)
4428
4429 self.cameraType = Enum.CameraType.Fixed
4430 self.lastUpdate = tick()
4431 self.lastDistanceToSubject = nil
4432
4433 return self
4434end
4435
4436function LegacyCamera:GetModuleName()
4437 return "LegacyCamera"
4438end
4439
4440function LegacyCamera:Test()
4441 print("LegacyCamera:Test()")
4442end
4443
4444--[[ Functions overridden from BaseCamera ]]--
4445function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
4446 return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance)
4447end
4448
4449function LegacyCamera:Update(dt)
4450
4451 -- Cannot update until cameraType has been set
4452 if not self.cameraType then return end
4453
4454 local now = tick()
4455 local timeDelta = (now - self.lastUpdate)
4456 local camera = workspace.CurrentCamera
4457 local newCameraCFrame = camera.CFrame
4458 local newCameraFocus = camera.Focus
4459 local player = PlayersService.LocalPlayer
4460 local humanoid = self:GetHumanoid()
4461 local cameraSubject = camera and camera.CameraSubject
4462 local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
4463 local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
4464 local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
4465
4466 if self.lastUpdate == nil or timeDelta > 1 then
4467 self.lastDistanceToSubject = nil
4468 end
4469 local subjectPosition = self:GetSubjectPosition()
4470
4471 if self.cameraType == Enum.CameraType.Fixed then
4472 if self.lastUpdate then
4473 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
4474 local delta = math.min(0.1, now - self.lastUpdate)
4475 local gamepadRotation = self:UpdateGamepad()
4476 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
4477 end
4478
4479 if subjectPosition and player and camera then
4480 local distanceToSubject = self:GetCameraToSubjectDistance()
4481 local newLookVector = self:CalculateNewLookVector()
4482 self.rotateInput = ZERO_VECTOR2
4483
4484 newCameraFocus = camera.Focus -- Fixed camera does not change focus
4485 newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector))
4486 end
4487 elseif self.cameraType == Enum.CameraType.Attach then
4488 if subjectPosition and camera then
4489 local distanceToSubject = self:GetCameraToSubjectDistance()
4490 local humanoid = self:GetHumanoid()
4491 if self.lastUpdate and humanoid and humanoid.RootPart then
4492
4493 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
4494 local delta = math.min(0.1, now - self.lastUpdate)
4495 local gamepadRotation = self:UpdateGamepad()
4496 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
4497
4498 local forwardVector = humanoid.RootPart.CFrame.lookVector
4499
4500 local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
4501 if Util.IsFinite(y) then
4502 -- Preserve vertical rotation from user input
4503 self.rotateInput = Vector2.new(y, self.rotateInput.Y)
4504 end
4505 end
4506
4507 local newLookVector = self:CalculateNewLookVector()
4508 self.rotateInput = ZERO_VECTOR2
4509
4510 newCameraFocus = CFrame.new(subjectPosition)
4511 newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
4512 end
4513 elseif self.cameraType == Enum.CameraType.Watch then
4514 if subjectPosition and player and camera then
4515 local cameraLook = nil
4516
4517 local humanoid = self:GetHumanoid()
4518 if humanoid and humanoid.RootPart then
4519 local diffVector = subjectPosition - camera.CFrame.p
4520 cameraLook = diffVector.unit
4521
4522 if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then
4523 -- Don't clobber the zoom if they zoomed the camera
4524 local newDistanceToSubject = diffVector.magnitude
4525 self:SetCameraToSubjectDistance(newDistanceToSubject)
4526 end
4527 end
4528
4529 local distanceToSubject = self:GetCameraToSubjectDistance()
4530 local newLookVector = self:CalculateNewLookVector(cameraLook)
4531 self.rotateInput = ZERO_VECTOR2
4532
4533 newCameraFocus = CFrame.new(subjectPosition)
4534 newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
4535
4536 self.lastDistanceToSubject = distanceToSubject
4537 end
4538 else
4539 -- Unsupported type, return current values unchanged
4540 return camera.CFrame, camera.Focus
4541 end
4542
4543 self.lastUpdate = now
4544 return newCameraCFrame, newCameraFocus
4545end
4546
4547return LegacyCamera
4548
4549
4550--[[
4551 Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view.
4552--]]
4553
4554local ZoomController = require(script.Parent:WaitForChild("ZoomController"))
4555
4556local TransformExtrapolator = {} do
4557 TransformExtrapolator.__index = TransformExtrapolator
4558
4559 local CF_IDENTITY = CFrame.new()
4560
4561 local function cframeToAxis(cframe)
4562 local axis, angle = cframe:toAxisAngle()
4563 return axis*angle
4564 end
4565
4566 local function axisToCFrame(axis)
4567 local angle = axis.magnitude
4568 if angle > 1e-5 then
4569 return CFrame.fromAxisAngle(axis, angle)
4570 end
4571 return CF_IDENTITY
4572 end
4573
4574 local function extractRotation(cf)
4575 local _, _, _, xx, yx, zx, xy, yy, zy, xz, yz, zz = cf:components()
4576 return CFrame.new(0, 0, 0, xx, yx, zx, xy, yy, zy, xz, yz, zz)
4577 end
4578
4579 function TransformExtrapolator.new()
4580 return setmetatable({
4581 lastCFrame = nil,
4582 }, TransformExtrapolator)
4583 end
4584
4585 function TransformExtrapolator:Step(dt, currentCFrame)
4586 local lastCFrame = self.lastCFrame or currentCFrame
4587 self.lastCFrame = currentCFrame
4588
4589 local currentPos = currentCFrame.p
4590 local currentRot = extractRotation(currentCFrame)
4591
4592 local lastPos = lastCFrame.p
4593 local lastRot = extractRotation(lastCFrame)
4594
4595 -- Estimate velocities from the delta between now and the last frame
4596 -- This estimation can be a little noisy.
4597 local dp = (currentPos - lastPos)/dt
4598 local dr = cframeToAxis(currentRot*lastRot:inverse())/dt
4599
4600 local function extrapolate(t)
4601 local p = dp*t + currentPos
4602 local r = axisToCFrame(dr*t)*currentRot
4603 return r + p
4604 end
4605
4606 return {
4607 extrapolate = extrapolate,
4608 posVelocity = dp,
4609 rotVelocity = dr,
4610 }
4611 end
4612
4613 function TransformExtrapolator:Reset()
4614 self.lastCFrame = nil
4615 end
4616end
4617
4618--[[ The Module ]]--
4619local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
4620local Poppercam = setmetatable({}, BaseOcclusion)
4621Poppercam.__index = Poppercam
4622
4623function Poppercam.new()
4624 local self = setmetatable(BaseOcclusion.new(), Poppercam)
4625 self.focusExtrapolator = TransformExtrapolator.new()
4626 return self
4627end
4628
4629function Poppercam:GetOcclusionMode()
4630 return Enum.DevCameraOcclusionMode.Zoom
4631end
4632
4633function Poppercam:Enable(enable)
4634 self.focusExtrapolator:Reset()
4635end
4636
4637function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController)
4638 local rotatedFocus = CFrame.new(desiredCameraFocus.p, desiredCameraCFrame.p)*CFrame.new(
4639 0, 0, 0,
4640 -1, 0, 0,
4641 0, 1, 0,
4642 0, 0, -1
4643 )
4644 local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus)
4645 local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation)
4646 return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus
4647end
4648
4649-- Called when character is added
4650function Poppercam:CharacterAdded(character, player)
4651end
4652
4653-- Called when character is about to be removed
4654function Poppercam:CharacterRemoving(character, player)
4655end
4656
4657function Poppercam:OnCameraSubjectChanged(newSubject)
4658end
4659
4660return Poppercam
4661
4662--[[
4663 TransparencyController - Manages transparency of player character at close camera-to-subject distances
4664 2018 Camera Update - AllYourBlox
4665--]]
4666
4667local MAX_TWEEN_RATE = 2.8 -- per second
4668
4669local Util = require(script.Parent:WaitForChild("CameraUtils"))
4670
4671--[[ The Module ]]--
4672local TransparencyController = {}
4673TransparencyController.__index = TransparencyController
4674
4675function TransparencyController.new()
4676 local self = setmetatable({}, TransparencyController)
4677
4678 self.lastUpdate = tick()
4679 self.transparencyDirty = false
4680 self.enabled = false
4681 self.lastTransparency = nil
4682
4683 self.descendantAddedConn, self.descendantRemovingConn = nil, nil
4684 self.toolDescendantAddedConns = {}
4685 self.toolDescendantRemovingConns = {}
4686 self.cachedParts = {}
4687
4688 return self
4689end
4690
4691
4692function TransparencyController:HasToolAncestor(object)
4693 if object.Parent == nil then return false end
4694 return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent)
4695end
4696
4697function TransparencyController:IsValidPartToModify(part)
4698 if part:IsA('BasePart') or part:IsA('Decal') then
4699 return not self:HasToolAncestor(part)
4700 end
4701 return false
4702end
4703
4704function TransparencyController:CachePartsRecursive(object)
4705 if object then
4706 if self:IsValidPartToModify(object) then
4707 self.cachedParts[object] = true
4708 self.transparencyDirty = true
4709 end
4710 for _, child in pairs(object:GetChildren()) do
4711 self:CachePartsRecursive(child)
4712 end
4713 end
4714end
4715
4716function TransparencyController:TeardownTransparency()
4717 for child, _ in pairs(self.cachedParts) do
4718 child.LocalTransparencyModifier = 0
4719 end
4720 self.cachedParts = {}
4721 self.transparencyDirty = true
4722 self.lastTransparency = nil
4723
4724 if self.descendantAddedConn then
4725 self.descendantAddedConn:disconnect()
4726 self.descendantAddedConn = nil
4727 end
4728 if self.descendantRemovingConn then
4729 self.descendantRemovingConn:disconnect()
4730 self.descendantRemovingConn = nil
4731 end
4732 for object, conn in pairs(self.toolDescendantAddedConns) do
4733 conn:Disconnect()
4734 self.toolDescendantAddedConns[object] = nil
4735 end
4736 for object, conn in pairs(self.toolDescendantRemovingConns) do
4737 conn:Disconnect()
4738 self.toolDescendantRemovingConns[object] = nil
4739 end
4740end
4741
4742function TransparencyController:SetupTransparency(character)
4743 self:TeardownTransparency()
4744
4745 if self.descendantAddedConn then self.descendantAddedConn:disconnect() end
4746 self.descendantAddedConn = character.DescendantAdded:Connect(function(object)
4747 -- This is a part we want to invisify
4748 if self:IsValidPartToModify(object) then
4749 self.cachedParts[object] = true
4750 self.transparencyDirty = true
4751 -- There is now a tool under the character
4752 elseif object:IsA('Tool') then
4753 if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end
4754 self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild)
4755 self.cachedParts[toolChild] = nil
4756 if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
4757 -- Reset the transparency
4758 toolChild.LocalTransparencyModifier = 0
4759 end
4760 end)
4761 if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end
4762 self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild)
4763 wait() -- wait for new parent
4764 if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
4765 if self:IsValidPartToModify(formerToolChild) then
4766 self.cachedParts[formerToolChild] = true
4767 self.transparencyDirty = true
4768 end
4769 end
4770 end)
4771 end
4772 end)
4773 if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end
4774 self.descendantRemovingConn = character.DescendantRemoving:connect(function(object)
4775 if self.cachedParts[object] then
4776 self.cachedParts[object] = nil
4777 -- Reset the transparency
4778 object.LocalTransparencyModifier = 0
4779 end
4780 end)
4781 self:CachePartsRecursive(character)
4782end
4783
4784
4785function TransparencyController:Enable(enable)
4786 if self.enabled ~= enable then
4787 self.enabled = enable
4788 self:Update()
4789 end
4790end
4791
4792function TransparencyController:SetSubject(subject)
4793 local character = nil
4794 if subject and subject:IsA("Humanoid") then
4795 character = subject.Parent
4796 end
4797 if subject and subject:IsA("VehicleSeat") and subject.Occupant then
4798 character = subject.Occupant.Parent
4799 end
4800 if character then
4801 self:SetupTransparency(character)
4802 else
4803 self:TeardownTransparency()
4804 end
4805end
4806
4807function TransparencyController:Update()
4808 local instant = false
4809 local now = tick()
4810 local currentCamera = workspace.CurrentCamera
4811
4812 if currentCamera then
4813 local transparency = 0
4814 if not self.enabled then
4815 instant = true
4816 else
4817 local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
4818 transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 --(7 - distance) / 5
4819 if transparency < 0.5 then
4820 transparency = 0
4821 end
4822
4823 if self.lastTransparency then
4824 local deltaTransparency = transparency - self.lastTransparency
4825
4826 -- Don't tween transparency if it is instant or your character was fully invisible last frame
4827 if not instant and transparency < 1 and self.lastTransparency < 0.95 then
4828 local maxDelta = MAX_TWEEN_RATE * (now - self.lastUpdate)
4829 deltaTransparency = Util.Clamp(-maxDelta, maxDelta, deltaTransparency)
4830 end
4831 transparency = self.lastTransparency + deltaTransparency
4832 else
4833 self.transparencyDirty = true
4834 end
4835
4836 transparency = Util.Clamp(0, 1, Util.Round(transparency, 2))
4837 end
4838
4839 if self.transparencyDirty or self.lastTransparency ~= transparency then
4840 for child, _ in pairs(self.cachedParts) do
4841 child.LocalTransparencyModifier = transparency
4842 end
4843 self.transparencyDirty = false
4844 self.lastTransparency = transparency
4845 end
4846 end
4847 self.lastUpdate = now
4848end
4849
4850return TransparencyController
4851
4852
4853--[[
4854 OrbitalCamera - Spherical coordinates control camera for top-down games
4855 2018 Camera Update - AllYourBlox
4856--]]
4857
4858-- Local private variables and constants
4859local UNIT_X = Vector3.new(1,0,0)
4860local UNIT_Y = Vector3.new(0,1,0)
4861local UNIT_Z = Vector3.new(0,0,1)
4862local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
4863local ZERO_VECTOR3 = Vector3.new(0,0,0)
4864local ZERO_VECTOR2 = Vector2.new(0,0)
4865local TAU = 2 * math.pi
4866
4867local VR_PITCH_FRACTION = 0.25
4868local tweenAcceleration = math.rad(220) --Radians/Second^2
4869local tweenSpeed = math.rad(0) --Radians/Second
4870local tweenMaxSpeed = math.rad(250) --Radians/Second
4871local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
4872local PORTRAIT_OFFSET = Vector3.new(0,-3,0)
4873
4874local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
4875 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
4876end)
4877local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
4878
4879--[[ Gamepad Support ]]--
4880local THUMBSTICK_DEADZONE = 0.2
4881
4882-- Do not edit these values, they are not the developer-set limits, they are limits
4883-- to the values the camera system equations can correctly handle
4884local MIN_ALLOWED_ELEVATION_DEG = -80
4885local MAX_ALLOWED_ELEVATION_DEG = 80
4886
4887local externalProperties = {}
4888externalProperties["InitialDistance"] = 25
4889externalProperties["MinDistance"] = 10
4890externalProperties["MaxDistance"] = 100
4891externalProperties["InitialElevation"] = 35
4892externalProperties["MinElevation"] = 35
4893externalProperties["MaxElevation"] = 35
4894externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
4895externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
4896externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
4897externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
4898
4899local Util = require(script.Parent:WaitForChild("CameraUtils"))
4900
4901--[[ Services ]]--
4902local PlayersService = game:GetService('Players')
4903local VRService = game:GetService("VRService")
4904
4905--[[ Utility functions specific to OrbitalCamera ]]--
4906local function GetValueObject(name, defaultValue)
4907 local valueObj = script:FindFirstChild(name)
4908 if valueObj then
4909 return valueObj.Value
4910 end
4911 return defaultValue
4912end
4913
4914--[[ The Module ]]--
4915local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
4916local OrbitalCamera = setmetatable({}, BaseCamera)
4917OrbitalCamera.__index = OrbitalCamera
4918
4919
4920function OrbitalCamera.new()
4921 local self = setmetatable(BaseCamera.new(), OrbitalCamera)
4922
4923 self.lastUpdate = tick()
4924
4925 -- OrbitalCamera-specific members
4926 self.changedSignalConnections = {}
4927 self.refAzimuthRad = nil
4928 self.curAzimuthRad = nil
4929 self.minAzimuthAbsoluteRad = nil
4930 self.maxAzimuthAbsoluteRad = nil
4931 self.useAzimuthLimits = nil
4932 self.curElevationRad = nil
4933 self.minElevationRad = nil
4934 self.maxElevationRad = nil
4935 self.curDistance = nil
4936 self.minDistance = nil
4937 self.maxDistance = nil
4938
4939 -- Gamepad
4940 self.r3ButtonDown = false
4941 self.l3ButtonDown = false
4942 self.gamepadDollySpeedMultiplier = 1
4943
4944 self.lastUserPanCamera = tick()
4945
4946 self.externalProperties = {}
4947 self.externalProperties["InitialDistance"] = 25
4948 self.externalProperties["MinDistance"] = 10
4949 self.externalProperties["MaxDistance"] = 100
4950 self.externalProperties["InitialElevation"] = 35
4951 self.externalProperties["MinElevation"] = 35
4952 self.externalProperties["MaxElevation"] = 35
4953 self.externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
4954 self.externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
4955 self.externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
4956 self.externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
4957 self:LoadNumberValueParameters()
4958
4959 return self
4960end
4961
4962function OrbitalCamera:LoadOrCreateNumberValueParameter(name, valueType, updateFunction)
4963 local valueObj = script:FindFirstChild(name)
4964
4965 if valueObj and valueObj:isA(valueType) then
4966 -- Value object exists and is the correct type, use its value
4967 self.externalProperties[name] = valueObj.Value
4968 elseif self.externalProperties[name] ~= nil then
4969 -- Create missing (or replace incorrectly-typed) valueObject with default value
4970 valueObj = Instance.new(valueType)
4971 valueObj.Name = name
4972 valueObj.Parent = script
4973 valueObj.Value = self.externalProperties[name]
4974 else
4975 print("externalProperties table has no entry for ",name)
4976 return
4977 end
4978
4979 if updateFunction then
4980 if self.changedSignalConnections[name] then
4981 self.changedSignalConnections[name]:Disconnect()
4982 end
4983 self.changedSignalConnections[name] = valueObj.Changed:Connect(function(newValue)
4984 self.externalProperties[name] = newValue
4985 updateFunction(self)
4986 end)
4987 end
4988end
4989
4990function OrbitalCamera:SetAndBoundsCheckAzimuthValues()
4991 self.minAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(self.externalProperties["CWAzimuthTravel"]))
4992 self.maxAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(self.externalProperties["CCWAzimuthTravel"]))
4993 self.useAzimuthLimits = self.externalProperties["UseAzimuthLimits"]
4994 if self.useAzimuthLimits then
4995 self.curAzimuthRad = math.max(self.curAzimuthRad, self.minAzimuthAbsoluteRad)
4996 self.curAzimuthRad = math.min(self.curAzimuthRad, self.maxAzimuthAbsoluteRad)
4997 end
4998end
4999
5000function OrbitalCamera:SetAndBoundsCheckElevationValues()
5001 -- These degree values are the direct user input values. It is deliberate that they are
5002 -- ranged checked only against the extremes, and not against each other. Any time one
5003 -- is changed, both of the internal values in radians are recalculated. This allows for
5004 -- A developer to change the values in any order and for the end results to be that the
5005 -- internal values adjust to match intent as best as possible.
5006 local minElevationDeg = math.max(self.externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
5007 local maxElevationDeg = math.min(self.externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
5008
5009 -- Set internal values in radians
5010 self.minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
5011 self.maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
5012 self.curElevationRad = math.max(self.curElevationRad, self.minElevationRad)
5013 self.curElevationRad = math.min(self.curElevationRad, self.maxElevationRad)
5014end
5015
5016function OrbitalCamera:SetAndBoundsCheckDistanceValues()
5017 self.minDistance = self.externalProperties["MinDistance"]
5018 self.maxDistance = self.externalProperties["MaxDistance"]
5019 self.curDistance = math.max(self.curDistance, self.minDistance)
5020 self.curDistance = math.min(self.curDistance, self.maxDistance)
5021end
5022
5023-- This loads from, or lazily creates, NumberValue objects for exposed parameters
5024function OrbitalCamera:LoadNumberValueParameters()
5025 -- These initial values do not require change listeners since they are read only once
5026 self:LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
5027 self:LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
5028
5029 -- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
5030 self:LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", self.SetAndBoundsCheckAzimuthValue)
5031 self:LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
5032 self:LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
5033 self:LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
5034 self:LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
5035 self:LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
5036 self:LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
5037 self:LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", self.SetAndBoundsCheckAzimuthValues)
5038
5039 -- Internal values set (in radians, from degrees), plus sanitization
5040 self.curAzimuthRad = math.rad(self.externalProperties["ReferenceAzimuth"])
5041 self.curElevationRad = math.rad(self.externalProperties["InitialElevation"])
5042 self.curDistance = self.externalProperties["InitialDistance"]
5043
5044 self:SetAndBoundsCheckAzimuthValues()
5045 self:SetAndBoundsCheckElevationValues()
5046 self:SetAndBoundsCheckDistanceValues()
5047end
5048
5049function OrbitalCamera:GetModuleName()
5050 return "OrbitalCamera"
5051end
5052
5053function OrbitalCamera:SetInitialOrientation(humanoid)
5054 if not humanoid or not humanoid.RootPart then
5055 warn("OrbitalCamera could not set initial orientation due to missing humanoid")
5056 return
5057 end
5058 local newDesiredLook = (humanoid.RootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
5059 local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, self:GetCameraLookVector())
5060 local vertShift = math.asin(self:GetCameraLookVector().y) - math.asin(newDesiredLook.y)
5061 if not Util.IsFinite(horizontalShift) then
5062 horizontalShift = 0
5063 end
5064 if not Util.IsFinite(vertShift) then
5065 vertShift = 0
5066 end
5067 self.rotateInput = Vector2.new(horizontalShift, vertShift)
5068end
5069
5070--[[ Functions of BaseCamera that are overridden by OrbitalCamera ]]--
5071function OrbitalCamera:GetCameraToSubjectDistance()
5072 return self.curDistance
5073end
5074
5075function OrbitalCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
5076 print("OrbitalCamera SetCameraToSubjectDistance ",desiredSubjectDistance)
5077 local player = PlayersService.LocalPlayer
5078 if player then
5079 self.currentSubjectDistance = Util.Clamp(self.minDistance, self.maxDistance, desiredSubjectDistance)
5080
5081 -- OrbitalCamera is not allowed to go into the first-person range
5082 self.currentSubjectDistance = math.max(self.currentSubjectDistance, self.FIRST_PERSON_DISTANCE_THRESHOLD)
5083 end
5084 self.inFirstPerson = false
5085 self:UpdateMouseBehavior()
5086 return self.currentSubjectDistance
5087end
5088
5089function OrbitalCamera:CalculateNewLookVector(suppliedLookVector, xyRotateVector)
5090 local currLookVector = suppliedLookVector or self:GetCameraLookVector()
5091 local currPitchAngle = math.asin(currLookVector.y)
5092 local yTheta = Util.Clamp(currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG), xyRotateVector.y)
5093 local constrainedRotateInput = Vector2.new(xyRotateVector.x, yTheta)
5094 local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
5095 local newLookVector = (CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)).lookVector
5096 return newLookVector
5097end
5098
5099function OrbitalCamera:GetGamepadPan(name, state, input)
5100 if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
5101 if self.r3ButtonDown or self.l3ButtonDown then
5102 -- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
5103 if (input.Position.Y > THUMBSTICK_DEADZONE) then
5104 self.gamepadDollySpeedMultiplier = 0.96
5105 elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
5106 self.gamepadDollySpeedMultiplier = 1.04
5107 else
5108 self.gamepadDollySpeedMultiplier = 1.00
5109 end
5110 else
5111 if state == Enum.UserInputState.Cancel then
5112 self.gamepadPanningCamera = ZERO_VECTOR2
5113 return
5114 end
5115
5116 local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
5117 if inputVector.magnitude > THUMBSTICK_DEADZONE then
5118 self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
5119 else
5120 self.gamepadPanningCamera = ZERO_VECTOR2
5121 end
5122 end
5123 if FFlagPlayerScriptsBindAtPriority then
5124 return Enum.ContextActionResult.Sink
5125 end
5126 end
5127 if FFlagPlayerScriptsBindAtPriority then
5128 return Enum.ContextActionResult.Pass
5129 end
5130end
5131
5132function OrbitalCamera:DoGamepadZoom(name, state, input)
5133 if input.UserInputType == self.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
5134 if (state == Enum.UserInputState.Begin) then
5135 self.r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
5136 self.l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
5137 elseif (state == Enum.UserInputState.End) then
5138 if (input.KeyCode == Enum.KeyCode.ButtonR3) then
5139 self.r3ButtonDown = false
5140 elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
5141 self.l3ButtonDown = false
5142 end
5143 if (not self.r3ButtonDown) and (not self.l3ButtonDown) then
5144 self.gamepadDollySpeedMultiplier = 1.00
5145 end
5146 end
5147 if FFlagPlayerScriptsBindAtPriority then
5148 return Enum.ContextActionResult.Sink
5149 end
5150 end
5151 if FFlagPlayerScriptsBindAtPriority then
5152 return Enum.ContextActionResult.Pass
5153 end
5154end
5155
5156function OrbitalCamera:BindGamepadInputActions()
5157 if FFlagPlayerScriptsBindAtPriority then
5158 self:BindAction("OrbitalCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end,
5159 false, Enum.KeyCode.Thumbstick2)
5160 self:BindAction("OrbitalCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end,
5161 false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonL3)
5162 else
5163 local ContextActionService = game:GetService('ContextActionService')
5164
5165 ContextActionService:BindAction("OrbitalCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2)
5166 ContextActionService:BindAction("OrbitalCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3)
5167 ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonL3)
5168 end
5169end
5170
5171
5172-- [[ Update ]]--
5173function OrbitalCamera:Update(dt)
5174 local now = tick()
5175 local timeDelta = (now - self.lastUpdate)
5176 local userPanningTheCamera = (self.UserPanningTheCamera == true)
5177 local camera = workspace.CurrentCamera
5178 local newCameraCFrame = camera.CFrame
5179 local newCameraFocus = camera.Focus
5180 local player = PlayersService.LocalPlayer
5181 local humanoid = self:GetHumanoid()
5182 local cameraSubject = camera and camera.CameraSubject
5183 local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
5184 local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
5185
5186 if self.lastUpdate == nil or timeDelta > 1 then
5187 self.lastCameraTransform = nil
5188 end
5189
5190 if self.lastUpdate then
5191 local gamepadRotation = self:UpdateGamepad()
5192
5193 if self:ShouldUseVRRotation() then
5194 self.RotateInput = self.RotateInput + self:GetVRRotationInput()
5195 else
5196 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
5197 local delta = math.min(0.1, timeDelta)
5198
5199 if gamepadRotation ~= ZERO_VECTOR2 then
5200 userPanningTheCamera = true
5201 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
5202 end
5203
5204 local angle = 0
5205 if not (isInVehicle or isOnASkateboard) then
5206 angle = angle + (self.TurningLeft and -120 or 0)
5207 angle = angle + (self.TurningRight and 120 or 0)
5208 end
5209
5210 if angle ~= 0 then
5211 self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
5212 userPanningTheCamera = true
5213 end
5214 end
5215 end
5216
5217 -- Reset tween speed if user is panning
5218 if userPanningTheCamera then
5219 tweenSpeed = 0
5220 self.lastUserPanCamera = tick()
5221 end
5222
5223 local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
5224 local subjectPosition = self:GetSubjectPosition()
5225
5226 if subjectPosition and player and camera then
5227
5228 -- Process any dollying being done by gamepad
5229 -- TODO: Move this
5230 if self.gamepadDollySpeedMultiplier ~= 1 then
5231 self:SetCameraToSubjectDistance(self.currentSubjectDistance * self.gamepadDollySpeedMultiplier)
5232 end
5233
5234 local VREnabled = VRService.VREnabled
5235 newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition)
5236
5237 local cameraFocusP = newCameraFocus.p
5238 if VREnabled and not self:IsInFirstPerson() then
5239 local cameraHeight = self:GetCameraHeight()
5240 local vecToSubject = (subjectPosition - camera.CFrame.p)
5241 local distToSubject = vecToSubject.magnitude
5242
5243 -- Only move the camera if it exceeded a maximum distance to the subject in VR
5244 if distToSubject > self.currentSubjectDistance or self.rotateInput.x ~= 0 then
5245 local desiredDist = math.min(distToSubject, self.currentSubjectDistance)
5246
5247 -- Note that CalculateNewLookVector is overridden from BaseCamera
5248 vecToSubject = self:CalculateNewLookVector(vecToSubject.unit * X1_Y0_Z1, Vector2.new(self.rotateInput.x, 0)) * desiredDist
5249
5250 local newPos = cameraFocusP - vecToSubject
5251 local desiredLookDir = camera.CFrame.lookVector
5252 if self.rotateInput.x ~= 0 then
5253 desiredLookDir = vecToSubject
5254 end
5255 local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
5256 self.RotateInput = ZERO_VECTOR2
5257
5258 newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
5259 end
5260 else
5261 -- self.RotateInput is a Vector2 of mouse movement deltas since last update
5262 self.curAzimuthRad = self.curAzimuthRad - self.rotateInput.x
5263
5264 if self.useAzimuthLimits then
5265 self.curAzimuthRad = Util.Clamp(self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad, self.curAzimuthRad)
5266 else
5267 self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0
5268 end
5269
5270 self.curElevationRad = Util.Clamp(self.minElevationRad, self.maxElevationRad, self.curElevationRad + self.rotateInput.y)
5271
5272 local cameraPosVector = self.currentSubjectDistance * ( CFrame.fromEulerAnglesYXZ( -self.curElevationRad, self.curAzimuthRad, 0 ) * UNIT_Z )
5273 local camPos = subjectPosition + cameraPosVector
5274
5275 newCameraCFrame = CFrame.new(camPos, subjectPosition)
5276
5277 self.rotateInput = ZERO_VECTOR2
5278 end
5279
5280 self.lastCameraTransform = newCameraCFrame
5281 self.lastCameraFocus = newCameraFocus
5282 if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
5283 self.lastSubjectCFrame = cameraSubject.CFrame
5284 else
5285 self.lastSubjectCFrame = nil
5286 end
5287 end
5288
5289 self.lastUpdate = now
5290 return newCameraCFrame, newCameraFocus
5291end
5292
5293return OrbitalCamera
5294
5295
5296--[[
5297 MouseLockController - Replacement for ShiftLockController, manages use of mouse-locked mode
5298 2018 Camera Update - AllYourBlox
5299--]]
5300
5301--[[ Constants ]]--
5302local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png"
5303
5304local CONTEXT_ACTION_NAME = "MouseLockSwitchAction"
5305local MOUSELOCK_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
5306
5307local Util = require(script.Parent:WaitForChild("CameraUtils"))
5308
5309--[[ Services ]]--
5310local PlayersService = game:GetService("Players")
5311local UserInputService = game:GetService("UserInputService")
5312local ContextActionService = game:GetService("ContextActionService")
5313local Settings = UserSettings() -- ignore warning
5314local GameSettings = Settings.GameSettings
5315local Mouse = PlayersService.LocalPlayer:GetMouse()
5316
5317--[[ Variables ]]
5318local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
5319 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
5320end)
5321local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
5322
5323--[[ The Module ]]--
5324local MouseLockController = {}
5325MouseLockController.__index = MouseLockController
5326
5327function MouseLockController.new()
5328 local self = setmetatable({}, MouseLockController)
5329
5330 self.inputBeganConn = nil -- Remove with FFlagPlayerScriptsBindAtPriority
5331 self.isMouseLocked = false
5332 self.savedMouseCursor = nil
5333 self.boundKeys = {Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift} -- defaults
5334
5335 self.mouseLockToggledEvent = Instance.new("BindableEvent")
5336
5337 local boundKeysObj = script:FindFirstChild("BoundKeys")
5338 if (not boundKeysObj) or (not boundKeysObj:IsA("StringValue")) then
5339 -- If object with correct name was found, but it's not a StringValue, destroy and replace
5340 if boundKeysObj then
5341 boundKeysObj:Destroy()
5342 end
5343
5344 boundKeysObj = Instance.new("StringValue")
5345 boundKeysObj.Name = "BoundKeys"
5346 boundKeysObj.Value = "LeftShift,RightShift"
5347 boundKeysObj.Parent = script
5348 end
5349
5350 if boundKeysObj then
5351 boundKeysObj.Changed:Connect(function(value)
5352 self:OnBoundKeysObjectChanged(value)
5353 end)
5354 self:OnBoundKeysObjectChanged(boundKeysObj.Value) -- Initial setup call
5355 end
5356
5357 -- Watch for changes to user's ControlMode and ComputerMovementMode settings and update the feature availability accordingly
5358 GameSettings.Changed:Connect(function(property)
5359 if property == "ControlMode" or property == "ComputerMovementMode" then
5360 self:UpdateMouseLockAvailability()
5361 end
5362 end)
5363
5364 -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
5365 PlayersService.LocalPlayer:GetPropertyChangedSignal("DevEnableMouseLock"):Connect(function()
5366 self:UpdateMouseLockAvailability()
5367 end)
5368
5369 -- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
5370 PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function()
5371 self:UpdateMouseLockAvailability()
5372 end)
5373
5374 self:UpdateMouseLockAvailability()
5375
5376 return self
5377end
5378
5379function MouseLockController:GetIsMouseLocked()
5380 return self.isMouseLocked
5381end
5382
5383function MouseLockController:GetBindableToggleEvent()
5384 return self.mouseLockToggledEvent.Event
5385end
5386
5387function MouseLockController:GetMouseLockOffset()
5388 local offsetValueObj = script:FindFirstChild("CameraOffset")
5389 if offsetValueObj and offsetValueObj:IsA("Vector3Value") then
5390 return offsetValueObj.Value
5391 else
5392 -- If CameraOffset object was found but not correct type, destroy
5393 if offsetValueObj then
5394 offsetValueObj:Destroy()
5395 end
5396 offsetValueObj = Instance.new("Vector3Value")
5397 offsetValueObj.Name = "CameraOffset"
5398 offsetValueObj.Value = Vector3.new(1.75,0,0) -- Legacy Default Value
5399 offsetValueObj.Parent = script
5400 end
5401
5402 if offsetValueObj and offsetValueObj.Value then
5403 return offsetValueObj.Value
5404 end
5405
5406 return Vector3.new(1.75,0,0)
5407end
5408
5409function MouseLockController:UpdateMouseLockAvailability()
5410 local devAllowsMouseLock = PlayersService.LocalPlayer.DevEnableMouseLock
5411 local devMovementModeIsScriptable = PlayersService.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable
5412 local userHasMouseLockModeEnabled = GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch
5413 local userHasClickToMoveEnabled = GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove
5414 local MouseLockAvailable = devAllowsMouseLock and userHasMouseLockModeEnabled and not userHasClickToMoveEnabled and not devMovementModeIsScriptable
5415
5416 if MouseLockAvailable~=self.enabled then
5417 self:EnableMouseLock(MouseLockAvailable)
5418 end
5419end
5420
5421function MouseLockController:OnBoundKeysObjectChanged(newValue)
5422 self.boundKeys = {} -- Overriding defaults, note: possibly with nothing at all if boundKeysObj.Value is "" or contains invalid values
5423 for token in string.gmatch(newValue,"[^%s,]+") do
5424 for keyCode, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do
5425 if token == keyEnum.Name then
5426 self.boundKeys[#self.boundKeys+1] = keyEnum
5427 break
5428 end
5429 end
5430 end
5431 if FFlagPlayerScriptsBindAtPriority then
5432 self:UnbindContextActions()
5433 self:BindContextActions()
5434 end
5435end
5436
5437--[[ Local Functions ]]--
5438function MouseLockController:OnMouseLockToggled()
5439 self.isMouseLocked = not self.isMouseLocked
5440
5441 if self.isMouseLocked then
5442 local cursorImageValueObj = script:FindFirstChild("CursorImage")
5443 if cursorImageValueObj and cursorImageValueObj:IsA("StringValue") and cursorImageValueObj.Value then
5444 self.savedMouseCursor = Mouse.Icon
5445 Mouse.Icon = cursorImageValueObj.Value
5446 else
5447 if cursorImageValueObj then
5448 cursorImageValueObj:Destroy()
5449 end
5450 cursorImageValueObj = Instance.new("StringValue")
5451 cursorImageValueObj.Name = "CursorImage"
5452 cursorImageValueObj.Value = DEFAULT_MOUSE_LOCK_CURSOR
5453 cursorImageValueObj.Parent = script
5454 self.savedMouseCursor = Mouse.Icon
5455 Mouse.Icon = DEFAULT_MOUSE_LOCK_CURSOR
5456 end
5457 else
5458 if self.savedMouseCursor then
5459 Mouse.Icon = self.savedMouseCursor
5460 self.savedMouseCursor = nil
5461 end
5462 end
5463
5464 self.mouseLockToggledEvent:Fire()
5465end
5466
5467-- Remove with FFlagPlayerScriptsBindAtPriority
5468function MouseLockController:OnInputBegan(input, processed)
5469 if processed then return end
5470
5471 if input.UserInputType == Enum.UserInputType.Keyboard then
5472 for _, keyCode in pairs(self.boundKeys) do
5473 if keyCode == input.KeyCode then
5474 self:OnMouseLockToggled()
5475 return
5476 end
5477 end
5478 end
5479end
5480
5481function MouseLockController:DoMouseLockSwitch(name, state, input)
5482 if state == Enum.UserInputState.Begin then
5483 self:OnMouseLockToggled()
5484 return Enum.ContextActionResult.Sink
5485 end
5486 return Enum.ContextActionResult.Pass
5487end
5488
5489function MouseLockController:BindContextActions()
5490 ContextActionService:BindActionAtPriority(CONTEXT_ACTION_NAME, function(name, state, input) self:DoMouseLockSwitch(name, state, input) end,
5491 false, MOUSELOCK_ACTION_PRIORITY, unpack(self.boundKeys))
5492end
5493
5494function MouseLockController:UnbindContextActions()
5495 ContextActionService:UnbindAction(CONTEXT_ACTION_NAME)
5496end
5497
5498function MouseLockController:IsMouseLocked()
5499 return self.enabled and self.isMouseLocked
5500end
5501
5502function MouseLockController:EnableMouseLock(enable)
5503 if enable~=self.enabled then
5504
5505 self.enabled = enable
5506
5507 if self.enabled then
5508 -- Enabling the mode
5509 if FFlagPlayerScriptsBindAtPriority then
5510 self:BindContextActions()
5511 else
5512 if self.inputBeganConn then
5513 self.inputBeganConn:Disconnect()
5514 end
5515 self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
5516 self:OnInputBegan(input, processed)
5517 end)
5518 end
5519 else
5520 -- Disabling
5521 -- Restore mouse cursor
5522 if Mouse.Icon~="" then
5523 Mouse.Icon = ""
5524 end
5525
5526 if FFlagPlayerScriptsBindAtPriority then
5527 self:UnbindContextActions()
5528 else
5529 if self.inputBeganConn then
5530 self.inputBeganConn:Disconnect()
5531 end
5532 self.inputBeganConn = nil
5533 end
5534
5535 -- If the mode is disabled while being used, fire the event to toggle it off
5536 if self.isMouseLocked then
5537 self.mouseLockToggledEvent:Fire()
5538 end
5539
5540 self.isMouseLocked = false
5541 end
5542
5543 end
5544end
5545
5546return MouseLockController
5547
5548
5549--[[
5550 Invisicam - Occlusion module that makes objects occluding character view semi-transparent
5551 2018 Camera Update - AllYourBlox
5552--]]
5553
5554--[[ Camera Maths Utilities Library ]]--
5555local Util = require(script.Parent:WaitForChild("CameraUtils"))
5556
5557--[[ Top Level Roblox Services ]]--
5558local PlayersService = game:GetService("Players")
5559local RunService = game:GetService("RunService")
5560
5561--[[ Constants ]]--
5562local ZERO_VECTOR3 = Vector3.new(0,0,0)
5563local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY
5564local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays
5565local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays
5566
5567local MODE = {
5568 --CUSTOM = 1, -- Retired, unused
5569 LIMBS = 2, -- Track limbs
5570 MOVEMENT = 3, -- Track movement
5571 CORNERS = 4, -- Char model corners
5572 CIRCLE1 = 5, -- Circle of casts around character
5573 CIRCLE2 = 6, -- Circle of casts around character, camera relative
5574 LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode
5575 SMART_CIRCLE = 8, -- More sample points on and around character
5576 CHAR_OUTLINE = 9, -- Dynamic outline around the character
5577}
5578
5579local LIMB_TRACKING_SET = {
5580 -- Body parts common to R15 and R6
5581 ['Head'] = true,
5582
5583 -- Body parts unique to R6
5584 ['Left Arm'] = true,
5585 ['Right Arm'] = true,
5586 ['Left Leg'] = true,
5587 ['Right Leg'] = true,
5588
5589 -- Body parts unique to R15
5590 ['LeftLowerArm'] = true,
5591 ['RightLowerArm'] = true,
5592 ['LeftUpperLeg'] = true,
5593 ['RightUpperLeg'] = true
5594}
5595
5596local CORNER_FACTORS = {
5597 Vector3.new(1,1,-1),
5598 Vector3.new(1,-1,-1),
5599 Vector3.new(-1,-1,-1),
5600 Vector3.new(-1,1,-1)
5601}
5602
5603local CIRCLE_CASTS = 10
5604local MOVE_CASTS = 3
5605local SMART_CIRCLE_CASTS = 24
5606local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS
5607local CHAR_OUTLINE_CASTS = 24
5608
5609-- Used to sanitize user-supplied functions
5610local function AssertTypes(param, ...)
5611 local allowedTypes = {}
5612 local typeString = ''
5613 for _, typeName in pairs({...}) do
5614 allowedTypes[typeName] = true
5615 typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName
5616 end
5617 local theType = type(param)
5618 assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType)
5619end
5620
5621-- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons
5622local function Det3x3(a,b,c,d,e,f,g,h,i)
5623 return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g))
5624end
5625
5626-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane
5627-- because they are generated from cross products with a common vector. This function is computing
5628-- that intersection, but it's actually the general solution for the point halfway between where
5629-- two skew lines come nearest to each other, which is more forgiving.
5630local function RayIntersection(p0, v0, p1, v1)
5631 local v2 = v0:Cross(v1)
5632 local d1 = p1.x - p0.x
5633 local d2 = p1.y - p0.y
5634 local d3 = p1.z - p0.z
5635 local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z)
5636
5637 if (denom == 0) then
5638 return ZERO_VECTOR3 -- No solution (rays are parallel)
5639 end
5640
5641 local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom
5642 local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom
5643 local s0 = p0 + t0 * v0
5644 local s1 = p1 + t1 * v1
5645 local s = s0 + 0.5 * ( s1 - s0 )
5646
5647 -- 0.25 studs is a threshold for deciding if the rays are
5648 -- close enough to be considered intersecting, found through testing
5649 if (s1-s0).Magnitude < 0.25 then
5650 return s
5651 else
5652 return ZERO_VECTOR3
5653 end
5654end
5655
5656
5657
5658--[[ The Module ]]--
5659local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
5660local Invisicam = setmetatable({}, BaseOcclusion)
5661Invisicam.__index = Invisicam
5662
5663function Invisicam.new()
5664 local self = setmetatable(BaseOcclusion.new(), Invisicam)
5665
5666 self.char = nil
5667 self.humanoidRootPart = nil
5668 self.torsoPart = nil
5669 self.headPart = nil
5670
5671 self.childAddedConn = nil
5672 self.childRemovedConn = nil
5673
5674 self.behaviors = {} -- Map of modes to behavior fns
5675 self.behaviors[MODE.LIMBS] = self.LimbBehavior
5676 self.behaviors[MODE.MOVEMENT] = self.MoveBehavior
5677 self.behaviors[MODE.CORNERS] = self.CornerBehavior
5678 self.behaviors[MODE.CIRCLE1] = self.CircleBehavior
5679 self.behaviors[MODE.CIRCLE2] = self.CircleBehavior
5680 self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior
5681 self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior
5682 self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior
5683
5684 self.mode = MODE.SMART_CIRCLE
5685 self.behaviorFunction = self.SmartCircleBehavior
5686
5687
5688 self.savedHits = {} -- Objects currently being faded in/out
5689 self.trackedLimbs = {} -- Used in limb-tracking casting modes
5690
5691 self.camera = game.Workspace.CurrentCamera
5692
5693 self.enabled = false
5694 return self
5695end
5696
5697function Invisicam:Enable(enable)
5698 self.enabled = enable
5699
5700 if not enable then
5701 self:Cleanup()
5702 end
5703end
5704
5705function Invisicam:GetOcclusionMode()
5706 return Enum.DevCameraOcclusionMode.Invisicam
5707end
5708
5709--[[ Module functions ]]--
5710function Invisicam:LimbBehavior(castPoints)
5711 for limb, _ in pairs(self.trackedLimbs) do
5712 castPoints[#castPoints + 1] = limb.Position
5713 end
5714end
5715
5716function Invisicam:MoveBehavior(castPoints)
5717 for i = 1, MOVE_CASTS do
5718 local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity
5719 local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2
5720 local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed
5721 castPoints[#castPoints + 1] = position + offsetVector
5722 end
5723end
5724
5725function Invisicam:CornerBehavior(castPoints)
5726 local cframe = self.humanoidRootPart.CFrame
5727 local centerPoint = cframe.p
5728 local rotation = cframe - centerPoint
5729 local halfSize = self.char:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations
5730 castPoints[#castPoints + 1] = centerPoint
5731 for i = 1, #CORNER_FACTORS do
5732 castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i]))
5733 end
5734end
5735
5736function Invisicam:CircleBehavior(castPoints)
5737 local cframe = nil
5738 if self.mode == MODE.CIRCLE1 then
5739 cframe = self.humanoidRootPart.CFrame
5740 else
5741 local camCFrame = self.camera.CoordinateFrame
5742 cframe = camCFrame - camCFrame.p + self.humanoidRootPart.Position
5743 end
5744 castPoints[#castPoints + 1] = cframe.p
5745 for i = 0, CIRCLE_CASTS - 1 do
5746 local angle = (2 * math.pi / CIRCLE_CASTS) * i
5747 local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0)
5748 castPoints[#castPoints + 1] = cframe * offset
5749 end
5750end
5751
5752function Invisicam:LimbMoveBehavior(castPoints)
5753 self:LimbBehavior(castPoints)
5754 self:MoveBehavior(castPoints)
5755end
5756
5757function Invisicam:CharacterOutlineBehavior(castPoints)
5758 local torsoUp = self.torsoPart.CFrame.upVector.unit
5759 local torsoRight = self.torsoPart.CFrame.rightVector.unit
5760
5761 -- Torso cross of points for interior coverage
5762 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
5763 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
5764 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
5765 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
5766 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
5767 if self.headPart then
5768 castPoints[#castPoints + 1] = self.headPart.CFrame.p
5769 end
5770
5771 local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z))
5772 local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
5773
5774 local partsWhitelist = {self.torsoPart}
5775 if self.headPart then
5776 partsWhitelist[#partsWhitelist + 1] = self.headPart
5777 end
5778
5779 for i = 1, CHAR_OUTLINE_CASTS do
5780 local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS)
5781 local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0))
5782
5783 offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z)
5784
5785 local ray = Ray.new(centerPoint + offset, -3 * offset)
5786 local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false)
5787
5788 if hit then
5789 -- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against
5790 -- walls is less likely to cause a transparency glitch
5791 castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit
5792 end
5793 end
5794end
5795
5796function Invisicam:SmartCircleBehavior(castPoints)
5797 local torsoUp = self.torsoPart.CFrame.upVector.unit
5798 local torsoRight = self.torsoPart.CFrame.rightVector.unit
5799
5800 -- SMART_CIRCLE mode includes rays to head and 5 to the torso.
5801 -- Hands, arms, legs and feet are not included since they
5802 -- are not canCollide and can therefore go inside of parts
5803 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
5804 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
5805 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
5806 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
5807 castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
5808 if self.headPart then
5809 castPoints[#castPoints + 1] = self.headPart.CFrame.p
5810 end
5811
5812 local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p
5813 local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
5814 local radius = 2.5
5815
5816 -- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the
5817 -- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within
5818 -- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that
5819 -- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the
5820 -- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are
5821 -- behind, below or beside the character and not really obstructing view of the character. This minimizes
5822 -- the undesirable situation where the character walks up to an exterior wall and it is made invisible even
5823 -- though it is behind the character.
5824 for i = 1, SMART_CIRCLE_CASTS do
5825 local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi
5826 local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0)
5827 local circlePoint = torsoPoint + cameraOrientation * offset
5828
5829 -- Vector from camera to point on the circle being tested
5830 local vp = circlePoint - self.camera.CFrame.p
5831
5832 local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
5833 local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
5834 local castPoint = circlePoint
5835
5836 if hit then
5837 local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface
5838 local v0 = hprime - torsoPoint -- Vector from torso to offset hit point
5839 local d0 = v0.magnitude
5840
5841 local perp = (v0:Cross(vp)).unit
5842
5843 -- Vector from the offset hit point, along the hit surface
5844 local v1 = (perp:Cross(hitNormal)).unit
5845
5846 -- Vector from camera to offset hit
5847 local vprime = (hprime - self.camera.CFrame.p).unit
5848
5849 -- This dot product checks to see if the vector along the hit surface would hit the correct
5850 -- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side
5851 if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then
5852 castPoint = RayIntersection(hprime, v1, circlePoint, vp)
5853
5854 if castPoint.Magnitude > 0 then
5855 local ray = Ray.new(hprime, castPoint - hprime)
5856 local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
5857
5858 if hit then
5859 local hprime2 = hitPoint + 0.1 * hitNormal.unit
5860 castPoint = hprime2
5861 end
5862 else
5863 castPoint = hprime
5864 end
5865 else
5866 castPoint = hprime
5867 end
5868
5869 local ray = Ray.new(torsoPoint, (castPoint - torsoPoint))
5870 local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
5871
5872 if hit then
5873 local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
5874 castPoint = castPoint2
5875 end
5876 end
5877
5878 castPoints[#castPoints + 1] = castPoint
5879 end
5880end
5881
5882function Invisicam:CheckTorsoReference()
5883 if self.char then
5884 self.torsoPart = self.char:FindFirstChild("Torso")
5885 if not self.torsoPart then
5886 self.torsoPart = self.char:FindFirstChild("UpperTorso")
5887 if not self.torsoPart then
5888 self.torsoPart = self.char:FindFirstChild("HumanoidRootPart")
5889 end
5890 end
5891
5892 self.headPart = self.char:FindFirstChild("Head")
5893 end
5894end
5895
5896function Invisicam:CharacterAdded(char, player)
5897 -- We only want the LocalPlayer's character
5898 if player~=PlayersService.LocalPlayer then return end
5899
5900 if self.childAddedConn then
5901 self.childAddedConn:Disconnect()
5902 self.childAddedConn = nil
5903 end
5904 if self.childRemovedConn then
5905 self.childRemovedConn:Disconnect()
5906 self.childRemovedConn = nil
5907 end
5908
5909 self.char = char
5910
5911 self.trackedLimbs = {}
5912 local function childAdded(child)
5913 if child:IsA("BasePart") then
5914 if LIMB_TRACKING_SET[child.Name] then
5915 self.trackedLimbs[child] = true
5916 end
5917
5918 if (child.Name == "Torso" or child.Name == "UpperTorso") then
5919 self.torsoPart = child
5920 end
5921
5922 if (child.Name == "Head") then
5923 self.headPart = child
5924 end
5925 end
5926 end
5927
5928 local function childRemoved(child)
5929 self.trackedLimbs[child] = nil
5930
5931 -- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
5932 self:CheckTorsoReference()
5933 end
5934
5935 self.childAddedConn = char.ChildAdded:Connect(childAdded)
5936 self.childRemovedConn = char.ChildRemoved:Connect(childRemoved)
5937 for _, child in pairs(self.char:GetChildren()) do
5938 childAdded(child)
5939 end
5940end
5941
5942function Invisicam:SetMode(newMode)
5943 AssertTypes(newMode, 'number')
5944 for modeName, modeNum in pairs(MODE) do
5945 if modeNum == newMode then
5946 self.mode = newMode
5947 self.behaviorFunction = self.behaviors[self.mode]
5948 return
5949 end
5950 end
5951 error("Invalid mode number")
5952end
5953
5954function Invisicam:GetObscuredParts()
5955 return self.savedHits
5956end
5957
5958-- Want to turn off Invisicam? Be sure to call this after.
5959function Invisicam:Cleanup()
5960 for hit, originalFade in pairs(self.savedHits) do
5961 hit.LocalTransparencyModifier = originalFade
5962 end
5963end
5964
5965function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
5966
5967 -- Bail if there is no Character
5968 if not self.enabled or not self.char then
5969 return desiredCameraCFrame, desiredCameraFocus
5970 end
5971
5972 self.camera = game.Workspace.CurrentCamera
5973
5974 -- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference
5975 -- Make sure we still have a HumanoidRootPart
5976 if not self.humanoidRootPart then
5977 local humanoid = self.char:FindFirstChildOfClass("Humanoid")
5978 if humanoid and humanoid.RootPart then
5979 self.humanoidRootPart = humanoid.RootPart
5980 else
5981 -- Not set up with Humanoid? Try and see if there's one in the Character at all:
5982 self.humanoidRootPart = self.char:FindFirstChild("HumanoidRootPart")
5983 if not self.humanoidRootPart then
5984 -- Bail out, since we're relying on HumanoidRootPart existing
5985 return desiredCameraCFrame, desiredCameraFocus
5986 end
5987 end
5988
5989 -- TODO: Replace this with something more sensible
5990 local ancestryChangedConn
5991 ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent)
5992 if child == self.humanoidRootPart and not parent then
5993 self.humanoidRootPart = nil
5994 if ancestryChangedConn and ancestryChangedConn.Connected then
5995 ancestryChangedConn:Disconnect()
5996 ancestryChangedConn = nil
5997 end
5998 end
5999 end)
6000 end
6001
6002 if not self.torsoPart then
6003 self:CheckTorsoReference()
6004 if not self.torsoPart then
6005 -- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
6006 return desiredCameraCFrame, desiredCameraFocus
6007 end
6008 end
6009
6010 -- Make a list of world points to raycast to
6011 local castPoints = {}
6012 self.behaviorFunction(self, castPoints)
6013
6014 -- Cast to get a list of objects between the camera and the cast points
6015 local currentHits = {}
6016 local ignoreList = {self.char}
6017 local function add(hit)
6018 currentHits[hit] = true
6019 if not self.savedHits[hit] then
6020 self.savedHits[hit] = hit.LocalTransparencyModifier
6021 end
6022 end
6023
6024 local hitParts
6025 local hitPartCount = 0
6026
6027 -- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays
6028 -- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled
6029 local headTorsoRayHitParts = {}
6030 local partIsTouchingCamera = {}
6031
6032 local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY
6033 local perPartTransparencyOtherHits = TARGET_TRANSPARENCY
6034
6035 if USE_STACKING_TRANSPARENCY then
6036
6037 -- This first call uses head and torso rays to find out how many parts are stacked up
6038 -- for the purpose of calculating required per-part transparency
6039 local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1]
6040 local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2]
6041 hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList)
6042
6043 -- Count how many things the sample rays passed through, including decals. This should only
6044 -- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals,
6045 -- so my compromise for now is to just let any decal increase the part count by 1. Only one
6046 -- decal per part will be considered.
6047 for i = 1, #hitParts do
6048 local hitPart = hitParts[i]
6049 hitPartCount = hitPartCount + 1 -- count the part itself
6050 headTorsoRayHitParts[hitPart] = true
6051 for _, child in pairs(hitPart:GetChildren()) do
6052 if child:IsA('Decal') or child:IsA('Texture') then
6053 hitPartCount = hitPartCount + 1 -- count first decal hit, then break
6054 break
6055 end
6056 end
6057 end
6058
6059 if (hitPartCount > 0) then
6060 perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount )
6061 perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount )
6062 end
6063 end
6064
6065 -- Now get all the parts hit by all the rays
6066 hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList)
6067
6068 local partTargetTransparency = {}
6069
6070 -- Include decals and textures
6071 for i = 1, #hitParts do
6072 local hitPart = hitParts[i]
6073
6074 partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits
6075
6076 -- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of
6077 -- parts to be modified by invisicam
6078 if hitPart.Transparency < partTargetTransparency[hitPart] then
6079 add(hitPart)
6080 end
6081
6082 -- Check all decals and textures on the part
6083 for _, child in pairs(hitPart:GetChildren()) do
6084 if child:IsA('Decal') or child:IsA('Texture') then
6085 if (child.Transparency < partTargetTransparency[hitPart]) then
6086 partTargetTransparency[child] = partTargetTransparency[hitPart]
6087 add(child)
6088 end
6089 end
6090 end
6091 end
6092
6093 -- Invisibilize objects that are in the way, restore those that aren't anymore
6094 for hitPart, originalLTM in pairs(self.savedHits) do
6095 if currentHits[hitPart] then
6096 -- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency
6097 hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0
6098 else -- Restore original pre-invisicam value of LTM
6099 hitPart.LocalTransparencyModifier = originalLTM
6100 self.savedHits[hitPart] = nil
6101 end
6102 end
6103
6104 -- Invisicam does not change the camera values
6105 return desiredCameraCFrame, desiredCameraFocus
6106end
6107
6108return Invisicam
6109
6110--[[
6111 ClassicCamera - Classic Roblox camera control module
6112 2018 Camera Update - AllYourBlox
6113
6114 Note: This module also handles camera control types Follow and Track, the
6115 latter of which is currently not distinguished from Classic
6116--]]
6117
6118-- Local private variables and constants
6119local ZERO_VECTOR2 = Vector2.new(0,0)
6120
6121local tweenAcceleration = math.rad(220) --Radians/Second^2
6122local tweenSpeed = math.rad(0) --Radians/Second
6123local tweenMaxSpeed = math.rad(250) --Radians/Second
6124local TIME_BEFORE_AUTO_ROTATE = 1.0 --Seconds, used when auto-aligning camera with vehicles
6125local PORTRAIT_OFFSET = Vector3.new(0, 2, 0)
6126local MOBILE_OFFSET = Vector3.new(0, 1, 0)
6127local MinSpeed_BeforeReset = 10
6128
6129local INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0)
6130
6131--[[ Services ]]--
6132local PlayersService = game:GetService('Players')
6133local VRService = game:GetService("VRService")
6134
6135local Util = require(script.Parent:WaitForChild("CameraUtils"))
6136
6137local newDefaultCameraAngleFlagExists, newDefaultCameraAngleFlagEnabled = pcall(function()
6138 return UserSettings():IsUserFeatureEnabled("UserNewDefaultCameraAngle")
6139end)
6140local FFlagUserNewDefaultCameraAngle = newDefaultCameraAngleFlagExists and newDefaultCameraAngleFlagEnabled
6141
6142--[[ The Module ]]--
6143local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
6144local ClassicCamera = setmetatable({}, BaseCamera)
6145local lastSign = 1
6146ClassicCamera.__index = ClassicCamera
6147
6148function ClassicCamera.new()
6149 local self = setmetatable(BaseCamera.new(), ClassicCamera)
6150
6151 self.isFollowCamera = false
6152 self.lastUpdate = tick()
6153
6154 return self
6155end
6156
6157function ClassicCamera:GetModuleName()
6158 return "ClassicCamera"
6159end
6160
6161-- Movement mode standardized to Enum.ComputerCameraMovementMode values
6162function ClassicCamera:SetCameraMovementMode( cameraMovementMode )
6163 BaseCamera.SetCameraMovementMode(self,cameraMovementMode)
6164 self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow
6165end
6166
6167function ClassicCamera:Test()
6168 print("ClassicCamera:Test()")
6169end
6170
6171function ClassicCamera:GetCameraSubjectOffset()
6172 if self:IsInFirstPerson() then
6173 return Vector3.new(0, 0, 0)
6174 elseif self.portraitMode then
6175 return PORTRAIT_OFFSET
6176 elseif self.isSmallTouchScreen then
6177 return MOBILE_OFFSET
6178 end
6179 return Vector3.new(0, 0, 0)
6180end
6181
6182function ClassicCamera:Update()
6183 local now = tick()
6184 local timeDelta = (now - self.lastUpdate)
6185
6186 local camera = workspace.CurrentCamera
6187 local newCameraCFrame = camera.CFrame
6188 local newCameraFocus = camera.Focus
6189 local overrideCameraLookVector = nil
6190 if FFlagUserNewDefaultCameraAngle and self.resetCameraAngle then
6191 local rootPart = self:GetHumanoidRootPart()
6192 if rootPart then
6193 overrideCameraLookVector = (rootPart.CFrame * INITIAL_CAMERA_ANGLE).lookVector
6194 else
6195 overrideCameraLookVector = INITIAL_CAMERA_ANGLE.lookVector
6196 end
6197 self.resetCameraAngle = false
6198 end
6199
6200 local player = PlayersService.LocalPlayer
6201 local humanoid = self:GetHumanoid()
6202 local cameraSubject = camera.CameraSubject
6203
6204 local VehicleVelocityPassed;
6205 local char = player.Character;
6206 if char then
6207 local stateinfo = char:FindFirstChild("StateInfo");
6208 if stateinfo then
6209 local val = stateinfo:FindFirstChild("VehicleVelocity");
6210 if val then
6211 if val.Value >= 1 then
6212 VehicleVelocityPassed=true;
6213 end
6214 end
6215 end
6216 end
6217
6218 local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat') or VehicleVelocityPassed
6219 local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
6220 local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
6221
6222 if self.lastUpdate == nil or timeDelta > 1 then
6223 self.lastCameraTransform = nil
6224 end
6225
6226 if self.lastUpdate then
6227 local gamepadRotation = self:UpdateGamepad()
6228
6229 if self:ShouldUseVRRotation() then
6230 self.rotateInput = self.rotateInput + self:GetVRRotationInput()
6231 else
6232 -- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
6233 local delta = math.min(0.1, timeDelta)
6234
6235 if gamepadRotation ~= ZERO_VECTOR2 then
6236 self.rotateInput = self.rotateInput + (gamepadRotation * delta)
6237 end
6238
6239 local angle = 0
6240 if not (isInVehicle or isOnASkateboard) then
6241 angle = angle + (self.turningLeft and -120 or 0)
6242 angle = angle + (self.turningRight and 120 or 0)
6243 end
6244
6245 if angle ~= 0 then
6246 self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
6247 end
6248 end
6249 end
6250
6251 -- Reset tween speed if user is panning
6252 if self.userPanningTheCamera then
6253 tweenSpeed = 0
6254 self.lastUserPanCamera = tick()
6255 end
6256
6257 local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
6258 local subjectPosition = self:GetSubjectPosition()
6259
6260 if subjectPosition and player and camera then
6261 local zoom = self:GetCameraToSubjectDistance()
6262 if zoom < 0.5 then
6263 zoom = 0.5
6264 end
6265
6266 if self:GetIsMouseLocked() and not self:IsInFirstPerson() then
6267 -- We need to use the right vector of the camera after rotation, not before
6268 local newLookCFrame = self:CalculateNewLookCFrame(overrideCameraLookVector)
6269
6270 local offset = self:GetMouseLockOffset()
6271 local cameraRelativeOffset = offset.X * newLookCFrame.rightVector + offset.Y * newLookCFrame.upVector + offset.Z * newLookCFrame.lookVector
6272
6273 --offset can be NAN, NAN, NAN if newLookVector has only y component
6274 if Util.IsFiniteVector3(cameraRelativeOffset) then
6275 subjectPosition = subjectPosition + cameraRelativeOffset
6276 end
6277 else
6278 if not self.userPanningTheCamera and self.lastCameraTransform then
6279
6280 local isInFirstPerson = self:IsInFirstPerson()
6281 local currentVelocity = humanoid.Torso.Velocity
6282
6283 if (isInVehicle or isOnASkateboard or (self.isFollowCamera and isClimbing)) and self.lastUpdate and humanoid and humanoid.Torso then
6284 if isInFirstPerson then
6285 if self.lastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
6286 local y = -Util.GetAngleBetweenXZVectors(self.lastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
6287 if Util.IsFinite(y) then
6288 self.rotateInput = self.rotateInput + Vector2.new(y, 0)
6289 end
6290 tweenSpeed = 0
6291 end
6292 elseif not userRecentlyPannedCamera and currentVelocity.Magnitude > MinSpeed_BeforeReset then --Main vehicle part
6293 local forwardVector = humanoid.Torso.CFrame.lookVector
6294
6295 if isOnASkateboard then
6296 forwardVector = cameraSubject.CFrame.lookVector
6297 end
6298
6299 local sign_dot_forward_velocity = forwardVector:Dot(currentVelocity) < 0 and -1 or 1 --math.sign returns 0 at 0
6300 forwardVector = sign_dot_forward_velocity*forwardVector
6301 if lastSign ~= sign_dot_forward_velocity then --reset tween speed when reversing velocity direction
6302 lastSign = sign_dot_forward_velocity
6303 tweenSpeed = 0
6304 end
6305
6306 tweenSpeed = Util.Clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
6307
6308 local percent = Util.Clamp(0, 1, tweenSpeed * timeDelta)
6309 if self:IsInFirstPerson() and not (self.isFollowCamera and self.isClimbing) then
6310 percent = 1
6311 end
6312
6313 local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
6314 if Util.IsFinite(y) and math.abs(y) > 0.0001 then
6315 self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0)
6316 end
6317 end
6318
6319 elseif self.isFollowCamera and (not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled) then
6320 -- Logic that was unique to the old FollowCamera module
6321 local lastVec = -(self.lastCameraTransform.p - subjectPosition)
6322
6323 local y = Util.GetAngleBetweenXZVectors(lastVec, self:GetCameraLookVector())
6324
6325 -- This cutoff is to decide if the humanoid's angle of movement,
6326 -- relative to the camera's look vector, is enough that
6327 -- we want the camera to be following them. The point is to provide
6328 -- a sizable dead zone to allow more precise forward movements.
6329 local thetaCutoff = 0.4
6330
6331 -- Check for NaNs
6332 if Util.IsFinite(y) and math.abs(y) > 0.0001 and math.abs(y) > thetaCutoff * timeDelta then
6333 self.rotateInput = self.rotateInput + Vector2.new(y, 0)
6334 end
6335 end
6336 end
6337 end
6338
6339 if not self.isFollowCamera then
6340 local VREnabled = VRService.VREnabled
6341
6342 if FFlagUserNewDefaultCameraAngle then
6343 if VREnabled then
6344 newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
6345 else
6346 newCameraFocus = CFrame.new(subjectPosition + self:GetCameraSubjectOffset())
6347 end
6348 else
6349 newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition)
6350 end
6351
6352 local cameraFocusP = newCameraFocus.p
6353 if VREnabled and not self:IsInFirstPerson() then
6354 local cameraHeight = self:GetCameraHeight()
6355 local vecToSubject = (subjectPosition - camera.CFrame.p)
6356 local distToSubject = vecToSubject.magnitude
6357
6358 -- Only move the camera if it exceeded a maximum distance to the subject in VR
6359 if distToSubject > zoom or self.rotateInput.x ~= 0 then
6360 local desiredDist = math.min(distToSubject, zoom)
6361 vecToSubject = self:CalculateNewLookVectorVR() * desiredDist
6362 local newPos = cameraFocusP - vecToSubject
6363 local desiredLookDir = camera.CFrame.lookVector
6364 if self.rotateInput.x ~= 0 then
6365 desiredLookDir = vecToSubject
6366 end
6367 local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
6368 self.rotateInput = ZERO_VECTOR2
6369
6370 newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
6371 end
6372 else
6373 local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
6374 self.rotateInput = ZERO_VECTOR2
6375 newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
6376 end
6377 else -- is FollowCamera
6378 local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
6379 self.rotateInput = ZERO_VECTOR2
6380
6381 if FFlagUserNewDefaultCameraAngle then
6382 if VRService.VREnabled then
6383 newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
6384 else
6385 newCameraFocus = CFrame.new(subjectPosition + self:GetCameraSubjectOffset())
6386 end
6387 else
6388 if VRService.VREnabled then
6389 newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
6390 elseif self.portraitMode then
6391 newCameraFocus = CFrame.new(subjectPosition + PORTRAIT_OFFSET)
6392 else
6393 newCameraFocus = CFrame.new(subjectPosition)
6394 end
6395 end
6396 newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, self:GetCameraHeight(), 0)
6397 end
6398
6399 self.lastCameraTransform = newCameraCFrame
6400 self.lastCameraFocus = newCameraFocus
6401 if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
6402 self.lastSubjectCFrame = cameraSubject.CFrame
6403 else
6404 self.lastSubjectCFrame = nil
6405 end
6406 end
6407
6408 self.lastUpdate = now
6409 return newCameraCFrame, newCameraFocus
6410end
6411
6412function ClassicCamera:EnterFirstPerson()
6413 self.inFirstPerson = true
6414 self:UpdateMouseBehavior()
6415end
6416
6417function ClassicCamera:LeaveFirstPerson()
6418 self.inFirstPerson = false
6419 self:UpdateMouseBehavior()
6420end
6421
6422return ClassicCamera
6423
6424
6425--[[
6426 CameraUtils - Math utility functions shared by multiple camera scripts
6427 2018 Camera Update - AllYourBlox
6428--]]
6429
6430local CameraUtils = {}
6431
6432local function round(num)
6433 return math.floor(num + 0.5)
6434end
6435
6436-- Note, arguments do not match the new math.clamp
6437-- Eventually we will replace these calls with math.clamp, but right now
6438-- this is safer as math.clamp is not tolerant of min>max
6439function CameraUtils.Clamp(low, high, val)
6440 return math.min(math.max(val, low), high)
6441end
6442
6443-- From TransparencyController
6444function CameraUtils.Round(num, places)
6445 local decimalPivot = 10^places
6446 return math.floor(num * decimalPivot + 0.5) / decimalPivot
6447end
6448
6449function CameraUtils.IsFinite(val)
6450 return val == val and val ~= math.huge and val ~= -math.huge
6451end
6452
6453function CameraUtils.IsFiniteVector3(vec3)
6454 return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z)
6455end
6456
6457-- Legacy implementation renamed
6458function CameraUtils.GetAngleBetweenXZVectors(v1, v2)
6459 return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
6460end
6461
6462function CameraUtils.RotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount)
6463 if camLook.Magnitude > 0 then
6464 camLook = camLook.unit
6465 local currAngle = math.atan2(camLook.z, camLook.x)
6466 local newAngle = round((math.atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount
6467 return newAngle - currAngle
6468 end
6469 return 0
6470end
6471
6472-- K is a tunable parameter that changes the shape of the S-curve
6473-- the larger K is the more straight/linear the curve gets
6474local k = 0.35
6475local lowerK = 0.8
6476local function SCurveTranform(t)
6477 t = CameraUtils.Clamp(-1,1,t)
6478 if t >= 0 then
6479 return (k*t) / (k - t + 1)
6480 end
6481 return -((lowerK*-t) / (lowerK + t + 1))
6482end
6483
6484local DEADZONE = 0.1
6485local function toSCurveSpace(t)
6486 return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE
6487end
6488
6489local function fromSCurveSpace(t)
6490 return t/2 + 0.5
6491end
6492
6493function CameraUtils.GamepadLinearToCurve(thumbstickPosition)
6494 local function onAxis(axisValue)
6495 local sign = 1
6496 if axisValue < 0 then
6497 sign = -1
6498 end
6499 local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue))))
6500 point = point * sign
6501 return CameraUtils.Clamp(-1, 1, point)
6502 end
6503 return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y))
6504end
6505
6506-- This function converts 4 different, redundant enumeration types to one standard so the values can be compared
6507function CameraUtils.ConvertCameraModeEnumToStandard( enumValue )
6508
6509 if enumValue == Enum.TouchCameraMovementMode.Default then
6510 return Enum.ComputerCameraMovementMode.Follow
6511 end
6512
6513 if enumValue == Enum.ComputerCameraMovementMode.Default then
6514 return Enum.ComputerCameraMovementMode.Classic
6515 end
6516
6517 if enumValue == Enum.TouchCameraMovementMode.Classic or
6518 enumValue == Enum.DevTouchCameraMovementMode.Classic or
6519 enumValue == Enum.DevComputerCameraMovementMode.Classic or
6520 enumValue == Enum.ComputerCameraMovementMode.Classic then
6521 return Enum.ComputerCameraMovementMode.Classic
6522 end
6523
6524 if enumValue == Enum.TouchCameraMovementMode.Follow or
6525 enumValue == Enum.DevTouchCameraMovementMode.Follow or
6526 enumValue == Enum.DevComputerCameraMovementMode.Follow or
6527 enumValue == Enum.ComputerCameraMovementMode.Follow then
6528 return Enum.ComputerCameraMovementMode.Follow
6529 end
6530
6531 if enumValue == Enum.TouchCameraMovementMode.Orbital or
6532 enumValue == Enum.DevTouchCameraMovementMode.Orbital or
6533 enumValue == Enum.DevComputerCameraMovementMode.Orbital or
6534 enumValue == Enum.ComputerCameraMovementMode.Orbital then
6535 return Enum.ComputerCameraMovementMode.Orbital
6536 end
6537
6538 -- Note: Only the Dev versions of the Enums have UserChoice as an option
6539 if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or
6540 enumValue == Enum.DevComputerCameraMovementMode.UserChoice then
6541 return Enum.DevComputerCameraMovementMode.UserChoice
6542 end
6543
6544 -- For any unmapped options return Classic camera
6545 return Enum.ComputerCameraMovementMode.Classic
6546end
6547
6548return CameraUtils
6549
6550
6551
6552--[[
6553 BaseOcclusion - Abstract base class for character occlusion control modules
6554 2018 Camera Update - AllYourBlox
6555--]]
6556
6557--[[ The Module ]]--
6558local BaseOcclusion = {}
6559BaseOcclusion.__index = BaseOcclusion
6560setmetatable(BaseOcclusion, { __call = function(_, ...) return BaseOcclusion.new(...) end})
6561
6562function BaseOcclusion.new()
6563 local self = setmetatable({}, BaseOcclusion)
6564 return self
6565end
6566
6567-- Called when character is added
6568function BaseOcclusion:CharacterAdded(char, player)
6569
6570end
6571
6572-- Called when character is about to be removed
6573function BaseOcclusion:CharacterRemoving(char, player)
6574
6575end
6576
6577function BaseOcclusion:OnCameraSubjectChanged(newSubject)
6578
6579end
6580
6581--[[ Derived classes are required to override and implement all of the following functions ]]--
6582function GetOcclusionMode()
6583 -- Must be overridden in derived classes to return an Enum.DevCameraOcclusionMode value
6584 warn("BaseOcclusion GetOcclusionMode must be overridden by derived classes")
6585 return nil
6586end
6587
6588function BaseOcclusion:Enable(enabled)
6589 warn("BaseOcclusion Enable must be overridden by derived classes")
6590end
6591
6592function BaseOcclusion:Update(dt, desiredCameraCFrame, desiredCameraFocus)
6593 warn("BaseOcclusion Update must be overridden by derived classes")
6594 return desiredCameraCFrame, desiredCameraFocus
6595end
6596
6597return BaseOcclusion
6598
6599
6600--[[
6601 BaseCamera - Abstract base class for camera control modules
6602 2018 Camera Update - AllYourBlox
6603--]]
6604
6605--[[ Local Constants ]]--
6606local UNIT_Z = Vector3.new(0,0,1)
6607local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
6608
6609local THUMBSTICK_DEADZONE = 0.2
6610local DEFAULT_DISTANCE = 12.5 -- Studs
6611local PORTRAIT_DEFAULT_DISTANCE = 25 -- Studs
6612local FIRST_PERSON_DISTANCE_THRESHOLD = 1.0 -- Below this value, snap into first person
6613
6614local CAMERA_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
6615
6616-- Note: DotProduct check in CoordinateFrame::lookAt() prevents using values within about
6617-- 8.11 degrees of the +/- Y axis, that's why these limits are currently 80 degrees
6618local MIN_Y = math.rad(-80)
6619local MAX_Y = math.rad(80)
6620
6621local VR_ANGLE = math.rad(15)
6622local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0)
6623local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0)
6624local VR_LOW_INTENSITY_REPEAT = 0.1
6625local VR_HIGH_INTENSITY_REPEAT = 0.4
6626
6627local ZERO_VECTOR2 = Vector2.new(0,0)
6628local ZERO_VECTOR3 = Vector3.new(0,0,0)
6629
6630local TOUCH_SENSITIVTY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi)
6631local MOUSE_SENSITIVITY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi )
6632
6633local MAX_TIME_FOR_DOUBLE_TAP = 1.5
6634local MAX_TAP_POS_DELTA = 15
6635local MAX_TAP_TIME_DELTA = 0.75
6636
6637local SEAT_OFFSET = Vector3.new(0,5,0)
6638local VR_SEAT_OFFSET = Vector3.new(0,4,0)
6639local HEAD_OFFSET = Vector3.new(0,1.5,0)
6640local R15_HEAD_OFFSET = Vector3.new(0,2,0)
6641
6642local bindAtPriorityFlagExists, bindAtPriorityFlagEnabled = pcall(function()
6643 return UserSettings():IsUserFeatureEnabled("UserPlayerScriptsBindAtPriority")
6644end)
6645local FFlagPlayerScriptsBindAtPriority = bindAtPriorityFlagExists and bindAtPriorityFlagEnabled
6646
6647local newDefaultCameraAngleFlagExists, newDefaultCameraAngleFlagEnabled = pcall(function()
6648 return UserSettings():IsUserFeatureEnabled("UserNewDefaultCameraAngle")
6649end)
6650local FFlagUserNewDefaultCameraAngle = newDefaultCameraAngleFlagExists and newDefaultCameraAngleFlagEnabled
6651
6652local Util = require(script.Parent:WaitForChild("CameraUtils"))
6653local ZoomController = require(script.Parent:WaitForChild("ZoomController"))
6654
6655--[[ Roblox Services ]]--
6656local Players = game:GetService("Players")
6657local UserInputService = game:GetService("UserInputService")
6658local StarterGui = game:GetService("StarterGui")
6659local GuiService = game:GetService("GuiService")
6660local ContextActionService = game:GetService("ContextActionService")
6661local VRService = game:GetService("VRService")
6662local UserGameSettings = UserSettings():GetService("UserGameSettings")
6663
6664--[[ The Module ]]--
6665local BaseCamera = {}
6666BaseCamera.__index = BaseCamera
6667
6668function BaseCamera.new()
6669 local self = setmetatable({}, BaseCamera)
6670
6671 -- So that derived classes have access to this
6672 self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD
6673
6674 self.cameraType = nil
6675 self.cameraMovementMode = nil
6676
6677 local player = Players.LocalPlayer
6678 self.lastCameraTransform = nil
6679 self.rotateInput = ZERO_VECTOR2
6680 self.userPanningCamera = false
6681 self.lastUserPanCamera = tick()
6682
6683 self.humanoidRootPart = nil
6684 self.humanoidCache = {}
6685
6686 -- Subject and position on last update call
6687 self.lastSubject = nil
6688 self.lastSubjectPosition = Vector3.new(0,5,0)
6689
6690 -- These subject distance members refer to the nominal camera-to-subject follow distance that the camera
6691 -- is trying to maintain, not the actual measured value.
6692 -- The default is updated when screen orientation or the min/max distances change,
6693 -- to be sure the default is always in range and appropriate for the orientation.
6694 self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE)
6695 self.currentSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE)
6696
6697 self.inFirstPerson = false
6698 self.inMouseLockedMode = false
6699 self.portraitMode = false
6700 self.isSmallTouchScreen = false
6701
6702 -- Used by modules which want to reset the camera angle on respawn.
6703 self.resetCameraAngle = true
6704
6705 self.enabled = false
6706
6707 -- Input Event Connections
6708 self.inputBeganConn = nil
6709 self.inputChangedConn = nil
6710 self.inputEndedConn = nil
6711
6712 self.startPos = nil
6713 self.lastPos = nil
6714 self.panBeginLook = nil
6715
6716 self.panEnabled = true
6717 self.keyPanEnabled = true
6718 self.distanceChangeEnabled = true
6719
6720 self.PlayerGui = nil
6721
6722 self.cameraChangedConn = nil
6723 self.viewportSizeChangedConn = nil
6724
6725 self.boundContextActions = {}
6726
6727 -- VR Support
6728 self.shouldUseVRRotation = false
6729 self.VRRotationIntensityAvailable = false
6730 self.lastVRRotationIntensityCheckTime = 0
6731 self.lastVRRotationTime = 0
6732 self.vrRotateKeyCooldown = {}
6733 self.cameraTranslationConstraints = Vector3.new(1, 1, 1)
6734 self.humanoidJumpOrigin = nil
6735 self.trackingHumanoid = nil
6736 self.cameraFrozen = false
6737 self.headHeightR15 = R15_HEAD_OFFSET
6738 self.heightScaleChangedConn = nil
6739 self.subjectStateChangedConn = nil
6740 self.humanoidChildAddedConn = nil
6741 self.humanoidChildRemovedConn = nil
6742
6743 -- Gamepad support
6744 self.activeGamepad = nil
6745 self.gamepadPanningCamera = false
6746 self.lastThumbstickRotate = nil
6747 self.numOfSeconds = 0.7
6748 self.currentSpeed = 0
6749 self.maxSpeed = 6
6750 self.vrMaxSpeed = 4
6751 self.lastThumbstickPos = Vector2.new(0,0)
6752 self.ySensitivity = 0.65
6753 self.lastVelocity = nil
6754 self.gamepadConnectedConn = nil
6755 self.gamepadDisconnectedConn = nil
6756 self.currentZoomSpeed = 1.0
6757 self.L3ButtonDown = false
6758 self.dpadLeftDown = false
6759 self.dpadRightDown = false
6760
6761 -- Touch input support
6762 self.isDynamicThumbstickEnabled = false
6763 self.fingerTouches = {}
6764 self.numUnsunkTouches = 0
6765 self.inputStartPositions = {}
6766 self.inputStartTimes = {}
6767 self.startingDiff = nil
6768 self.pinchBeginZoom = nil
6769 self.userPanningTheCamera = false
6770 self.touchActivateConn = nil
6771
6772 -- Mouse locked formerly known as shift lock mode
6773 self.mouseLockOffset = ZERO_VECTOR3
6774
6775 -- [[ NOTICE ]] --
6776 -- Initialization things used to always execute at game load time, but now these camera modules are instantiated
6777 -- when needed, so the code here may run well after the start of the game
6778
6779 if player.Character then
6780 self:OnCharacterAdded(player.Character)
6781 end
6782
6783 player.CharacterAdded:Connect(function(char)
6784 self:OnCharacterAdded(char)
6785 end)
6786
6787 if self.cameraChangedConn then self.cameraChangedConn:Disconnect() end
6788 self.cameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
6789 self:OnCurrentCameraChanged()
6790 end)
6791 if FFlagUserNewDefaultCameraAngle then
6792 self:OnCurrentCameraChanged()
6793 end
6794
6795 if self.playerCameraModeChangeConn then self.playerCameraModeChangeConn:Disconnect() end
6796 self.playerCameraModeChangeConn = player:GetPropertyChangedSignal("CameraMode"):Connect(function()
6797 self:OnPlayerCameraPropertyChange()
6798 end)
6799
6800 if self.minDistanceChangeConn then self.minDistanceChangeConn:Disconnect() end
6801 self.minDistanceChangeConn = player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(function()
6802 self:OnPlayerCameraPropertyChange()
6803 end)
6804
6805 if self.maxDistanceChangeConn then self.maxDistanceChangeConn:Disconnect() end
6806 self.maxDistanceChangeConn = player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(function()
6807 self:OnPlayerCameraPropertyChange()
6808 end)
6809
6810 if self.playerDevTouchMoveModeChangeConn then self.playerDevTouchMoveModeChangeConn:Disconnect() end
6811 self.playerDevTouchMoveModeChangeConn = player:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function()
6812 self:OnDevTouchMovementModeChanged()
6813 end)
6814 self:OnDevTouchMovementModeChanged() -- Init
6815
6816 if self.gameSettingsTouchMoveMoveChangeConn then self.gameSettingsTouchMoveMoveChangeConn:Disconnect() end
6817 self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function()
6818 self:OnGameSettingsTouchMovementModeChanged()
6819 end)
6820 self:OnGameSettingsTouchMovementModeChanged() -- Init
6821
6822 UserGameSettings:SetCameraYInvertVisible()
6823 UserGameSettings:SetGamepadCameraSensitivityVisible()
6824
6825 self.hasGameLoaded = game:IsLoaded()
6826 if not self.hasGameLoaded then
6827 self.gameLoadedConn = game.Loaded:Connect(function()
6828 self.hasGameLoaded = true
6829 self.gameLoadedConn:Disconnect()
6830 self.gameLoadedConn = nil
6831 end)
6832 end
6833
6834 return self
6835end
6836
6837function BaseCamera:GetModuleName()
6838 return "BaseCamera"
6839end
6840
6841function BaseCamera:OnCharacterAdded(char)
6842 if FFlagUserNewDefaultCameraAngle then
6843 self.resetCameraAngle = self.resetCameraAngle or self:GetEnabled()
6844 self.humanoidRootPart = nil
6845 end
6846 if UserInputService.TouchEnabled then
6847 self.PlayerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
6848 for _, child in ipairs(char:GetChildren()) do
6849 if child:IsA("Tool") then
6850 self.isAToolEquipped = true
6851 end
6852 end
6853 char.ChildAdded:Connect(function(child)
6854 if child:IsA("Tool") then
6855 self.isAToolEquipped = true
6856 end
6857 end)
6858 char.ChildRemoved:Connect(function(child)
6859 if child:IsA("Tool") then
6860 self.isAToolEquipped = false
6861 end
6862 end)
6863 end
6864end
6865
6866function BaseCamera:GetHumanoidRootPart()
6867 if not self.humanoidRootPart then
6868 local player = Players.LocalPlayer
6869 if player.Character then
6870 local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
6871 if humanoid then
6872 self.humanoidRootPart = humanoid.RootPart
6873 end
6874 end
6875 end
6876 return self.humanoidRootPart
6877end
6878
6879function BaseCamera:GetBodyPartToFollow(humanoid, isDead)
6880 -- If the humanoid is dead, prefer the head part if one still exists as a sibling of the humanoid
6881 if humanoid:GetState() == Enum.HumanoidStateType.Dead then
6882 local character = humanoid.Parent
6883 if character and character:IsA("Model") then
6884 return character:FindFirstChild("Head") or humanoid.RootPart
6885 end
6886 end
6887
6888 return humanoid.RootPart
6889end
6890
6891function BaseCamera:GetSubjectPosition()
6892 local result = self.lastSubjectPosition
6893 local camera = game.Workspace.CurrentCamera
6894 local cameraSubject = camera and camera.CameraSubject
6895
6896 if cameraSubject then
6897 if cameraSubject:IsA("Humanoid") then
6898 local humanoid = cameraSubject
6899 local humanoidIsDead = humanoid:GetState() == Enum.HumanoidStateType.Dead
6900
6901 if VRService.VREnabled and humanoidIsDead and humanoid == self.lastSubject then
6902 result = self.lastSubjectPosition
6903 else
6904 local bodyPartToFollow = humanoid.RootPart
6905
6906 -- If the humanoid is dead, prefer their head part as a follow target, if it exists
6907 if humanoidIsDead then
6908 if humanoid.Parent and humanoid.Parent:IsA("Model") then
6909 bodyPartToFollow = humanoid.Parent:FindFirstChild("Head") or bodyPartToFollow
6910 end
6911 end
6912
6913 if bodyPartToFollow and bodyPartToFollow:IsA("BasePart") then
6914 local heightOffset = humanoid.RigType == Enum.HumanoidRigType.R15 and R15_HEAD_OFFSET or HEAD_OFFSET
6915 if humanoidIsDead then
6916 heightOffset = ZERO_VECTOR3
6917 end
6918
6919 result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:vectorToWorldSpace(heightOffset + humanoid.CameraOffset)
6920 end
6921 end
6922
6923 elseif cameraSubject:IsA("VehicleSeat") then
6924 local offset = SEAT_OFFSET
6925 if VRService.VREnabled then
6926 offset = VR_SEAT_OFFSET
6927 end
6928 result = cameraSubject.CFrame.p + cameraSubject.CFrame:vectorToWorldSpace(offset)
6929 elseif cameraSubject:IsA("SkateboardPlatform") then
6930 result = cameraSubject.CFrame.p + SEAT_OFFSET
6931 elseif cameraSubject:IsA("BasePart") then
6932 result = cameraSubject.CFrame.p
6933 elseif cameraSubject:IsA("Model") then
6934 if cameraSubject.PrimaryPart then
6935 result = cameraSubject:GetPrimaryPartCFrame().p
6936 else
6937 result = cameraSubject:GetModelCFrame().p
6938 end
6939 end
6940 else
6941 -- cameraSubject is nil
6942 -- Note: Previous RootCamera did not have this else case and let self.lastSubject and self.lastSubjectPosition
6943 -- both get set to nil in the case of cameraSubject being nil. This function now exits here to preserve the
6944 -- last set valid values for these, as nil values are not handled cases
6945 return
6946 end
6947
6948 self.lastSubject = cameraSubject
6949 self.lastSubjectPosition = result
6950
6951 return result
6952end
6953
6954function BaseCamera:UpdateDefaultSubjectDistance()
6955 local player = Players.LocalPlayer
6956 if self.portraitMode then
6957 self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, PORTRAIT_DEFAULT_DISTANCE)
6958 else
6959 self.defaultSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, DEFAULT_DISTANCE)
6960 end
6961end
6962
6963function BaseCamera:OnViewportSizeChanged()
6964 local camera = game.Workspace.CurrentCamera
6965 local size = camera.ViewportSize
6966 self.portraitMode = size.X < size.Y
6967 self.isSmallTouchScreen = UserInputService.TouchEnabled and (size.Y < 500 or size.X < 700)
6968
6969 self:UpdateDefaultSubjectDistance()
6970end
6971
6972-- Listener for changes to workspace.CurrentCamera
6973function BaseCamera:OnCurrentCameraChanged()
6974 if UserInputService.TouchEnabled then
6975 if self.viewportSizeChangedConn then
6976 self.viewportSizeChangedConn:Disconnect()
6977 self.viewportSizeChangedConn = nil
6978 end
6979
6980 local newCamera = game.Workspace.CurrentCamera
6981
6982 if newCamera then
6983 self:OnViewportSizeChanged()
6984 self.viewportSizeChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function()
6985 self:OnViewportSizeChanged()
6986 end)
6987 end
6988 end
6989
6990 -- VR support additions
6991 if self.cameraSubjectChangedConn then
6992 self.cameraSubjectChangedConn:Disconnect()
6993 self.cameraSubjectChangedConn = nil
6994 end
6995
6996 local camera = game.Workspace.CurrentCamera
6997 if camera then
6998 self.cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
6999 self:OnNewCameraSubject()
7000 end)
7001 self:OnNewCameraSubject()
7002 end
7003end
7004
7005function BaseCamera:OnDynamicThumbstickEnabled()
7006 if UserInputService.TouchEnabled then
7007 self.isDynamicThumbstickEnabled = true
7008 end
7009end
7010
7011function BaseCamera:OnDynamicThumbstickDisabled()
7012 self.isDynamicThumbstickEnabled = false
7013end
7014
7015function BaseCamera:OnGameSettingsTouchMovementModeChanged()
7016 if Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then
7017 if UserGameSettings.TouchMovementMode.Name == "DynamicThumbstick" then
7018 self:OnDynamicThumbstickEnabled()
7019 else
7020 self:OnDynamicThumbstickDisabled()
7021 end
7022 end
7023end
7024
7025function BaseCamera:OnDevTouchMovementModeChanged()
7026 if Players.LocalPlayer.DevTouchMovementMode.Name == "DynamicThumbstick" then
7027 self:OnDynamicThumbstickEnabled()
7028 else
7029 self:OnGameSettingsTouchMovementModeChanged()
7030 end
7031end
7032
7033function BaseCamera:OnPlayerCameraPropertyChange()
7034 -- This call forces re-evaluation of player.CameraMode and clamping to min/max distance which may have changed
7035 self:SetCameraToSubjectDistance(self.currentSubjectDistance)
7036end
7037
7038function BaseCamera:GetCameraHeight()
7039 if VRService.VREnabled and not self.inFirstPerson then
7040 return math.sin(VR_ANGLE) * self.currentSubjectDistance
7041 end
7042 return 0
7043end
7044
7045function BaseCamera:InputTranslationToCameraAngleChange(translationVector, sensitivity)
7046 local camera = game.Workspace.CurrentCamera
7047 if camera and camera.ViewportSize.X > 0 and camera.ViewportSize.Y > 0 and (camera.ViewportSize.Y > camera.ViewportSize.X) then
7048 -- Screen has portrait orientation, swap X and Y sensitivity
7049 return translationVector * Vector2.new( sensitivity.Y, sensitivity.X)
7050 end
7051 return translationVector * sensitivity
7052end
7053
7054function BaseCamera:Enable(enable)
7055 if self.enabled ~= enable then
7056 self.enabled = enable
7057 if self.enabled then
7058 self:ConnectInputEvents()
7059 if FFlagPlayerScriptsBindAtPriority then
7060 self:BindContextActions()
7061 end
7062
7063 if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then
7064 self.currentSubjectDistance = 0.5
7065 if not self.inFirstPerson then
7066 self:EnterFirstPerson()
7067 end
7068 end
7069 else
7070 self:DisconnectInputEvents()
7071 if FFlagPlayerScriptsBindAtPriority then
7072 self:UnbindContextActions()
7073 end
7074 -- Clean up additional event listeners and reset a bunch of properties
7075 self:Cleanup()
7076 end
7077 end
7078end
7079
7080function BaseCamera:GetEnabled()
7081 return self.enabled
7082end
7083
7084function BaseCamera:OnInputBegan(input, processed)
7085 if input.UserInputType == Enum.UserInputType.Touch then
7086 self:OnTouchBegan(input, processed)
7087 elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
7088 self:OnMouse2Down(input, processed)
7089 elseif input.UserInputType == Enum.UserInputType.MouseButton3 then
7090 self:OnMouse3Down(input, processed)
7091 end
7092 -- Keyboard
7093 if not FFlagPlayerScriptsBindAtPriority then
7094 if input.UserInputType == Enum.UserInputType.Keyboard then
7095 self:OnKeyDown(input, processed)
7096 end
7097 end
7098end
7099
7100function BaseCamera:OnInputChanged(input, processed)
7101 if input.UserInputType == Enum.UserInputType.Touch then
7102 self:OnTouchChanged(input, processed)
7103 elseif input.UserInputType == Enum.UserInputType.MouseMovement then
7104 self:OnMouseMoved(input, processed)
7105 elseif input.UserInputType == Enum.UserInputType.MouseWheel then
7106 self:OnMouseWheel(input, processed)
7107 end
7108end
7109
7110function BaseCamera:OnInputEnded(input, processed)
7111 if input.UserInputType == Enum.UserInputType.Touch then
7112 self:OnTouchEnded(input, processed)
7113 elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
7114 self:OnMouse2Up(input, processed)
7115 elseif input.UserInputType == Enum.UserInputType.MouseButton3 then
7116 self:OnMouse3Up(input, processed)
7117 end
7118 -- Keyboard
7119 if not FFlagPlayerScriptsBindAtPriority then
7120 if input.UserInputType == Enum.UserInputType.Keyboard then
7121 self:OnKeyUp(input, processed)
7122 end
7123 end
7124end
7125
7126function BaseCamera:ConnectInputEvents()
7127 self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
7128 self:OnInputBegan(input, processed)
7129 end)
7130
7131 self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed)
7132 self:OnInputChanged(input, processed)
7133 end)
7134
7135 self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed)
7136 self:OnInputEnded(input, processed)
7137 end)
7138
7139 self.touchActivateConn = UserInputService.TouchTapInWorld:Connect(function(touchPos, processed)
7140 self:OnTouchTap(touchPos)
7141 end)
7142
7143 self.menuOpenedConn = GuiService.MenuOpened:connect(function()
7144 self:ResetInputStates()
7145 end)
7146
7147 self.gamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
7148 if self.activeGamepad ~= gamepadEnum then return end
7149 self.activeGamepad = nil
7150 self:AssignActivateGamepad()
7151 end)
7152
7153 self.gamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum)
7154 if self.activeGamepad == nil then
7155 self:AssignActivateGamepad()
7156 end
7157 end)
7158
7159 if not FFlagPlayerScriptsBindAtPriority then
7160 self:BindGamepadInputActions()
7161 end
7162 self:AssignActivateGamepad()
7163 self:UpdateMouseBehavior()
7164end
7165
7166function BaseCamera:BindContextActions()
7167 self:BindGamepadInputActions()
7168 self:BindKeyboardInputActions()
7169end
7170
7171function BaseCamera:AssignActivateGamepad()
7172 local connectedGamepads = UserInputService:GetConnectedGamepads()
7173 if #connectedGamepads > 0 then
7174 for i = 1, #connectedGamepads do
7175 if self.activeGamepad == nil then
7176 self.activeGamepad = connectedGamepads[i]
7177 elseif connectedGamepads[i].Value < self.activeGamepad.Value then
7178 self.activeGamepad = connectedGamepads[i]
7179 end
7180 end
7181 end
7182
7183 if self.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1
7184 self.activeGamepad = Enum.UserInputType.Gamepad1
7185 end
7186end
7187
7188function BaseCamera:DisconnectInputEvents()
7189 if self.inputBeganConn then
7190 self.inputBeganConn:Disconnect()
7191 self.inputBeganConn = nil
7192 end
7193 if self.inputChangedConn then
7194 self.inputChangedConn:Disconnect()
7195 self.inputChangedConn = nil
7196 end
7197 if self.inputEndedConn then
7198 self.inputEndedConn:Disconnect()
7199 self.inputEndedConn = nil
7200 end
7201end
7202
7203function BaseCamera:UnbindContextActions()
7204 for i = 1, #self.boundContextActions do
7205 ContextActionService:UnbindAction(self.boundContextActions[i])
7206 end
7207 self.boundContextActions = {}
7208end
7209
7210function BaseCamera:Cleanup()
7211 if self.menuOpenedConn then
7212 self.menuOpenedConn:Disconnect()
7213 self.menuOpenedConn = nil
7214 end
7215 if self.mouseLockToggleConn then
7216 self.mouseLockToggleConn:Disconnect()
7217 self.mouseLockToggleConn = nil
7218 end
7219 if self.gamepadConnectedConn then
7220 self.gamepadConnectedConn:Disconnect()
7221 self.gamepadConnectedConn = nil
7222 end
7223 if self.gamepadDisconnectedConn then
7224 self.gamepadDisconnectedConn:Disconnect()
7225 self.gamepadDisconnectedConn = nil
7226 end
7227 if self.subjectStateChangedConn then
7228 self.subjectStateChangedConn:Disconnect()
7229 self.subjectStateChangedConn = nil
7230 end
7231 if self.viewportSizeChangedConn then
7232 self.viewportSizeChangedConn:Disconnect()
7233 self.viewportSizeChangedConn = nil
7234 end
7235 if self.touchActivateConn then
7236 self.touchActivateConn:Disconnect()
7237 self.touchActivateConn = nil
7238 end
7239
7240 self.turningLeft = false
7241 self.turningRight = false
7242 self.lastCameraTransform = nil
7243 self.lastSubjectCFrame = nil
7244 self.userPanningTheCamera = false
7245 self.rotateInput = Vector2.new()
7246 self.gamepadPanningCamera = Vector2.new(0,0)
7247
7248 -- Reset input states
7249 self.startPos = nil
7250 self.lastPos = nil
7251 self.panBeginLook = nil
7252 self.isRightMouseDown = false
7253 self.isMiddleMouseDown = false
7254
7255 self.fingerTouches = {}
7256 self.numUnsunkTouches = 0
7257
7258 self.startingDiff = nil
7259 self.pinchBeginZoom = nil
7260
7261 -- Unlock mouse for example if right mouse button was being held down
7262 if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then
7263 UserInputService.MouseBehavior = Enum.MouseBehavior.Default
7264 end
7265end
7266
7267-- This is called when settings menu is opened
7268function BaseCamera:ResetInputStates()
7269 self.isRightMouseDown = false
7270 self.isMiddleMouseDown = false
7271 self:OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters
7272
7273 if UserInputService.TouchEnabled then
7274 --[[menu opening was causing serious touch issues
7275 this should disable all active touch events if
7276 they're active when menu opens.]]
7277 for inputObject in pairs(self.fingerTouches) do
7278 self.fingerTouches[inputObject] = nil
7279 end
7280 self.panBeginLook = nil
7281 self.startPos = nil
7282 self.lastPos = nil
7283 self.userPanningTheCamera = false
7284 self.startingDiff = nil
7285 self.pinchBeginZoom = nil
7286 self.numUnsunkTouches = 0
7287 end
7288end
7289
7290function BaseCamera:GetGamepadPan(name, state, input)
7291 if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
7292-- if self.L3ButtonDown then
7293-- -- L3 Thumbstick is depressed, right stick controls dolly in/out
7294-- if (input.Position.Y > THUMBSTICK_DEADZONE) then
7295-- self.currentZoomSpeed = 0.96
7296-- elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
7297-- self.currentZoomSpeed = 1.04
7298-- else
7299-- self.currentZoomSpeed = 1.00
7300-- end
7301-- else
7302 if state == Enum.UserInputState.Cancel then
7303 self.gamepadPanningCamera = ZERO_VECTOR2
7304 return
7305 end
7306
7307 local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
7308 if inputVector.magnitude > THUMBSTICK_DEADZONE then
7309 self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
7310 else
7311 self.gamepadPanningCamera = ZERO_VECTOR2
7312 end
7313 --end
7314 if FFlagPlayerScriptsBindAtPriority then
7315 return Enum.ContextActionResult.Sink
7316 end
7317 end
7318 if FFlagPlayerScriptsBindAtPriority then
7319 return Enum.ContextActionResult.Pass
7320 end
7321end
7322
7323function BaseCamera:DoKeyboardPanTurn(name, state, input)
7324 if not self.hasGameLoaded and VRService.VREnabled then
7325 return Enum.ContextActionResult.Pass
7326 end
7327
7328 if state == Enum.UserInputState.Cancel then
7329 self.turningLeft = false
7330 self.turningRight = false
7331 return Enum.ContextActionResult.Sink
7332 end
7333
7334 if self.panBeginLook == nil and self.keyPanEnabled then
7335 if input.KeyCode == Enum.KeyCode.Left then
7336 self.turningLeft = state == Enum.UserInputState.Begin
7337 elseif input.KeyCode == Enum.KeyCode.Right then
7338 self.turningRight = state == Enum.UserInputState.Begin
7339 end
7340 return Enum.ContextActionResult.Sink
7341 end
7342 return Enum.ContextActionResult.Pass
7343end
7344
7345function BaseCamera:DoPanRotateCamera(rotateAngle)
7346 local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), rotateAngle, math.pi*0.25)
7347 if angle ~= 0 then
7348 self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
7349 self.lastUserPanCamera = tick()
7350 self.lastCameraTransform = nil
7351 end
7352end
7353
7354function BaseCamera:DoKeyboardPan(name, state, input)
7355 if not self.hasGameLoaded and VRService.VREnabled then
7356 return Enum.ContextActionResult.Pass
7357 end
7358
7359 if state ~= Enum.UserInputState.Begin then
7360 return Enum.ContextActionResult.Pass
7361 end
7362
7363 if self.panBeginLook == nil and self.keyPanEnabled then
7364 if input.KeyCode == Enum.KeyCode.Comma then
7365 self:DoPanRotateCamera(-math.pi*0.1875)
7366 elseif input.KeyCode == Enum.KeyCode.Period then
7367 self:DoPanRotateCamera(math.pi*0.1875)
7368 elseif input.KeyCode == Enum.KeyCode.PageUp then
7369 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(15))
7370 self.lastCameraTransform = nil
7371 elseif input.KeyCode == Enum.KeyCode.PageDown then
7372 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(-15))
7373 self.lastCameraTransform = nil
7374 end
7375 return Enum.ContextActionResult.Sink
7376 end
7377 return Enum.ContextActionResult.Pass
7378end
7379
7380function BaseCamera:DoGamepadZoom(name, state, input)
7381 if input.UserInputType == self.activeGamepad then
7382 if input.KeyCode == Enum.KeyCode.ButtonR3 then
7383 if state == Enum.UserInputState.Begin then
7384 if self.distanceChangeEnabled then
7385 if self:GetCameraToSubjectDistance() > 0.5 then
7386 self:SetCameraToSubjectDistance(0)
7387 else
7388 self:SetCameraToSubjectDistance(10)
7389 end
7390 end
7391 end
7392 elseif input.KeyCode == Enum.KeyCode.DPadLeft then
7393 self.dpadLeftDown = (state == Enum.UserInputState.Begin)
7394 elseif input.KeyCode == Enum.KeyCode.DPadRight then
7395 self.dpadRightDown = (state == Enum.UserInputState.Begin)
7396 end
7397
7398 if self.dpadLeftDown then
7399 self.currentZoomSpeed = 1.04
7400 elseif self.dpadRightDown then
7401 self.currentZoomSpeed = 0.96
7402 else
7403 self.currentZoomSpeed = 1.00
7404 end
7405 if FFlagPlayerScriptsBindAtPriority then
7406 return Enum.ContextActionResult.Sink
7407 end
7408 end
7409 if FFlagPlayerScriptsBindAtPriority then
7410 return Enum.ContextActionResult.Pass
7411 end
7412-- elseif input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonL3 then
7413-- if (state == Enum.UserInputState.Begin) then
7414-- self.L3ButtonDown = true
7415-- elseif (state == Enum.UserInputState.End) then
7416-- self.L3ButtonDown = false
7417-- self.currentZoomSpeed = 1.00
7418-- end
7419-- end
7420end
7421
7422function BaseCamera:DoKeyboardZoom(name, state, input)
7423 if not self.hasGameLoaded and VRService.VREnabled then
7424 return Enum.ContextActionResult.Pass
7425 end
7426
7427 if state ~= Enum.UserInputState.Begin then
7428 return Enum.ContextActionResult.Pass
7429 end
7430
7431 if self.distanceChangeEnabled then
7432 if input.KeyCode == Enum.KeyCode.I then
7433 self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 )
7434 elseif input.KeyCode == Enum.KeyCode.O then
7435 self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 )
7436 end
7437 return Enum.ContextActionResult.Sink
7438 end
7439 return Enum.ContextActionResult.Pass
7440end
7441
7442function BaseCamera:BindAction(actionName, actionFunc, createTouchButton, ...)
7443 table.insert(self.boundContextActions, actionName)
7444 ContextActionService:BindActionAtPriority(actionName, actionFunc, createTouchButton,
7445 CAMERA_ACTION_PRIORITY, ...)
7446end
7447
7448function BaseCamera:BindGamepadInputActions()
7449 if FFlagPlayerScriptsBindAtPriority then
7450 self:BindAction("BaseCameraGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end,
7451 false, Enum.KeyCode.Thumbstick2)
7452 self:BindAction("BaseCameraGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end,
7453 false, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight, Enum.KeyCode.ButtonR3)
7454 else
7455 ContextActionService:BindAction("RootCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2)
7456 ContextActionService:BindAction("RootCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3)
7457 --ContextActionService:BindAction("RootGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonL3)
7458 ContextActionService:BindAction("RootGamepadZoomOut", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadLeft)
7459 ContextActionService:BindAction("RootGamepadZoomIn", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.DPadRight)
7460 end
7461end
7462
7463function BaseCamera:BindKeyboardInputActions()
7464 self:BindAction("BaseCameraKeyboardPanArrowKeys", function(name, state, input) return self:DoKeyboardPanTurn(name, state, input) end,
7465 false, Enum.KeyCode.Left, Enum.KeyCode.Right)
7466 self:BindAction("BaseCameraKeyboardPan", function(name, state, input) return self:DoKeyboardPan(name, state, input) end,
7467 false, Enum.KeyCode.Comma, Enum.KeyCode.Period, Enum.KeyCode.PageUp, Enum.KeyCode.PageDown)
7468 self:BindAction("BaseCameraKeyboardZoom", function(name, state, input) return self:DoKeyboardZoom(name, state, input) end,
7469 false, Enum.KeyCode.I, Enum.KeyCode.O)
7470end
7471
7472function BaseCamera:OnTouchBegan(input, processed)
7473 local canUseDynamicTouch = self.isDynamicThumbstickEnabled and not processed
7474 if canUseDynamicTouch then
7475 self.fingerTouches[input] = processed
7476 if not processed then
7477 self.inputStartPositions[input] = input.Position
7478 self.inputStartTimes[input] = tick()
7479 self.numUnsunkTouches = self.numUnsunkTouches + 1
7480 end
7481 end
7482end
7483
7484function BaseCamera:OnTouchChanged(input, processed)
7485 if self.fingerTouches[input] == nil then
7486 if self.isDynamicThumbstickEnabled then
7487 return
7488 end
7489 self.fingerTouches[input] = processed
7490 if not processed then
7491 self.numUnsunkTouches = self.numUnsunkTouches + 1
7492 end
7493 end
7494
7495 if self.numUnsunkTouches == 1 then
7496 if self.fingerTouches[input] == false then
7497 self.panBeginLook = self.panBeginLook or self:GetCameraLookVector()
7498 self.startPos = self.startPos or input.Position
7499 self.lastPos = self.lastPos or self.startPos
7500 self.userPanningTheCamera = true
7501
7502 local delta = input.Position - self.lastPos
7503 delta = Vector2.new(delta.X, delta.Y * UserGameSettings:GetCameraYInvertValue())
7504 if self.panEnabled then
7505 local desiredXYVector = self:InputTranslationToCameraAngleChange(delta, TOUCH_SENSITIVTY)
7506 self.rotateInput = self.rotateInput + desiredXYVector
7507 end
7508 self.lastPos = input.Position
7509 end
7510 else
7511 self.panBeginLook = nil
7512 self.startPos = nil
7513 self.lastPos = nil
7514 self.userPanningTheCamera = false
7515 end
7516 if self.numUnsunkTouches == 2 then
7517 local unsunkTouches = {}
7518 for touch, wasSunk in pairs(self.fingerTouches) do
7519 if not wasSunk then
7520 table.insert(unsunkTouches, touch)
7521 end
7522 end
7523 if #unsunkTouches == 2 then
7524 local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude
7525 if self.startingDiff and self.pinchBeginZoom then
7526 local scale = difference / math.max(0.01, self.startingDiff)
7527 local clampedScale = Util.Clamp(0.1, 10, scale)
7528 if self.distanceChangeEnabled then
7529 self:SetCameraToSubjectDistance(self.pinchBeginZoom / clampedScale)
7530 end
7531 else
7532 self.startingDiff = difference
7533 self.pinchBeginZoom = self:GetCameraToSubjectDistance()
7534 end
7535 end
7536 else
7537 self.startingDiff = nil
7538 self.pinchBeginZoom = nil
7539 end
7540end
7541
7542function BaseCamera:CalcLookBehindRotateInput()
7543 if not self.humanoidRootPart or not game.Workspace.CurrentCamera then
7544 return nil
7545 end
7546
7547 local cameraLookVector = game.Workspace.CurrentCamera.CFrame.lookVector
7548 local newDesiredLook = (self.humanoidRootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
7549 local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, cameraLookVector)
7550 local vertShift = math.asin(cameraLookVector.Y) - math.asin(newDesiredLook.Y)
7551 if not Util.IsFinite(horizontalShift) then
7552 horizontalShift = 0
7553 end
7554 if not Util.IsFinite(vertShift) then
7555 vertShift = 0
7556 end
7557
7558 return Vector2.new(horizontalShift, vertShift)
7559end
7560
7561function BaseCamera:OnTouchTap(position)
7562 if self.isDynamicThumbstickEnabled and not self.isAToolEquipped then
7563 if self.lastTapTime and tick() - self.lastTapTime < MAX_TIME_FOR_DOUBLE_TAP then
7564 self:SetCameraToSubjectDistance(self.defaultSubjectDistance)
7565 else
7566 if self.humanoidRootPart then
7567 self.rotateInput = self:CalcLookBehindRotateInput()
7568 end
7569 end
7570 self.lastTapTime = tick()
7571 end
7572end
7573
7574function BaseCamera:IsTouchTap(input)
7575 -- We can't make the assumption that the input exists in the inputStartPositions because we may have switched from a different camera type.
7576 if self.inputStartPositions[input] then
7577 local posDelta = (self.inputStartPositions[input] - input.Position).magnitude
7578 if posDelta < MAX_TAP_POS_DELTA then
7579 local timeDelta = self.inputStartTimes[input] - tick()
7580 if timeDelta < MAX_TAP_TIME_DELTA then
7581 return true
7582 end
7583 end
7584 end
7585 return false
7586end
7587
7588function BaseCamera:OnTouchEnded(input, processed)
7589 if self.fingerTouches[input] == false then
7590 if self.numUnsunkTouches == 1 then
7591 self.panBeginLook = nil
7592 self.startPos = nil
7593 self.lastPos = nil
7594 self.userPanningTheCamera = false
7595 if self:IsTouchTap(input) then
7596 self:OnTouchTap(input.Position)
7597 end
7598 elseif self.numUnsunkTouches == 2 then
7599 self.startingDiff = nil
7600 self.pinchBeginZoom = nil
7601 end
7602 end
7603
7604 if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then
7605 self.numUnsunkTouches = self.numUnsunkTouches - 1
7606 end
7607 self.fingerTouches[input] = nil
7608 self.inputStartPositions[input] = nil
7609 self.inputStartTimes[input] = nil
7610end
7611
7612function BaseCamera:OnMouse2Down(input, processed)
7613 if processed then return end
7614
7615 self.isRightMouseDown = true
7616 self:OnMousePanButtonPressed(input, processed)
7617end
7618
7619function BaseCamera:OnMouse2Up(input, processed)
7620 self.isRightMouseDown = false
7621 self:OnMousePanButtonReleased(input, processed)
7622end
7623
7624function BaseCamera:OnMouse3Down(input, processed)
7625 if processed then return end
7626
7627 self.isMiddleMouseDown = true
7628 self:OnMousePanButtonPressed(input, processed)
7629end
7630
7631function BaseCamera:OnMouse3Up(input, processed)
7632 self.isMiddleMouseDown = false
7633 self:OnMousePanButtonReleased(input, processed)
7634end
7635
7636function BaseCamera:OnMouseMoved(input, processed)
7637 if not self.hasGameLoaded and VRService.VREnabled then
7638 return
7639 end
7640
7641 local inputDelta = input.Delta
7642 inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * UserGameSettings:GetCameraYInvertValue())
7643
7644 if self.panEnabled and ((self.startPos and self.lastPos and self.panBeginLook) or self.inFirstPerson or self.inMouseLockedMode) then
7645 local desiredXYVector = self:InputTranslationToCameraAngleChange(inputDelta,MOUSE_SENSITIVITY)
7646 self.rotateInput = self.rotateInput + desiredXYVector
7647 end
7648
7649 if self.startPos and self.lastPos and self.panBeginLook then
7650 self.lastPos = self.lastPos + input.Delta
7651 end
7652end
7653
7654function BaseCamera:OnMousePanButtonPressed(input, processed)
7655 if processed then return end
7656 self:UpdateMouseBehavior()
7657 self.panBeginLook = self.panBeginLook or self:GetCameraLookVector()
7658 self.startPos = self.startPos or input.Position
7659 self.lastPos = self.lastPos or self.startPos
7660 self.userPanningTheCamera = true
7661end
7662
7663function BaseCamera:OnMousePanButtonReleased(input, processed)
7664 self:UpdateMouseBehavior()
7665 if not (self.isRightMouseDown or self.isMiddleMouseDown) then
7666 self.panBeginLook = nil
7667 self.startPos = nil
7668 self.lastPos = nil
7669 self.userPanningTheCamera = false
7670 end
7671end
7672
7673function BaseCamera:OnMouseWheel(input, processed)
7674 if not self.hasGameLoaded and VRService.VREnabled then
7675 return
7676 end
7677 if not processed then
7678 if self.distanceChangeEnabled then
7679 local wheelInput = Util.Clamp(-1, 1, -input.Position.Z)
7680
7681 local newDistance
7682 if self.inFirstPerson and wheelInput > 0 then
7683 newDistance = FIRST_PERSON_DISTANCE_THRESHOLD
7684 else
7685 -- The 0.156 and 1.7 values are the slope and intercept of a line that is replacing the old
7686 -- rk4Integrator function which was not being used as an integrator, only to get a delta as a function of distance,
7687 -- which was linear as it was being used. These constants preserve the status quo behavior.
7688 newDistance = self.currentSubjectDistance + 0.156 * self.currentSubjectDistance * wheelInput + 1.7 * math.sign(wheelInput)
7689 end
7690
7691 self:SetCameraToSubjectDistance(newDistance)
7692 end
7693 end
7694end
7695
7696--Remove with FFlagPlayerScriptsBindAtPriority
7697function BaseCamera:OnKeyDown(input, processed)
7698 if not self.hasGameLoaded and VRService.VREnabled then
7699 return
7700 end
7701
7702 if processed then
7703 return
7704 end
7705
7706 if self.distanceChangeEnabled then
7707 if input.KeyCode == Enum.KeyCode.I then
7708 self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 )
7709 elseif input.KeyCode == Enum.KeyCode.O then
7710 self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 )
7711 end
7712 end
7713
7714 if self.panBeginLook == nil and self.keyPanEnabled then
7715 if input.KeyCode == Enum.KeyCode.Left then
7716 self.turningLeft = true
7717 elseif input.KeyCode == Enum.KeyCode.Right then
7718 self.turningRight = true
7719 elseif input.KeyCode == Enum.KeyCode.Comma then
7720 local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), -math.pi*0.1875, math.pi*0.25)
7721 if angle ~= 0 then
7722 self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
7723 self.lastUserPanCamera = tick()
7724 self.lastCameraTransform = nil
7725 end
7726 elseif input.KeyCode == Enum.KeyCode.Period then
7727 local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), math.pi*0.1875, math.pi*0.25)
7728 if angle ~= 0 then
7729 self.rotateInput = self.rotateInput + Vector2.new(angle, 0)
7730 self.lastUserPanCamera = tick()
7731 self.lastCameraTransform = nil
7732 end
7733 elseif input.KeyCode == Enum.KeyCode.PageUp then
7734 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(15))
7735 self.lastCameraTransform = nil
7736 elseif input.KeyCode == Enum.KeyCode.PageDown then
7737 self.rotateInput = self.rotateInput + Vector2.new(0,math.rad(-15))
7738 self.lastCameraTransform = nil
7739 end
7740 end
7741end
7742
7743--Remove with FFlagPlayerScriptsBindAtPriority
7744function BaseCamera:OnKeyUp(input, processed)
7745 if input.KeyCode == Enum.KeyCode.Left then
7746 self.turningLeft = false
7747 elseif input.KeyCode == Enum.KeyCode.Right then
7748 self.turningRight = false
7749 end
7750end
7751
7752function BaseCamera:UpdateMouseBehavior()
7753 -- first time transition to first person mode or mouse-locked third person
7754 if self.inFirstPerson or self.inMouseLockedMode then
7755 UserGameSettings.RotationType = Enum.RotationType.CameraRelative
7756 UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
7757 else
7758 UserGameSettings.RotationType = Enum.RotationType.MovementRelative
7759 if self.isRightMouseDown or self.isMiddleMouseDown then
7760 UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
7761 else
7762 UserInputService.MouseBehavior = Enum.MouseBehavior.Default
7763 end
7764 end
7765end
7766
7767function BaseCamera:UpdateForDistancePropertyChange()
7768 -- Calling this setter with the current value will force checking that it is still
7769 -- in range after a change to the min/max distance limits
7770 self:SetCameraToSubjectDistance(self.currentSubjectDistance)
7771end
7772
7773function BaseCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
7774 local player = Players.LocalPlayer
7775
7776 local lastSubjectDistance = self.currentSubjectDistance
7777
7778 -- By default, camera modules will respect LockFirstPerson and override the currentSubjectDistance with 0
7779 -- regardless of what Player.CameraMinZoomDistance is set to, so that first person can be made
7780 -- available by the developer without needing to allow players to mousewheel dolly into first person.
7781 -- Some modules will override this function to remove or change first-person capability.
7782 if player.CameraMode == Enum.CameraMode.LockFirstPerson then
7783 self.currentSubjectDistance = 0.5
7784 if not self.inFirstPerson then
7785 self:EnterFirstPerson()
7786 end
7787 else
7788 local newSubjectDistance = Util.Clamp(player.CameraMinZoomDistance, player.CameraMaxZoomDistance, desiredSubjectDistance)
7789 if newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD then
7790 self.currentSubjectDistance = 0.5
7791 if not self.inFirstPerson then
7792 self:EnterFirstPerson()
7793 end
7794 else
7795 self.currentSubjectDistance = newSubjectDistance
7796 if self.inFirstPerson then
7797 self:LeaveFirstPerson()
7798 end
7799 end
7800 end
7801
7802 -- Pass target distance and zoom direction to the zoom controller
7803 ZoomController.SetZoomParameters(self.currentSubjectDistance, math.sign(desiredSubjectDistance - lastSubjectDistance))
7804
7805 -- Returned only for convenience to the caller to know the outcome
7806 return self.currentSubjectDistance
7807end
7808
7809function BaseCamera:SetCameraType( cameraType )
7810 --Used by derived classes
7811 self.cameraType = cameraType
7812end
7813
7814function BaseCamera:GetCameraType()
7815 return self.cameraType
7816end
7817
7818-- Movement mode standardized to Enum.ComputerCameraMovementMode values
7819function BaseCamera:SetCameraMovementMode( cameraMovementMode )
7820 self.cameraMovementMode = cameraMovementMode
7821end
7822
7823function BaseCamera:GetCameraMovementMode()
7824 return self.cameraMovementMode
7825end
7826
7827function BaseCamera:SetIsMouseLocked(mouseLocked)
7828 self.inMouseLockedMode = mouseLocked
7829 self:UpdateMouseBehavior()
7830end
7831
7832function BaseCamera:GetIsMouseLocked()
7833 return self.inMouseLockedMode
7834end
7835
7836function BaseCamera:SetMouseLockOffset(offsetVector)
7837 self.mouseLockOffset = offsetVector
7838end
7839
7840function BaseCamera:GetMouseLockOffset()
7841 return self.mouseLockOffset
7842end
7843
7844function BaseCamera:InFirstPerson()
7845 return self.inFirstPerson
7846end
7847
7848function BaseCamera:EnterFirstPerson()
7849 -- Overridden in ClassicCamera, the only module which supports FirstPerson
7850end
7851
7852function BaseCamera:LeaveFirstPerson()
7853 -- Overridden in ClassicCamera, the only module which supports FirstPerson
7854end
7855
7856-- Nominal distance, set by dollying in and out with the mouse wheel or equivalent, not measured distance
7857function BaseCamera:GetCameraToSubjectDistance()
7858 return self.currentSubjectDistance
7859end
7860
7861-- Actual measured distance to the camera Focus point, which may be needed in special circumstances, but should
7862-- never be used as the starting point for updating the nominal camera-to-subject distance (self.currentSubjectDistance)
7863-- since that is a desired target value set only by mouse wheel (or equivalent) input, PopperCam, and clamped to min max camera distance
7864function BaseCamera:GetMeasuredDistanceToFocus()
7865 local camera = game.Workspace.CurrentCamera
7866 if camera then
7867 return (camera.CoordinateFrame.p - camera.Focus.p).magnitude
7868 end
7869 return nil
7870end
7871
7872function BaseCamera:GetCameraLookVector()
7873 return game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame.lookVector or UNIT_Z
7874end
7875
7876-- Replacements for RootCamera:RotateCamera() which did not actually rotate the camera
7877-- suppliedLookVector is not normally passed in, it's used only by Watch camera
7878function BaseCamera:CalculateNewLookCFrame(suppliedLookVector)
7879 local currLookVector = suppliedLookVector or self:GetCameraLookVector()
7880 local currPitchAngle = math.asin(currLookVector.y)
7881 local yTheta = Util.Clamp(-MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle, self.rotateInput.y)
7882 local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta)
7883 local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
7884 local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)
7885 return newLookCFrame
7886end
7887function BaseCamera:CalculateNewLookVector(suppliedLookVector)
7888 local newLookCFrame = self:CalculateNewLookCFrame(suppliedLookVector)
7889 return newLookCFrame.lookVector
7890end
7891
7892function BaseCamera:CalculateNewLookVectorVR()
7893 local subjectPosition = self:GetSubjectPosition()
7894 local vecToSubject = (subjectPosition - game.Workspace.CurrentCamera.CFrame.p)
7895 local currLookVector = (vecToSubject * X1_Y0_Z1).unit
7896 local vrRotateInput = Vector2.new(self.rotateInput.x, 0)
7897 local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
7898 local yawRotatedVector = (CFrame.Angles(0, -vrRotateInput.x, 0) * startCFrame * CFrame.Angles(-vrRotateInput.y,0,0)).lookVector
7899 return (yawRotatedVector * X1_Y0_Z1).unit
7900end
7901
7902function BaseCamera:GetHumanoid()
7903 local player = Players.LocalPlayer
7904 local character = player and player.Character
7905 if character then
7906 local resultHumanoid = self.humanoidCache[player]
7907 if resultHumanoid and resultHumanoid.Parent == character then
7908 return resultHumanoid
7909 else
7910 self.humanoidCache[player] = nil -- Bust Old Cache
7911 local humanoid = character:FindFirstChildOfClass("Humanoid")
7912 if humanoid then
7913 self.humanoidCache[player] = humanoid
7914 end
7915 return humanoid
7916 end
7917 end
7918 return nil
7919end
7920
7921function BaseCamera:GetHumanoidPartToFollow(humanoid, humanoidStateType)
7922 if humanoidStateType == Enum.HumanoidStateType.Dead then
7923 local character = humanoid.Parent
7924 if character then
7925 return character:FindFirstChild("Head") or humanoid.Torso
7926 else
7927 return humanoid.Torso
7928 end
7929 else
7930 return humanoid.Torso
7931 end
7932end
7933
7934function BaseCamera:UpdateGamepad()
7935 local gamepadPan = self.gamepadPanningCamera
7936 if gamepadPan and (self.hasGameLoaded or not VRService.VREnabled) then
7937 gamepadPan = Util.GamepadLinearToCurve(gamepadPan)
7938 local currentTime = tick()
7939 if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then
7940 self.userPanningTheCamera = true
7941 elseif gamepadPan == ZERO_VECTOR2 then
7942 self.lastThumbstickRotate = nil
7943 if self.lastThumbstickPos == ZERO_VECTOR2 then
7944 self.currentSpeed = 0
7945 end
7946 end
7947
7948 local finalConstant = 0
7949
7950 if self.lastThumbstickRotate then
7951 if VRService.VREnabled then
7952 self.currentSpeed = self.vrMaxSpeed
7953 else
7954 local elapsedTime = (currentTime - self.lastThumbstickRotate) * 10
7955 self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds))
7956
7957 if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end
7958
7959 if self.lastVelocity then
7960 local velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
7961 local velocityDeltaMag = (velocity - self.lastVelocity).magnitude
7962
7963 if velocityDeltaMag > 12 then
7964 self.currentSpeed = self.currentSpeed * (20/velocityDeltaMag)
7965 if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end
7966 end
7967 end
7968 end
7969
7970 finalConstant = UserGameSettings.GamepadCameraSensitivity * self.currentSpeed
7971 self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
7972 end
7973
7974 self.lastThumbstickPos = gamepadPan
7975 self.lastThumbstickRotate = currentTime
7976
7977 return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:GetCameraYInvertValue())
7978 end
7979
7980 return ZERO_VECTOR2
7981end
7982
7983-- [[ VR Support Section ]] --
7984
7985function BaseCamera:ApplyVRTransform()
7986 if not VRService.VREnabled then
7987 return
7988 end
7989
7990 --we only want this to happen in first person VR
7991 local rootJoint = self.humanoidRootPart and self.humanoidRootPart:FindFirstChild("RootJoint")
7992 if not rootJoint then
7993 return
7994 end
7995
7996 local cameraSubject = game.Workspace.CurrentCamera.CameraSubject
7997 local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat")
7998
7999 if self.inFirstPerson and not isInVehicle then
8000 local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head)
8001 local vrRotation = vrFrame - vrFrame.p
8002 rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0)
8003 else
8004 rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0)
8005 end
8006end
8007
8008function BaseCamera:IsInFirstPerson()
8009 return self.inFirstPerson
8010end
8011
8012function BaseCamera:ShouldUseVRRotation()
8013 if not VRService.VREnabled then
8014 return false
8015 end
8016
8017 if not self.VRRotationIntensityAvailable and tick() - self.lastVRRotationIntensityCheckTime < 1 then
8018 return false
8019 end
8020
8021 local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end)
8022 self.VRRotationIntensityAvailable = success and vrRotationIntensity ~= nil
8023 self.lastVRRotationIntensityCheckTime = tick()
8024
8025 self.shouldUseVRRotation = success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth"
8026
8027 return self.shouldUseVRRotation
8028end
8029
8030function BaseCamera:GetVRRotationInput()
8031 local vrRotateSum = ZERO_VECTOR2
8032 local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end)
8033
8034 if not success then
8035 return
8036 end
8037
8038 local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2
8039 local delayExpired = (tick() - self.lastVRRotationTime) >= self:GetRepeatDelayValue(vrRotationIntensity)
8040
8041 if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then
8042 if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then
8043 local sign = 1
8044 if vrGamepadRotation.x < 0 then
8045 sign = -1
8046 end
8047 vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign
8048 self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true
8049 end
8050 elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then
8051 self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil
8052 end
8053 if self.turningLeft then
8054 if delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Left] then
8055 vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity)
8056 self.vrRotateKeyCooldown[Enum.KeyCode.Left] = true
8057 end
8058 else
8059 self.vrRotateKeyCooldown[Enum.KeyCode.Left] = nil
8060 end
8061 if self.turningRight then
8062 if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Right]) then
8063 vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity)
8064 self.vrRotateKeyCooldown[Enum.KeyCode.Right] = true
8065 end
8066 else
8067 self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil
8068 end
8069
8070 if vrRotateSum ~= ZERO_VECTOR2 then
8071 self.lastVRRotationTime = tick()
8072 end
8073
8074 return vrRotateSum
8075end
8076
8077function BaseCamera:CancelCameraFreeze(keepConstraints)
8078 if not keepConstraints then
8079 self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z)
8080 end
8081 if self.cameraFrozen then
8082 self.trackingHumanoid = nil
8083 self.cameraFrozen = false
8084 end
8085end
8086
8087function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack)
8088 if not self.cameraFrozen then
8089 self.humanoidJumpOrigin = subjectPosition
8090 self.trackingHumanoid = humanoidToTrack
8091 self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z)
8092 self.cameraFrozen = true
8093 end
8094end
8095
8096function BaseCamera:RescaleCameraOffset(newScaleFactor)
8097 self.headHeightR15 = R15_HEAD_OFFSET * newScaleFactor
8098end
8099
8100function BaseCamera:OnHumanoidSubjectChildAdded(child)
8101 if child.Name == "BodyHeightScale" and child:IsA("NumberValue") then
8102 if self.heightScaleChangedConn then
8103 self.heightScaleChangedConn:Disconnect()
8104 end
8105 self.heightScaleChangedConn = child.Changed:Connect(function(newScaleFactor)
8106 self:RescaleCameraOffset(newScaleFactor)
8107 end)
8108 self:RescaleCameraOffset(child.Value)
8109 end
8110end
8111
8112function BaseCamera:OnHumanoidSubjectChildRemoved(child)
8113 if child.Name == "BodyHeightScale" then
8114 self:RescaleCameraOffset(1)
8115 if self.heightScaleChangedConn then
8116 self.heightScaleChangedConn:Disconnect()
8117 self.heightScaleChangedConn = nil
8118 end
8119 end
8120end
8121
8122function BaseCamera:OnNewCameraSubject()
8123 if self.subjectStateChangedConn then
8124 self.subjectStateChangedConn:Disconnect()
8125 self.subjectStateChangedConn = nil
8126 end
8127 if self.humanoidChildAddedConn then
8128 self.humanoidChildAddedConn:Disconnect()
8129 self.humanoidChildAddedConn = nil
8130 end
8131 if self.humanoidChildRemovedConn then
8132 self.humanoidChildRemovedConn:Disconnect()
8133 self.humanoidChildRemovedConn = nil
8134 end
8135 if self.heightScaleChangedConn then
8136 self.heightScaleChangedConn:Disconnect()
8137 self.heightScaleChangedConn = nil
8138 end
8139
8140 local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject
8141 if self.trackingHumanoid ~= humanoid then
8142 self:CancelCameraFreeze()
8143 end
8144 if humanoid and humanoid:IsA("Humanoid") then
8145 self.humanoidChildAddedConn = humanoid.ChildAdded:Connect(function(child)
8146 self:OnHumanoidSubjectChildAdded(child)
8147 end)
8148 self.humanoidChildRemovedConn = humanoid.ChildRemoved:Connect(function(child)
8149 self:OnHumanoidSubjectChildRemoved(child)
8150 end)
8151 for _, child in pairs(humanoid:GetChildren()) do
8152 self:OnHumanoidSubjectChildAdded(child)
8153 end
8154
8155 self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState)
8156 if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then
8157 self:StartCameraFreeze(self:GetSubjectPosition(), humanoid)
8158 elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then
8159 self:CancelCameraFreeze(true)
8160 end
8161 end)
8162 end
8163end
8164
8165function BaseCamera:GetVRFocus(subjectPosition, timeDelta)
8166 local lastFocus = self.LastCameraFocus or subjectPosition
8167 if not self.cameraFrozen then
8168 self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z)
8169 end
8170
8171 local newFocus
8172 if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then
8173 newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z))
8174 else
8175 newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y))
8176 end
8177
8178 if self.cameraFrozen then
8179 -- No longer in 3rd person
8180 if self.inFirstPerson then -- not VRService.VREnabled
8181 self:CancelCameraFreeze()
8182 end
8183 -- This case you jumped off a cliff and want to keep your character in view
8184 -- 0.5 is to fix floating point error when not jumping off cliffs
8185 if self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then
8186 self:CancelCameraFreeze()
8187 end
8188 end
8189
8190 return newFocus
8191end
8192
8193function BaseCamera:GetRotateAmountValue(vrRotationIntensity)
8194 vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
8195 if vrRotationIntensity then
8196 if vrRotationIntensity == "Low" then
8197 return VR_LOW_INTENSITY_ROTATION
8198 elseif vrRotationIntensity == "High" then
8199 return VR_HIGH_INTENSITY_ROTATION
8200 end
8201 end
8202 return ZERO_VECTOR2
8203end
8204
8205function BaseCamera:GetRepeatDelayValue(vrRotationIntensity)
8206 vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
8207 if vrRotationIntensity then
8208 if vrRotationIntensity == "Low" then
8209 return VR_LOW_INTENSITY_REPEAT
8210 elseif vrRotationIntensity == "High" then
8211 return VR_HIGH_INTENSITY_REPEAT
8212 end
8213 end
8214 return 0
8215end
8216
8217function BaseCamera:Test()
8218 print("BaseCamera:Test()")
8219end
8220
8221function BaseCamera:Update(dt)
8222 warn("BaseCamera:Update() This is a virtual function that should never be getting called.")
8223 return game.Workspace.CurrentCamera.CFrame, game.Workspace.CurrentCamera.Focus
8224end
8225
8226return BaseCamera
8227
8228
8229--[[
8230 Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view
8231 Refactored for 2018 Camera Update but functionality is unchanged - AllYourBlox
8232--]]
8233
8234--[[ Camera Maths Utilities Library ]]--
8235local Util = require(script.Parent:WaitForChild("CameraUtils"))
8236
8237local PlayersService = game:GetService("Players")
8238local POP_RESTORE_RATE = 0.3
8239local MIN_CAMERA_ZOOM = 0.5
8240local VALID_SUBJECTS = {
8241 'Humanoid',
8242 'VehicleSeat',
8243 'SkateboardPlatform',
8244}
8245
8246local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function()
8247 return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix")
8248end)
8249local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled
8250
8251
8252--[[ The Module ]]--
8253local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
8254local Poppercam = setmetatable({}, BaseOcclusion)
8255Poppercam.__index = Poppercam
8256
8257function Poppercam.new()
8258 local self = setmetatable(BaseOcclusion.new(), Poppercam)
8259
8260 self.camera = nil
8261 self.cameraSubjectChangeConn = nil
8262
8263 self.subjectPart = nil
8264
8265 self.playerCharacters = {} -- For ignoring in raycasts
8266 self.vehicleParts = {} -- Also just for ignoring
8267
8268 self.lastPopAmount = 0
8269 self.lastZoomLevel = 0
8270 self.popperEnabled = false
8271
8272 return self
8273end
8274
8275function Poppercam:GetOcclusionMode()
8276 return Enum.DevCameraOcclusionMode.Zoom
8277end
8278
8279function Poppercam:Enable(enable)
8280
8281end
8282
8283-- Called when character is added
8284function Poppercam:CharacterAdded(char, player)
8285 self.playerCharacters[player] = char
8286end
8287
8288-- Called when character is about to be removed
8289function Poppercam:CharacterRemoving(char, player)
8290 self.playerCharacters[player] = nil
8291end
8292
8293function Poppercam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
8294 if self.popperEnabled then
8295 self.camera = game.Workspace.CurrentCamera
8296 local newCameraCFrame = desiredCameraCFrame
8297 local focusPoint = desiredCameraFocus.p
8298
8299 if FFlagUserPortraitPopperFix and self.subjectPart then
8300 focusPoint = self.subjectPart.CFrame.p
8301 end
8302
8303 local ignoreList = {}
8304 for _, character in pairs(self.playerCharacters) do
8305 ignoreList[#ignoreList + 1] = character
8306 end
8307 for i = 1, #self.vehicleParts do
8308 ignoreList[#ignoreList + 1] = self.vehicleParts[i]
8309 end
8310
8311 -- Get largest cutoff distance
8312 -- Note that the camera CFrame must be set here, because the current implementation of GetLargestCutoffDistance
8313 -- uses the current camera CFrame directly (it cannot yet be passed the desiredCameraCFrame).
8314 local prevCameraCFrame = self.camera.CFrame
8315 self.camera.CFrame = desiredCameraCFrame
8316 self.camera.Focus = desiredCameraFocus
8317 local largest = self.camera:GetLargestCutoffDistance(ignoreList)
8318
8319 -- Then check if the player zoomed since the last frame,
8320 -- and if so, reset our pop history so we stop tweening
8321 local zoomLevel = (desiredCameraCFrame.p - focusPoint).Magnitude
8322 if math.abs(zoomLevel - self.lastZoomLevel) > 0.001 then
8323 self.lastPopAmount = 0
8324 end
8325
8326 -- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more
8327 local popAmount = largest
8328 if self.lastPopAmount > popAmount then
8329 popAmount = self.lastPopAmount
8330 end
8331
8332 if popAmount > 0 then
8333 newCameraCFrame = desiredCameraCFrame + (desiredCameraCFrame.lookVector * popAmount)
8334 self.lastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame
8335 if self.lastPopAmount < 0 then
8336 self.lastPopAmount = 0
8337 end
8338 end
8339
8340 self.lastZoomLevel = zoomLevel
8341
8342 -- Stop shift lock being able to see through walls by manipulating Camera focus inside the wall
8343-- if EnabledCamera and EnabledCamera:GetShiftLock() and not EnabledCamera:IsInFirstPerson() then
8344-- if EnabledCamera:GetCameraActualZoom() < 1 then
8345-- local subjectPosition = EnabledCamera.lastSubjectPosition
8346-- if subjectPosition then
8347-- Camera.Focus = CFrame_new(subjectPosition)
8348-- Camera.CFrame = CFrame_new(subjectPosition - MIN_CAMERA_ZOOM*EnabledCamera:GetCameraLook(), subjectPosition)
8349-- end
8350-- end
8351-- end
8352 return newCameraCFrame, desiredCameraFocus
8353 end
8354
8355 -- Return unchanged values
8356 return desiredCameraCFrame, desiredCameraFocus
8357end
8358
8359function Poppercam:OnCameraSubjectChanged(newSubject)
8360 self.vehicleParts = {}
8361
8362 self.lastPopAmount = 0
8363
8364 if newSubject then
8365 -- Determine if we should be popping at all
8366 self.popperEnabled = false
8367 for _, subjectType in pairs(VALID_SUBJECTS) do
8368 if newSubject:IsA(subjectType) then
8369 self.popperEnabled = true
8370 break
8371 end
8372 end
8373
8374 -- Get all parts of the vehicle the player is controlling
8375 if newSubject:IsA('VehicleSeat') then
8376 self.vehicleParts = newSubject:GetConnectedParts(true)
8377 end
8378
8379 if FFlagUserPortraitPopperFix then
8380 if newSubject:IsA("BasePart") then
8381 self.subjectPart = newSubject
8382 elseif newSubject:IsA("Model") then
8383 if newSubject.PrimaryPart then
8384 self.subjectPart = newSubject.PrimaryPart
8385 else
8386 -- Model has no PrimaryPart set, just use first BasePart
8387 -- we can find as better-than-nothing solution (can still fail)
8388 for _, child in pairs(newSubject:GetChildren()) do
8389 if child:IsA("BasePart") then
8390 self.subjectPart = child
8391 break
8392 end
8393 end
8394 end
8395 elseif newSubject:IsA("Humanoid") then
8396 self.subjectPart = newSubject.RootPart
8397 end
8398 end
8399 end
8400end
8401
8402return Poppercam
8403
8404-- Zoom
8405-- Controls the distance between the focus and the camera.
8406
8407local ZOOM_STIFFNESS = 4.5
8408local ZOOM_DEFAULT = 12.5
8409local ZOOM_ACCELERATION = 0.0375
8410
8411local DIST_OPAQUE = 1
8412
8413local Popper = require(script:WaitForChild("Popper"))
8414
8415local clamp = math.clamp
8416local exp = math.exp
8417local min = math.min
8418local max = math.max
8419local pi = math.pi
8420
8421local cameraMinZoomDistance, cameraMaxZoomDistance do
8422 local Player = game:GetService("Players").LocalPlayer
8423
8424 local function updateBounds()
8425 cameraMinZoomDistance = Player.CameraMinZoomDistance
8426 cameraMaxZoomDistance = Player.CameraMaxZoomDistance
8427 end
8428
8429 updateBounds()
8430
8431 Player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(updateBounds)
8432 Player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(updateBounds)
8433end
8434
8435local ConstrainedSpring = {} do
8436 ConstrainedSpring.__index = ConstrainedSpring
8437
8438 function ConstrainedSpring.new(freq, x, minValue, maxValue)
8439 x = clamp(x, minValue, maxValue)
8440 return setmetatable({
8441 freq = freq, -- Undamped frequency (Hz)
8442 x = x, -- Current position
8443 v = 0, -- Current velocity
8444 minValue = minValue, -- Minimum bound
8445 maxValue = maxValue, -- Maximum bound
8446 goal = x, -- Goal position
8447 }, ConstrainedSpring)
8448 end
8449
8450 function ConstrainedSpring:Step(dt)
8451 local freq = self.freq*2*pi -- Convert from Hz to rad/s
8452 local x = self.x
8453 local v = self.v
8454 local minValue = self.minValue
8455 local maxValue = self.maxValue
8456 local goal = self.goal
8457
8458 -- Solve the spring ODE for position and velocity after time t, assuming critical damping:
8459 -- 2*f*x'[t] + x''[t] = f^2*(g - x[t])
8460 -- Knowns are x[0] and x'[0].
8461 -- Solve for x[t] and x'[t].
8462
8463 local offset = goal - x
8464 local step = freq*dt
8465 local decay = exp(-step)
8466
8467 local x1 = goal + (v*dt - offset*(step + 1))*decay
8468 local v1 = ((offset*freq - v)*step + v)*decay
8469
8470 -- Constrain
8471 if x1 < minValue then
8472 x1 = minValue
8473 v1 = 0
8474 elseif x1 > maxValue then
8475 x1 = maxValue
8476 v1 = 0
8477 end
8478
8479 self.x = x1
8480 self.v = v1
8481
8482 return x1
8483 end
8484end
8485
8486local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, cameraMinZoomDistance, cameraMaxZoomDistance)
8487
8488local function stepTargetZoom(z, dz, zoomMin, zoomMax)
8489 z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax)
8490 if z < DIST_OPAQUE then
8491 z = dz <= 0 and zoomMin or DIST_OPAQUE
8492 end
8493 return z
8494end
8495
8496local zoomDelta = 0
8497
8498local Zoom = {} do
8499 function Zoom.Update(renderDt, focus, extrapolation)
8500 local poppedZoom = math.huge
8501
8502 if zoomSpring.goal > DIST_OPAQUE then
8503 -- Make a pessimistic estimate of zoom distance for this step without accounting for poppercam
8504 local maxPossibleZoom = max(
8505 zoomSpring.x,
8506 stepTargetZoom(zoomSpring.goal, zoomDelta, cameraMinZoomDistance, cameraMaxZoomDistance)
8507 )
8508
8509 -- Run the Popper algorithm on the feasible zoom range, [cameraMinZoomDistance, maxPossibleZoom]
8510 poppedZoom = Popper(
8511 focus*CFrame.new(0, 0, cameraMinZoomDistance),
8512 maxPossibleZoom - cameraMinZoomDistance,
8513 extrapolation
8514 ) + cameraMinZoomDistance
8515 end
8516
8517 zoomSpring.minValue = cameraMinZoomDistance
8518 zoomSpring.maxValue = min(cameraMaxZoomDistance, poppedZoom)
8519
8520 return zoomSpring:Step(renderDt)
8521 end
8522
8523 function Zoom.SetZoomParameters(targetZoom, newZoomDelta)
8524 zoomSpring.goal = targetZoom
8525 zoomDelta = newZoomDelta
8526 end
8527end
8528
8529return Zoom
8530
8531
8532--------------------------------------------------------------------------------
8533-- Popper.lua
8534-- Prevents your camera from clipping through walls.
8535--------------------------------------------------------------------------------
8536
8537local Players = game:GetService('Players')
8538
8539local FFlagUserPoppercamLooseOpacityThreshold do
8540 local success, enabled = pcall(function()
8541 return UserSettings():IsUserFeatureEnabled("UserPoppercamLooseOpacityThreshold")
8542 end)
8543 FFlagUserPoppercamLooseOpacityThreshold = success and enabled
8544end
8545
8546local camera = game.Workspace.CurrentCamera
8547
8548local min = math.min
8549local tan = math.tan
8550local rad = math.rad
8551local inf = math.huge
8552local ray = Ray.new
8553
8554local function eraseFromEnd(t, toSize)
8555 for i = #t, toSize + 1, -1 do
8556 t[i] = nil
8557 end
8558end
8559
8560local nearPlaneZ, projX, projY do
8561 local function updateProjection()
8562 local fov = rad(camera.FieldOfView)
8563 local view = camera.ViewportSize
8564 local ar = view.X/view.Y
8565
8566 projY = 2*tan(fov/2)
8567 projX = ar*projY
8568 end
8569
8570 camera:GetPropertyChangedSignal('FieldOfView'):Connect(updateProjection)
8571 camera:GetPropertyChangedSignal('ViewportSize'):Connect(updateProjection)
8572
8573 updateProjection()
8574
8575 nearPlaneZ = camera.NearPlaneZ
8576 camera:GetPropertyChangedSignal('NearPlaneZ'):Connect(function()
8577 nearPlaneZ = camera.NearPlaneZ
8578 end)
8579end
8580
8581local blacklist = {} do
8582 local charMap = {}
8583
8584 local function refreshIgnoreList()
8585 local n = 1
8586 blacklist = {}
8587 for _, character in pairs(charMap) do
8588 blacklist[n] = character
8589 n = n + 1
8590 end
8591 end
8592
8593 local function playerAdded(player)
8594 local function characterAdded(character)
8595 charMap[player] = character
8596 refreshIgnoreList()
8597 end
8598 local function characterRemoving()
8599 charMap[player] = nil
8600 refreshIgnoreList()
8601 end
8602
8603 player.CharacterAdded:Connect(characterAdded)
8604 player.CharacterRemoving:Connect(characterRemoving)
8605 if player.Character then
8606 characterAdded(player.Character)
8607 end
8608 end
8609
8610 local function playerRemoving(player)
8611 charMap[player] = nil
8612 refreshIgnoreList()
8613 end
8614
8615 Players.PlayerAdded:Connect(playerAdded)
8616 Players.PlayerRemoving:Connect(playerRemoving)
8617
8618 for _, player in ipairs(Players:GetPlayers()) do
8619 playerAdded(player)
8620 end
8621 refreshIgnoreList()
8622end
8623
8624--------------------------------------------------------------------------------------------
8625-- Popper uses the level geometry find an upper bound on subject-to-camera distance.
8626--
8627-- Hard limits are applied immediately and unconditionally. They're generally caused
8628-- when level geometry intersects with the near plane (with exceptions, see below).
8629--
8630-- Soft limits are only applied under certain conditions.
8631-- They're caused when level geometry occludes the subject without actually intersecting
8632-- with the near plane at the target distance.
8633--
8634-- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits.
8635-- We usually don't want the latter to happen.
8636--
8637-- A soft limit will be promoted to a hard limit if an obstruction
8638-- lies between the current and target camera positions.
8639--------------------------------------------------------------------------------------------
8640
8641local subjectRoot
8642local subjectPart
8643
8644camera:GetPropertyChangedSignal('CameraSubject'):Connect(function()
8645 local subject = camera.CameraSubject
8646 if subject:IsA('Humanoid') then
8647 subjectPart = subject.RootPart
8648 elseif subject:IsA('BasePart') then
8649 subjectPart = subject
8650 else
8651 subjectPart = nil
8652 end
8653end)
8654
8655local function canOcclude(part)
8656 -- Occluders must be:
8657 -- 1. Opaque
8658 -- 2. Interactable
8659 -- 3. Not in the same assembly as the subject
8660
8661 if FFlagUserPoppercamLooseOpacityThreshold then
8662 return
8663 part.Transparency < 0.25 and
8664 part.CanCollide and
8665 subjectRoot ~= (part:GetRootPart() or part) and
8666 not part:IsA('TrussPart')
8667 else
8668 return
8669 part.Transparency < 0.95 and
8670 part.CanCollide and
8671 subjectRoot ~= (part:GetRootPart() or part)
8672 end
8673end
8674
8675-- Offsets for the volume visibility test
8676local SCAN_SAMPLE_OFFSETS = {
8677 Vector2.new( 0.4, 0.0),
8678 Vector2.new(-0.4, 0.0),
8679 Vector2.new( 0.0,-0.4),
8680 Vector2.new( 0.0, 0.4),
8681 Vector2.new( 0.0, 0.2),
8682}
8683
8684--------------------------------------------------------------------------------
8685-- Piercing raycasts
8686
8687local function getCollisionPoint(origin, dir)
8688 local originalSize = #blacklist
8689 repeat
8690 local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList(
8691 ray(origin, dir), blacklist, false, true
8692 )
8693
8694 if hitPart then
8695 if hitPart.CanCollide then
8696 eraseFromEnd(blacklist, originalSize)
8697 return hitPoint, true
8698 end
8699 blacklist[#blacklist + 1] = hitPart
8700 end
8701 until not hitPart
8702
8703 eraseFromEnd(blacklist, originalSize)
8704 return origin + dir, false
8705end
8706
8707--------------------------------------------------------------------------------
8708
8709local function queryPoint(origin, unitDir, dist, lastPos)
8710 debug.profilebegin('queryPoint')
8711
8712 local originalSize = #blacklist
8713
8714 dist = dist + nearPlaneZ
8715 local target = origin + unitDir*dist
8716
8717 local softLimit = inf
8718 local hardLimit = inf
8719 local movingOrigin = origin
8720
8721 repeat
8722 local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
8723
8724 if entryPart then
8725 if canOcclude(entryPart) then
8726 local wl = {entryPart}
8727 local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)
8728
8729 local lim = (entryPos - origin).Magnitude
8730
8731 if exitPart then
8732 local promote = false
8733 if lastPos then
8734 promote =
8735 workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or
8736 workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
8737 end
8738
8739 if promote then
8740 -- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
8741 hardLimit = lim
8742 elseif dist < softLimit then
8743 -- Trivial soft limit
8744 softLimit = lim
8745 end
8746 else
8747 -- Trivial hard limit
8748 hardLimit = lim
8749 end
8750 end
8751
8752 blacklist[#blacklist + 1] = entryPart
8753 movingOrigin = entryPos - unitDir*1e-3
8754 end
8755 until hardLimit < inf or not entryPart
8756
8757 eraseFromEnd(blacklist, originalSize)
8758
8759 debug.profileend()
8760 return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
8761end
8762
8763local function queryViewport(focus, dist)
8764 debug.profilebegin('queryViewport')
8765
8766 local fP = focus.p
8767 local fX = focus.rightVector
8768 local fY = focus.upVector
8769 local fZ = -focus.lookVector
8770
8771 local viewport = camera.ViewportSize
8772
8773 local hardBoxLimit = inf
8774 local softBoxLimit = inf
8775
8776 -- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
8777 for viewX = 0, 1 do
8778 local worldX = fX*((viewX - 0.5)*projX)
8779
8780 for viewY = 0, 1 do
8781 local worldY = fY*((viewY - 0.5)*projY)
8782
8783 local origin = fP + nearPlaneZ*(worldX + worldY)
8784 local lastPos = camera:ViewportPointToRay(
8785 viewport.x*viewX,
8786 viewport.y*viewY
8787 ).Origin
8788
8789 local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)
8790
8791 if hardPointLimit < hardBoxLimit then
8792 hardBoxLimit = hardPointLimit
8793 end
8794 if softPointLimit < softBoxLimit then
8795 softBoxLimit = softPointLimit
8796 end
8797 end
8798 end
8799 debug.profileend()
8800
8801 return softBoxLimit, hardBoxLimit
8802end
8803
8804local function testPromotion(focus, dist, focusExtrapolation)
8805 debug.profilebegin('testPromotion')
8806
8807 local fP = focus.p
8808 local fX = focus.rightVector
8809 local fY = focus.upVector
8810 local fZ = -focus.lookVector
8811
8812 do
8813 -- Dead reckoning the camera rotation and focus
8814 debug.profilebegin('extrapolate')
8815
8816 local SAMPLE_DT = 0.0625
8817 local SAMPLE_MAX_T = 1.25
8818
8819 local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude
8820 -- Metric that decides how many samples to take
8821 local combinedSpeed = focusExtrapolation.posVelocity.magnitude
8822
8823 for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do
8824 local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt
8825
8826 if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
8827 return false
8828 end
8829 end
8830
8831 debug.profileend()
8832 end
8833
8834 do
8835 -- Test screen-space offsets from the focus for the presence of soft limits
8836 debug.profilebegin('testOffsets')
8837
8838 for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
8839 local scaledOffset = offset
8840 local pos, isHit = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
8841 if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
8842 return false
8843 end
8844 end
8845
8846 debug.profileend()
8847 end
8848
8849 debug.profileend()
8850 return true
8851end
8852
8853local function Popper(focus, targetDist, focusExtrapolation)
8854 debug.profilebegin('popper')
8855
8856 subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart
8857
8858 local dist = targetDist
8859 local soft, hard = queryViewport(focus, targetDist)
8860 if hard < dist then
8861 dist = hard
8862 end
8863 if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
8864 dist = soft
8865 end
8866
8867 subjectRoot = nil
8868
8869 debug.profileend()
8870 return dist
8871end
8872
8873return Popper