add gs
This commit is contained in:
@@ -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)
|
||||
+427
@@ -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)
|
||||
+1587
File diff suppressed because it is too large
Load Diff
+1099
File diff suppressed because it is too large
Load Diff
+584
@@ -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
|
||||
+194
@@ -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
|
||||
+1535
File diff suppressed because it is too large
Load Diff
+75
@@ -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
|
||||
+195
@@ -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
|
||||
+48
@@ -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
|
||||
+192
@@ -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
|
||||
+450
@@ -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
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
local RootCameraCreator = require(script.Parent)
|
||||
|
||||
local function CreateScriptableCamera()
|
||||
local module = RootCameraCreator()
|
||||
|
||||
function module:Update()
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
return CreateScriptableCamera
|
||||
+54
@@ -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
|
||||
+260
@@ -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
|
||||
|
||||
+63
@@ -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
|
||||
+206
@@ -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
|
||||
+190
@@ -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)
|
||||
+174
@@ -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
|
||||
|
||||
+1210
File diff suppressed because it is too large
Load Diff
+170
@@ -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
|
||||
+618
@@ -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
|
||||
+522
@@ -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
|
||||
+198
@@ -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
|
||||
+175
@@ -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
|
||||
+228
@@ -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
|
||||
+178
@@ -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
|
||||
+172
@@ -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
|
||||
+433
@@ -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
|
||||
+180
@@ -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
|
||||
+131
@@ -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
|
||||
+27
@@ -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()
|
||||
+548
@@ -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()
|
||||
+1460
File diff suppressed because it is too large
Load Diff
+46
@@ -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
|
||||
+125
@@ -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
|
||||
|
||||
+240
@@ -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
|
||||
+560
@@ -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
|
||||
+153
@@ -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
|
||||
+210
@@ -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
|
||||
+417
@@ -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
|
||||
+219
@@ -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
|
||||
+45
@@ -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
|
||||
+19
@@ -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
|
||||
+75
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+49
@@ -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
|
||||
+41
@@ -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
|
||||
+82
@@ -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
|
||||
+61
@@ -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
|
||||
+341
@@ -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
|
||||
+174
@@ -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
|
||||
+189
@@ -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
|
||||
+405
@@ -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()
|
||||
+40
@@ -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
|
||||
+1129
File diff suppressed because it is too large
Load Diff
+530
@@ -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
|
||||
+234
@@ -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
|
||||
+149
@@ -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
|
||||
+131
@@ -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
|
||||
+176
@@ -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
|
||||
+201
@@ -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
|
||||
+247
@@ -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
|
||||
+194
@@ -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
|
||||
+448
@@ -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
|
||||
+104
@@ -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
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
--[[
|
||||
PlayerScriptsLoader - This script requires and instantiates the PlayerModule singleton
|
||||
|
||||
2018 PlayerScripts Update - AllYourBlox
|
||||
--]]
|
||||
local PlayerModule = require(script.Parent:WaitForChild("PlayerModule"))
|
||||
Reference in New Issue
Block a user