494 lines
13 KiB
Lua
494 lines
13 KiB
Lua
local FFlagUserCameraInputRefactor do
|
|
local success, result = pcall(function()
|
|
return UserSettings():IsUserFeatureEnabled("UserCameraInputRefactor2")
|
|
end)
|
|
FFlagUserCameraInputRefactor = success and result
|
|
end
|
|
|
|
local ContextActionService = game:GetService("ContextActionService")
|
|
local UserInputService = game:GetService("UserInputService")
|
|
local Players = game:GetService("Players")
|
|
local UserGameSettings = UserSettings():GetService("UserGameSettings")
|
|
|
|
local player = Players.LocalPlayer
|
|
|
|
local CAMERA_INPUT_PRIORITY = Enum.ContextActionPriority.Default.Value
|
|
local MB_TAP_LENGTH = 0.3 -- (s) length of time for a short mouse button tap to be registered
|
|
|
|
local ROTATION_SPEED_KEYS = math.rad(2) -- (rad/s)
|
|
local ROTATION_SPEED_MOUSE = Vector2.new(1, 0.77)*math.rad(0.5) -- (rad/s)
|
|
local ROTATION_SPEED_TOUCH = Vector2.new(1, 0.66)*math.rad(1) -- (rad/s)
|
|
local ROTATION_SPEED_GAMEPAD = Vector2.new(1, 0.77)*math.rad(4) -- (rad/s)
|
|
|
|
local ZOOM_SPEED_MOUSE = 1 -- (scaled studs/wheel click)
|
|
local ZOOM_SPEED_KEYS = 0.1 -- (studs/s)
|
|
local ZOOM_SPEED_TOUCH = 0.04 -- (scaled studs/DIP %)
|
|
|
|
local MIN_TOUCH_SENSITIVITY_FRACTION = 0.25 -- 25% sensitivity at 90°
|
|
|
|
-- right mouse button up & down events
|
|
local rmbDown, rmbUp do
|
|
local rmbDownBindable = Instance.new("BindableEvent")
|
|
local rmbUpBindable = Instance.new("BindableEvent")
|
|
|
|
rmbDown = rmbDownBindable.Event
|
|
rmbUp = rmbUpBindable.Event
|
|
|
|
UserInputService.InputBegan:Connect(function(input, gpe)
|
|
if not gpe and input.UserInputType == Enum.UserInputType.MouseButton2 then
|
|
rmbDownBindable:Fire()
|
|
end
|
|
end)
|
|
|
|
UserInputService.InputEnded:Connect(function(input, gpe)
|
|
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
|
rmbUpBindable:Fire()
|
|
end
|
|
end)
|
|
end
|
|
|
|
local thumbstickCurve do
|
|
local K_CURVATURE = 2 -- amount of upwards curvature (0 is flat)
|
|
local K_DEADZONE = 0.1 -- deadzone
|
|
|
|
function thumbstickCurve(x)
|
|
-- remove sign, apply linear deadzone
|
|
local fDeadzone = (math.abs(x) - K_DEADZONE)/(1 - K_DEADZONE)
|
|
|
|
-- apply exponential curve and scale to fit in [0, 1]
|
|
local fCurve = (math.exp(K_CURVATURE*fDeadzone) - 1)/(math.exp(K_CURVATURE) - 1)
|
|
|
|
-- reapply sign and clamp
|
|
return math.sign(x)*math.clamp(fCurve, 0, 1)
|
|
end
|
|
end
|
|
|
|
-- Adjust the touch sensitivity so that sensitivity is reduced when swiping up
|
|
-- or down, but stays the same when swiping towards the middle of the screen
|
|
local function adjustTouchPitchSensitivity(delta)
|
|
local camera = workspace.CurrentCamera
|
|
|
|
if not camera then
|
|
return delta
|
|
end
|
|
|
|
-- get the camera pitch in world space
|
|
local pitch = camera.CFrame:ToEulerAnglesYXZ()
|
|
|
|
if delta.Y*pitch >= 0 then
|
|
-- do not reduce sensitivity when pitching towards the horizon
|
|
return delta
|
|
end
|
|
|
|
-- set up a line to fit:
|
|
-- 1 = f(0)
|
|
-- 0 = f(±pi/2)
|
|
local curveY = 1 - (2*math.abs(pitch)/math.pi)^0.75
|
|
|
|
-- remap curveY from [0, 1] -> [MIN_TOUCH_SENSITIVITY_FRACTION, 1]
|
|
local sensitivity = curveY*(1 - MIN_TOUCH_SENSITIVITY_FRACTION) + MIN_TOUCH_SENSITIVITY_FRACTION
|
|
|
|
return Vector2.new(1, sensitivity)*delta
|
|
end
|
|
|
|
local function isInDynamicThumbstickArea(pos)
|
|
local playerGui = player:FindFirstChildOfClass("PlayerGui")
|
|
local touchGui = playerGui and playerGui:FindFirstChild("TouchGui")
|
|
local touchFrame = touchGui and touchGui:FindFirstChild("TouchControlFrame")
|
|
local thumbstickFrame = touchFrame and touchFrame:FindFirstChild("DynamicThumbstickFrame")
|
|
|
|
if not thumbstickFrame then
|
|
return false
|
|
end
|
|
|
|
if not touchGui.Enabled then
|
|
return false
|
|
end
|
|
|
|
local posTopLeft = thumbstickFrame.AbsolutePosition
|
|
local posBottomRight = posTopLeft + thumbstickFrame.AbsoluteSize
|
|
|
|
return
|
|
pos.X >= posTopLeft.X and
|
|
pos.Y >= posTopLeft.Y and
|
|
pos.X <= posBottomRight.X and
|
|
pos.Y <= posBottomRight.Y
|
|
end
|
|
|
|
local CameraInput = {}
|
|
|
|
do
|
|
local connectionList = {}
|
|
|
|
local touchPitchSensitivity = 1
|
|
local gamepadState = {
|
|
Thumbstick2 = Vector2.new(),
|
|
}
|
|
local keyboardState = {
|
|
Left = 0,
|
|
Right = 0,
|
|
I = 0,
|
|
O = 0
|
|
}
|
|
local mouseState = {
|
|
Movement = Vector2.new(),
|
|
Wheel = 0, -- PointerAction
|
|
Pan = Vector2.new(), -- PointerAction
|
|
Pinch = 0, -- PointerAction
|
|
MouseButton2 = 0,
|
|
}
|
|
local touchState = {
|
|
Move = Vector2.new(),
|
|
Pinch = 0,
|
|
}
|
|
|
|
local gamepadZoomPressBindable = Instance.new("BindableEvent")
|
|
CameraInput.gamepadZoomPress = gamepadZoomPressBindable.Event
|
|
|
|
function CameraInput.getPanning()
|
|
for _, input in pairs(UserInputService:GetMouseButtonsPressed()) do
|
|
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function CameraInput.getRotation()
|
|
local kKeyboard = Vector2.new(keyboardState.Right - keyboardState.Left, 0)
|
|
local kGamepad = gamepadState.Thumbstick2
|
|
local kMouse = mouseState.Movement + mouseState.Pan
|
|
local kTouch = adjustTouchPitchSensitivity(touchState.Move)
|
|
|
|
local result =
|
|
kKeyboard*ROTATION_SPEED_KEYS +
|
|
kGamepad*ROTATION_SPEED_GAMEPAD +
|
|
kMouse*ROTATION_SPEED_MOUSE +
|
|
kTouch*ROTATION_SPEED_TOUCH
|
|
|
|
return result
|
|
end
|
|
|
|
function CameraInput.getZoomDelta()
|
|
local kKeyboard = keyboardState.O - keyboardState.I
|
|
local kMouse = -mouseState.Wheel + mouseState.Pinch
|
|
local kTouch = -touchState.Pinch
|
|
return kKeyboard*ZOOM_SPEED_KEYS + kMouse*ZOOM_SPEED_MOUSE + kTouch*ZOOM_SPEED_TOUCH
|
|
end
|
|
|
|
do
|
|
local function thumbstick(action, state, input)
|
|
local position = input.Position
|
|
gamepadState[input.KeyCode.Name] = Vector2.new(thumbstickCurve(position.X), -thumbstickCurve(position.Y))
|
|
end
|
|
|
|
local function mouseMove(action, state, input)
|
|
local delta = input.Delta
|
|
mouseState.Movement = Vector2.new(delta.X, delta.Y)
|
|
return Enum.ContextActionResult.Pass
|
|
end
|
|
|
|
local function mouseWheel(action, state, input)
|
|
mouseState.Wheel = input.Position.Z
|
|
return Enum.ContextActionResult.Pass
|
|
end
|
|
|
|
local function keypress(action, state, input)
|
|
keyboardState[input.KeyCode.Name] = state == Enum.UserInputState.Begin and 1 or 0
|
|
end
|
|
|
|
local function gamepadZoomPress(action, state, input)
|
|
if state == Enum.UserInputState.Begin then
|
|
gamepadZoomPressBindable:Fire()
|
|
end
|
|
end
|
|
|
|
local function resetInputDevices()
|
|
for _, device in pairs({
|
|
gamepadState,
|
|
keyboardState,
|
|
mouseState,
|
|
touchState,
|
|
}) do
|
|
for k, v in pairs(device) do
|
|
if type(v) == "boolean" then
|
|
device[k] = false
|
|
else
|
|
device[k] *= 0 -- Mul by zero to preserve vector types
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local touchBegan, touchChanged, touchEnded do
|
|
-- Use TouchPan & TouchPinch when they work in the Studio emulator
|
|
|
|
local touches = {} -- {[InputObject] = sunk}
|
|
local dynamicThumbstickInput -- Special-cased
|
|
local lastPinchDiameter
|
|
|
|
function touchBegan(input, sunk)
|
|
assert(input.UserInputType == Enum.UserInputType.Touch)
|
|
assert(input.UserInputState == Enum.UserInputState.Begin)
|
|
|
|
if dynamicThumbstickInput == nil and isInDynamicThumbstickArea(input.Position) and not sunk then
|
|
-- any finger down starting in the dynamic thumbstick area should always be
|
|
-- ignored for camera purposes. these must be handled specially from all other
|
|
-- inputs, as the DT does not sink inputs by itself
|
|
dynamicThumbstickInput = input
|
|
return
|
|
end
|
|
|
|
-- register the finger
|
|
touches[input] = sunk
|
|
end
|
|
|
|
function touchEnded(input, sunk)
|
|
assert(input.UserInputType == Enum.UserInputType.Touch)
|
|
assert(input.UserInputState == Enum.UserInputState.End)
|
|
|
|
-- reset the DT input
|
|
if input == dynamicThumbstickInput then
|
|
dynamicThumbstickInput = nil
|
|
end
|
|
|
|
-- reset pinch state if one unsunk finger lifts
|
|
if touches[input] == false then
|
|
lastPinchDiameter = nil
|
|
end
|
|
|
|
-- unregister input
|
|
touches[input] = nil
|
|
end
|
|
|
|
function touchChanged(input, sunk)
|
|
assert(input.UserInputType == Enum.UserInputType.Touch)
|
|
assert(input.UserInputState == Enum.UserInputState.Change)
|
|
|
|
-- ignore movement from the DT finger
|
|
if input == dynamicThumbstickInput then
|
|
return
|
|
end
|
|
|
|
-- fixup unknown touches
|
|
if touches[input] == nil then
|
|
touches[input] = sunk
|
|
end
|
|
|
|
-- collect unsunk touches
|
|
local unsunkTouches = {}
|
|
for touch, sunk in pairs(touches) do
|
|
if not sunk then
|
|
table.insert(unsunkTouches, touch)
|
|
end
|
|
end
|
|
|
|
-- 1 finger: pan
|
|
if #unsunkTouches == 1 then
|
|
if touches[input] == false then
|
|
local delta = input.Delta
|
|
touchState.Move = Vector2.new(delta.X, delta.Y)
|
|
end
|
|
end
|
|
|
|
-- 2 fingers: pinch
|
|
if #unsunkTouches == 2 then
|
|
local pinchDiameter = (unsunkTouches[1].Position - unsunkTouches[2].Position).Magnitude
|
|
|
|
if lastPinchDiameter then
|
|
touchState.Pinch = pinchDiameter - lastPinchDiameter
|
|
end
|
|
|
|
lastPinchDiameter = pinchDiameter
|
|
else
|
|
lastPinchDiameter = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function pointerAction(wheel, pan, pinch, gpe)
|
|
if not gpe then
|
|
local inversionVector = Vector2.new(1, UserGameSettings:GetCameraYInvertValue())
|
|
|
|
mouseState.Wheel = wheel
|
|
mouseState.Pan = pan*inversionVector
|
|
mouseState.Pinch = -pinch
|
|
end
|
|
end
|
|
|
|
local function inputBegan(input, sunk)
|
|
if input.UserInputType == Enum.UserInputType.Touch then
|
|
touchBegan(input, sunk)
|
|
end
|
|
end
|
|
|
|
local function inputChanged(input, sunk)
|
|
if input.UserInputType == Enum.UserInputType.Touch then
|
|
touchChanged(input, sunk)
|
|
end
|
|
end
|
|
|
|
local function inputEnded(input, sunk)
|
|
if input.UserInputType == Enum.UserInputType.Touch then
|
|
touchEnded(input, sunk)
|
|
end
|
|
end
|
|
|
|
local inputEnabled = false
|
|
|
|
function CameraInput.setInputEnabled(_inputEnabled)
|
|
assert(FFlagUserCameraInputRefactor)
|
|
|
|
if inputEnabled == _inputEnabled then
|
|
return
|
|
end
|
|
inputEnabled = _inputEnabled
|
|
|
|
if inputEnabled then -- enable
|
|
resetInputDevices()
|
|
|
|
ContextActionService:BindActionAtPriority(
|
|
"RbxCameraThumbstick",
|
|
thumbstick,
|
|
false,
|
|
CAMERA_INPUT_PRIORITY,
|
|
Enum.KeyCode.Thumbstick2
|
|
)
|
|
|
|
ContextActionService:BindActionAtPriority(
|
|
"RbxCameraMouseMove",
|
|
mouseMove,
|
|
false,
|
|
CAMERA_INPUT_PRIORITY,
|
|
Enum.UserInputType.MouseMovement
|
|
)
|
|
|
|
ContextActionService:BindActionAtPriority(
|
|
"RbxCameraKeypress",
|
|
keypress,
|
|
false,
|
|
CAMERA_INPUT_PRIORITY,
|
|
Enum.KeyCode.Left,
|
|
Enum.KeyCode.Right,
|
|
Enum.KeyCode.I,
|
|
Enum.KeyCode.O
|
|
)
|
|
|
|
ContextActionService:BindAction(
|
|
"RbxCameraGamepadZoom",
|
|
gamepadZoomPress,
|
|
false,
|
|
Enum.KeyCode.ButtonR3
|
|
)
|
|
|
|
table.insert(connectionList, UserInputService.InputBegan:Connect(inputBegan))
|
|
table.insert(connectionList, UserInputService.InputChanged:Connect(inputChanged))
|
|
table.insert(connectionList, UserInputService.InputEnded:Connect(inputEnded))
|
|
table.insert(connectionList, UserInputService.PointerAction:Connect(pointerAction))
|
|
|
|
else -- disable
|
|
ContextActionService:UnbindAction("RbxCameraThumbstick")
|
|
ContextActionService:UnbindAction("RbxCameraMouseMove")
|
|
ContextActionService:UnbindAction("RbxCameraMouseWheel")
|
|
ContextActionService:UnbindAction("RbxCameraKeypress")
|
|
|
|
resetInputDevices()
|
|
|
|
for _, conn in pairs(connectionList) do
|
|
conn:Disconnect()
|
|
end
|
|
connectionList = {}
|
|
end
|
|
end
|
|
|
|
function CameraInput.getInputEnabled()
|
|
return inputEnabled
|
|
end
|
|
|
|
function CameraInput.resetInputForFrameEnd()
|
|
mouseState.Movement = Vector2.new()
|
|
touchState.Move = Vector2.new()
|
|
mouseState.Wheel = 0
|
|
touchState.Pinch = 0
|
|
end
|
|
|
|
UserInputService.WindowFocused:Connect(resetInputDevices)
|
|
UserInputService.WindowFocusReleased:Connect(resetInputDevices)
|
|
end
|
|
end
|
|
|
|
-- Toggle pan
|
|
do
|
|
local holdPan = false
|
|
local togglePan = false
|
|
local lastRmbDown = 0 -- tick() timestamp of the last right mouse button down event
|
|
|
|
function CameraInput.getHoldPan()
|
|
return holdPan
|
|
end
|
|
|
|
function CameraInput.getTogglePan()
|
|
return togglePan
|
|
end
|
|
|
|
function CameraInput.getPanning()
|
|
return togglePan or holdPan
|
|
end
|
|
|
|
function CameraInput.setTogglePan(value)
|
|
togglePan = value
|
|
end
|
|
|
|
local cameraToggleInputEnabled = false
|
|
local rmbDownConnection
|
|
local rmbUpConnection
|
|
|
|
function CameraInput.enableCameraToggleInput()
|
|
if cameraToggleInputEnabled then
|
|
return
|
|
end
|
|
cameraToggleInputEnabled = true
|
|
|
|
holdPan = false
|
|
togglePan = false
|
|
|
|
if rmbDownConnection then
|
|
rmbDownConnection:Disconnect()
|
|
end
|
|
|
|
if rmbUpConnection then
|
|
rmbUpConnection:Disconnect()
|
|
end
|
|
|
|
rmbDownConnection = rmbDown:Connect(function()
|
|
holdPan = true
|
|
lastRmbDown = tick()
|
|
end)
|
|
|
|
rmbUpConnection = rmbUp:Connect(function()
|
|
holdPan = false
|
|
if tick() - lastRmbDown < MB_TAP_LENGTH and (togglePan or UserInputService:GetMouseDelta().Magnitude < 2) then
|
|
togglePan = not togglePan
|
|
end
|
|
end)
|
|
end
|
|
|
|
function CameraInput.disableCameraToggleInput()
|
|
if not cameraToggleInputEnabled then
|
|
return
|
|
end
|
|
cameraToggleInputEnabled = false
|
|
|
|
if rmbDownConnection then
|
|
rmbDownConnection:Disconnect()
|
|
rmbDownConnection = nil
|
|
end
|
|
|
|
if rmbUpConnection then
|
|
rmbUpConnection:Disconnect()
|
|
rmbUpConnection = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
return CameraInput |