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