SyntaxGameServer/RCCService2021/ExtraContent/scripts/PlayerScripts/StarterPlayerScripts/ControlScript/MasterControl/VRNavigation.lua

434 lines
12 KiB
Lua

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