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,98 @@
--[[
Author: @spotco
This script creates sounds which are placed under the character head.
These sounds are used by the "LocalSound" script.
To modify this script, copy it to your "StarterPlayer/StarterCharacterScripts" folder keeping the same script name ("Sound").
The default Sound script loaded for every character will then be replaced with your copy of the script.
]]--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local SOUND_EVENT_FOLDER_NAME = "DefaultSoundEvents"
local DEFAULT_SERVER_SOUND_EVENT_NAME = "DefaultServerSoundEvent"
local SoundEventFolder = ReplicatedStorage:FindFirstChild(SOUND_EVENT_FOLDER_NAME)
local DefaultServerSoundEvent = nil
local useSoundDispatcher = UserSettings():IsUserFeatureEnabled("UserUseSoundDispatcher")
if useSoundDispatcher then
if not SoundEventFolder then
SoundEventFolder = Instance.new("Folder")
SoundEventFolder.Name = SOUND_EVENT_FOLDER_NAME
SoundEventFolder.Archivable = false
SoundEventFolder.Parent = ReplicatedStorage
end
DefaultServerSoundEvent = SoundEventFolder:FindFirstChild(DEFAULT_SERVER_SOUND_EVENT_NAME)
else
DefaultServerSoundEvent = ReplicatedStorage:FindFirstChild(DEFAULT_SERVER_SOUND_EVENT_NAME)
end
if not DefaultServerSoundEvent then
if useSoundDispatcher then
DefaultServerSoundEvent = Instance.new("RemoteEvent", SoundEventFolder)
else
DefaultServerSoundEvent = Instance.new("RemoteEvent", ReplicatedStorage)
end
DefaultServerSoundEvent.Name = DEFAULT_SERVER_SOUND_EVENT_NAME
DefaultServerSoundEvent.OnServerEvent:Connect(function() end)
end
local function CreateNewSound(name, id, looped, pitch, parent)
local sound = Instance.new("Sound")
sound.SoundId = id
sound.Name = name
sound.archivable = false
sound.Pitch = pitch
sound.Looped = looped
sound.MinDistance = 5
sound.MaxDistance = 150
sound.Volume = 0.65
sound.Parent = parent
if DefaultServerSoundEvent then
local CharacterSoundEvent = Instance.new("RemoteEvent", sound)
CharacterSoundEvent.Name = "CharacterSoundEvent"
CharacterSoundEvent.OnServerEvent:Connect(function(player, playing, resetPosition)
if type(playing) ~= "boolean" then
return
end
if type(resetPosition) ~= "boolean" then
return
end
if player.Character ~= script.Parent then
return
end
for _, p in pairs(Players:GetPlayers()) do
if p ~= player then
-- Connect to the dispatcher to check if the player has loaded.
if useSoundDispatcher then
SoundEventFolder:FindFirstChild("SoundDispatcher"):Fire(p, sound, playing, resetPosition)
else
DefaultServerSoundEvent:FireClient(p, sound, playing, resetPosition)
end
end
end
end)
end
return sound
end
local head = script.Parent:FindFirstChild("Head")
if not head then
error("Sound script parent has no child Head.")
return
end
CreateNewSound("GettingUp", "rbxasset://sounds/action_get_up.mp3", false, 1, head)
CreateNewSound("Died", "rbxasset://sounds/uuhhh.mp3", false, 1, head)
CreateNewSound("FreeFalling", "rbxasset://sounds/action_falling.mp3", true, 1, head)
CreateNewSound("Jumping", "rbxasset://sounds/action_jump.mp3", false, 1, head)
CreateNewSound("Landing", "rbxasset://sounds/action_jump_land.mp3", false, 1, head)
CreateNewSound("Splash", "rbxasset://sounds/impact_water.mp3", false, 1, head)
CreateNewSound("Running", "rbxasset://sounds/action_footsteps_plastic.mp3", true, 1.85, head)
CreateNewSound("Swimming", "rbxasset://sounds/action_swim.mp3", true, 1.6, head)
CreateNewSound("Climbing", "rbxasset://sounds/action_footsteps_plastic.mp3", true, 1, head)
@@ -0,0 +1,427 @@
--[[
Author: @spotco
This script runs locally for the player of the given humanoid.
This script triggers humanoid sound play/pause actions locally.
The Playing/TimePosition properties of Sound objects bypass FilteringEnabled, so this triggers the sound
immediately for the player and is replicated to all other players.
This script is optimized to reduce network traffic through minimizing the amount of property replication.
]]--
--All sounds are referenced by this ID
local SFX = {
Died = 0;
Running = 1;
Swimming = 2;
Climbing = 3,
Jumping = 4;
GettingUp = 5;
FreeFalling = 6;
FallingDown = 7;
Landing = 8;
Splash = 9;
}
local useUpdatedLocalSoundFlag = UserSettings():IsUserFeatureEnabled("UserFixCharacterSoundIssues")
local Humanoid = nil
local Head = nil
--SFX ID to Sound object
local Sounds = {}
local SoundService = game:GetService("SoundService")
local soundEventFolderName = "DefaultSoundEvents"
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AddCharacterLoadedEvent = nil
local RemoveCharacterEvent = nil
local soundEventFolder = ReplicatedStorage:FindFirstChild(soundEventFolderName)
local useSoundDispatcher = UserSettings():IsUserFeatureEnabled("UserUseSoundDispatcher")
if useSoundDispatcher then
if not soundEventFolder then
soundEventFolder = Instance.new("Folder", ReplicatedStorage)
soundEventFolder.Name = soundEventFolderName
soundEventFolder.Archivable = false
end
-- Load the RemoveCharacterEvent
RemoveCharacterEvent = soundEventFolder:FindFirstChild("RemoveCharacterEvent")
if RemoveCharacterEvent == nil then
RemoveCharacterEvent = Instance.new("RemoteEvent", soundEventFolder)
RemoveCharacterEvent.Name = "RemoveCharacterEvent"
end
AddCharacterLoadedEvent = soundEventFolder:FindFirstChild("AddCharacterLoadedEvent")
if AddCharacterLoadedEvent == nil then
AddCharacterLoadedEvent = Instance.new("RemoteEvent", soundEventFolder)
AddCharacterLoadedEvent.Name = "AddCharacterLoadedEvent"
end
-- Notify the server a new character has been loaded
AddCharacterLoadedEvent:FireServer()
-- Notify the sound dispatcher this character has left.
game.Players.LocalPlayer.CharacterRemoving:connect(function(character)
RemoveCharacterEvent:FireServer(game.Players.LocalPlayer)
end)
end
do
local Figure = script.Parent.Parent
Head = Figure:WaitForChild("Head")
while not Humanoid do
for _,NewHumanoid in pairs(Figure:GetChildren()) do
if NewHumanoid:IsA("Humanoid") then
Humanoid = NewHumanoid
break
end
end
if Humanoid then break end
Figure.ChildAdded:wait()
end
Sounds[SFX.Died] = Head:WaitForChild("Died")
Sounds[SFX.Running] = Head:WaitForChild("Running")
Sounds[SFX.Swimming] = Head:WaitForChild("Swimming")
Sounds[SFX.Climbing] = Head:WaitForChild("Climbing")
Sounds[SFX.Jumping] = Head:WaitForChild("Jumping")
Sounds[SFX.GettingUp] = Head:WaitForChild("GettingUp")
Sounds[SFX.FreeFalling] = Head:WaitForChild("FreeFalling")
Sounds[SFX.Landing] = Head:WaitForChild("Landing")
Sounds[SFX.Splash] = Head:WaitForChild("Splash")
local DefaultServerSoundEvent = nil
if useSoundDispatcher then
DefaultServerSoundEvent = soundEventFolder:FindFirstChild("DefaultServerSoundEvent")
else
DefaultServerSoundEvent = game:GetService("ReplicatedStorage"):FindFirstChild("DefaultServerSoundEvent")
end
if DefaultServerSoundEvent then
DefaultServerSoundEvent.OnClientEvent:connect(function(sound, playing, resetPosition)
if resetPosition and sound.TimePosition ~= 0 then
sound.TimePosition = 0
end
if sound.IsPlaying ~= playing then
sound.Playing = playing
end
end)
end
end
local IsSoundFilteringEnabled = function()
return game.Workspace.FilteringEnabled and SoundService.RespectFilteringEnabled
end
local Util
Util = {
--Define linear relationship between (pt1x,pt2x) and (pt2x,pt2y). Evaluate this at x.
YForLineGivenXAndTwoPts = function(x,pt1x,pt1y,pt2x,pt2y)
--(y - y1)/(x - x1) = m
local m = (pt1y - pt2y) / (pt1x - pt2x)
--float b = pt1.y - m * pt1.x;
local b = (pt1y - m * pt1x)
return m * x + b
end;
--Clamps the value of "val" between the "min" and "max"
Clamp = function(val,min,max)
return math.min(max,math.max(min,val))
end;
--Gets the horizontal (x,z) velocity magnitude of the given part
HorizontalSpeed = function(Head)
local hVel = Head.Velocity + Vector3.new(0,-Head.Velocity.Y,0)
return hVel.magnitude
end;
--Gets the vertical (y) velocity magnitude of the given part
VerticalSpeed = function(Head)
return math.abs(Head.Velocity.Y)
end;
--Setting Playing/TimePosition values directly result in less network traffic than Play/Pause/Resume/Stop
--If these properties are enabled, use them.
Play = function(sound)
if IsSoundFilteringEnabled() then
sound.CharacterSoundEvent:FireServer(true, true)
end
if sound.TimePosition ~= 0 then
sound.TimePosition = 0
end
if not sound.IsPlaying then
sound.Playing = true
end
end;
Pause = function(sound)
if IsSoundFilteringEnabled() then
sound.CharacterSoundEvent:FireServer(false, false)
end
if sound.IsPlaying then
sound.Playing = false
end
end;
Resume = function(sound)
if IsSoundFilteringEnabled() then
sound.CharacterSoundEvent:FireServer(true, false)
end
if not sound.IsPlaying then
sound.Playing = true
end
end;
Stop = function(sound)
if IsSoundFilteringEnabled() then
sound.CharacterSoundEvent:FireServer(false, true)
end
if sound.IsPlaying then
sound.Playing = false
end
if sound.TimePosition ~= 0 then
sound.TimePosition = 0
end
end;
}
do
-- List of all active Looped sounds
local playingLoopedSounds = {}
-- Last seen Enum.HumanoidStateType
local activeState = nil
local fallSpeed = 0
-- Verify and set that "sound" is in "playingLoopedSounds".
function setSoundInPlayingLoopedSounds(sound)
for i=1, #playingLoopedSounds do
if playingLoopedSounds[i] == sound then
return
end
end
table.insert(playingLoopedSounds,sound)
end
-- Stop all active looped sounds except parameter "except". If "except" is not passed, all looped sounds will be stopped.
function stopPlayingLoopedSoundsExcept(except)
for i=#playingLoopedSounds,1,-1 do
if playingLoopedSounds[i] ~= except then
Util.Pause(playingLoopedSounds[i])
table.remove(playingLoopedSounds,i)
end
end
end
-- Table of Enum.HumanoidStateType to handling function
local stateUpdateHandler = {
[Enum.HumanoidStateType.Dead] = function()
stopPlayingLoopedSoundsExcept()
local sound = Sounds[SFX.Died]
Util.Play(sound)
end;
[Enum.HumanoidStateType.RunningNoPhysics] = function(speed)
stateUpdated(Enum.HumanoidStateType.Running, speed)
end;
[Enum.HumanoidStateType.Running] = function(speed)
local sound = Sounds[SFX.Running]
stopPlayingLoopedSoundsExcept(sound)
if(useUpdatedLocalSoundFlag and activeState == Enum.HumanoidStateType.Freefall and fallSpeed > 0.1) then
-- Play a landing sound if the character dropped from a large distance
local vol = math.min(1.0, math.max(0.0, (fallSpeed - 50) / 110))
local freeFallSound = Sounds[SFX.FreeFalling]
freeFallSound.Volume = vol
Util.Play(freeFallSound)
fallSpeed = 0
end
if useUpdatedLocalSoundFlag then
if speed ~= nil and speed > 0.5 then
Util.Resume(sound)
setSoundInPlayingLoopedSounds(sound)
elseif speed ~= nil then
stopPlayingLoopedSoundsExcept()
end
else
if Util.HorizontalSpeed(Head) > 0.5 then
Util.Resume(sound)
setSoundInPlayingLoopedSounds(sound)
else
stopPlayingLoopedSoundsExcept()
end
end
end;
[Enum.HumanoidStateType.Swimming] = function(speed)
local threshold
if useUpdatedLocalSoundFlag then threshold = speed else threshold = Util.VerticalSpeed(Head) end
if activeState ~= Enum.HumanoidStateType.Swimming and threshold > 0.1 then
local splashSound = Sounds[SFX.Splash]
splashSound.Volume = Util.Clamp(
Util.YForLineGivenXAndTwoPts(
Util.VerticalSpeed(Head),
100, 0.28,
350, 1),
0,1)
Util.Play(splashSound)
end
do
local sound = Sounds[SFX.Swimming]
stopPlayingLoopedSoundsExcept(sound)
Util.Resume(sound)
setSoundInPlayingLoopedSounds(sound)
end
end;
[Enum.HumanoidStateType.Climbing] = function(speed)
local sound = Sounds[SFX.Climbing]
if useUpdatedLocalSoundFlag then
if speed ~= nil and math.abs(speed) > 0.1 then
Util.Resume(sound)
stopPlayingLoopedSoundsExcept(sound)
else
Util.Pause(sound)
stopPlayingLoopedSoundsExcept(sound)
end
else
if Util.VerticalSpeed(Head) > 0.1 then
Util.Resume(sound)
stopPlayingLoopedSoundsExcept(sound)
else
stopPlayingLoopedSoundsExcept()
end
end
setSoundInPlayingLoopedSounds(sound)
end;
[Enum.HumanoidStateType.Jumping] = function()
if activeState == Enum.HumanoidStateType.Jumping then
return
end
stopPlayingLoopedSoundsExcept()
local sound = Sounds[SFX.Jumping]
Util.Play(sound)
end;
[Enum.HumanoidStateType.GettingUp] = function()
stopPlayingLoopedSoundsExcept()
local sound = Sounds[SFX.GettingUp]
Util.Play(sound)
end;
[Enum.HumanoidStateType.Freefall] = function()
if activeState == Enum.HumanoidStateType.Freefall then
return
end
local sound = Sounds[SFX.FreeFalling]
sound.Volume = 0
stopPlayingLoopedSoundsExcept()
fallSpeed = math.max(fallSpeed, math.abs(Head.Velocity.y))
end;
[Enum.HumanoidStateType.FallingDown] = function()
stopPlayingLoopedSoundsExcept()
end;
[Enum.HumanoidStateType.Landed] = function()
stopPlayingLoopedSoundsExcept()
if Util.VerticalSpeed(Head) > 75 then
local landingSound = Sounds[SFX.Landing]
landingSound.Volume = Util.Clamp(
Util.YForLineGivenXAndTwoPts(
Util.VerticalSpeed(Head),
50, 0,
100, 1),
0,1)
Util.Play(landingSound)
end
end;
[Enum.HumanoidStateType.Seated] = function()
stopPlayingLoopedSoundsExcept()
end;
}
-- Handle state event fired or OnChange fired
function stateUpdated(state, speed)
if stateUpdateHandler[state] ~= nil then
if useUpdatedLocalSoundFlag and (state == Enum.HumanoidStateType.Running
or state == Enum.HumanoidStateType.Climbing
or state == Enum.HumanoidStateType.Swimming
or state == Enum.HumanoidStateType.RunningNoPhysics) then
stateUpdateHandler[state](speed)
else
stateUpdateHandler[state]()
end
end
activeState = state
end
Humanoid.Died:connect( function() stateUpdated(Enum.HumanoidStateType.Dead) end)
Humanoid.Running:connect( function(speed) stateUpdated(Enum.HumanoidStateType.Running, speed) end)
Humanoid.Swimming:connect( function(speed) stateUpdated(Enum.HumanoidStateType.Swimming, speed) end)
Humanoid.Climbing:connect( function(speed) stateUpdated(Enum.HumanoidStateType.Climbing, speed) end)
Humanoid.Jumping:connect( function() stateUpdated(Enum.HumanoidStateType.Jumping) end)
Humanoid.GettingUp:connect( function() stateUpdated(Enum.HumanoidStateType.GettingUp) end)
Humanoid.FreeFalling:connect( function() stateUpdated(Enum.HumanoidStateType.Freefall) end)
Humanoid.FallingDown:connect( function() stateUpdated(Enum.HumanoidStateType.FallingDown) end)
-- required for proper handling of Landed event
Humanoid.StateChanged:connect(function(old, new)
stateUpdated(new)
end)
function onUpdate(stepDeltaSeconds, tickSpeedSeconds)
local stepScale = stepDeltaSeconds / tickSpeedSeconds
do
local sound = Sounds[SFX.FreeFalling]
if activeState == Enum.HumanoidStateType.Freefall then
if Head.Velocity.Y < 0 and Util.VerticalSpeed(Head) > 75 then
Util.Resume(sound)
--Volume takes 1.1 seconds to go from volume 0 to 1
local ANIMATION_LENGTH_SECONDS = 1.1
local normalizedIncrement = tickSpeedSeconds / ANIMATION_LENGTH_SECONDS
sound.Volume = Util.Clamp(sound.Volume + normalizedIncrement * stepScale, 0, 1)
else
sound.Volume = 0
end
else
Util.Pause(sound)
end
end
do
local sound = Sounds[SFX.Running]
if activeState == Enum.HumanoidStateType.Running then
if Util.HorizontalSpeed(Head) < 0.5 then
Util.Pause(sound)
end
end
end
end
local lastTick = tick()
local TICK_SPEED_SECONDS = 0.25
while true do
onUpdate(tick() - lastTick,TICK_SPEED_SECONDS)
lastTick = tick()
wait(TICK_SPEED_SECONDS)
end
end
@@ -0,0 +1,320 @@
local RunService = game:GetService('RunService')
local UserInputService = game:GetService('UserInputService')
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local StarterPlayer = game:GetService('StarterPlayer')
local RootCamera = script:WaitForChild('RootCamera')
local AttachCamera = require(RootCamera:WaitForChild('AttachCamera'))()
local FixedCamera = require(RootCamera:WaitForChild('FixedCamera'))()
local ScriptableCamera = require(RootCamera:WaitForChild('ScriptableCamera'))()
local TrackCamera = require(RootCamera:WaitForChild('TrackCamera'))()
local WatchCamera = require(RootCamera:WaitForChild('WatchCamera'))()
local OrbitalCamera = require(RootCamera:WaitForChild('OrbitalCamera'))()
local ClassicCamera = require(RootCamera:WaitForChild('ClassicCamera'))()
local FollowCamera = require(RootCamera:WaitForChild('FollowCamera'))()
local PopperCam = require(script:WaitForChild('PopperCam'))
local Invisicam = require(script:WaitForChild('Invisicam'))
local TransparencyController = require(script:WaitForChild('TransparencyController'))()
local VRCamera = require(RootCamera:WaitForChild("VRCamera"))()
local GameSettings = UserSettings().GameSettings
local AllCamerasInLua = false
local success, msg = pcall(function()
AllCamerasInLua = UserSettings():IsUserFeatureEnabled("UserAllCamerasInLua")
end)
if not success then
print("Couldn't get feature UserAllCamerasInLua because:" , msg)
end
local FFlagUserNoCameraClickToMoveSuccess, FFlagUserNoCameraClickToMoveResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoCameraClickToMove") end)
local FFlagUserNoCameraClickToMove = FFlagUserNoCameraClickToMoveSuccess and FFlagUserNoCameraClickToMoveResult
local ClickToMove = FFlagUserNoCameraClickToMove and nil or require(script:WaitForChild('ClickToMove'))()
local isOrbitalCameraEnabled = pcall(function() local test = Enum.CameraType.Orbital end)
-- register what camera scripts we are using
do
local PlayerScripts = PlayersService.LocalPlayer:WaitForChild("PlayerScripts")
local canRegisterCameras = pcall(function() PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default) end)
if canRegisterCameras then
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)
end
end
local CameraTypeEnumMap =
{
[Enum.CameraType.Attach] = AttachCamera;
[Enum.CameraType.Fixed] = FixedCamera;
[Enum.CameraType.Scriptable] = ScriptableCamera;
[Enum.CameraType.Track] = TrackCamera;
[Enum.CameraType.Watch] = WatchCamera;
[Enum.CameraType.Follow] = FollowCamera;
}
if isOrbitalCameraEnabled then
CameraTypeEnumMap[Enum.CameraType.Orbital] = OrbitalCamera;
end
local EnabledCamera = nil
local EnabledOcclusion = nil
local cameraSubjectChangedConn = nil
local cameraTypeChangedConn = nil
local renderSteppedConn = nil
local lastInputType = nil
local hasLastInput = false
local function IsTouch()
return UserInputService.TouchEnabled
end
local function shouldUsePlayerScriptsCamera()
local player = PlayersService.LocalPlayer
local currentCamera = workspace.CurrentCamera
if AllCamerasInLua then
return true
else
if player then
if currentCamera == nil or (currentCamera.CameraType == Enum.CameraType.Custom)
or (isOrbitalCameraEnabled and currentCamera.CameraType == Enum.CameraType.Orbital) then
return true
end
end
end
return false
end
local function isClickToMoveOn()
local usePlayerScripts = shouldUsePlayerScriptsCamera()
local player = PlayersService.LocalPlayer
if usePlayerScripts and player then
if (hasLastInput and lastInputType == Enum.UserInputType.Touch) or IsTouch() then -- Touch
if player.DevTouchMovementMode == Enum.DevTouchMovementMode.ClickToMove or
(player.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice and GameSettings.TouchMovementMode == Enum.TouchMovementMode.ClickToMove) then
return true
end
else -- Computer
if player.DevComputerMovementMode == Enum.DevComputerMovementMode.ClickToMove or
(player.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice and GameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove) then
return true
end
end
end
return false
end
local function getCurrentCameraMode()
local usePlayerScripts = shouldUsePlayerScriptsCamera()
local player = PlayersService.LocalPlayer
if usePlayerScripts and player then
if (hasLastInput and lastInputType == Enum.UserInputType.Touch) or IsTouch() then -- Touch (iPad, etc...)
if not FFlagUserNoCameraClickToMove and isClickToMoveOn() then
return Enum.DevTouchMovementMode.ClickToMove.Name
elseif player.DevTouchCameraMode == Enum.DevTouchCameraMovementMode.UserChoice then
local touchMovementMode = GameSettings.TouchCameraMovementMode
if touchMovementMode == Enum.TouchCameraMovementMode.Default then
return Enum.TouchCameraMovementMode.Follow.Name
end
return touchMovementMode.Name
else
return player.DevTouchCameraMode.Name
end
else -- Computer
if not FFlagUserNoCameraClickToMove and isClickToMoveOn() then
return Enum.DevComputerMovementMode.ClickToMove.Name
elseif player.DevComputerCameraMode == Enum.DevComputerCameraMovementMode.UserChoice then
local computerMovementMode = GameSettings.ComputerCameraMovementMode
if computerMovementMode == Enum.ComputerCameraMovementMode.Default then
return Enum.ComputerCameraMovementMode.Classic.Name
end
return computerMovementMode.Name
else
return player.DevComputerCameraMode.Name
end
end
end
end
local function getCameraOcclusionMode()
local usePlayerScripts = shouldUsePlayerScriptsCamera()
local player = PlayersService.LocalPlayer
if usePlayerScripts and player then
return player.DevCameraOcclusionMode
end
end
-- New for AllCameraInLua support
local function shouldUseOcclusionModule()
local player = PlayersService.LocalPlayer
if player and game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CameraType == Enum.CameraType.Custom then
return true
end
return false
end
local function Update()
if EnabledCamera then
EnabledCamera:Update()
end
if EnabledOcclusion and not VRService.VREnabled then
EnabledOcclusion:Update(EnabledCamera)
end
if shouldUsePlayerScriptsCamera() then
TransparencyController:Update()
end
end
local function SetEnabledCamera(newCamera)
if EnabledCamera ~= newCamera then
if EnabledCamera then
EnabledCamera:SetEnabled(false)
end
EnabledCamera = newCamera
if EnabledCamera then
EnabledCamera:SetEnabled(true)
end
end
end
local function OnCameraMovementModeChange(newCameraMode)
if newCameraMode == Enum.DevComputerMovementMode.ClickToMove.Name then
if FFlagUserNoCameraClickToMove then
--No longer responding to ClickToMove here!
return
end
ClickToMove:Start()
SetEnabledCamera(nil)
TransparencyController:SetEnabled(true)
else
local currentCameraType = workspace.CurrentCamera and workspace.CurrentCamera.CameraType
if VRService.VREnabled and currentCameraType ~= Enum.CameraType.Scriptable then
SetEnabledCamera(VRCamera)
TransparencyController:SetEnabled(false)
elseif (currentCameraType == Enum.CameraType.Custom or not AllCamerasInLua) and newCameraMode == Enum.ComputerCameraMovementMode.Classic.Name then
SetEnabledCamera(ClassicCamera)
TransparencyController:SetEnabled(true)
elseif (currentCameraType == Enum.CameraType.Custom or not AllCamerasInLua) and newCameraMode == Enum.ComputerCameraMovementMode.Follow.Name then
SetEnabledCamera(FollowCamera)
TransparencyController:SetEnabled(true)
elseif (currentCameraType == Enum.CameraType.Custom or not AllCamerasInLua) and (isOrbitalCameraEnabled and (newCameraMode == Enum.ComputerCameraMovementMode.Orbital.Name)) then
SetEnabledCamera(OrbitalCamera)
TransparencyController:SetEnabled(true)
elseif AllCamerasInLua and CameraTypeEnumMap[currentCameraType] then
SetEnabledCamera(CameraTypeEnumMap[currentCameraType])
TransparencyController:SetEnabled(false)
else -- Our camera movement code was disabled by the developer
SetEnabledCamera(nil)
TransparencyController:SetEnabled(false)
end
ClickToMove:Stop()
end
local useOcclusion = shouldUseOcclusionModule()
local newOcclusionMode = getCameraOcclusionMode()
if EnabledOcclusion == Invisicam and (newOcclusionMode ~= Enum.DevCameraOcclusionMode.Invisicam or (not useOcclusion)) then
Invisicam:Cleanup()
end
-- PopperCam does not work with OrbitalCamera, as OrbitalCamera's distance can be fixed.
if useOcclusion then
if newOcclusionMode == Enum.DevCameraOcclusionMode.Zoom and ( isOrbitalCameraEnabled and newCameraMode ~= Enum.ComputerCameraMovementMode.Orbital.Name ) then
EnabledOcclusion = PopperCam
elseif newOcclusionMode == Enum.DevCameraOcclusionMode.Invisicam then
EnabledOcclusion = Invisicam
else
EnabledOcclusion = nil
end
else
EnabledOcclusion = nil
end
end
local function OnCameraTypeChanged(newCameraType)
if newCameraType == Enum.CameraType.Scriptable then
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
end
end
local function OnCameraSubjectChanged(newSubject)
TransparencyController:SetSubject(newSubject)
end
local function OnNewCamera()
OnCameraMovementModeChange(getCurrentCameraMode())
local currentCamera = workspace.CurrentCamera
if currentCamera then
if cameraSubjectChangedConn then
cameraSubjectChangedConn:disconnect()
end
if cameraTypeChangedConn then
cameraTypeChangedConn:disconnect()
end
cameraSubjectChangedConn = currentCamera:GetPropertyChangedSignal("CameraSubject"):connect(function()
OnCameraSubjectChanged(currentCamera.CameraSubject)
end)
cameraTypeChangedConn = currentCamera:GetPropertyChangedSignal("CameraType"):connect(function()
OnCameraMovementModeChange(getCurrentCameraMode())
OnCameraTypeChanged(currentCamera.CameraType)
end)
OnCameraSubjectChanged(currentCamera.CameraSubject)
OnCameraTypeChanged(currentCamera.CameraType)
end
end
local function OnPlayerAdded(player)
workspace.Changed:connect(function(prop)
if prop == 'CurrentCamera' then
OnNewCamera()
end
end)
player.Changed:connect(function(prop)
OnCameraMovementModeChange(getCurrentCameraMode())
end)
GameSettings.Changed:connect(function(prop)
OnCameraMovementModeChange(getCurrentCameraMode())
end)
RunService:BindToRenderStep("cameraRenderUpdate", Enum.RenderPriority.Camera.Value, Update)
OnNewCamera()
OnCameraMovementModeChange(getCurrentCameraMode())
end
do
while PlayersService.LocalPlayer == nil do PlayersService.PlayerAdded:wait() end
hasLastInput = pcall(function()
lastInputType = UserInputService:GetLastInputType()
UserInputService.LastInputTypeChanged:connect(function(newLastInputType)
lastInputType = newLastInputType
end)
end)
OnPlayerAdded(PlayersService.LocalPlayer)
end
local function OnVREnabled()
OnCameraMovementModeChange(getCurrentCameraMode())
end
VRService:GetPropertyChangedSignal("VREnabled"):connect(OnVREnabled)
@@ -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,194 @@
-- 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',
}
local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix")
end)
local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled
-----------------
--| 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
if FFlagUserPortraitPopperFix then
if newSubject:IsA("BasePart") then
SubjectPart = newSubject
elseif newSubject:IsA("Model") then
SubjectPart = newSubject.PrimaryPart
elseif newSubject:IsA("Humanoid") then
SubjectPart = newSubject.Torso
end
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
if FFlagUserPortraitPopperFix and SubjectPart then
focusPoint = SubjectPart.CFrame.p
end
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,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
@@ -0,0 +1,487 @@
--[[
// FileName: ControlScript.lua
// Version 1.1
// Written by: jmargh and jeditkacheff
// Description: Manages in game controls for both touch and keyboard/mouse devices.
// This script will be inserted into PlayerScripts under each player by default. If you want to
// create your own custom controls or modify these controls, you must place a script with this
// name, ControlScript, under StarterPlayer -> PlayerScripts.
// Required Modules:
ClickToMove
DPad
KeyboardMovement
Thumbpad
Thumbstick
TouchJump
MasterControl
VehicleController
--]]
--[[ Services ]]--
local ContextActionService = game:GetService('ContextActionService')
local Players = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local VRService = game:GetService('VRService')
-- Settings and GameSettings are read only
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
-- Issue with play solo? (F6)
while not UserInputService.KeyboardEnabled and not UserInputService.TouchEnabled and not UserInputService.GamepadEnabled do
wait()
end
--[[ Script Variables ]]--
while not Players.LocalPlayer do
wait()
end
local lastInputType = nil
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild('PlayerGui')
local IsTouchDevice = UserInputService.TouchEnabled
local UserMovementMode = IsTouchDevice and GameSettings.TouchMovementMode or GameSettings.ComputerMovementMode
local DevMovementMode = IsTouchDevice and LocalPlayer.DevTouchMovementMode or LocalPlayer.DevComputerMovementMode
local IsUserChoice = (IsTouchDevice and DevMovementMode == Enum.DevTouchMovementMode.UserChoice) or (DevMovementMode == Enum.DevComputerMovementMode.UserChoice)
local TouchGui = nil
local TouchControlFrame = nil
local IsModalEnabled = UserInputService.ModalEnabled
local BindableEvent_OnFailStateChanged = nil
local isJumpEnabled = false
-- register what control scripts we are using
do
local PlayerScripts = LocalPlayer:WaitForChild("PlayerScripts")
local canRegisterControls = pcall(function() PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Default) end)
if canRegisterControls then
PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Thumbstick)
PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.DPad)
PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Thumbpad)
PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.ClickToMove)
PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.DynamicThumbstick)
PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.Default)
PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.KeyboardMouse)
PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.ClickToMove)
end
end
local DynamicThumbstickAvailable = pcall(function()
return Enum.DevTouchMovementMode.DynamicThumbstick and Enum.TouchMovementMode.DynamicThumbstick
end)
local FFlagUserNoCameraClickToMoveSuccess, FFlagUserNoCameraClickToMoveResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoCameraClickToMove") end)
local FFlagUserNoCameraClickToMove = FFlagUserNoCameraClickToMoveSuccess and FFlagUserNoCameraClickToMoveResult
--[[ Modules ]]--
local ClickToMoveTouchControls = nil
local ControlModules = {}
local ControlState = {}
ControlState.Current = nil
function ControlState:SwitchTo(newControl)
if ControlState.Current == newControl then return end
if ControlState.Current then
ControlState.Current:Disable()
end
ControlState.Current = newControl
if ControlState.Current then
ControlState.Current:Enable()
end
end
function ControlState:IsTouchJumpModuleUsed()
return isJumpEnabled
end
local MasterControl = require(script:WaitForChild('MasterControl'))
--MasterControl needs access to ControlState in order to be able to fully enable and disable control
MasterControl.ControlState = ControlState
local DynamicThumbstickModule = require(script.MasterControl:WaitForChild('DynamicThumbstick'))
local ThumbstickModule = require(script.MasterControl:WaitForChild('Thumbstick'))
local ThumbpadModule = require(script.MasterControl:WaitForChild('Thumbpad'))
local DPadModule = require(script.MasterControl:WaitForChild('DPad'))
local DefaultModule = ControlModules.Thumbstick
local TouchJumpModule = require(script.MasterControl:WaitForChild('TouchJump'))
local ClickToMoveModule = FFlagUserNoCameraClickToMove and require(script.MasterControl:WaitForChild('ClickToMoveController')) or nil
MasterControl.TouchJumpModule = TouchJumpModule
local VRNavigationModule = require(script.MasterControl:WaitForChild('VRNavigation'))
local keyboardModule = require(script.MasterControl:WaitForChild('KeyboardMovement'))
ControlModules.Gamepad = require(script.MasterControl:WaitForChild('Gamepad'))
function getTouchModule()
local module = nil
if not IsUserChoice then
if DynamicThumbstickAvailable and DevMovementMode == Enum.DevTouchMovementMode.DynamicThumbstick then
module = DynamicThumbstickModule
isJumpEnabled = true
elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbstick then
module = ThumbstickModule
isJumpEnabled = true
elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbpad then
module = ThumbpadModule
isJumpEnabled = true
elseif DevMovementMode == Enum.DevTouchMovementMode.DPad then
module = DPadModule
isJumpEnabled = false
elseif DevMovementMode == Enum.DevTouchMovementMode.ClickToMove then
if FFlagUserNoCameraClickToMove then
module = ClickToMoveModule
isJumpEnabled = false -- TODO: What should this be, true or false?
else
module = nil
end
elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
module = nil
end
else
if DynamicThumbstickAvailable and UserMovementMode == Enum.TouchMovementMode.DynamicThumbstick then
module = DynamicThumbstickModule
isJumpEnabled = true
elseif UserMovementMode == Enum.TouchMovementMode.Default or UserMovementMode == Enum.TouchMovementMode.Thumbstick then
module = ThumbstickModule
isJumpEnabled = true
elseif UserMovementMode == Enum.TouchMovementMode.Thumbpad then
module = ThumbpadModule
isJumpEnabled = true
elseif UserMovementMode == Enum.TouchMovementMode.DPad then
module = DPadModule
isJumpEnabled = false
elseif UserMovementMode == Enum.TouchMovementMode.ClickToMove then
if FFlagUserNoCameraClickToMove then
module = ClickToMoveModule
isJumpEnabled = false -- TODO: What should this be, true or false?
else
module = nil
end
end
end
return module
end
function setJumpModule(isEnabled)
if not isEnabled then
TouchJumpModule:Disable()
elseif ControlState.Current == ControlModules.Touch then
TouchJumpModule:Enable()
end
end
function setClickToMove()
if DevMovementMode == Enum.DevTouchMovementMode.ClickToMove or DevMovementMode == Enum.DevComputerMovementMode.ClickToMove or
UserMovementMode == Enum.ComputerMovementMode.ClickToMove or UserMovementMode == Enum.TouchMovementMode.ClickToMove then
if lastInputType == Enum.UserInputType.Touch then
ClickToMoveTouchControls = ControlState.Current
end
elseif ClickToMoveTouchControls then
ClickToMoveTouchControls:Disable()
ClickToMoveTouchControls = nil
end
end
ControlModules.Touch = {}
ControlModules.Touch.Current = nil
ControlModules.Touch.LocalPlayerChangedCon = nil
ControlModules.Touch.GameSettingsChangedCon = nil
function ControlModules.Touch:RefreshControlStyle()
if ControlModules.Touch.Current then
ControlModules.Touch.Current:Disable()
end
setJumpModule(false)
TouchJumpModule:Disable()
ControlModules.Touch:Enable()
end
function ControlModules.Touch:DisconnectEvents()
if ControlModules.Touch.LocalPlayerChangedCon then
ControlModules.Touch.LocalPlayerChangedCon:disconnect()
ControlModules.Touch.LocalPlayerChangedCon = nil
end
if ControlModules.Touch.GameSettingsChangedCon then
ControlModules.Touch.GameSettingsChangedCon:disconnect()
ControlModules.Touch.GameSettingsChangedCon = nil
end
end
function ControlModules.Touch:Enable()
DevMovementMode = LocalPlayer.DevTouchMovementMode
IsUserChoice = DevMovementMode == Enum.DevTouchMovementMode.UserChoice
if IsUserChoice then
UserMovementMode = GameSettings.TouchMovementMode
end
local newModuleToEnable = getTouchModule()
if newModuleToEnable then
setClickToMove()
setJumpModule(isJumpEnabled)
newModuleToEnable:Enable()
ControlModules.Touch.Current = newModuleToEnable
if isJumpEnabled then TouchJumpModule:Enable() end
end
-- This being within the above if statement was causing issues with ClickToMove, which isn't a module within this script.
ControlModules.Touch:DisconnectEvents()
ControlModules.Touch.LocalPlayerChangedCon = LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):connect(function()
ControlModules.Touch:RefreshControlStyle()
end)
ControlModules.Touch.GameSettingsChangedCon = GameSettings:GetPropertyChangedSignal("TouchMovementMode"):connect(function()
ControlModules.Touch:RefreshControlStyle()
end)
end
function ControlModules.Touch:Disable()
ControlModules.Touch:DisconnectEvents()
local newModuleToDisable = getTouchModule()
if newModuleToDisable == ThumbstickModule or
newModuleToDisable == DPadModule or
newModuleToDisable == ThumbpadModule or
newModuleToDisable == DynamicThumbstickModule then
newModuleToDisable:Disable()
setJumpModule(false)
TouchJumpModule:Disable()
end
-- UserMovementMode will still have the previous value at this point
if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
ClickToMoveModule:Disable()
end
end
local function getKeyboardModule()
-- NOTE: Click to move still uses keyboard. Leaving cases in case this ever changes.
local whichModule = nil
if not IsUserChoice then
if DevMovementMode == Enum.DevComputerMovementMode.KeyboardMouse then
whichModule = keyboardModule
elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then
whichModule = keyboardModule
end
else
if UserMovementMode == Enum.ComputerMovementMode.KeyboardMouse or UserMovementMode == Enum.ComputerMovementMode.Default then
whichModule = keyboardModule
elseif UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
whichModule = keyboardModule
end
end
return whichModule
end
ControlModules.Keyboard = {}
function ControlModules.Keyboard:RefreshControlStyle()
ControlModules.Keyboard:Disable()
ControlModules.Keyboard:Enable()
end
function ControlModules.Keyboard:Enable()
DevMovementMode = LocalPlayer.DevComputerMovementMode
IsUserChoice = DevMovementMode == Enum.DevComputerMovementMode.UserChoice
if IsUserChoice then
UserMovementMode = GameSettings.ComputerMovementMode
end
local newModuleToEnable = getKeyboardModule()
if newModuleToEnable then
newModuleToEnable:Enable()
end
if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
ClickToMoveModule:Enable()
end
ControlModules.Keyboard:DisconnectEvents()
ControlModules.Keyboard.LocalPlayerChangedCon = LocalPlayer.Changed:connect(function(property)
if property == 'DevComputerMovementMode' then
ControlModules.Keyboard:RefreshControlStyle()
end
end)
ControlModules.Keyboard.GameSettingsChangedCon = GameSettings.Changed:connect(function(property)
if property == 'ComputerMovementMode' then
ControlModules.Keyboard:RefreshControlStyle()
end
end)
end
function ControlModules.Keyboard:DisconnectEvents()
if ControlModules.Keyboard.LocalPlayerChangedCon then
ControlModules.Keyboard.LocalPlayerChangedCon:disconnect()
ControlModules.Keyboard.LocalPlayerChangedCon = nil
end
if ControlModules.Keyboard.GameSettingsChangedCon then
ControlModules.Keyboard.GameSettingsChangedCon:disconnect()
ControlModules.Keyboard.GameSettingsChangedCon = nil
end
end
function ControlModules.Keyboard:Disable()
ControlModules.Keyboard:DisconnectEvents()
local newModuleToDisable = getKeyboardModule()
if newModuleToDisable then
newModuleToDisable:Disable()
end
-- UserMovementMode will still be set to previous movement type
if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
ClickToMoveModule:Disable()
end
end
ControlModules.VRNavigation = {}
function ControlModules.VRNavigation:Enable()
VRNavigationModule:Enable()
end
function ControlModules.VRNavigation:Disable()
VRNavigationModule:Disable()
end
if not FFlagUserNoCameraClickToMove and IsTouchDevice then
BindableEvent_OnFailStateChanged = script.Parent:WaitForChild("OnClickToMoveFailStateChange")
end
-- not used, but needs to be required
local VehicleController = require(script.MasterControl:WaitForChild('VehicleController'))
--[[ Initialization/Setup ]]--
local function createTouchGuiContainer()
if TouchGui then TouchGui:Destroy() end
-- Container for all touch device guis
TouchGui = Instance.new('ScreenGui')
TouchGui.Name = "TouchGui"
TouchGui.ResetOnSpawn = false
TouchGui.Parent = PlayerGui
TouchControlFrame = Instance.new('Frame')
TouchControlFrame.Name = "TouchControlFrame"
TouchControlFrame.Size = UDim2.new(1, 0, 1, 0)
TouchControlFrame.BackgroundTransparency = 1
TouchControlFrame.Parent = TouchGui
ThumbstickModule:Create(TouchControlFrame)
DPadModule:Create(TouchControlFrame)
ThumbpadModule:Create(TouchControlFrame)
TouchJumpModule:Create(TouchControlFrame)
DynamicThumbstickModule:Create(TouchControlFrame)
end
--[[ Settings Changed Connections ]]--
LocalPlayer.Changed:connect(function(property)
if lastInputType == Enum.UserInputType.Touch and property == 'DevTouchMovementMode' then
ControlState:SwitchTo(ControlModules.Touch)
elseif UserInputService.KeyboardEnabled and property == 'DevComputerMovementMode' then
ControlState:SwitchTo(ControlModules.Keyboard)
end
end)
GameSettings.Changed:connect(function(property)
if not IsUserChoice then return end
if property == 'TouchMovementMode' or property == 'ComputerMovementMode' then
UserMovementMode = GameSettings[property]
if property == 'TouchMovementMode' then
ControlState:SwitchTo(ControlModules.Touch)
elseif property == 'ComputerMovementMode' then
ControlState:SwitchTo(ControlModules.Keyboard)
end
end
end)
--[[ Touch Events ]]--
UserInputService.Changed:connect(function(property)
if property == 'ModalEnabled' then
IsModalEnabled = UserInputService.ModalEnabled
if lastInputType == Enum.UserInputType.Touch then
if ControlState.Current == ControlModules.Touch and IsModalEnabled then
ControlState:SwitchTo(nil)
elseif ControlState.Current == nil and not IsModalEnabled then
ControlState:SwitchTo(ControlModules.Touch)
end
end
end
end)
if FFlagUserNoCameraClickToMove then
BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged()
end
if BindableEvent_OnFailStateChanged then
BindableEvent_OnFailStateChanged.Event:connect(function(isOn)
if lastInputType == Enum.UserInputType.Touch and ClickToMoveTouchControls then
if isOn then
ControlState:SwitchTo(ClickToMoveTouchControls)
else
ControlState:SwitchTo(nil)
end
end
end)
end
local switchToInputType = function(newLastInputType)
lastInputType = newLastInputType
if VRService.VREnabled then
ControlState:SwitchTo(ControlModules.VRNavigation)
return
end
if lastInputType == Enum.UserInputType.Touch then
ControlState:SwitchTo(ControlModules.Touch)
elseif lastInputType == Enum.UserInputType.Keyboard or
lastInputType == Enum.UserInputType.MouseButton1 or
lastInputType == Enum.UserInputType.MouseButton2 or
lastInputType == Enum.UserInputType.MouseButton3 or
lastInputType == Enum.UserInputType.MouseWheel or
lastInputType == Enum.UserInputType.MouseMovement then
ControlState:SwitchTo(ControlModules.Keyboard)
elseif lastInputType == Enum.UserInputType.Gamepad1 or
lastInputType == Enum.UserInputType.Gamepad2 or
lastInputType == Enum.UserInputType.Gamepad3 or
lastInputType == Enum.UserInputType.Gamepad4 then
ControlState:SwitchTo(ControlModules.Gamepad)
end
end
if IsTouchDevice then
createTouchGuiContainer()
end
MasterControl:Init()
UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
local connectedGamepads = UserInputService:GetConnectedGamepads()
if #connectedGamepads > 0 then return end
if not VRService.VREnabled then
if UserInputService.KeyboardEnabled then
ControlState:SwitchTo(ControlModules.Keyboard)
elseif IsTouchDevice then
ControlState:SwitchTo(ControlModules.Touch)
end
end
end)
UserInputService.GamepadConnected:connect(function(gamepadEnum)
if not VRService.VREnabled then
ControlState:SwitchTo(ControlModules.Gamepad)
end
end)
switchToInputType(UserInputService:GetLastInputType())
UserInputService.LastInputTypeChanged:connect(switchToInputType)
VRService:GetPropertyChangedSignal("VREnabled"):Connect(function()
if VRService.VREnabled then
ControlState:SwitchTo(ControlModules.VRNavigation)
end
end)
@@ -0,0 +1,174 @@
--[[
// FileName: MasterControl
// Version 1.0
// Written by: jeditkacheff
// Description: All character control scripts go thru this script, this script makes sure all actions are performed
--]]
-- [[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0, 0, 0)
local STATE_JUMPING = Enum.HumanoidStateType.Jumping
local STATE_FREEFALL = Enum.HumanoidStateType.Freefall
local STATE_LANDED = Enum.HumanoidStateType.Landed
--[[ Local Variables ]]--
local MasterControl = {}
local Players = game:GetService('Players')
local RunService = game:GetService('RunService')
while not Players.LocalPlayer do
Players.PlayerAdded:wait()
end
local LocalPlayer = Players.LocalPlayer
local LocalCharacter = LocalPlayer.Character
local CachedHumanoid = nil
local isJumping = false
local moveValue = Vector3.new(0, 0, 0)
local isJumpEnabled = true
local areControlsEnabled = true
local clickToMoveFailStateChanged = Instance.new("BindableEvent")
clickToMoveFailStateChanged.Name = "ClickToMoveFailStateChanged"
--[[ Local Functions ]]--
function MasterControl:GetHumanoid()
if LocalCharacter then
if CachedHumanoid then
return CachedHumanoid
else
CachedHumanoid = LocalCharacter:FindFirstChildOfClass("Humanoid")
return CachedHumanoid
end
end
end
local characterAncestryChangedConn = nil
local characterChildRemovedConn = nil
local function characterAdded(character)
if characterAncestryChangedConn then
characterAncestryChangedConn:disconnect()
end
if characterChildRemovedConn then
characterChildRemovedConn:disconnect()
end
LocalCharacter = character
CachedHumanoid = LocalCharacter:FindFirstChildOfClass("Humanoid")
characterAncestryChangedConn = character.AncestryChanged:connect(function()
if character.Parent == nil then
LocalCharacter = nil
else
LocalCharacter = character
end
end)
characterChildRemovedConn = character.ChildRemoved:connect(function(child)
if child == CachedHumanoid then
CachedHumanoid = nil
end
end)
end
if LocalCharacter then
characterAdded(LocalCharacter)
end
LocalPlayer.CharacterAdded:connect(characterAdded)
local getHumanoid = MasterControl.GetHumanoid
local moveFunc = LocalPlayer.Move
local updateMovement = function()
if not areControlsEnabled then return end
local humanoid = getHumanoid()
if not humanoid then return end
if isJumpEnabled and isJumping and not humanoid.PlatformStand then
local state = humanoid:GetState()
if state ~= STATE_JUMPING and state ~= STATE_FREEFALL and state ~= STATE_LANDED then
humanoid.Jump = isJumping
end
end
moveFunc(LocalPlayer, moveValue, true)
end
--[[ Public API ]]--
function MasterControl:Init()
RunService:BindToRenderStep("MasterControlStep", Enum.RenderPriority.Input.Value, updateMovement)
end
function MasterControl:Enable()
areControlsEnabled = true
isJumpEnabled = true
if self.ControlState.Current then
self.ControlState.Current:Enable()
end
end
function MasterControl:Disable()
if self.ControlState.Current then
self.ControlState.Current:Disable()
end
--After current control state is disabled, moveValue has been set to zero,
--Call updateMovement one last time to make sure this propagates to the engine -
--Otherwise if disabled while humanoid is moving, humanoid won't stop moving.
updateMovement()
isJumping = false
areControlsEnabled = false
end
function MasterControl:EnableJump()
isJumpEnabled = true
if areControlsEnabled and self.ControlState:IsTouchJumpModuleUsed() then
self.TouchJumpModule:Enable()
end
end
function MasterControl:DisableJump()
isJumpEnabled = false
if self.ControlState:IsTouchJumpModuleUsed() then
self.TouchJumpModule:Disable()
end
end
function MasterControl:AddToPlayerMovement(playerMoveVector)
moveValue = Vector3.new(moveValue.X + playerMoveVector.X, moveValue.Y + playerMoveVector.Y, moveValue.Z + playerMoveVector.Z)
end
function MasterControl:GetMoveVector()
return moveValue
end
function MasterControl:SetIsJumping(jumping)
if not isJumpEnabled then return end
isJumping = jumping
local humanoid = self:GetHumanoid()
if humanoid and not humanoid.PlatformStand then
humanoid.Jump = isJumping
end
end
function MasterControl:DoJump()
if not isJumpEnabled then return end
local humanoid = self:GetHumanoid()
if humanoid then
humanoid.Jump = true
end
end
function MasterControl:GetClickToMoveFailStateChanged()
return clickToMoveFailStateChanged
end
return MasterControl
@@ -0,0 +1,170 @@
--[[
// FileName: DPad
// Version 1.0
// Written by: jmargh
// Description: Implements DPad controls for touch devices
--]]
local Players = game:GetService('Players')
local GuiService = game:GetService('GuiService')
local DPad = {}
local MasterControl = require(script.Parent)
--[[ Script Variables ]]--
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local DPadFrame = nil
local TouchObject = nil
local OnInputEnded = nil -- defined in Create()
--[[ Constants ]]--
local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png"
local COMPASS_DIR = {
Vector3.new(1, 0, 0), -- E
Vector3.new(1, 0, 1).unit, -- SE
Vector3.new(0, 0, 1), -- S
Vector3.new(-1, 0, 1).unit, -- SW
Vector3.new(-1, 0, 0), -- W
Vector3.new(-1, 0, -1).unit, -- NW
Vector3.new(0, 0, -1), -- N
Vector3.new(1, 0, -1).unit, -- NE
}
--[[ lua Function Cache ]]--
local ATAN2 = math.atan2
local FLOOR = math.floor
local PI = math.pi
--[[ Local Functions ]]--
local function createArrowLabel(name, position, size, rectOffset, rectSize)
local image = Instance.new('ImageLabel')
image.Name = name
image.Image = DPAD_SHEET
image.ImageRectOffset = rectOffset
image.ImageRectSize = rectSize
image.BackgroundTransparency = 1
image.Size = size
image.Position = position
image.Parent = DPadFrame
return image
end
local function getCenterPosition()
return Vector2.new(DPadFrame.AbsolutePosition.x + DPadFrame.AbsoluteSize.x/2, DPadFrame.AbsolutePosition.y + DPadFrame.AbsoluteSize.y/2)
end
--[[ Public API ]]--
function DPad:Enable()
DPadFrame.Visible = true
end
function DPad:Disable()
DPadFrame.Visible = false
OnInputEnded()
end
function DPad:Create(parentFrame)
if DPadFrame then
DPadFrame:Destroy()
DPadFrame = nil
end
local position = UDim2.new(0, 10, 1, -230)
DPadFrame = Instance.new('Frame')
DPadFrame.Name = "DPadFrame"
DPadFrame.Active = true
DPadFrame.Visible = false
DPadFrame.Size = UDim2.new(0, 192, 0, 192)
DPadFrame.Position = position
DPadFrame.BackgroundTransparency = 1
local smArrowSize = UDim2.new(0, 23, 0, 23)
local lgArrowSize = UDim2.new(0, 64, 0, 64)
local smImgOffset = Vector2.new(46, 46)
local lgImgOffset = Vector2.new(128, 128)
local bBtn = createArrowLabel("BackButton", UDim2.new(0.5, -32, 1, -64), lgArrowSize, Vector2.new(0, 0), lgImgOffset)
local fBtn = createArrowLabel("ForwardButton", UDim2.new(0.5, -32, 0, 0), lgArrowSize, Vector2.new(0, 258), lgImgOffset)
local lBtn = createArrowLabel("LeftButton", UDim2.new(0, 0, 0.5, -32), lgArrowSize, Vector2.new(129, 129), lgImgOffset)
local rBtn = createArrowLabel("RightButton", UDim2.new(1, -64, 0.5, -32), lgArrowSize, Vector2.new(0, 129), lgImgOffset)
local jumpBtn = createArrowLabel("JumpButton", UDim2.new(0.5, -32, 0.5, -32), lgArrowSize, Vector2.new(129, 0), lgImgOffset)
local flBtn = createArrowLabel("ForwardLeftButton", UDim2.new(0, 35, 0, 35), smArrowSize, Vector2.new(129, 258), smImgOffset)
local frBtn = createArrowLabel("ForwardRightButton", UDim2.new(1, -55, 0, 35), smArrowSize, Vector2.new(176, 258), smImgOffset)
flBtn.Visible = false
frBtn.Visible = false
-- input connections
jumpBtn.InputBegan:connect(function(inputObject)
MasterControl:DoJump()
end)
local movementVector = Vector3.new(0,0,0)
local function normalizeDirection(inputPosition)
local jumpRadius = jumpBtn.AbsoluteSize.x/2
local centerPosition = getCenterPosition()
local direction = Vector2.new(inputPosition.x - centerPosition.x, inputPosition.y - centerPosition.y)
if direction.magnitude > jumpRadius then
local angle = ATAN2(direction.y, direction.x)
local octant = (FLOOR(8 * angle / (2 * PI) + 8.5)%8) + 1
movementVector = COMPASS_DIR[octant]
end
if not flBtn.Visible and movementVector == COMPASS_DIR[7] then
flBtn.Visible = true
frBtn.Visible = true
end
end
DPadFrame.InputBegan:connect(function(inputObject)
if TouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
MasterControl:AddToPlayerMovement(-movementVector)
TouchObject = inputObject
normalizeDirection(TouchObject.Position)
MasterControl:AddToPlayerMovement(movementVector)
end)
DPadFrame.InputChanged:connect(function(inputObject)
if inputObject == TouchObject then
MasterControl:AddToPlayerMovement(-movementVector)
normalizeDirection(TouchObject.Position)
MasterControl:AddToPlayerMovement(movementVector)
MasterControl:SetIsJumping(false)
end
end)
OnInputEnded = function()
TouchObject = nil
flBtn.Visible = false
frBtn.Visible = false
MasterControl:AddToPlayerMovement(-movementVector)
movementVector = Vector3.new(0, 0, 0)
end
DPadFrame.InputEnded:connect(function(inputObject)
if inputObject == TouchObject then
OnInputEnded()
end
end)
GuiService.MenuOpened:connect(function()
if TouchObject then
OnInputEnded()
end
end)
DPadFrame.Parent = parentFrame
end
return DPad
@@ -0,0 +1,618 @@
--[[
// FileName: DynamicThumbstick
// Version 0.9
// Written by: jhelms
// Description: Implements dynamic thumbstick controls for touch devices
--]]
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local MasterControl = require(script.Parent)
local FFlagUserEnableDynamicThumbstickIntroSuccess, FFlagUserEnableDynamicThumbstickIntroResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserEnableDynamicThumbstickIntro") end)
local FFlagUserEnableDynamicThumbstickIntro = FFlagUserEnableDynamicThumbstickIntroSuccess and FFlagUserEnableDynamicThumbstickIntroResult
local Intro = FFlagUserEnableDynamicThumbstickIntro and require(script:WaitForChild("Intro")) or nil
local Thumbstick = {}
local Enabled = false
--[[ Script Variables ]]--
while not Players.LocalPlayer do
Players.PlayerAdded:wait()
end
local LocalPlayer = Players.LocalPlayer
local Tools = {}
local ToolEquipped = nil
local RevertAutoJumpEnabledToFalse = false
local ThumbstickFrame = nil
local GestureArea = nil
local StartImage = nil
local EndImage = nil
local MiddleImages = {}
local MoveTouchObject = nil
local IsFollowStick = false
local ThumbstickFrame = nil
local OnMoveTouchEnded = nil -- defined in Create()
local OnTouchMovedCn = nil
local OnTouchEndedCn = nil
local TouchActivateCn = nil
local OnRenderSteppedCn = nil
local currentMoveVector = Vector3.new(0,0,0)
local IsFirstTouch = true
--[[ Constants ]]--
local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
local MIDDLE_TRANSPARENCIES = {
1 - 0.89,
1 - 0.70,
1 - 0.60,
1 - 0.50,
1 - 0.40,
1 - 0.30,
1 - 0.25
}
local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES
local TOUCH_IS_TAP_TIME_THRESHOLD = 0.5
local TOUCH_IS_TAP_DISTANCE_THRESHOLD = 25
local HasFadedBackgroundInPortrait = false
local HasFadedBackgroundInLandscape = false
local FadeInAndOutBackground = true
local FadeInAndOutMaxAlpha = 0.35
local TweenInAlphaStart = nil
local TweenOutAlphaStart = nil
local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3
local FADE_IN_OUT_HALF_DURATION_ORIENTATION_CHANGE = 2
local FADE_IN_OUT_BALANCE_DEFAULT = 0.5
local FadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
local FadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
--[[ Local functionality ]]--
local function isDynamicThumbstickEnabled()
return ThumbstickFrame and ThumbstickFrame.Visible
end
local function enableAutoJump(humanoid)
if humanoid and isDynamicThumbstickEnabled() then
local shouldRevert = humanoid.AutoJumpEnabled == false
shouldRevert = shouldRevert and LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice
RevertAutoJumpEnabledToFalse = shouldRevert
humanoid.AutoJumpEnabled = true
end
end
do
local function onCharacterAdded(character)
for _, child in ipairs(LocalPlayer.Character:GetChildren()) do
if child:IsA("Tool") then
ToolEquipped = child
end
end
character.ChildAdded:Connect(function(child)
if child:IsA("Tool") then
ToolEquipped = child
elseif child:IsA("Humanoid") then
enableAutoJump(child)
end
end)
character.ChildRemoved:Connect(function(child)
if child:IsA("Tool") then
if child == ToolEquipped then
ToolEquipped = nil
end
end
end)
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
enableAutoJump(humanoid)
end
end
LocalPlayer.CharacterAdded:Connect(onCharacterAdded)
if LocalPlayer.Character then
onCharacterAdded(LocalPlayer.Character)
end
end
--[[ Public API ]]--
function Thumbstick:Enable()
Enabled = true
ThumbstickFrame.Visible = true
local humanoid = MasterControl:GetHumanoid()
enableAutoJump(humanoid)
if FFlagUserEnableDynamicThumbstickIntro and Intro then
coroutine.wrap(function()
--TODO: Remove this pcall when API is stable
local success, shouldShowIntro = pcall(function()
local wasIntroShown = GameSettings:GetOnboardingCompleted("DynamicThumbstick")
return not wasIntroShown
end)
if success and not shouldShowIntro then return end
--Give the game some time to initialize
wait(1)
--Wait to play the intro until the character can move
while true do
if not humanoid then
humanoid = MasterControl:GetHumanoid()
end
if humanoid and humanoid.WalkSpeed ~= 0 and not humanoid.Torso.Anchored then
break
else
wait()
end
end
Intro.play()
end)()
end
end
function Thumbstick:Disable()
Enabled = false
if RevertAutoJumpEnabledToFalse then
local humanoid = MasterControl:GetHumanoid()
if humanoid then
humanoid.AutoJumpEnabled = false
end
end
ThumbstickFrame.Visible = false
OnMoveTouchEnded()
end
function Thumbstick:GetInputObject()
return MoveTouchObject
end
function Thumbstick:Create(parentFrame)
if ThumbstickFrame then
ThumbstickFrame:Destroy()
ThumbstickFrame = nil
if OnTouchMovedCn then
OnTouchMovedCn:disconnect()
OnTouchMovedCn = nil
end
if OnTouchEndedCn then
OnTouchEndedCn:disconnect()
OnTouchEndedCn = nil
end
if OnRenderSteppedCn then
OnRenderSteppedCn:disconnect()
OnRenderSteppedCn = nil
end
if TouchActivateCn then
TouchActivateCn:disconnect()
TouchActivateCn = nil
end
end
local ThumbstickSize = 45
local ThumbstickRingSize = 20
local MiddleSize = 10
local MiddleSpacing = MiddleSize + 4
local RadiusOfDeadZone = 2
local RadiusOfMaxSpeed = 50
local screenSize = parentFrame.AbsoluteSize
local isBigScreen = math.min(screenSize.x, screenSize.y) > 500
if isBigScreen then
ThumbstickSize = ThumbstickSize * 2
ThumbstickRingSize = ThumbstickRingSize * 2
MiddleSize = MiddleSize * 2
MiddleSpacing = MiddleSpacing * 2
RadiusOfDeadZone = RadiusOfDeadZone * 2
RadiusOfMaxSpeed = RadiusOfMaxSpeed * 2
end
local color = Color3.fromRGB(255, 255, 255)
local function layoutThumbstickFrame(portraitMode)
if portraitMode then
ThumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0)
ThumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0)
GestureArea.Size = UDim2.new(1, 0, 0.6, 0)
GestureArea.Position = UDim2.new(0, 0, 0, 0)
else
ThumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0)
ThumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0)
GestureArea.Size = UDim2.new(1, 0, 1, 0)
GestureArea.Position = UDim2.new(0, 0, 0, 0)
end
end
GestureArea = Instance.new("Frame")
GestureArea.Name = "GestureArea"
GestureArea.Active = false
GestureArea.Visible = true
GestureArea.BackgroundTransparency = 1
GestureArea.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
ThumbstickFrame = Instance.new('Frame')
ThumbstickFrame.Name = "DynamicThumbstickFrame"
ThumbstickFrame.Active = false
ThumbstickFrame.Visible = false
ThumbstickFrame.BackgroundTransparency = 1.0
ThumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
layoutThumbstickFrame()
StartImage = Instance.new("ImageLabel")
StartImage.Name = "ThumbstickStart"
StartImage.Visible = true
StartImage.BackgroundTransparency = 1
StartImage.Image = TOUCH_CONTROLS_SHEET
StartImage.ImageRectOffset = Vector2.new(1,1)
StartImage.ImageRectSize = Vector2.new(144, 144)
StartImage.ImageColor3 = Color3.new(0, 0, 0)
StartImage.AnchorPoint = Vector2.new(0.5, 0.5)
StartImage.Position = UDim2.new(0, ThumbstickRingSize * 3.3, 1, -ThumbstickRingSize * 2.8)
StartImage.Size = UDim2.new(0, ThumbstickRingSize * 3.7, 0, ThumbstickRingSize * 3.7)
StartImage.ZIndex = 10
StartImage.Parent = ThumbstickFrame
EndImage = Instance.new("ImageLabel")
EndImage.Name = "ThumbstickEnd"
EndImage.Visible = true
EndImage.BackgroundTransparency = 1
EndImage.Image = TOUCH_CONTROLS_SHEET
EndImage.ImageRectOffset = Vector2.new(1,1)
EndImage.ImageRectSize = Vector2.new(144, 144)
EndImage.AnchorPoint = Vector2.new(0.5, 0.5)
EndImage.Position = StartImage.Position
EndImage.Size = UDim2.new(0, ThumbstickSize * 0.8, 0, ThumbstickSize * 0.8)
EndImage.ZIndex = 10
EndImage.Parent = ThumbstickFrame
for i = 1, NUM_MIDDLE_IMAGES do
MiddleImages[i] = Instance.new("ImageLabel")
MiddleImages[i].Name = "ThumbstickMiddle"
MiddleImages[i].Visible = false
MiddleImages[i].BackgroundTransparency = 1
MiddleImages[i].Image = TOUCH_CONTROLS_SHEET
MiddleImages[i].ImageRectOffset = Vector2.new(1,1)
MiddleImages[i].ImageRectSize = Vector2.new(144, 144)
MiddleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i]
MiddleImages[i].AnchorPoint = Vector2.new(0.5, 0.5)
MiddleImages[i].ZIndex = 9
MiddleImages[i].Parent = ThumbstickFrame
end
if FFlagUserEnableDynamicThumbstickIntro and Intro then
Intro.setup(isBigScreen, parentFrame, GestureArea, ThumbstickFrame, StartImage, EndImage, MiddleImages)
end
local CameraChangedConn = nil
local function onCurrentCameraChanged()
if CameraChangedConn then
CameraChangedConn:Disconnect()
CameraChangedConn = nil
end
local newCamera = workspace.CurrentCamera
if newCamera then
local function onViewportSizeChanged()
local size = newCamera.ViewportSize
local portraitMode = size.X < size.Y
layoutThumbstickFrame(portraitMode)
if FFlagUserEnableDynamicThumbstickIntro then
Intro.setPortraitMode(portraitMode)
end
end
CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged)
onViewportSizeChanged()
end
end
workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged)
if workspace.CurrentCamera then
onCurrentCameraChanged()
end
MoveTouchObject = nil
local MoveTouchStartTime = nil
local MoveTouchStartPosition = nil
local startImageFadeTween, endImageFadeTween, middleImageFadeTweens = nil, nil, {}
local function fadeThumbstick(visible)
if not visible and MoveTouchObject then
return
end
if IsFirstTouch then return end
if startImageFadeTween then
startImageFadeTween:Cancel()
end
if endImageFadeTween then
endImageFadeTween:Cancel()
end
for i = 1, #MiddleImages do
if middleImageFadeTweens[i] then
middleImageFadeTweens[i]:Cancel()
end
end
if visible then
startImageFadeTween = TweenService:Create(StartImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 })
startImageFadeTween:Play()
endImageFadeTween = TweenService:Create(EndImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 })
endImageFadeTween:Play()
for i = 1, #MiddleImages do
middleImageFadeTweens[i] = TweenService:Create(MiddleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] })
middleImageFadeTweens[i]:Play()
end
else
startImageFadeTween = TweenService:Create(StartImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
startImageFadeTween:Play()
endImageFadeTween = TweenService:Create(EndImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
endImageFadeTween:Play()
for i = 1, #MiddleImages do
middleImageFadeTweens[i] = TweenService:Create(MiddleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
middleImageFadeTweens[i]:Play()
end
end
end
local function fadeThumbstickFrame(fadeDuration, fadeRatio)
FadeInAndOutHalfDuration = fadeDuration * 0.5
FadeInAndOutBalance = fadeRatio
TweenInAlphaStart = tick()
end
if FFlagUserEnableDynamicThumbstickIntro and Intro then
Intro.fadeThumbstick = fadeThumbstick
Intro.fadeThumbstickFrame = fadeThumbstickFrame
end
local function doMove(direction)
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = direction
-- Scaled Radial Dead Zone
local inputAxisMagnitude = currentMoveVector.magnitude
if inputAxisMagnitude < RadiusOfDeadZone then
currentMoveVector = Vector3.new()
else
currentMoveVector = currentMoveVector.unit*(1 - math.max(0, (RadiusOfMaxSpeed - currentMoveVector.magnitude)/RadiusOfMaxSpeed))
currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
end
MasterControl:AddToPlayerMovement(currentMoveVector)
end
local function layoutMiddleImages(startPos, endPos)
local startDist = (ThumbstickSize / 2) + MiddleSize
local vector = endPos - startPos
local distAvailable = vector.magnitude - (ThumbstickRingSize / 2) - MiddleSize
local direction = vector.unit
local distNeeded = MiddleSpacing * NUM_MIDDLE_IMAGES
local spacing = MiddleSpacing
if distNeeded < distAvailable then
spacing = distAvailable / NUM_MIDDLE_IMAGES
end
for i = 1, NUM_MIDDLE_IMAGES do
local image = MiddleImages[i]
local distWithout = startDist + (spacing * (i - 2))
local currentDist = startDist + (spacing * (i - 1))
if distWithout < distAvailable then
local pos = endPos - direction * currentDist
local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1)
image.Visible = true
image.Position = UDim2.new(0, pos.X, 0, pos.Y)
image.Size = UDim2.new(0, MiddleSize * exposedFraction, 0, MiddleSize * exposedFraction)
else
image.Visible = false
end
end
end
local function moveStick(pos)
local startPos = Vector2.new(MoveTouchStartPosition.X, MoveTouchStartPosition.Y) - ThumbstickFrame.AbsolutePosition
local endPos = Vector2.new(pos.X, pos.Y) - ThumbstickFrame.AbsolutePosition
local relativePosition = endPos - startPos
local length = relativePosition.magnitude
local maxLength = ThumbstickFrame.AbsoluteSize.X
length = math.min(length, maxLength)
relativePosition = relativePosition*length
EndImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y)
layoutMiddleImages(startPos, endPos)
end
-- input connections
ThumbstickFrame.InputBegan:connect(function(inputObject)
if inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
if MoveTouchObject then
return
end
if IsFirstTouch then
IsFirstTouch = false
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0)
TweenService:Create(StartImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play()
TweenService:Create(EndImage, tweenInfo, {Size = UDim2.new(0, ThumbstickSize, 0, ThumbstickSize), ImageColor3 = Color3.new(0,0,0)}):Play()
end
if FFlagUserEnableDynamicThumbstickIntro and Intro then
Intro.onThumbstickMoveBegin()
end
MoveTouchObject = inputObject
MoveTouchStartTime = tick()
MoveTouchStartPosition = inputObject.Position
local startPosVec2 = Vector2.new(inputObject.Position.X - ThumbstickFrame.AbsolutePosition.X, inputObject.Position.Y - ThumbstickFrame.AbsolutePosition.Y)
StartImage.Visible = true
StartImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y)
EndImage.Visible = true
EndImage.Position = StartImage.Position
fadeThumbstick(true)
moveStick(inputObject.Position)
if FadeInAndOutBackground then
local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
local hasFadedBackgroundInOrientation = false
-- only fade in/out the background once per orientation
if playerGui then
if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then
hasFadedBackgroundInOrientation = HasFadedBackgroundInLandscape
HasFadedBackgroundInLandscape = true
elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then
hasFadedBackgroundInOrientation = HasFadedBackgroundInPortrait
HasFadedBackgroundInPortrait = true
end
end
if not hasFadedBackgroundInOrientation then
FadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
FadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
TweenInAlphaStart = tick()
end
end
end)
OnTouchMovedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed)
if inputObject == MoveTouchObject then
local direction = Vector2.new(inputObject.Position.x - MoveTouchStartPosition.x, inputObject.Position.y - MoveTouchStartPosition.y)
if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then
doMove(direction)
moveStick(inputObject.Position)
end
end
end)
OnMoveTouchEnded = function(inputObject)
if inputObject then
local direction = Vector2.new(inputObject.Position.x - MoveTouchStartPosition.x, inputObject.Position.y - MoveTouchStartPosition.y)
if FFlagUserEnableDynamicThumbstickIntro and Intro then
coroutine.wrap(function() Intro.onThumbstickMoved(direction.magnitude) end)()
end
end
MoveTouchObject = nil
fadeThumbstick(false)
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
end
OnRenderSteppedCn = RunService.RenderStepped:Connect(function(step)
if TweenInAlphaStart ~= nil then
local delta = tick() - TweenInAlphaStart
local fadeInTime = (FadeInAndOutHalfDuration * 2 * FadeInAndOutBalance)
ThumbstickFrame.BackgroundTransparency = 1 - FadeInAndOutMaxAlpha*math.min(delta/fadeInTime, 1)
if delta > fadeInTime then
TweenOutAlphaStart = tick()
TweenInAlphaStart = nil
end
elseif TweenOutAlphaStart ~= nil then
local delta = tick() - TweenOutAlphaStart
local fadeOutTime = (FadeInAndOutHalfDuration * 2) - (FadeInAndOutHalfDuration * 2 * FadeInAndOutBalance)
ThumbstickFrame.BackgroundTransparency = 1 - FadeInAndOutMaxAlpha + FadeInAndOutMaxAlpha*math.min(delta/fadeOutTime, 1)
if delta > fadeOutTime then
TweenOutAlphaStart = nil
end
end
if FFlagUserEnableDynamicThumbstickIntro and Intro then
if Intro.currentState == Intro.states.MoveThumbstick then
local startPos = StartImage.AbsolutePosition - ThumbstickFrame.AbsolutePosition + (StartImage.AbsoluteSize * 0.5)
local endPos = EndImage.AbsolutePosition - ThumbstickFrame.AbsolutePosition + (EndImage.AbsoluteSize * 0.5)
layoutMiddleImages(startPos, endPos)
end
end
end)
OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject, isProcessed)
if inputObject == MoveTouchObject then
OnMoveTouchEnded(inputObject)
end
end)
GuiService.MenuOpened:connect(function()
if MoveTouchObject then
OnMoveTouchEnded(nil)
end
end)
local playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
while not playerGui do
LocalPlayer.ChildAdded:wait()
playerGui = LocalPlayer:FindFirstChildOfClass("PlayerGui")
end
local playerGuiChangedConn = nil
local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight
local function longShowBackground()
FadeInAndOutHalfDuration = 2.5
FadeInAndOutBalance = 0.05
TweenInAlphaStart = tick()
end
playerGuiChangedConn = playerGui.Changed:connect(function(prop)
if prop == "CurrentScreenOrientation" then
if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or
(not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then
playerGuiChangedConn:disconnect()
longShowBackground()
if originalScreenOrientationWasLandscape then
HasFadedBackgroundInPortrait = true
else
HasFadedBackgroundInLandscape = true
end
end
end
end)
GestureArea.Parent = parentFrame.Parent
ThumbstickFrame.Parent = parentFrame
spawn(function()
if game:IsLoaded() then
longShowBackground()
else
game.Loaded:wait()
longShowBackground()
end
end)
end
return Thumbstick
@@ -0,0 +1,522 @@
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local LocalPlayer = Players.LocalPlayer
local IMAGE_INTRO_MOVE = "rbxasset://textures/ui/Input/IntroMove.png"
local IMAGE_INTRO_CAMERA = "rbxasset://textures/ui/Input/IntroCamera.png"
local IMAGE_INTRO_CAMERA_PINCH = "rbxasset://textures/ui/Input/IntroCameraPinch.png"
local IMAGE_DASHED_LINE = "rbxasset://textures/ui/Input/DashedLine.png"
local IMAGE_DASHED_LINE_90 = "rbxasset://textures/ui/Input/DashedLine90.png"
local MIDDLE_TRANSPARENCIES = {
1 - 0.69,
1 - 0.50,
1 - 0.40,
1 - 0.30,
1 - 0.20,
1 - 0.10,
1 - 0.05
}
local MOVE_CAMERA_THRESHOLD = 50
local ParentFrame = nil
local GestureArea = nil
local ThumbstickFrame = nil
local StartImage, EndImage, MiddleImages = nil, nil, {}
local IntroMoveImage = nil
local IntroJumpImage = nil
local IntroJumpImageTap = nil
local IntroCameraImage = nil
local IntroCornerAnim = nil
local IntroDashedLineTop, IntroDashedLineSide = nil
local GenericFadeTweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local IntroMoveSpriteSize = 128
local IntroMoveImageSize = 128
local noThumbstickFrameFade = true
local function create(className)
return function(props)
local instance = Instance.new(className)
for key, val in pairs(props) do
if typeof(key) == "string" then
if key ~= "Parent" then
instance[key] = val
end
else
if typeof(val) == "Instance" then
val.Parent = instance
end
end
end
instance.Parent = props.Parent
return instance
end
end
local Intro = {
running = false,
portraitMode = false,
fadeThumbstick = nil,
fadeThumbstickFrame = nil,
didMoveThumbstick = false,
didJump = false,
didMoveCamera = false,
currentState = nil,
states = {},
}
function Intro.setup(isBigScreen, parentFrame, gestureArea, thumbstickFrame, startImage, endImage, middleImages)
ParentFrame = parentFrame
GestureArea = gestureArea
ThumbstickFrame = thumbstickFrame
StartImage = startImage
EndImage = endImage
MiddleImages = middleImages
if isBigScreen then
IntroMoveImageSize = IntroMoveImageSize * 2
end
IntroMoveImage = create("ImageLabel") {
Name = "IntroMoveAnimation",
BackgroundTransparency = 1,
Image = IMAGE_INTRO_MOVE,
AnchorPoint = Vector2.new(102/256, 55/256),
Position = UDim2.new(0.5, 0, 0.5, 0),
Size = UDim2.new(0, IntroMoveImageSize, 0, IntroMoveImageSize),
ImageRectOffset = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize),
ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize),
ZIndex = 10,
Visible = false,
Parent = EndImage
}
IntroJumpImage = create("ImageLabel") {
Name = "IntroJumpAnimation",
BackgroundTransparency = 1,
AnchorPoint = Vector2.new(102/256, 55/256),
Position = UDim2.new(0.75, 0, 0.25, 0),
Image = IMAGE_INTRO_MOVE,
ImageRectOffset = Vector2.new(0, 0),
ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize),
ZIndex = 10,
Visible = false,
Parent = ThumbstickFrame
}
IntroJumpImageTap = create("ImageLabel") {
Name = "IntroJumpTapAnimation",
BackgroundTransparency = 1,
Image = IMAGE_INTRO_MOVE,
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
Size = UDim2.new(1, 0, 1, 0),
ImageTransparency = 1,
ImageRectOffset = Vector2.new(IntroMoveSpriteSize, 0),
ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize),
ZIndex = 10,
Visible = false,
Parent = IntroJumpImage
}
IntroCameraImage = create("ImageLabel") {
Name = "IntroCameraDragAnimation",
BackgroundTransparency = 1,
Image = IMAGE_INTRO_MOVE,
ImageRectOffset = Vector2.new(0, IntroMoveSpriteSize),
ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize),
Position = UDim2.new(0.75, 0, -0.6, 0),
Size = UDim2.new(0, IntroMoveImageSize, 0, IntroMoveImageSize),
AnchorPoint = Vector2.new(0.5, 0),
ZIndex = 10,
Visible = false,
Parent = GestureArea
}
IntroPinchImage = create("ImageLabel") {
Name = "IntroCameraPinchAnimation",
BackgroundTransparency = 1,
Image = IMAGE_INTRO_CAMERA_PINCH,
ImageRectOffset = Vector2.new(0, 0),
ImageRectSize = Vector2.new(IntroMoveSpriteSize, IntroMoveSpriteSize),
Position = UDim2.new(0.95, 0, 0.15, 0),
Size = UDim2.new(0, IntroMoveImageSize, 0, IntroMoveImageSize),
AnchorPoint = Vector2.new(1, 0),
Rotation = 50,
ZIndex = 10,
Visible = false,
Parent = GestureArea
}
IntroDashedLineTop = create("ImageLabel") {
Name = "DashedLine",
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, -5, 0, 10),
AnchorPoint = Vector2.new(0, 0.5),
Image = IMAGE_DASHED_LINE,
ImageTransparency = 0.5,
ScaleType = Enum.ScaleType.Tile,
TileSize = UDim2.new(0, 64, 0, 10),
Visible = false,
Parent = ThumbstickFrame
}
IntroDashedLineSide = IntroDashedLineTop:Clone()
IntroDashedLineSide.Image = IMAGE_DASHED_LINE_90
IntroDashedLineSide.Position = UDim2.new(1, 0, 0, -5)
IntroDashedLineSide.AnchorPoint = Vector2.new(0.5, 0)
IntroDashedLineSide.Size = UDim2.new(0, 10, 1, 0)
IntroDashedLineSide.TileSize = UDim2.new(0, 10, 0, 64)
IntroDashedLineSide.Parent = ThumbstickFrame
end
function Intro.setPortraitMode(portraitMode)
if not IntroDashedLineSide or not Intro.running then return end
Intro.portraitMode = portraitMode
IntroDashedLineSide.Visible = not portraitMode
end
function Intro.addState(stateName)
local state = {}
state.counter = 0
Intro.states[stateName] = state
return state
end
function Intro.onCompleted()
--TODO: Remove this pcall when API is stable
pcall(function() GameSettings:SetOnboardingCompleted("DynamicThumbstick") end)
local fadeOutTop = TweenService:Create(IntroDashedLineTop, GenericFadeTweenInfo, { ImageTransparency = 1 })
local fadeOutSide = TweenService:Create(IntroDashedLineSide, GenericFadeTweenInfo, { ImageTransparency = 1})
fadeOutTop:Play()
fadeOutSide:Play()
fadeOutTop.Completed:wait()
IntroDashedLineTop.Visible = false
IntroDashedLineSide.Visible = false
Intro.running = false
end
function Intro.setState(newState)
if Intro.currentState == newState then return end
if Intro.currentState then
coroutine.wrap(function() Intro.currentState:stop() end)()
end
Intro.currentState = newState
Intro.currentState.counter = Intro.currentState.counter + 1
Intro.currentState:start()
end
function Intro.repeatState()
if not Intro.currentState then return end
coroutine.wrap(function() Intro.currentState:stop() end)()
Intro.currentState.counter = Intro.currentState.counter + 1
Intro.currentState:start()
end
function Intro.pause()
if not Intro.currentState then return end
Intro.currentState:stop()
Intro.currentState = nil
end
function Intro.onThumbstickMoveBegin()
if Intro.currentState == Intro.states.MoveThumbstick then
Intro.states.MoveThumbstick.startedMoving = true
local moveTween = Intro.states.MoveThumbstick.moveTween
if moveTween then
moveTween:Cancel()
end
end
end
function Intro.onThumbstickMoved(dist)
if Intro.currentState == Intro.states.MoveThumbstick then
if dist > 100 then
Intro.pause()
spawn(function()
wait(0.5)
while StartImage.ImageTransparency ~= 1 do
wait()
end
if Intro.states.MoveThumbstick.counter == 2 then
Intro.setState(Intro.states.MoveCamera)
else
Intro.setState(Intro.states.MoveThumbstick)
end
end)
else
Intro.states.MoveThumbstick.startedMoving = false
end
end
end
function Intro.onJumped()
if Intro.currentState == Intro.states.Jump then
Intro.pause()
spawn(function()
wait(1)
if Intro.states.Jump.counter > 1 then
Intro.setState(Intro.states.MoveCamera)
else
Intro.setState(Intro.states.Jump)
end
end)
end
end
function Intro.onCameraMoved()
if Intro.currentState == Intro.states.MoveCamera then
wait(1)
Intro.setState(Intro.states.ZoomCamera)
end
end
function Intro.onCameraZoomed()
if Intro.currentState == Intro.states.ZoomCamera then
Intro.pause()
end
end
function Intro.play()
for _, state in pairs(Intro.states) do
state.counter = 0
end
Intro.running = true
Intro.currentState = nil
local portraitMode = false
local currentCamera = workspace.CurrentCamera
if currentCamera then
portraitMode = currentCamera.ViewportSize.X < currentCamera.ViewportSize.Y
end
Intro.setPortraitMode(portraitMode)
Intro.setState(Intro.states.MoveThumbstick)
end
Intro.addState("MoveThumbstick") do
function Intro.states.MoveThumbstick:start()
StartImage.Visible = true
EndImage.Visible = true
IntroMoveImage.Visible = true
IntroDashedLineTop.Visible = true
if not Intro.portraitMode then
IntroDashedLineSide.Visible = true
end
self.startedMoving = false
StartImage.Position = UDim2.new(0.5, 0, 0.6, 0)
EndImage.Position = UDim2.new(0.5, 0, 0.6, 0)
local moveTweenTarget = UDim2.new(0.3, 0, -0.25, 0)
if Intro.states.MoveThumbstick.counter % 2 ~= 0 then
moveTweenTarget = UDim2.new(0.7, 0, -0.25, 0)
end
local moveTweenInfo = TweenInfo.new(1.5, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut)
self.moveTween = TweenService:Create(EndImage, moveTweenInfo, { Position = moveTweenTarget })
local moveFadeInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local fadeOutTween = TweenService:Create(IntroMoveImage, moveFadeInfo, { ImageTransparency = 1 })
local fadeInTween = TweenService:Create(IntroMoveImage, moveFadeInfo, { ImageTransparency = 0 })
self.moveAnimActive = true
coroutine.wrap(function()
while Intro.currentState == self do
if not self.startedMoving then
if not noThumbstickFrameFade then
Intro.fadeThumbstickFrame(moveTweenInfo.Time, 0.05)
end
StartImage.Position = UDim2.new(0.5, 0, 0.6, 0)
EndImage.Position = UDim2.new(0.5, 0, 0.6, 0)
self.moveTween:Play()
Intro.fadeThumbstick(true)
fadeInTween:Play()
self.moveTween.Completed:wait()
if Intro.currentState == self then
Intro.fadeThumbstick(false)
fadeOutTween:Play()
fadeOutTween.Completed:wait()
wait(0.5)
end
else
wait()
end
end
end)()
end
function Intro.states.MoveThumbstick:stop()
self.moveTween:Cancel()
IntroMoveImage.Visible = false
if self.iconOptConn then
self.iconOptConn:disconnect()
self.iconOptConn = nil
end
end
end
Intro.addState("Jump") do
function Intro.states.Jump:start()
IntroJumpImage.Visible = true
IntroJumpImageTap.Visible = true
local jumpImageBaseSize = IntroMoveImageSize
IntroJumpImage.Size = UDim2.new(0, jumpImageBaseSize * 1.5, 0, jumpImageBaseSize * 1.25)
IntroJumpImageTap.ImageTransparency = 1
local fadeInInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut)
local fadeOutInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
self.fadeHandIn = TweenService:Create(IntroJumpImage, fadeInInfo, { ImageTransparency = 0, Size = UDim2.new(0, jumpImageBaseSize, 0, jumpImageBaseSize * 0.95) })
self.fadeHandOut = TweenService:Create(IntroJumpImage, fadeOutInfo, { ImageTransparency = 0.25, Size = UDim2.new(0, jumpImageBaseSize * 1.25, 0, jumpImageBaseSize * 1.25) })
self.fadeTapIn = TweenService:Create(IntroJumpImageTap, fadeInInfo, { ImageTransparency = 0 })
self.fadeTapOut = TweenService:Create(IntroJumpImageTap, fadeOutInfo, { ImageTransparency = 1 })
coroutine.wrap(function()
while Intro.currentState == self do
self.fadeHandIn:Play()
IntroJumpImageTap.ImageTransparency = 1
if self.iconOptConn then
self.iconOptConn:disconnect()
self.iconOptConn = nil
end
wait(self.fadeHandIn.Completed:wait())
if not noThumbstickFrameFade then
Intro.fadeThumbstickFrame(fadeOutInfo.Time, 0)
end
IntroJumpImageTap.ImageTransparency = 0
self.fadeHandOut:Play()
self.fadeTapOut:Play()
self.fadeHandOut.Completed:wait()
end
end)()
end
function Intro.states.Jump:stop()
IntroJumpImage.Visible = false
IntroJumpImageTap.Visible = false
end
end
Intro.addState("MoveCamera") do
function Intro.states.MoveCamera:start()
IntroCameraImage.Visible = true
local swipeInfo = TweenInfo.new(1, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut)
local swipeTweenLeft = TweenService:Create(IntroCameraImage, swipeInfo, { Position = UDim2.new(0.25, 0, 0.25, 0) })
local swipeTweenRight = TweenService:Create(IntroCameraImage, swipeInfo, { Position = UDim2.new(0.75, 0, 0.25, 0) })
local camera = workspace.CurrentCamera
local cameraLookStart = camera.CFrame.lookVector
coroutine.wrap(function()
while Intro.currentState == self do
swipeTweenLeft:Play()
swipeTweenLeft.Completed:wait()
swipeTweenRight:Play()
swipeTweenRight.Completed:wait()
end
end)()
local function isInCameraArea(touchPos)
return
touchPos.X >= GestureArea.AbsolutePosition.X and touchPos.Y >= GestureArea.AbsolutePosition.Y and
touchPos.X < GestureArea.AbsolutePosition.X + GestureArea.AbsoluteSize.X and
touchPos.Y < GestureArea.AbsolutePosition.Y + GestureArea.AbsoluteSize.Y
end
local touchObj = nil
local recordedMovement = Vector3.new()
self.onInputBeganConn = UserInputService.InputBegan:Connect(function(inputObj, wasProcessed)
if inputObj.UserInputType == Enum.UserInputType.Touch and isInCameraArea(inputObj.Position) and not wasProcessed then
touchObj = inputObj
end
end)
self.onInputChangedConn = UserInputService.InputChanged:Connect(function(inputObj, wasProcessed)
if inputObj == touchObj then
recordedMovement = recordedMovement + inputObj.Delta
if recordedMovement.magnitude > MOVE_CAMERA_THRESHOLD then
IntroCameraImage.Visible = false
end
end
end)
self.onInputEndedConn = UserInputService.InputEnded:Connect(function(inputObj, wasProcessed)
if inputObj == touchObj then
touchObj = nil
if recordedMovement.magnitude > MOVE_CAMERA_THRESHOLD then --todo: make this number a constant
Intro.onCameraMoved()
end
end
end)
end
function Intro.states.MoveCamera:stop()
if self.iconOptConn then
self.iconOptConn:disconnect()
self.iconOptConn = nil
end
IntroCameraImage.Visible = false
end
end
Intro.addState("ZoomCamera") do
function Intro.states.ZoomCamera:start()
IntroPinchImage.Visible = true
local camera = workspace.CurrentCamera
local zoomStart = (camera.Focus.p - camera.CFrame.p).magnitude
coroutine.wrap(function()
while Intro.currentState == self do
IntroPinchImage.ImageRectOffset = Vector2.new(0, 0)
wait(0.75)
IntroPinchImage.ImageRectOffset = Vector2.new(128, 0)
wait(0.25)
end
end)()
coroutine.wrap(function()
while Intro.currentState == self do
local zoom = (camera.Focus.p - camera.CFrame.p).magnitude
if math.abs(zoom - zoomStart) > 3 then
Intro.onCameraZoomed()
end
wait()
end
end)()
end
function Intro.states.ZoomCamera:stop()
IntroPinchImage.Visible = false
Intro.onCompleted()
end
end
return Intro
@@ -0,0 +1,198 @@
--[[
// FileName: Gamepad
// Written by: jeditkacheff
// Description: Implements movement controls for gamepad devices (XBox, PS4, MFi, etc.)
--]]
local Gamepad = {}
local MasterControl = require(script.Parent)
local Players = game:GetService('Players')
local RunService = game:GetService('RunService')
local UserInputService = game:GetService('UserInputService')
local ContextActionService = game:GetService('ContextActionService')
local StarterPlayer = game:GetService('StarterPlayer')
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local currentMoveVector = Vector3.new(0,0,0)
local activateGamepad = nil
local gamepadConnectedCon = nil
local gamepadDisconnectedCon = nil
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
--[[ Constants ]]--
local thumbstickDeadzone = 0.22 --raised from 14% on 3/1/16 to accommodate looser XB360 controllers
function assignActivateGamepad()
local connectedGamepads = UserInputService:GetConnectedGamepads()
if #connectedGamepads > 0 then
for i = 1, #connectedGamepads do
if activateGamepad == nil then
activateGamepad = connectedGamepads[i]
elseif connectedGamepads[i].Value < activateGamepad.Value then
activateGamepad = connectedGamepads[i]
end
end
end
if activateGamepad == nil then -- nothing is connected, at least set up for gamepad1
activateGamepad = Enum.UserInputType.Gamepad1
end
end
--[[ Public API ]]--
function Gamepad:Enable()
local forwardValue = 0
local backwardValue = 0
local leftValue = 0
local rightValue = 0
local moveFunc = LocalPlayer.Move
local gamepadSupports = UserInputService.GamepadSupports
local controlCharacterGamepad = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Cancel then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
return
end
if activateGamepad ~= inputObject.UserInputType then return end
if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
if inputObject.Position.magnitude > thumbstickDeadzone then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
MasterControl:AddToPlayerMovement(currentMoveVector)
else
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
end
end
local jumpCharacterGamepad = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Cancel then
MasterControl:SetIsJumping(false)
return
end
if activateGamepad ~= inputObject.UserInputType then return end
if inputObject.KeyCode ~= Enum.KeyCode.ButtonA then return end
MasterControl:SetIsJumping(inputObject.UserInputState == Enum.UserInputState.Begin)
end
local doDpadMoveUpdate = function(userInputType)
if LocalPlayer and LocalPlayer.Character then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(leftValue + rightValue,0,forwardValue + backwardValue)
MasterControl:AddToPlayerMovement(currentMoveVector)
end
end
local moveForwardFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
forwardValue = -1
elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then
forwardValue = 0
end
doDpadMoveUpdate(inputObject.UserInputType)
end
local moveBackwardFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
backwardValue = 1
elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then
backwardValue = 0
end
doDpadMoveUpdate(inputObject.UserInputType)
end
local moveLeftFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
leftValue = -1
elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then
leftValue = 0
end
doDpadMoveUpdate(inputObject.UserInputType)
end
local moveRightFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.End then
rightValue = 1
elseif inputState == Enum.UserInputState.Begin or inputState == Enum.UserInputState.Cancel then
rightValue = 0
end
doDpadMoveUpdate(inputObject.UserInputType)
end
local function setActivateGamepad()
if activateGamepad then
ContextActionService:UnbindActivate(activateGamepad, Enum.KeyCode.ButtonR2)
end
assignActivateGamepad()
if activateGamepad then
ContextActionService:BindActivate(activateGamepad, Enum.KeyCode.ButtonR2)
end
end
ContextActionService:BindAction("JumpButton",jumpCharacterGamepad, false, Enum.KeyCode.ButtonA)
ContextActionService:BindAction("MoveThumbstick",controlCharacterGamepad, false, Enum.KeyCode.Thumbstick1)
setActivateGamepad()
if not gamepadSupports(UserInputService, activateGamepad, Enum.KeyCode.Thumbstick1) then
-- if the gamepad supports thumbsticks, theres no point in having the dpad buttons getting eaten up by these actions
ContextActionService:BindAction("forwardDpad", moveForwardFunc, false, Enum.KeyCode.DPadUp)
ContextActionService:BindAction("backwardDpad", moveBackwardFunc, false, Enum.KeyCode.DPadDown)
ContextActionService:BindAction("leftDpad", moveLeftFunc, false, Enum.KeyCode.DPadLeft)
ContextActionService:BindAction("rightDpad", moveRightFunc, false, Enum.KeyCode.DPadRight)
end
gamepadConnectedCon = UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
if activateGamepad ~= gamepadEnum then return end
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
activateGamepad = nil
setActivateGamepad()
end)
gamepadDisconnectedCon = UserInputService.GamepadConnected:connect(function(gamepadEnum)
if activateGamepad == nil then
setActivateGamepad()
end
end)
end
function Gamepad:Disable()
ContextActionService:UnbindAction("forwardDpad")
ContextActionService:UnbindAction("backwardDpad")
ContextActionService:UnbindAction("leftDpad")
ContextActionService:UnbindAction("rightDpad")
ContextActionService:UnbindAction("MoveThumbstick")
ContextActionService:UnbindAction("JumpButton")
ContextActionService:UnbindActivate(activateGamepad, Enum.KeyCode.ButtonR2)
if gamepadConnectedCon then gamepadConnectedCon:disconnect() end
if gamepadDisconnectedCon then gamepadDisconnectedCon:disconnect() end
activateGamepad = nil
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
MasterControl:SetIsJumping(false)
end
return Gamepad
@@ -0,0 +1,175 @@
--[[
// FileName: ComputerMovementKeyboardMovement
// Version 1.2
// Written by: jeditkacheff/jmargh
// Description: Implements movement controls for keyboard devices
--]]
local Players = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local ContextActionService = game:GetService('ContextActionService')
local StarterPlayer = game:GetService('StarterPlayer')
local Settings = UserSettings()
local GameSettings = Settings.GameSettings
local KeyboardMovement = {}
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local CachedHumanoid = nil
local SeatJumpCn = nil
local TextFocusReleasedCn = nil
local TextFocusGainedCn = nil
local WindowFocusReleasedCn = nil
local MasterControl = require(script.Parent)
local currentMoveVector = Vector3.new(0,0,0)
--[[ Local Functions ]]--
local function getHumanoid()
local character = LocalPlayer and LocalPlayer.Character
if character then
if CachedHumanoid and CachedHumanoid.Parent == character then
return CachedHumanoid
else
CachedHumanoid = nil
for _,child in pairs(character:GetChildren()) do
if child:IsA('Humanoid') then
CachedHumanoid = child
return CachedHumanoid
end
end
end
end
end
--[[ Public API ]]--
function KeyboardMovement:Enable()
if not UserInputService.KeyboardEnabled then
return
end
local forwardValue = 0
local backwardValue = 0
local leftValue = 0
local rightValue = 0
local updateMovement = function(inputState)
if inputState == Enum.UserInputState.Cancel then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0, 0, 0)
else
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(leftValue + rightValue,0,forwardValue + backwardValue)
MasterControl:AddToPlayerMovement(currentMoveVector)
end
end
local moveForwardFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
forwardValue = -1
elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
forwardValue = 0
end
updateMovement(inputState)
end
local moveBackwardFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
backwardValue = 1
elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
backwardValue = 0
end
updateMovement(inputState)
end
local moveLeftFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
leftValue = -1
elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
leftValue = 0
end
updateMovement(inputState)
end
local moveRightFunc = function(actionName, inputState, inputObject)
if inputState == Enum.UserInputState.Begin then
rightValue = 1
elseif inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then
rightValue = 0
end
updateMovement(inputState)
end
local jumpFunc = function(actionName, inputState, inputObject)
MasterControl:SetIsJumping(inputState == Enum.UserInputState.Begin)
end
-- TODO: remove up and down arrows, these seem unnecessary
ContextActionService:BindActionToInputTypes("forwardMovement", moveForwardFunc, false, Enum.PlayerActions.CharacterForward)
ContextActionService:BindActionToInputTypes("backwardMovement", moveBackwardFunc, false, Enum.PlayerActions.CharacterBackward)
ContextActionService:BindActionToInputTypes("leftMovement", moveLeftFunc, false, Enum.PlayerActions.CharacterLeft)
ContextActionService:BindActionToInputTypes("rightMovement", moveRightFunc, false, Enum.PlayerActions.CharacterRight)
ContextActionService:BindActionToInputTypes("jumpAction", jumpFunc, false, Enum.PlayerActions.CharacterJump)
-- TODO: make sure we check key state before binding to check if key is already down
local function onFocusReleased()
local humanoid = getHumanoid()
if humanoid then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0, 0, 0)
forwardValue, backwardValue, leftValue, rightValue = 0, 0, 0, 0
MasterControl:SetIsJumping(false)
end
end
local function onTextFocusGained(textboxFocused)
MasterControl:SetIsJumping(false)
end
SeatJumpCn = UserInputService.InputBegan:connect(function(inputObject, isProcessed)
if inputObject.KeyCode == Enum.KeyCode.Backspace and not isProcessed then
local humanoid = getHumanoid()
if humanoid and (humanoid.Sit or humanoid.PlatformStand) then
MasterControl:DoJump()
end
end
end)
TextFocusReleasedCn = UserInputService.TextBoxFocusReleased:connect(onFocusReleased)
TextFocusGainedCn = UserInputService.TextBoxFocused:connect(onTextFocusGained)
-- TODO: remove pcall when API is live
WindowFocusReleasedCn = UserInputService.WindowFocusReleased:connect(onFocusReleased)
end
function KeyboardMovement:Disable()
ContextActionService:UnbindAction("forwardMovement")
ContextActionService:UnbindAction("backwardMovement")
ContextActionService:UnbindAction("leftMovement")
ContextActionService:UnbindAction("rightMovement")
ContextActionService:UnbindAction("jumpAction")
if SeatJumpCn then
SeatJumpCn:disconnect()
SeatJumpCn = nil
end
if TextFocusReleasedCn then
TextFocusReleasedCn:disconnect()
TextFocusReleasedCn = nil
end
if TextFocusGainedCn then
TextFocusGainedCn:disconnect()
TextFocusGainedCn = nil
end
if WindowFocusReleasedCn then
WindowFocusReleasedCn:disconnect()
WindowFocusReleasedCn = nil
end
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
MasterControl:SetIsJumping(false)
end
return KeyboardMovement
@@ -0,0 +1,228 @@
--[[
// FileName: Thumbpad
// Version 1.0
// Written by: jmargh
// Description: Implements thumbpad controls for touch devices
--]]
local Players = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local GuiService = game:GetService('GuiService')
local Thumbpad = {}
local MasterControl = require(script.Parent)
--[[ Script Variables ]]--
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local ThumbpadFrame = nil
local TouchObject = nil
local OnInputEnded = nil -- is defined in Create()
local OnTouchChangedCn = nil
local OnTouchEndedCn = nil
local currentMoveVector = Vector3.new(0,0,0)
--[[ Constants ]]--
local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png"
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
--[[ Local Functions ]]--
local function createArrowLabel(name, parent, position, size, rectOffset, rectSize)
local image = Instance.new('ImageLabel')
image.Name = name
image.Image = DPAD_SHEET
image.ImageRectOffset = rectOffset
image.ImageRectSize = rectSize
image.BackgroundTransparency = 1
image.ImageColor3 = Color3.new(190/255, 190/255, 190/255)
image.Size = size
image.Position = position
image.Parent = parent
return image
end
--[[ Public API ]]--
function Thumbpad:Enable()
ThumbpadFrame.Visible = true
end
function Thumbpad:Disable()
ThumbpadFrame.Visible = false
OnInputEnded()
end
function Thumbpad:Create(parentFrame)
if ThumbpadFrame then
ThumbpadFrame:Destroy()
ThumbpadFrame = nil
if OnTouchChangedCn then
OnTouchChangedCn:disconnect()
OnTouchChangedCn = nil
end
if OnTouchEndedCn then
OnTouchEndedCn:disconnect()
OnTouchEndedCn = nil
end
end
local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
local isSmallScreen = minAxis <= 500
local thumbpadSize = isSmallScreen and 70 or 120
local position = isSmallScreen and UDim2.new(0, thumbpadSize * 1.25, 1, -thumbpadSize - 20) or
UDim2.new(0, thumbpadSize/2 - 10, 1, -thumbpadSize * 1.75 - 10)
ThumbpadFrame = Instance.new('Frame')
ThumbpadFrame.Name = "ThumbpadFrame"
ThumbpadFrame.Visible = false
ThumbpadFrame.Active = true
ThumbpadFrame.Size = UDim2.new(0, thumbpadSize + 20, 0, thumbpadSize + 20)
ThumbpadFrame.Position = position
ThumbpadFrame.BackgroundTransparency = 1
local outerImage = Instance.new('ImageLabel')
outerImage.Name = "OuterImage"
outerImage.Image = TOUCH_CONTROL_SHEET
outerImage.ImageRectOffset = Vector2.new(0, 0)
outerImage.ImageRectSize = Vector2.new(220, 220)
outerImage.BackgroundTransparency = 1
outerImage.Size = UDim2.new(0, thumbpadSize, 0, thumbpadSize)
outerImage.Position = UDim2.new(0, 10, 0, 10)
outerImage.Parent = ThumbpadFrame
local smArrowSize = isSmallScreen and UDim2.new(0, 32, 0, 32) or UDim2.new(0, 64, 0, 64)
local lgArrowSize = UDim2.new(0, smArrowSize.X.Offset * 2, 0, smArrowSize.Y.Offset * 2)
local imgRectSize = Vector2.new(110, 110)
local smImgOffset = isSmallScreen and -4 or -9
local lgImgOffset = isSmallScreen and -28 or -55
local dArrow = createArrowLabel("DownArrow", outerImage, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset), smArrowSize, Vector2.new(8, 8), imgRectSize)
local uArrow = createArrowLabel("UpArrow", outerImage, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset), smArrowSize, Vector2.new(8, 266), imgRectSize)
local lArrow = createArrowLabel("LeftArrow", outerImage, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2), smArrowSize, Vector2.new(137, 137), imgRectSize)
local rArrow = createArrowLabel("RightArrow", outerImage, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2), smArrowSize, Vector2.new(8, 137), imgRectSize)
local function doTween(guiObject, endSize, endPosition)
guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true)
end
local padOrigin = nil
local deadZone = 0.1
local isRight, isLeft, isUp, isDown = false, false, false, false
local vForward = Vector3.new(0, 0, -1)
local vRight = Vector3.new(1, 0, 0)
local function doMove(pos)
MasterControl:AddToPlayerMovement(-currentMoveVector)
local delta = Vector2.new(pos.x, pos.y) - padOrigin
currentMoveVector = delta / (thumbpadSize/2)
-- Scaled Radial Dead Zone
local inputAxisMagnitude = currentMoveVector.magnitude
if inputAxisMagnitude < deadZone then
currentMoveVector = Vector3.new(0, 0, 0)
else
currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone))
-- catch possible NAN Vector
if currentMoveVector.magnitude == 0 then
currentMoveVector = Vector3.new(0, 0, 0)
else
currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y).unit
end
end
MasterControl:AddToPlayerMovement(currentMoveVector)
local forwardDot = currentMoveVector:Dot(vForward)
local rightDot = currentMoveVector:Dot(vRight)
if forwardDot > 0.5 then -- UP
if not isUp then
isUp, isDown = true, false
doTween(uArrow, lgArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset, 0, smImgOffset - smArrowSize.Y.Offset * 1.5))
doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset))
end
elseif forwardDot < -0.5 then -- DOWN
if not isDown then
isDown, isUp = true, false
doTween(dArrow, lgArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset, 1, lgImgOffset + smArrowSize.Y.Offset/2))
doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset))
end
else
isUp, isDown = false, false
doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset))
doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset))
end
if rightDot > 0.5 then
if not isRight then
isRight, isLeft = true, false
doTween(rArrow, lgArrowSize, UDim2.new(1, lgImgOffset + smArrowSize.X.Offset/2, 0.5, -smArrowSize.Y.Offset))
doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2))
end
elseif rightDot < -0.5 then
if not isLeft then
isLeft, isRight = true, false
doTween(lArrow, lgArrowSize, UDim2.new(0, smImgOffset - smArrowSize.X.Offset * 1.5, 0.5, -smArrowSize.Y.Offset))
doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2))
end
else
isRight, isLeft = false, false
doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2))
doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2))
end
end
--input connections
ThumbpadFrame.InputBegan:connect(function(inputObject)
--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
if TouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
ThumbpadFrame.Position = UDim2.new(0, inputObject.Position.x - ThumbpadFrame.AbsoluteSize.x/2, 0, inputObject.Position.y - ThumbpadFrame.Size.Y.Offset/2)
padOrigin = Vector2.new(ThumbpadFrame.AbsolutePosition.x + ThumbpadFrame.AbsoluteSize.x/2,
ThumbpadFrame.AbsolutePosition.y + ThumbpadFrame.AbsoluteSize.y/2)
doMove(inputObject.Position)
TouchObject = inputObject
end)
OnTouchChangedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed)
if inputObject == TouchObject then
doMove(TouchObject.Position)
end
end)
OnInputEnded = function()
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
MasterControl:SetIsJumping(false)
ThumbpadFrame.Position = position
TouchObject = nil
isUp, isDown, isLeft, isRight = false, false, false, false
doTween(dArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 1, lgImgOffset))
doTween(uArrow, smArrowSize, UDim2.new(0.5, -smArrowSize.X.Offset/2, 0, smImgOffset))
doTween(lArrow, smArrowSize, UDim2.new(0, smImgOffset, 0.5, -smArrowSize.Y.Offset/2))
doTween(rArrow, smArrowSize, UDim2.new(1, lgImgOffset, 0.5, -smArrowSize.Y.Offset/2))
end
OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject)
if inputObject == TouchObject then
OnInputEnded()
end
end)
GuiService.MenuOpened:connect(function()
if TouchObject then
OnInputEnded()
end
end)
ThumbpadFrame.Parent = parentFrame
end
return Thumbpad
@@ -0,0 +1,178 @@
--[[
// FileName: Thumbstick
// Version 1.0
// Written by: jmargh
// Description: Implements thumbstick controls for touch devices
--]]
local Players = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local GuiService = game:GetService('GuiService')
local MasterControl = require(script.Parent)
local Thumbstick = {}
--[[ Script Variables ]]--
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local IsFollowStick = false
local ThumbstickFrame = nil
local MoveTouchObject = nil
local OnTouchEnded = nil -- defined in Create()
local OnTouchMovedCn = nil
local OnTouchEndedCn = nil
local currentMoveVector = Vector3.new(0,0,0)
--[[ Constants ]]--
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
--[[ Public API ]]--
function Thumbstick:Enable()
ThumbstickFrame.Visible = true
end
function Thumbstick:Disable()
ThumbstickFrame.Visible = false
OnTouchEnded()
end
function Thumbstick:Create(parentFrame)
if ThumbstickFrame then
ThumbstickFrame:Destroy()
ThumbstickFrame = nil
if OnTouchMovedCn then
OnTouchMovedCn:disconnect()
OnTouchMovedCn = nil
end
if OnTouchEndedCn then
OnTouchEndedCn:disconnect()
OnTouchEndedCn = nil
end
end
local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
local isSmallScreen = minAxis <= 500
local thumbstickSize = isSmallScreen and 70 or 120
local position = isSmallScreen and UDim2.new(0, (thumbstickSize/2) - 10, 1, -thumbstickSize - 20) or
UDim2.new(0, thumbstickSize/2, 1, -thumbstickSize * 1.75)
ThumbstickFrame = Instance.new('Frame')
ThumbstickFrame.Name = "ThumbstickFrame"
ThumbstickFrame.Active = true
ThumbstickFrame.Visible = false
ThumbstickFrame.Size = UDim2.new(0, thumbstickSize, 0, thumbstickSize)
ThumbstickFrame.Position = position
ThumbstickFrame.BackgroundTransparency = 1
local outerImage = Instance.new('ImageLabel')
outerImage.Name = "OuterImage"
outerImage.Image = TOUCH_CONTROL_SHEET
outerImage.ImageRectOffset = Vector2.new()
outerImage.ImageRectSize = Vector2.new(220, 220)
outerImage.BackgroundTransparency = 1
outerImage.Size = UDim2.new(0, thumbstickSize, 0, thumbstickSize)
outerImage.Position = UDim2.new(0, 0, 0, 0)
outerImage.Parent = ThumbstickFrame
StickImage = Instance.new('ImageLabel')
StickImage.Name = "StickImage"
StickImage.Image = TOUCH_CONTROL_SHEET
StickImage.ImageRectOffset = Vector2.new(220, 0)
StickImage.ImageRectSize = Vector2.new(111, 111)
StickImage.BackgroundTransparency = 1
StickImage.Size = UDim2.new(0, thumbstickSize/2, 0, thumbstickSize/2)
StickImage.Position = UDim2.new(0, thumbstickSize/2 - thumbstickSize/4, 0, thumbstickSize/2 - thumbstickSize/4)
StickImage.ZIndex = 2
StickImage.Parent = ThumbstickFrame
local centerPosition = nil
local deadZone = 0.05
local function doMove(direction)
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = direction / (thumbstickSize/2)
-- Scaled Radial Dead Zone
local inputAxisMagnitude = currentMoveVector.magnitude
if inputAxisMagnitude < deadZone then
currentMoveVector = Vector3.new()
else
currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone))
-- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed
-- must check for zero length vector is using unit
currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
end
MasterControl:AddToPlayerMovement(currentMoveVector)
end
local function moveStick(pos)
local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y)
local length = relativePosition.magnitude
local maxLength = ThumbstickFrame.AbsoluteSize.x/2
if IsFollowStick and length > maxLength then
local offset = relativePosition.unit * maxLength
ThumbstickFrame.Position = UDim2.new(
0, pos.x - ThumbstickFrame.AbsoluteSize.x/2 - offset.x,
0, pos.y - ThumbstickFrame.AbsoluteSize.y/2 - offset.y)
else
length = math.min(length, maxLength)
relativePosition = relativePosition.unit * length
end
StickImage.Position = UDim2.new(0, relativePosition.x + StickImage.AbsoluteSize.x/2, 0, relativePosition.y + StickImage.AbsoluteSize.y/2)
end
-- input connections
ThumbstickFrame.InputBegan:connect(function(inputObject)
--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
if MoveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
MoveTouchObject = inputObject
ThumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - ThumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - ThumbstickFrame.Size.Y.Offset/2)
centerPosition = Vector2.new(ThumbstickFrame.AbsolutePosition.x + ThumbstickFrame.AbsoluteSize.x/2,
ThumbstickFrame.AbsolutePosition.y + ThumbstickFrame.AbsoluteSize.y/2)
local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
end)
OnTouchMovedCn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed)
if inputObject == MoveTouchObject then
centerPosition = Vector2.new(ThumbstickFrame.AbsolutePosition.x + ThumbstickFrame.AbsoluteSize.x/2,
ThumbstickFrame.AbsolutePosition.y + ThumbstickFrame.AbsoluteSize.y/2)
local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
doMove(direction)
moveStick(inputObject.Position)
end
end)
OnTouchEnded = function()
ThumbstickFrame.Position = position
StickImage.Position = UDim2.new(0, ThumbstickFrame.Size.X.Offset/2 - thumbstickSize/4, 0, ThumbstickFrame.Size.Y.Offset/2 - thumbstickSize/4)
MoveTouchObject = nil
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
MasterControl:SetIsJumping(false)
end
OnTouchEndedCn = UserInputService.TouchEnded:connect(function(inputObject, isProcessed)
if inputObject == MoveTouchObject then
OnTouchEnded()
end
end)
GuiService.MenuOpened:connect(function()
if MoveTouchObject then
OnTouchEnded()
end
end)
ThumbstickFrame.Parent = parentFrame
end
return Thumbstick
@@ -0,0 +1,172 @@
--[[
// FileName: TouchJump
// Version 1.0
// Written by: jmargh
// Description: Implements jump controls for touch devices. Use with Thumbstick and Thumbpad
--]]
local Players = game:GetService('Players')
local GuiService = game:GetService('GuiService')
local TouchJump = {}
local MasterControl = require(script.Parent)
--[[ Script Variables ]]--
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local Humanoid = MasterControl:GetHumanoid()
local JumpButton = nil
local OnInputEnded = nil -- defined in Create()
local CharacterAddedConnection = nil
local HumStateConnection = nil
local HumChangeConnection = nil
local ExternallyEnabled = false
local JumpPower = 0
local JumpStateEnabled = true
--[[ Constants ]]--
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
--[[ Private Functions ]]--
local function disableButton()
JumpButton.Visible = false
OnInputEnded()
end
local function enableButton()
if Humanoid and ExternallyEnabled then
if ExternallyEnabled then
if Humanoid.JumpPower > 0 then
JumpButton.Visible = true
end
end
end
end
local function updateEnabled()
if JumpPower > 0 and JumpStateEnabled then
enableButton()
else
disableButton()
end
end
local function humanoidChanged(prop)
if prop == "JumpPower" then
JumpPower = Humanoid.JumpPower
updateEnabled()
elseif prop == "Parent" then
if not Humanoid.Parent then
HumChangeConnection:disconnect()
end
end
end
local function humandoidStateEnabledChanged(state, isEnabled)
if state == Enum.HumanoidStateType.Jumping then
JumpStateEnabled = isEnabled
updateEnabled()
end
end
local function characterAdded(newCharacter)
if HumChangeConnection then
HumChangeConnection:disconnect()
end
-- rebind event to new Humanoid
Humanoid = nil
repeat
Humanoid = MasterControl:GetHumanoid()
wait()
until Humanoid and Humanoid.Parent == newCharacter
HumChangeConnection = Humanoid.Changed:connect(humanoidChanged)
HumStateConnection = Humanoid.StateEnabledChanged:connect(humandoidStateEnabledChanged)
JumpPower = Humanoid.JumpPower
JumpStateEnabled = Humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping)
updateEnabled()
end
local function setupCharacterAddedFunction()
CharacterAddedConnection = LocalPlayer.CharacterAdded:connect(characterAdded)
if LocalPlayer.Character then
characterAdded(LocalPlayer.Character)
end
end
--[[ Public API ]]--
function TouchJump:Enable()
ExternallyEnabled = true
enableButton()
end
function TouchJump:Disable()
ExternallyEnabled = false
disableButton()
end
function TouchJump:Create(parentFrame)
if JumpButton then
JumpButton:Destroy()
JumpButton = nil
end
local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
local isSmallScreen = minAxis <= 500
local jumpButtonSize = isSmallScreen and 70 or 120
JumpButton = Instance.new('ImageButton')
JumpButton.Name = "JumpButton"
JumpButton.Visible = false
JumpButton.BackgroundTransparency = 1
JumpButton.Image = TOUCH_CONTROL_SHEET
JumpButton.ImageRectOffset = Vector2.new(1, 146)
JumpButton.ImageRectSize = Vector2.new(144, 144)
JumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize)
JumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or
UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75)
local touchObject = nil
JumpButton.InputBegan:connect(function(inputObject)
--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
touchObject = inputObject
JumpButton.ImageRectOffset = Vector2.new(146, 146)
MasterControl:SetIsJumping(true)
end)
OnInputEnded = function()
touchObject = nil
MasterControl:SetIsJumping(false)
JumpButton.ImageRectOffset = Vector2.new(1, 146)
end
JumpButton.InputEnded:connect(function(inputObject)
if inputObject == touchObject then
OnInputEnded()
end
end)
GuiService.MenuOpened:connect(function()
if touchObject then
OnInputEnded()
end
end)
if not CharacterAddedConnection then
setupCharacterAddedFunction()
end
JumpButton.Parent = parentFrame
end
return TouchJump
@@ -0,0 +1,433 @@
local VRService = game:GetService("VRService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local ContextActionService = game:GetService("ContextActionService")
local StarterGui = game:GetService("StarterGui")
local MasterControl = require(script.Parent)
local PathDisplay = nil
local LocalPlayer = Players.LocalPlayer
local VRNavigation = {}
local RECALCULATE_PATH_THRESHOLD = 4
local NO_PATH_THRESHOLD = 12
local MAX_PATHING_DISTANCE = 200
local POINT_REACHED_THRESHOLD = 1
local STOPPING_DISTANCE = 4
local OFFTRACK_TIME_THRESHOLD = 2
local ZERO_VECTOR3 = Vector3.new(0, 0, 0)
local XZ_VECTOR3 = Vector3.new(1, 0, 1)
local THUMBSTICK_DEADZONE = 0.22
local navigationRequestedConn = nil
local heartbeatConn = nil
local currentDestination = nil
local currentPath = nil
local currentPoints = nil
local currentPointIdx = 0
local currentMoveVector = Vector3.new(0, 0, 0)
local expectedTimeToNextPoint = 0
local timeReachedLastPoint = tick()
local movementUpdateEvent = Instance.new("BindableEvent")
movementUpdateEvent.Name = "MovementUpdate"
movementUpdateEvent.Parent = script
coroutine.wrap(function()
local PathDisplayModule = script.Parent.Parent:WaitForChild("PathDisplay")
if PathDisplayModule then
PathDisplay = require(PathDisplayModule)
end
end)()
local function setLaserPointerMode(mode)
pcall(function()
StarterGui:SetCore("VRLaserPointerMode", mode)
end)
end
local function getLocalHumanoid()
local character = LocalPlayer.Character
if not character then
return
end
for _, child in pairs(character:GetChildren()) do
if child:IsA("Humanoid") then
return child
end
end
return nil
end
local function hasBothHandControllers()
return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end
local function hasAnyHandControllers()
return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end
local function isMobileVR()
return UserInputService.TouchEnabled
end
local function hasGamepad()
return UserInputService.GamepadEnabled
end
local function shouldUseNavigationLaser()
--Places where we use the navigation laser:
-- mobile VR with any number of hands tracked
-- desktop VR with only one hand tracked
-- desktop VR with no hands and no gamepad (i.e. with Oculus remote?)
--using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick.
--in the future, we should query thumbstick presence with a features API
if isMobileVR() then
return true
else
if hasBothHandControllers() then
return false
end
if not hasAnyHandControllers() then
return not hasGamepad()
end
return true
end
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
local moving = false
local function startFollowingPath(newPath)
currentPath = newPath
currentPoints = currentPath:GetPointCoordinates()
currentPointIdx = 1
moving = true
timeReachedLastPoint = tick()
local humanoid = getLocalHumanoid()
if humanoid and humanoid.Torso and #currentPoints >= 1 then
local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude
expectedTimeToNextPoint = dist / humanoid.WalkSpeed
end
movementUpdateEvent:Fire("targetPoint", currentDestination)
end
local function goToPoint(point)
currentPath = true
currentPoints = { point }
currentPointIdx = 1
moving = true
local humanoid = getLocalHumanoid()
local distance = (humanoid.Torso.Position - point).magnitude
local estimatedTimeRemaining = distance / humanoid.WalkSpeed
timeReachedLastPoint = tick()
expectedTimeToNextPoint = estimatedTimeRemaining
movementUpdateEvent:Fire("targetPoint", point)
end
local function stopFollowingPath()
currentPath = nil
currentPoints = nil
currentPointIdx = 0
moving = false
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = ZERO_VECTOR3
end
local function tryComputePath(startPos, destination)
local numAttempts = 0
local newPath = nil
while not newPath and numAttempts < 5 do
newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE)
numAttempts = numAttempts + 1
if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then
newPath = nil
break
end
if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then
startPos = startPos + (destination - startPos).unit
newPath = nil
end
if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then
destination = destination + Vector3.new(0, 1, 0)
newPath = nil
end
end
return newPath
end
local function onNavigationRequest(destinationCFrame, requestedWith)
local destinationPosition = destinationCFrame.p
local lastDestination = currentDestination
if not IsFiniteVector3(destinationPosition) then
return
end
currentDestination = destinationPosition
local humanoid = getLocalHumanoid()
if not humanoid or not humanoid.Torso then
return
end
local currentPosition = humanoid.Torso.Position
local distanceToDestination = (currentDestination - currentPosition).magnitude
if distanceToDestination < NO_PATH_THRESHOLD then
goToPoint(currentDestination)
return
end
if not lastDestination or (currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then
local newPath = tryComputePath(currentPosition, currentDestination)
if newPath then
startFollowingPath(newPath)
if PathDisplay then
PathDisplay.setCurrentPoints(currentPoints)
PathDisplay.renderPath()
end
else
stopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
end
else
if moving then
currentPoints[#currentPoints] = currentDestination
else
goToPoint(currentDestination)
end
end
end
local isJumpBound = false
local function onJumpAction(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin then
MasterControl:DoJump()
end
end
local function bindJumpAction(active)
if active then
if not isJumpBound then
isJumpBound = true
ContextActionService:BindAction("VRJumpAction", onJumpAction, false, Enum.KeyCode.ButtonA)
end
else
if isJumpBound then
isJumpBound = false
ContextActionService:UnbindAction("VRJumpAction")
end
end
end
local moveLatch = false
local controlCharacterGamepad = function(actionName, inputState, inputObject)
if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
if inputState == Enum.UserInputState.Cancel then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
return
end
if inputState ~= Enum.UserInputState.End then
stopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
if shouldUseNavigationLaser() then
bindJumpAction(true)
setLaserPointerMode("Hidden")
end
if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
if currentMoveVector.magnitude > 0 then
currentMoveVector = currentMoveVector.unit * math.min(1, inputObject.Position.magnitude)
end
MasterControl:AddToPlayerMovement(currentMoveVector)
moveLatch = true
end
else
MasterControl:AddToPlayerMovement(-currentMoveVector)
currentMoveVector = Vector3.new(0,0,0)
if shouldUseNavigationLaser() then
bindJumpAction(false)
setLaserPointerMode("Navigation")
end
if moveLatch then
moveLatch = false
movementUpdateEvent:Fire("offtrack")
end
end
end
local function onHeartbeat(dt)
local newMoveVector = currentMoveVector
local humanoid = getLocalHumanoid()
if not humanoid or not humanoid.Torso then
return
end
if moving and currentPoints then
local currentPosition = humanoid.Torso.Position
local goalPosition = currentPoints[1]
local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3
local moveDist = vectorToGoal.magnitude
local moveDir = vectorToGoal / moveDist
if moveDist < POINT_REACHED_THRESHOLD then
local estimatedTimeRemaining = 0
local prevPoint = currentPoints[1]
for i, point in pairs(currentPoints) do
if i ~= 1 then
local dist = (point - prevPoint).magnitude
prevPoint = point
estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed)
end
end
table.remove(currentPoints, 1)
currentPointIdx = currentPointIdx + 1
if #currentPoints == 0 then
stopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
return
else
if PathDisplay then
PathDisplay.setCurrentPoints(currentPoints)
PathDisplay.renderPath()
end
local newGoal = currentPoints[1]
local distanceToGoal = (newGoal - currentPosition).magnitude
expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed
timeReachedLastPoint = tick()
end
else
local ignoreTable = {
game.Players.LocalPlayer.Character,
workspace.CurrentCamera
}
local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3)
local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable)
if obstructPart then
local heightOffset = Vector3.new(0, 100, 0)
local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset)
local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable)
local heightDifference = jumpCheckPoint.Y - currentPosition.Y
if heightDifference < 6 and heightDifference > -2 then
humanoid.Jump = true
end
end
local timeSinceLastPoint = tick() - timeReachedLastPoint
if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then
stopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
movementUpdateEvent:Fire("offtrack")
end
newMoveVector = currentMoveVector:Lerp(moveDir, dt * 10)
end
end
if IsFiniteVector3(newMoveVector) then
MasterControl:AddToPlayerMovement(newMoveVector - currentMoveVector)
currentMoveVector = newMoveVector
end
end
local userCFrameEnabledConn = nil
local function onUserCFrameEnabled()
if shouldUseNavigationLaser() then
bindJumpAction(false)
setLaserPointerMode("Navigation")
else
bindJumpAction(true)
setLaserPointerMode("Hidden")
end
end
function VRNavigation:Enable()
navigationRequestedConn = VRService.NavigationRequested:connect(onNavigationRequest)
heartbeatConn = RunService.Heartbeat:connect(onHeartbeat)
ContextActionService:BindAction("MoveThumbstick", controlCharacterGamepad, false, Enum.KeyCode.Thumbstick1)
ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
userCFrameEnabledConn = VRService.UserCFrameEnabled:connect(onUserCFrameEnabled)
onUserCFrameEnabled()
pcall(function()
VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick)
VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY)
end)
end
function VRNavigation:Disable()
stopFollowingPath()
ContextActionService:UnbindAction("MoveThumbstick")
ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
bindJumpAction(false)
setLaserPointerMode("Disabled")
if navigationRequestedConn then
navigationRequestedConn:disconnect()
navigationRequestedConn = nil
end
if heartbeatConn then
heartbeatConn:disconnect()
heartbeatConn = nil
end
if userCFrameEnabledConn then
userCFrameEnabledConn:disconnect()
userCFrameEnabledConn = nil
end
end
return VRNavigation
@@ -0,0 +1,180 @@
--[[
// FileName: VehicleControl
// Version 1.0
// Written by: jmargh
// Description: Implements in-game vehicle controls for all input devices
// NOTE: This works for basic vehicles (single vehicle seat). If you use custom VehicleSeat code,
// multiple VehicleSeats or your own implementation of a VehicleSeat this will not work.
--]]
local VehicleController = {}
local ContextActionService = game:GetService('ContextActionService')
local Players = game:GetService('Players')
local RunService = game:GetService('RunService')
local MasterControl = require(script.Parent)
while not Players.LocalPlayer do
wait()
end
local LocalPlayer = Players.LocalPlayer
local CurrentVehicleSeat = nil
local CurrentThrottle = 0
local CurrentSteer = 0
local HumanoidSeatedCn = nil
local RenderSteppedCn = nil
local Accelerating = false
local Deccelerating = false
local TurningRight = false
local TurningLeft = false
-- 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 function onThrottleAccel(actionName, inputState, inputObject)
MasterControl:AddToPlayerMovement(Vector3.new(0, 0, -CurrentThrottle))
CurrentThrottle = (inputState == Enum.UserInputState.End or Deccelerating) and 0 or -1
MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle))
Accelerating = not (inputState == Enum.UserInputState.End)
if (inputState == Enum.UserInputState.End) and Deccelerating then
CurrentThrottle = 1
MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle))
end
end
local function onThrottleDeccel(actionName, inputState, inputObject)
MasterControl:AddToPlayerMovement(Vector3.new(0, 0, -CurrentThrottle))
CurrentThrottle = (inputState == Enum.UserInputState.End or Accelerating) and 0 or 1
MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle))
Deccelerating = not (inputState == Enum.UserInputState.End)
if (inputState == Enum.UserInputState.End) and Accelerating then
CurrentThrottle = -1
MasterControl:AddToPlayerMovement(Vector3.new(0, 0, CurrentThrottle))
end
end
local function onSteerRight(actionName, inputState, inputObject)
MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, 0))
CurrentSteer = (inputState == Enum.UserInputState.End or TurningLeft) and 0 or 1
MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0))
TurningRight = not (inputState == Enum.UserInputState.End)
if (inputState == Enum.UserInputState.End) and TurningLeft then
CurrentSteer = -1
MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0))
end
end
local function onSteerLeft(actionName, inputState, inputObject)
MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, 0))
CurrentSteer = (inputState == Enum.UserInputState.End or TurningRight) and 0 or -1
MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0))
TurningLeft = not (inputState == Enum.UserInputState.End)
if (inputState == Enum.UserInputState.End) and TurningRight then
CurrentSteer = 1
MasterControl:AddToPlayerMovement(Vector3.new(CurrentSteer, 0, 0))
end
end
local function getHumanoid()
local character = LocalPlayer and LocalPlayer.Character
if character then
for _,child in pairs(character:GetChildren()) do
if child:IsA('Humanoid') then
return child
end
end
end
end
local function getClosestFittingValue(value)
if value > 0.5 then
return 1
elseif value < -0.5 then
return -1
end
return 0
end
local function onRenderStepped()
if CurrentVehicleSeat then
local moveValue = MasterControl:GetMoveVector()
local didSetThrottleSteerFloat = false
didSetThrottleSteerFloat = pcall(function()
if game:GetService("UserInputService"):GetGamepadConnected(Enum.UserInputType.Gamepad1) and onlyTriggersForThrottle and useTriggersForThrottle then
CurrentVehicleSeat.ThrottleFloat = -CurrentThrottle
else
CurrentVehicleSeat.ThrottleFloat = -moveValue.z
end
CurrentVehicleSeat.SteerFloat = moveValue.x
end)
if didSetThrottleSteerFloat == false then
if game:GetService("UserInputService"):GetGamepadConnected(Enum.UserInputType.Gamepad1) and onlyTriggersForThrottle and useTriggersForThrottle then
CurrentVehicleSeat.Throttle = -CurrentThrottle
else
CurrentVehicleSeat.Throttle = getClosestFittingValue(-moveValue.z)
end
CurrentVehicleSeat.Steer = getClosestFittingValue(moveValue.x)
end
end
end
local function onSeated(active, currentSeatPart)
if active then
if currentSeatPart and currentSeatPart:IsA('VehicleSeat') then
CurrentVehicleSeat = currentSeatPart
if useTriggersForThrottle then
ContextActionService:BindAction("throttleAccel", onThrottleAccel, false, Enum.KeyCode.ButtonR2)
ContextActionService:BindAction("throttleDeccel", onThrottleDeccel, false, Enum.KeyCode.ButtonL2)
end
ContextActionService:BindAction("arrowSteerRight", onSteerRight, false, Enum.KeyCode.Right)
ContextActionService:BindAction("arrowSteerLeft", onSteerLeft, false, Enum.KeyCode.Left)
local success = pcall(function() RunService:BindToRenderStep("VehicleControlStep", Enum.RenderPriority.Input.Value, onRenderStepped) end)
if not success then
if RenderSteppedCn then return end
RenderSteppedCn = RunService.RenderStepped:connect(onRenderStepped)
end
end
else
CurrentVehicleSeat = nil
if useTriggersForThrottle then
ContextActionService:UnbindAction("throttleAccel")
ContextActionService:UnbindAction("throttleDeccel")
end
ContextActionService:UnbindAction("arrowSteerRight")
ContextActionService:UnbindAction("arrowSteerLeft")
MasterControl:AddToPlayerMovement(Vector3.new(-CurrentSteer, 0, -CurrentThrottle))
CurrentThrottle = 0
CurrentSteer = 0
local success = pcall(function() RunService:UnbindFromRenderStep("VehicleControlStep") end)
if not success and RenderSteppedCn then
RenderSteppedCn:disconnect()
RenderSteppedCn = nil
end
end
end
local function CharacterAdded(character)
local humanoid = getHumanoid()
while not humanoid do
wait()
humanoid = getHumanoid()
end
--
if HumanoidSeatedCn then
HumanoidSeatedCn:disconnect()
HumanoidSeatedCn = nil
end
HumanoidSeatedCn = humanoid.Seated:connect(onSeated)
end
if LocalPlayer.Character then
CharacterAdded(LocalPlayer.Character)
end
LocalPlayer.CharacterAdded:connect(CharacterAdded)
return VehicleController
@@ -0,0 +1,131 @@
local PathDisplay = {}
PathDisplay.spacing = 8
PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
PathDisplay.imageSize = Vector2.new(2, 2)
local currentPoints = {}
local renderedPoints = {}
local pointModel = Instance.new("Model")
pointModel.Name = "PathDisplayPoints"
local adorneePart = Instance.new("Part")
adorneePart.Anchored = true
adorneePart.CanCollide = false
adorneePart.Transparency = 1
adorneePart.Name = "PathDisplayAdornee"
adorneePart.CFrame = CFrame.new(0, 0, 0)
adorneePart.Parent = pointModel
local pointPool = {}
local poolTop = 30
for i = 1, poolTop do
local point = Instance.new("ImageHandleAdornment")
point.Archivable = false
point.Adornee = adorneePart
point.Image = PathDisplay.image
point.Size = PathDisplay.imageSize
pointPool[i] = point
end
local function retrieveFromPool()
local point = pointPool[1]
if not point then
return
end
pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil
poolTop = poolTop - 1
return point
end
local function returnToPool(point)
poolTop = poolTop + 1
pointPool[poolTop] = point
end
local function renderPoint(point, isLast)
if poolTop == 0 then
return
end
local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0))
local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { game.Players.LocalPlayer.Character, workspace.CurrentCamera })
if not hitPart then
return
end
local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
local point = retrieveFromPool()
point.CFrame = pointCFrame
point.Parent = pointModel
return point
end
function PathDisplay.setCurrentPoints(points)
if typeof(points) == 'table' then
currentPoints = points
else
currentPoints = {}
end
end
function PathDisplay.clearRenderedPath()
for _, oldPoint in ipairs(renderedPoints) do
oldPoint.Parent = nil
returnToPool(oldPoint)
end
renderedPoints = {}
pointModel.Parent = nil
end
function PathDisplay.renderPath()
PathDisplay.clearRenderedPath()
if not currentPoints or #currentPoints == 0 then
return
end
local currentIdx = #currentPoints
local lastPos = currentPoints[currentIdx]
local distanceBudget = 0
renderedPoints[1] = renderPoint(lastPos, true)
if not renderedPoints[1] then
return
end
while true do
local currentPoint = currentPoints[currentIdx]
local nextPoint = currentPoints[currentIdx - 1]
if currentIdx < 2 then
break
else
local toNextPoint = nextPoint - currentPoint
local distToNextPoint = toNextPoint.magnitude
if distanceBudget > distToNextPoint then
distanceBudget = distanceBudget - distToNextPoint
currentIdx = currentIdx - 1
else
local dirToNextPoint = toNextPoint.unit
local pointPos = currentPoint + (dirToNextPoint * distanceBudget)
local point = renderPoint(pointPos, false)
if point then
renderedPoints[#renderedPoints + 1] = point
end
distanceBudget = distanceBudget + PathDisplay.spacing
end
end
end
pointModel.Parent = workspace.CurrentCamera
end
return PathDisplay
@@ -0,0 +1,27 @@
--[[
PlayerModule - This module requires and instantiates the camera and control modules,
and provides getters for developers to access methods on these singletons without
having to modify Roblox-supplied scripts.
2018 PlayerScripts Update - AllYourBlox
--]]
local PlayerModule = {}
PlayerModule.__index = PlayerModule
function PlayerModule.new()
local self = setmetatable({},PlayerModule)
self.cameras = require(script:WaitForChild("CameraModule"))
self.controls = require(script:WaitForChild("ControlModule"))
return self
end
function PlayerModule:GetCameras()
return self.cameras
end
function PlayerModule:GetControls()
return self.controls
end
return PlayerModule.new()
@@ -0,0 +1,548 @@
--[[
CameraModule - This ModuleScript implements a singleton class to manage the
selection, activation, and deactivation of the current camera controller,
character occlusion controller, and transparency controller. This script binds to
RenderStepped at Camera priority and calls the Update() methods on the active
controller instances.
The camera controller ModuleScripts implement classes which are instantiated and
activated as-needed, they are no longer all instantiated up front as they were in
the previous generation of PlayerScripts.
2018 PlayerScripts Update - AllYourBlox
--]]
local CameraModule = {}
CameraModule.__index = CameraModule
-- 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 StarterPlayer = game:GetService("StarterPlayer")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
-- Camera math utility library
local CameraUtils = require(script:WaitForChild("CameraUtils"))
-- Load Roblox Camera Controller Modules
local ClassicCamera = require(script:WaitForChild("ClassicCamera"))
local OrbitalCamera = require(script:WaitForChild("OrbitalCamera"))
local LegacyCamera = require(script:WaitForChild("LegacyCamera"))
-- Load Roblox Occlusion Modules
local Invisicam = require(script:WaitForChild("Invisicam"))
local Poppercam do
local success, useNewPoppercam = pcall(UserSettings().IsUserFeatureEnabled, UserSettings(), "UserNewPoppercam")
if success and useNewPoppercam then
Poppercam = require(script:WaitForChild("Poppercam"))
else
Poppercam = require(script:WaitForChild("Poppercam_Classic"))
end
end
-- Load the near-field character transparency controller and the mouse lock "shift lock" controller
local TransparencyController = require(script:WaitForChild("TransparencyController"))
local MouseLockController = require(script:WaitForChild("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")
local canRegisterCameras = pcall(function() PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default) end)
if canRegisterCameras then
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)
end
end
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
self:OnPlayerAdded(Players.LocalPlayer)
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 = nil
self.hasLastInput = pcall(function()
self.lastInputType = UserInputService:GetLastInputType()
UserInputService.LastInputTypeChanged:Connect(function(newLastInputType)
self.lastInputType = newLastInputType
end)
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 = nil
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
-- Poppercam and any others that get added in the future and need the full player list for 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 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 = nil
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)
local newCameraCFrame = nil
local newCameraFocus = nil
if self.activeCameraController then
newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt)
self.activeCameraController:ApplyVRTransform()
else
newCameraCFrame = game.Workspace.CurrentCamera.CFrame
newCameraFocus = game.Workspace.CurrentCamera.Focus
end
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
-- 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.hasLastInput and 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
return CameraModule.new()
@@ -0,0 +1,46 @@
--[[
BaseOcclusion - Abstract base class for character occlusion control modules
2018 Camera Update - AllYourBlox
--]]
--[[ 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 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
@@ -0,0 +1,125 @@
--[[
CameraUtils - Math utility functions shared by multiple camera scripts
2018 Camera Update - AllYourBlox
--]]
local CameraUtils = {}
local function round(num)
return math.floor(num + 0.5)
end
-- Note, arguments do not match the new math.clamp
-- Eventually we will replace these calls with math.clamp, but right now
-- this is safer as math.clamp is not tolerant of min>max
function CameraUtils.Clamp(low, high, val)
return math.min(math.max(val, low), high)
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 = CameraUtils.Clamp(-1,1,t)
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 CameraUtils.Clamp(-1, 1, point)
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
-- 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
@@ -0,0 +1,240 @@
--[[
ClassicCamera - Classic Roblox camera control module
2018 Camera Update - AllYourBlox
Note: This module also handles camera control types Follow and Track, the
latter of which is currently not distinguished from Classic
--]]
-- 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 PORTRAIT_OFFSET = Vector3.new(0,-3,0)
--[[ Services ]]--
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
local Util = require(script.Parent:WaitForChild("CameraUtils"))
--[[ The Module ]]--
local BaseCamera = require(script.Parent:WaitForChild("BaseCamera"))
local ClassicCamera = setmetatable({}, BaseCamera)
ClassicCamera.__index = ClassicCamera
function ClassicCamera.new()
local self = setmetatable(BaseCamera.new(), ClassicCamera)
self.isFollowCamera = false
self.lastUpdate = tick()
return self
end
function ClassicCamera:GetModuleName()
return "ClassicCamera"
end
-- Movement mode standardized to Enum.ComputerCameraMovementMode values
function ClassicCamera:SetCameraMovementMode( cameraMovementMode )
BaseCamera.SetCameraMovementMode(self,cameraMovementMode)
self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow
end
function ClassicCamera:Test()
print("ClassicCamera:Test()")
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 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
-- 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()
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 = Util.Clamp(0, tweenMaxSpeed, tweenSpeed + tweenAcceleration * timeDelta)
local percent = Util.Clamp(0, 1, tweenSpeed * timeDelta)
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
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 > 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()
self.rotateInput = ZERO_VECTOR2
newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP)
end
else -- is FollowCamera
local newLookVector = self:CalculateNewLookVector()
self.rotateInput = ZERO_VECTOR2
if VRService.VREnabled then
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta)
elseif self.portraitMode then
newCameraFocus = CFrame.new(subjectPosition + PORTRAIT_OFFSET)
else
newCameraFocus = CFrame.new(subjectPosition)
end
newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, self:GetCameraHeight(), 0)
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
@@ -0,0 +1,560 @@
--[[
Invisicam - Occlusion module that makes objects occluding character view semi-transparent
2018 Camera Update - AllYourBlox
--]]
--[[ Camera Maths Utilities Library ]]--
local Util = require(script.Parent:WaitForChild("CameraUtils"))
--[[ Top Level Roblox Services ]]--
local PlayersService = game:GetService("Players")
local RunService = game:GetService("RunService")
--[[ 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 = require(script.Parent:WaitForChild("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 = nil
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 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 - 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 modeName, 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 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 = 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
@@ -0,0 +1,153 @@
--[[
LegacyCamera - Implements legacy controller types: Attach, Fixed, Watch
2018 Camera Update - AllYourBlox
--]]
-- Local private variables and constants
local UNIT_X = Vector3.new(1,0,0)
local UNIT_Y = Vector3.new(0,1,0)
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 VR_PITCH_FRACTION = 0.25
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 PORTRAIT_OFFSET = Vector3.new(0,-3,0)
local Util = require(script.Parent:WaitForChild("CameraUtils"))
--[[ Services ]]--
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
--[[ The Module ]]--
local BaseCamera = require(script.Parent:WaitForChild("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
function LegacyCamera:Test()
print("LegacyCamera:Test()")
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
local humanoid = self:GetHumanoid()
local cameraSubject = camera and 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.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
@@ -0,0 +1,210 @@
--[[
MouseLockController - Replacement for ShiftLockController, manages use of mouse-locked mode
2018 Camera Update - AllYourBlox
--]]
--[[ Constants ]]--
local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png"
local Util = require(script.Parent:WaitForChild("CameraUtils"))
--[[ Services ]]--
local PlayersService = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
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.inputBeganConn = nil
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 keyCode, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do
if token == keyEnum.Name then
self.boundKeys[#self.boundKeys+1] = keyEnum
break
end
end
end
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:OnInputBegan(input, processed)
if processed then return end
if input.UserInputType == Enum.UserInputType.Keyboard then
for _, keyCode in pairs(self.boundKeys) do
if keyCode == input.KeyCode then
self:OnMouseLockToggled()
return
end
end
end
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
if self.inputBeganConn then
self.inputBeganConn:Disconnect()
end
self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed)
self:OnInputBegan(input, processed)
end)
else
-- Disabling
-- Restore mouse cursor
if Mouse.Icon~="" then
Mouse.Icon = ""
end
if self.inputBeganConn then
self.inputBeganConn:Disconnect()
end
self.inputBeganConn = nil
-- 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
@@ -0,0 +1,417 @@
--[[
OrbitalCamera - Spherical coordinates control camera for top-down games
2018 Camera Update - AllYourBlox
--]]
-- Local private variables and constants
local UNIT_X = Vector3.new(1,0,0)
local UNIT_Y = Vector3.new(0,1,0)
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
local VR_PITCH_FRACTION = 0.25
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 PORTRAIT_OFFSET = Vector3.new(0,-3,0)
--[[ 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 = require(script.Parent:WaitForChild("CameraUtils"))
--[[ Services ]]--
local PlayersService = game:GetService('Players')
local VRService = game:GetService("VRService")
--[[ Utility functions specific to OrbitalCamera ]]--
local function GetValueObject(name, defaultValue)
local valueObj = script:FindFirstChild(name)
if valueObj then
return valueObj.Value
end
return defaultValue
end
--[[ The Module ]]--
local BaseCamera = require(script.Parent:WaitForChild("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 = Util.Clamp(self.minDistance, self.maxDistance, desiredSubjectDistance)
-- 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 = Util.Clamp(currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG), xyRotateVector.y)
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
end
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
end
end
function OrbitalCamera:BindGamepadInputActions()
local ContextActionService = game:GetService('ContextActionService')
ContextActionService:BindAction("OrbitalCamGamepadPan", function(name, state, input) self:GetGamepadPan(name, state, input) end, false, Enum.KeyCode.Thumbstick2)
ContextActionService:BindAction("OrbitalCamGamepadZoom", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, Enum.KeyCode.ButtonR3)
ContextActionService:BindAction("OrbitalCamGamepadZoomAlt", function(name, state, input) self:DoGamepadZoom(name, state, input) end, false, 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 humanoid = self:GetHumanoid()
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
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
-- 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 = Util.Clamp(self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad, self.curAzimuthRad)
else
self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0
end
self.curElevationRad = Util.Clamp(self.minElevationRad, self.maxElevationRad, self.curElevationRad + self.rotateInput.y)
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
@@ -0,0 +1,219 @@
--[[
Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view.
--]]
--[[ Camera Maths Utilities Library ]]--
local Util = require(script.Parent:WaitForChild("CameraUtils"))
local RunService = game:GetService("RunService")
local PlayersService = game:GetService("Players")
local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local GameSettings = UserSettings().GameSettings
-- Note: Zoom and Subject modules return single functions, Zoom() and Subject() respectively,
-- whereas Rotate returns a table with two functions, Step() and GetDelta()
local Zoom = require(script:WaitForChild("Zoom"))
local Subject = require(script:WaitForChild("Subject"))
local Rotate = require(script:WaitForChild("Rotate"))
local SetTransparency = require(script:WaitForChild("SetTransparency"))
-- From Input
local INPUT_PRIORITY = Enum.ContextActionPriority.Low.Value
local ROTATION_SPEED_KEYS = math.rad(120)
local ROTATION_SPEED_MOUSE = Vector2.new(1, 0.77)*math.rad(15)
local ROTATION_SPEED_GAMEPAD = Vector2.new(1, 0.77)*math.rad(165)
local ZOOM_SPEED_MOUSE = 4
local ZOOM_SPEED_KEYS = 0.75
-- Gamepad Thumbstick Helper Function
local AnalogCurve do
local DEADZONE = 0.1
function AnalogCurve(x)
local y = (math.abs(x) - DEADZONE)/(1 - DEADZONE)
y = 0.255000975*(2^(2.299113817*y) - 1)
return math.clamp(y, 0, 1)*math.sign(x)
end
end
local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix")
end)
local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled
--[[ The Module ]]--
local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
local Poppercam = setmetatable({}, BaseOcclusion)
Poppercam.__index = Poppercam
function Poppercam.new()
local self = setmetatable(BaseOcclusion.new(), Poppercam)
self.gamepad = {
Thumbstick2 = Vector2.new(),
}
self.keyboard = {
Left = 0,
Right = 0,
I = 0,
O = 0
}
self.mouse = {
Movement = Vector2.new(),
Wheel = 0,
}
self.UISConnections = {}
self.steppedConn = nil
self.worldDt = 1/60
return self
end
function Poppercam:GetPanning()
for _, input in ipairs(UserInputService:GetMouseButtonsPressed()) do
if input.UserInputType == Enum.UserInputType.MouseButton2 then
return true
end
end
return false
end
function Poppercam:GetRotation()
local kKeyboard = Vector2.new(self.keyboard.Left - self.keyboard.Right, 0)
local kGamepad = self.gamepad.Thumbstick2
local kMouse = self.mouse.Movement
self.mouse.Movement = Vector2.new()
local result = kKeyboard*ROTATION_SPEED_KEYS + kGamepad*ROTATION_SPEED_GAMEPAD + kMouse*ROTATION_SPEED_MOUSE
return result.Y, result.X
end
function Poppercam:GetZoomDelta()
local kKeyboard = self.keyboard.O - self.keyboard.I
local kMouse = -self.mouse.Wheel
self.mouse.Wheel = 0
return kKeyboard*ZOOM_SPEED_KEYS + kMouse*ZOOM_SPEED_MOUSE
end
function Poppercam:GetOcclusionMode()
return Enum.DevCameraOcclusionMode.Zoom
end
function Poppercam:Enable(enable)
if enable then
-- Enabling
self:BindInputs()
else
-- Disabling
self:UnbindInputs()
self:ResetDeviceInputs()
end
end
function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus)
self.camera = game.Workspace.CurrentCamera
if self.camera.CameraType == Enum.CameraType.Custom then
local subject, subjectTransform, subjectRoot = Subject(self.worldDt)
if subject then
local pitch, yaw = self:GetRotation()
local zoomDelta = self:GetZoomDelta()
local newFocus = Rotate:Step(self.worldDt, subjectTransform, pitch, yaw) -- vehicle yaw spring must use the world step
local zoom, transparency, firstPerson = Zoom(renderDt, zoomDelta, newFocus, subjectRoot)
local newCameraCFrame = newFocus*CFrame.new(0, 0, zoom)
return newCameraCFrame, newFocus
end
end
return desiredCameraCFrame, desiredCameraFocus
end
function Poppercam:ResetDeviceInputs()
for _, device in pairs{self.gamepad, self.keyboard, self.mouse} do
for k, v in pairs(device) do
device[k] = v*0
end
end
end
function Poppercam:BindInputs()
local function Thumbstick(action, state, input)
local position = input.Position
self.gamepad[input.KeyCode.Name] = Vector2.new(AnalogCurve(position.X), AnalogCurve(position.Y))
end
local function MouseMove(action, state, input)
local delta = input.Delta
self.mouse.Movement = Vector2.new(-delta.X, -delta.Y)
return Enum.ContextActionResult.Pass
end
local function MouseWheel(action, state, input)
self.mouse.Wheel = input.Position.Z
return Enum.ContextActionResult.Pass
end
local function Keypress(action, state, input)
self.keyboard[input.KeyCode.Name] = state == Enum.UserInputState.Begin and 1 or 0
end
ContextActionService:BindActionAtPriority("Camera/Thumbstick", Thumbstick, false, INPUT_PRIORITY,
Enum.KeyCode.Thumbstick2
)
ContextActionService:BindActionAtPriority("Camera/MouseMove", MouseMove, false, INPUT_PRIORITY,
Enum.UserInputType.MouseMovement
)
ContextActionService:BindActionAtPriority("Camera/MouseWheel", MouseWheel, false, INPUT_PRIORITY,
Enum.UserInputType.MouseWheel
)
ContextActionService:BindActionAtPriority("Camera/Keypress", Keypress, false, INPUT_PRIORITY,
Enum.KeyCode.Left,
Enum.KeyCode.Right,
Enum.KeyCode.I,
Enum.KeyCode.O
)
local resetFunction = function() self:ResetDeviceInputs() end
self.UISConnections["TextBoxFocused"] = UserInputService.TextBoxFocused:Connect(resetFunction)
self.UISConnections["TextBoxFocusReleased"] = UserInputService.TextBoxFocusReleased:Connect(resetFunction)
self.UISConnections["WindowFocused"] = UserInputService.WindowFocused:Connect(resetFunction)
self.UISConnections["WindowFocusReleased"] = UserInputService.WindowFocusReleased:Connect(resetFunction)
self.steppedConn = RunService.Stepped:Connect(function(t,dt)
self.worldDt = dt
end)
end
function Poppercam:UnbindInputs()
ContextActionService:UnbindAction("Camera/Thumbstick")
ContextActionService:UnbindAction("Camera/MouseMove")
ContextActionService:UnbindAction("Camera/MouseWheel")
ContextActionService:UnbindAction("Camera/Keypress")
for _, connection in pairs(self.UISConnections) do
connection:Disconnect()
end
self.UISConnections = {}
if self.steppedConn then
self.steppedConn:Disconnect()
end
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
return Poppercam
@@ -0,0 +1,45 @@
local tau = math.pi*2
local clamp = math.clamp
local fromEulerYXZ = CFrame.fromEulerAnglesYXZ
local function lowpass(responsiveness)
local x = 0
return function(nx)
x = responsiveness*nx + (1 - responsiveness)*x
return x
end
end
------------------------------------------------------------------------------
local PITCH_LIMIT = math.rad(88)
local pitch = math.rad(-10)
local yaw = math.rad(90)
local dPitch = 0
local dYaw = 0
local oldSubjectTransform
local dpf = lowpass(0.01)
local dyf = lowpass(0.01)
------------------------------------------------------------------------------
local Rotate = {} do
function Rotate:Step(worldDt, subjectTransform, dPitch, dYaw)
yaw = (yaw + dYaw*worldDt)%tau
pitch = clamp(pitch + dPitch*worldDt, -PITCH_LIMIT, PITCH_LIMIT)
oldSubjectTransform = subjectTransform*fromEulerYXZ(pitch, yaw, 0)
return oldSubjectTransform
end
function Rotate:GetDelta(dt)
local dp = dpf(dPitch)
local dy = dyf(dYaw)
return fromEulerYXZ(dp*dt, dy*dt, 0)
end
end
return Rotate
@@ -0,0 +1,19 @@
local lastSubject = nil
local lastTransparency = 0
return function(subject, transparency)
if transparency == lastTransparency and subject == lastSubject then
return
end
lastSubject = subject
lastTransparency = transparency
local descendants = subject:GetDescendants()
for i = 1, #descendants do
local obj = descendants[i]
if obj:IsA'BasePart' then
obj.LocalTransparencyModifier = transparency
end
end
end
@@ -0,0 +1,75 @@
local ROOT_FOCUS_OFFSET = CFrame.new(0, 2, 0)
local Maid = require(script:WaitForChild("Maid"))
local AngleSpring = require(script:WaitForChild("AngleSpring"))
local camera = game.Workspace.CurrentCamera
---------------------------------------------------------------------------------------
local subjectModel = nil
local subjectRootPart = nil
local subjectIsVehicle = false
---------------------------------------------------------------------------------------
do
local maid = Maid.new()
local function SubjectChanged()
maid:sweep()
local subj = camera.CameraSubject
local function handleHumanoid(humanoid, isVehicle)
subjectModel = humanoid.Parent
subjectRootPart = humanoid.RootPart
subjectIsVehicle = isVehicle
maid:mark(humanoid:GetPropertyChangedSignal('RootPart'):Connect(SubjectChanged))
maid:mark(humanoid:GetPropertyChangedSignal('Parent'):Connect(SubjectChanged))
end
if subj and subj:IsA('Humanoid') then
handleHumanoid(subj, false)
elseif subj and subj:IsA('VehicleSeat') then
handleHumanoid(subj.Occupant, true)
else
subjectModel = nil
subjectRootPart = nil
subjectIsVehicle = false
end
end
SubjectChanged()
camera:GetPropertyChangedSignal'CameraSubject':Connect(SubjectChanged)
end
---------------------------------------------------------------------------------------
local yawSpring = AngleSpring.new(2, 0)
return function(dt)
if subjectModel and subjectRootPart then
local subjectTransform = subjectRootPart.CFrame*ROOT_FOCUS_OFFSET
if not subjectIsVehicle then
subjectTransform = CFrame.new(subjectTransform.p)
else
local _, yaw = subjectTransform:toEulerAnglesYXZ()
yawSpring:setGoal(yaw)
yawSpring:step(dt)
subjectTransform = CFrame.new(subjectTransform.p)*CFrame.fromEulerAnglesYXZ(0, yawSpring:getState(), 0)
end
return subjectModel, subjectTransform, subjectRootPart
else
return nil, CFrame.new(), Vector3.new()
end
end
@@ -0,0 +1,49 @@
-- Critically damped angular spring
local AngleSpring = {} do
AngleSpring.__index = AngleSpring
local pi = math.pi
local tau = math.pi*2
local exp = math.exp
function AngleSpring.new(frequency, position)
local self = setmetatable({}, AngleSpring)
self.f = frequency -- nominal frequency
self.g = position -- goal position
self.p = position -- position
self.v = position*0 -- velocity (*0 so that the types match)
return self
end
function AngleSpring:setGoal(g)
self.g = g
end
function AngleSpring:getState()
return self.p, self.v
end
function AngleSpring:step(dt)
local f = self.f*tau
local g = self.g
local p = self.p
local v = self.v
local offset = (p - g + pi)%tau - pi
local decay = exp(-dt*f)
-- Given:
-- f^2*(x[dt] - g) + 2*d*f*x'[dt] + x''[dt] = 0,
-- x[0] = p,
-- x'[0] = v
-- Solve for x[dt], x'[dt]
self.p = (v*dt + offset*(f*dt + 1))*decay + g
self.v = (v - f*dt*(offset*f + v))*decay
end
end
return AngleSpring
@@ -0,0 +1,41 @@
local Maid = {} do
Maid.__index = Maid
local destructors = {
['function'] = function(item)
item()
end,
['Instance'] = function(item)
item:Destroy()
end,
['RBXScriptConnection'] = function(item)
item:Disconnect()
end,
}
function Maid.new()
local self = setmetatable({}, Maid)
self.trash = {}
return self
end
function Maid:mark(item)
if destructors[typeof(item)] then
local trash = self.trash
trash[#trash + 1] = item
else
error(('No destructor defined for type "%s"'):format(typeof(item)), 2)
end
end
function Maid:sweep()
local trash = self.trash
for i = 1, #trash do
local item = trash[i]
destructors[typeof(item)](item)
end
self.trash = {}
end
end
return Maid
@@ -0,0 +1,82 @@
--------------------------------------------------------------------------------
-- Zoom.lua
-- Controls the distance between the focus and the camera.
--------------------------------------------------------------------------------
local ZOOM_STIFFNESS = 4.5
local ZOOM_DEFAULT = 16
local ZOOM_ACCELERATION = 0.0375
local ZOOM_OPAQUE = 2
local ZOOM_TRANSPARENT = 0.5
local ConstrainedSpring = require(script:WaitForChild("ConstrainedSpring"))
local Popper = require(script:WaitForChild("Popper"))
local cframe = CFrame.new
local clamp = math.clamp
local min = math.min
local max = math.max
local DIST_MIN, DIST_MAX do
local Player = game:GetService("Players").LocalPlayer
local function updateBounds()
DIST_MIN = Player.CameraMinZoomDistance
DIST_MAX = Player.CameraMaxZoomDistance
end
updateBounds()
Player:GetPropertyChangedSignal('CameraMinZoomDistance'):Connect(updateBounds)
Player:GetPropertyChangedSignal('CameraMaxZoomDistance'):Connect(updateBounds)
end
--------------------------------------------------------------------------------
local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, DIST_MIN, DIST_MAX)
local function zoomToTransparency(zoom)
zoom = clamp(zoom, ZOOM_TRANSPARENT, ZOOM_OPAQUE)
local t = (zoom - ZOOM_TRANSPARENT)/(ZOOM_OPAQUE - ZOOM_TRANSPARENT)
return 1 - (3 - 2*t)*t*t
end
local function stepTargetZoom(z, dz, zoomMin, zoomMax)
z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax)
if z < ZOOM_OPAQUE then
z = dz <= 0 and zoomMin or ZOOM_OPAQUE
end
return z
end
--------------------------------------------------------------------------------
function Zoom(renderDt, zoomDelta, focus, subjectRootPart)
local prmax = max(zoomSpring.x, stepTargetZoom(zoomSpring.goal, zoomDelta, DIST_MIN, DIST_MAX))
local poppedZoom = Popper(focus*cframe(0, 0, DIST_MIN), prmax - DIST_MIN, subjectRootPart) + DIST_MIN
local zoomMin = DIST_MIN
local zoomMax = min(DIST_MAX, poppedZoom)
zoomSpring:setBounds(zoomMin, zoomMax)
local isPopped = zoomSpring.goal > zoomMax
if zoomDelta ~= 0 then
if not isPopped then
zoomSpring.goal = stepTargetZoom(zoomSpring.goal, zoomDelta, zoomMin, zoomMax)
elseif zoomDelta < 0 then
zoomSpring.goal = stepTargetZoom(zoomMax, zoomDelta, zoomMin, zoomMax)
end
end
local zoom = zoomSpring:step(renderDt)
local fp = zoomSpring.goal == DIST_MIN
local transparency = fp and zoomToTransparency(zoom) or 0
return zoom, transparency, fp
end
return Zoom
@@ -0,0 +1,61 @@
local ConstrainedSpring = {} do
ConstrainedSpring.__index = ConstrainedSpring
local exp = math.exp
local tau = 2*math.pi
function ConstrainedSpring.new(f, x, min, max)
x = math.clamp(x, min, max)
local self = setmetatable({}, ConstrainedSpring)
self.f = f
self.x = x
self.dx = 0
self.min = min
self.max = max
self.goal = x
return self
end
function ConstrainedSpring:step(dt)
local f = self.f*tau
local x = self.x
local dx = self.dx
local min = self.min
local max = self.max
local goal = self.goal
-- Given:
-- f^2*(x[dt] - g) + 2*d*f*x'[dt] + x''[dt] = 0,
-- x[0] = p,
-- x'[0] = v
-- Solve for x[dt], x'[dt]
local offset = goal - x
local step = f*dt
local decay = exp(-step)
x = goal + (dx*dt - offset*(step + 1))*decay
dx = ((offset*f - dx)*step + dx)*decay
-- Constrain
if x < min then
x = min
dx = 0
elseif x > max then
x = max
dx = 0
end
self.x = x
self.dx = dx
return x
end
function ConstrainedSpring:setBounds(min, max)
self.min = min
self.max = max
end
end
return ConstrainedSpring
@@ -0,0 +1,341 @@
--------------------------------------------------------------------------------
-- Popper.lua
-- Prevents your camera from clipping through walls.
--------------------------------------------------------------------------------
local Rotate = require(script.Parent.Parent:WaitForChild("Rotate"))
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 eraseFromEnd(t, toSize)
for i = #t, toSize + 1, -1 do
t[i] = nil
end
end
local nearZ, 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()
nearZ = camera.NearZ
camera:GetPropertyChangedSignal('NearPlaneZ'):Connect(function()
nearZ = camera.NearPlaneZ
end)
end
local blacklist = {} do
local charMap = {}
local function refreshIgnoreList()
local n = 1
blacklist = {}
for player, 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
do
local Players = game:GetService('Players')
Players.PlayerAdded:Connect(playerAdded)
Players.PlayerRemoving:Connect(playerRemoving)
for _, player in ipairs(Players:GetPlayers()) do
playerAdded(player)
end
refreshIgnoreList()
end
end
--------------------------------------------------------------------------------------------
-- Popper uses the level geometry find an upper bound on subject-to-camera distance.
--
-- Hard limits are applied immediately and unconditionally. They're generally caused
-- when level geometry intersects with the near plane (with exceptions, see below).
--
-- Soft limits are only applied under certain conditions.
-- They're 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 subjectRootPart
-- @todo cache
local function canCollide(part)
return subjectRootPart:CanCollideWith(part)
end
local function canOcclude(part)
-- Filter for opaque, interactable objects
return
part.Transparency < 0.2 and
canCollide(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, blacklist)
local originalSize = #blacklist
repeat
local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList(
ray(origin, dir), blacklist, false, true
)
if hitPart then
if canCollide(hitPart) 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 + nearZ
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, false, true)
local lim = (entryPos - origin).Magnitude
if exitPart then
local promote = false
if lastPos then
promote =
workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, false, true) or
workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, false, 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
end
until hardLimit < inf or not entryPart
eraseFromEnd(blacklist, originalSize)
debug.profileend()
return softLimit - nearZ, hardLimit - nearZ
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 + nearZ*(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 getBodyScale(humanoid, scaleName)
local scaleValue = humanoid:FindFirstChild(scaleName)
if scaleValue and scaleValue:IsA('NumberValue') then
return scaleValue.Value
end
return 1
end
local function testPromotion(focus, dist)
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 vel = subjectRootPart.Velocity
local speed = vel.Magnitude
local maxDist = (getCollisionPoint(fP, vel*SAMPLE_MAX_T) - fP).Magnitude
for dt = 0, min(SAMPLE_MAX_T, maxDist/speed), SAMPLE_DT do
local origin = fP + vel*dt
local dir = -(focus*Rotate:GetDelta(dt)).lookVector
if queryPoint(origin, dir, 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')
local humanoid = subjectRootPart.Parent:FindFirstChildOfClass('Humanoid')
if humanoid then
local scaleX = getBodyScale(humanoid, 'BodyWidthScale')
local scaleY = getBodyScale(humanoid, 'BodyHeightScale')
local scaleZ = getBodyScale(humanoid, 'BodyDepthScale')
local sampleScale = Vector2.new(math.sqrt(scaleX*scaleX + scaleZ*scaleZ), scaleY)
for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do
local scaledOffset = offset*sampleScale
local pos, isHit = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y)
if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then
return false
end
end
end
debug.profileend()
end
debug.profileend()
return true
end
--------------------------------------------------------------------------------
function Popper(focus, targetDist, _subjectRootPart)
debug.profilebegin('popper')
subjectRootPart = _subjectRootPart
local dist = targetDist
local soft, hard = queryViewport(focus, targetDist)
if hard < dist then
dist = hard
end
if soft < dist and testPromotion(focus, targetDist) then
dist = soft
end
subjectRootPart = nil
debug.profileend()
return dist
end
return Popper
@@ -0,0 +1,174 @@
--[[
Poppercam - Occlusion module that brings the camera closer to the subject when objects are blocking the view
Refactored for 2018 Camera Update but functionality is unchanged - AllYourBlox
--]]
--[[ Camera Maths Utilities Library ]]--
local Util = require(script.Parent:WaitForChild("CameraUtils"))
local PlayersService = game:GetService("Players")
local POP_RESTORE_RATE = 0.3
local MIN_CAMERA_ZOOM = 0.5
local VALID_SUBJECTS = {
'Humanoid',
'VehicleSeat',
'SkateboardPlatform',
}
local portraitPopperFixFlagExists, portraitPopperFixFlagEnabled = pcall(function()
return UserSettings():IsUserFeatureEnabled("UserPortraitPopperFix")
end)
local FFlagUserPortraitPopperFix = portraitPopperFixFlagExists and portraitPopperFixFlagEnabled
--[[ The Module ]]--
local BaseOcclusion = require(script.Parent:WaitForChild("BaseOcclusion"))
local Poppercam = setmetatable({}, BaseOcclusion)
Poppercam.__index = Poppercam
function Poppercam.new()
local self = setmetatable(BaseOcclusion.new(), Poppercam)
self.camera = nil
self.cameraSubjectChangeConn = nil
self.subjectPart = nil
self.playerCharacters = {} -- For ignoring in raycasts
self.vehicleParts = {} -- Also just for ignoring
self.lastPopAmount = 0
self.lastZoomLevel = 0
self.popperEnabled = false
return self
end
function Poppercam:GetOcclusionMode()
return Enum.DevCameraOcclusionMode.Zoom
end
function Poppercam:Enable(enable)
end
-- Called when character is added
function Poppercam:CharacterAdded(char, player)
self.playerCharacters[player] = char
end
-- Called when character is about to be removed
function Poppercam:CharacterRemoving(char, player)
self.playerCharacters[player] = nil
end
function Poppercam:Update(dt, desiredCameraCFrame, desiredCameraFocus)
if self.popperEnabled then
self.camera = game.Workspace.CurrentCamera
local newCameraCFrame = desiredCameraCFrame
local focusPoint = desiredCameraFocus.p
if FFlagUserPortraitPopperFix and self.subjectPart then
focusPoint = self.subjectPart.CFrame.p
end
local ignoreList = {}
for _, character in pairs(self.playerCharacters) do
ignoreList[#ignoreList + 1] = character
end
for i = 1, #self.vehicleParts do
ignoreList[#ignoreList + 1] = self.vehicleParts[i]
end
-- Get largest cutoff distance
-- Note that the camera CFrame must be set here, because the current implementation of GetLargestCutoffDistance
-- uses the current camera CFrame directly (it cannot yet be passed the desiredCameraCFrame).
local prevCameraCFrame = self.camera.CFrame
self.camera.CFrame = desiredCameraCFrame
self.camera.Focus = desiredCameraFocus
local largest = self.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 = (desiredCameraCFrame.p - focusPoint).Magnitude
if math.abs(zoomLevel - self.lastZoomLevel) > 0.001 then
self.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 self.lastPopAmount > popAmount then
popAmount = self.lastPopAmount
end
if popAmount > 0 then
newCameraCFrame = desiredCameraCFrame + (desiredCameraCFrame.lookVector * popAmount)
self.lastPopAmount = popAmount - POP_RESTORE_RATE -- Shrink it for the next frame
if self.lastPopAmount < 0 then
self.lastPopAmount = 0
end
end
self.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
return newCameraCFrame, desiredCameraFocus
end
-- Return unchanged values
return desiredCameraCFrame, desiredCameraFocus
end
function Poppercam:OnCameraSubjectChanged(newSubject)
self.vehicleParts = {}
self.lastPopAmount = 0
if newSubject then
-- Determine if we should be popping at all
self.popperEnabled = false
for _, subjectType in pairs(VALID_SUBJECTS) do
if newSubject:IsA(subjectType) then
self.popperEnabled = true
break
end
end
-- Get all parts of the vehicle the player is controlling
if newSubject:IsA('VehicleSeat') then
self.vehicleParts = newSubject:GetConnectedParts(true)
end
if FFlagUserPortraitPopperFix then
if newSubject:IsA("BasePart") then
self.subjectPart = newSubject
elseif newSubject:IsA("Model") then
if newSubject.PrimaryPart then
self.subjectPart = newSubject.PrimaryPart
else
-- Model has no PrimaryPart set, just use first BasePart
-- we can find as better-than-nothing solution (can still fail)
for _, child in pairs(newSubject:GetChildren()) do
if child:IsA("BasePart") then
self.subjectPart = child
break
end
end
end
elseif newSubject:IsA("Humanoid") then
self.subjectPart = newSubject.RootPart
end
end
end
end
return Poppercam
@@ -0,0 +1,189 @@
--[[
TransparencyController - Manages transparency of player character at close camera-to-subject distances
2018 Camera Update - AllYourBlox
--]]
local MAX_TWEEN_RATE = 2.8 -- per second
local Util = require(script.Parent:WaitForChild("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 = Util.Clamp(-maxDelta, maxDelta, deltaTransparency)
end
transparency = self.lastTransparency + deltaTransparency
else
self.transparencyDirty = true
end
transparency = Util.Clamp(0, 1, Util.Round(transparency, 2))
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
@@ -0,0 +1,405 @@
--[[
ControlModule - This ModuleScript implements a singleton class to manage the
selection, activation, and deactivation of the current character movement controller.
This script binds to RenderStepped at Input priority and calls the Update() methods
on the active controller instances.
The character controller ModuleScripts implement classes which are instantiated and
activated as-needed, they are no longer all instantiated up front as they were in
the previous generation of PlayerScripts.
2018 PlayerScripts Update - AllYourBlox
--]]
local ControlModule = {}
ControlModule.__index = ControlModule
--[[ Roblox Services ]]--
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local UserGameSettings = UserSettings():GetService("UserGameSettings")
-- Roblox User Input Control Modules - each returns a new() constructor function used to create controllers as needed
local Keyboard = require(script:WaitForChild("Keyboard"))
local Gamepad = require(script:WaitForChild("Gamepad"))
local TouchDPad = require(script:WaitForChild("TouchDPad"))
local DynamicThumbstick = require(script:WaitForChild("DynamicThumbstick"))
-- These controllers handle only walk/run movement, jumping is handled by the
-- TouchJump controller if any of these are active
local ClickToMove = require(script:WaitForChild("ClickToMoveController"))
local TouchThumbstick = require(script:WaitForChild("TouchThumbstick"))
local TouchThumbpad = require(script:WaitForChild("TouchThumbpad"))
local TouchJump = require(script:WaitForChild("TouchJump"))
-- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching
local movementEnumToModuleMap = {
[Enum.TouchMovementMode.DPad] = TouchDPad,
[Enum.DevTouchMovementMode.DPad] = TouchDPad,
[Enum.TouchMovementMode.Thumbpad] = TouchThumbpad,
[Enum.DevTouchMovementMode.Thumbpad] = TouchThumbpad,
[Enum.TouchMovementMode.Thumbstick] = TouchThumbstick,
[Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick,
[Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
[Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
[Enum.TouchMovementMode.ClickToMove] = ClickToMove,
[Enum.DevTouchMovementMode.ClickToMove] = ClickToMove,
-- Current default
[Enum.TouchMovementMode.Default] = TouchThumbstick,
[Enum.ComputerMovementMode.Default] = Keyboard,
[Enum.ComputerMovementMode.KeyboardMouse] = Keyboard,
[Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard,
[Enum.DevComputerMovementMode.Scriptable] = nil,
[Enum.ComputerMovementMode.ClickToMove] = ClickToMove,
[Enum.DevComputerMovementMode.ClickToMove] = ClickToMove,
}
-- Keyboard controller is really keyboard and mouse controller
local computerInputTypeToModuleMap = {
[Enum.UserInputType.Keyboard] = Keyboard,
[Enum.UserInputType.MouseButton1] = Keyboard,
[Enum.UserInputType.MouseButton2] = Keyboard,
[Enum.UserInputType.MouseButton3] = Keyboard,
[Enum.UserInputType.MouseWheel] = Keyboard,
[Enum.UserInputType.MouseMovement] = Keyboard,
[Enum.UserInputType.Gamepad1] = Gamepad,
[Enum.UserInputType.Gamepad2] = Gamepad,
[Enum.UserInputType.Gamepad3] = Gamepad,
[Enum.UserInputType.Gamepad4] = Gamepad,
}
function ControlModule.new()
local self = setmetatable({},ControlModule)
-- The Modules above are used to construct controller instances as-needed, and this
-- table is a map from Module to the instance created from it
self.controllers = {}
self.activeControlModule = nil -- Used to prevent unnecessarily expensive checks on each input event
self.activeController = nil
self.touchJumpController = nil
self.moveFunction = Players.LocalPlayer.Move
self.humanoid = nil
self.lastInputType = Enum.UserInputType.None
self.cameraRelative = true
-- For Roblox self.vehicleController
self.humanoidSeatedConn = nil
self.vehicleController = nil
self.touchControlFrame = nil
self.vehicleController = require(script:WaitForChild("VehicleController"))
Players.LocalPlayer.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end)
Players.LocalPlayer.CharacterRemoving:Connect(function(char) self:OnCharacterAdded(char) end)
if Players.LocalPlayer.Character then
self:OnCharacterAdded(Players.LocalPlayer.Character)
end
RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, function(dt) self:OnRenderStepped(dt) end)
UserInputService.LastInputTypeChanged:Connect(function(newLastInputType) self:OnLastInputTypeChanged(newLastInputType) end)
local propertyChangeListeners = {
UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function() self:OnTouchMovementModeChange() end),
Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function() self:OnTouchMovementModeChange() end),
UserGameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(function() self:OnComputerMovementModeChange() end),
Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(function() self:OnComputerMovementModeChange() end),
}
--[[ Touch Device UI ]]--
self.playerGui = nil
self.touchGui = nil
self.playerGuiAddedConn = nil
if UserInputService.TouchEnabled then
self.playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
if self.playerGui then
self:CreateTouchGuiContainer()
self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
else
self.playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child)
if child:IsA("PlayerGui") then
self.playerGui = child
self:CreateTouchGuiContainer()
self.playerGuiAddedConn:Disconnect()
self.playerGuiAddedConn = nil
self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
end
end)
end
else
self:OnLastInputTypeChanged(UserInputService:GetLastInputType())
end
return self
end
-- Convenience function so that calling code does not have to first get the activeController
-- and then call GetMoveVector on it. When there is no active controller, this function returns
-- nil so that this case can be distinguished from no current movement (which returns zero vector).
function ControlModule:GetMoveVector()
if self.activeController then
return self.activeController:GetMoveVector()
end
return nil
end
function ControlModule:GetActiveController()
return self.activeController
end
function ControlModule:Enable(enable)
if not self.activeController then
return
end
if enable == nil then
enable = true
end
if enable then
if self.touchControlFrame then
self.activeController:Enable(true, self.touchControlFrame)
else
if self.activeControlModule == ClickToMove then
-- For ClickToMove, when it is the player's choice, we also enable the full keyboard controls.
-- When the developer is forcing click to move, the most keyboard controls (WASD) are not available, only spacebar to jump.
self.activeController:Enable(true, Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice)
else
self.activeController:Enable(true)
end
end
else
self.activeController:Enable(false)
end
end
-- For those who prefer distinct functions
function ControlModule:Disable()
if self.activeController then
self.activeController:Enable(false)
end
end
-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
function ControlModule:SelectComputerMovementModule()
if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then
return nil, false
end
local computerModule = nil
local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode
if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then
computerModule = computerInputTypeToModuleMap[lastInputType]
if UserGameSettings.ComputerMovementMode == Enum.ComputerMovementMode.ClickToMove and computerModule == Keyboard then
-- User has ClickToMove set in Settings, prefer ClickToMove controller for keyboard and mouse lastInputTypes
computerModule = ClickToMove
end
else
-- Developer has selected a mode that must be used.
computerModule = movementEnumToModuleMap[DevMovementMode]
-- computerModule is expected to be nil here only when developer has selected Scriptable
if (not computerModule) and DevMovementMode ~= Enum.DevComputerMovementMode.Scriptable then
warn("No character control module is associated with DevComputerMovementMode ", DevMovementMode)
end
end
if computerModule then
return computerModule, true
elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then
-- Special case where nil is returned and we actually want to set self.activeController to nil for Scriptable
return nil, true
else
-- This case is for when computerModule is nil because of an error and no suitable control module could
-- be found.
return nil, false
end
end
-- Choose current Touch control module based on settings (user, dev)
-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
function ControlModule:SelectTouchModule()
if not UserInputService.TouchEnabled then
return nil, false
end
local touchModule = nil
local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode
if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then
touchModule = movementEnumToModuleMap[UserGameSettings.TouchMovementMode]
elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
return nil, true
else
touchModule = movementEnumToModuleMap[DevMovementMode]
end
return touchModule, true
end
function ControlModule:OnRenderStepped(dt)
if self.activeController and self.activeController.enabled and self.humanoid then
local moveVector = self.activeController:GetMoveVector()
local vehicleConsumedInput = false
if self.vehicleController then
moveVector, vehicleConsumedInput = self.vehicleController:Update(moveVector, self.activeControlModule==Gamepad)
end
-- User of vehicleConsumedInput is commented out to preserve legacy behavior, in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat
--if not vehicleConsumedInput then
self.moveFunction(Players.LocalPlayer, moveVector, self.cameraRelative)
--end
self.humanoid.Jump = self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping())
end
end
function ControlModule:OnHumanoidSeated(active, currentSeatPart)
if active then
if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then
if not self.vehicleController then
self.vehicleController = self.vehicleController.new()
end
self.vehicleController:Enable(true, currentSeatPart)
end
else
if self.vehicleController then
self.vehicleController:Enable(false, currentSeatPart)
end
end
end
function ControlModule:OnCharacterAdded(char)
self.humanoid = char:FindFirstChildOfClass("Humanoid")
while not self.humanoid do
char.ChildAdded:wait()
self.humanoid = char:FindFirstChildOfClass("Humanoid")
end
if self.humanoidSeatedConn then
self.humanoidSeatedConn:Disconnect()
self.humanoidSeatedConn = nil
end
self.humanoidSeatedConn = self.humanoid.Seated:Connect(function(active, currentSeatPart) self:OnHumanoidSeated(active, currentSeatPart) end)
end
function ControlModule:OnCharacterRemoving(char)
self.humanoid = nil
end
-- Helper function to lazily instantiate a controller if it does not yet exist,
-- disable the active controller if it is different from the on being switched to,
-- and then enable the requested controller. The argument to this function must be
-- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc.
function ControlModule:SwitchToController(controlModule)
if not controlModule then
if self.activeController then
self.activeController:Enable(false)
end
self.activeController = nil
self.activeControlModule = nil
else
if not self.controllers[controlModule] then
self.controllers[controlModule] = controlModule.new()
end
if self.activeController ~= self.controllers[controlModule] then
if self.activeController then
self.activeController:Enable(false)
end
self.activeController = self.controllers[controlModule]
self.activeControlModule = controlModule -- Only used to check if controller switch is necessary
if self.touchControlFrame then
self.activeController:Enable(true, self.touchControlFrame)
else
if self.activeControlModule == ClickToMove then
-- For ClickToMove, when it is the player's choice, we also enable the full keyboard controls.
-- When the developer is forcing click to move, the most keyboard controls (WASD) are not available, only spacebar to jump.
self.activeController:Enable(true, Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice)
else
self.activeController:Enable(true)
end
end
if self.touchControlFrame and (self.activeControlModule == TouchThumbpad
or self.activeControlModule == TouchThumbstick
or self.activeControlModule == ClickToMove
or self.activeControlModule == DynamicThumbstick) then
self.touchJumpController = self.controllers[TouchJump]
if not self.touchJumpController then
self.touchJumpController = TouchJump.new()
end
self.touchJumpController:Enable(true, self.touchControlFrame)
else
if self.touchJumpController then
self.touchJumpController:Enable(false)
end
end
end
end
end
function ControlModule:OnLastInputTypeChanged(newLastInputType)
if lastInputType == newLastInputType then
warn("LastInputType Change listener called with current type.")
end
lastInputType = newLastInputType
if lastInputType == Enum.UserInputType.Touch then
-- TODO: Check if touch module already active
local touchModule, success = self:SelectTouchModule()
if success then
while not self.touchControlFrame do
wait()
end
self:SwitchToController(touchModule)
end
elseif computerInputTypeToModuleMap[lastInputType] ~= nil then
local computerModule = self:SelectComputerMovementModule()
if computerModule then
self:SwitchToController(computerModule)
end
end
end
-- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of
-- current control scheme
function ControlModule:OnComputerMovementModeChange()
local controlModule, success = self:SelectComputerMovementModule()
if success then
self:SwitchToController(controlModule)
end
end
function ControlModule:OnTouchMovementModeChange()
local touchModule, success = self:SelectTouchModule()
if success then
while not self.touchControlFrame do
wait()
end
self:SwitchToController(touchModule)
end
end
function ControlModule:CreateTouchGuiContainer()
if self.touchGui then self.touchGui:Destroy() end
-- Container for all touch device guis
self.touchGui = Instance.new('ScreenGui')
self.touchGui.Name = "TouchGui"
self.touchGui.ResetOnSpawn = false
self.touchControlFrame = Instance.new("Frame")
self.touchControlFrame.Name = "TouchControlFrame"
self.touchControlFrame.Size = UDim2.new(1, 0, 1, 0)
self.touchControlFrame.BackgroundTransparency = 1
self.touchControlFrame.Parent = self.touchGui
self.touchGui.Parent = self.playerGui
end
return ControlModule.new()
@@ -0,0 +1,40 @@
--[[
BaseCharacterController - Abstract base class for character controllers, not intended to be
directly instantiated.
2018 PlayerScripts Update - AllYourBlox
--]]
local ZERO_VECTOR3 = Vector3.new(0,0,0)
--[[ Roblox Services ]]--
local Players = game:GetService("Players")
--[[ The Module ]]--
local BaseCharacterController = {}
BaseCharacterController.__index = BaseCharacterController
function BaseCharacterController.new()
local self = setmetatable({}, BaseCharacterController)
self.enabled = false
self.moveVector = ZERO_VECTOR3
self.isJumping = false
return self
end
function BaseCharacterController:GetMoveVector()
return self.moveVector
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
@@ -0,0 +1,530 @@
--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
local MIDDLE_TRANSPARENCIES = {
1 - 0.89,
1 - 0.70,
1 - 0.60,
1 - 0.50,
1 - 0.40,
1 - 0.30,
1 - 0.25
}
local NUM_MIDDLE_IMAGES = #MIDDLE_TRANSPARENCIES
local FADE_IN_OUT_BACKGROUND = true
local FADE_IN_OUT_MAX_ALPHA = 0.35
local FADE_IN_OUT_HALF_DURATION_DEFAULT = 0.3
local FADE_IN_OUT_BALANCE_DEFAULT = 0.5
local ThumbstickFadeTweenInfo = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local DynamicThumbstick = setmetatable({}, BaseCharacterController)
DynamicThumbstick.__index = DynamicThumbstick
function DynamicThumbstick.new()
local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick)
self.humanoid = nil
self.tools = {}
self.toolEquipped = nil
self.revertAutoJumpEnabledToFalse = false
self.moveTouchObject = nil
self.moveTouchStartPosition = nil
self.startImage = nil
self.endImage = nil
self.middleImages = {}
self.startImageFadeTween = nil
self.endImageFadeTween = nil
self.middleImageFadeTweens = {}
self.isFirstTouch = true
self.isFollowStick = false
self.thumbstickFrame = nil
self.onTouchMovedConn = nil
self.onTouchEndedConn = nil
self.onTouchActivateConn = nil
self.onRenderSteppedConn = nil
self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
self.hasFadedBackgroundInPortrait = false
self.hasFadedBackgroundInLandscape = false
self.tweenInAlphaStart = nil
self.tweenOutAlphaStart = nil
-- If this module changes a player's humanoid's AutoJumpEnabled, it saves
-- the previous state in this variable to revert to
self.shouldRevertAutoJumpOnDisable = false
return self
end
-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump
-- rather than sustained jumping. This is only to preserve the current behavior through the refactor.
function DynamicThumbstick:GetIsJumping()
local wasJumping = self.isJumping
self.isJumping = false
return wasJumping
end
function DynamicThumbstick:EnableAutoJump(enable)
local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
if humanoid then
if enable then
self.shouldRevertAutoJumpOnDisable = (humanoid.AutoJumpEnabled == false) and (Players.LocalPlayer.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice)
humanoid.AutoJumpEnabled = true
elseif self.shouldRevertAutoJumpOnDisable then
humanoid.AutoJumpEnabled = false
end
end
end
function DynamicThumbstick:Enable(enable, uiParentFrame)
if enable == nil then return false end -- If nil, return false (invalid argument)
enable = enable and true or false -- Force anything non-nil to boolean before comparison
if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
if enable then
-- Enable
if not self.thumbstickFrame then
self:Create(uiParentFrame)
end
if Players.LocalPlayer.Character then
self:OnCharacterAdded(Players.LocalPlayer.Character)
else
Players.LocalPlayer.CharacterAdded:Connect(function(char)
self:OnCharacterAdded(char)
end)
end
else
-- Disable
self:OnInputEnded() -- Cleanup
end
self.enabled = enable
self.thumbstickFrame.Visible = enable
end
function DynamicThumbstick:OnCharacterAdded(char)
for _, child in ipairs(char:GetChildren()) do
if child:IsA("Tool") then
self.toolEquipped = child
end
end
char.ChildAdded:Connect(function(child)
if child:IsA("Tool") then
self.toolEquipped = child
elseif child:IsA("Humanoid") then
self:EnableAutoJump(true)
end
end)
char.ChildRemoved:Connect(function(child)
if child == self.toolEquipped then
self.toolEquipped = nil
end
end)
self.humanoid = char:FindFirstChildOfClass("Humanoid")
if self.humanoid then
self:EnableAutoJump(true)
end
end
-- Was called OnMoveTouchEnded in previous version
function DynamicThumbstick:OnInputEnded()
self.moveTouchObject = nil
self.moveVector = ZERO_VECTOR3
self:FadeThumbstick(false)
self.thumbstickFrame.Active = true
end
function DynamicThumbstick:FadeThumbstick(visible)
if not visible and self.moveTouchObject then
return
end
if self.isFirstTouch then return end
if self.startImageFadeTween then
self.startImageFadeTween:Cancel()
end
if self.endImageFadeTween then
self.endImageFadeTween:Cancel()
end
for i = 1, #self.middleImages do
if self. middleImageFadeTweens[i] then
self.middleImageFadeTweens[i]:Cancel()
end
end
if visible then
self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0 })
self.startImageFadeTween:Play()
self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 0.2 })
self.endImageFadeTween:Play()
for i = 1, #self.middleImages do
self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = MIDDLE_TRANSPARENCIES[i] })
self.middleImageFadeTweens[i]:Play()
end
else
self.startImageFadeTween = TweenService:Create(self.startImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
self.startImageFadeTween:Play()
self.endImageFadeTween = TweenService:Create(self.endImage, ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
self.endImageFadeTween:Play()
for i = 1, #self.middleImages do
self.middleImageFadeTweens[i] = TweenService:Create(self.middleImages[i], ThumbstickFadeTweenInfo, { ImageTransparency = 1 })
self.middleImageFadeTweens[i]:Play()
end
end
end
function DynamicThumbstick:FadeThumbstickFrame(fadeDuration, fadeRatio)
self.fadeInAndOutHalfDuration = fadeDuration * 0.5
self.fadeInAndOutBalance = fadeRatio
self.tweenInAlphaStart = tick()
end
function DynamicThumbstick:Create(parentFrame)
if self.thumbstickFrame then
self.thumbstickFrame:Destroy()
self.thumbstickFrame = nil
if self.onTouchMovedConn then
self.onTouchMovedConn:Disconnect()
self.onTouchMovedConn = nil
end
if self.onTouchEndedConn then
self.onTouchEndedCon:Disconnect()
self.onTouchEndedCon = nil
end
if self.onRenderSteppedConn then
self.onRenderSteppedConn:Disconnect()
self.onRenderSteppedConn = nil
end
if self.onTouchActivateConn then
self.onTouchActivateConn:Disconnect()
self.onTouchActivateConn = nil
end
end
local ThumbstickSize = 45
local ThumbstickRingSize = 20
local MiddleSize = 10
local MiddleSpacing = MiddleSize + 4
local RadiusOfDeadZone = 2
local RadiusOfMaxSpeed = 20
local screenSize = parentFrame.AbsoluteSize
local isBigScreen = math.min(screenSize.x, screenSize.y) > 500
if isBigScreen then
ThumbstickSize = ThumbstickSize * 2
ThumbstickRingSize = ThumbstickRingSize * 2
MiddleSize = MiddleSize * 2
MiddleSpacing = MiddleSpacing * 2
RadiusOfDeadZone = RadiusOfDeadZone * 2
RadiusOfMaxSpeed = RadiusOfMaxSpeed * 2
end
local function layoutThumbstickFrame(portraitMode)
if portraitMode then
self.thumbstickFrame.Size = UDim2.new(1, 0, 0.4, 0)
self.thumbstickFrame.Position = UDim2.new(0, 0, 0.6, 0)
else
self.thumbstickFrame.Size = UDim2.new(0.4, 0, 2/3, 0)
self.thumbstickFrame.Position = UDim2.new(0, 0, 1/3, 0)
end
end
self.thumbstickFrame = Instance.new("TextButton")
self.thumbstickFrame.Text = ""
self.thumbstickFrame.Name = "DynamicThumbstickFrame"
self.thumbstickFrame.Visible = false
self.thumbstickFrame.BackgroundTransparency = 1.0
self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
layoutThumbstickFrame(false)
self.startImage = Instance.new("ImageLabel")
self.startImage.Name = "ThumbstickStart"
self.startImage.Visible = true
self.startImage.BackgroundTransparency = 1
self.startImage.Image = TOUCH_CONTROLS_SHEET
self.startImage.ImageRectOffset = Vector2.new(1,1)
self.startImage.ImageRectSize = Vector2.new(144, 144)
self.startImage.ImageColor3 = Color3.new(0, 0, 0)
self.startImage.AnchorPoint = Vector2.new(0.5, 0.5)
self.startImage.Position = UDim2.new(0, ThumbstickRingSize * 3.3, 1, -ThumbstickRingSize * 2.8)
self.startImage.Size = UDim2.new(0, ThumbstickRingSize * 3.7, 0, ThumbstickRingSize * 3.7)
self.startImage.ZIndex = 10
self.startImage.Parent = self.thumbstickFrame
self.endImage = Instance.new("ImageLabel")
self.endImage.Name = "ThumbstickEnd"
self.endImage.Visible = true
self.endImage.BackgroundTransparency = 1
self.endImage.Image = TOUCH_CONTROLS_SHEET
self.endImage.ImageRectOffset = Vector2.new(1,1)
self.endImage.ImageRectSize = Vector2.new(144, 144)
self.endImage.AnchorPoint = Vector2.new(0.5, 0.5)
self.endImage.Position = self.startImage.Position
self.endImage.Size = UDim2.new(0, ThumbstickSize * 0.8, 0, ThumbstickSize * 0.8)
self.endImage.ZIndex = 10
self.endImage.Parent = self.thumbstickFrame
for i = 1, NUM_MIDDLE_IMAGES do
self.middleImages[i] = Instance.new("ImageLabel")
self.middleImages[i].Name = "ThumbstickMiddle"
self.middleImages[i].Visible = false
self.middleImages[i].BackgroundTransparency = 1
self.middleImages[i].Image = TOUCH_CONTROLS_SHEET
self.middleImages[i].ImageRectOffset = Vector2.new(1,1)
self.middleImages[i].ImageRectSize = Vector2.new(144, 144)
self.middleImages[i].ImageTransparency = MIDDLE_TRANSPARENCIES[i]
self.middleImages[i].AnchorPoint = Vector2.new(0.5, 0.5)
self.middleImages[i].ZIndex = 9
self.middleImages[i].Parent = self.thumbstickFrame
end
local CameraChangedConn = nil
local function onCurrentCameraChanged()
if CameraChangedConn then
CameraChangedConn:Disconnect()
CameraChangedConn = nil
end
local newCamera = workspace.CurrentCamera
if newCamera then
local function onViewportSizeChanged()
local size = newCamera.ViewportSize
local portraitMode = size.X < size.Y
layoutThumbstickFrame(portraitMode)
end
CameraChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(onViewportSizeChanged)
onViewportSizeChanged()
end
end
workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(onCurrentCameraChanged)
if workspace.CurrentCamera then
onCurrentCameraChanged()
end
self.moveTouchStartPosition = nil
self.startImageFadeTween = nil
self.endImageFadeTween = nil
self.middleImageFadeTweens = {}
local function doMove(direction)
local currentMoveVector = direction
-- Scaled Radial Dead Zone
local inputAxisMagnitude = currentMoveVector.magnitude
if inputAxisMagnitude < RadiusOfDeadZone then
currentMoveVector = Vector3.new()
else
currentMoveVector = currentMoveVector.unit*(1 - math.max(0, (RadiusOfMaxSpeed - currentMoveVector.magnitude)/RadiusOfMaxSpeed))
currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
end
self.moveVector = currentMoveVector
end
local function layoutMiddleImages(startPos, endPos)
local startDist = (ThumbstickSize / 2) + MiddleSize
local vector = endPos - startPos
local distAvailable = vector.magnitude - (ThumbstickRingSize / 2) - MiddleSize
local direction = vector.unit
local distNeeded = MiddleSpacing * NUM_MIDDLE_IMAGES
local spacing = MiddleSpacing
if distNeeded < distAvailable then
spacing = distAvailable / NUM_MIDDLE_IMAGES
end
for i = 1, NUM_MIDDLE_IMAGES do
local image = self.middleImages[i]
local distWithout = startDist + (spacing * (i - 2))
local currentDist = startDist + (spacing * (i - 1))
if distWithout < distAvailable then
local pos = endPos - direction * currentDist
local exposedFraction = math.clamp(1 - ((currentDist - distAvailable) / spacing), 0, 1)
image.Visible = true
image.Position = UDim2.new(0, pos.X, 0, pos.Y)
image.Size = UDim2.new(0, MiddleSize * exposedFraction, 0, MiddleSize * exposedFraction)
else
image.Visible = false
end
end
end
local function moveStick(pos)
local startPos = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y) - self.thumbstickFrame.AbsolutePosition
local endPos = Vector2.new(pos.X, pos.Y) - self.thumbstickFrame.AbsolutePosition
self.endImage.Position = UDim2.new(0, endPos.X, 0, endPos.Y)
layoutMiddleImages(startPos, endPos)
end
-- input connections
self.thumbstickFrame.InputBegan:Connect(function(inputObject)
if inputObject.UserInputType ~= Enum.UserInputType.Touch or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
if self.moveTouchObject then
return
end
if self.isFirstTouch then
self.isFirstTouch = false
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out,0,false,0)
TweenService:Create(self.startImage, tweenInfo, {Size = UDim2.new(0, 0, 0, 0)}):Play()
TweenService:Create(self.endImage, tweenInfo, {Size = UDim2.new(0, ThumbstickSize, 0, ThumbstickSize), ImageColor3 = Color3.new(0,0,0)}):Play()
end
self.moveTouchObject = inputObject
self.moveTouchStartPosition = inputObject.Position
local startPosVec2 = Vector2.new(inputObject.Position.X - self.thumbstickFrame.AbsolutePosition.X, inputObject.Position.Y - self.thumbstickFrame.AbsolutePosition.Y)
self.startImage.Visible = true
self.startImage.Position = UDim2.new(0, startPosVec2.X, 0, startPosVec2.Y)
self.endImage.Visible = true
self.endImage.Position = self.startImage.Position
self:FadeThumbstick(true)
moveStick(inputObject.Position)
if FADE_IN_OUT_BACKGROUND then
local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
local hasFadedBackgroundInOrientation = false
-- only fade in/out the background once per orientation
if playerGui then
if playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight then
hasFadedBackgroundInOrientation = self.hasFadedBackgroundInLandscape
self.hasFadedBackgroundInLandscape = true
elseif playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait then
hasFadedBackgroundInOrientation = self.hasFadedBackgroundInPortrait
self.hasFadedBackgroundInPortrait = true
end
end
if not hasFadedBackgroundInOrientation then
self.fadeInAndOutHalfDuration = FADE_IN_OUT_HALF_DURATION_DEFAULT
self.fadeInAndOutBalance = FADE_IN_OUT_BALANCE_DEFAULT
self.tweenInAlphaStart = tick()
end
end
end)
self.onTouchMovedConn = UserInputService.TouchMoved:connect(function(inputObject)
if inputObject == self.moveTouchObject then
self.thumbstickFrame.Active = false
local direction = Vector2.new(inputObject.Position.x - self.moveTouchStartPosition.x, inputObject.Position.y - self.moveTouchStartPosition.y)
if math.abs(direction.x) > 0 or math.abs(direction.y) > 0 then
doMove(direction)
moveStick(inputObject.Position)
end
end
end)
self.onRenderSteppedConn = RunService.RenderStepped:Connect(function()
if self.tweenInAlphaStart ~= nil then
local delta = tick() - self.tweenInAlphaStart
local fadeInTime = (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeInTime, 1)
if delta > fadeInTime then
self.tweenOutAlphaStart = tick()
self.tweenInAlphaStart = nil
end
elseif self.tweenOutAlphaStart ~= nil then
local delta = tick() - self.tweenOutAlphaStart
local fadeOutTime = (self.fadeInAndOutHalfDuration * 2) - (self.fadeInAndOutHalfDuration * 2 * self.fadeInAndOutBalance)
self.thumbstickFrame.BackgroundTransparency = 1 - FADE_IN_OUT_MAX_ALPHA + FADE_IN_OUT_MAX_ALPHA*math.min(delta/fadeOutTime, 1)
if delta > fadeOutTime then
self.tweenOutAlphaStart = nil
end
end
end)
self.onTouchEndedConn = UserInputService.TouchEnded:connect(function(inputObject)
if inputObject == self.moveTouchObject then
self:OnInputEnded()
end
end)
GuiService.MenuOpened:connect(function()
if self.moveTouchObject then
self:OnInputEnded()
end
end)
local playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
while not playerGui do
Players.LocalPlayer.ChildAdded:wait()
playerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
end
local playerGuiChangedConn = nil
local originalScreenOrientationWasLandscape = playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeLeft or
playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.LandscapeRight
local function longShowBackground()
self.fadeInAndOutHalfDuration = 2.5
self.fadeInAndOutBalance = 0.05
self.tweenInAlphaStart = tick()
end
playerGuiChangedConn = playerGui.Changed:connect(function(prop)
if prop == "CurrentScreenOrientation" then
if (originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation == Enum.ScreenOrientation.Portrait) or
(not originalScreenOrientationWasLandscape and playerGui.CurrentScreenOrientation ~= Enum.ScreenOrientation.Portrait) then
playerGuiChangedConn:disconnect()
longShowBackground()
if originalScreenOrientationWasLandscape then
self.hasFadedBackgroundInPortrait = true
else
self.hasFadedBackgroundInLandscape = true
end
end
end
end)
self.thumbstickFrame.Parent = parentFrame
spawn(function()
if game:IsLoaded() then
longShowBackground()
else
game.Loaded:wait()
longShowBackground()
end
end)
end
return DynamicThumbstick
@@ -0,0 +1,234 @@
--[[
Gamepad Character Control - This module handles controlling your avatar using a game console-style controller
2018 PlayerScripts Update - AllYourBlox
--]]
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local NONE = Enum.UserInputType.None
local thumbstickDeadzone = 0.2
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local Gamepad = setmetatable({}, BaseCharacterController)
Gamepad.__index = Gamepad
function Gamepad.new()
local self = setmetatable(BaseCharacterController.new(), Gamepad)
self.forwardValue = 0
self.backwardValue = 0
self.leftValue = 0
self.rightValue = 0
self.activeGamepad = NONE -- Enum.UserInputType.Gamepad1, 2, 3...
self.gamepadConnectedConn = nil
self.gamepadDisconnectedConn = nil
return self
end
function Gamepad:Enable(enable)
if not UserInputService.GamepadEnabled then
return false
end
if enable == self.enabled then
-- Module is already in the state being requested. True is returned here since the module will be in the state
-- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
-- no action was necessary. False indicates failure to be in requested/expected state.
return true
end
self.forwardValue = 0
self.backwardValue = 0
self.leftValue = 0
self.rightValue = 0
self.moveVector = ZERO_VECTOR3
if enable then
self.activeGamepad = self:GetHighestPriorityGamepad()
if self.activeGamepad ~= NONE then
self:BindContextActions()
self:ConnectGamepadConnectionListeners()
else
-- No connected gamepads, failure to enable
return false
end
else
self:UnbindContextActions()
self:DisconnectGamepadConnectionListeners()
self.activeGamepad = NONE
end
self.enabled = enable
return true
end
-- This function selects the lowest number gamepad from the currently-connected gamepad
-- and sets it as the active gamepad
function Gamepad:GetHighestPriorityGamepad()
local connectedGamepads = UserInputService:GetConnectedGamepads()
local bestGamepad = NONE -- Note that this value is higher than all valid gamepad values
for _, gamepad in pairs(connectedGamepads) do
if gamepad.Value < bestGamepad.Value then
bestGamepad = gamepad
end
end
return bestGamepad
end
function Gamepad:BindContextActions()
if self.activeGamepad == NONE then
-- There must be an active gamepad to set up bindings
return false
end
local updateMovement = function(inputState)
if inputState == Enum.UserInputState.Cancel then
self.moveVector = ZERO_VECTOR3
else
self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
end
end
-- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
-- which fixes them from getting stuck on.
local handleMoveForward = function(actionName, inputState, inputObject)
self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
updateMovement(inputState)
end
local handleMoveBackward = function(actionName, inputState, inputObject)
self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
updateMovement(inputState)
end
local handleMoveLeft = function(actionName, inputState, inputObject)
self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
updateMovement(inputState)
end
local handleMoveRight = function(actionName, inputState, inputObject)
self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
updateMovement(inputState)
end
local handleJumpAction = function(actionName, inputState, inputObject)
self.isJumping = (inputState == Enum.UserInputState.Begin)
end
local handleThumbstickInput = function(actionName, inputState, inputObject)
if self.activeGamepad ~= inputObject.UserInputType then return end
if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
if inputState == Enum.UserInputState.Cancel then
self.moveVector = ZERO_VECTOR3
return
end
if inputObject.Position.magnitude > thumbstickDeadzone then
self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
else
self.moveVector = ZERO_VECTOR3
end
end
ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
ContextActionService:BindAction("jumpAction",handleJumpAction, false, Enum.KeyCode.ButtonA)
ContextActionService:BindAction("moveThumbstick",handleThumbstickInput, false, Enum.KeyCode.Thumbstick1)
return true
end
function Gamepad:UnbindContextActions()
if self.activeGamepad ~= NONE then
ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
end
ContextActionService:UnbindAction("moveThumbstick")
ContextActionService:UnbindAction("jumpAction")
end
function Gamepad:OnNewGamepadConnected()
-- A new gamepad has been connected.
local bestGamepad = self:GetHighestPriorityGamepad()
if bestGamepad == self.activeGamepad then
-- A new gamepad was connected, but our active gamepad is not changing
return
end
if bestGamepad == NONE then
-- There should be an active gamepad when GamepadConnected fires, so this should not
-- normally be hit. If there is no active gamepad, unbind actions but leave
-- the module enabled and continue to listen for a new gamepad connection.
warn("Gamepad:OnNewGamepadConnected found no connected gamepads")
self:UnbindContextActions()
return
end
if self.activeGamepad ~= NONE then
-- Switching from one active gamepad to another
ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
end
self.activeGamepad = bestGamepad
ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
end
function Gamepad:OnCurrentGamepadDisconnected()
if self.activeGamepad ~= NONE then
ContextActionService:UnbindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
end
local bestGamepad = self:GetHighestPriorityGamepad()
if self.activeGamepad ~= NONE and bestGamepad == self.activeGamepad then
warn("Gamepad:OnCurrentGamepadDisconnected found the supposedly disconnected gamepad in connectedGamepads.")
self:UnbindContextActions()
self.activeGamepad = NONE
return
end
if bestGamepad == NONE then
-- No active gamepad, unbinding actions but leaving gamepad connection listener active
self:UnbindContextActions()
self.activeGamepad = NONE
else
-- Set new gamepad as active and bind to tool activation
self.activeGamepad = bestGamepad
ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2)
end
end
function Gamepad:ConnectGamepadConnectionListeners()
self.gamepadConnectedConn = UserInputService.GamepadConnected:Connect(function(gamepadEnum)
self:OnNewGamepadConnected()
end)
self.gamepadDisconnectedConn = UserInputService.GamepadDisconnected:Connect(function(gamepadEnum)
if self.activeGamepad == gamepadEnum then
self:OnCurrentGamepadDisconnected()
end
end)
end
function Gamepad:DisconnectGamepadConnectionListeners()
if self.gamepadConnectedConn then
self.gamepadConnectedConn:Disconnect()
self.gamepadConnectedConn = nil
end
if self.gamepadDisconnectedConn then
self.gamepadDisconnectedConn:Disconnect()
self.gamepadDisconnectedConn = nil
end
end
return Gamepad
@@ -0,0 +1,149 @@
--[[
Keyboard Character Control - This module handles controlling your avatar from a keyboard
2018 PlayerScripts Update - AllYourBlox
--]]
--[[ Roblox Services ]]--
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local Keyboard = setmetatable({}, BaseCharacterController)
Keyboard.__index = Keyboard
function Keyboard.new()
local self = setmetatable(BaseCharacterController.new(), Keyboard)
self.textFocusReleasedConn = nil
self.textFocusGainedConn = nil
self.windowFocusReleasedConn = nil
self.forwardValue = 0
self.backwardValue = 0
self.leftValue = 0
self.rightValue = 0
return self
end
function Keyboard:Enable(enable)
if not UserInputService.KeyboardEnabled then
return false
end
if enable == self.enabled then
-- Module is already in the state being requested. True is returned here since the module will be in the state
-- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
-- no action was necessary. False indicates failure to be in requested/expected state.
return true
end
self.forwardValue = 0
self.backwardValue = 0
self.leftValue = 0
self.rightValue = 0
if enable then
self:BindContextActions()
self:ConnectFocusEventListeners()
else
self:UnbindContextActions()
self:DisconnectFocusEventListeners()
end
self.enabled = enable
return true
end
function Keyboard:UpdateMovement(inputState)
if inputState == Enum.UserInputState.Cancel then
self.moveVector = ZERO_VECTOR3
else
self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
end
end
function Keyboard:BindContextActions()
-- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
-- which fixes them from getting stuck on.
local handleMoveForward = function(actionName, inputState, inputObject)
self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
self:UpdateMovement(inputState)
end
local handleMoveBackward = function(actionName, inputState, inputObject)
self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
self:UpdateMovement(inputState)
end
local handleMoveLeft = function(actionName, inputState, inputObject)
self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
self:UpdateMovement(inputState)
end
local handleMoveRight = function(actionName, inputState, inputObject)
self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
self:UpdateMovement(inputState)
end
local handleJumpAction = function(actionName, inputState, inputObject)
self.isJumping = (inputState == Enum.UserInputState.Begin)
end
-- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to
-- movement direction is done in Lua
ContextActionService:BindAction("moveForwardAction", handleMoveForward, false, Enum.PlayerActions.CharacterForward)
ContextActionService:BindAction("moveBackwardAction", handleMoveBackward, false, Enum.PlayerActions.CharacterBackward)
ContextActionService:BindAction("moveLeftAction", handleMoveLeft, false, Enum.PlayerActions.CharacterLeft)
ContextActionService:BindAction("moveRightAction", handleMoveRight, false, Enum.PlayerActions.CharacterRight)
ContextActionService:BindAction("jumpAction",handleJumpAction,false,Enum.PlayerActions.CharacterJump)
end
function Keyboard:UnbindContextActions()
ContextActionService:UnbindAction("moveForwardAction")
ContextActionService:UnbindAction("moveBackwardAction")
ContextActionService:UnbindAction("moveLeftAction")
ContextActionService:UnbindAction("moveRightAction")
ContextActionService:UnbindAction("jumpAction")
end
function Keyboard:ConnectFocusEventListeners()
local function onFocusReleased()
self.moveVector = ZERO_VECTOR3
self.forwardValue = 0
self.backwardValue = 0
self.leftValue = 0
self.rightValue = 0
self.isJumping = false
end
local function onTextFocusGained(textboxFocused)
self.isJumping = false
end
self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased)
self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained)
self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased)
end
function Keyboard:DisconnectFocusEventListeners()
if self.textFocusReleasedCon then
self.textFocusReleasedCon:Disconnect()
self.textFocusReleasedCon = nil
end
if self.textFocusGainedConn then
self.textFocusGainedConn:Disconnect()
self.textFocusGainedConn = nil
end
if self.windowFocusReleasedConn then
self.windowFocusReleasedConn:Disconnect()
self.windowFocusReleasedConn = nil
end
end
return Keyboard
@@ -0,0 +1,131 @@
local PathDisplay = {}
PathDisplay.spacing = 8
PathDisplay.image = "rbxasset://textures/Cursors/Gamepad/Pointer.png"
PathDisplay.imageSize = Vector2.new(2, 2)
local currentPoints = {}
local renderedPoints = {}
local pointModel = Instance.new("Model")
pointModel.Name = "PathDisplayPoints"
local adorneePart = Instance.new("Part")
adorneePart.Anchored = true
adorneePart.CanCollide = false
adorneePart.Transparency = 1
adorneePart.Name = "PathDisplayAdornee"
adorneePart.CFrame = CFrame.new(0, 0, 0)
adorneePart.Parent = pointModel
local pointPool = {}
local poolTop = 30
for i = 1, poolTop do
local point = Instance.new("ImageHandleAdornment")
point.Archivable = false
point.Adornee = adorneePart
point.Image = PathDisplay.image
point.Size = PathDisplay.imageSize
pointPool[i] = point
end
local function retrieveFromPool()
local point = pointPool[1]
if not point then
return
end
pointPool[1], pointPool[poolTop] = pointPool[poolTop], nil
poolTop = poolTop - 1
return point
end
local function returnToPool(point)
poolTop = poolTop + 1
pointPool[poolTop] = point
end
local function renderPoint(point, isLast)
if poolTop == 0 then
return
end
local rayDown = Ray.new(point + Vector3.new(0, 2, 0), Vector3.new(0, -8, 0))
local hitPart, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(rayDown, { game.Players.LocalPlayer.Character, workspace.CurrentCamera })
if not hitPart then
return
end
local pointCFrame = CFrame.new(hitPoint, hitPoint + hitNormal)
local point = retrieveFromPool()
point.CFrame = pointCFrame
point.Parent = pointModel
return point
end
function PathDisplay.setCurrentPoints(points)
if typeof(points) == 'table' then
currentPoints = points
else
currentPoints = {}
end
end
function PathDisplay.clearRenderedPath()
for _, oldPoint in ipairs(renderedPoints) do
oldPoint.Parent = nil
returnToPool(oldPoint)
end
renderedPoints = {}
pointModel.Parent = nil
end
function PathDisplay.renderPath()
PathDisplay.clearRenderedPath()
if not currentPoints or #currentPoints == 0 then
return
end
local currentIdx = #currentPoints
local lastPos = currentPoints[currentIdx]
local distanceBudget = 0
renderedPoints[1] = renderPoint(lastPos, true)
if not renderedPoints[1] then
return
end
while true do
local currentPoint = currentPoints[currentIdx]
local nextPoint = currentPoints[currentIdx - 1]
if currentIdx < 2 then
break
else
local toNextPoint = nextPoint - currentPoint
local distToNextPoint = toNextPoint.magnitude
if distanceBudget > distToNextPoint then
distanceBudget = distanceBudget - distToNextPoint
currentIdx = currentIdx - 1
else
local dirToNextPoint = toNextPoint.unit
local pointPos = currentPoint + (dirToNextPoint * distanceBudget)
local point = renderPoint(pointPos, false)
if point then
renderedPoints[#renderedPoints + 1] = point
end
distanceBudget = distanceBudget + PathDisplay.spacing
end
end
end
pointModel.Parent = workspace.CurrentCamera
end
return PathDisplay
@@ -0,0 +1,176 @@
--[[
--]]
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
--[[ Constants ]]--
local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png"
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local COMPASS_DIR = {
Vector3.new(1, 0, 0), -- E
Vector3.new(1, 0, 1).unit, -- SE
Vector3.new(0, 0, 1), -- S
Vector3.new(-1, 0, 1).unit, -- SW
Vector3.new(-1, 0, 0), -- W
Vector3.new(-1, 0, -1).unit, -- NW
Vector3.new(0, 0, -1), -- N
Vector3.new(1, 0, -1).unit, -- NE
}
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local TouchDPad = setmetatable({}, BaseCharacterController)
TouchDPad.__index = TouchDPad
function TouchDPad.new()
local self = setmetatable(BaseCharacterController.new(), TouchDPad)
self.DPadFrame = nil
self.touchObject = nil
self.flBtn = nil
self.frBtn = nil
return self
end
--[[ Local Functions ]]--
local function CreateArrowLabel(name, position, size, rectOffset, rectSize, parent)
local image = Instance.new("ImageLabel")
image.Name = name
image.Image = DPAD_SHEET
image.ImageRectOffset = rectOffset
image.ImageRectSize = rectSize
image.BackgroundTransparency = 1
image.Size = size
image.Position = position
image.Parent = parent
return image
end
function TouchDPad:GetCenterPosition()
return Vector2.new(self.DPadFrame.AbsolutePosition.x + self.DPadFrame.AbsoluteSize.x * 0.5, self.DPadFrame.AbsolutePosition.y + self.DPadFrame.AbsoluteSize.y * 0.5)
end
function TouchDPad:Enable(enable, uiParentFrame)
if enable == nil then return false end -- If nil, return false (invalid argument)
enable = enable and true or false -- Force anything non-nil to boolean before comparison
if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
if enable then
-- Enable
if not self.DPadFrame then
self:Create(uiParentFrame)
end
self.DPadFrame.Visible = true
else
-- Disable
self.DPadFrame.Visible = false
self:OnInputEnded()
end
self.enabled = enable
end
-- Note: Overrides base class GetIsJumping with get-and-clear behavior to do a single jump
-- rather than sustained jumping. This is only to preserve the current behavior through the refactor.
function TouchDPad:GetIsJumping()
local wasJumping = self.isJumping
self.isJumping = false
return wasJumping
end
function TouchDPad:OnInputEnded()
self.touchObject =nil
if self.flBtn then self.flBtn.Visible = false end
if self.frBtn then self.frBtn.Visible = false end
self.moveVector = ZERO_VECTOR3
end
function TouchDPad:Create(parentFrame)
if self.DPadFrame then
self.DPadFrame:Destroy()
self.DPadFrame = nil
end
local position = UDim2.new(0, 10, 1, -230)
self.DPadFrame = Instance.new("Frame")
self.DPadFrame.Name = "DPadFrame"
self.DPadFrame.Active = true
self.DPadFrame.Visible = false
self.DPadFrame.Size = UDim2.new(0, 192, 0, 192)
self.DPadFrame.Position = position
self.DPadFrame.BackgroundTransparency = 1
local smArrowSize = UDim2.new(0, 23, 0, 23)
local lgArrowSize = UDim2.new(0, 64, 0, 64)
local smImgOffset = Vector2.new(46, 46)
local lgImgOffset = Vector2.new(128, 128)
local bBtn = CreateArrowLabel("BackButton", UDim2.new(0.5, -32, 1, -64), lgArrowSize, Vector2.new(0, 0), lgImgOffset, self.DPadFrame)
local fBtn = CreateArrowLabel("ForwardButton", UDim2.new(0.5, -32, 0, 0), lgArrowSize, Vector2.new(0, 258), lgImgOffset, self.DPadFrame)
local lBtn = CreateArrowLabel("LeftButton", UDim2.new(0, 0, 0.5, -32), lgArrowSize, Vector2.new(129, 129), lgImgOffset, self.DPadFrame)
local rBtn = CreateArrowLabel("RightButton", UDim2.new(1, -64, 0.5, -32), lgArrowSize, Vector2.new(0, 129), lgImgOffset, self.DPadFrame)
local jumpBtn = CreateArrowLabel("JumpButton", UDim2.new(0.5, -32, 0.5, -32), lgArrowSize, Vector2.new(129, 0), lgImgOffset, self.DPadFrame)
self.flBtn = CreateArrowLabel("ForwardLeftButton", UDim2.new(0, 35, 0, 35), smArrowSize, Vector2.new(129, 258), smImgOffset, self.DPadFrame)
self.frBtn = CreateArrowLabel("ForwardRightButton", UDim2.new(1, -55, 0, 35), smArrowSize, Vector2.new(176, 258), smImgOffset, self.DPadFrame)
self.flBtn.Visible = false
self.frBtn.Visible = false
-- input connections
jumpBtn.InputBegan:Connect(function(inputObject)
self.isJumping = true
end)
local function normalizeDirection(inputPosition)
local jumpRadius = jumpBtn.AbsoluteSize.x*0.5
local centerPosition = self:GetCenterPosition()
local direction = Vector2.new(inputPosition.x - centerPosition.x, inputPosition.y - centerPosition.y)
if direction.magnitude > jumpRadius then
local angle = math.atan2(direction.y, direction.x)
local octant = (math.floor(8 * angle / (2 * math.pi) + 8.5)%8) + 1
self.moveVector = COMPASS_DIR[octant]
end
if not self.flBtn.Visible and self.moveVector == COMPASS_DIR[7] then
self.flBtn.Visible = true
self.frBtn.Visible = true
end
end
self.DPadFrame.InputBegan:Connect(function(inputObject)
if self.touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch then
return
end
self.touchObject = inputObject
normalizeDirection(self.touchObject.Position)
end)
self.DPadFrame.InputChanged:Connect(function(inputObject)
if inputObject == self.touchObject then
normalizeDirection(self.touchObject.Position)
self.isJumping = false
end
end)
self.DPadFrame.InputEnded:connect(function(inputObject)
if inputObject == self.touchObject then
self:OnInputEnded()
end
end)
GuiService.MenuOpened:Connect(function()
if self.touchObject then
self:OnInputEnded()
end
end)
self.DPadFrame.Parent = parentFrame
end
return TouchDPad
@@ -0,0 +1,201 @@
--[[
// FileName: TouchJump
// Version 1.0
// Written by: jmargh
// Description: Implements jump controls for touch devices. Use with Thumbstick and Thumbpad
--]]
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
--[[ Constants ]]--
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png"
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local TouchJump = setmetatable({}, BaseCharacterController)
TouchJump.__index = TouchJump
function TouchJump.new()
local self = setmetatable(BaseCharacterController.new(), TouchJump)
self.parentUIFrame = nil
self.jumpButton = nil
self.characterAddedConn = nil
self.humanoidStateEnabledChangedConn = nil
self.humanoidJumpPowerConn = nil
self.humanoidParentConn = nil
self.externallyEnabled = false
self.jumpPower = 0
self.jumpStateEnabled = true
self.isJumping = false
self.humanoid = nil -- saved reference because property change connections are made using it
return self
end
function TouchJump:EnableButton(enable)
if enable then
if not self.jumpButton then
self:Create()
end
local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
if humanoid and self.externallyEnabled then
if self.externallyEnabled then
if humanoid.JumpPower > 0 then
self.jumpButton.Visible = true
end
end
end
else
self.jumpButton.Visible = false
self.isJumping = false
self.jumpButton.ImageRectOffset = Vector2.new(176, 222)
end
end
function TouchJump:UpdateEnabled()
if self.jumpPower > 0 and self.jumpStateEnabled then
self:EnableButton(true)
else
self:EnableButton(false)
end
end
function TouchJump:HumanoidChanged(prop)
local humanoid = Players.LocalPlayer.Character and Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
if humanoid then
if prop == "JumpPower" then
self.jumpPower = humanoid.JumpPower
self:UpdateEnabled()
elseif prop == "Parent" then
if not humanoid.Parent then
self.humanoidChangeConn:Disconnect()
end
end
end
end
function TouchJump:HumanoidStateEnabledChanged(state, isEnabled)
if state == Enum.HumanoidStateType.Jumping then
self.jumpStateEnabled = isEnabled
self:UpdateEnabled()
end
end
function TouchJump:CharacterAdded(char)
if self.humanoidChangeConn then
self.humanoidChangeConn:Disconnect()
self.humanoidChangeConn = nil
end
self.humanoid = char:FindFirstChildOfClass("Humanoid")
while not self.humanoid do
char.ChildAdded:wait()
self.humanoid = char:FindFirstChildOfClass("Humanoid")
end
self.humanoidJumpPowerConn = self.humanoid:GetPropertyChangedSignal("JumpPower"):Connect(function()
self.jumpPower = self.humanoid.JumpPower
self:UpdateEnabled()
end)
self.humanoidParentConn = self.humanoid:GetPropertyChangedSignal("Parent"):Connect(function()
if not self.humanoid.Parent then
self.humanoidJumpPowerConn:Disconnect()
self.humanoidJumpPowerConn = nil
self.humanoidParentConn:Disconnect()
self.humanoidParentConn = nil
end
end)
self.humanoidStateEnabledChangedConn = self.humanoid.StateEnabledChanged:Connect(function(state, enabled)
self:HumanoidStateEnabledChanged(state, enabled)
end)
self.jumpPower = self.humanoid.JumpPower
self.jumpStateEnabled = self.humanoid:GetStateEnabled(Enum.HumanoidStateType.Jumping)
self:UpdateEnabled()
end
function TouchJump:SetupCharacterAddedFunction()
self.characterAddedConn = Players.LocalPlayer.CharacterAdded:Connect(function(char)
self:CharacterAdded(char)
end)
if Players.LocalPlayer.Character then
self:CharacterAdded(Players.LocalPlayer.Character)
end
end
function TouchJump:Enable(enable, parentFrame)
self.parentUIFrame = parentFrame
self.externallyEnabled = enable
self:EnableButton(enable)
end
function TouchJump:Create()
if not self.parentUIFrame then
return
end
if self.jumpButton then
self.jumpButton:Destroy()
self.jumpButton = nil
end
local minAxis = math.min(self.parentUIFrame.AbsoluteSize.x, self.parentUIFrame.AbsoluteSize.y)
local isSmallScreen = minAxis <= 500
local jumpButtonSize = isSmallScreen and 70 or 120
self.jumpButton = Instance.new("ImageButton")
self.jumpButton.Name = "JumpButton"
self.jumpButton.Visible = false
self.jumpButton.BackgroundTransparency = 1
self.jumpButton.Image = TOUCH_CONTROL_SHEET
self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
self.jumpButton.ImageRectSize = Vector2.new(144, 144)
self.jumpButton.Size = UDim2.new(0, jumpButtonSize, 0, jumpButtonSize)
self.jumpButton.Position = isSmallScreen and UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize - 20) or
UDim2.new(1, -(jumpButtonSize*1.5-10), 1, -jumpButtonSize * 1.75)
local touchObject = nil
self.jumpButton.InputBegan:connect(function(inputObject)
--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
if touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
touchObject = inputObject
self.jumpButton.ImageRectOffset = Vector2.new(146, 146)
self.isJumping = true
end)
local OnInputEnded = function()
touchObject = nil
self.isJumping = false
self.jumpButton.ImageRectOffset = Vector2.new(1, 146)
end
self.jumpButton.InputEnded:connect(function(inputObject)
if inputObject == touchObject then
OnInputEnded()
end
end)
GuiService.MenuOpened:connect(function()
if touchObject then
OnInputEnded()
end
end)
if not self.characterAddedConn then
self:SetupCharacterAddedFunction()
end
self.jumpButton.Parent = self.parentUIFrame
end
return TouchJump
@@ -0,0 +1,247 @@
--[[
TouchThumbpad
--]]
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
--[[ Constants ]]--
local DPAD_SHEET = "rbxasset://textures/ui/DPadSheet.png"
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local UNIT_Z = Vector3.new(0,0,1)
local UNIT_X = Vector3.new(1,0,0)
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local TouchThumbpad = setmetatable({}, BaseCharacterController)
TouchThumbpad.__index = TouchThumbpad
function TouchThumbpad.new()
local self = setmetatable(BaseCharacterController.new(), TouchThumbpad)
self.thumbpadFrame = nil
self.touchChangedConn = nil
self.touchEndedConn = nil
self.menuOpenedConn = nil
self.screenPos = nil
self.isRight, self.isLeft, self.isUp, self.isDown = false, false, false, false
self.smArrowSize = nil
self.lgArrowSize = nil
self.smImgOffset = nil
self.lgImgOffset = nil
return self
end
--[[ Local Helper Functions ]]--
local function doTween(guiObject, endSize, endPosition)
guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true)
end
local function CreateArrowLabel(name, position, size, rectOffset, rectSize, parent)
local image = Instance.new("ImageLabel")
image.Name = name
image.Image = DPAD_SHEET
image.ImageRectOffset = rectOffset
image.ImageRectSize = rectSize
image.BackgroundTransparency = 1
image.ImageColor3 = Color3.fromRGB(190, 190, 190)
image.Size = size
image.Position = position
image.Parent = parent
return image
end
function TouchThumbpad:Enable(enable, uiParentFrame)
if enable == nil then return false end -- If nil, return false (invalid argument)
enable = enable and true or false -- Force anything non-nil to boolean before comparison
if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
if enable then
-- Enable
if not self.thumbpadFrame then
self:Create(uiParentFrame)
end
self.thumbpadFrame.Visible = true
else
-- Disable
self.thumbpadFrame.Visible = false
self:OnInputEnded()
end
self.enabled = enable
end
function TouchThumbpad:OnInputEnded()
self.moveVector = Vector3.new(0,0,0)
self.isJumping = false
self.thumbpadFrame.Position = self.screenPos
self.touchObject = nil
self.isUp, self.isDown, self.isLeft, self.isRight = false, false, false, false
doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset))
doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset))
doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
end
function TouchThumbpad:Create(parentFrame)
if self.thumbpadFrame then
self.thumbpadFrame:Destroy()
self.thumbpadFrame = nil
end
if self.touchChangedConn then
self.touchChangedConn:Disconnect()
self.touchChangedConn = nil
end
if self.touchEndedConn then
self.touchEndedConn:Disconnect()
self.touchEndedConn = nil
end
if self.menuOpenedConn then
self.menuOpenedConn:Disconnect()
self.menuOpenedConn = nil
end
local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
local isSmallScreen = minAxis <= 500
local thumbpadSize = isSmallScreen and 70 or 120
self.screenPos = isSmallScreen and UDim2.new(0, thumbpadSize * 1.25, 1, -thumbpadSize - 20) or
UDim2.new(0, thumbpadSize * 0.5 - 10, 1, -thumbpadSize * 1.75 - 10)
self.thumbpadFrame = Instance.new("Frame")
self.thumbpadFrame.Name = "ThumbpadFrame"
self.thumbpadFrame.Visible = false
self.thumbpadFrame.Active = true
self.thumbpadFrame.Size = UDim2.new(0, thumbpadSize + 20, 0, thumbpadSize + 20)
self.thumbpadFrame.Position = self.screenPos
self.thumbpadFrame.BackgroundTransparency = 1
local outerImage = Instance.new("ImageLabel")
outerImage.Name = "OuterImage"
outerImage.Image = TOUCH_CONTROL_SHEET
outerImage.ImageRectOffset = Vector2.new(0, 0)
outerImage.ImageRectSize = Vector2.new(220, 220)
outerImage.BackgroundTransparency = 1
outerImage.Size = UDim2.new(0, thumbpadSize, 0, thumbpadSize)
outerImage.Position = UDim2.new(0, 10, 0, 10)
outerImage.Parent = self.thumbpadFrame
self.smArrowSize = isSmallScreen and UDim2.new(0, 32, 0, 32) or UDim2.new(0, 64, 0, 64)
self.lgArrowSize = UDim2.new(0, self.smArrowSize.X.Offset * 2, 0, self.smArrowSize.Y.Offset * 2)
local imgRectSize = Vector2.new(110, 110)
self.smImgOffset = isSmallScreen and -4 or -9
self.lgImgOffset = isSmallScreen and -28 or -55
self.dArrow = CreateArrowLabel("DownArrow", UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset), self.smArrowSize, Vector2.new(8, 8), imgRectSize, outerImage)
self.uArrow = CreateArrowLabel("UpArrow", UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset), self.smArrowSize, Vector2.new(8, 266), imgRectSize, outerImage)
self.lArrow = CreateArrowLabel("LeftArrow", UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset), self.smArrowSize, Vector2.new(137, 137), imgRectSize, outerImage)
self.rArrow = CreateArrowLabel("RightArrow", UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset), self.smArrowSize, Vector2.new(8, 137), imgRectSize, outerImage)
local function doTween(guiObject, endSize, endPosition)
guiObject:TweenSizeAndPosition(endSize, endPosition, Enum.EasingDirection.InOut, Enum.EasingStyle.Linear, 0.15, true)
end
local padOrigin = nil
local deadZone = 0.1
self.isRight, self.isLeft, self.isUp, self.isDown = false, false, false, false
local function doMove(pos)
local moveDelta = pos - padOrigin
local moveVector2 = 2 * moveDelta / thumbpadSize
-- Scaled Radial Dead Zone
if moveVector2.Magnitude < deadZone then
self.moveVector = ZERO_VECTOR3
else
moveVector2 = moveVector2.unit * ((moveVector2.Magnitude - deadZone) / (1 - deadZone))
-- prevent NAN Vector from trying to do zerovector.Unit
if moveVector2.Magnitude == 0 then
self.moveVector = ZERO_VECTOR3
else
self.moveVector = Vector3.new(moveVector2.x, 0, moveVector2.y).Unit
end
end
local forwardDot = self.moveVector:Dot(-UNIT_Z)
local rightDot = self.moveVector:Dot(UNIT_X)
if forwardDot > 0.5 then -- UP
if not self.isUp then
self.isUp, self.isDown = true, false
doTween(self.uArrow, self.lgArrowSize, UDim2.new(0.5, -self.smArrowSize.X.Offset, 0, self.smImgOffset - 1.5*self.smArrowSize.Y.Offset))
doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset))
end
elseif forwardDot < -0.5 then -- DOWN
if not self.isDown then
self.isDown, self.isUp = true, false
doTween(self.dArrow, self.lgArrowSize, UDim2.new(0.5, -self.smArrowSize.X.Offset, 1, self.lgImgOffset + 0.5*self.smArrowSize.Y.Offset))
doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset))
end
else
self.isUp, self.isDown = false, false
doTween(self.dArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 1, self.lgImgOffset))
doTween(self.uArrow, self.smArrowSize, UDim2.new(0.5, -0.5*self.smArrowSize.X.Offset, 0, self.smImgOffset))
end
if rightDot > 0.5 then
if not self.isRight then
self.isRight, self.isLeft = true, false
doTween(self.rArrow, self.lgArrowSize, UDim2.new(1, self.lgImgOffset + 0.5*self.smArrowSize.X.Offset, 0.5, -self.smArrowSize.Y.Offset))
doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
end
elseif rightDot < -0.5 then
if not self.isLeft then
self.isLeft, self.isRight = true, false
doTween(self.lArrow, self.lgArrowSize, UDim2.new(0, self.smImgOffset - 1.5*self.smArrowSize.X.Offset, 0.5, -self.smArrowSize.Y.Offset))
doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
end
else
self.isRight, self.isLeft = false, false
doTween(self.lArrow, self.smArrowSize, UDim2.new(0, self.smImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
doTween(self.rArrow, self.smArrowSize, UDim2.new(1, self.lgImgOffset, 0.5, -0.5*self.smArrowSize.Y.Offset))
end
end
--input connections
self.thumbpadFrame.InputBegan:connect(function(inputObject)
--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
if self.touchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
self.thumbpadFrame.Position = UDim2.new(0, inputObject.Position.x - 0.5*self.thumbpadFrame.AbsoluteSize.x, 0, inputObject.Position.y - 0.5*self.thumbpadFrame.Size.Y.Offset)
padOrigin = Vector3.new(self.thumbpadFrame.AbsolutePosition.x +0.5* self.thumbpadFrame.AbsoluteSize.x,
self.thumbpadFrame.AbsolutePosition.y + 0.5*self.thumbpadFrame.AbsoluteSize.y, 0)
doMove(inputObject.Position)
self.touchObject = inputObject
end)
self.touchChangedConn = UserInputService.TouchMoved:connect(function(inputObject, isProcessed)
if inputObject == self.touchObject then
doMove(self.touchObject.Position)
end
end)
self.touchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject)
if inputObject == self.touchObject then
self:OnInputEnded()
end
end)
self.menuOpenedConn = GuiService.MenuOpened:Connect(function()
if self.touchObject then
self:OnInputEnded()
end
end)
self.thumbpadFrame.Parent = parentFrame
end
return TouchThumbpad
@@ -0,0 +1,194 @@
--[[
TouchThumbstick
--]]
local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local TOUCH_CONTROL_SHEET = "rbxasset://textures/ui/TouchControlsSheet.png"
--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local TouchThumbstick = setmetatable({}, BaseCharacterController)
TouchThumbstick.__index = TouchThumbstick
function TouchThumbstick.new()
local self = setmetatable(BaseCharacterController.new(), TouchThumbstick)
self.isFollowStick = false
self.thumbstickFrame = nil
self.moveTouchObject = nil
self.onTouchMovedConn = nil
self.onTouchEndedConn = nil
self.screenPos = nil
self.stickImage = nil
self.thumbstickSize = nil -- Float
return self
end
function TouchThumbstick:Enable(enable, uiParentFrame)
if enable == nil then return false end -- If nil, return false (invalid argument)
enable = enable and true or false -- Force anything non-nil to boolean before comparison
if self.enabled == enable then return true end -- If no state change, return true indicating already in requested state
if enable then
-- Enable
if not self.thumbstickFrame then
self:Create(uiParentFrame)
end
self.thumbstickFrame.Visible = true
else
-- Disable
self.thumbstickFrame.Visible = false
self:OnInputEnded()
end
self.enabled = enable
end
function TouchThumbstick:OnInputEnded()
self.thumbstickFrame.Position = self.screenPos
self.stickImage.Position = UDim2.new(0, self.thumbstickFrame.Size.X.Offset/2 - self.thumbstickSize/4, 0, self.thumbstickFrame.Size.Y.Offset/2 - self.thumbstickSize/4)
self.moveVector = Vector3.new(0,0,0)
self.isJumping = false
self.thumbstickFrame.Position = self.screenPos
self.moveTouchObject = nil
end
function TouchThumbstick:Create(parentFrame)
if self.thumbstickFrame then
self.thumbstickFrame:Destroy()
self.thumbstickFrame = nil
if self.onTouchMovedConn then
self.onTouchMovedConn:Disconnect()
self.onTouchMovedConn = nil
end
if self.onTouchEndedConn then
self.onTouchEndedConn:Disconnect()
self.onTouchEndedConn = nil
end
end
local minAxis = math.min(parentFrame.AbsoluteSize.x, parentFrame.AbsoluteSize.y)
local isSmallScreen = minAxis <= 500
self.thumbstickSize = isSmallScreen and 70 or 120
self.screenPos = isSmallScreen and UDim2.new(0, (self.thumbstickSize/2) - 10, 1, -self.thumbstickSize - 20) or
UDim2.new(0, self.thumbstickSize/2, 1, -self.thumbstickSize * 1.75)
self.thumbstickFrame = Instance.new("Frame")
self.thumbstickFrame.Name = "ThumbstickFrame"
self.thumbstickFrame.Active = true
self.thumbstickFrame.Visible = false
self.thumbstickFrame.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
self.thumbstickFrame.Position = self.screenPos
self.thumbstickFrame.BackgroundTransparency = 1
local outerImage = Instance.new("ImageLabel")
outerImage.Name = "OuterImage"
outerImage.Image = TOUCH_CONTROL_SHEET
outerImage.ImageRectOffset = Vector2.new()
outerImage.ImageRectSize = Vector2.new(220, 220)
outerImage.BackgroundTransparency = 1
outerImage.Size = UDim2.new(0, self.thumbstickSize, 0, self.thumbstickSize)
outerImage.Position = UDim2.new(0, 0, 0, 0)
outerImage.Parent = self.thumbstickFrame
self.stickImage = Instance.new("ImageLabel")
self.stickImage.Name = "StickImage"
self.stickImage.Image = TOUCH_CONTROL_SHEET
self.stickImage.ImageRectOffset = Vector2.new(220, 0)
self.stickImage.ImageRectSize = Vector2.new(111, 111)
self.stickImage.BackgroundTransparency = 1
self.stickImage.Size = UDim2.new(0, self.thumbstickSize/2, 0, self.thumbstickSize/2)
self.stickImage.Position = UDim2.new(0, self.thumbstickSize/2 - self.thumbstickSize/4, 0, self.thumbstickSize/2 - self.thumbstickSize/4)
self.stickImage.ZIndex = 2
self.stickImage.Parent = self.thumbstickFrame
local centerPosition = nil
local deadZone = 0.05
local function DoMove(direction)
local currentMoveVector = direction / (self.thumbstickSize/2)
-- Scaled Radial Dead Zone
local inputAxisMagnitude = currentMoveVector.magnitude
if inputAxisMagnitude < deadZone then
currentMoveVector = Vector3.new()
else
currentMoveVector = currentMoveVector.unit * ((inputAxisMagnitude - deadZone) / (1 - deadZone))
-- NOTE: Making currentMoveVector a unit vector will cause the player to instantly go max speed
-- must check for zero length vector is using unit
currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y)
end
self.moveVector = currentMoveVector
end
local function MoveStick(pos)
local relativePosition = Vector2.new(pos.x - centerPosition.x, pos.y - centerPosition.y)
local length = relativePosition.magnitude
local maxLength = self.thumbstickFrame.AbsoluteSize.x/2
if self.isFollowStick and length > maxLength then
local offset = relativePosition.unit * maxLength
self.thumbstickFrame.Position = UDim2.new(
0, pos.x - self.thumbstickFrame.AbsoluteSize.x/2 - offset.x,
0, pos.y - self.thumbstickFrame.AbsoluteSize.y/2 - offset.y)
else
length = math.min(length, maxLength)
relativePosition = relativePosition.unit * length
end
self.stickImage.Position = UDim2.new(0, relativePosition.x + self.stickImage.AbsoluteSize.x/2, 0, relativePosition.y + self.stickImage.AbsoluteSize.y/2)
end
-- input connections
self.thumbstickFrame.InputBegan:Connect(function(inputObject)
--A touch that starts elsewhere on the screen will be sent to a frame's InputBegan event
--if it moves over the frame. So we check that this is actually a new touch (inputObject.UserInputState ~= Enum.UserInputState.Begin)
if self.moveTouchObject or inputObject.UserInputType ~= Enum.UserInputType.Touch
or inputObject.UserInputState ~= Enum.UserInputState.Begin then
return
end
self.moveTouchObject = inputObject
self.thumbstickFrame.Position = UDim2.new(0, inputObject.Position.x - self.thumbstickFrame.Size.X.Offset/2, 0, inputObject.Position.y - self.thumbstickFrame.Size.Y.Offset/2)
centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2,
self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2)
local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
end)
self.onTouchMovedConn = UserInputService.TouchMoved:Connect(function(inputObject, isProcessed)
if inputObject == self.moveTouchObject then
centerPosition = Vector2.new(self.thumbstickFrame.AbsolutePosition.x + self.thumbstickFrame.AbsoluteSize.x/2,
self.thumbstickFrame.AbsolutePosition.y + self.thumbstickFrame.AbsoluteSize.y/2)
local direction = Vector2.new(inputObject.Position.x - centerPosition.x, inputObject.Position.y - centerPosition.y)
DoMove(direction)
MoveStick(inputObject.Position)
end
end)
self.onTouchEndedConn = UserInputService.TouchEnded:Connect(function(inputObject, isProcessed)
if inputObject == self.moveTouchObject then
self:OnInputEnded()
end
end)
GuiService.MenuOpened:Connect(function()
if self.moveTouchObject then
self:OnInputEnded()
end
end)
self.thumbstickFrame.Parent = parentFrame
end
return TouchThumbstick
@@ -0,0 +1,448 @@
--[[
VRNavigation
--]]
local VRService = game:GetService("VRService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local ContextActionService = game:GetService("ContextActionService")
local StarterGui = game:GetService("StarterGui")
--local MasterControl = require(script.Parent)
local PathDisplay = nil
local LocalPlayer = Players.LocalPlayer
--[[ Constants ]]--
local RECALCULATE_PATH_THRESHOLD = 4
local NO_PATH_THRESHOLD = 12
local MAX_PATHING_DISTANCE = 200
local POINT_REACHED_THRESHOLD = 1
local STOPPING_DISTANCE = 4
local OFFTRACK_TIME_THRESHOLD = 2
local THUMBSTICK_DEADZONE = 0.22
local ZERO_VECTOR3 = Vector3.new(0,0,0)
local XZ_VECTOR3 = Vector3.new(1,0,1)
--[[ Utility Functions ]]--
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
local movementUpdateEvent = Instance.new("BindableEvent")
movementUpdateEvent.Name = "MovementUpdate"
movementUpdateEvent.Parent = script
coroutine.wrap(function()
local PathDisplayModule = script.Parent:WaitForChild("PathDisplay")
if PathDisplayModule then
PathDisplay = require(PathDisplayModule)
end
end)()
--[[ The Class ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local VRNavigation = setmetatable({}, BaseCharacterController)
VRNavigation.__index = VRNavigation
function VRNavigation.new()
local self = setmetatable(BaseCharacterController.new(), VRNavigation)
self.navigationRequestedConn = nil
self.heartbeatConn = nil
self.currentDestination = nil
self.currentPath = nil
self.currentPoints = nil
self.currentPointIdx = 0
self.expectedTimeToNextPoint = 0
self.timeReachedLastPoint = tick()
self.moving = false
self.isJumpBound = false
self.moveLatch = false
self.userCFrameEnabledConn = nil
return self
end
function VRNavigation:SetLaserPointerMode(mode)
pcall(function()
StarterGui:SetCore("VRLaserPointerMode", mode)
end)
end
function VRNavigation:GetLocalHumanoid()
local character = LocalPlayer.Character
if not character then
return
end
for _, child in pairs(character:GetChildren()) do
if child:IsA("Humanoid") then
return child
end
end
return nil
end
function VRNavigation:HasBothHandControllers()
return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) and VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end
function VRNavigation:HasAnyHandControllers()
return VRService:GetUserCFrameEnabled(Enum.UserCFrame.RightHand) or VRService:GetUserCFrameEnabled(Enum.UserCFrame.LeftHand)
end
function VRNavigation:IsMobileVR()
return UserInputService.TouchEnabled
end
function VRNavigation:HasGamepad()
return UserInputService.GamepadEnabled
end
function VRNavigation:ShouldUseNavigationLaser()
--Places where we use the navigation laser:
-- mobile VR with any number of hands tracked
-- desktop VR with only one hand tracked
-- desktop VR with no hands and no gamepad (i.e. with Oculus remote?)
--using an Xbox controller with a desktop VR headset means no laser since the user has a thumbstick.
--in the future, we should query thumbstick presence with a features API
if self:IsMobileVR() then
return true
else
if self:HasBothHandControllers() then
return false
end
if not self:HasAnyHandControllers() then
return not self:HasGamepad()
end
return true
end
end
function VRNavigation:StartFollowingPath(newPath)
currentPath = newPath
currentPoints = currentPath:GetPointCoordinates()
currentPointIdx = 1
moving = true
timeReachedLastPoint = tick()
local humanoid = self:GetLocalHumanoid()
if humanoid and humanoid.Torso and #currentPoints >= 1 then
local dist = (currentPoints[1] - humanoid.Torso.Position).magnitude
expectedTimeToNextPoint = dist / humanoid.WalkSpeed
end
movementUpdateEvent:Fire("targetPoint", self.currentDestination)
end
function VRNavigation:GoToPoint(point)
currentPath = true
currentPoints = { point }
currentPointIdx = 1
moving = true
local humanoid = self:GetLocalHumanoid()
local distance = (humanoid.Torso.Position - point).magnitude
local estimatedTimeRemaining = distance / humanoid.WalkSpeed
timeReachedLastPoint = tick()
expectedTimeToNextPoint = estimatedTimeRemaining
movementUpdateEvent:Fire("targetPoint", point)
end
function VRNavigation:StopFollowingPath()
currentPath = nil
currentPoints = nil
currentPointIdx = 0
moving = false
self.moveVector = ZERO_VECTOR3
end
function VRNavigation:TryComputePath(startPos, destination)
local numAttempts = 0
local newPath = nil
while not newPath and numAttempts < 5 do
newPath = PathfindingService:ComputeSmoothPathAsync(startPos, destination, MAX_PATHING_DISTANCE)
numAttempts = numAttempts + 1
if newPath.Status == Enum.PathStatus.ClosestNoPath or newPath.Status == Enum.PathStatus.ClosestOutOfRange then
newPath = nil
break
end
if newPath and newPath.Status == Enum.PathStatus.FailStartNotEmpty then
startPos = startPos + (destination - startPos).unit
newPath = nil
end
if newPath and newPath.Status == Enum.PathStatus.FailFinishNotEmpty then
destination = destination + Vector3.new(0, 1, 0)
newPath = nil
end
end
return newPath
end
function VRNavigation:OnNavigationRequest(destinationCFrame, inputUserCFrame )
local destinationPosition = destinationCFrame.p
local lastDestination = self.currentDestination
if not IsFiniteVector3(destinationPosition) then
return
end
self.currentDestination = destinationPosition
local humanoid = self:GetLocalHumanoid()
if not humanoid or not humanoid.Torso then
return
end
local currentPosition = humanoid.Torso.Position
local distanceToDestination = (self.currentDestination - currentPosition).magnitude
if distanceToDestination < NO_PATH_THRESHOLD then
self:GoToPoint(self.currentDestination)
return
end
if not lastDestination or (self.currentDestination - lastDestination).magnitude > RECALCULATE_PATH_THRESHOLD then
local newPath = self:TryComputePath(currentPosition, self.currentDestination)
if newPath then
self:StartFollowingPath(newPath)
if PathDisplay then
PathDisplay.setCurrentPoints(self.currentPoints)
PathDisplay.renderPath()
end
else
self:StopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
end
else
if moving then
self.currentPoints[#currentPoints] = self.currentDestination
else
self:GoToPoint(self.currentDestination)
end
end
end
function VRNavigation:OnJumpAction(actionName, inputState, inputObj)
if inputState == Enum.UserInputState.Begin then
self.isJumping = true
end
end
function VRNavigation:BindJumpAction(active)
if active then
if not self.isJumpBound then
self.isJumpBound = true
ContextActionService:BindAction("VRJumpAction", (function() self:OnJumpAction() end), false, Enum.KeyCode.ButtonA)
end
else
if self.isJumpBound then
self.isJumpBound = false
ContextActionService:UnbindAction("VRJumpAction")
end
end
end
function VRNavigation:ControlCharacterGamepad(actionName, inputState, inputObject)
if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then return end
if inputState == Enum.UserInputState.Cancel then
self.moveVector = ZERO_VECTOR3
return
end
if inputState ~= Enum.UserInputState.End then
self:StopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
if self:ShouldUseNavigationLaser() then
self:BindJumpAction(true)
self:SetLaserPointerMode("Hidden")
end
if inputObject.Position.magnitude > THUMBSTICK_DEADZONE then
self.moveVector = Vector3.new(inputObject.Position.X, 0, -inputObject.Position.Y)
if self.moveVector.magnitude > 0 then
self.moveVector = self.moveVector.unit * math.min(1, inputObject.Position.magnitude)
end
self.moveLatch = true
end
else
self.moveVector = ZERO_VECTOR3
if self:ShouldUseNavigationLaser() then
self:BindJumpAction(false)
self:SetLaserPointerMode("Navigation")
end
if self.moveLatch then
self.moveLatch = false
movementUpdateEvent:Fire("offtrack")
end
end
end
function VRNavigation:OnHeartbeat(dt)
local newMoveVector = self.moveVector
local humanoid = self:GetLocalHumanoid()
if not humanoid or not humanoid.Torso then
return
end
if self.moving and self.currentPoints then
local currentPosition = humanoid.Torso.Position
local goalPosition = currentPoints[1]
local vectorToGoal = (goalPosition - currentPosition) * XZ_VECTOR3
local moveDist = vectorToGoal.magnitude
local moveDir = vectorToGoal / moveDist
if moveDist < POINT_REACHED_THRESHOLD then
local estimatedTimeRemaining = 0
local prevPoint = currentPoints[1]
for i, point in pairs(currentPoints) do
if i ~= 1 then
local dist = (point - prevPoint).magnitude
prevPoint = point
estimatedTimeRemaining = estimatedTimeRemaining + (dist / humanoid.WalkSpeed)
end
end
table.remove(currentPoints, 1)
currentPointIdx = currentPointIdx + 1
if #currentPoints == 0 then
self:StopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
return
else
if PathDisplay then
PathDisplay.setCurrentPoints(currentPoints)
PathDisplay.renderPath()
end
local newGoal = currentPoints[1]
local distanceToGoal = (newGoal - currentPosition).magnitude
expectedTimeToNextPoint = distanceToGoal / humanoid.WalkSpeed
timeReachedLastPoint = tick()
end
else
local ignoreTable = {
game.Players.LocalPlayer.Character,
workspace.CurrentCamera
}
local obstructRay = Ray.new(currentPosition - Vector3.new(0, 1, 0), moveDir * 3)
local obstructPart, obstructPoint, obstructNormal = workspace:FindPartOnRayWithIgnoreList(obstructRay, ignoreTable)
if obstructPart then
local heightOffset = Vector3.new(0, 100, 0)
local jumpCheckRay = Ray.new(obstructPoint + moveDir * 0.5 + heightOffset, -heightOffset)
local jumpCheckPart, jumpCheckPoint, jumpCheckNormal = workspace:FindPartOnRayWithIgnoreList(jumpCheckRay, ignoreTable)
local heightDifference = jumpCheckPoint.Y - currentPosition.Y
if heightDifference < 6 and heightDifference > -2 then
humanoid.Jump = true
end
end
local timeSinceLastPoint = tick() - timeReachedLastPoint
if timeSinceLastPoint > expectedTimeToNextPoint + OFFTRACK_TIME_THRESHOLD then
self:StopFollowingPath()
if PathDisplay then
PathDisplay.clearRenderedPath()
end
movementUpdateEvent:Fire("offtrack")
end
newMoveVector = self.moveVector:Lerp(moveDir, dt * 10)
end
end
if IsFiniteVector3(newMoveVector) then
self.moveVector = newMoveVector
end
end
function VRNavigation:OnUserCFrameEnabled()
if self:ShouldUseNavigationLaser() then
self:BindJumpAction(false)
self:SetLaserPointerMode("Navigation")
else
self:BindJumpAction(true)
self:SetLaserPointerMode("Hidden")
end
end
function VRNavigation:Enable(enable)
if enable then
self.navigationRequestedConn = VRService.NavigationRequested:Connect(function(destinationCFrame, inputUserCFrame) self:OnNavigationRequest(destinationCFrame, inputUserCFrame) end)
self.heartbeatConn = RunService.Heartbeat:Connect(function(dt) self:OnHeartbeat(dt) end)
ContextActionService:BindAction("MoveThumbstick", (function(actionName, inputState, inputObject) self:ControlCharacterGamepad(actionName, inputState, inputObject) end), false, Enum.KeyCode.Thumbstick1)
ContextActionService:BindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
self.userCFrameEnabledConn = VRService.UserCFrameEnabled:Connect(function() self:OnUserCFrameEnabled() end)
self:OnUserCFrameEnabled()
pcall(function()
VRService:SetTouchpadMode(Enum.VRTouchpad.Left, Enum.VRTouchpadMode.VirtualThumbstick)
VRService:SetTouchpadMode(Enum.VRTouchpad.Right, Enum.VRTouchpadMode.ABXY)
end)
self.enabled = true
else
-- Disable
self:StopFollowingPath()
ContextActionService:UnbindAction("MoveThumbstick")
ContextActionService:UnbindActivate(Enum.UserInputType.Gamepad1, Enum.KeyCode.ButtonR2)
self:BindJumpAction(false)
self:SetLaserPointerMode("Disabled")
if self.navigationRequestedConn then
self.navigationRequestedConn:Disconnect()
self.navigationRequestedConn = nil
end
if self.heartbeatConn then
self.heartbeatConn:Disconnect()
self.heartbeatConn = nil
end
if self.userCFrameEnabledConn then
self.userCFrameEnabledConn:Disconnect()
self.userCFrameEnabledConn = nil
end
self.enabled = false
end
end
return VRNavigation
@@ -0,0 +1,104 @@
--[[
// FileName: VehicleControl
// Version 1.0
// Written by: jmargh
// Description: Implements in-game vehicle controls for all input devices
// NOTE: This works for basic vehicles (single vehicle seat). If you use custom VehicleSeat code,
// multiple VehicleSeats or your own implementation of a VehicleSeat this will not work.
--]]
local ContextActionService = game:GetService("ContextActionService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
--[[ 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)
-- Note that VehicleController does not derive from BaseCharacterController, it is a special case
local VehicleController = {}
VehicleController.__index = VehicleController
function VehicleController.new()
local self = setmetatable({}, VehicleController)
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
return self
end
function VehicleController:Enable(enable, vehicleSeat)
if enable == self.enabled and vehicleSeat == self.vehicleSeat then
return
end
if enable then
if vehicleSeat then
self.vehicleSeat = vehicleSeat
if useTriggersForThrottle then
ContextActionService:BindAction("throttleAccel", (function() self:OnThrottleAccel() end), false, Enum.KeyCode.ButtonR2)
ContextActionService:BindAction("throttleDeccel", (function() self:OnThrottleDeccel() end), false, Enum.KeyCode.ButtonL2)
end
ContextActionService:BindAction("arrowSteerRight", (function() self:OnSteerRight() end), false, Enum.KeyCode.Right)
ContextActionService:BindAction("arrowSteerLeft", (function() self:OnSteerLeft() end), false, Enum.KeyCode.Left)
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)
self.acceleration = (inputState ~= Enum.UserInputState.End) and -1 or 0
self.throttle = self.acceleration + self.decceleration
end
function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject)
self.decceleration = (inputState ~= Enum.UserInputState.End) and 1 or 0
self.throttle = self.acceleration + self.decceleration
end
function VehicleController:OnSteerRight(actionName, inputState, inputObject)
self.turningRight = (inputState ~= Enum.UserInputState.End) and 1 or 0
self.steer = self.turningRight + self.turningLeft
end
function VehicleController:OnSteerLeft(actionName, inputState, inputObject)
self.turningLeft = (inputState ~= Enum.UserInputState.End) and -1 or 0
self.steer = self.turningRight + self.turningLeft
end
-- Call this from a function bound to Renderstep with Input Priority
function VehicleController:Update(moveVector, usingGamepad)
if self.vehicleSeat then
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
end
return moveVector, false
end
return VehicleController
@@ -0,0 +1,6 @@
--[[
PlayerScriptsLoader - This script requires and instantiates the PlayerModule singleton
2018 PlayerScripts Update - AllYourBlox
--]]
local PlayerModule = require(script.Parent:WaitForChild("PlayerModule"))