This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,5 @@
{
"lint": {
"ImportUnused": "fatal"
}
}
@@ -0,0 +1,584 @@
--[[
Invisicam
Modified 5.16.2017 by AllYourBlox to consider combined transparency when looking through multiple parts, and to add
a mode that takes advantage of the reduced cost of ray casts from re-implementing GetPartsObscuringTarget
on the C++ side to be O(N) for N parts hit, rather than O(N^2), and to have optimizations specifically
improving performance of closely bundled rays. Fading is also removed, since it is a frame rate killer with high-poly models,
and on mobile.
Based on Invisicam Version 2.5 by OnlyTwentyCharacters
--]]
local Invisicam = {}
---------------
-- Constants --
---------------
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, -- Whatever you want!
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,
}
Invisicam.MODE = MODE
local STARTING_MODE = MODE.SMART_CIRCLE
local LIMB_TRACKING_SET = {
-- Common to R6, R15
['Head'] = true,
-- R6 Only
['Left Arm'] = true,
['Right Arm'] = true,
['Left Leg'] = true,
['Right Leg'] = true,
-- R15 Only
['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
---------------
-- Variables --
---------------
local RunService = game:GetService('RunService')
local PlayersService = game:GetService('Players')
local Player = PlayersService.LocalPlayer
local Camera = nil
local Character = nil
local HumanoidRootPart = nil
local TorsoPart = nil
local HeadPart = nil
local Mode = nil
local BehaviorFunction = nil
local childAddedConn = nil
local childRemovedConn = nil
local Behaviors = {} -- Map of modes to behavior fns
local SavedHits = {} -- Objects currently being faded in/out
local TrackedLimbs = {} -- Used in limb-tracking casting modes
---------------
--| Utility |--
---------------
local math_min = math.min
local math_max = math.max
local math_cos = math.cos
local math_sin = math.sin
local math_pi = math.pi
local Vector3_new = Vector3.new
local ZERO_VECTOR3 = Vector3_new(0,0,0)
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
-----------------------
--| Local Functions |--
-----------------------
local function LimbBehavior(castPoints)
for limb, _ in pairs(TrackedLimbs) do
castPoints[#castPoints + 1] = limb.Position
end
end
local function MoveBehavior(castPoints)
for i = 1, MOVE_CASTS do
local position, velocity = HumanoidRootPart.Position, HumanoidRootPart.Velocity
local horizontalSpeed = Vector3_new(velocity.X, 0, velocity.Z).Magnitude / 2
local offsetVector = (i - 1) * HumanoidRootPart.CFrame.lookVector * horizontalSpeed
castPoints[#castPoints + 1] = position + offsetVector
end
end
local function CornerBehavior(castPoints)
local cframe = HumanoidRootPart.CFrame
local centerPoint = cframe.p
local rotation = cframe - centerPoint
local halfSize = Character: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
local function CircleBehavior(castPoints)
local cframe = nil
if Mode == MODE.CIRCLE1 then
cframe = HumanoidRootPart.CFrame
else
local camCFrame = Camera.CoordinateFrame
cframe = camCFrame - camCFrame.p + 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
local function LimbMoveBehavior(castPoints)
LimbBehavior(castPoints)
MoveBehavior(castPoints)
end
local function CharacterOutlineBehavior(castPoints)
local torsoUp = TorsoPart.CFrame.upVector.unit
local torsoRight = TorsoPart.CFrame.rightVector.unit
-- Torso cross of points for interior coverage
castPoints[#castPoints + 1] = TorsoPart.CFrame.p
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoUp
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoUp
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoRight
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoRight
if HeadPart then
castPoints[#castPoints + 1] = HeadPart.CFrame.p
end
local cframe = CFrame.new(ZERO_VECTOR3,Vector3_new(Camera.CoordinateFrame.lookVector.X,0,Camera.CoordinateFrame.lookVector.Z))
local centerPoint = (TorsoPart and TorsoPart.Position or HumanoidRootPart.Position)
local partsWhitelist = {TorsoPart}
if HeadPart then
partsWhitelist[#partsWhitelist + 1] = 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
-- Helper function for Determinant of 3x3
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
local function SmartCircleBehavior(castPoints)
local torsoUp = TorsoPart.CFrame.upVector.unit
local torsoRight = 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] = TorsoPart.CFrame.p
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoUp
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoUp
castPoints[#castPoints + 1] = TorsoPart.CFrame.p + torsoRight
castPoints[#castPoints + 1] = TorsoPart.CFrame.p - torsoRight
if HeadPart then
castPoints[#castPoints + 1] = HeadPart.CFrame.p
end
local cameraOrientation = Camera.CFrame - Camera.CFrame.p
local torsoPoint = Vector3_new(0,0.5,0) + (TorsoPart and TorsoPart.Position or 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 - Camera.CFrame.p
local ray = Ray.new(torsoPoint, circlePoint - torsoPoint)
local hit, hp, hitNormal = game.Workspace:FindPartOnRayWithIgnoreList(ray, {Character}, 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 d0 = v0.magnitude
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 - 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, {Character}, 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, {Character}, false, false )
if hit then
local castPoint2 = hitPoint - 0.1 * (castPoint - torsoPoint).unit
castPoint = castPoint2
end
end
castPoints[#castPoints + 1] = castPoint
end
end
local function CheckTorsoReference()
if Character then
TorsoPart = Character:FindFirstChild("Torso")
if not TorsoPart then
TorsoPart = Character:FindFirstChild("UpperTorso")
if not TorsoPart then
TorsoPart = Character:FindFirstChild("HumanoidRootPart")
end
end
HeadPart = Character:FindFirstChild("Head")
end
end
local function OnCharacterAdded(character)
if childAddedConn then
childAddedConn:disconnect()
childAddedConn = nil
end
if childRemovedConn then
childRemovedConn:disconnect()
childRemovedConn = nil
end
Character = character
TrackedLimbs = {}
local function childAdded(child)
if child:IsA('BasePart') then
if LIMB_TRACKING_SET[child.Name] then
TrackedLimbs[child] = true
end
if (child.Name == 'Torso' or child.Name == 'UpperTorso') then
TorsoPart = child
end
if (child.Name == 'Head') then
HeadPart = child
end
end
end
local function childRemoved(child)
TrackedLimbs[child] = nil
-- If removed/replaced part is 'Torso' or 'UpperTorso' double check that we still have a TorsoPart to use
CheckTorsoReference()
end
childAddedConn = character.ChildAdded:connect(childAdded)
childRemovedConn = character.ChildRemoved:connect(childRemoved)
for _, child in pairs(Character:GetChildren()) do
childAdded(child)
end
end
local function OnCurrentCameraChanged()
local newCamera = workspace.CurrentCamera
if newCamera then
Camera = newCamera
end
end
-----------------------
-- Exposed Functions --
-----------------------
-- Update. Called every frame after the camera movement step
function Invisicam:Update()
-- Bail if there is no Character
if not Character then return end
-- Make sure we still have a HumanoidRootPart
if not HumanoidRootPart then
local humanoid = Character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Torso then
HumanoidRootPart = humanoid.Torso
else
-- Not set up with Humanoid? Try and see if there's one in the Character at all:
HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
if not HumanoidRootPart then
-- Bail out, since we're relying on HumanoidRootPart existing
return
end
end
local ancestryChangedConn;
ancestryChangedConn = HumanoidRootPart.AncestryChanged:connect(function(child, parent)
if child == HumanoidRootPart and not parent then
HumanoidRootPart = nil
if ancestryChangedConn and ancestryChangedConn.Connected then
ancestryChangedConn:Disconnect()
ancestryChangedConn = nil
end
end
end)
end
if not TorsoPart then
CheckTorsoReference()
if not TorsoPart then
-- Bail out, since we're relying on Torso existing, should never happen since we fall back to using HumanoidRootPart as torso
return
end
end
-- Make a list of world points to raycast to
local castPoints = {}
BehaviorFunction(castPoints)
-- Cast to get a list of objects between the camera and the cast points
local currentHits = {}
local ignoreList = {Character}
local function add(hit)
currentHits[hit] = true
if not SavedHits[hit] then
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 partIsTouchingCamera = {}
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 = HeadPart and HeadPart.CFrame.p or castPoints[1]
local torsoPoint = TorsoPart and TorsoPart.CFrame.p or castPoints[2]
hitParts = 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 = 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(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
SavedHits[hitPart] = nil
end
end
end
function Invisicam:SetMode(newMode)
AssertTypes(newMode, 'number')
for modeName, modeNum in pairs(MODE) do
if modeNum == newMode then
Mode = newMode
BehaviorFunction = Behaviors[Mode]
return
end
end
error("Invalid mode number")
end
function Invisicam:SetCustomBehavior(func)
AssertTypes(func, 'function')
Behaviors[MODE.CUSTOM] = func
if Mode == MODE.CUSTOM then
BehaviorFunction = func
end
end
function Invisicam:GetObscuredParts()
return SavedHits
end
-- Want to turn off Invisicam? Be sure to call this after.
function Invisicam:Cleanup()
for hit, originalFade in pairs(SavedHits) do
hit.LocalTransparencyModifier = originalFade
end
end
---------------------
--| Running Logic |--
---------------------
-- Connect to the current and all future cameras
workspace:GetPropertyChangedSignal("CurrentCamera"):connect(OnCurrentCameraChanged)
OnCurrentCameraChanged()
Player.CharacterAdded:connect(OnCharacterAdded)
if Player.Character then
OnCharacterAdded(Player.Character)
end
Behaviors[MODE.CUSTOM] = function() end -- (Does nothing until SetCustomBehavior)
Behaviors[MODE.LIMBS] = LimbBehavior
Behaviors[MODE.MOVEMENT] = MoveBehavior
Behaviors[MODE.CORNERS] = CornerBehavior
Behaviors[MODE.CIRCLE1] = CircleBehavior
Behaviors[MODE.CIRCLE2] = CircleBehavior
Behaviors[MODE.LIMBMOVE] = LimbMoveBehavior
Behaviors[MODE.SMART_CIRCLE] = SmartCircleBehavior
Behaviors[MODE.CHAR_OUTLINE] = CharacterOutlineBehavior
Invisicam:SetMode(STARTING_MODE)
return Invisicam
@@ -0,0 +1,175 @@
-- PopperCam Version 16
-- OnlyTwentyCharacters
local PopperCam = {} -- Guarantees your players won't see outside the bounds of your map!
-----------------
--| Constants |--
-----------------
local POP_RESTORE_RATE = 0.3
local MIN_CAMERA_ZOOM = 0.5
local VALID_SUBJECTS = {
'Humanoid',
'VehicleSeat',
'SkateboardPlatform',
}
-----------------
--| Variables |--
-----------------
local PlayersService = game:GetService('Players')
local Camera = nil
local CameraSubjectChangeConn = nil
local SubjectPart = nil
local PlayerCharacters = {} -- For ignoring in raycasts
local VehicleParts = {} -- Also just for ignoring
local LastPopAmount = 0
local LastZoomLevel = 0
local PopperEnabled = false
local CFrame_new = CFrame.new
-----------------------
--| Local Functions |--
-----------------------
local math_abs = math.abs
local function OnCameraSubjectChanged()
VehicleParts = {}
local newSubject = Camera.CameraSubject
if newSubject then
-- Determine if we should be popping at all
PopperEnabled = false
for _, subjectType in pairs(VALID_SUBJECTS) do
if newSubject:IsA(subjectType) then
PopperEnabled = true
break
end
end
-- Get all parts of the vehicle the player is controlling
if newSubject:IsA('VehicleSeat') then
VehicleParts = newSubject:GetConnectedParts(true)
end
end
end
local function OnCharacterAdded(player, character)
PlayerCharacters[player] = character
end
local function OnPlayersChildAdded(child)
if child:IsA('Player') then
child.CharacterAdded:connect(function(character)
OnCharacterAdded(child, character)
end)
if child.Character then
OnCharacterAdded(child, child.Character)
end
end
end
local function OnPlayersChildRemoved(child)
if child:IsA('Player') then
PlayerCharacters[child] = nil
end
end
local function OnWorkspaceChanged(property)
if property == 'CurrentCamera' then
local newCamera = workspace.CurrentCamera
if newCamera then
Camera = newCamera
if CameraSubjectChangeConn then
CameraSubjectChangeConn:disconnect()
end
CameraSubjectChangeConn = Camera:GetPropertyChangedSignal("CameraSubject"):connect(OnCameraSubjectChanged)
OnCameraSubjectChanged()
end
end
end
-------------------------
--| Exposed Functions |--
-------------------------
function PopperCam:Update(EnabledCamera)
if PopperEnabled then
-- First, prep some intermediate vars
local cameraCFrame = Camera.CFrame
local focusPoint = Camera.Focus.p
local ignoreList = {}
for _, character in pairs(PlayerCharacters) do
ignoreList[#ignoreList + 1] = character
end
for i = 1, #VehicleParts do
ignoreList[#ignoreList + 1] = VehicleParts[i]
end
-- Get largest cutoff distance
local largest = Camera:GetLargestCutoffDistance(ignoreList)
-- Then check if the player zoomed since the last frame,
-- and if so, reset our pop history so we stop tweening
local zoomLevel = (cameraCFrame.p - focusPoint).Magnitude
if math_abs(zoomLevel - LastZoomLevel) > 0.001 then
LastPopAmount = 0
end
-- Finally, zoom the camera in (pop) by that most-cut-off amount, or the last pop amount if that's more
local popAmount = largest
if LastPopAmount > popAmount then
popAmount = LastPopAmount
end
if popAmount > 0 then
Camera.CFrame = cameraCFrame + (cameraCFrame.lookVector * popAmount)
LastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame
if LastPopAmount < 0 then
LastPopAmount = 0
end
end
LastZoomLevel = zoomLevel
-- Stop shift lock being able to see through walls by manipulating Camera focus inside the wall
if EnabledCamera and EnabledCamera:GetShiftLock() and not EnabledCamera:IsInFirstPerson() then
if EnabledCamera:GetCameraActualZoom() < 1 then
local subjectPosition = EnabledCamera.lastSubjectPosition
if subjectPosition then
Camera.Focus = CFrame_new(subjectPosition)
Camera.CFrame = CFrame_new(subjectPosition - MIN_CAMERA_ZOOM*EnabledCamera:GetCameraLook(), subjectPosition)
end
end
end
end
end
--------------------
--| Script Logic |--
--------------------
-- Connect to the current and all future cameras
workspace.Changed:connect(OnWorkspaceChanged)
OnWorkspaceChanged('CurrentCamera')
-- Connect to all Players so we can ignore their Characters
PlayersService.ChildRemoved:connect(OnPlayersChildRemoved)
PlayersService.ChildAdded:connect(OnPlayersChildAdded)
for _, player in pairs(PlayersService:GetPlayers()) do
OnPlayersChildAdded(player)
end
return PopperCam
@@ -0,0 +1,6 @@
{
"lint": {
"LocalShadow": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,75 @@
local PlayersService = game:GetService('Players')
local RootCameraCreator = require(script.Parent)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local XZ_VECTOR = Vector3.new(1,0,1)
local Vector2_new = Vector2.new
local CFrame_new = CFrame.new
local math_atan2 = math.atan2
local math_min = math.min
local function IsFinite(num)
return num == num and num ~= 1/0 and num ~= -1/0
end
-- May return NaN or inf or -inf
-- This is a way of finding the angle between the two vectors:
local function findAngleBetweenXZVectors(vec2, vec1)
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
end
local function CreateAttachCamera()
local module = RootCameraCreator()
local lastUpdate = tick()
function module:Update()
local now = tick()
local camera = workspace.CurrentCamera
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and camera then
local zoom = self:GetCameraZoom()
if zoom <= 0 then
zoom = 0.1
end
local humanoid = self:GetHumanoid()
if lastUpdate and humanoid and humanoid.Torso 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 - lastUpdate)
local gamepadRotation = self:UpdateGamepad()
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
local forwardVector = humanoid.Torso.CFrame.lookVector
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
if IsFinite(y) then
-- Preserve vertical rotation from user input
self.RotateInput = Vector2_new(y, self.RotateInput.Y)
end
end
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
self.RotateInput = ZERO_VECTOR2
camera.Focus = CFrame_new(subjectPosition)
local newCFrame = CFrame_new(subjectPosition - (zoom * newLookVector), subjectPosition)
camera.CFrame = newCFrame
self.LastCameraTransform = newCFrame
end
lastUpdate = now
end
return module
end
return CreateAttachCamera
@@ -0,0 +1,195 @@
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local RootCameraCreator = require(script.Parent)
local UP_VECTOR = Vector3.new(0, 1, 0)
local XZ_VECTOR = Vector3.new(1, 0, 1)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local VR_PITCH_FRACTION = 0.25
local Vector3_new = Vector3.new
local CFrame_new = CFrame.new
local math_min = math.min
local math_max = math.max
local math_atan2 = math.atan2
local math_rad = math.rad
local math_abs = math.abs
local function clamp(low, high, num)
return (num > high and high or num < low and low or num)
end
local function IsFinite(num)
return num == num and num ~= 1/0 and num ~= -1/0
end
local function IsFiniteVector3(vec3)
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
end
-- May return NaN or inf or -inf
-- This is a way of finding the angle between the two vectors:
local function findAngleBetweenXZVectors(vec2, vec1)
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
end
local function CreateClassicCamera()
local module = RootCameraCreator()
local tweenAcceleration = math_rad(220)
local tweenSpeed = math_rad(0)
local tweenMaxSpeed = math_rad(250)
local timeBeforeAutoRotate = 2
local lastUpdate = tick()
module.LastUserPanCamera = tick()
function module:Update()
module:ProcessTweens()
local now = tick()
local timeDelta = (now - lastUpdate)
local userPanningTheCamera = (self.UserPanningTheCamera == true)
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
local humanoid = self:GetHumanoid()
local cameraSubject = camera and camera.CameraSubject
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
if 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, now - lastUpdate)
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
tweenSpeed = 0
module.LastUserPanCamera = tick()
end
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:GetCameraZoom()
if zoom < 0.5 then
zoom = 0.5
end
if self:GetShiftLock() and not self:IsInFirstPerson() then
-- We need to use the right vector of the camera after rotation, not before
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75)
if IsFiniteVector3(offset) then
subjectPosition = subjectPosition + offset
end
else
if not userPanningTheCamera and self.LastCameraTransform then
local isInFirstPerson = self:IsInFirstPerson()
if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
if isInFirstPerson then
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
if 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 = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
local percent = clamp(0, 1, tweenSpeed * timeDelta)
if self:IsInFirstPerson() then
percent = 1
end
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
if IsFinite(y) and math_abs(y) > 0.0001 then
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
end
end
end
end
end
local VREnabled = VRService.VREnabled
camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)
local cameraFocusP = camera.Focus.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 > zoom or self.RotateInput.x ~= 0 then
local desiredDist = math_min(distToSubject, zoom)
vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, 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
camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
end
else
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
self.RotateInput = ZERO_VECTOR2
camera.CFrame = CFrame_new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
end
self.LastCameraTransform = camera.CFrame
self.LastCameraFocus = camera.Focus
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
self.LastSubjectCFrame = cameraSubject.CFrame
else
self.LastSubjectCFrame = nil
end
end
lastUpdate = now
end
return module
end
return CreateClassicCamera
@@ -0,0 +1,48 @@
local PlayersService = game:GetService('Players')
local RootCameraCreator = require(script.Parent)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local CFrame_new = CFrame.new
local math_min = math.min
local function CreateFixedCamera()
local module = RootCameraCreator()
local lastUpdate = tick()
function module:Update()
local now = tick()
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
if 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 - lastUpdate)
local gamepadRotation = self:UpdateGamepad()
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
end
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:GetCameraZoom()
if zoom <= 0 then
zoom = 0.1
end
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
self.RotateInput = ZERO_VECTOR2
camera.CFrame = CFrame_new(camera.CFrame.p, camera.CFrame.p + (zoom * newLookVector))
self.LastCameraTransform = camera.CFrame
end
lastUpdate = now
end
return module
end
return CreateFixedCamera
@@ -0,0 +1,192 @@
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local RootCameraCreator = require(script.Parent)
local CFrame_new = CFrame.new
local Vector2_new = Vector2.new
local Vector3_new = Vector3.new
local math_min = math.min
local math_max = math.max
local math_atan2 = math.atan2
local math_rad = math.rad
local math_abs = math.abs
local HUMANOIDSTATE_CLIMBING = Enum.HumanoidStateType.Climbing
local ZERO_VECTOR2 = Vector2.new(0, 0)
local UP_VECTOR = Vector3.new(0, 1, 0)
local XZ_VECTOR = Vector3.new(1, 0, 1)
local ZERO_VECTOR3 = Vector3.new(0, 0, 0)
local PORTRAIT_OFFSET = Vector3.new(0, -3, 0)
local function clamp(low, high, num)
return num > high and high or num < low and low or num
end
local function IsFinite(num)
return num == num and num ~= 1/0 and num ~= -1/0
end
local function IsFiniteVector3(vec3)
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
end
-- May return NaN or inf or -inf
local function findAngleBetweenXZVectors(vec2, vec1)
-- This is a way of finding the angle between the two vectors:
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
end
local function CreateFollowCamera()
local module = RootCameraCreator()
local tweenAcceleration = math_rad(220)
local tweenSpeed = math_rad(0)
local tweenMaxSpeed = math_rad(250)
local timeBeforeAutoRotate = 2
local lastUpdate = tick()
module.LastUserPanCamera = tick()
function module:Update()
module:ProcessTweens()
local now = tick()
local timeDelta = (now - lastUpdate)
local userPanningTheCamera = (self.UserPanningTheCamera == true)
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
local humanoid = self:GetHumanoid()
local cameraSubject = camera and camera.CameraSubject
local isClimbing = humanoid and humanoid:GetState() == HUMANOIDSTATE_CLIMBING
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
if lastUpdate then
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, now - lastUpdate)
local angle = 0
-- NOTE: Traditional follow camera does not rotate with arrow keys
if not (isInVehicle or isOnASkateboard) then
angle = angle + (self.TurningLeft and -120 or 0)
angle = angle + (self.TurningRight and 120 or 0)
end
local gamepadRotation = self:UpdateGamepad()
if gamepadRotation ~= Vector2.new(0,0) then
userPanningTheCamera = true
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
end
if angle ~= 0 then
userPanningTheCamera = true
self.RotateInput = self.RotateInput + Vector2_new(math_rad(angle * delta), 0)
end
end
end
-- Reset tween speed if user is panning
if userPanningTheCamera then
tweenSpeed = 0
module.LastUserPanCamera = tick()
end
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:GetCameraZoom()
if zoom < 0.5 then
zoom = 0.5
end
if self:GetShiftLock() and not self:IsInFirstPerson() then
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
local offset = ((newLookVector * XZ_VECTOR):Cross(UP_VECTOR).unit * 1.75)
if IsFiniteVector3(offset) then
subjectPosition = subjectPosition + offset
end
else
if self.LastCameraTransform and not userPanningTheCamera then
local isInFirstPerson = self:IsInFirstPerson()
if (isClimbing or isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
if isInFirstPerson then
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
if 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 = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
local percent = clamp(0, 1, tweenSpeed*timeDelta)
if not isClimbing and self:IsInFirstPerson() then
percent = 1
end
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
-- Check for NaN
if IsFinite(y) and math_abs(y) > 0.0001 then
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
end
end
elseif not (isInFirstPerson or userRecentlyPannedCamera) and not VRService.VREnabled then
local lastVec = -(self.LastCameraTransform.p - subjectPosition)
local y = findAngleBetweenXZVectors(lastVec, self:GetCameraLook())
-- 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 deadzone to allow more precise forward movements.
local thetaCutoff = 0.4
-- Check for NaNs
if 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
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
self.RotateInput = ZERO_VECTOR2
if VRService.VREnabled then
camera.Focus = self:GetVRFocus(subjectPosition, timeDelta)
elseif self:IsPortraitMode() then
camera.Focus = CFrame_new(subjectPosition + PORTRAIT_OFFSET)
else
camera.Focus = CFrame_new(subjectPosition)
end
camera.CFrame = CFrame_new(camera.Focus.p - (zoom * newLookVector), camera.Focus.p) + Vector3.new(0, self:GetCameraHeight(), 0)
self.LastCameraTransform = camera.CFrame
self.LastCameraFocus = camera.Focus
if isInVehicle or isOnASkateboard and cameraSubject:IsA('BasePart') then
self.LastSubjectCFrame = cameraSubject.CFrame
else
self.LastSubjectCFrame = nil
end
end
lastUpdate = now
end
return module
end
return CreateFollowCamera
@@ -0,0 +1,450 @@
--[[
Orbital Camera 1.0.0
AllYourBlox
Derived from ClassicCamera, adds camera angle constraints, and represents position values in spherical
coordinates (azimuth, elevation, radius), instead of Cartesian coordinates (x, y, z). Azimuth is the
angle of rotation about the Y axis, with the zero-angle reference point corresponding to offsetting
the camera in the +Z direction, from where it will be looking in the -Z direction by default. Elevation
is the angle up from the XZ plane, where zero degrees is on the plane, and +90 degrees is on the +Y
axis. Distance is the camera-to-subject distance, the spherical coordinates radius, specified in studs.
--]]
-- 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 refAzimuthRad
local curAzimuthRad
local minAzimuthAbsoluteRad
local maxAzimuthAbsoluteRad
local useAzimuthLimits
local curElevationRad
local minElevationRad
local maxElevationRad
local curDistance
local minDistance
local maxDistance
local UNIT_Z = Vector3.new(0,0,1)
local TAU = 2 * math.pi
local changedSignalConnections = {}
-- End of OrbitalCamera additions
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local RootCameraCreator = require(script.Parent)
local UP_VECTOR = Vector3.new(0, 1, 0)
local XZ_VECTOR = Vector3.new(1, 0, 1)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local VR_PITCH_FRACTION = 0.25
local Vector3_new = Vector3.new
local CFrame_new = CFrame.new
local math_min = math.min
local math_max = math.max
local math_atan2 = math.atan2
local math_rad = math.rad
local math_abs = math.abs
--[[ Gamepad Support ]]--
local THUMBSTICK_DEADZONE = 0.2
local r3ButtonDown = false
local l3ButtonDown = false
local currentZoomSpeed = 1 -- Multiplier, so 1 == no zooming
local function clamp(value, minValue, maxValue)
if maxValue < minValue then
maxValue = minValue
end
return math.clamp(value, minValue, maxValue)
end
local function IsFinite(num)
return num == num and num ~= 1/0 and num ~= -1/0
end
local function IsFiniteVector3(vec3)
return IsFinite(vec3.x) and IsFinite(vec3.y) and IsFinite(vec3.z)
end
-- May return NaN or inf or -inf
-- This is a way of finding the angle between the two vectors:
local function findAngleBetweenXZVectors(vec2, vec1)
return math_atan2(vec1.X*vec2.Z-vec1.Z*vec2.X, vec1.X*vec2.X + vec1.Z*vec2.Z)
end
local function GetValueObject(name, defaultValue)
local valueObj = script:FindFirstChild(name)
if valueObj then
return valueObj.Value
end
return defaultValue
end
local function 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
externalProperties[name] = valueObj.Value
elseif 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 = externalProperties[name]
else
print("externalProperties table has no entry for ",name)
return
end
if updateFunction then
if changedSignalConnections[name] then
changedSignalConnections[name]:disconnect()
end
changedSignalConnections[name] = valueObj.Changed:connect(function(newValue)
externalProperties[name] = newValue
updateFunction()
end)
end
end
local function SetAndBoundsCheckAzimuthValues()
minAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) - math.abs(math.rad(externalProperties["CWAzimuthTravel"]))
maxAzimuthAbsoluteRad = math.rad(externalProperties["ReferenceAzimuth"]) + math.abs(math.rad(externalProperties["CCWAzimuthTravel"]))
useAzimuthLimits = externalProperties["UseAzimuthLimits"]
if useAzimuthLimits then
curAzimuthRad = math.max(curAzimuthRad, minAzimuthAbsoluteRad)
curAzimuthRad = math.min(curAzimuthRad, maxAzimuthAbsoluteRad)
end
end
local function 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(externalProperties["MinElevation"], MIN_ALLOWED_ELEVATION_DEG)
local maxElevationDeg = math.min(externalProperties["MaxElevation"], MAX_ALLOWED_ELEVATION_DEG)
-- Set internal values in radians
minElevationRad = math.rad(math.min(minElevationDeg, maxElevationDeg))
maxElevationRad = math.rad(math.max(minElevationDeg, maxElevationDeg))
curElevationRad = math.max(curElevationRad, minElevationRad)
curElevationRad = math.min(curElevationRad, maxElevationRad)
end
local function SetAndBoundsCheckDistanceValues()
minDistance = externalProperties["MinDistance"]
maxDistance = externalProperties["MaxDistance"]
curDistance = math.max(curDistance, minDistance)
curDistance = math.min(curDistance, maxDistance)
end
-- This loads from, or lazily creates, NumberValue objects for exposed parameters
local function LoadNumberValueParameters()
-- These initial values do not require change listeners since they are read only once
LoadOrCreateNumberValueParameter("InitialElevation", "NumberValue", nil)
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
LoadOrCreateNumberValueParameter("ReferenceAzimuth", "NumberValue", SetAndBoundsCheckAzimuthValues)
LoadOrCreateNumberValueParameter("CWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
LoadOrCreateNumberValueParameter("CCWAzimuthTravel", "NumberValue", SetAndBoundsCheckAzimuthValues)
LoadOrCreateNumberValueParameter("MinElevation", "NumberValue", SetAndBoundsCheckElevationValues)
LoadOrCreateNumberValueParameter("MaxElevation", "NumberValue", SetAndBoundsCheckElevationValues)
LoadOrCreateNumberValueParameter("MinDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
LoadOrCreateNumberValueParameter("MaxDistance", "NumberValue", SetAndBoundsCheckDistanceValues)
LoadOrCreateNumberValueParameter("UseAzimuthLimits", "BoolValue", SetAndBoundsCheckAzimuthValues)
-- Internal values set (in radians, from degrees), plus sanitization
curAzimuthRad = math.rad(externalProperties["ReferenceAzimuth"])
curElevationRad = math.rad(externalProperties["InitialElevation"])
curDistance = externalProperties["InitialDistance"]
SetAndBoundsCheckAzimuthValues()
SetAndBoundsCheckElevationValues()
SetAndBoundsCheckDistanceValues()
end
local function CreateOrbitalCamera()
local module = RootCameraCreator()
LoadNumberValueParameters()
module.DefaultZoom = curDistance
local tweenAcceleration = math_rad(220)
local tweenSpeed = math_rad(0)
local tweenMaxSpeed = math_rad(250)
local timeBeforeAutoRotate = 2
local lastUpdate = tick()
module.LastUserPanCamera = tick()
function module:Update()
module:ProcessTweens()
local now = tick()
local timeDelta = (now - lastUpdate)
local userPanningTheCamera = (self.UserPanningTheCamera == true)
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
local humanoid = self:GetHumanoid()
local cameraSubject = camera and camera.CameraSubject
local isInVehicle = cameraSubject and cameraSubject:IsA('VehicleSeat')
local isOnASkateboard = cameraSubject and cameraSubject:IsA('SkateboardPlatform')
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
if 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, now - lastUpdate)
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
tweenSpeed = 0
module.LastUserPanCamera = tick()
end
local userRecentlyPannedCamera = now - module.LastUserPanCamera < timeBeforeAutoRotate
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:ZoomCamera(curDistance * currentZoomSpeed)
if not userPanningTheCamera and self.LastCameraTransform then
local isInFirstPerson = self:IsInFirstPerson()
if (isInVehicle or isOnASkateboard) and lastUpdate and humanoid and humanoid.Torso then
if isInFirstPerson then
if self.LastSubjectCFrame and (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
local y = -findAngleBetweenXZVectors(self.LastSubjectCFrame.lookVector, cameraSubject.CFrame.lookVector)
if 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 = clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
local percent = clamp(0, 1, tweenSpeed * timeDelta)
if self:IsInFirstPerson() then
percent = 1
end
local y = findAngleBetweenXZVectors(forwardVector, self:GetCameraLook())
if IsFinite(y) and math_abs(y) > 0.0001 then
self.RotateInput = self.RotateInput + Vector2.new(y * percent, 0)
end
end
end
end
local VREnabled = VRService.VREnabled
camera.Focus = VREnabled and self:GetVRFocus(subjectPosition, timeDelta) or CFrame_new(subjectPosition)
local cameraFocusP = camera.Focus.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 > zoom or self.RotateInput.x ~= 0 then
local desiredDist = math_min(distToSubject, zoom)
vecToSubject = self:RotateCamera(vecToSubject.unit * XZ_VECTOR, 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
camera.CFrame = CFrame_new(newPos, lookAt) + Vector3_new(0, cameraHeight, 0)
end
else
-- self.RotateInput is a Vector2 of mouse movement deltas since last update
curAzimuthRad = curAzimuthRad - self.RotateInput.x
if useAzimuthLimits then
curAzimuthRad = clamp(curAzimuthRad, minAzimuthAbsoluteRad, maxAzimuthAbsoluteRad)
else
curAzimuthRad = (curAzimuthRad ~= 0) and (math.sign(curAzimuthRad) * (math.abs(curAzimuthRad) % TAU)) or 0
end
curDistance = clamp(zoom, minDistance, maxDistance)
curElevationRad = clamp(curElevationRad + self.RotateInput.y, minElevationRad,maxElevationRad)
local cameraPosVector = curDistance * ( CFrame.fromEulerAnglesYXZ( -curElevationRad, curAzimuthRad, 0 ) * UNIT_Z )
local camPos = subjectPosition + cameraPosVector
camera.CFrame = CFrame.new(camPos, subjectPosition)
self.RotateInput = ZERO_VECTOR2
end
self.LastCameraTransform = camera.CFrame
self.LastCameraFocus = camera.Focus
if (isInVehicle or isOnASkateboard) and cameraSubject:IsA('BasePart') then
self.LastSubjectCFrame = cameraSubject.CFrame
else
self.LastSubjectCFrame = nil
end
end
lastUpdate = now
end
function module:GetCameraZoom()
return curDistance
end
function module:ZoomCamera(desiredZoom)
local player = PlayersService.LocalPlayer
if player then
curDistance = clamp(desiredZoom, minDistance, maxDistance)
end
local isFirstPerson = self:GetCameraZoom() < 2
--ShiftLockController:SetIsInFirstPerson(isFirstPerson)
-- set mouse behavior
self:UpdateMouseBehavior()
return self:GetCameraZoom()
end
function module:ZoomCameraBy(zoomScale)
local newDist = curDistance
-- Can break into more steps to get more accurate integration
newDist = self:rk4Integrator(curDistance, zoomScale, 1)
self:ZoomCamera(newDist)
return self:GetCameraZoom()
end
function module:ZoomCameraFixedBy(zoomIncrement)
return self:ZoomCamera(self:GetCameraZoom() + zoomIncrement)
end
function module:SetInitialOrientation(humanoid)
local newDesiredLook = (humanoid.Torso.CFrame.lookVector - Vector3.new(0,0.23,0)).unit
local horizontalShift = findAngleBetweenXZVectors(newDesiredLook, self:GetCameraLook())
local vertShift = math.asin(self:GetCameraLook().y) - math.asin(newDesiredLook.y)
if not IsFinite(horizontalShift) then
horizontalShift = 0
end
if not IsFinite(vertShift) then
vertShift = 0
end
self.RotateInput = Vector2.new(horizontalShift, vertShift)
end
function module.getGamepadPan(name, state, input)
if input.UserInputType == module.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then
if r3ButtonDown or l3ButtonDown then
-- R3 or L3 Thumbstick is depressed, right stick controls dolly in/out
if (input.Position.Y > THUMBSTICK_DEADZONE) then
currentZoomSpeed = 0.96
elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then
currentZoomSpeed = 1.04
else
currentZoomSpeed = 1.00
end
else
if state == Enum.UserInputState.Cancel then
module.GamepadPanningCamera = ZERO_VECTOR2
return
end
local inputVector = Vector2.new(input.Position.X, -input.Position.Y)
if inputVector.magnitude > THUMBSTICK_DEADZONE then
module.GamepadPanningCamera = Vector2.new(input.Position.X, -input.Position.Y)
else
module.GamepadPanningCamera = ZERO_VECTOR2
end
end
end
end
function module.doGamepadZoom(name, state, input)
if input.UserInputType == module.activeGamepad and (input.KeyCode == Enum.KeyCode.ButtonR3 or input.KeyCode == Enum.KeyCode.ButtonL3) then
if (state == Enum.UserInputState.Begin) then
r3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonR3
l3ButtonDown = input.KeyCode == Enum.KeyCode.ButtonL3
elseif (state == Enum.UserInputState.End) then
if (input.KeyCode == Enum.KeyCode.ButtonR3) then
r3ButtonDown = false
elseif (input.KeyCode == Enum.KeyCode.ButtonL3) then
l3ButtonDown = false
end
if (not r3ButtonDown) and (not l3ButtonDown) then
currentZoomSpeed = 1.00
end
end
end
end
function module:BindGamepadInputActions()
local ContextActionService = game:GetService('ContextActionService')
ContextActionService:BindAction("OrbitalCamGamepadPan", module.getGamepadPan, false, Enum.KeyCode.Thumbstick2)
ContextActionService:BindAction("OrbitalCamGamepadZoom", module.doGamepadZoom, false, Enum.KeyCode.ButtonR3)
ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", module.doGamepadZoom, false, Enum.KeyCode.ButtonL3)
end
return module
end
return CreateOrbitalCamera
@@ -0,0 +1,12 @@
local RootCameraCreator = require(script.Parent)
local function CreateScriptableCamera()
local module = RootCameraCreator()
function module:Update()
end
return module
end
return CreateScriptableCamera
@@ -0,0 +1,54 @@
local PlayersService = game:GetService('Players')
local RootCameraCreator = require(script.Parent)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local CFrame_new = CFrame.new
local math_min = math.min
local function CreateTrackCamera()
local module = RootCameraCreator()
local lastUpdate = tick()
function module:Update()
local now = tick()
local userPanningTheCamera = (self.UserPanningTheCamera == true)
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastCameraTransform = nil
end
if 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 - lastUpdate)
local gamepadRotation = self:UpdateGamepad()
if gamepadRotation ~= ZERO_VECTOR2 then
userPanningTheCamera = true
self.RotateInput = self.RotateInput + (gamepadRotation * delta)
end
end
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local zoom = self:GetCameraZoom()
if zoom <= 0 then
zoom = 0.1
end
local newLookVector = self:RotateCamera(self:GetCameraLook(), self.RotateInput)
self.RotateInput = ZERO_VECTOR2
camera.Focus = CFrame_new(subjectPosition)
camera.CFrame = CFrame_new(subjectPosition - (zoom * newLookVector), subjectPosition)
self.LastCameraTransform = camera.CoordinateFrame
end
lastUpdate = now
end
return module
end
return CreateTrackCamera
@@ -0,0 +1,260 @@
local PlayersService = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local VRService = game:GetService("VRService")
local ContextActionService = game:GetService("ContextActionService")
local LocalPlayer = PlayersService.LocalPlayer
local RootCameraCreator = require(script.Parent)
local XZ_VECTOR = Vector3.new(1, 0, 1)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local PRESNAP_TIME_OFFSET = 0.5 --seconds
local PRESNAP_TIME_LATCH = PRESNAP_TIME_OFFSET * 10
local HEIGHT_OFFSET = 3
local HORZ_OFFSET = 4
local function CreateVRCamera()
local module = RootCameraCreator()
module.AllowOcclusion = false
local lastUpdate = tick()
local forceSnap = true
local forceSnapPoint = nil
local lastLook = Vector3.new(0, 0, -1)
local movementUpdateEventConn = nil
local lastSnapTimeEstimate = math.huge
local snapId = 0
local waitingForSnap = false
local snappedHeadOffset = VRService:GetUserCFrame(Enum.UserCFrame.Head).p
local snapPendingWhileJumping = false
local isJumping = false
local autoSnapsPaused = false
local currentDestination = nil
local function forceSnapTo(snapPoint)
forceSnap = true
forceSnapPoint = snapPoint
snapId = snapId + 1
waitingForSnap = false
end
local function onMovementUpdateEvent(updateType, arg1, arg2)
if updateType == "targetPoint" then
currentDestination = arg1
end
if updateType == "timing" then
local estimatedTimeRemaining = arg1
local snapPoint = arg2
if waitingForSnap and estimatedTimeRemaining > lastSnapTimeEstimate then
--our estimate grew, so cancel this snap and potentially re-evaluate it
waitingForSnap = false
snapId = snapId + 1
end
if estimatedTimeRemaining < PRESNAP_TIME_LATCH and estimatedTimeRemaining > PRESNAP_TIME_OFFSET then
waitingForSnap = true
snapId = snapId + 1
local thisSnapId = snapId
local timeToWait = estimatedTimeRemaining - PRESNAP_TIME_OFFSET
coroutine.wrap(function()
wait(timeToWait)
if waitingForSnap and snapId == thisSnapId then
waitingForSnap = false
forceSnap = true
forceSnapPoint = snapPoint
end
end)()
end
elseif updateType == "shortPath" or updateType == "pathFailure" or updateType == "offtrack" and not autoSnapsPaused then
if isJumping then
snapPendingWhileJumping = true
return
end
local snapPoint = arg1
forceSnapTo(snapPoint)
elseif updateType == "force" then
snapPendingWhileJumping = false
isJumping = false
forceSnapTo(nil)
end
end
local function onResetCameraAction(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin then
autoSnapsPaused = true
onMovementUpdateEvent("force", nil, nil)
else
autoSnapsPaused = false
end
end
local function bindActions(bind)
if bind then
ContextActionService:BindActionAtPriority("ResetCameraVR", onResetCameraAction, false, Enum.ContextActionPriority.Low.Value, Enum.KeyCode.ButtonL2)
else
autoSnapsPaused = false
ContextActionService:UnbindAction("ResetCameraVR")
end
end
local lastKnownTorsoPosition = Vector3.new()
local function onCharacterAdded(character)
local humanoid = character:WaitForChild("Humanoid")
humanoid.StateChanged:connect(function(oldState, newState)
local camera = workspace.CurrentCamera
if not camera or camera.CameraSubject ~= humanoid or not VRService.VREnabled then
return
end
if newState == Enum.HumanoidStateType.Jumping then
isJumping = true
elseif newState == Enum.HumanoidStateType.Landed then
if snapPendingWhileJumping then
forceSnapTo(nil)
snapPendingWhileJumping = false
end
isJumping = false
elseif newState == Enum.HumanoidStateType.Dead then
forceSnapTo(nil)
elseif newState == Enum.HumanoidStateType.Swimming then
if isJumping and snapPendingWhileJumping then
--Jumped into water and let go of controls during flight, treat as a normal landing
forceSnapTo(nil)
snapPendingWhileJumping = false
end
isJumping = false
end
end)
local humanoidRootPart = humanoid.Torso
humanoidRootPart.Changed:connect(function(property)
local camera = workspace.CurrentCamera
local cameraSubject = camera.CameraSubject
if camera and cameraSubject == humanoid and property == "CFrame" or property == "Position" then
if (humanoidRootPart.Position - lastKnownTorsoPosition).magnitude > 5 then
forceSnapTo(nil)
end
end
end)
end
if LocalPlayer.Character then
onCharacterAdded(LocalPlayer.Character)
end
LocalPlayer.CharacterAdded:connect(onCharacterAdded)
spawn(function()
local rootCamera = script.Parent
if not rootCamera then return end
local cameraScript = rootCamera.Parent
if not cameraScript then return end
local playerScripts = cameraScript.Parent
if not playerScripts then return end
local controlScript = playerScripts:WaitForChild("ControlScript")
local masterControlModule = controlScript:WaitForChild("MasterControl")
local vrNavigationModule = masterControlModule:WaitForChild("VRNavigation")
local movementUpdateEvent = vrNavigationModule:WaitForChild("MovementUpdate")
movementUpdateEventConn = movementUpdateEvent.Event:connect(onMovementUpdateEvent)
end)
local onCameraSubjectChangedConn = nil
local function onCameraSubjectChanged()
local camera = workspace.CurrentCamera
if camera and camera.CameraSubject then
delay(1, function () forceSnap = true end)
end
end
local function onCurrentCameraChanged()
if onCameraSubjectChangedConn then
onCameraSubjectChangedConn:disconnect()
onCameraSubjectChangedConn = nil
end
if workspace.CurrentCamera then
onCameraSubjectChangedConn = workspace.CurrentCamera:GetPropertyChangedSignal("CameraSubject"):connect(onCameraSubjectChanged)
onCameraSubjectChanged()
end
end
workspace:GetPropertyChangedSignal("CurrentCamera"):connect(onCurrentCameraChanged)
onCurrentCameraChanged()
local function onVREnabled()
bindActions(VRService.VREnabled)
end
VRService:GetPropertyChangedSignal("VREnabled"):connect(onVREnabled)
function module:Update()
local now = tick()
local timeDelta = now - lastUpdate
local camera = workspace.CurrentCamera
local cameraSubject = camera and camera.CameraSubject
local player = PlayersService.LocalPlayer
local subjectPosition = self:GetSubjectPosition()
local zoom = self:GetCameraZoom()
local subjectPosition = self:GetSubjectPosition()
local gamepadRotation = self:UpdateGamepad()
if subjectPosition and currentDestination then
local dist = (currentDestination - subjectPosition).magnitude
if dist < 10 then
forceSnap = true
forceSnapPoint = currentDestination
currentDestination = nil
end
end
if cameraSubject and cameraSubject:IsA("Humanoid") then
local rootPart = cameraSubject.RootPart
if rootPart then
lastKnownTorsoPosition = rootPart.Position
end
end
local look = lastLook
if subjectPosition and forceSnap then
forceSnap = false
local newFocusPoint = subjectPosition
if forceSnapPoint then
newFocusPoint = Vector3.new(forceSnapPoint.X, subjectPosition.Y, forceSnapPoint.Z)
end
camera.Focus = CFrame.new(newFocusPoint)
forceSnapPoint = nil
look = camera:GetRenderCFrame().lookVector
snappedHeadOffset = VRService:GetUserCFrame(Enum.UserCFrame.Head).p
end
if subjectPosition and player and camera then
local cameraFocusP = camera.Focus.p
self.RotateInput = ZERO_VECTOR2
look = (look * XZ_VECTOR).unit
camera.CFrame = CFrame.new(cameraFocusP - (HORZ_OFFSET * look)) + Vector3.new(0, HEIGHT_OFFSET, 0) - snappedHeadOffset
self.LastCameraTransform = camera.CFrame
self.LastCameraFocus = camera.Focus
end
lastLook = look
lastUpdate = now
end
return module
end
return CreateVRCamera
@@ -0,0 +1,63 @@
local PlayersService = game:GetService('Players')
local RootCameraCreator = require(script.Parent)
local ZERO_VECTOR2 = Vector2.new(0, 0)
local CFrame_new = CFrame.new
local function CreateWatchCamera()
local module = RootCameraCreator()
module.PanEnabled = false
local lastUpdate = tick()
function module:Update()
local now = tick()
local camera = workspace.CurrentCamera
local player = PlayersService.LocalPlayer
if lastUpdate == nil or now - lastUpdate > 1 then
module:ResetCameraLook()
self.LastZoom = nil
end
local subjectPosition = self:GetSubjectPosition()
if subjectPosition and player and camera then
local cameraLook = nil
local humanoid = self:GetHumanoid()
if humanoid and humanoid.Torso then
-- TODO: let the paging buttons move the camera but not the mouse/touch
-- currently neither do
local diffVector = subjectPosition - camera.CFrame.p
cameraLook = diffVector.unit
if self.LastZoom and self.LastZoom == self:GetCameraZoom() then
-- Don't clobber the zoom if they zoomed the camera
local zoom = diffVector.magnitude
self:ZoomCamera(zoom)
end
end
local zoom = self:GetCameraZoom()
if zoom <= 0 then
zoom = 0.1
end
local newLookVector = self:RotateVector(cameraLook or self:GetCameraLook(), self.RotateInput)
self.RotateInput = ZERO_VECTOR2
local newFocus = CFrame_new(subjectPosition)
local newCamCFrame = CFrame_new(newFocus.p - (zoom * newLookVector), subjectPosition)
camera.Focus = newFocus
camera.CFrame = newCamCFrame
self.LastZoom = zoom
end
lastUpdate = now
end
return module
end
return CreateWatchCamera
@@ -0,0 +1,206 @@
--[[
// FileName: ShiftLockController
// Written by: jmargh
// Version 1.2
// Description: Manages the state of shift lock mode
// Required by:
RootCamera
// Note: ContextActionService sinks keys, so until we allow binding to ContextActionService without sinking
// keys, this module will use UserInputService.
--]]
local Players = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
-- Settings and GameSettings are read only
local Settings = UserSettings() -- ignore warning
local GameSettings = Settings.GameSettings
local ShiftLockController = {}
--[[ Script Variables ]]--
while not Players.LocalPlayer do
Players.PlayerAdded:wait()
end
local LocalPlayer = Players.LocalPlayer
local Mouse = LocalPlayer:GetMouse()
local PlayerGui = LocalPlayer:WaitForChild('PlayerGui')
local ScreenGui = nil
local ShiftLockIcon = nil
local InputCn = nil
local IsShiftLockMode = false
local IsShiftLocked = false
local IsActionBound = false
local IsInFirstPerson = false
-- Toggle Event
ShiftLockController.OnShiftLockToggled = Instance.new('BindableEvent')
-- wrapping long conditional in function
local function isShiftLockMode()
return LocalPlayer.DevEnableMouseLock and GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch and
LocalPlayer.DevComputerMovementMode ~= Enum.DevComputerMovementMode.ClickToMove and
GameSettings.ComputerMovementMode ~= Enum.ComputerMovementMode.ClickToMove and
LocalPlayer.DevComputerMovementMode ~= Enum.DevComputerMovementMode.Scriptable
end
if not UserInputService.TouchEnabled then -- TODO: Remove when safe on mobile
IsShiftLockMode = isShiftLockMode()
end
--[[ Constants ]]--
local SHIFT_LOCK_OFF = 'rbxasset://textures/ui/mouseLock_off.png'
local SHIFT_LOCK_ON = 'rbxasset://textures/ui/mouseLock_on.png'
local SHIFT_LOCK_CURSOR = 'rbxasset://textures/MouseLockedCursor.png'
--[[ Local Functions ]]--
local function onShiftLockToggled()
IsShiftLocked = not IsShiftLocked
if IsShiftLocked then
ShiftLockIcon.Image = SHIFT_LOCK_ON
Mouse.Icon = SHIFT_LOCK_CURSOR
else
ShiftLockIcon.Image = SHIFT_LOCK_OFF
Mouse.Icon = ""
end
ShiftLockController.OnShiftLockToggled:Fire()
end
local function initialize()
if ScreenGui then
ScreenGui:Destroy()
ScreenGui = nil
end
ScreenGui = Instance.new('ScreenGui')
ScreenGui.Name = "ControlGui"
local frame = Instance.new('Frame')
frame.Name = "BottomLeftControl"
frame.Size = UDim2.new(0, 130, 0, 46)
frame.Position = UDim2.new(0, 0, 1, -46)
frame.BackgroundTransparency = 1
frame.Parent = ScreenGui
ShiftLockIcon = Instance.new('ImageButton')
ShiftLockIcon.Name = "MouseLockLabel"
ShiftLockIcon.Size = UDim2.new(0, 31, 0, 31)
ShiftLockIcon.Position = UDim2.new(0, 12, 0, 2)
ShiftLockIcon.BackgroundTransparency = 1
ShiftLockIcon.Image = IsShiftLocked and SHIFT_LOCK_ON or SHIFT_LOCK_OFF
ShiftLockIcon.Visible = true
ShiftLockIcon.Parent = frame
ShiftLockIcon.MouseButton1Click:connect(onShiftLockToggled)
ScreenGui.Parent = IsShiftLockMode and PlayerGui or nil
end
--[[ Public API ]]--
function ShiftLockController:IsShiftLocked()
return IsShiftLockMode and IsShiftLocked
end
function ShiftLockController:SetIsInFirstPerson(isInFirstPerson)
IsInFirstPerson = isInFirstPerson
end
--[[ Input/Settings Changed Events ]]--
local mouseLockSwitchFunc = function(actionName, inputState, inputObject)
if IsShiftLockMode then
onShiftLockToggled()
end
end
local function disableShiftLock()
if ScreenGui then ScreenGui.Parent = nil end
IsShiftLockMode = false
Mouse.Icon = ""
if InputCn then
InputCn:disconnect()
InputCn = nil
end
IsActionBound = false
ShiftLockController.OnShiftLockToggled:Fire()
end
-- TODO: Remove when we figure out ContextActionService without sinking keys
local function onShiftInputBegan(inputObject, isProcessed)
if isProcessed then return end
if inputObject.UserInputType == Enum.UserInputType.Keyboard and
(inputObject.KeyCode == Enum.KeyCode.LeftShift or inputObject.KeyCode == Enum.KeyCode.RightShift) then
--
mouseLockSwitchFunc()
end
end
local function enableShiftLock()
IsShiftLockMode = isShiftLockMode()
if IsShiftLockMode then
if ScreenGui then
ScreenGui.Parent = PlayerGui
end
if IsShiftLocked then
Mouse.Icon = SHIFT_LOCK_CURSOR
ShiftLockController.OnShiftLockToggled:Fire()
end
if not IsActionBound then
InputCn = UserInputService.InputBegan:connect(onShiftInputBegan)
IsActionBound = true
end
end
end
GameSettings.Changed:connect(function(property)
if property == 'ControlMode' then
if GameSettings.ControlMode == Enum.ControlMode.MouseLockSwitch then
enableShiftLock()
else
disableShiftLock()
end
elseif property == 'ComputerMovementMode' then
if GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove then
disableShiftLock()
else
enableShiftLock()
end
end
end)
LocalPlayer.Changed:connect(function(property)
if property == 'DevEnableMouseLock' then
if LocalPlayer.DevEnableMouseLock then
enableShiftLock()
else
disableShiftLock()
end
elseif property == 'DevComputerMovementMode' then
if LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.ClickToMove or
LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.Scriptable then
--
disableShiftLock()
else
enableShiftLock()
end
end
end)
LocalPlayer.CharacterAdded:connect(function(character)
-- we need to recreate guis on character load
if not UserInputService.TouchEnabled then
initialize()
end
end)
--[[ Initialization ]]--
-- TODO: Remove when safe! ContextActionService crashes touch clients with tupele is 2 or more
if not UserInputService.TouchEnabled then
initialize()
if isShiftLockMode() then
InputCn = UserInputService.InputBegan:connect(onShiftInputBegan)
IsActionBound = true
end
end
return ShiftLockController
@@ -0,0 +1,190 @@
-- SolarCrane
local MAX_TWEEN_RATE = 2.8 -- per second
local function clamp(low, high, num)
return (num > high and high or num < low and low or num)
end
local math_floor = math.floor
local function Round(num, places)
local decimalPivot = 10^places
return math_floor(num * decimalPivot + 0.5) / decimalPivot
end
local function CreateTransparencyController()
local module = {}
local LastUpdate = tick()
local TransparencyDirty = false
local Enabled = false
local LastTransparency = nil
local DescendantAddedConn, DescendantRemovingConn = nil, nil
local ToolDescendantAddedConns = {}
local ToolDescendantRemovingConns = {}
local CachedParts = {}
local function HasToolAncestor(object)
if object.Parent == nil then return false end
return object.Parent:IsA('Tool') or HasToolAncestor(object.Parent)
end
local function IsValidPartToModify(part)
if part:IsA('BasePart') or part:IsA('Decal') then
return not HasToolAncestor(part)
end
return false
end
local function CachePartsRecursive(object)
if object then
if IsValidPartToModify(object) then
CachedParts[object] = true
TransparencyDirty = true
end
for _, child in pairs(object:GetChildren()) do
CachePartsRecursive(child)
end
end
end
local function TeardownTransparency()
for child, _ in pairs(CachedParts) do
child.LocalTransparencyModifier = 0
end
CachedParts = {}
TransparencyDirty = true
LastTransparency = nil
if DescendantAddedConn then
DescendantAddedConn:disconnect()
DescendantAddedConn = nil
end
if DescendantRemovingConn then
DescendantRemovingConn:disconnect()
DescendantRemovingConn = nil
end
for object, conn in pairs(ToolDescendantAddedConns) do
conn:disconnect()
ToolDescendantAddedConns[object] = nil
end
for object, conn in pairs(ToolDescendantRemovingConns) do
conn:disconnect()
ToolDescendantRemovingConns[object] = nil
end
end
local function SetupTransparency(character)
TeardownTransparency()
if DescendantAddedConn then DescendantAddedConn:disconnect() end
DescendantAddedConn = character.DescendantAdded:connect(function(object)
-- This is a part we want to invisify
if IsValidPartToModify(object) then
CachedParts[object] = true
TransparencyDirty = true
-- There is now a tool under the character
elseif object:IsA('Tool') then
if ToolDescendantAddedConns[object] then ToolDescendantAddedConns[object]:disconnect() end
ToolDescendantAddedConns[object] = object.DescendantAdded:connect(function(toolChild)
CachedParts[toolChild] = nil
if toolChild:IsA('BasePart') or toolChild:IsA('Decal') then
-- Reset the transparency
toolChild.LocalTransparencyModifier = 0
end
end)
if ToolDescendantRemovingConns[object] then ToolDescendantRemovingConns[object]:disconnect() end
ToolDescendantRemovingConns[object] = object.DescendantRemoving:connect(function(formerToolChild)
wait() -- wait for new parent
if character and formerToolChild and formerToolChild:IsDescendantOf(character) then
if IsValidPartToModify(formerToolChild) then
CachedParts[formerToolChild] = true
TransparencyDirty = true
end
end
end)
end
end)
if DescendantRemovingConn then DescendantRemovingConn:disconnect() end
DescendantRemovingConn = character.DescendantRemoving:connect(function(object)
if CachedParts[object] then
CachedParts[object] = nil
-- Reset the transparency
object.LocalTransparencyModifier = 0
end
end)
CachePartsRecursive(character)
end
function module:SetEnabled(newState)
if Enabled ~= newState then
Enabled = newState
self:Update()
end
end
function module: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
SetupTransparency(character)
else
TeardownTransparency()
end
end
function module:Update()
local instant = false
local now = tick()
local currentCamera = workspace.CurrentCamera
if currentCamera then
local transparency = 0
if not Enabled then
instant = true
else
local distance = (currentCamera.Focus.p - currentCamera.CoordinateFrame.p).magnitude
transparency = (7 - distance) / 5
if transparency < 0.5 then
transparency = 0
end
if LastTransparency then
local deltaTransparency = transparency - LastTransparency
-- Don't tween transparency if it is instant or your character was fully invisible last frame
if not instant and transparency < 1 and LastTransparency < 0.95 then
local maxDelta = MAX_TWEEN_RATE * (now - LastUpdate)
deltaTransparency = clamp(-maxDelta, maxDelta, deltaTransparency)
end
transparency = LastTransparency + deltaTransparency
else
TransparencyDirty = true
end
transparency = clamp(0, 1, Round(transparency, 2))
end
if TransparencyDirty or LastTransparency ~= transparency then
for child, _ in pairs(CachedParts) do
child.LocalTransparencyModifier = transparency
end
TransparencyDirty = false
LastTransparency = transparency
end
end
LastUpdate = now
end
return module
end
return CreateTransparencyController