434 lines
12 KiB
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
|