Run it
Copy the script and paste it into your executor.
()
a = (()
:().LocalPlayer:().ChildAdded:((c)
c.Name ==
c.Disabled =
)
)
a ==
==
:().LocalPlayer:().ChildAdded:((c)
c.Name ==
c.Disabled =
)
()
Players = :()
TweenService = :()
LocalPlayer = Players.LocalPlayer
LocalPlayer
Players:():()
LocalPlayer = Players.LocalPlayer
(parent, class)
child = parent:(class)
child child.ClassName ~= class
child = parent.ChildAdded:()
child
PlayerGui = (LocalPlayer, )
TOAST_OPEN_SIZE = .(, , , )
TOAST_CLOSED_SIZE = .(, , , )
TOAST_BACKGROUND_COLOR = .(, , )
TOAST_BACKGROUND_TRANS =
TOAST_FOREGROUND_COLOR = .(, , )
TOAST_FOREGROUND_TRANS =
(className)
(props)
inst = .(className)
parent = props.Parent
props.Parent =
name, val (props)
(name) ==
inst[name] = val
val.Parent = inst
inst.Parent = parent
inst
initialized =
uiRoot
toast
toastIcon
toastUpperText
toastLowerText
()
( initialized)
uiRoot = (){
Name = ,
AutoLocalize = ,
Enabled = ,
DisplayOrder = -,
IgnoreGuiInset = ,
ResetOnSpawn = ,
ZIndexBehavior = .ZIndexBehavior.Sibling,
(){
Name = ,
Visible = ,
AnchorPoint = .(, ),
BackgroundTransparency = ,
BorderSizePixel = ,
Position = .(, , , ),
Size = TOAST_CLOSED_SIZE,
Image = ,
ImageColor3 = TOAST_BACKGROUND_COLOR,
ImageRectSize = .(, ),
ImageTransparency = ,
ScaleType = .ScaleType.Slice,
SliceCenter = Rect.(, , , ),
ClipsDescendants = ,
(){
Name = ,
BackgroundTransparency = ,
BorderSizePixel = ,
Position = .(, , , ),
Size = .(, , , ),
(){
Name = ,
AnchorPoint = .(, ),
BackgroundTransparency = ,
Position = .(, , , ),
Size = .(, , , ),
ZIndex = ,
Image = ,
ImageColor3 = TOAST_FOREGROUND_COLOR,
ImageTransparency = ,
}
},
(){
Name = ,
BackgroundTransparency = ,
BorderSizePixel = ,
Position = .(, , , ),
Size = .(, -, , ),
ClipsDescendants = ,
(){
Name = ,
AnchorPoint = .(, ),
BackgroundTransparency = ,
Position = .(, , , ),
Size = .(, , , ),
Font = .Font.GothamSemibold,
Text = ,
TextColor3 = TOAST_FOREGROUND_COLOR,
TextTransparency = ,
TextSize = ,
TextXAlignment = .TextXAlignment.Left,
TextYAlignment = .TextYAlignment.Center,
},
(){
Name = ,
AnchorPoint = .(, ),
BackgroundTransparency = ,
Position = .(, , , ),
Size = .(, , , ),
Font = .Font.Gotham,
Text = ,
TextColor3 = TOAST_FOREGROUND_COLOR,
TextTransparency = ,
TextSize = ,
TextXAlignment = .TextXAlignment.Left,
TextYAlignment = .TextYAlignment.Center,
},
},
},
Parent = PlayerGui,
}
toast = uiRoot.Toast
toastIcon = toast.IconBuffer.Icon
toastUpperText = toast.TextBuffer.Upper
toastLowerText = toast.TextBuffer.Lower
initialized =
CameraUI = {}
CameraUI.(enabled)
enabled initialized
initialized
()
toast.Visible = enabled
enabled
CameraUI.()
tweenInfo = TweenInfo.(, .EasingStyle.Quad, .EasingDirection.Out)
CameraUI.(open)
(initialized)
TweenService:(toast, tweenInfo, {
Size = open TOAST_OPEN_SIZE TOAST_CLOSED_SIZE,
ImageTransparency = open TOAST_BACKGROUND_TRANS ,
}):()
TweenService:(toastIcon, tweenInfo, {
ImageTransparency = open TOAST_FOREGROUND_TRANS ,
}):()
TweenService:(toastUpperText, tweenInfo, {
TextTransparency = open TOAST_FOREGROUND_TRANS ,
}):()
TweenService:(toastLowerText, tweenInfo, {
TextTransparency = open TOAST_FOREGROUND_TRANS ,
}):()
CameraUI
()
Players = :()
UserInputService = :()
GameSettings = ():()
LocalPlayer = Players.LocalPlayer
LocalPlayer
Players:():()
LocalPlayer = Players.LocalPlayer
Mouse = LocalPlayer:()
Input = ()
CameraUI = ()
lastTogglePan =
lastTogglePanChange = ()
CROSS_MOUSE_ICON =
lockStateDirty =
wasTogglePanOnTheLastTimeYouWentIntoFirstPerson =
lastFirstPerson =
CameraUI.()
(isFirstPerson)
togglePan = Input.()
toastTimeout =
isFirstPerson togglePan ~= lastTogglePan
lockStateDirty =
lastTogglePan ~= togglePan () - lastTogglePanChange > toastTimeout
doShow = togglePan () - lastTogglePanChange < toastTimeout
CameraUI.(doShow)
togglePan
lockStateDirty =
lastTogglePanChange = ()
lastTogglePan = togglePan
isFirstPerson ~= lastFirstPerson
isFirstPerson
wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = Input.()
Input.()
lockStateDirty
Input.(wasTogglePanOnTheLastTimeYouWentIntoFirstPerson)
isFirstPerson
Input.()
Mouse.Icon = CROSS_MOUSE_ICON
UserInputService.MouseBehavior = .MouseBehavior.LockCenter
Mouse.Icon =
UserInputService.MouseBehavior = .MouseBehavior.Default
Input.()
Mouse.Icon = CROSS_MOUSE_ICON
UserInputService.MouseBehavior = .MouseBehavior.LockCenter
GameSettings.RotationType = .RotationType.MovementRelative
Input.()
Mouse.Icon =
UserInputService.MouseBehavior = .MouseBehavior.LockCurrentPosition
GameSettings.RotationType = .RotationType.MovementRelative
Mouse.Icon =
UserInputService.MouseBehavior = .MouseBehavior.Default
GameSettings.RotationType = .RotationType.MovementRelative
lastFirstPerson = isFirstPerson
()
UserInputService = :()
MB_TAP_LENGTH =
rmbDown, rmbUp
rmbDownBindable = .()
rmbUpBindable = .()
rmbDown = rmbDownBindable.Event
rmbUp = rmbUpBindable.Event
UserInputService.InputBegan:((input, gpe)
gpe input.UserInputType == .UserInputType.MouseButton2
rmbDownBindable:()
)
UserInputService.InputEnded:((input, gpe)
input.UserInputType == .UserInputType.MouseButton2
rmbUpBindable:()
)
holdPan =
togglePan =
lastRmbDown =
CameraInput = {}
CameraInput.()
holdPan
CameraInput.()
togglePan
CameraInput.()
togglePan holdPan
CameraInput.(value)
togglePan = value
cameraToggleInputEnabled =
rmbDownConnection
rmbUpConnection
CameraInput.()
cameraToggleInputEnabled
cameraToggleInputEnabled =
holdPan =
togglePan =
rmbDownConnection
rmbDownConnection:()
rmbUpConnection
rmbUpConnection:()
rmbDownConnection = rmbDown:(()
holdPan =
lastRmbDown = ()
)
rmbUpConnection = rmbUp:(()
holdPan =
() - lastRmbDown < MB_TAP_LENGTH (togglePan UserInputService:().Magnitude < )
togglePan = togglePan
)
CameraInput.()
cameraToggleInputEnabled
cameraToggleInputEnabled =
rmbDownConnection
rmbDownConnection:()
rmbDownConnection =
rmbUpConnection
rmbUpConnection:()
rmbUpConnection =
CameraInput
()
UNIT_Z = .(,,)
X1_Y0_Z1 = .(,,)
THUMBSTICK_DEADZONE =
DEFAULT_DISTANCE =
PORTRAIT_DEFAULT_DISTANCE =
FIRST_PERSON_DISTANCE_THRESHOLD =
CAMERA_ACTION_PRIORITY = .ContextActionPriority.Default.Value
MIN_Y = .(-)
MAX_Y = .()
TOUCH_ADJUST_AREA_UP = .()
TOUCH_ADJUST_AREA_DOWN = .(-)
TOUCH_SENSITIVTY_ADJUST_MAX_Y =
TOUCH_SENSITIVTY_ADJUST_MIN_Y =
VR_ANGLE = .()
VR_LOW_INTENSITY_ROTATION = .(.(), )
VR_HIGH_INTENSITY_ROTATION = .(.(), )
VR_LOW_INTENSITY_REPEAT =
VR_HIGH_INTENSITY_REPEAT =
ZERO_VECTOR2 = .(,)
ZERO_VECTOR3 = .(,,)
TOUCH_SENSITIVTY = .( * .pi, * .pi)
MOUSE_SENSITIVITY = .( * .pi, * .pi )
SEAT_OFFSET = .(,,)
VR_SEAT_OFFSET = .(,,)
HEAD_OFFSET = .(,,)
R15_HEAD_OFFSET = .(, , )
R15_HEAD_OFFSET_NO_SCALING = .(, , )
HUMANOID_ROOT_PART_SIZE = .(, , )
GAMEPAD_ZOOM_STEP_1 =
GAMEPAD_ZOOM_STEP_2 =
GAMEPAD_ZOOM_STEP_3 =
PAN_SENSITIVITY =
ZOOM_SENSITIVITY_CURVATURE =
abs = .abs
sign = .sign
FFlagUserCameraToggle
success, result = (()
():()
)
FFlagUserCameraToggle = success result
FFlagUserDontAdjustSensitvityForPortrait
success, result = (()
():()
)
FFlagUserDontAdjustSensitvityForPortrait = success result
FFlagUserFixZoomInZoomOutDiscrepancy
success, result = (()
():()
)
FFlagUserFixZoomInZoomOutDiscrepancy = success result
Util = ()
ZoomController = ()
CameraToggleStateController = ()
CameraInput = ()
CameraUI = ()
Players = :()
UserInputService = :()
StarterGui = :()
GuiService = :()
ContextActionService = :()
VRService = :()
UserGameSettings = ():()
player = Players.LocalPlayer
BaseCamera = {}
BaseCamera.__index = BaseCamera
BaseCamera.()
self = ({}, BaseCamera)
self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD
self.cameraType =
self.cameraMovementMode =
self.lastCameraTransform =
self.rotateInput = ZERO_VECTOR2
self.userPanningCamera =
self.lastUserPanCamera = ()
self.humanoidRootPart =
self.humanoidCache = {}
self.lastSubject =
self.lastSubjectPosition = .(,,)
self.defaultSubjectDistance = .(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
self.currentSubjectDistance = .(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
self.inFirstPerson =
self.inMouseLockedMode =
self.portraitMode =
self.isSmallTouchScreen =
self.resetCameraAngle =
self.enabled =
self.inputBeganConn =
self.inputChangedConn =
self.inputEndedConn =
self.startPos =
self.lastPos =
self.panBeginLook =
self.panEnabled =
self.keyPanEnabled =
self.distanceChangeEnabled =
self.PlayerGui =
self.cameraChangedConn =
self.viewportSizeChangedConn =
self.boundContextActions = {}
self.shouldUseVRRotation =
self.VRRotationIntensityAvailable =
self.lastVRRotationIntensityCheckTime =
self.lastVRRotationTime =
self.vrRotateKeyCooldown = {}
self.cameraTranslationConstraints = .(, , )
self.humanoidJumpOrigin =
self.trackingHumanoid =
self.cameraFrozen =
self.subjectStateChangedConn =
self.activeGamepad =
self.gamepadPanningCamera =
self.lastThumbstickRotate =
self.numOfSeconds =
self.currentSpeed =
self.maxSpeed =
self.vrMaxSpeed =
self.lastThumbstickPos = .(,)
self.ySensitivity =
self.lastVelocity =
self.gamepadConnectedConn =
self.gamepadDisconnectedConn =
self.currentZoomSpeed =
self.L3ButtonDown =
self.dpadLeftDown =
self.dpadRightDown =
self.isDynamicThumbstickEnabled =
self.fingerTouches = {}
self.dynamicTouchInput =
self.numUnsunkTouches =
self.inputStartPositions = {}
self.inputStartTimes = {}
self.startingDiff =
self.pinchBeginZoom =
self.userPanningTheCamera =
self.touchActivateConn =
self.mouseLockOffset = ZERO_VECTOR3
player.Character
self:(player.Character)
player.CharacterAdded:((char)
self:(char)
)
self.cameraChangedConn self.cameraChangedConn:()
self.cameraChangedConn = :():(()
self:()
)
self:()
self.playerCameraModeChangeConn self.playerCameraModeChangeConn:()
self.playerCameraModeChangeConn = player:():(()
self:()
)
self.minDistanceChangeConn self.minDistanceChangeConn:()
self.minDistanceChangeConn = player:():(()
self:()
)
self.maxDistanceChangeConn self.maxDistanceChangeConn:()
self.maxDistanceChangeConn = player:():(()
self:()
)
self.playerDevTouchMoveModeChangeConn self.playerDevTouchMoveModeChangeConn:()
self.playerDevTouchMoveModeChangeConn = player:():(()
self:()
)
self:()
self.gameSettingsTouchMoveMoveChangeConn self.gameSettingsTouchMoveMoveChangeConn:()
self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:():(()
self:()
)
self:()
UserGameSettings:()
UserGameSettings:()
self.hasGameLoaded = :()
self.hasGameLoaded
self.gameLoadedConn = .Loaded:(()
self.hasGameLoaded =
self.gameLoadedConn:()
self.gameLoadedConn =
)
self:()
self
BaseCamera:()
BaseCamera:(char)
self.resetCameraAngle = self.resetCameraAngle self:()
self.humanoidRootPart =
UserInputService.TouchEnabled
self.PlayerGui = player:()
_, child (char:())
child:()
self.isAToolEquipped =
char.ChildAdded:((child)
child:()
self.isAToolEquipped =
)
char.ChildRemoved:((child)
child:()
self.isAToolEquipped =
)
BaseCamera:()
self.humanoidRootPart
player.Character
humanoid = player.Character:()
humanoid
self.humanoidRootPart = humanoid.RootPart
self.humanoidRootPart
BaseCamera:(humanoid, isDead)
humanoid:() == .HumanoidStateType.Dead
character = humanoid.Parent
character character:()
character:() humanoid.RootPart
humanoid.RootPart
BaseCamera:()
result = self.lastSubjectPosition
camera = .Workspace.CurrentCamera
cameraSubject = camera camera.CameraSubject
cameraSubject
cameraSubject:()
humanoid = cameraSubject
humanoidIsDead = humanoid:() == .HumanoidStateType.Dead
VRService.VREnabled humanoidIsDead humanoid == self.lastSubject
result = self.lastSubjectPosition
bodyPartToFollow = humanoid.RootPart
humanoidIsDead
humanoid.Parent humanoid.Parent:()
bodyPartToFollow = humanoid.Parent:() bodyPartToFollow
bodyPartToFollow bodyPartToFollow:()
heightOffset
humanoid.RigType == .HumanoidRigType.R15
humanoid.AutomaticScalingEnabled
heightOffset = R15_HEAD_OFFSET
bodyPartToFollow == humanoid.RootPart
rootPartSizeOffset = (humanoid.RootPart.Size.Y/) - (HUMANOID_ROOT_PART_SIZE.Y/)
heightOffset = heightOffset + .(, rootPartSizeOffset, )
heightOffset = R15_HEAD_OFFSET_NO_SCALING
heightOffset = HEAD_OFFSET
humanoidIsDead
heightOffset = ZERO_VECTOR3
result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:(heightOffset + humanoid.CameraOffset)
cameraSubject:()
offset = SEAT_OFFSET
VRService.VREnabled
offset = VR_SEAT_OFFSET
result = cameraSubject.CFrame.p + cameraSubject.CFrame:(offset)
cameraSubject:()
result = cameraSubject.CFrame.p + SEAT_OFFSET
cameraSubject:()
result = cameraSubject.CFrame.p
cameraSubject:()
cameraSubject.PrimaryPart
result = cameraSubject:().p
result = cameraSubject:().p
self.lastSubject = cameraSubject
self.lastSubjectPosition = result
result
BaseCamera:()
self.portraitMode
self.defaultSubjectDistance = .(PORTRAIT_DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
self.defaultSubjectDistance = .(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
BaseCamera:()
camera = .Workspace.CurrentCamera
size = camera.ViewportSize
self.portraitMode = size.X < size.Y
self.isSmallTouchScreen = UserInputService.TouchEnabled (size.Y < size.X < )
self:()
BaseCamera:()
UserInputService.TouchEnabled
self.viewportSizeChangedConn
self.viewportSizeChangedConn:()
self.viewportSizeChangedConn =
newCamera = .Workspace.CurrentCamera
newCamera
self:()
self.viewportSizeChangedConn = newCamera:():(()
self:()
)
self.cameraSubjectChangedConn
self.cameraSubjectChangedConn:()
self.cameraSubjectChangedConn =
camera = .Workspace.CurrentCamera
camera
self.cameraSubjectChangedConn = camera:():(()
self:()
)
self:()
BaseCamera:()
UserInputService.TouchEnabled
self.isDynamicThumbstickEnabled =
BaseCamera:()
self.isDynamicThumbstickEnabled =
BaseCamera:()
player.DevTouchMovementMode == .DevTouchMovementMode.UserChoice
(UserGameSettings.TouchMovementMode == .TouchMovementMode.DynamicThumbstick
UserGameSettings.TouchMovementMode == .TouchMovementMode.Default)
self:()
self:()
BaseCamera:()
player.DevTouchMovementMode.Name ==
self:()
self:()
BaseCamera:()
self:(self.currentSubjectDistance)
BaseCamera:()
VRService.VREnabled self.inFirstPerson
.(VR_ANGLE) * self.currentSubjectDistance
BaseCamera:(translationVector, sensitivity)
FFlagUserDontAdjustSensitvityForPortrait
camera = .Workspace.CurrentCamera
camera camera.ViewportSize.X > camera.ViewportSize.Y > (camera.ViewportSize.Y > camera.ViewportSize.X)
translationVector * .( sensitivity.Y, sensitivity.X)
translationVector * sensitivity
BaseCamera:(enable)
self.enabled ~= enable
self.enabled = enable
self.enabled
self:()
self:()
player.CameraMode == .CameraMode.LockFirstPerson
self.currentSubjectDistance =
self.inFirstPerson
self:()
self:()
self:()
self:()
BaseCamera:()
self.enabled
BaseCamera:(input, processed)
input.UserInputType == .UserInputType.Touch
self:(input, processed)
input.UserInputType == .UserInputType.MouseButton2
self:(input, processed)
input.UserInputType == .UserInputType.MouseButton3
self:(input, processed)
BaseCamera:(input, processed)
input.UserInputType == .UserInputType.Touch
self:(input, processed)
input.UserInputType == .UserInputType.MouseMovement
self:(input, processed)
BaseCamera:(input, processed)
input.UserInputType == .UserInputType.Touch
self:(input, processed)
input.UserInputType == .UserInputType.MouseButton2
self:(input, processed)
input.UserInputType == .UserInputType.MouseButton3
self:(input, processed)
BaseCamera:(wheel, pan, pinch, processed)
processed
pan.Magnitude >
inversionVector = .(, UserGameSettings:())
rotateDelta = self:(PAN_SENSITIVITY*pan, MOUSE_SENSITIVITY)*inversionVector
self.rotateInput = self.rotateInput + rotateDelta
zoom = self.currentSubjectDistance
zoomDelta = -(wheel + pinch)
(zoomDelta) >
newZoom
self.inFirstPerson zoomDelta >
newZoom = FIRST_PERSON_DISTANCE_THRESHOLD
FFlagUserFixZoomInZoomOutDiscrepancy
(zoomDelta > )
newZoom = zoom + zoomDelta*( + zoom*ZOOM_SENSITIVITY_CURVATURE)
newZoom = (zoom + zoomDelta) / ( - zoomDelta*ZOOM_SENSITIVITY_CURVATURE)
newZoom = zoom + zoomDelta*( + zoom*ZOOM_SENSITIVITY_CURVATURE)
self:(newZoom)
BaseCamera:()
self.pointerActionConn = UserInputService.PointerAction:((wheel, pan, pinch, processed)
self:(wheel, pan, pinch, processed)
)
self.inputBeganConn = UserInputService.InputBegan:((input, processed)
self:(input, processed)
)
self.inputChangedConn = UserInputService.InputChanged:((input, processed)
self:(input, processed)
)
self.inputEndedConn = UserInputService.InputEnded:((input, processed)
self:(input, processed)
)
self.menuOpenedConn = GuiService.MenuOpened:(()
self:()
)
self.gamepadConnectedConn = UserInputService.GamepadDisconnected:((gamepadEnum)
self.activeGamepad ~= gamepadEnum
self.activeGamepad =
self:()
)
self.gamepadDisconnectedConn = UserInputService.GamepadConnected:((gamepadEnum)
self.activeGamepad ==
self:()
)
self:()
FFlagUserCameraToggle
self:()
BaseCamera:()
self:()
self:()
BaseCamera:()
connectedGamepads = UserInputService:()
#connectedGamepads >
i = , #connectedGamepads
self.activeGamepad ==
self.activeGamepad = connectedGamepads[i]
connectedGamepads[i].Value < self.activeGamepad.Value
self.activeGamepad = connectedGamepads[i]
self.activeGamepad ==
self.activeGamepad = .UserInputType.Gamepad1
BaseCamera:()
self.inputBeganConn
self.inputBeganConn:()
self.inputBeganConn =
self.inputChangedConn
self.inputChangedConn:()
self.inputChangedConn =
self.inputEndedConn
self.inputEndedConn:()
self.inputEndedConn =
BaseCamera:()
i = , #self.boundContextActions
ContextActionService:(self.boundContextActions[i])
self.boundContextActions = {}
BaseCamera:()
self.pointerActionConn
self.pointerActionConn:()
self.pointerActionConn =
self.menuOpenedConn
self.menuOpenedConn:()
self.menuOpenedConn =
self.mouseLockToggleConn
self.mouseLockToggleConn:()
self.mouseLockToggleConn =
self.gamepadConnectedConn
self.gamepadConnectedConn:()
self.gamepadConnectedConn =
self.gamepadDisconnectedConn
self.gamepadDisconnectedConn:()
self.gamepadDisconnectedConn =
self.subjectStateChangedConn
self.subjectStateChangedConn:()
self.subjectStateChangedConn =
self.viewportSizeChangedConn
self.viewportSizeChangedConn:()
self.viewportSizeChangedConn =
self.touchActivateConn
self.touchActivateConn:()
self.touchActivateConn =
self.turningLeft =
self.turningRight =
self.lastCameraTransform =
self.lastSubjectCFrame =
self.userPanningTheCamera =
self.rotateInput = .()
self.gamepadPanningCamera = .(,)
self.startPos =
self.lastPos =
self.panBeginLook =
self.isRightMouseDown =
self.isMiddleMouseDown =
self.fingerTouches = {}
self.dynamicTouchInput =
self.numUnsunkTouches =
self.startingDiff =
self.pinchBeginZoom =
UserInputService.MouseBehavior ~= .MouseBehavior.LockCenter
UserInputService.MouseBehavior = .MouseBehavior.Default
BaseCamera:()
self.isRightMouseDown =
self.isMiddleMouseDown =
self:()
UserInputService.TouchEnabled
inputObject (self.fingerTouches)
self.fingerTouches[inputObject] =
self.dynamicTouchInput =
self.panBeginLook =
self.startPos =
self.lastPos =
self.userPanningTheCamera =
self.startingDiff =
self.pinchBeginZoom =
self.numUnsunkTouches =
BaseCamera:(name, state, input)
input.UserInputType == self.activeGamepad input.KeyCode == .KeyCode.Thumbstick2
state == .UserInputState.Cancel
self.gamepadPanningCamera = ZERO_VECTOR2
inputVector = .(input.Position.X, -input.Position.Y)
inputVector.magnitude > THUMBSTICK_DEADZONE
self.gamepadPanningCamera = .(input.Position.X, -input.Position.Y)
self.gamepadPanningCamera = ZERO_VECTOR2
.ContextActionResult.Sink
.ContextActionResult.Pass
BaseCamera:(name, state, input)
self.hasGameLoaded VRService.VREnabled
.ContextActionResult.Pass
state == .UserInputState.Cancel
self.turningLeft =
self.turningRight =
.ContextActionResult.Sink
self.panBeginLook == self.keyPanEnabled
input.KeyCode == .KeyCode.Left
self.turningLeft = state == .UserInputState.Begin
input.KeyCode == .KeyCode.Right
self.turningRight = state == .UserInputState.Begin
.ContextActionResult.Sink
.ContextActionResult.Pass
BaseCamera:(rotateAngle)
angle = Util.(self:() * .(,,), rotateAngle, .pi*)
angle ~=
self.rotateInput = self.rotateInput + .(angle, )
self.lastUserPanCamera = ()
self.lastCameraTransform =
BaseCamera:(name, state, input)
input.UserInputType == self.activeGamepad
input.KeyCode == .KeyCode.ButtonR3
state == .UserInputState.Begin
self.distanceChangeEnabled
dist = self:()
dist > (GAMEPAD_ZOOM_STEP_2 + GAMEPAD_ZOOM_STEP_3)/
self:(GAMEPAD_ZOOM_STEP_2)
dist > (GAMEPAD_ZOOM_STEP_1 + GAMEPAD_ZOOM_STEP_2)/
self:(GAMEPAD_ZOOM_STEP_1)
self:(GAMEPAD_ZOOM_STEP_3)
input.KeyCode == .KeyCode.DPadLeft
self.dpadLeftDown = (state == .UserInputState.Begin)
input.KeyCode == .KeyCode.DPadRight
self.dpadRightDown = (state == .UserInputState.Begin)
self.dpadLeftDown
self.currentZoomSpeed =
self.dpadRightDown
self.currentZoomSpeed =
self.currentZoomSpeed =
.ContextActionResult.Sink
.ContextActionResult.Pass
BaseCamera:(name, state, input)
self.hasGameLoaded VRService.VREnabled
.ContextActionResult.Pass
state ~= .UserInputState.Begin
.ContextActionResult.Pass
self.distanceChangeEnabled player.CameraMode ~= .CameraMode.LockFirstPerson
input.KeyCode == .KeyCode.I
self:( self.currentSubjectDistance - )
input.KeyCode == .KeyCode.O
self:( self.currentSubjectDistance + )
.ContextActionResult.Sink
.ContextActionResult.Pass
BaseCamera:(actionName, actionFunc, createTouchButton, ...)
.(self.boundContextActions, actionName)
ContextActionService:(actionName, actionFunc, createTouchButton,
CAMERA_ACTION_PRIORITY, ...)
BaseCamera:()
self:(, (name, state, input) self:(name, state, input) ,
, .KeyCode.Thumbstick2)
self:(, (name, state, input) self:(name, state, input) ,
, .KeyCode.DPadLeft, .KeyCode.DPadRight, .KeyCode.ButtonR3)
BaseCamera:()
self:(, (name, state, input) self:(name, state, input) ,
, .KeyCode.Left, .KeyCode.Right)
self:(, (name, state, input) self:(name, state, input) ,
, .KeyCode.I, .KeyCode.O)
(input)
playerGui = player:()
touchGui = playerGui playerGui:()
touchFrame = touchGui touchGui:()
thumbstickFrame = touchFrame touchFrame:()
thumbstickFrame
frameCornerTopLeft = thumbstickFrame.AbsolutePosition
frameCornerBottomRight = frameCornerTopLeft + thumbstickFrame.AbsoluteSize
input.Position.X >= frameCornerTopLeft.X input.Position.Y >= frameCornerTopLeft.Y
input.Position.X <= frameCornerBottomRight.X input.Position.Y <= frameCornerBottomRight.Y
BaseCamera:(delta, sensitivity)
cameraCFrame = .Workspace.CurrentCamera .Workspace.CurrentCamera.CFrame
cameraCFrame
sensitivity
currPitchAngle = cameraCFrame:()
multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y
currPitchAngle > TOUCH_ADJUST_AREA_UP delta.Y <
fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_UP)/(MAX_Y - TOUCH_ADJUST_AREA_UP)
fractionAdjust = - ( - fractionAdjust)^
multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * (
TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y)
currPitchAngle < TOUCH_ADJUST_AREA_DOWN delta.Y >
fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_DOWN)/(MIN_Y - TOUCH_ADJUST_AREA_DOWN)
fractionAdjust = - ( - fractionAdjust)^
multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * (
TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y)
.(
sensitivity.X,
sensitivity.Y * multiplierY
)
BaseCamera:(input, processed)
canUseDynamicTouch = self.isDynamicThumbstickEnabled processed
canUseDynamicTouch
self.dynamicTouchInput == (input)
self.dynamicTouchInput = input
self.fingerTouches[input] = processed
self.inputStartPositions[input] = input.Position
self.inputStartTimes[input] = ()
self.numUnsunkTouches = self.numUnsunkTouches +
BaseCamera:(input, processed)
self.fingerTouches[input] ==
self.isDynamicThumbstickEnabled
self.fingerTouches[input] = processed
processed
self.numUnsunkTouches = self.numUnsunkTouches +
self.numUnsunkTouches ==
self.fingerTouches[input] ==
self.panBeginLook = self.panBeginLook self:()
self.startPos = self.startPos input.Position
self.lastPos = self.lastPos self.startPos
self.userPanningTheCamera =
delta = input.Position - self.lastPos
delta = .(delta.X, delta.Y * UserGameSettings:())
self.panEnabled
adjustedTouchSensitivity = TOUCH_SENSITIVTY
self:(delta, TOUCH_SENSITIVTY)
desiredXYVector = self:(delta, adjustedTouchSensitivity)
self.rotateInput = self.rotateInput + desiredXYVector
self.lastPos = input.Position
self.panBeginLook =
self.startPos =
self.lastPos =
self.userPanningTheCamera =
self.numUnsunkTouches ==
unsunkTouches = {}
touch, wasSunk (self.fingerTouches)
wasSunk
.(unsunkTouches, touch)
#unsunkTouches ==
difference = (unsunkTouches[].Position - unsunkTouches[].Position).magnitude
self.startingDiff self.pinchBeginZoom
scale = difference / .(, self.startingDiff)
clampedScale = .(scale, , )
self.distanceChangeEnabled
self:(self.pinchBeginZoom / clampedScale)
self.startingDiff = difference
self.pinchBeginZoom = self:()
self.startingDiff =
self.pinchBeginZoom =
BaseCamera:(input, processed)
input == self.dynamicTouchInput
self.dynamicTouchInput =
self.fingerTouches[input] ==
self.numUnsunkTouches ==
self.panBeginLook =
self.startPos =
self.lastPos =
self.userPanningTheCamera =
self.numUnsunkTouches ==
self.startingDiff =
self.pinchBeginZoom =
self.fingerTouches[input] ~= self.fingerTouches[input] ==
self.numUnsunkTouches = self.numUnsunkTouches -
self.fingerTouches[input] =
self.inputStartPositions[input] =
self.inputStartTimes[input] =
BaseCamera:(input, processed)
processed
self.isRightMouseDown =
self:(input, processed)
BaseCamera:(input, processed)
self.isRightMouseDown =
self:(input, processed)
BaseCamera:(input, processed)
processed
self.isMiddleMouseDown =
self:(input, processed)
BaseCamera:(input, processed)
self.isMiddleMouseDown =
self:(input, processed)
BaseCamera:(input, processed)
self.hasGameLoaded VRService.VREnabled
inputDelta = input.Delta
inputDelta = .(inputDelta.X, inputDelta.Y * UserGameSettings:())
isInputPanning = FFlagUserCameraToggle CameraInput.()
isBeginLook = self.startPos self.lastPos self.panBeginLook
isPanning = isBeginLook self.inFirstPerson self.inMouseLockedMode isInputPanning
self.panEnabled isPanning
desiredXYVector = self:(inputDelta, MOUSE_SENSITIVITY)
self.rotateInput = self.rotateInput + desiredXYVector
self.startPos self.lastPos self.panBeginLook
self.lastPos = self.lastPos + input.Delta
BaseCamera:(input, processed)
processed
FFlagUserCameraToggle
self:()
self.panBeginLook = self.panBeginLook self:()
self.startPos = self.startPos input.Position
self.lastPos = self.lastPos self.startPos
self.userPanningTheCamera =
BaseCamera:(input, processed)
FFlagUserCameraToggle
self:()
(self.isRightMouseDown self.isMiddleMouseDown)
self.panBeginLook =
self.startPos =
self.lastPos =
self.userPanningTheCamera =
BaseCamera:()
FFlagUserCameraToggle self.isCameraToggle
CameraUI.()
CameraInput.()
(self.inFirstPerson)
FFlagUserCameraToggle
CameraUI.()
CameraInput.()
self.inFirstPerson self.inMouseLockedMode
UserInputService.MouseBehavior = .MouseBehavior.LockCenter
UserGameSettings.RotationType = .RotationType.MovementRelative
self.isRightMouseDown self.isMiddleMouseDown
UserInputService.MouseBehavior = .MouseBehavior.LockCurrentPosition
UserInputService.MouseBehavior = .MouseBehavior.Default
BaseCamera:()
self:(self.currentSubjectDistance)
BaseCamera:(desiredSubjectDistance)
lastSubjectDistance = self.currentSubjectDistance
player.CameraMode == .CameraMode.LockFirstPerson
self.currentSubjectDistance =
self.inFirstPerson
self:()
newSubjectDistance = .(desiredSubjectDistance, player.CameraMinZoomDistance, player.CameraMaxZoomDistance)
newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD
self.currentSubjectDistance =
self.inFirstPerson
self:()
self.currentSubjectDistance = newSubjectDistance
self.inFirstPerson
self:()
ZoomController.(self.currentSubjectDistance, .(desiredSubjectDistance - lastSubjectDistance))
self.currentSubjectDistance
BaseCamera:( cameraType )
self.cameraType = cameraType
BaseCamera:()
self.cameraType
BaseCamera:( cameraMovementMode )
self.cameraMovementMode = cameraMovementMode
BaseCamera:()
self.cameraMovementMode
BaseCamera:(mouseLocked)
self.inMouseLockedMode = mouseLocked
FFlagUserCameraToggle
self:()
BaseCamera:()
self.inMouseLockedMode
BaseCamera:(offsetVector)
self.mouseLockOffset = offsetVector
BaseCamera:()
self.mouseLockOffset
BaseCamera:()
self.inFirstPerson
BaseCamera:()
BaseCamera:()
BaseCamera:()
self.currentSubjectDistance
BaseCamera:()
camera = .Workspace.CurrentCamera
camera
(camera.CoordinateFrame.p - camera.Focus.p).magnitude
BaseCamera:()
.Workspace.CurrentCamera .Workspace.CurrentCamera.CFrame.lookVector UNIT_Z
BaseCamera:(suppliedLookVector)
currLookVector = suppliedLookVector self:()
currPitchAngle = .(currLookVector.y)
yTheta = .(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle)
constrainedRotateInput = .(self.rotateInput.x, yTheta)
startCFrame = .(ZERO_VECTOR3, currLookVector)
newLookCFrame = .(, -constrainedRotateInput.x, ) * startCFrame * .(-constrainedRotateInput.y,,)
newLookCFrame
BaseCamera:(suppliedLookVector)
newLookCFrame = self:(suppliedLookVector)
newLookCFrame.lookVector
BaseCamera:()
subjectPosition = self:()
vecToSubject = (subjectPosition - .Workspace.CurrentCamera.CFrame.p)
currLookVector = (vecToSubject * X1_Y0_Z1).unit
vrRotateInput = .(self.rotateInput.x, )
startCFrame = .(ZERO_VECTOR3, currLookVector)
yawRotatedVector = (.(, -vrRotateInput.x, ) * startCFrame * .(-vrRotateInput.y,,)).lookVector
(yawRotatedVector * X1_Y0_Z1).unit
BaseCamera:()
character = player player.Character
character
resultHumanoid = self.humanoidCache[player]
resultHumanoid resultHumanoid.Parent == character
resultHumanoid
self.humanoidCache[player] =
humanoid = character:()
humanoid
self.humanoidCache[player] = humanoid
humanoid
BaseCamera:(humanoid, humanoidStateType)
humanoidStateType == .HumanoidStateType.Dead
character = humanoid.Parent
character
character:() humanoid.Torso
humanoid.Torso
humanoid.Torso
BaseCamera:()
gamepadPan = self.gamepadPanningCamera
gamepadPan (self.hasGameLoaded VRService.VREnabled)
gamepadPan = Util.(gamepadPan)
currentTime = ()
gamepadPan.X ~= gamepadPan.Y ~=
self.userPanningTheCamera =
gamepadPan == ZERO_VECTOR2
self.lastThumbstickRotate =
self.lastThumbstickPos == ZERO_VECTOR2
self.currentSpeed =
finalConstant =
self.lastThumbstickRotate
VRService.VREnabled
self.currentSpeed = self.vrMaxSpeed
elapsedTime = (currentTime - self.lastThumbstickRotate) *
self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds))
self.currentSpeed > self.maxSpeed self.currentSpeed = self.maxSpeed
self.lastVelocity
velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
velocityDeltaMag = (velocity - self.lastVelocity).magnitude
velocityDeltaMag >
self.currentSpeed = self.currentSpeed * (/velocityDeltaMag)
self.currentSpeed > self.maxSpeed self.currentSpeed = self.maxSpeed
finalConstant = UserGameSettings.GamepadCameraSensitivity * self.currentSpeed
self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate)
self.lastThumbstickPos = gamepadPan
self.lastThumbstickRotate = currentTime
.( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:())
ZERO_VECTOR2
BaseCamera:()
VRService.VREnabled
rootJoint = self.humanoidRootPart self.humanoidRootPart:()
rootJoint
cameraSubject = .Workspace.CurrentCamera.CameraSubject
isInVehicle = cameraSubject cameraSubject:()
self.inFirstPerson isInVehicle
vrFrame = VRService:(.UserCFrame.Head)
vrRotation = vrFrame - vrFrame.p
rootJoint.C0 = .(vrRotation:(vrFrame.p)) * .(, , , -, , , , , , , , )
rootJoint.C0 = .(, , , -, , , , , , , , )
BaseCamera:()
self.inFirstPerson
BaseCamera:()
VRService.VREnabled
self.VRRotationIntensityAvailable () - self.lastVRRotationIntensityCheckTime <
success, vrRotationIntensity = (() StarterGui:() )
self.VRRotationIntensityAvailable = success vrRotationIntensity ~=
self.lastVRRotationIntensityCheckTime = ()
self.shouldUseVRRotation = success vrRotationIntensity ~= vrRotationIntensity ~=
self.shouldUseVRRotation
BaseCamera:()
vrRotateSum = ZERO_VECTOR2
success, vrRotationIntensity = (() StarterGui:() )
success
vrGamepadRotation = self.GamepadPanningCamera ZERO_VECTOR2
delayExpired = (() - self.lastVRRotationTime) >= self:(vrRotationIntensity)
.(vrGamepadRotation.x) >= self:()
(delayExpired self.vrRotateKeyCooldown[.KeyCode.Thumbstick2])
sign =
vrGamepadRotation.x <
sign = -
vrRotateSum = vrRotateSum + self:(vrRotationIntensity) * sign
self.vrRotateKeyCooldown[.KeyCode.Thumbstick2] =
.(vrGamepadRotation.x) < self:() -
self.vrRotateKeyCooldown[.KeyCode.Thumbstick2] =
self.turningLeft
delayExpired self.vrRotateKeyCooldown[.KeyCode.Left]
vrRotateSum = vrRotateSum - self:(vrRotationIntensity)
self.vrRotateKeyCooldown[.KeyCode.Left] =
self.vrRotateKeyCooldown[.KeyCode.Left] =
self.turningRight
(delayExpired self.vrRotateKeyCooldown[.KeyCode.Right])
vrRotateSum = vrRotateSum + self:(vrRotationIntensity)
self.vrRotateKeyCooldown[.KeyCode.Right] =
self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil
end
if vrRotateSum ~= ZERO_VECTOR2 then
self.lastVRRotationTime = tick()
end
return vrRotateSum
end
function BaseCamera:CancelCameraFreeze(keepConstraints)
if not keepConstraints then
self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z)
end
if self.cameraFrozen then
self.trackingHumanoid = nil
self.cameraFrozen = false
end
end
function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack)
if not self.cameraFrozen then
self.humanoidJumpOrigin = subjectPosition
self.trackingHumanoid = humanoidToTrack
self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z)
self.cameraFrozen = true
end
end
function BaseCamera:OnNewCameraSubject()
if self.subjectStateChangedConn then
self.subjectStateChangedConn:Disconnect()
self.subjectStateChangedConn = nil
end
local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject
if self.trackingHumanoid ~= humanoid then
self:CancelCameraFreeze()
end
if humanoid and humanoid:IsA("Humanoid") then
self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState)
if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then
self:StartCameraFreeze(self:GetSubjectPosition(), humanoid)
elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then
self:CancelCameraFreeze(true)
end
end)
end
end
function BaseCamera:GetVRFocus(subjectPosition, timeDelta)
local lastFocus = self.LastCameraFocus or subjectPosition
if not self.cameraFrozen then
self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z)
end
local newFocus
if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then
newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z))
else
newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y))
end
if self.cameraFrozen then
-- No longer in 3rd person
if self.inFirstPerson then -- not VRService.VREnabled
self:CancelCameraFreeze()
end
-- This case you jumped off a cliff and want to keep your character in view
-- 0.5 is to fix floating point error when not jumping off cliffs
if self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then
self:CancelCameraFreeze()
end
end
return newFocus
end
function BaseCamera:GetRotateAmountValue(vrRotationIntensity)
vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
if vrRotationIntensity then
if vrRotationIntensity == "Low" then
return VR_LOW_INTENSITY_ROTATION
elseif vrRotationIntensity == "High" then
return VR_HIGH_INTENSITY_ROTATION
end
end
return ZERO_VECTOR2
end
function BaseCamera:GetRepeatDelayValue(vrRotationIntensity)
vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity")
if vrRotationIntensity then
if vrRotationIntensity == "Low" then
return VR_LOW_INTENSITY_REPEAT
elseif vrRotationIntensity == "High" then
return VR_HIGH_INTENSITY_REPEAT
end
end
return 0
end
function BaseCamera:Update(dt)
error("BaseCamera:Update() This is a virtual function that should never be getting called.", 2)
end
BaseCamera.UpCFrame = CFrame.new()
function BaseCamera:UpdateUpCFrame(cf)
self.UpCFrame = cf
end
local ZERO = Vector3.new(0, 0, 0)
function BaseCamera:CalculateNewLookCFrame(suppliedLookVector)
local currLookVector = suppliedLookVector or self:GetCameraLookVector()
currLookVector = self.UpCFrame:VectorToObjectSpace(currLookVector)
local currPitchAngle = math.asin(currLookVector.y)
local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle)
local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta)
local startCFrame = CFrame.new(ZERO, currLookVector)
local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)
return newLookCFrame
end
return BaseCamera
end
function _BaseOcclusion()
--[[ The Module ]]--
local BaseOcclusion = {}
BaseOcclusion.__index = BaseOcclusion
setmetatable(BaseOcclusion, {
__call = function(_, ...)
return BaseOcclusion.new(...)
end
})
function BaseOcclusion.new()
local self = setmetatable({}, BaseOcclusion)
return self
end
-- Called when character is added
function BaseOcclusion:CharacterAdded(char, player)
end
-- Called when character is about to be removed
function BaseOcclusion:CharacterRemoving(char, player)
end
function BaseOcclusion:OnCameraSubjectChanged(newSubject)
end
--[[ Derived classes are required to override and implement all of the following functions ]]--
function BaseOcclusion:GetOcclusionMode()
-- Must be overridden in derived classes to return an Enum.DevCameraOcclusionMode value
warn("BaseOcclusion GetOcclusionMode must be overridden by derived classes")
return nil
end
function BaseOcclusion:Enable(enabled)
warn("BaseOcclusion Enable must be overridden by derived classes")
end
function BaseOcclusion:Update(dt, desiredCameraCFrame, desiredCameraFocus)
warn("BaseOcclusion Update must be overridden by derived classes")
return desiredCameraCFrame, desiredCameraFocus
end
return BaseOcclusion
end
function _Popper()
local Players = game:GetService("Players")
local camera = game.Workspace.CurrentCamera
local min = math.min
local tan = math.tan
local rad = math.rad
local inf = math.huge
local ray = Ray.new
local function getTotalTransparency(part)
return 1 - (1 - part.Transparency)*(1 - part.LocalTransparencyModifier)
end
local function eraseFromEnd(t, toSize)
for i = #t, toSize + 1, -1 do
t[i] = nil
end
end
local nearPlaneZ, projX, projY do
local function updateProjection()
local fov = rad(camera.FieldOfView)
local view = camera.ViewportSize
local ar = view.X/view.Y
projY = 2*tan(fov/2)
projX = ar*projY
end
camera:GetPropertyChangedSignal("FieldOfView"):Connect(updateProjection)
camera:GetPropertyChangedSignal("ViewportSize"):Connect(updateProjection)
updateProjection()
nearPlaneZ = camera.NearPlaneZ
camera:GetPropertyChangedSignal("NearPlaneZ"):Connect(function()
nearPlaneZ = camera.NearPlaneZ
end)
end
local blacklist = {} do
local charMap = {}
local function refreshIgnoreList()
local n = 1
blacklist = {}
for _, character in pairs(charMap) do
blacklist[n] = character
n = n + 1
end
end
local function playerAdded(player)
local function characterAdded(character)
charMap[player] = character
refreshIgnoreList()
end
local function characterRemoving()
charMap[player] = nil
refreshIgnoreList()
end
player.CharacterAdded:Connect(characterAdded)
player.CharacterRemoving:Connect(characterRemoving)
if player.Character then
characterAdded(player.Character)
end
end
local function playerRemoving(player)
charMap[player] = nil
refreshIgnoreList()
end
Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(playerRemoving)
for _, player in ipairs(Players:GetPlayers()) do
playerAdded(player)
end
refreshIgnoreList()
end
--------------------------------------------------------------------------------------------
-- Popper uses the level geometry find an upper bound on subject-to-camera distance.
--
-- Hard limits are applied immediately and unconditionally. They are generally caused
-- when level geometry intersects with the near plane (with exceptions, see below).
--
-- Soft limits are only applied under certain conditions.
-- They are caused when level geometry occludes the subject without actually intersecting
-- with the near plane at the target distance.
--
-- Soft limits can be promoted to hard limits and hard limits can be demoted to soft limits.
-- We usually don"t want the latter to happen.
--
-- A soft limit will be promoted to a hard limit if an obstruction
-- lies between the current and target camera positions.
--------------------------------------------------------------------------------------------
local subjectRoot
local subjectPart
camera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
local subject = camera.CameraSubject
if subject:IsA("Humanoid") then
subjectPart = subject.RootPart
elseif subject:IsA("BasePart") then
subjectPart = subject
else
subjectPart = nil
end
end)
local function canOcclude(part)
-- Occluders must be:
-- 1. Opaque
-- 2. Interactable
-- 3. Not in the same assembly as the subject
return
getTotalTransparency(part) < 0.25 and
part.CanCollide and
subjectRoot ~= (part:GetRootPart() or part) and
not part:IsA("TrussPart")
end
-- Offsets for the volume visibility test
local SCAN_SAMPLE_OFFSETS = {
Vector2.new( 0.4, 0.0),
Vector2.new(-0.4, 0.0),
Vector2.new( 0.0,-0.4),
Vector2.new( 0.0, 0.4),
Vector2.new( 0.0, 0.2),
}
--------------------------------------------------------------------------------
-- Piercing raycasts
local function getCollisionPoint(origin, dir)
local originalSize = #blacklist
repeat
local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList(
ray(origin, dir), blacklist, false, true
)
if hitPart then
if hitPart.CanCollide then
eraseFromEnd(blacklist, originalSize)
return hitPoint, true
end
blacklist[#blacklist + 1] = hitPart
end
until not hitPart
eraseFromEnd(blacklist, originalSize)
return origin + dir, false
end
--------------------------------------------------------------------------------
local function queryPoint(origin, unitDir, dist, lastPos)
debug.profilebegin("queryPoint")
local originalSize = #blacklist
dist = dist + nearPlaneZ
local target = origin + unitDir*dist
local softLimit = inf
local hardLimit = inf
local movingOrigin = origin
repeat
local entryPart, entryPos = workspace:FindPartOnRayWithIgnoreList(ray(movingOrigin, target - movingOrigin), blacklist, false, true)
if entryPart then
if canOcclude(entryPart) then
local wl = {entryPart}
local exitPart = workspace:FindPartOnRayWithWhitelist(ray(target, entryPos - target), wl, true)
local lim = (entryPos - origin).Magnitude
if exitPart then
local promote = false
if lastPos then
promote =
workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or
workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, true)
end
if promote then
-- Ostensibly a soft limit, but the camera has passed through it in the last frame, so promote to a hard limit.
hardLimit = lim
elseif dist < softLimit then
-- Trivial soft limit
softLimit = lim
end
else
-- Trivial hard limit
hardLimit = lim
end
end
blacklist[#blacklist + 1] = entryPart
movingOrigin = entryPos - unitDir*1e-3
end
until hardLimit < inf or not entryPart
eraseFromEnd(blacklist, originalSize)
debug.profileend()
return softLimit - nearPlaneZ, hardLimit - nearPlaneZ
end
local function queryViewport(focus, dist)
debug.profilebegin("queryViewport")
local fP = focus.p
local fX = focus.rightVector
local fY = focus.upVector
local fZ = -focus.lookVector
local viewport = camera.ViewportSize
local hardBoxLimit = inf
local softBoxLimit = inf
-- Center the viewport on the PoI, sweep points on the edge towards the target, and take the minimum limits
for viewX = 0, 1 do
local worldX = fX*((viewX - 0.5)*projX)
for viewY = 0, 1 do
local worldY = fY*((viewY - 0.5)*projY)
local origin = fP + nearPlaneZ*(worldX + worldY)
local lastPos = camera:ViewportPointToRay(
viewport.x*viewX,
viewport.y*viewY
).Origin
local softPointLimit, hardPointLimit = queryPoint(origin, fZ, dist, lastPos)
if hardPointLimit < hardBoxLimit then
hardBoxLimit = hardPointLimit
end
if softPointLimit < softBoxLimit then
softBoxLimit = softPointLimit
end
end
end
debug.profileend()
return softBoxLimit, hardBoxLimit
end
local function testPromotion(focus, dist, focusExtrapolation)
debug.profilebegin("testPromotion")
local fP = focus.p
local fX = focus.rightVector
local fY = focus.upVector
local fZ = -focus.lookVector
do
-- Dead reckoning the camera rotation and focus
debug.profilebegin("extrapolate")
local SAMPLE_DT = 0.0625
local SAMPLE_MAX_T = 1.25
local maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude
-- Metric that decides how many samples to take
local combinedSpeed = focusExtrapolation.posVelocity.magnitude
for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do
local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt
if queryPoint(cfDt.p, -cfDt.lookVector, dist) >= dist then
return false
end
end
debug.profileend()
end
do
-- Test screen-space offsets from the focus for the presence of soft limits
debug.profilebegin("testOffsets")
for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
local scaledOffset = offset
local pos = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
return false
end
end
debug.profileend()
end
debug.profileend()
return true
end
local function Popper(focus, targetDist, focusExtrapolation)
debug.profilebegin("popper")
subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart
local dist = targetDist
local soft, hard = queryViewport(focus, targetDist)
if hard < dist then
dist = hard
end
if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then
dist = soft
end
subjectRoot = nil
debug.profileend()
return dist
end
return Popper
end
function _ZoomController()
local ZOOM_STIFFNESS = 4.5
local ZOOM_DEFAULT = 12.5
local ZOOM_ACCELERATION = 0.0375
local MIN_FOCUS_DIST = 0.5
local DIST_OPAQUE = 1
local Popper = _Popper()
local clamp = math.clamp
local exp = math.exp
local min = math.min
local max = math.max
local pi = math.pi
local cameraMinZoomDistance, cameraMaxZoomDistance do
local Player = game:GetService("Players").LocalPlayer
local function updateBounds()
cameraMinZoomDistance = Player.CameraMinZoomDistance
cameraMaxZoomDistance = Player.CameraMaxZoomDistance
end
updateBounds()
Player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(updateBounds)
Player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(updateBounds)
end
local ConstrainedSpring = {} do
ConstrainedSpring.__index = ConstrainedSpring
function ConstrainedSpring.new(freq, x, minValue, maxValue)
x = clamp(x, minValue, maxValue)
return setmetatable({
freq = freq, -- Undamped frequency (Hz)
x = x, -- Current position
v = 0, -- Current velocity
minValue = minValue, -- Minimum bound
maxValue = maxValue, -- Maximum bound
goal = x, -- Goal position
}, ConstrainedSpring)
end
function ConstrainedSpring:Step(dt)
local freq = self.freq*2*pi -- Convert from Hz to rad/s
local x = self.x
local v = self.v
local minValue = self.minValue
local maxValue = self.maxValue
local goal = self.goal
-- Solve the spring ODE for position and velocity after time t, assuming critical damping:
-- 2*f*x'[t] + x''[t] = f^2*(g - x[t])
-- Knowns are x[0] and x'[0].
-- Solve for x[t] and x'[t].
local offset = goal - x
local step = freq*dt
local decay = exp(-step)
local x1 = goal + (v*dt - offset*(step + 1))*decay
local v1 = ((offset*freq - v)*step + v)*decay
-- Constrain
if x1 < minValue then
x1 = minValue
v1 = 0
elseif x1 > maxValue then
x1 = maxValue
v1 = 0
end
self.x = x1
self.v = v1
return x1
end
end
local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, MIN_FOCUS_DIST, cameraMaxZoomDistance)
local function stepTargetZoom(z, dz, zoomMin, zoomMax)
z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax)
if z < DIST_OPAQUE then
z = dz <= 0 and zoomMin or DIST_OPAQUE
end
return z
end
local zoomDelta = 0
local Zoom = {} do
function Zoom.Update(renderDt, focus, extrapolation)
local poppedZoom = math.huge
if zoomSpring.goal > DIST_OPAQUE then
-- Make a pessimistic estimate of zoom distance for this step without accounting for poppercam
local maxPossibleZoom = max(
zoomSpring.x,
stepTargetZoom(zoomSpring.goal, zoomDelta, cameraMinZoomDistance, cameraMaxZoomDistance)
)
-- Run the Popper algorithm on the feasible zoom range, [MIN_FOCUS_DIST, maxPossibleZoom]
poppedZoom = Popper(
focus*CFrame.new(0, 0, MIN_FOCUS_DIST),
maxPossibleZoom - MIN_FOCUS_DIST,
extrapolation
) + MIN_FOCUS_DIST
end
zoomSpring.minValue = MIN_FOCUS_DIST
zoomSpring.maxValue = min(cameraMaxZoomDistance, poppedZoom)
return zoomSpring:Step(renderDt)
end
function Zoom.SetZoomParameters(targetZoom, newZoomDelta)
zoomSpring.goal = targetZoom
zoomDelta = newZoomDelta
end
end
return Zoom
end
function _MouseLockController()
--[[ Constants ]]--
local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png"
local CONTEXT_ACTION_NAME = "MouseLockSwitchAction"
local MOUSELOCK_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value
--[[ Services ]]--
local PlayersService = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local Settings = UserSettings() -- ignore warning
local GameSettings = Settings.GameSettings
local Mouse = PlayersService.LocalPlayer:GetMouse()
--[[ The Module ]]--
local MouseLockController = {}
MouseLockController.__index = MouseLockController
function MouseLockController.new()
local self = setmetatable({}, MouseLockController)
self.isMouseLocked = false
self.savedMouseCursor = nil
self.boundKeys = {Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift} -- defaults
self.mouseLockToggledEvent = Instance.new("BindableEvent")
local boundKeysObj = script:FindFirstChild("BoundKeys")
if (not boundKeysObj) or (not boundKeysObj:IsA("StringValue")) then
-- If object with correct name was found, but it's not a StringValue, destroy and replace
if boundKeysObj then
boundKeysObj:Destroy()
end
boundKeysObj = Instance.new("StringValue")
boundKeysObj.Name = "BoundKeys"
boundKeysObj.Value = "LeftShift,RightShift"
boundKeysObj.Parent = script
end
if boundKeysObj then
boundKeysObj.Changed:Connect(function(value)
self:OnBoundKeysObjectChanged(value)
end)
self:OnBoundKeysObjectChanged(boundKeysObj.Value) -- Initial setup call
end
-- Watch for changes to user's ControlMode and ComputerMovementMode settings and update the feature availability accordingly
GameSettings.Changed:Connect(function(property)
if property == "ControlMode" or property == "ComputerMovementMode" then
self:UpdateMouseLockAvailability()
end
end)
-- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
PlayersService.LocalPlayer:GetPropertyChangedSignal("DevEnableMouseLock"):Connect(function()
self:UpdateMouseLockAvailability()
end)
-- Watch for changes to DevEnableMouseLock and update the feature availability accordingly
PlayersService.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function()
self:UpdateMouseLockAvailability()
end)
self:UpdateMouseLockAvailability()
return self
end
function MouseLockController:GetIsMouseLocked()
return self.isMouseLocked
end
function MouseLockController:GetBindableToggleEvent()
return self.mouseLockToggledEvent.Event
end
function MouseLockController:GetMouseLockOffset()
local offsetValueObj = script:FindFirstChild("CameraOffset")
if offsetValueObj and offsetValueObj:IsA("Vector3Value") then
return offsetValueObj.Value
else
-- If CameraOffset object was found but not correct type, destroy
if offsetValueObj then
offsetValueObj:Destroy()
end
offsetValueObj = Instance.new("Vector3Value")
offsetValueObj.Name = "CameraOffset"
offsetValueObj.Value = Vector3.new(1.75,0,0) -- Legacy Default Value
offsetValueObj.Parent = script
end
if offsetValueObj and offsetValueObj.Value then
return offsetValueObj.Value
end
return Vector3.new(1.75,0,0)
end
function MouseLockController:UpdateMouseLockAvailability()
local devAllowsMouseLock = PlayersService.LocalPlayer.DevEnableMouseLock
local devMovementModeIsScriptable = PlayersService.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable
local userHasMouseLockModeEnabled = GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch
local userHasClickToMoveEnabled = GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove
local MouseLockAvailable = devAllowsMouseLock and userHasMouseLockModeEnabled and not userHasClickToMoveEnabled and not devMovementModeIsScriptable
if MouseLockAvailable~=self.enabled then
self:EnableMouseLock(MouseLockAvailable)
end
end
function MouseLockController:OnBoundKeysObjectChanged(newValue)
self.boundKeys = {} -- Overriding defaults, note: possibly with nothing at all if boundKeysObj.Value is "" or contains invalid values
for token in string.gmatch(newValue,"[^%s,]+") do
for _, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do
if token == keyEnum.Name then
self.boundKeys[#self.boundKeys+1] = keyEnum
break
end
end
end
self:UnbindContextActions()
self:BindContextActions()
end
--[[ Local Functions ]]--
function MouseLockController:OnMouseLockToggled()
self.isMouseLocked = not self.isMouseLocked
if self.isMouseLocked then
local cursorImageValueObj = script:FindFirstChild("CursorImage")
if cursorImageValueObj and cursorImageValueObj:IsA("StringValue") and cursorImageValueObj.Value then
self.savedMouseCursor = Mouse.Icon
Mouse.Icon = cursorImageValueObj.Value
else
if cursorImageValueObj then
cursorImageValueObj:Destroy()
end
cursorImageValueObj = Instance.new("StringValue")
cursorImageValueObj.Name = "CursorImage"
cursorImageValueObj.Value = DEFAULT_MOUSE_LOCK_CURSOR
cursorImageValueObj.Parent = script
self.savedMouseCursor = Mouse.Icon
Mouse.Icon = DEFAULT_MOUSE_LOCK_CURSOR
end
else
if self.savedMouseCursor then
Mouse.Icon = self.savedMouseCursor
self.savedMouseCursor = nil
end
end
self.mouseLockToggledEvent:Fire()
end
function MouseLockController:DoMouseLockSwitch(name, state, input)
if state == Enum.UserInputState.Begin then
self:OnMouseLockToggled()
return Enum.ContextActionResult.Sink
end
return Enum.ContextActionResult.Pass
end
function MouseLockController:BindContextActions()
ContextActionService:BindActionAtPriority(CONTEXT_ACTION_NAME, function(name, state, input)
return self:DoMouseLockSwitch(name, state, input)
end, false, MOUSELOCK_ACTION_PRIORITY, unpack(self.boundKeys))
end
function MouseLockController:UnbindContextActions()
ContextActionService:UnbindAction(CONTEXT_ACTION_NAME)
end
function MouseLockController:IsMouseLocked()
return self.enabled and self.isMouseLocked
end
function MouseLockController:EnableMouseLock(enable)
if enable ~= self.enabled then
self.enabled = enable
if self.enabled then
-- Enabling the mode
self:BindContextActions()
else
-- Disabling
-- Restore mouse cursor
if Mouse.Icon~="" then
Mouse.Icon = ""
end
self:UnbindContextActions()
-- If the mode is disabled while being used, fire the event to toggle it off
if self.isMouseLocked then
self.mouseLockToggledEvent:Fire()
end
self.isMouseLocked = false
end
end
end
return MouseLockController
end
function _TransparencyController()
local MAX_TWEEN_RATE = 2.8 -- per second
local Util = _CameraUtils()
--[[ The Module ]]--
local TransparencyController = {}
TransparencyController.__index = TransparencyController
function TransparencyController.new()
local self = setmetatable({}, TransparencyController)
self.lastUpdate = tick()
self.transparencyDirty = false
self.enabled = false
self.lastTransparency = nil
self.descendantAddedConn, self.descendantRemovingConn = nil, nil
self.toolDescendantAddedConns = {}
self.toolDescendantRemovingConns = {}
self.cachedParts = {}
return self
end
function TransparencyController:HasToolAncestor(object)
if object.Parent == nil then return false end
return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent)
end
function TransparencyController:IsValidPartToModify(part)
if part:IsA('BasePart') or part:IsA('Decal') then
return not self:HasToolAncestor(part)
end
return false
end
function TransparencyController:CachePartsRecursive(object)
if object then
if self:IsValidPartToModify(object) then
self.cachedParts[object] = true
self.transparencyDirty = true
end
for _, child in pairs(object:GetChildren()) do
self:CachePartsRecursive(child)
end
end
end
function TransparencyController:TeardownTransparency()
for child, _ in pairs(self.cachedParts) do
child.LocalTransparencyModifier = 0
end
self.cachedParts = {}
self.transparencyDirty = true
self.lastTransparency = nil
if self.descendantAddedConn then
self.descendantAddedConn:disconnect()
self.descendantAddedConn = nil
end
if self.descendantRemovingConn then
self.descendantRemovingConn:disconnect()
self.descendantRemovingConn = nil
end
for object, conn in pairs(self.toolDescendantAddedConns) do
conn:Disconnect()
self.toolDescendantAddedConns[object] = nil
end
for object, conn in pairs(self.toolDescendantRemovingConns) do
conn:Disconnect()
self.toolDescendantRemovingConns[object] = nil
end
end
function TransparencyController:SetupTransparency(character)
self:TeardownTransparency()
if self.descendantAddedConn then self.descendantAddedConn:disconnect() end
self.descendantAddedConn = character.DescendantAdded:Connect(function(object)
-- This is a part we want to invisify
if self:IsValidPartToModify(object) then
self.cachedParts[object] = true
self.transparencyDirty = true
-- There is now a tool under the character
elseif object:IsA('Tool') then
if self.toolDescendantAddedConns[object] then self.toolDescendantAddedConns[object]:Disconnect() end
self.toolDescendantAddedConns[object] = object.DescendantAdded:Connect(function(toolChild)
self.cachedParts[toolChild] = nil
if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
-- Reset the transparency
toolChild.LocalTransparencyModifier = 0
end
end)
if self.toolDescendantRemovingConns[object] then self.toolDescendantRemovingConns[object]:disconnect() end
self.toolDescendantRemovingConns[object] = object.DescendantRemoving:Connect(function(formerToolChild)
wait() -- wait for new parent
if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
if self:IsValidPartToModify(formerToolChild) then
self.cachedParts[formerToolChild] = true
self.transparencyDirty = true
end
end
end)
end
end)
if self.descendantRemovingConn then self.descendantRemovingConn:disconnect() end
self.descendantRemovingConn = character.DescendantRemoving:connect(function(object)
if self.cachedParts[object] then
self.cachedParts[object] = nil
-- Reset the transparency
object.LocalTransparencyModifier = 0
end
end)
self:CachePartsRecursive(character)
end
function TransparencyController:Enable(enable)
if self.enabled ~= enable then
self.enabled = enable
self:Update()
end
end
function TransparencyController:SetSubject(subject)
local character = nil
if subject and subject:IsA("Humanoid") then
character = subject.Parent
end
if subject and subject:IsA("VehicleSeat") and subject.Occupant then
character = subject.Occupant.Parent
end
if character then
self:SetupTransparency(character)
else
self:TeardownTransparency()
end
end
function TransparencyController:Update()
local instant = false
local now = tick()
local currentCamera = workspace.CurrentCamera
if currentCamera then
local transparency = 0
if not self.enabled then
instant = true
else
local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
transparency = (distance<2) and (1.0-(distance-0.5)/1.5) or 0 --(7 - distance) / 5
if transparency < 0.5 then
transparency = 0
end
if self.lastTransparency then
local deltaTransparency = transparency - self.lastTransparency
-- Don't tween transparency if it is instant or your character was fully invisible last frame
if not instant and transparency < 1 and self.lastTransparency < 0.95 then
local maxDelta = MAX_TWEEN_RATE * (now - self.lastUpdate)
deltaTransparency = math.clamp(deltaTransparency, -maxDelta, maxDelta)
end
transparency = self.lastTransparency + deltaTransparency
else
self.transparencyDirty = true
end
transparency = math.clamp(Util.Round(transparency, 2), 0, 1)
end
if self.transparencyDirty or self.lastTransparency ~= transparency then
for child, _ in pairs(self.cachedParts) do
child.LocalTransparencyModifier = transparency
end
self.transparencyDirty = false
self.lastTransparency = transparency
end
end
self.lastUpdate = now
end
return TransparencyController
end
function _Poppercam()
local ZoomController = _ZoomController()
local TransformExtrapolator = {} do
TransformExtrapolator.__index = TransformExtrapolator
local CF_IDENTITY = CFrame.new()
local function cframeToAxis(cframe)
local axis, angle = cframe:toAxisAngle()
return axis*angle
end
local function axisToCFrame(axis)
local angle = axis.magnitude
if angle > 1e-5 then
return CFrame.fromAxisAngle(axis, angle)
end
return CF_IDENTITY
end
local function extractRotation(cf)
local _, _, _, xx, yx, zx, xy, yy, zy, xz, yz, zz = cf:components()
return CFrame.new(0, 0, 0, xx, yx, zx, xy, yy, zy, xz, yz, zz)
end
function TransformExtrapolator.new()
return setmetatable({
lastCFrame = nil,
}, TransformExtrapolator)
end
function TransformExtrapolator:Step(dt, currentCFrame)
local lastCFrame = self.lastCFrame or currentCFrame
self.lastCFrame = currentCFrame
local currentPos = currentCFrame.p
local currentRot = extractRotation(currentCFrame)
local lastPos = lastCFrame.p
local lastRot = extractRotation(lastCFrame)
-- Estimate velocities from the delta between now and the last frame
-- This estimation can be a little noisy.
local dp = (currentPos - lastPos)/dt
local dr = cframeToAxis(currentRot*lastRot:inverse())/dt
local function extrapolate(t)
local p = dp*t + currentPos
local r = axisToCFrame(dr*t)*currentRot
return r + p
end
return {
extrapolate = extrapolate,
posVelocity = dp,
rotVelocity = dr,
}
end
function TransformExtrapolator:Reset()
self.lastCFrame = nil
end
end
--[[ The Module ]]--
local BaseOcclusion = _BaseOcclusion()
local Poppercam = setmetatable({}, BaseOcclusion)
Poppercam.__index = Poppercam
function Poppercam.new()
local self = setmetatable(BaseOcclusion.new(), Poppercam)
self.focusExtrapolator = TransformExtrapolator.new()
return self
end
function Poppercam:GetOcclusionMode()
return Enum.DevCameraOcclusionMode.Zoom
end
function Poppercam:Enable(enable)
self.focusExtrapolator:Reset()
end
function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController)
local rotatedFocus = CFrame.new(desiredCameraFocus.p, desiredCameraCFrame.p)*CFrame.new(
0, 0, 0,
-1, 0, 0,
0, 1, 0,
0, 0, -1
)
local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus)
local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation)
return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus
end
-- Called when character is added
function Poppercam:CharacterAdded(character, player)
end
-- Called when character is about to be removed
function Poppercam:CharacterRemoving(character, player)
end
function Poppercam:OnCameraSubjectChanged(newSubject)
end
local ZoomController = _ZoomController()
function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController)
local rotatedFocus = desiredCameraFocus * (desiredCameraCFrame - desiredCameraCFrame.p)
local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus)
local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation)
return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus
end
return Poppercam
end
function _Invisicam()
--[[ Top Level Roblox Services ]]--
local PlayersService = game:GetService("Players")
--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local USE_STACKING_TRANSPARENCY = true -- Multiple items between the subject and camera get transparency values that add up to TARGET_TRANSPARENCY
local TARGET_TRANSPARENCY = 0.75 -- Classic Invisicam's Value, also used by new invisicam for parts hit by head and torso rays
local TARGET_TRANSPARENCY_PERIPHERAL = 0.5 -- Used by new SMART_CIRCLE mode for items not hit by head and torso rays
local MODE = {
--CUSTOM = 1, -- Retired, unused
LIMBS = 2, -- Track limbs
MOVEMENT = 3, -- Track movement
CORNERS = 4, -- Char model corners
CIRCLE1 = 5, -- Circle of casts around character
CIRCLE2 = 6, -- Circle of casts around character, camera relative
LIMBMOVE = 7, -- LIMBS mode + MOVEMENT mode
SMART_CIRCLE = 8, -- More sample points on and around character
CHAR_OUTLINE = 9, -- Dynamic outline around the character
}
local LIMB_TRACKING_SET = {
-- Body parts common to R15 and R6
['Head'] = true,
-- Body parts unique to R6
['Left Arm'] = true,
['Right Arm'] = true,
['Left Leg'] = true,
['Right Leg'] = true,
-- Body parts unique to R15
['LeftLowerArm'] = true,
['RightLowerArm'] = true,
['LeftUpperLeg'] = true,
['RightUpperLeg'] = true
}
local CORNER_FACTORS = {
Vector3.new(1,1,-1),
Vector3.new(1,-1,-1),
Vector3.new(-1,-1,-1),
Vector3.new(-1,1,-1)
}
local CIRCLE_CASTS = 10
local MOVE_CASTS = 3
local SMART_CIRCLE_CASTS = 24
local SMART_CIRCLE_INCREMENT = 2.0 * math.pi / SMART_CIRCLE_CASTS
local CHAR_OUTLINE_CASTS = 24
-- Used to sanitize user-supplied functions
local function AssertTypes(param, ...)
local allowedTypes = {}
local typeString = ''
for _, typeName in pairs({...}) do
allowedTypes[typeName] = true
typeString = typeString .. (typeString == '' and '' or ' or ') .. typeName
end
local theType = type(param)
assert(allowedTypes[theType], typeString .. " type expected, got: " .. theType)
end
-- Helper function for Determinant of 3x3, not in CameraUtils for performance reasons
local function Det3x3(a,b,c,d,e,f,g,h,i)
return (a*(e*i-f*h)-b*(d*i-f*g)+c*(d*h-e*g))
end
-- Smart Circle mode needs the intersection of 2 rays that are known to be in the same plane
-- because they are generated from cross products with a common vector. This function is computing
-- that intersection, but it's actually the general solution for the point halfway between where
-- two skew lines come nearest to each other, which is more forgiving.
local function RayIntersection(p0, v0, p1, v1)
local v2 = v0:Cross(v1)
local d1 = p1.x - p0.x
local d2 = p1.y - p0.y
local d3 = p1.z - p0.z
local denom = Det3x3(v0.x,-v1.x,v2.x,v0.y,-v1.y,v2.y,v0.z,-v1.z,v2.z)
if (denom == 0) then
return ZERO_VECTOR3 -- No solution (rays are parallel)
end
local t0 = Det3x3(d1,-v1.x,v2.x,d2,-v1.y,v2.y,d3,-v1.z,v2.z) / denom
local t1 = Det3x3(v0.x,d1,v2.x,v0.y,d2,v2.y,v0.z,d3,v2.z) / denom
local s0 = p0 + t0 * v0
local s1 = p1 + t1 * v1
local s = s0 + 0.5 * ( s1 - s0 )
-- 0.25 studs is a threshold for deciding if the rays are
-- close enough to be considered intersecting, found through testing
if (s1-s0).Magnitude < 0.25 then
return s
else
return ZERO_VECTOR3
end
end
--[[ The Module ]]--
local BaseOcclusion = _BaseOcclusion()
local Invisicam = setmetatable({}, BaseOcclusion)
Invisicam.__index = Invisicam
function Invisicam.new()
local self = setmetatable(BaseOcclusion.new(), Invisicam)
self.char = nil
self.humanoidRootPart = nil
self.torsoPart = nil
self.headPart = nil
self.childAddedConn = nil
self.childRemovedConn = nil
self.behaviors = {} -- Map of modes to behavior fns
self.behaviors[MODE.LIMBS] = self.LimbBehavior
self.behaviors[MODE.MOVEMENT] = self.MoveBehavior
self.behaviors[MODE.CORNERS] = self.CornerBehavior
self.behaviors[MODE.CIRCLE1] = self.CircleBehavior
self.behaviors[MODE.CIRCLE2] = self.CircleBehavior
self.behaviors[MODE.LIMBMOVE] = self.LimbMoveBehavior
self.behaviors[MODE.SMART_CIRCLE] = self.SmartCircleBehavior
self.behaviors[MODE.CHAR_OUTLINE] = self.CharacterOutlineBehavior
self.mode = MODE.SMART_CIRCLE
self.behaviorFunction = self.SmartCircleBehavior
self.savedHits = {} -- Objects currently being faded in/out
self.trackedLimbs = {} -- Used in limb-tracking casting modes
self.camera = game.Workspace.CurrentCamera
self.enabled = false
return self
end
function Invisicam:Enable(enable)
self.enabled = enable
if not enable then
self:Cleanup()
end
end
function Invisicam:GetOcclusionMode()
return Enum.DevCameraOcclusionMode.Invisicam
end
--[[ Module functions ]]--
function Invisicam:LimbBehavior(castPoints)
for limb, _ in pairs(self.trackedLimbs) do
castPoints[#castPoints + 1] = limb.Position
end
end
function Invisicam:MoveBehavior(castPoints)
for i = 1, MOVE_CASTS do
local position, velocity = self.humanoidRootPart.Position, self.humanoidRootPart.Velocity
local horizontalSpeed = Vector3.new(velocity.X, 0, velocity.Z).Magnitude / 2
local offsetVector = (i - 1) * self.humanoidRootPart.CFrame.lookVector * horizontalSpeed
castPoints[#castPoints + 1] = position + offsetVector
end
end
function Invisicam:CornerBehavior(castPoints)
local cframe = self.humanoidRootPart.CFrame
local centerPoint = cframe.p
local rotation = cframe - centerPoint
local halfSize = self.char:GetExtentsSize() / 2 --NOTE: Doesn't update w/ limb animations
castPoints[#castPoints + 1] = centerPoint
for i = 1, #CORNER_FACTORS do
castPoints[#castPoints + 1] = centerPoint + (rotation * (halfSize * CORNER_FACTORS[i]))
end
end
function Invisicam:CircleBehavior(castPoints)
local cframe
if self.mode == MODE.CIRCLE1 then
cframe = self.humanoidRootPart.CFrame
else
local camCFrame = self.camera.CoordinateFrame
cframe = camCFrame - camCFrame.p + self.humanoidRootPart.Position
end
castPoints[#castPoints + 1] = cframe.p
for i = 0, CIRCLE_CASTS - 1 do
local angle = (2 * math.pi / CIRCLE_CASTS) * i
local offset = 3 * Vector3.new(math.cos(angle), math.sin(angle), 0)
castPoints[#castPoints + 1] = cframe * offset
end
end
function Invisicam:LimbMoveBehavior(castPoints)
self:LimbBehavior(castPoints)
self:MoveBehavior(castPoints)
end
function Invisicam:CharacterOutlineBehavior(castPoints)
local torsoUp = self.torsoPart.CFrame.upVector.unit
local torsoRight = self.torsoPart.CFrame.rightVector.unit
-- Torso cross of points for interior coverage
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
if self.headPart then
castPoints[#castPoints + 1] = self.headPart.CFrame.p
end
local cframe = CFrame.new(ZERO_VECTOR3,Vector3.new(self.camera.CoordinateFrame.lookVector.X,0,self.camera.CoordinateFrame.lookVector.Z))
local centerPoint = (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
local partsWhitelist = {self.torsoPart}
if self.headPart then
partsWhitelist[#partsWhitelist + 1] = self.headPart
end
for i = 1, CHAR_OUTLINE_CASTS do
local angle = (2 * math.pi * i / CHAR_OUTLINE_CASTS)
local offset = cframe * (3 * Vector3.new(math.cos(angle), math.sin(angle), 0))
offset = Vector3.new(offset.X, math.max(offset.Y, -2.25), offset.Z)
local ray = Ray.new(centerPoint + offset, -3 * offset)
local hit, hitPoint = game.Workspace:FindPartOnRayWithWhitelist(ray, partsWhitelist, false, false)
if hit then
-- Use hit point as the cast point, but nudge it slightly inside the character so that bumping up against
-- walls is less likely to cause a transparency glitch
castPoints[#castPoints + 1] = hitPoint + 0.2 * (centerPoint - hitPoint).unit
end
end
end
function Invisicam:SmartCircleBehavior(castPoints)
local torsoUp = self.torsoPart.CFrame.upVector.unit
local torsoRight = self.torsoPart.CFrame.rightVector.unit
-- SMART_CIRCLE mode includes rays to head and 5 to the torso.
-- Hands, arms, legs and feet are not included since they
-- are not canCollide and can therefore go inside of parts
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoUp
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoUp
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p + torsoRight
castPoints[#castPoints + 1] = self.torsoPart.CFrame.p - torsoRight
if self.headPart then
castPoints[#castPoints + 1] = self.headPart.CFrame.p
end
local cameraOrientation = self.camera.CFrame - self.camera.CFrame.p
local torsoPoint = Vector3.new(0,0.5,0) + (self.torsoPart and self.torsoPart.Position or self.humanoidRootPart.Position)
local radius = 2.5
-- This loop first calculates points in a circle of radius 2.5 around the torso of the character, in the
-- plane orthogonal to the camera's lookVector. Each point is then raycast to, to determine if it is within
-- the free space surrounding the player (not inside anything). Two iterations are done to adjust points that
-- are inside parts, to try to move them to valid locations that are still on their camera ray, so that the
-- circle remains circular from the camera's perspective, but does not cast rays into walls or parts that are
-- behind, below or beside the character and not really obstructing view of the character. This minimizes
-- the undesirable situation where the character walks up to an exterior wall and it is made invisible even
-- though it is behind the character.
for i = 1, SMART_CIRCLE_CASTS do
local angle = SMART_CIRCLE_INCREMENT * i - 0.5 * math.pi
local offset = radius * Vector3.new(math.cos(angle), math.sin(angle), 0)
local circlePoint = torsoPoint + cameraOrientation * offset
-- Vector from camera to point on the circle being tested
local vp = circlePoint - self.camera.CFrame.p
local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
local castPoint = circlePoint
if hit then
local hprime = hp + 0.1 * hitNormal.unit -- Slightly offset hit point from the hit surface
local v0 = hprime - torsoPoint -- Vector from torso to offset hit point
local perp = (v0:Cross(vp)).unit
-- Vector from the offset hit point, along the hit surface
local v1 = (perp:Cross(hitNormal)).unit
-- Vector from camera to offset hit
local vprime = (hprime - self.camera.CFrame.p).unit
-- This dot product checks to see if the vector along the hit surface would hit the correct
-- side of the invisicam cone, or if it would cross the camera look vector and hit the wrong side
if ( v0.unit:Dot(-v1) < v0.unit:Dot(vprime)) then
castPoint = RayIntersection(hprime, v1, circlePoint, vp)
if castPoint.Magnitude > 0 then
local ray = Ray.new(hprime, castPoint - hprime)
local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
if hit then
local hprime2 = hitPoint + 0.1 * hitNormal.unit
castPoint = hprime2
end
else
castPoint = hprime
end
else
castPoint = hprime
end
local ray = Ray.new(torsoPoint, (castPoint - torsoPoint))
local hit, hitPoint, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {self.char}, false, false )
if hit then
local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
castPoint = castPoint2
end
end
castPoints[#castPoints + 1] = castPoint
end
end
function Invisicam:CheckTorsoReference()
if self.char then
self.torsoPart = self.char:FindFirstChild("Torso")
if not self.torsoPart then
self.torsoPart = self.char:FindFirstChild("UpperTorso")
if not self.torsoPart then
self.torsoPart = self.char:FindFirstChild("HumanoidRootPart")
end
end
self.headPart = self.char:FindFirstChild("Head")
end
end
function Invisicam:CharacterAdded(char, player)
-- We only want the LocalPlayer's character
if player~=PlayersService.LocalPlayer then return end
if self.childAddedConn then
self.childAddedConn:Disconnect()
self.childAddedConn = nil
end
if self.childRemovedConn then
self.childRemovedConn:Disconnect()
self.childRemovedConn = nil
end
self.char = char
self.trackedLimbs = {}
local function childAdded(child)
if child:IsA("BasePart") then
if LIMB_TRACKING_SET[child.Name] then
self.trackedLimbs[child] = true
end
if child.Name == "Torso" or child.Name == "UpperTorso" then
self.torsoPart = child
end
if child.Name == "Head" then
self.headPart = child
end
end
end
local function childRemoved(child)
self.trackedLimbs[child] = nil
-- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
self:CheckTorsoReference()
end
self.childAddedConn = char.ChildAdded:Connect(childAdded)
self.childRemovedConn = char.ChildRemoved:Connect(childRemoved)
for _, child in pairs(self.char:GetChildren()) do
childAdded(child)
end
end
function Invisicam:SetMode(newMode)
AssertTypes(newMode, 'number')
for _, modeNum in pairs(MODE) do
if modeNum == newMode then
self.mode = newMode
self.behaviorFunction = self.behaviors[self.mode]
return
end
end
error("Invalid mode number")
end
function Invisicam:GetObscuredParts()
return self.savedHits
end
-- Want to turn off Invisicam? Be sure to call this after.
function Invisicam:Cleanup()
for hit, originalFade in pairs(self.savedHits) do
hit.LocalTransparencyModifier = originalFade
end
end
function Invisicam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
-- Bail if there is no Character
if not self.enabled or not self.char then
return desiredCameraCFrame, desiredCameraFocus
end
self.camera = game.Workspace.CurrentCamera
-- TODO: Move this to a GetHumanoidRootPart helper, probably combine with CheckTorsoReference
-- Make sure we still have a HumanoidRootPart
if not self.humanoidRootPart then
local humanoid = self.char:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.RootPart then
self.humanoidRootPart = humanoid.RootPart
else
-- Not set up with Humanoid? Try and see if there's one in the Character at all:
self.humanoidRootPart = self.char:FindFirstChild("HumanoidRootPart")
if not self.humanoidRootPart then
-- Bail out, since we're relying on HumanoidRootPart existing
return desiredCameraCFrame, desiredCameraFocus
end
end
-- TODO: Replace this with something more sensible
local ancestryChangedConn
ancestryChangedConn = self.humanoidRootPart.AncestryChanged:Connect(function(child, parent)
if child == self.humanoidRootPart and not parent then
self.humanoidRootPart = nil
if ancestryChangedConn and ancestryChangedConn.Connected then
ancestryChangedConn:Disconnect()
ancestryChangedConn = nil
end
end
end)
end
if not self.torsoPart then
self:CheckTorsoReference()
if not self.torsoPart then
-- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
return desiredCameraCFrame, desiredCameraFocus
end
end
-- Make a list of world points to raycast to
local castPoints = {}
self.behaviorFunction(self, castPoints)
-- Cast to get a list of objects between the camera and the cast points
local currentHits = {}
local ignoreList = {self.char}
local function add(hit)
currentHits[hit] = true
if not self.savedHits[hit] then
self.savedHits[hit] = hit.LocalTransparencyModifier
end
end
local hitParts
local hitPartCount = 0
-- Hash table to treat head-ray-hit parts differently than the rest of the hit parts hit by other rays
-- head/torso ray hit parts will be more transparent than peripheral parts when USE_STACKING_TRANSPARENCY is enabled
local headTorsoRayHitParts = {}
local perPartTransparencyHeadTorsoHits = TARGET_TRANSPARENCY
local perPartTransparencyOtherHits = TARGET_TRANSPARENCY
if USE_STACKING_TRANSPARENCY then
-- This first call uses head and torso rays to find out how many parts are stacked up
-- for the purpose of calculating required per-part transparency
local headPoint = self.headPart and self.headPart.CFrame.p or castPoints[1]
local torsoPoint = self.torsoPart and self.torsoPart.CFrame.p or castPoints[2]
hitParts = self.camera:GetPartsObscuringTarget({headPoint, torsoPoint}, ignoreList)
-- Count how many things the sample rays passed through, including decals. This should only
-- count decals facing the camera, but GetPartsObscuringTarget does not return surface normals,
-- so my compromise for now is to just let any decal increase the part count by 1. Only one
-- decal per part will be considered.
for i = 1, #hitParts do
local hitPart = hitParts[i]
hitPartCount = hitPartCount + 1 -- count the part itself
headTorsoRayHitParts[hitPart] = true
for _, child in pairs(hitPart:GetChildren()) do
if child:IsA('Decal') or child:IsA('Texture') then
hitPartCount = hitPartCount + 1 -- count first decal hit, then break
break
end
end
end
if (hitPartCount > 0) then
perPartTransparencyHeadTorsoHits = math.pow( ((0.5 * TARGET_TRANSPARENCY) + (0.5 * TARGET_TRANSPARENCY / hitPartCount)), 1 / hitPartCount )
perPartTransparencyOtherHits = math.pow( ((0.5 * TARGET_TRANSPARENCY_PERIPHERAL) + (0.5 * TARGET_TRANSPARENCY_PERIPHERAL / hitPartCount)), 1 / hitPartCount )
end
end
-- Now get all the parts hit by all the rays
hitParts = self.camera:GetPartsObscuringTarget(castPoints, ignoreList)
local partTargetTransparency = {}
-- Include decals and textures
for i = 1, #hitParts do
local hitPart = hitParts[i]
partTargetTransparency[hitPart] =headTorsoRayHitParts[hitPart] and perPartTransparencyHeadTorsoHits or perPartTransparencyOtherHits
-- If the part is not already as transparent or more transparent than what invisicam requires, add it to the list of
-- parts to be modified by invisicam
if hitPart.Transparency < partTargetTransparency[hitPart] then
add(hitPart)
end
-- Check all decals and textures on the part
for _, child in pairs(hitPart:GetChildren()) do
if child:IsA('Decal') or child:IsA('Texture') then
if (child.Transparency < partTargetTransparency[hitPart]) then
partTargetTransparency[child] = partTargetTransparency[hitPart]
add(child)
end
end
end
end
-- Invisibilize objects that are in the way, restore those that aren't anymore
for hitPart, originalLTM in pairs(self.savedHits) do
if currentHits[hitPart] then
-- LocalTransparencyModifier gets whatever value is required to print the part's total transparency to equal perPartTransparency
hitPart.LocalTransparencyModifier = (hitPart.Transparency < 1) and ((partTargetTransparency[hitPart] - hitPart.Transparency) / (1.0 - hitPart.Transparency)) or 0
else -- Restore original pre-invisicam value of LTM
hitPart.LocalTransparencyModifier = originalLTM
self.savedHits[hitPart] = nil
end
end
-- Invisicam does not change the camera values
return desiredCameraCFrame, desiredCameraFocus
end
return Invisicam
end
function _LegacyCamera()
local ZERO_VECTOR2 = Vector2.new(0,0)
local Util = _CameraUtils()
--[[ Services ]]--
local PlayersService = game:GetService('Players')
--[[ The Module ]]--
local BaseCamera = _BaseCamera()
local LegacyCamera = setmetatable({}, BaseCamera)
LegacyCamera.__index = LegacyCamera
function LegacyCamera.new()
local self = setmetatable(BaseCamera.new(), LegacyCamera)
self.cameraType = Enum.CameraType.Fixed
self.lastUpdate = tick()
self.lastDistanceToSubject = nil
return self
end
function LegacyCamera:GetModuleName()
return "LegacyCamera"
end
--[[ Functions overridden from BaseCamera ]]--
function LegacyCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
return BaseCamera.SetCameraToSubjectDistance(self,desiredSubjectDistance)
end
function LegacyCamera:Update(dt)
-- Cannot update until cameraType has been set
if not self.cameraType then return end
local now = tick()
local timeDelta = (now - self.lastUpdate)
local camera = workspace.CurrentCamera
local newCameraCFrame = camera.CFrame
local newCameraFocus = camera.Focus
local player = PlayersService.LocalPlayer
if self.lastUpdate == nil or timeDelta > 1 then
self.lastDistanceToSubject = nil
end
local subjectPosition = self:GetSubjectPosition()
if self.cameraType == Enum.CameraType.Fixed then
if self.lastUpdate then
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
local delta = math.min(0.1, now - self.lastUpdate)
local gamepadRotation = self:UpdateGamepad()
self.rotateInput = self.rotateInput + (gamepadRotation * delta)
end
if subjectPosition and player and camera then
local distanceToSubject = self:GetCameraToSubjectDistance()
local newLookVector = self:CalculateNewLookVector()
self.rotateInput = ZERO_VECTOR2
newCameraFocus = camera.Focus -- Fixed camera does not change focus
newCameraCFrame = CFrame.new(camera.CFrame.p, camera.CFrame.p + (distanceToSubject * newLookVector))
end
elseif self.cameraType == Enum.CameraType.Attach then
if subjectPosition and camera then
local distanceToSubject = self:GetCameraToSubjectDistance()
local humanoid = self:GetHumanoid()
if self.lastUpdate and humanoid and humanoid.RootPart then
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
local delta = math.min(0.1, now - self.lastUpdate)
local gamepadRotation = self:UpdateGamepad()
self.rotateInput = self.rotateInput + (gamepadRotation * delta)
local forwardVector = humanoid.RootPart.CFrame.lookVector
local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
if Util.IsFinite(y) then
-- Preserve vertical rotation from user input
self.rotateInput = Vector2.new(y, self.rotateInput.Y)
end
end
local newLookVector = self:CalculateNewLookVector()
self.rotateInput = ZERO_VECTOR2
newCameraFocus = CFrame.new(subjectPosition)
newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
end
elseif self.cameraType == Enum.CameraType.Watch then
if subjectPosition and player and camera then
local cameraLook = nil
local humanoid = self:GetHumanoid()
if humanoid and humanoid.RootPart then
local diffVector = subjectPosition - camera.CFrame.p
cameraLook = diffVector.unit
if self.lastDistanceToSubject and self.lastDistanceToSubject == self:GetCameraToSubjectDistance() then
-- Don't clobber the zoom if they zoomed the camera
local newDistanceToSubject = diffVector.magnitude
self:SetCameraToSubjectDistance(newDistanceToSubject)
end
end
local distanceToSubject = self:GetCameraToSubjectDistance()
local newLookVector = self:CalculateNewLookVector(cameraLook)
self.rotateInput = ZERO_VECTOR2
newCameraFocus = CFrame.new(subjectPosition)
newCameraCFrame = CFrame.new(subjectPosition - (distanceToSubject * newLookVector), subjectPosition)
self.lastDistanceToSubject = distanceToSubject
end
else
-- Unsupported type, return current values unchanged
return camera.CFrame, camera.Focus
end
self.lastUpdate = now
return newCameraCFrame, newCameraFocus
end
return LegacyCamera
end
function _OrbitalCamera()
-- Local private variables and constants
local UNIT_Z = Vector3.new(0,0,1)
local X1_Y0_Z1 = Vector3.new(1,0,1) --Note: not a unit vector, used for projecting onto XZ plane
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local ZERO_VECTOR2 = Vector2.new(0,0)
local TAU = 2 * math.pi
--[[ Gamepad Support ]]--
local THUMBSTICK_DEADZONE = 0.2
-- Do not edit these values, they are not the developer-set limits, they are limits
-- to the values the camera system equations can correctly handle
local MIN_ALLOWED_ELEVATION_DEG = -80
local MAX_ALLOWED_ELEVATION_DEG = 80
local externalProperties = {}
externalProperties["InitialDistance"] = 25
externalProperties["MinDistance"] = 10
externalProperties["MaxDistance"] = 100
externalProperties["InitialElevation"] = 35
externalProperties["MinElevation"] = 35
externalProperties["MaxElevation"] = 35
externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
local Util = _CameraUtils()
--[[ Services ]]--
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
--[[ The Module ]]--
local BaseCamera = _BaseCamera()
local OrbitalCamera = setmetatable({}, BaseCamera)
OrbitalCamera.__index = OrbitalCamera
function OrbitalCamera.new()
local self = setmetatable(BaseCamera.new(), OrbitalCamera)
self.lastUpdate = tick()
-- OrbitalCamera-specific members
self.changedSignalConnections = {}
self.refAzimuthRad = nil
self.curAzimuthRad = nil
self.minAzimuthAbsoluteRad = nil
self.maxAzimuthAbsoluteRad = nil
self.useAzimuthLimits = nil
self.curElevationRad = nil
self.minElevationRad = nil
self.maxElevationRad = nil
self.curDistance = nil
self.minDistance = nil
self.maxDistance = nil
-- Gamepad
self.r3ButtonDown = false
self.l3ButtonDown = false
self.gamepadDollySpeedMultiplier = 1
self.lastUserPanCamera = tick()
self.externalProperties = {}
self.externalProperties["InitialDistance"] = 25
self.externalProperties["MinDistance"] = 10
self.externalProperties["MaxDistance"] = 100
self.externalProperties["InitialElevation"] = 35
self.externalProperties["MinElevation"] = 35
self.externalProperties["MaxElevation"] = 35
self.externalProperties["ReferenceAzimuth"] = -45 -- Angle around the Y axis where the camera starts. -45 offsets the camera in the -X and +Z directions equally
self.externalProperties["CWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CW as seen from above
self.externalProperties["CCWAzimuthTravel"] = 90 -- How many degrees the camera is allowed to rotate from the reference position, CCW as seen from above
self.externalProperties["UseAzimuthLimits"] = false -- Full rotation around Y axis available by default
self:LoadNumberValueParameters()
return self
end
function OrbitalCamera:LoadOrCreateNumberValueParameter(name, valueType, updateFunction)
local valueObj = script:FindFirstChild(name)
if valueObj and valueObj:isA(valueType) then
-- Value object exists and is the correct type, use its value
self.externalProperties[name] = valueObj.Value
elseif self.externalProperties[name] ~= nil then
-- Create missing (or replace incorrectly-typed) valueObject with default value
valueObj = Instance.new(valueType)
valueObj.Name = name
valueObj.Parent = script
valueObj.Value = self.externalProperties[name]
else
print("externalProperties table has no entry for ",name)
return
end
if updateFunction then
if self.changedSignalConnections[name] then
self.changedSignalConnections[name]:Disconnect()
end
self.changedSignalConnections[name] = valueObj.Changed:Connect(function(newValue)
self.externalProperties[name] = newValue
updateFunction(self)
end)
end
end
function OrbitalCamera:SetAndBoundsCheckAzimuthValues()
self.minAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(self.externalProperties["CWAzimuthTravel"]))
self.maxAzimuthAbsoluteRad = math.rad(self.externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(self.externalProperties["CCWAzimuthTravel"]))
self.useAzimuthLimits = self.externalProperties["UseAzimuthLimits"]
if self.useAzimuthLimits then
self.curAzimuthRad = math.max(self.curAzimuthRad, self.minAzimuthAbsoluteRad)
self.curAzimuthRad = math.min(self.curAzimuthRad, self.maxAzimuthAbsoluteRad)
end
end
function OrbitalCamera:SetAndBoundsCheckElevationValues()
-- These degree values are the direct user input values. It is deliberate that they are
-- ranged checked only against the extremes, and not against each other. Any time one
-- is changed, both of the internal values in radians are recalculated. This allows for
-- A developer to change the values in any order and for the end results to be that the
-- internal values adjust to match intent as best as possible.
local minElevationDeg = math.max(self.externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
local maxElevationDeg = math.min(self.externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
-- Set internal values in radians
self.minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
self.maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
self.curElevationRad = math.max(self.curElevationRad, self.minElevationRad)
self.curElevationRad = math.min(self.curElevationRad, self.maxElevationRad)
end
function OrbitalCamera:SetAndBoundsCheckDistanceValues()
self.minDistance = self.externalProperties["MinDistance"]
self.maxDistance = self.externalProperties["MaxDistance"]
self.curDistance = math.max(self.curDistance, self.minDistance)
self.curDistance = math.min(self.curDistance, self.maxDistance)
end
-- This loads from, or lazily creates, NumberValue objects for exposed parameters
function OrbitalCamera:LoadNumberValueParameters()
-- These initial values do not require change listeners since they are read only once
self:LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
self:LoadOrCreateNumberValueParameter("InitialDistance", "NumberValue", nil)
-- Note: ReferenceAzimuth is also used as an initial value, but needs a change listener because it is used in the calculation of the limits
self:LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", self.SetAndBoundsCheckAzimuthValue)
self:LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
self:LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", self.SetAndBoundsCheckAzimuthValues)
self:LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
self:LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", self.SetAndBoundsCheckElevationValues)
self:LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
self:LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", self.SetAndBoundsCheckDistanceValues)
self:LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", self.SetAndBoundsCheckAzimuthValues)
-- Internal values set (in radians, from degrees), plus sanitization
self.curAzimuthRad = math.rad(self.externalProperties["ReferenceAzimuth"])
self.curElevationRad = math.rad(self.externalProperties["InitialElevation"])
self.curDistance = self.externalProperties["InitialDistance"]
self:SetAndBoundsCheckAzimuthValues()
self:SetAndBoundsCheckElevationValues()
self:SetAndBoundsCheckDistanceValues()
end
function OrbitalCamera:GetModuleName()
return "OrbitalCamera"
end
function OrbitalCamera:SetInitialOrientation(humanoid)
if not humanoid or not humanoid.RootPart then
warn("OrbitalCamera could not set initial orientation due to missing humanoid")
return
end
local newDesiredLook = (humanoid.RootPart.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
local horizontalShift = Util.GetAngleBetweenXZVectors(newDesiredLook, self:GetCameraLookVector())
local vertShift = math.asin(self:GetCameraLookVector().y) - math.asin(newDesiredLook.y)
if not Util.IsFinite(horizontalShift) then
horizontalShift = 0
end
if not Util.IsFinite(vertShift) then
vertShift = 0
end
self.rotateInput = Vector2.new(horizontalShift, vertShift)
end
--[[ Functions of BaseCamera that are overridden by OrbitalCamera ]]--
function OrbitalCamera:GetCameraToSubjectDistance()
return self.curDistance
end
function OrbitalCamera:SetCameraToSubjectDistance(desiredSubjectDistance)
print("OrbitalCamera SetCameraToSubjectDistance ",desiredSubjectDistance)
local player = PlayersService.LocalPlayer
if player then
self.currentSubjectDistance = math.clamp(desiredSubjectDistance, self.minDistance, self.maxDistance)
-- OrbitalCamera is not allowed to go into the first-person range
self.currentSubjectDistance = math.max(self.currentSubjectDistance, self.FIRST_PERSON_DISTANCE_THRESHOLD)
end
self.inFirstPerson = false
self:UpdateMouseBehavior()
return self.currentSubjectDistance
end
function OrbitalCamera:CalculateNewLookVector(suppliedLookVector, xyRotateVector)
local currLookVector = suppliedLookVector or self:GetCameraLookVector()
local currPitchAngle = math.asin(currLookVector.y)
local yTheta = math.clamp(xyRotateVector.y, currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG))
local constrainedRotateInput = Vector2.new(xyRotateVector.x, yTheta)
local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector)
local newLookVector = (CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0)).lookVector
return newLookVector
end
function OrbitalCamera:GetGamepadPan(name, state, input)
if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
if self.r3ButtonDown or self.l3ButtonDown then
-- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
if (input.Position.Y > THUMBSTICK_DEADZONE) then
self.gamepadDollySpeedMultiplier = 0.96
elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
self.gamepadDollySpeedMultiplier = 1.04
else
self.gamepadDollySpeedMultiplier = 1.00
end
else
if state == Enum.UserInputState.Cancel then
self.gamepadPanningCamera = ZERO_VECTOR2
return
end
local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
if inputVector.magnitude > THUMBSTICK_DEADZONE then
self.gamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
else
self.gamepadPanningCamera = ZERO_VECTOR2
end
end
return Enum.ContextActionResult.Sink
end
return Enum.ContextActionResult.Pass
end
function OrbitalCamera:DoGamepadZoom(name, state, input)
if input.UserInputType == self.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
if (state == Enum.UserInputState.Begin) then
self.r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
self.l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
elseif (state == Enum.UserInputState.End) then
if (input.KeyCode == Enum.KeyCode.ButtonR3) then
self.r3ButtonDown = false
elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
self.l3ButtonDown = false
end
if (not self.r3ButtonDown) and (not self.l3ButtonDown) then
self.gamepadDollySpeedMultiplier = 1.00
end
end
return Enum.ContextActionResult.Sink
end
return Enum.ContextActionResult.Pass
end
function OrbitalCamera:BindGamepadInputActions()
self:BindAction("OrbitalCamGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end,
false, Enum.KeyCode.Thumbstick2)
self:BindAction("OrbitalCamGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end,
false, Enum.KeyCode.ButtonR3, Enum.KeyCode.ButtonL3)
end
-- [[ Update ]]--
function OrbitalCamera:Update(dt)
local now = tick()
local timeDelta = (now - self.lastUpdate)
local userPanningTheCamera = (self.UserPanningTheCamera == true)
local camera = workspace.CurrentCamera
local newCameraCFrame = camera.CFrame
local newCameraFocus = camera.Focus
local player = PlayersService.LocalPlayer
local cameraSubject = camera and camera.CameraSubject
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
if self.lastUpdate == nil or timeDelta > 1 then
self.lastCameraTransform = nil
end
if self.lastUpdate then
local gamepadRotation = self:UpdateGamepad()
if self:ShouldUseVRRotation() then
self.RotateInput = self.RotateInput + self:GetVRRotationInput()
else
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
local delta = math.min(0.1, timeDelta)
if gamepadRotation ~= ZERO_VECTOR2 then
userPanningTheCamera = true
self.rotateInput = self.rotateInput + (gamepadRotation * delta)
end
local angle = 0
if not (isInVehicle or isOnASkateboard) then
angle = angle + (self.TurningLeft and -120 or 0)
angle = angle + (self.TurningRight and 120 or 0)
end
if angle ~= 0 then
self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
userPanningTheCamera = true
end
end
end
-- Reset tween speed if user is panning
if userPanningTheCamera then
self.lastUserPanCamera = tick()
end
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
-- Process any dollying being done by gamepad
-- TODO: Move this
if self.gamepadDollySpeedMultiplier ~= 1 then
self:SetCameraToSubjectDistance(self.currentSubjectDistance * self.gamepadDollySpeedMultiplier)
end
local VREnabled = VRService.VREnabled
newCameraFocus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame.new(subjectPosition)
local cameraFocusP = newCameraFocus.p
if VREnabled and not self:IsInFirstPerson() then
local cameraHeight = self:GetCameraHeight()
local vecToSubject = (subjectPosition - camera.CFrame.p)
local distToSubject = vecToSubject.magnitude
-- Only move the camera if it exceeded a maximum distance to the subject in VR
if distToSubject > self.currentSubjectDistance or self.rotateInput.x ~= 0 then
local desiredDist = math.min(distToSubject, self.currentSubjectDistance)
-- Note that CalculateNewLookVector is overridden from BaseCamera
vecToSubject = self:CalculateNewLookVector(vecToSubject.unit * X1_Y0_Z1, Vector2.new(self.rotateInput.x, 0)) * desiredDist
local newPos = cameraFocusP - vecToSubject
local desiredLookDir = camera.CFrame.lookVector
if self.rotateInput.x ~= 0 then
desiredLookDir = vecToSubject
end
local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
self.RotateInput = ZERO_VECTOR2
newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
end
else
-- self.RotateInput is a Vector2 of mouse movement deltas since last update
self.curAzimuthRad = self.curAzimuthRad - self.rotateInput.x
if self.useAzimuthLimits then
self.curAzimuthRad = math.clamp(self.curAzimuthRad, self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad)
else
self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0
end
self.curElevationRad = math.clamp(self.curElevationRad + self.rotateInput.y, self.minElevationRad, self.maxElevationRad)
local cameraPosVector = self.currentSubjectDistance * ( CFrame.fromEulerAnglesYXZ( -self.curElevationRad, self.curAzimuthRad, 0 ) * UNIT_Z )
local camPos = subjectPosition + cameraPosVector
newCameraCFrame = CFrame.new(camPos, subjectPosition)
self.rotateInput = ZERO_VECTOR2
end
self.lastCameraTransform = newCameraCFrame
self.lastCameraFocus = newCameraFocus
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
self.lastSubjectCFrame = cameraSubject.CFrame
else
self.lastSubjectCFrame = nil
end
end
self.lastUpdate = now
return newCameraCFrame, newCameraFocus
end
return OrbitalCamera
end
function _ClassicCamera()
-- Local private variables and constants
local ZERO_VECTOR2 = Vector2.new(0,0)
local tweenAcceleration = math.rad(220) --Radians/Second^2
local tweenSpeed = math.rad(0) --Radians/Second
local tweenMaxSpeed = math.rad(250) --Radians/Second
local TIME_BEFORE_AUTO_ROTATE = 2.0 --Seconds, used when auto-aligning camera with vehicles
local INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0)
local FFlagUserCameraToggle do
local success, result = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
end)
FFlagUserCameraToggle = success and result
end
--[[ Services ]]--
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local CameraInput = _CameraInput()
local Util = _CameraUtils()
--[[ The Module ]]--
local BaseCamera = _BaseCamera()
local ClassicCamera = setmetatable({}, BaseCamera)
ClassicCamera.__index = ClassicCamera
function ClassicCamera.new()
local self = setmetatable(BaseCamera.new(), ClassicCamera)
self.isFollowCamera = false
self.isCameraToggle = false
self.lastUpdate = tick()
self.cameraToggleSpring = Util.Spring.new(5, 0)
return self
end
function ClassicCamera:GetCameraToggleOffset(dt)
assert(FFlagUserCameraToggle)
if self.isCameraToggle then
local zoom = self.currentSubjectDistance
if CameraInput.getTogglePan() then
self.cameraToggleSpring.goal = math.clamp(Util.map(zoom, 0.5, self.FIRST_PERSON_DISTANCE_THRESHOLD, 0, 1), 0, 1)
else
self.cameraToggleSpring.goal = 0
end
local distanceOffset = math.clamp(Util.map(zoom, 0.5, 64, 0, 1), 0, 1) + 1
return Vector3.new(0, self.cameraToggleSpring:step(dt)*distanceOffset, 0)
end
return Vector3.new()
end
-- Movement mode standardized to Enum.ComputerCameraMovementMode values
function ClassicCamera:SetCameraMovementMode(cameraMovementMode)
BaseCamera.SetCameraMovementMode(self, cameraMovementMode)
self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow
self.isCameraToggle = cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle
end
function ClassicCamera:Update()
local now = tick()
local timeDelta = now - self.lastUpdate
local camera = workspace.CurrentCamera
local newCameraCFrame = camera.CFrame
local newCameraFocus = camera.Focus
local overrideCameraLookVector = nil
if self.resetCameraAngle then
local rootPart = self:GetHumanoidRootPart()
if rootPart then
overrideCameraLookVector = (rootPart.CFrame * INITIAL_CAMERA_ANGLE).lookVector
else
overrideCameraLookVector = INITIAL_CAMERA_ANGLE.lookVector
end
self.resetCameraAngle = false
end
local player = PlayersService.LocalPlayer
local humanoid = self:GetHumanoid()
local cameraSubject = camera.CameraSubject
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
local isClimbing = humanoid and humanoid:GetState() == Enum.HumanoidStateType.Climbing
if self.lastUpdate == nil or timeDelta > 1 then
self.lastCameraTransform = nil
end
if self.lastUpdate then
local gamepadRotation = self:UpdateGamepad()
if self:ShouldUseVRRotation() then
self.rotateInput = self.rotateInput + self:GetVRRotationInput()
else
-- Cap out the delta to 0.1 so we don't get some crazy things when we re-resume from
local delta = math.min(0.1, timeDelta)
if gamepadRotation ~= ZERO_VECTOR2 then
self.rotateInput = self.rotateInput + (gamepadRotation * delta)
end
local angle = 0
if not (isInVehicle or isOnASkateboard) then
angle = angle + (self.turningLeft and -120 or 0)
angle = angle + (self.turningRight and 120 or 0)
end
if angle ~= 0 then
self.rotateInput = self.rotateInput + Vector2.new(math.rad(angle * delta), 0)
end
end
end
local cameraHeight = self:GetCameraHeight()
-- Reset tween speed if user is panning
if self.userPanningTheCamera then
tweenSpeed = 0
self.lastUserPanCamera = tick()
end
local userRecentlyPannedCamera = now - self.lastUserPanCamera < TIME_BEFORE_AUTO_ROTATE
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:GetCameraToSubjectDistance()
if zoom < 0.5 then
zoom = 0.5
end
if self:GetIsMouseLocked() and not self:IsInFirstPerson() then
-- We need to use the right vector of the camera after rotation, not before
local newLookCFrame = self:CalculateNewLookCFrame(overrideCameraLookVector)
local offset = self:GetMouseLockOffset()
local cameraRelativeOffset = offset.X * newLookCFrame.rightVector + offset.Y * newLookCFrame.upVector + offset.Z * newLookCFrame.lookVector
--offset can be NAN, NAN, NAN if newLookVector has only y component
if Util.IsFiniteVector3(cameraRelativeOffset) then
subjectPosition = subjectPosition + cameraRelativeOffset
end
else
if not self.userPanningTheCamera and self.lastCameraTransform then
local isInFirstPerson = self:IsInFirstPerson()
if (isInVehicle or isOnASkateboard or (self.isFollowCamera and isClimbing)) and self.lastUpdate and humanoid and humanoid.Torso then
if isInFirstPerson then
if self.lastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
local y = -Util.GetAngleBetweenXZVectors(self.lastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
if Util.IsFinite(y) then
self.rotateInput = self.rotateInput + Vector2.new(y, 0)
end
tweenSpeed = 0
end
elseif not userRecentlyPannedCamera then
local forwardVector = humanoid.Torso.CFrame.lookVector
if isOnASkateboard then
forwardVector = cameraSubject.CFrame.lookVector
end
tweenSpeed = math.clamp(tweenSpeed + tweenAcceleration * timeDelta, 0, tweenMaxSpeed)
local percent = math.clamp(tweenSpeed * timeDelta, 0, 1)
if self:IsInFirstPerson() and not (self.isFollowCamera and self.isClimbing) then
percent = 1
end
local y = Util.GetAngleBetweenXZVectors(forwardVector, self:GetCameraLookVector())
if Util.IsFinite(y) and math.abs(y) > 0.0001 then
self.rotateInput = self.rotateInput + Vector2.new(y * percent, 0)
end
end
elseif self.isFollowCamera and (not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled) then
-- Logic that was unique to the old FollowCamera module
local lastVec = -(self.lastCameraTransform.p - subjectPosition)
local y = Util.GetAngleBetweenXZVectors(lastVec, self:GetCameraLookVector())
-- This cutoff is to decide if the humanoid's angle of movement,
-- relative to the camera's look vector, is enough that
-- we want the camera to be following them. The point is to provide
-- a sizable dead zone to allow more precise forward movements.
local thetaCutoff = 0.4
-- Check for NaNs
if Util.IsFinite(y) and math.abs(y) > 0.0001 and math.abs(y) > thetaCutoff * timeDelta then
self.rotateInput = self.rotateInput + Vector2.new(y, 0)
end
end
end
end
if not self.isFollowCamera then
local VREnabled = VRService.VREnabled
if VREnabled then
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
else
newCameraFocus = CFrame.new(subjectPosition)
end
local cameraFocusP = newCameraFocus.p
if VREnabled and not self:IsInFirstPerson() then
local vecToSubject = (subjectPosition - camera.CFrame.p)
local distToSubject = vecToSubject.magnitude
-- Only move the camera if it exceeded a maximum distance to the subject in VR
if distToSubject > zoom or self.rotateInput.x ~= 0 then
local desiredDist = math.min(distToSubject, zoom)
vecToSubject = self:CalculateNewLookVectorVR() * desiredDist
local newPos = cameraFocusP - vecToSubject
local desiredLookDir = camera.CFrame.lookVector
if self.rotateInput.x ~= 0 then
desiredLookDir = vecToSubject
end
local lookAt = Vector3.new(newPos.x + desiredLookDir.x, newPos.y, newPos.z + desiredLookDir.z)
self.rotateInput = ZERO_VECTOR2
newCameraCFrame = CFrame.new(newPos, lookAt) + Vector3.new(0, cameraHeight, 0)
end
else
local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
self.rotateInput = ZERO_VECTOR2
newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
end
else -- is FollowCamera
local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector)
self.rotateInput = ZERO_VECTOR2
if VRService.VREnabled then
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
else
newCameraFocus = CFrame.new(subjectPosition)
end
newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, cameraHeight, 0)
end
if FFlagUserCameraToggle then
local toggleOffset = self:GetCameraToggleOffset(timeDelta)
newCameraFocus = newCameraFocus + toggleOffset
newCameraCFrame = newCameraCFrame + toggleOffset
end
self.lastCameraTransform = newCameraCFrame
self.lastCameraFocus = newCameraFocus
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
self.lastSubjectCFrame = cameraSubject.CFrame
else
self.lastSubjectCFrame = nil
end
end
self.lastUpdate = now
return newCameraCFrame, newCameraFocus
end
function ClassicCamera:EnterFirstPerson()
self.inFirstPerson = true
self:UpdateMouseBehavior()
end
function ClassicCamera:LeaveFirstPerson()
self.inFirstPerson = false
self:UpdateMouseBehavior()
end
return ClassicCamera
end
function _CameraUtils()
local CameraUtils = {}
local FFlagUserCameraToggle do
local success, result = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
end)
FFlagUserCameraToggle = success and result
end
local function round(num)
return math.floor(num + 0.5)
end
-- Critically damped spring class for fluid motion effects
local Spring = {} do
Spring.__index = Spring
-- Initialize to a given undamped frequency and default position
function Spring.new(freq, pos)
return setmetatable({
freq = freq,
goal = pos,
pos = pos,
vel = 0,
}, Spring)
end
-- Advance the spring simulation by `dt` seconds
function Spring:step(dt)
local f = self.freq*2*math.pi
local g = self.goal
local p0 = self.pos
local v0 = self.vel
local offset = p0 - g
local decay = math.exp(-f*dt)
local p1 = (offset*(1 + f*dt) + v0*dt)*decay + g
local v1 = (v0*(1 - f*dt) - offset*(f*f*dt))*decay
self.pos = p1
self.vel = v1
return p1
end
end
CameraUtils.Spring = Spring
-- map a value from one range to another
function CameraUtils.map(x, inMin, inMax, outMin, outMax)
return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
end
-- From TransparencyController
function CameraUtils.Round(num, places)
local decimalPivot = 10^places
return math.floor(num * decimalPivot + 0.5) / decimalPivot
end
function CameraUtils.IsFinite(val)
return val == val and val ~= math.huge and val ~= -math.huge
end
function CameraUtils.IsFiniteVector3(vec3)
return CameraUtils.IsFinite(vec3.X) and CameraUtils.IsFinite(vec3.Y) and CameraUtils.IsFinite(vec3.Z)
end
-- Legacy implementation renamed
function CameraUtils.GetAngleBetweenXZVectors(v1, v2)
return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
end
function CameraUtils.RotateVectorByAngleAndRound(camLook, rotateAngle, roundAmount)
if camLook.Magnitude > 0 then
camLook = camLook.unit
local currAngle = math.atan2(camLook.z, camLook.x)
local newAngle = round((math.atan2(camLook.z, camLook.x) + rotateAngle) / roundAmount) * roundAmount
return newAngle - currAngle
end
return 0
end
-- K is a tunable parameter that changes the shape of the S-curve
-- the larger K is the more straight/linear the curve gets
local k = 0.35
local lowerK = 0.8
local function SCurveTranform(t)
t = math.clamp(t, -1, 1)
if t >= 0 then
return (k*t) / (k - t + 1)
end
return -((lowerK*-t) / (lowerK + t + 1))
end
local DEADZONE = 0.1
local function toSCurveSpace(t)
return (1 + DEADZONE) * (2*math.abs(t) - 1) - DEADZONE
end
local function fromSCurveSpace(t)
return t/2 + 0.5
end
function CameraUtils.GamepadLinearToCurve(thumbstickPosition)
local function onAxis(axisValue)
local sign = 1
if axisValue < 0 then
sign = -1
end
local point = fromSCurveSpace(SCurveTranform(toSCurveSpace(math.abs(axisValue))))
point = point * sign
return math.clamp(point, -1, 1)
end
return Vector2.new(onAxis(thumbstickPosition.x), onAxis(thumbstickPosition.y))
end
-- This function converts 4 different, redundant enumeration types to one standard so the values can be compared
function CameraUtils.ConvertCameraModeEnumToStandard(enumValue)
if enumValue == Enum.TouchCameraMovementMode.Default then
return Enum.ComputerCameraMovementMode.Follow
end
if enumValue == Enum.ComputerCameraMovementMode.Default then
return Enum.ComputerCameraMovementMode.Classic
end
if enumValue == Enum.TouchCameraMovementMode.Classic or
enumValue == Enum.DevTouchCameraMovementMode.Classic or
enumValue == Enum.DevComputerCameraMovementMode.Classic or
enumValue == Enum.ComputerCameraMovementMode.Classic then
return Enum.ComputerCameraMovementMode.Classic
end
if enumValue == Enum.TouchCameraMovementMode.Follow or
enumValue == Enum.DevTouchCameraMovementMode.Follow or
enumValue == Enum.DevComputerCameraMovementMode.Follow or
enumValue == Enum.ComputerCameraMovementMode.Follow then
return Enum.ComputerCameraMovementMode.Follow
end
if enumValue == Enum.TouchCameraMovementMode.Orbital or
enumValue == Enum.DevTouchCameraMovementMode.Orbital or
enumValue == Enum.DevComputerCameraMovementMode.Orbital or
enumValue == Enum.ComputerCameraMovementMode.Orbital then
return Enum.ComputerCameraMovementMode.Orbital
end
if FFlagUserCameraToggle then
if enumValue == Enum.ComputerCameraMovementMode.CameraToggle or
enumValue == Enum.DevComputerCameraMovementMode.CameraToggle then
return Enum.ComputerCameraMovementMode.CameraToggle
end
end
-- Note: Only the Dev versions of the Enums have UserChoice as an option
if enumValue == Enum.DevTouchCameraMovementMode.UserChoice or
enumValue == Enum.DevComputerCameraMovementMode.UserChoice then
return Enum.DevComputerCameraMovementMode.UserChoice
end
-- For any unmapped options return Classic camera
return Enum.ComputerCameraMovementMode.Classic
end
return CameraUtils
end
function _CameraModule()
local CameraModule = {}
CameraModule.__index = CameraModule
local FFlagUserCameraToggle do
local success, result = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserCameraToggle")
end)
FFlagUserCameraToggle = success and result
end
local FFlagUserRemoveTheCameraApi do
local success, result = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserRemoveTheCameraApi")
end)
FFlagUserRemoveTheCameraApi = success and result
end
-- NOTICE: Player property names do not all match their StarterPlayer equivalents,
-- with the differences noted in the comments on the right
local PLAYER_CAMERA_PROPERTIES =
{
"CameraMinZoomDistance",
"CameraMaxZoomDistance",
"CameraMode",
"DevCameraOcclusionMode",
"DevComputerCameraMode", -- Corresponds to StarterPlayer.DevComputerCameraMovementMode
"DevTouchCameraMode", -- Corresponds to StarterPlayer.DevTouchCameraMovementMode
-- Character movement mode
"DevComputerMovementMode",
"DevTouchMovementMode",
"DevEnableMouseLock", -- Corresponds to StarterPlayer.EnableMouseLockOption
}
local USER_GAME_SETTINGS_PROPERTIES =
{
"ComputerCameraMovementMode",
"ComputerMovementMode",
"ControlMode",
"GamepadCameraSensitivity",
"MouseSensitivity",
"RotationType",
"TouchCameraMovementMode",
"TouchMovementMode",
}
--[[ Roblox Services ]]--
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
-- Camera math utility library
local CameraUtils = _CameraUtils()
-- Load Roblox Camera Controller Modules
local ClassicCamera = _ClassicCamera()
local OrbitalCamera = _OrbitalCamera()
local LegacyCamera = _LegacyCamera()
-- Load Roblox Occlusion Modules
local Invisicam = _Invisicam()
local Poppercam = _Poppercam()
-- Load the near-field character transparency controller and the mouse lock "shift lock" controller
local TransparencyController = _TransparencyController()
local MouseLockController = _MouseLockController()
-- Table of camera controllers that have been instantiated. They are instantiated as they are used.
local instantiatedCameraControllers = {}
local instantiatedOcclusionModules = {}
-- Management of which options appear on the Roblox User Settings screen
do
local PlayerScripts = Players.LocalPlayer:WaitForChild("PlayerScripts")
PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default)
PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Follow)
PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Classic)
PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Default)
PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Follow)
PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.Classic)
if FFlagUserCameraToggle then
PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.CameraToggle)
end
end
CameraModule.FFlagUserCameraToggle = FFlagUserCameraToggle
function CameraModule.new()
local self = setmetatable({},CameraModule)
-- Current active controller instances
self.activeCameraController = nil
self.activeOcclusionModule = nil
self.activeTransparencyController = nil
self.activeMouseLockController = nil
self.currentComputerCameraMovementMode = nil
-- Connections to events
self.cameraSubjectChangedConn = nil
self.cameraTypeChangedConn = nil
-- Adds CharacterAdded and CharacterRemoving event handlers for all current players
for _,player in pairs(Players:GetPlayers()) do
self:OnPlayerAdded(player)
end
-- Adds CharacterAdded and CharacterRemoving event handlers for all players who join in the future
Players.PlayerAdded:Connect(function(player)
self:OnPlayerAdded(player)
end)
self.activeTransparencyController = TransparencyController.new()
self.activeTransparencyController:Enable(true)
if not UserInputService.TouchEnabled then
self.activeMouseLockController = MouseLockController.new()
local toggleEvent = self.activeMouseLockController:GetBindableToggleEvent()
if toggleEvent then
toggleEvent:Connect(function()
self:OnMouseLockToggled()
end)
end
end
self:ActivateCameraController(self:GetCameraControlChoice())
self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
self:OnCurrentCameraChanged() -- Does initializations and makes first camera controller
RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, function(dt) self:Update(dt) end)
-- Connect listeners to camera-related properties
for _, propertyName in pairs(PLAYER_CAMERA_PROPERTIES) do
Players.LocalPlayer:GetPropertyChangedSignal(propertyName):Connect(function()
self:OnLocalPlayerCameraPropertyChanged(propertyName)
end)
end
for _, propertyName in pairs(USER_GAME_SETTINGS_PROPERTIES) do
UserGameSettings:GetPropertyChangedSignal(propertyName):Connect(function()
self:OnUserGameSettingsPropertyChanged(propertyName)
end)
end
game.Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function()
self:OnCurrentCameraChanged()
end)
self.lastInputType = UserInputService:GetLastInputType()
UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
self.lastInputType = newLastInputType
end)
return self
end
function CameraModule:GetCameraMovementModeFromSettings()
local cameraMode = Players.LocalPlayer.CameraMode
-- Lock First Person trumps all other settings and forces ClassicCamera
if cameraMode == Enum.CameraMode.LockFirstPerson then
return CameraUtils.ConvertCameraModeEnumToStandard(Enum.ComputerCameraMovementMode.Classic)
end
local devMode, userMode
if UserInputService.TouchEnabled then
devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevTouchCameraMode)
userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.TouchCameraMovementMode)
else
devMode = CameraUtils.ConvertCameraModeEnumToStandard(Players.LocalPlayer.DevComputerCameraMode)
userMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
end
if devMode == Enum.DevComputerCameraMovementMode.UserChoice then
-- Developer is allowing user choice, so user setting is respected
return userMode
end
return devMode
end
function CameraModule:ActivateOcclusionModule( occlusionMode )
local newModuleCreator
if occlusionMode == Enum.DevCameraOcclusionMode.Zoom then
newModuleCreator = Poppercam
elseif occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
newModuleCreator = Invisicam
else
warn("CameraScript ActivateOcclusionModule called with unsupported mode")
return
end
-- First check to see if there is actually a change. If the module being requested is already
-- the currently-active solution then just make sure it's enabled and exit early
if self.activeOcclusionModule and self.activeOcclusionModule:GetOcclusionMode() == occlusionMode then
if not self.activeOcclusionModule:GetEnabled() then
self.activeOcclusionModule:Enable(true)
end
return
end
-- Save a reference to the current active module (may be nil) so that we can disable it if
-- we are successful in activating its replacement
local prevOcclusionModule = self.activeOcclusionModule
-- If there is no active module, see if the one we need has already been instantiated
self.activeOcclusionModule = instantiatedOcclusionModules[newModuleCreator]
-- If the module was not already instantiated and selected above, instantiate it
if not self.activeOcclusionModule then
self.activeOcclusionModule = newModuleCreator.new()
if self.activeOcclusionModule then
instantiatedOcclusionModules[newModuleCreator] = self.activeOcclusionModule
end
end
-- If we were successful in either selecting or instantiating the module,
-- enable it if it's not already the currently-active enabled module
if self.activeOcclusionModule then
local newModuleOcclusionMode = self.activeOcclusionModule:GetOcclusionMode()
-- Sanity check that the module we selected or instantiated actually supports the desired occlusionMode
if newModuleOcclusionMode ~= occlusionMode then
warn("CameraScript ActivateOcclusionModule mismatch: ",self.activeOcclusionModule:GetOcclusionMode(),"~=",occlusionMode)
end
-- Deactivate current module if there is one
if prevOcclusionModule then
-- Sanity check that current module is not being replaced by itself (that should have been handled above)
if prevOcclusionModule ~= self.activeOcclusionModule then
prevOcclusionModule:Enable(false)
else
warn("CameraScript ActivateOcclusionModule failure to detect already running correct module")
end
end
-- Occlusion modules need to be initialized with information about characters and cameraSubject
-- Invisicam needs the LocalPlayer's character
-- Poppercam needs all player characters and the camera subject
if occlusionMode == Enum.DevCameraOcclusionMode.Invisicam then
-- Optimization to only send Invisicam what we know it needs
if Players.LocalPlayer.Character then
self.activeOcclusionModule:CharacterAdded(Players.LocalPlayer.Character, Players.LocalPlayer )
end
else
-- When Poppercam is enabled, we send it all existing player characters for its raycast ignore list
for _, player in pairs(Players:GetPlayers()) do
if player and player.Character then
self.activeOcclusionModule:CharacterAdded(player.Character, player)
end
end
self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
end
-- Activate new choice
self.activeOcclusionModule:Enable(true)
end
end
-- When supplied, legacyCameraType is used and cameraMovementMode is ignored (should be nil anyways)
-- Next, if userCameraCreator is passed in, that is used as the cameraCreator
function CameraModule:ActivateCameraController(cameraMovementMode, legacyCameraType)
local newCameraCreator = nil
if legacyCameraType~=nil then
--[[
This function has been passed a CameraType enum value. Some of these map to the use of
the LegacyCamera module, the value "Custom" will be translated to a movementMode enum
value based on Dev and User settings, and "Scriptable" will disable the camera controller.
--]]
if legacyCameraType == Enum.CameraType.Scriptable then
if self.activeCameraController then
self.activeCameraController:Enable(false)
self.activeCameraController = nil
return
end
elseif legacyCameraType == Enum.CameraType.Custom then
cameraMovementMode = self:GetCameraMovementModeFromSettings()
elseif legacyCameraType == Enum.CameraType.Track then
-- Note: The TrackCamera module was basically an older, less fully-featured
-- version of ClassicCamera, no longer actively maintained, but it is re-implemented in
-- case a game was dependent on its lack of ClassicCamera's extra functionality.
cameraMovementMode = Enum.ComputerCameraMovementMode.Classic
elseif legacyCameraType == Enum.CameraType.Follow then
cameraMovementMode = Enum.ComputerCameraMovementMode.Follow
elseif legacyCameraType == Enum.CameraType.Orbital then
cameraMovementMode = Enum.ComputerCameraMovementMode.Orbital
elseif legacyCameraType == Enum.CameraType.Attach or
legacyCameraType == Enum.CameraType.Watch or
legacyCameraType == Enum.CameraType.Fixed then
newCameraCreator = LegacyCamera
else
warn("CameraScript encountered an unhandled Camera.CameraType value: ",legacyCameraType)
end
end
if not newCameraCreator then
if cameraMovementMode == Enum.ComputerCameraMovementMode.Classic or
cameraMovementMode == Enum.ComputerCameraMovementMode.Follow or
cameraMovementMode == Enum.ComputerCameraMovementMode.Default or
(FFlagUserCameraToggle and cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle) then
newCameraCreator = ClassicCamera
elseif cameraMovementMode == Enum.ComputerCameraMovementMode.Orbital then
newCameraCreator = OrbitalCamera
else
warn("ActivateCameraController did not select a module.")
return
end
end
-- Create the camera control module we need if it does not already exist in instantiatedCameraControllers
local newCameraController
if not instantiatedCameraControllers[newCameraCreator] then
newCameraController = newCameraCreator.new()
instantiatedCameraControllers[newCameraCreator] = newCameraController
else
newCameraController = instantiatedCameraControllers[newCameraCreator]
end
-- If there is a controller active and it's not the one we need, disable it,
-- if it is the one we need, make sure it's enabled
if self.activeCameraController then
if self.activeCameraController ~= newCameraController then
self.activeCameraController:Enable(false)
self.activeCameraController = newCameraController
self.activeCameraController:Enable(true)
elseif not self.activeCameraController:GetEnabled() then
self.activeCameraController:Enable(true)
end
elseif newCameraController ~= nil then
self.activeCameraController = newCameraController
self.activeCameraController:Enable(true)
end
if self.activeCameraController then
if cameraMovementMode~=nil then
self.activeCameraController:SetCameraMovementMode(cameraMovementMode)
elseif legacyCameraType~=nil then
-- Note that this is only called when legacyCameraType is not a type that
-- was convertible to a ComputerCameraMovementMode value, i.e. really only applies to LegacyCamera
self.activeCameraController:SetCameraType(legacyCameraType)
end
end
end
-- Note: The active transparency controller could be made to listen for this event itself.
function CameraModule:OnCameraSubjectChanged()
if self.activeTransparencyController then
self.activeTransparencyController:SetSubject(game.Workspace.CurrentCamera.CameraSubject)
end
if self.activeOcclusionModule then
self.activeOcclusionModule:OnCameraSubjectChanged(game.Workspace.CurrentCamera.CameraSubject)
end
end
function CameraModule:OnCameraTypeChanged(newCameraType)
if newCameraType == Enum.CameraType.Scriptable then
if UserInputService.MouseBehavior == Enum.MouseBehavior.LockCenter then
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
end
end
-- Forward the change to ActivateCameraController to handle
self:ActivateCameraController(nil, newCameraType)
end
-- Note: Called whenever workspace.CurrentCamera changes, but also on initialization of this script
function CameraModule:OnCurrentCameraChanged()
local currentCamera = game.Workspace.CurrentCamera
if not currentCamera then return end
if self.cameraSubjectChangedConn then
self.cameraSubjectChangedConn:Disconnect()
end
if self.cameraTypeChangedConn then
self.cameraTypeChangedConn:Disconnect()
end
self.cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):Connect(function()
self:OnCameraSubjectChanged(currentCamera.CameraSubject)
end)
self.cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):Connect(function()
self:OnCameraTypeChanged(currentCamera.CameraType)
end)
self:OnCameraSubjectChanged(currentCamera.CameraSubject)
self:OnCameraTypeChanged(currentCamera.CameraType)
end
function CameraModule:OnLocalPlayerCameraPropertyChanged(propertyName)
if propertyName == "CameraMode" then
-- CameraMode is only used to turn on/off forcing the player into first person view. The
-- Note: The case "Classic" is used for all other views and does not correspond only to the ClassicCamera module
if Players.LocalPlayer.CameraMode == Enum.CameraMode.LockFirstPerson then
-- Locked in first person, use ClassicCamera which supports this
if not self.activeCameraController or self.activeCameraController:GetModuleName() ~= "ClassicCamera" then
self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(Enum.DevComputerCameraMovementMode.Classic))
end
if self.activeCameraController then
self.activeCameraController:UpdateForDistancePropertyChange()
end
elseif Players.LocalPlayer.CameraMode == Enum.CameraMode.Classic then
-- Not locked in first person view
local cameraMovementMode =self: GetCameraMovementModeFromSettings()
self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
else
warn("Unhandled value for property player.CameraMode: ",Players.LocalPlayer.CameraMode)
end
elseif propertyName == "DevComputerCameraMode" or
propertyName == "DevTouchCameraMode" then
local cameraMovementMode = self:GetCameraMovementModeFromSettings()
self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
elseif propertyName == "DevCameraOcclusionMode" then
self:ActivateOcclusionModule(Players.LocalPlayer.DevCameraOcclusionMode)
elseif propertyName == "CameraMinZoomDistance" or propertyName == "CameraMaxZoomDistance" then
if self.activeCameraController then
self.activeCameraController:UpdateForDistancePropertyChange()
end
elseif propertyName == "DevTouchMovementMode" then
elseif propertyName == "DevComputerMovementMode" then
elseif propertyName == "DevEnableMouseLock" then
-- This is the enabling/disabling of "Shift Lock" mode, not LockFirstPerson (which is a CameraMode)
-- Note: Enabling and disabling of MouseLock mode is normally only a publish-time choice made via
-- the corresponding EnableMouseLockOption checkbox of StarterPlayer, and this script does not have
-- support for changing the availability of MouseLock at runtime (this would require listening to
-- Player.DevEnableMouseLock changes)
end
end
function CameraModule:OnUserGameSettingsPropertyChanged(propertyName)
if propertyName == "ComputerCameraMovementMode" then
local cameraMovementMode = self:GetCameraMovementModeFromSettings()
self:ActivateCameraController(CameraUtils.ConvertCameraModeEnumToStandard(cameraMovementMode))
end
end
--[[
Main RenderStep Update. The camera controller and occlusion module both have opportunities
to set and modify (respectively) the CFrame and Focus before it is set once on CurrentCamera.
The camera and occlusion modules should only return CFrames, not set the CFrame property of
CurrentCamera directly.
--]]
function CameraModule:Update(dt)
if self.activeCameraController then
if FFlagUserCameraToggle then
self.activeCameraController:UpdateMouseBehavior()
end
local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
self.activeCameraController:ApplyVRTransform()
if self.activeOcclusionModule then
newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus)
end
-- Here is where the new CFrame and Focus are set for this render frame
game.Workspace.CurrentCamera.CFrame = newCameraCFrame
game.Workspace.CurrentCamera.Focus = newCameraFocus
-- Update to character local transparency as needed based on camera-to-subject distance
if self.activeTransparencyController then
self.activeTransparencyController:Update()
end
end
end
-- Formerly getCurrentCameraMode, this function resolves developer and user camera control settings to
-- decide which camera control module should be instantiated. The old method of converting redundant enum types
function CameraModule:GetCameraControlChoice()
local player = Players.LocalPlayer
if player then
if self.lastInputType == Enum.UserInputType.Touch or UserInputService.TouchEnabled then
-- Touch
if player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then
return CameraUtils.ConvertCameraModeEnumToStandard( UserGameSettings.TouchCameraMovementMode )
else
return CameraUtils.ConvertCameraModeEnumToStandard( player.DevTouchCameraMode )
end
else
-- Computer
if player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then
local computerMovementMode = CameraUtils.ConvertCameraModeEnumToStandard(UserGameSettings.ComputerCameraMovementMode)
return CameraUtils.ConvertCameraModeEnumToStandard(computerMovementMode)
else
return CameraUtils.ConvertCameraModeEnumToStandard(player.DevComputerCameraMode)
end
end
end
end
function CameraModule:OnCharacterAdded(char, player)
if self.activeOcclusionModule then
self.activeOcclusionModule:CharacterAdded(char, player)
end
end
function CameraModule:OnCharacterRemoving(char, player)
if self.activeOcclusionModule then
self.activeOcclusionModule:CharacterRemoving(char, player)
end
end
function CameraModule:OnPlayerAdded(player)
player.CharacterAdded:Connect(function(char)
self:OnCharacterAdded(char, player)
end)
player.CharacterRemoving:Connect(function(char)
self:OnCharacterRemoving(char, player)
end)
end
function CameraModule:OnMouseLockToggled()
if self.activeMouseLockController then
local mouseLocked = self.activeMouseLockController:GetIsMouseLocked()
local mouseLockOffset = self.activeMouseLockController:GetMouseLockOffset()
if self.activeCameraController then
self.activeCameraController:SetIsMouseLocked(mouseLocked)
self.activeCameraController:SetMouseLockOffset(mouseLockOffset)
end
end
end
--begin edit
local Camera = CameraModule
local IDENTITYCF = CFrame.new()
local lastUpCFrame = IDENTITYCF
Camera.UpVector = Vector3.new(0, 1, 0)
Camera.TransitionRate = 0.15
Camera.UpCFrame = IDENTITYCF
function Camera:GetUpVector(oldUpVector)
return oldUpVector
end
local function getRotationBetween(u, v, axis)
local dot, uxv = u:Dot(v), u:Cross(v)
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end
function Camera:CalculateUpCFrame()
local oldUpVector = self.UpVector
local newUpVector = self:GetUpVector(oldUpVector)
local backup = game.Workspace.CurrentCamera.CFrame.RightVector
local transitionCF = getRotationBetween(oldUpVector, newUpVector, backup)
local vecSlerpCF = IDENTITYCF:Lerp(transitionCF, self.TransitionRate)
self.UpVector = vecSlerpCF * oldUpVector
self.UpCFrame = vecSlerpCF * self.UpCFrame
lastUpCFrame = self.UpCFrame
end
function Camera:Update(dt)
if self.activeCameraController then
if Camera.FFlagUserCameraToggle then
self.activeCameraController:UpdateMouseBehavior()
end
local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
self.activeCameraController:ApplyVRTransform()
self:CalculateUpCFrame()
self.activeCameraController:UpdateUpCFrame(self.UpCFrame)
-- undo shift-lock offset
local lockOffset = Vector3.new(0, 0, 0)
if (self.activeMouseLockController and self.activeMouseLockController:GetIsMouseLocked()) then
lockOffset = self.activeMouseLockController:GetMouseLockOffset()
end
local offset = newCameraFocus:ToObjectSpace(newCameraCFrame)
local camRotation = self.UpCFrame * offset
newCameraFocus = newCameraFocus - newCameraCFrame:VectorToWorldSpace(lockOffset) + camRotation:VectorToWorldSpace(lockOffset)
newCameraCFrame = newCameraFocus * camRotation
--local offset = newCameraFocus:Inverse() * newCameraCFrame
--newCameraCFrame = newCameraFocus * self.UpCFrame * offset
if (self.activeCameraController.lastCameraTransform) then
self.activeCameraController.lastCameraTransform = newCameraCFrame
self.activeCameraController.lastCameraFocus = newCameraFocus
end
if self.activeOcclusionModule then
newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus)
end
game.Workspace.CurrentCamera.CFrame = newCameraCFrame
game.Workspace.CurrentCamera.Focus = newCameraFocus
if self.activeTransparencyController then
self.activeTransparencyController:Update()
end
end
end
function Camera:IsFirstPerson()
if self.activeCameraController then
return self.activeCameraController:InFirstPerson()
end
return false
end
function Camera:IsMouseLocked()
if self.activeCameraController then
return self.activeCameraController:GetIsMouseLocked()
end
return false
end
function Camera:IsToggleMode()
if self.activeCameraController then
return self.activeCameraController.isCameraToggle
end
return false
end
function Camera:IsCamRelative()
return self:IsMouseLocked() or self:IsFirstPerson()
--return self:IsToggleMode(), self:IsMouseLocked(), self:IsFirstPerson()
end
--
local Utils = _CameraUtils()
function Utils.GetAngleBetweenXZVectors(v1, v2)
local upCFrame = lastUpCFrame
v1 = upCFrame:VectorToObjectSpace(v1)
v2 = upCFrame:VectorToObjectSpace(v2)
return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z)
end
--end edit
local cameraModuleObject = CameraModule.new()
local cameraApi = {}
return cameraModuleObject
end
function _ClickToMoveDisplay()
local ClickToMoveDisplay = {}
local FAILURE_ANIMATION_ID = "rbxassetid://2874840706"
local TrailDotIcon = "rbxasset://textures/ui/traildot.png"
local EndWaypointIcon = "rbxasset://textures/ui/waypoint.png"
local WaypointsAlwaysOnTop = false
local WAYPOINT_INCLUDE_FACTOR = 2
local LAST_DOT_DISTANCE = 3
local WAYPOINT_BILLBOARD_SIZE = UDim2.new(0, 1.68 * 25, 0, 2 * 25)
local ENDWAYPOINT_SIZE_OFFSET_MIN = Vector2.new(0, 0.5)
local ENDWAYPOINT_SIZE_OFFSET_MAX = Vector2.new(0, 1)
local FAIL_WAYPOINT_SIZE_OFFSET_CENTER = Vector2.new(0, 0.5)
local FAIL_WAYPOINT_SIZE_OFFSET_LEFT = Vector2.new(0.1, 0.5)
local FAIL_WAYPOINT_SIZE_OFFSET_RIGHT = Vector2.new(-0.1, 0.5)
local FAILURE_TWEEN_LENGTH = 0.125
local FAILURE_TWEEN_COUNT = 4
local TWEEN_WAYPOINT_THRESHOLD = 5
local TRAIL_DOT_PARENT_NAME = "ClickToMoveDisplay"
local TrailDotSize = Vector2.new(1.5, 1.5)
local TRAIL_DOT_MIN_SCALE = 1
local TRAIL_DOT_MIN_DISTANCE = 10
local TRAIL_DOT_MAX_SCALE = 2.5
local TRAIL_DOT_MAX_DISTANCE = 100
local PlayersService = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local LocalPlayer = PlayersService.LocalPlayer
local function CreateWaypointTemplates()
local TrailDotTemplate = Instance.new("Part")
TrailDotTemplate.Size = Vector3.new(1, 1, 1)
TrailDotTemplate.Anchored = true
TrailDotTemplate.CanCollide = false
TrailDotTemplate.Name = "TrailDot"
TrailDotTemplate.Transparency = 1
local TrailDotImage = Instance.new("ImageHandleAdornment")
TrailDotImage.Name = "TrailDotImage"
TrailDotImage.Size = TrailDotSize
TrailDotImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
TrailDotImage.AlwaysOnTop = WaypointsAlwaysOnTop
TrailDotImage.Image = TrailDotIcon
TrailDotImage.Adornee = TrailDotTemplate
TrailDotImage.Parent = TrailDotTemplate
local EndWaypointTemplate = Instance.new("Part")
EndWaypointTemplate.Size = Vector3.new(2, 2, 2)
EndWaypointTemplate.Anchored = true
EndWaypointTemplate.CanCollide = false
EndWaypointTemplate.Name = "EndWaypoint"
EndWaypointTemplate.Transparency = 1
local EndWaypointImage = Instance.new("ImageHandleAdornment")
EndWaypointImage.Name = "TrailDotImage"
EndWaypointImage.Size = TrailDotSize
EndWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
EndWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
EndWaypointImage.Image = TrailDotIcon
EndWaypointImage.Adornee = EndWaypointTemplate
EndWaypointImage.Parent = EndWaypointTemplate
local EndWaypointBillboard = Instance.new("BillboardGui")
EndWaypointBillboard.Name = "EndWaypointBillboard"
EndWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
EndWaypointBillboard.LightInfluence = 0
EndWaypointBillboard.SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MIN
EndWaypointBillboard.AlwaysOnTop = true
EndWaypointBillboard.Adornee = EndWaypointTemplate
EndWaypointBillboard.Parent = EndWaypointTemplate
local EndWaypointImageLabel = Instance.new("ImageLabel")
EndWaypointImageLabel.Image = EndWaypointIcon
EndWaypointImageLabel.BackgroundTransparency = 1
EndWaypointImageLabel.Size = UDim2.new(1, 0, 1, 0)
EndWaypointImageLabel.Parent = EndWaypointBillboard
local FailureWaypointTemplate = Instance.new("Part")
FailureWaypointTemplate.Size = Vector3.new(2, 2, 2)
FailureWaypointTemplate.Anchored = true
FailureWaypointTemplate.CanCollide = false
FailureWaypointTemplate.Name = "FailureWaypoint"
FailureWaypointTemplate.Transparency = 1
local FailureWaypointImage = Instance.new("ImageHandleAdornment")
FailureWaypointImage.Name = "TrailDotImage"
FailureWaypointImage.Size = TrailDotSize
FailureWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1)
FailureWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop
FailureWaypointImage.Image = TrailDotIcon
FailureWaypointImage.Adornee = FailureWaypointTemplate
FailureWaypointImage.Parent = FailureWaypointTemplate
local FailureWaypointBillboard = Instance.new("BillboardGui")
FailureWaypointBillboard.Name = "FailureWaypointBillboard"
FailureWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE
FailureWaypointBillboard.LightInfluence = 0
FailureWaypointBillboard.SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER
FailureWaypointBillboard.AlwaysOnTop = true
FailureWaypointBillboard.Adornee = FailureWaypointTemplate
FailureWaypointBillboard.Parent = FailureWaypointTemplate
local FailureWaypointFrame = Instance.new("Frame")
FailureWaypointFrame.BackgroundTransparency = 1
FailureWaypointFrame.Size = UDim2.new(0, 0, 0, 0)
FailureWaypointFrame.Position = UDim2.new(0.5, 0, 1, 0)
FailureWaypointFrame.Parent = FailureWaypointBillboard
local FailureWaypointImageLabel = Instance.new("ImageLabel")
FailureWaypointImageLabel.Image = EndWaypointIcon
FailureWaypointImageLabel.BackgroundTransparency = 1
FailureWaypointImageLabel.Position = UDim2.new(
0, -WAYPOINT_BILLBOARD_SIZE.X.Offset/2, 0, -WAYPOINT_BILLBOARD_SIZE.Y.Offset
)
FailureWaypointImageLabel.Size = WAYPOINT_BILLBOARD_SIZE
FailureWaypointImageLabel.Parent = FailureWaypointFrame
return TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate
end
local TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
local function getTrailDotParent()
local camera = Workspace.CurrentCamera
local trailParent = camera:FindFirstChild(TRAIL_DOT_PARENT_NAME)
if not trailParent then
trailParent = Instance.new("Model")
trailParent.Name = TRAIL_DOT_PARENT_NAME
trailParent.Parent = camera
end
return trailParent
end
local function placePathWaypoint(waypointModel, position)
local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
ray,
{ Workspace.CurrentCamera, LocalPlayer.Character }
)
if hitPart then
waypointModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
waypointModel.Parent = getTrailDotParent()
end
end
local TrailDot = {}
TrailDot.__index = TrailDot
function TrailDot:Destroy()
self.DisplayModel:Destroy()
end
function TrailDot:NewDisplayModel(position)
local newDisplayModel = TrailDotTemplate:Clone()
placePathWaypoint(newDisplayModel, position)
return newDisplayModel
end
function TrailDot.new(position, closestWaypoint)
local self = setmetatable({}, TrailDot)
self.DisplayModel = self:NewDisplayModel(position)
self.ClosestWayPoint = closestWaypoint
return self
end
local EndWaypoint = {}
EndWaypoint.__index = EndWaypoint
function EndWaypoint:Destroy()
self.Destroyed = true
self.Tween:Cancel()
self.DisplayModel:Destroy()
end
function EndWaypoint:NewDisplayModel(position)
local newDisplayModel = EndWaypointTemplate:Clone()
placePathWaypoint(newDisplayModel, position)
return newDisplayModel
end
function EndWaypoint:CreateTween()
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, -1, true)
local tween = TweenService:Create(
self.DisplayModel.EndWaypointBillboard,
tweenInfo,
{ SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MAX }
)
tween:Play()
return tween
end
function EndWaypoint:TweenInFrom(originalPosition)
local currentPositon = self.DisplayModel.Position
local studsOffset = originalPosition - currentPositon
self.DisplayModel.EndWaypointBillboard.StudsOffset = Vector3.new(0, studsOffset.Y, 0)
local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
local tween = TweenService:Create(
self.DisplayModel.EndWaypointBillboard,
tweenInfo,
{ StudsOffset = Vector3.new(0, 0, 0) }
)
tween:Play()
return tween
end
function EndWaypoint.new(position, closestWaypoint, originalPosition)
local self = setmetatable({}, EndWaypoint)
self.DisplayModel = self:NewDisplayModel(position)
self.Destroyed = false
if originalPosition and (originalPosition - position).magnitude > TWEEN_WAYPOINT_THRESHOLD then
self.Tween = self:TweenInFrom(originalPosition)
coroutine.wrap(function()
self.Tween.Completed:Wait()
if not self.Destroyed then
self.Tween = self:CreateTween()
end
end)()
else
self.Tween = self:CreateTween()
end
self.ClosestWayPoint = closestWaypoint
return self
end
local FailureWaypoint = {}
FailureWaypoint.__index = FailureWaypoint
function FailureWaypoint:Hide()
self.DisplayModel.Parent = nil
end
function FailureWaypoint:Destroy()
self.DisplayModel:Destroy()
end
function FailureWaypoint:NewDisplayModel(position)
local newDisplayModel = FailureWaypointTemplate:Clone()
placePathWaypoint(newDisplayModel, position)
local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0))
local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList(
ray, { Workspace.CurrentCamera, LocalPlayer.Character }
)
if hitPart then
newDisplayModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
newDisplayModel.Parent = getTrailDotParent()
end
return newDisplayModel
end
function FailureWaypoint:RunFailureTween()
wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore starting tweening
-- Tween out from center
local tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
local tweenLeft = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_LEFT })
tweenLeft:Play()
local tweenLeftRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
{ Rotation = 10 })
tweenLeftRoation:Play()
tweenLeft.Completed:wait()
-- Tween back and forth
tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
FAILURE_TWEEN_COUNT - 1, true)
local tweenSideToSide = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_RIGHT})
tweenSideToSide:Play()
-- Tween flash dark and roate left and right
tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out,
FAILURE_TWEEN_COUNT - 1, true)
local tweenFlash = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame.ImageLabel, tweenInfo,
{ ImageColor3 = Color3.new(0.75, 0.75, 0.75)})
tweenFlash:Play()
local tweenRotate = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
{ Rotation = -10 })
tweenRotate:Play()
tweenSideToSide.Completed:wait()
-- Tween back to center
tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
local tweenCenter = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo,
{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER })
tweenCenter:Play()
local tweenRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo,
{ Rotation = 0 })
tweenRoation:Play()
tweenCenter.Completed:wait()
wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore removing
end
function FailureWaypoint.new(position)
local self = setmetatable({}, FailureWaypoint)
self.DisplayModel = self:NewDisplayModel(position)
return self
end
local failureAnimation = Instance.new("Animation")
failureAnimation.AnimationId = FAILURE_ANIMATION_ID
local lastHumanoid = nil
local lastFailureAnimationTrack = nil
local function getFailureAnimationTrack(myHumanoid)
if myHumanoid == lastHumanoid then
return lastFailureAnimationTrack
end
lastFailureAnimationTrack = myHumanoid:LoadAnimation(failureAnimation)
lastFailureAnimationTrack.Priority = Enum.AnimationPriority.Action
lastFailureAnimationTrack.Looped = false
return lastFailureAnimationTrack
end
local function findPlayerHumanoid()
local character = LocalPlayer.Character
if character then
return character:FindFirstChildOfClass("Humanoid")
end
end
local function createTrailDots(wayPoints, originalEndWaypoint)
local newTrailDots = {}
local count = 1
for i = 1, #wayPoints - 1 do
local closeToEnd = (wayPoints[i].Position - wayPoints[#wayPoints].Position).magnitude < LAST_DOT_DISTANCE
local includeWaypoint = i % WAYPOINT_INCLUDE_FACTOR == 0 and not closeToEnd
if includeWaypoint then
local trailDot = TrailDot.new(wayPoints[i].Position, i)
newTrailDots[count] = trailDot
count = count + 1
end
end
local newEndWaypoint = EndWaypoint.new(wayPoints[#wayPoints].Position, #wayPoints, originalEndWaypoint)
table.insert(newTrailDots, newEndWaypoint)
local reversedTrailDots = {}
count = 1
for i = #newTrailDots, 1, -1 do
reversedTrailDots[count] = newTrailDots[i]
count = count + 1
end
return reversedTrailDots
end
local function getTrailDotScale(distanceToCamera, defaultSize)
local rangeLength = TRAIL_DOT_MAX_DISTANCE - TRAIL_DOT_MIN_DISTANCE
local inRangePoint = math.clamp(distanceToCamera - TRAIL_DOT_MIN_DISTANCE, 0, rangeLength)/rangeLength
local scale = TRAIL_DOT_MIN_SCALE + (TRAIL_DOT_MAX_SCALE - TRAIL_DOT_MIN_SCALE)*inRangePoint
return defaultSize * scale
end
local createPathCount = 0
-- originalEndWaypoint is optional, causes the waypoint to tween from that position.
function ClickToMoveDisplay.CreatePathDisplay(wayPoints, originalEndWaypoint)
createPathCount = createPathCount + 1
local trailDots = createTrailDots(wayPoints, originalEndWaypoint)
local function removePathBeforePoint(wayPointNumber)
-- kill all trailDots before and at wayPointNumber
for i = #trailDots, 1, -1 do
local trailDot = trailDots[i]
if trailDot.ClosestWayPoint <= wayPointNumber then
trailDot:Destroy()
trailDots[i] = nil
else
break
end
end
end
local reiszeTrailDotsUpdateName = "ClickToMoveResizeTrail" ..createPathCount
local function resizeTrailDots()
if #trailDots == 0 then
RunService:UnbindFromRenderStep(reiszeTrailDotsUpdateName)
return
end
local cameraPos = Workspace.CurrentCamera.CFrame.p
for i = 1, #trailDots do
local trailDotImage = trailDots[i].DisplayModel:FindFirstChild("TrailDotImage")
if trailDotImage then
local distanceToCamera = (trailDots[i].DisplayModel.Position - cameraPos).magnitude
trailDotImage.Size = getTrailDotScale(distanceToCamera, TrailDotSize)
end
end
end
RunService:BindToRenderStep(reiszeTrailDotsUpdateName, Enum.RenderPriority.Camera.Value - 1, resizeTrailDots)
local function removePath()
removePathBeforePoint(#wayPoints)
end
return removePath, removePathBeforePoint
end
local lastFailureWaypoint = nil
function ClickToMoveDisplay.DisplayFailureWaypoint(position)
if lastFailureWaypoint then
lastFailureWaypoint:Hide()
end
local failureWaypoint = FailureWaypoint.new(position)
lastFailureWaypoint = failureWaypoint
coroutine.wrap(function()
failureWaypoint:RunFailureTween()
failureWaypoint:Destroy()
failureWaypoint = nil
end)()
end
function ClickToMoveDisplay.CreateEndWaypoint(position)
return EndWaypoint.new(position)
end
function ClickToMoveDisplay.PlayFailureAnimation()
local myHumanoid = findPlayerHumanoid()
if myHumanoid then
local animationTrack = getFailureAnimationTrack(myHumanoid)
animationTrack:Play()
end
end
function ClickToMoveDisplay.CancelFailureAnimation()
if lastFailureAnimationTrack ~= nil and lastFailureAnimationTrack.IsPlaying then
lastFailureAnimationTrack:Stop()
end
end
function ClickToMoveDisplay.SetWaypointTexture(texture)
TrailDotIcon = texture
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end
function ClickToMoveDisplay.GetWaypointTexture()
return TrailDotIcon
end
function ClickToMoveDisplay.SetWaypointRadius(radius)
TrailDotSize = Vector2.new(radius, radius)
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end
function ClickToMoveDisplay.GetWaypointRadius()
return TrailDotSize.X
end
function ClickToMoveDisplay.SetEndWaypointTexture(texture)
EndWaypointIcon = texture
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end
function ClickToMoveDisplay.GetEndWaypointTexture()
return EndWaypointIcon
end
function ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop)
WaypointsAlwaysOnTop = alwaysOnTop
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates()
end
function ClickToMoveDisplay.GetWaypointsAlwaysOnTop()
return WaypointsAlwaysOnTop
end
return ClickToMoveDisplay
end
function _BaseCharacterController()
local ZERO_VECTOR3 = Vector3.new(0,0,0)
--[[ The Module ]]--
local BaseCharacterController = {}
BaseCharacterController.__index = BaseCharacterController
function BaseCharacterController.new()
local self = setmetatable({}, BaseCharacterController)
self.enabled = false
self.moveVector = ZERO_VECTOR3
self.moveVectorIsCameraRelative = true
self.isJumping = false
return self
end
function BaseCharacterController:OnRenderStepped(dt)
-- By default, nothing to do
end
function BaseCharacterController:GetMoveVector()
return self.moveVector
end
function BaseCharacterController:IsMoveVectorCameraRelative()
return self.moveVectorIsCameraRelative
end
function BaseCharacterController:GetIsJumping()
return self.isJumping
end
-- Override in derived classes to set self.enabled and return boolean indicating
-- whether Enable/Disable was successful. Return true if controller is already in the requested state.
function BaseCharacterController:Enable(enable)
error("BaseCharacterController:Enable must be overridden in derived classes and should not be called.")
return false
end
return BaseCharacterController
end
function _VehicleController()
local ContextActionService = game:GetService("ContextActionService")
--[[ Constants ]]--
-- Set this to true if you want to instead use the triggers for the throttle
local useTriggersForThrottle = true
-- Also set this to true if you want the thumbstick to not affect throttle, only triggers when a gamepad is conected
local onlyTriggersForThrottle = false
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE = 35
-- Note that VehicleController does not derive from BaseCharacterController, it is a special case
local VehicleController = {}
VehicleController.__index = VehicleController
function VehicleController.new(CONTROL_ACTION_PRIORITY)
local self = setmetatable({}, VehicleController)
self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY
self.enabled = false
self.vehicleSeat = nil
self.throttle = 0
self.steer = 0
self.acceleration = 0
self.decceleration = 0
self.turningRight = 0
self.turningLeft = 0
self.vehicleMoveVector = ZERO_VECTOR3
self.autoPilot = {}
self.autoPilot.MaxSpeed = 0
self.autoPilot.MaxSteeringAngle = 0
return self
end
function VehicleController:BindContextActions()
if useTriggersForThrottle then
ContextActionService:BindActionAtPriority("throttleAccel", (function(actionName, inputState, inputObject)
self:OnThrottleAccel(actionName, inputState, inputObject)
return Enum.ContextActionResult.Pass
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonR2)
ContextActionService:BindActionAtPriority("throttleDeccel", (function(actionName, inputState, inputObject)
self:OnThrottleDeccel(actionName, inputState, inputObject)
return Enum.ContextActionResult.Pass
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonL2)
end
ContextActionService:BindActionAtPriority("arrowSteerRight", (function(actionName, inputState, inputObject)
self:OnSteerRight(actionName, inputState, inputObject)
return Enum.ContextActionResult.Pass
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Right)
ContextActionService:BindActionAtPriority("arrowSteerLeft", (function(actionName, inputState, inputObject)
self:OnSteerLeft(actionName, inputState, inputObject)
return Enum.ContextActionResult.Pass
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Left)
end
function VehicleController:Enable(enable, vehicleSeat)
if enable == self.enabled and vehicleSeat == self.vehicleSeat then
return
end
self.enabled = enable
self.vehicleMoveVector = ZERO_VECTOR3
if enable then
if vehicleSeat then
self.vehicleSeat = vehicleSeat
self:SetupAutoPilot()
self:BindContextActions()
end
else
if useTriggersForThrottle then
ContextActionService:UnbindAction("throttleAccel")
ContextActionService:UnbindAction("throttleDeccel")
end
ContextActionService:UnbindAction("arrowSteerRight")
ContextActionService:UnbindAction("arrowSteerLeft")
self.vehicleSeat = nil
end
end
function VehicleController:OnThrottleAccel(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
self.acceleration = 0
else
self.acceleration = -1
end
self.throttle = self.acceleration + self.decceleration
end
function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
self.decceleration = 0
else
self.decceleration = 1
end
self.throttle = self.acceleration + self.decceleration
end
function VehicleController:OnSteerRight(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
self.turningRight = 0
else
self.turningRight = 1
end
self.steer = self.turningRight + self.turningLeft
end
function VehicleController:OnSteerLeft(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
self.turningLeft = 0
else
self.turningLeft = -1
end
self.steer = self.turningRight + self.turningLeft
end
-- Call this from a function bound to Renderstep with Input Priority
function VehicleController:Update(moveVector, cameraRelative, usingGamepad)
if self.vehicleSeat then
if cameraRelative then
-- This is the default steering mode
moveVector = moveVector + Vector3.new(self.steer, 0, self.throttle)
if usingGamepad and onlyTriggersForThrottle and useTriggersForThrottle then
self.vehicleSeat.ThrottleFloat = -self.throttle
else
self.vehicleSeat.ThrottleFloat = -moveVector.Z
end
self.vehicleSeat.SteerFloat = moveVector.X
return moveVector, true
else
-- This is the path following mode
local localMoveVector = self.vehicleSeat.Occupant.RootPart.CFrame:VectorToObjectSpace(moveVector)
self.vehicleSeat.ThrottleFloat = self:ComputeThrottle(localMoveVector)
self.vehicleSeat.SteerFloat = self:ComputeSteer(localMoveVector)
return ZERO_VECTOR3, true
end
end
return moveVector, false
end
function VehicleController:ComputeThrottle(localMoveVector)
if localMoveVector ~= ZERO_VECTOR3 then
local throttle = -localMoveVector.Z
return throttle
else
return 0.0
end
end
function VehicleController:ComputeSteer(localMoveVector)
if localMoveVector ~= ZERO_VECTOR3 then
local steerAngle = -math.atan2(-localMoveVector.x, -localMoveVector.z) * (180 / math.pi)
return steerAngle / self.autoPilot.MaxSteeringAngle
else
return 0.0
end
end
function VehicleController:SetupAutoPilot()
-- Setup default
self.autoPilot.MaxSpeed = self.vehicleSeat.MaxSpeed
self.autoPilot.MaxSteeringAngle = AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE
-- VehicleSeat should have a MaxSteeringAngle as well.
-- Or we could look for a child "AutoPilotConfigModule" to find these values
-- Or allow developer to set them through the API as like the CLickToMove customization API
end
return VehicleController
end
function _TouchJump()
local Players = game:GetService("Players")
lo