Created
May 27, 2021 05:54
-
-
Save thekaisbest/fb59c13e62ab2146e5f9fdcafeaa9b80 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
local _p = game:WaitForChild("Players") | |
local _plr = _p.ChildAdded:Wait() | |
if _plr == _p.LocalPlayer then | |
_plr.ChildAdded:Connect(function(cccc) | |
if c.Name == "PlayerScriptsLoader" then | |
c.Disabled = true | |
end | |
end) | |
end | |
]] | |
repeat wait() | |
a = pcall(function() | |
game:WaitForChild("Players").LocalPlayer:WaitForChild("PlayerScripts").ChildAdded:Connect(function(c) | |
if c.Name == "PlayerScriptsLoader"then | |
c.Disabled = true | |
end | |
end) | |
end) | |
if a == true then break end | |
until true == false | |
game:WaitForChild("Players").LocalPlayer:WaitForChild("PlayerScripts").ChildAdded:Connect(function(c) | |
if c.Name == "PlayerScriptsLoader"then | |
c.Disabled = true | |
end | |
end) | |
function _CameraUI() | |
local Players = game:GetService("Players") | |
local TweenService = game:GetService("TweenService") | |
local LocalPlayer = Players.LocalPlayer | |
if not LocalPlayer then | |
Players:GetPropertyChangedSignal("LocalPlayer"):Wait() | |
LocalPlayer = Players.LocalPlayer | |
end | |
local function waitForChildOfClass(parent, class) | |
local child = parent:FindFirstChildOfClass(class) | |
while not child or child.ClassName ~= class do | |
child = parent.ChildAdded:Wait() | |
end | |
return child | |
end | |
local PlayerGui = waitForChildOfClass(LocalPlayer, "PlayerGui") | |
local TOAST_OPEN_SIZE = UDim2.new(0, 326, 0, 58) | |
local TOAST_CLOSED_SIZE = UDim2.new(0, 80, 0, 58) | |
local TOAST_BACKGROUND_COLOR = Color3.fromRGB(32, 32, 32) | |
local TOAST_BACKGROUND_TRANS = 0.4 | |
local TOAST_FOREGROUND_COLOR = Color3.fromRGB(200, 200, 200) | |
local TOAST_FOREGROUND_TRANS = 0 | |
-- Convenient syntax for creating a tree of instanes | |
local function create(className) | |
return function(props) | |
local inst = Instance.new(className) | |
local parent = props.Parent | |
props.Parent = nil | |
for name, val in pairs(props) do | |
if type(name) == "string" then | |
inst[name] = val | |
else | |
val.Parent = inst | |
end | |
end | |
-- Only set parent after all other properties are initialized | |
inst.Parent = parent | |
return inst | |
end | |
end | |
local initialized = false | |
local uiRoot | |
local toast | |
local toastIcon | |
local toastUpperText | |
local toastLowerText | |
local function initializeUI() | |
assert(not initialized) | |
uiRoot = create("ScreenGui"){ | |
Name = "RbxCameraUI", | |
AutoLocalize = false, | |
Enabled = true, | |
DisplayOrder = -1, -- Appears behind default developer UI | |
IgnoreGuiInset = false, | |
ResetOnSpawn = false, | |
ZIndexBehavior = Enum.ZIndexBehavior.Sibling, | |
create("ImageLabel"){ | |
Name = "Toast", | |
Visible = false, | |
AnchorPoint = Vector2.new(0.5, 0), | |
BackgroundTransparency = 1, | |
BorderSizePixel = 0, | |
Position = UDim2.new(0.5, 0, 0, 8), | |
Size = TOAST_CLOSED_SIZE, | |
Image = "rbxasset://textures/ui/Camera/CameraToast9Slice.png", | |
ImageColor3 = TOAST_BACKGROUND_COLOR, | |
ImageRectSize = Vector2.new(6, 6), | |
ImageTransparency = 1, | |
ScaleType = Enum.ScaleType.Slice, | |
SliceCenter = Rect.new(3, 3, 3, 3), | |
ClipsDescendants = true, | |
create("Frame"){ | |
Name = "IconBuffer", | |
BackgroundTransparency = 1, | |
BorderSizePixel = 0, | |
Position = UDim2.new(0, 0, 0, 0), | |
Size = UDim2.new(0, 80, 1, 0), | |
create("ImageLabel"){ | |
Name = "Icon", | |
AnchorPoint = Vector2.new(0.5, 0.5), | |
BackgroundTransparency = 1, | |
Position = UDim2.new(0.5, 0, 0.5, 0), | |
Size = UDim2.new(0, 48, 0, 48), | |
ZIndex = 2, | |
Image = "rbxasset://textures/ui/Camera/CameraToastIcon.png", | |
ImageColor3 = TOAST_FOREGROUND_COLOR, | |
ImageTransparency = 1, | |
} | |
}, | |
create("Frame"){ | |
Name = "TextBuffer", | |
BackgroundTransparency = 1, | |
BorderSizePixel = 0, | |
Position = UDim2.new(0, 80, 0, 0), | |
Size = UDim2.new(1, -80, 1, 0), | |
ClipsDescendants = true, | |
create("TextLabel"){ | |
Name = "Upper", | |
AnchorPoint = Vector2.new(0, 1), | |
BackgroundTransparency = 1, | |
Position = UDim2.new(0, 0, 0.5, 0), | |
Size = UDim2.new(1, 0, 0, 19), | |
Font = Enum.Font.GothamSemibold, | |
Text = "Camera control enabled", | |
TextColor3 = TOAST_FOREGROUND_COLOR, | |
TextTransparency = 1, | |
TextSize = 19, | |
TextXAlignment = Enum.TextXAlignment.Left, | |
TextYAlignment = Enum.TextYAlignment.Center, | |
}, | |
create("TextLabel"){ | |
Name = "Lower", | |
AnchorPoint = Vector2.new(0, 0), | |
BackgroundTransparency = 1, | |
Position = UDim2.new(0, 0, 0.5, 3), | |
Size = UDim2.new(1, 0, 0, 15), | |
Font = Enum.Font.Gotham, | |
Text = "Right mouse button to toggle", | |
TextColor3 = TOAST_FOREGROUND_COLOR, | |
TextTransparency = 1, | |
TextSize = 15, | |
TextXAlignment = Enum.TextXAlignment.Left, | |
TextYAlignment = Enum.TextYAlignment.Center, | |
}, | |
}, | |
}, | |
Parent = PlayerGui, | |
} | |
toast = uiRoot.Toast | |
toastIcon = toast.IconBuffer.Icon | |
toastUpperText = toast.TextBuffer.Upper | |
toastLowerText = toast.TextBuffer.Lower | |
initialized = true | |
end | |
local CameraUI = {} | |
do | |
-- Instantaneously disable the toast or enable for opening later on. Used when switching camera modes. | |
function CameraUI.setCameraModeToastEnabled(enabled) | |
if not enabled and not initialized then | |
return | |
end | |
if not initialized then | |
initializeUI() | |
end | |
toast.Visible = enabled | |
if not enabled then | |
CameraUI.setCameraModeToastOpen(false) | |
end | |
end | |
local tweenInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) | |
-- Tween the toast in or out. Toast must be enabled with setCameraModeToastEnabled. | |
function CameraUI.setCameraModeToastOpen(open) | |
assert(initialized) | |
TweenService:Create(toast, tweenInfo, { | |
Size = open and TOAST_OPEN_SIZE or TOAST_CLOSED_SIZE, | |
ImageTransparency = open and TOAST_BACKGROUND_TRANS or 1, | |
}):Play() | |
TweenService:Create(toastIcon, tweenInfo, { | |
ImageTransparency = open and TOAST_FOREGROUND_TRANS or 1, | |
}):Play() | |
TweenService:Create(toastUpperText, tweenInfo, { | |
TextTransparency = open and TOAST_FOREGROUND_TRANS or 1, | |
}):Play() | |
TweenService:Create(toastLowerText, tweenInfo, { | |
TextTransparency = open and TOAST_FOREGROUND_TRANS or 1, | |
}):Play() | |
end | |
end | |
return CameraUI | |
end | |
function _CameraToggleStateController() | |
local Players = game:GetService("Players") | |
local UserInputService = game:GetService("UserInputService") | |
local GameSettings = UserSettings():GetService("UserGameSettings") | |
local LocalPlayer = Players.LocalPlayer | |
if not LocalPlayer then | |
Players:GetPropertyChangedSignal("LocalPlayer"):Wait() | |
LocalPlayer = Players.LocalPlayer | |
end | |
local Mouse = LocalPlayer:GetMouse() | |
local Input = _CameraInput() | |
local CameraUI = _CameraUI() | |
local lastTogglePan = false | |
local lastTogglePanChange = tick() | |
local CROSS_MOUSE_ICON = "rbxasset://textures/Cursors/CrossMouseIcon.png" | |
local lockStateDirty = false | |
local wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = false | |
local lastFirstPerson = false | |
CameraUI.setCameraModeToastEnabled(false) | |
return function(isFirstPerson) | |
local togglePan = Input.getTogglePan() | |
local toastTimeout = 3 | |
if isFirstPerson and togglePan ~= lastTogglePan then | |
lockStateDirty = true | |
end | |
if lastTogglePan ~= togglePan or tick() - lastTogglePanChange > toastTimeout then | |
local doShow = togglePan and tick() - lastTogglePanChange < toastTimeout | |
CameraUI.setCameraModeToastOpen(doShow) | |
if togglePan then | |
lockStateDirty = false | |
end | |
lastTogglePanChange = tick() | |
lastTogglePan = togglePan | |
end | |
if isFirstPerson ~= lastFirstPerson then | |
if isFirstPerson then | |
wasTogglePanOnTheLastTimeYouWentIntoFirstPerson = Input.getTogglePan() | |
Input.setTogglePan(true) | |
elseif not lockStateDirty then | |
Input.setTogglePan(wasTogglePanOnTheLastTimeYouWentIntoFirstPerson) | |
end | |
end | |
if isFirstPerson then | |
if Input.getTogglePan() then | |
Mouse.Icon = CROSS_MOUSE_ICON | |
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter | |
--GameSettings.RotationType = Enum.RotationType.CameraRelative | |
else | |
Mouse.Icon = "" | |
UserInputService.MouseBehavior = Enum.MouseBehavior.Default | |
--GameSettings.RotationType = Enum.RotationType.CameraRelative | |
end | |
elseif Input.getTogglePan() then | |
Mouse.Icon = CROSS_MOUSE_ICON | |
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter | |
GameSettings.RotationType = Enum.RotationType.MovementRelative | |
elseif Input.getHoldPan() then | |
Mouse.Icon = "" | |
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition | |
GameSettings.RotationType = Enum.RotationType.MovementRelative | |
else | |
Mouse.Icon = "" | |
UserInputService.MouseBehavior = Enum.MouseBehavior.Default | |
GameSettings.RotationType = Enum.RotationType.MovementRelative | |
end | |
lastFirstPerson = isFirstPerson | |
end | |
end | |
function _CameraInput() | |
local UserInputService = game:GetService("UserInputService") | |
local MB_TAP_LENGTH = 0.3 -- length of time for a short mouse button tap to be registered | |
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 holdPan = false | |
local togglePan = false | |
local lastRmbDown = 0 -- tick() timestamp of the last right mouse button down event | |
local CameraInput = {} | |
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 | |
return CameraInput | |
end | |
function _BaseCamera() | |
--[[ | |
BaseCamera - Abstract base class for camera control modules | |
2018 Camera Update - AllYourBlox | |
--]] | |
--[[ Local Constants ]]-- | |
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 THUMBSTICK_DEADZONE = 0.2 | |
local DEFAULT_DISTANCE = 12.5 -- Studs | |
local PORTRAIT_DEFAULT_DISTANCE = 25 -- Studs | |
local FIRST_PERSON_DISTANCE_THRESHOLD = 1.0 -- Below this value, snap into first person | |
local CAMERA_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value | |
-- Note: DotProduct check in CoordinateFrame::lookAt() prevents using values within about | |
-- 8.11 degrees of the +/- Y axis, that's why these limits are currently 80 degrees | |
local MIN_Y = math.rad(-80) | |
local MAX_Y = math.rad(80) | |
local TOUCH_ADJUST_AREA_UP = math.rad(30) | |
local TOUCH_ADJUST_AREA_DOWN = math.rad(-15) | |
local TOUCH_SENSITIVTY_ADJUST_MAX_Y = 2.1 | |
local TOUCH_SENSITIVTY_ADJUST_MIN_Y = 0.5 | |
local VR_ANGLE = math.rad(15) | |
local VR_LOW_INTENSITY_ROTATION = Vector2.new(math.rad(15), 0) | |
local VR_HIGH_INTENSITY_ROTATION = Vector2.new(math.rad(45), 0) | |
local VR_LOW_INTENSITY_REPEAT = 0.1 | |
local VR_HIGH_INTENSITY_REPEAT = 0.4 | |
local ZERO_VECTOR2 = Vector2.new(0,0) | |
local ZERO_VECTOR3 = Vector3.new(0,0,0) | |
local TOUCH_SENSITIVTY = Vector2.new(0.00945 * math.pi, 0.003375 * math.pi) | |
local MOUSE_SENSITIVITY = Vector2.new( 0.002 * math.pi, 0.0015 * math.pi ) | |
local SEAT_OFFSET = Vector3.new(0,5,0) | |
local VR_SEAT_OFFSET = Vector3.new(0,4,0) | |
local HEAD_OFFSET = Vector3.new(0,1.5,0) | |
local R15_HEAD_OFFSET = Vector3.new(0, 1.5, 0) | |
local R15_HEAD_OFFSET_NO_SCALING = Vector3.new(0, 2, 0) | |
local HUMANOID_ROOT_PART_SIZE = Vector3.new(2, 2, 1) | |
local GAMEPAD_ZOOM_STEP_1 = 0 | |
local GAMEPAD_ZOOM_STEP_2 = 10 | |
local GAMEPAD_ZOOM_STEP_3 = 20 | |
local PAN_SENSITIVITY = 20 | |
local ZOOM_SENSITIVITY_CURVATURE = 0.5 | |
local abs = math.abs | |
local sign = math.sign | |
local FFlagUserCameraToggle do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserCameraToggle") | |
end) | |
FFlagUserCameraToggle = success and result | |
end | |
local FFlagUserDontAdjustSensitvityForPortrait do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserDontAdjustSensitvityForPortrait") | |
end) | |
FFlagUserDontAdjustSensitvityForPortrait = success and result | |
end | |
local FFlagUserFixZoomInZoomOutDiscrepancy do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserFixZoomInZoomOutDiscrepancy") | |
end) | |
FFlagUserFixZoomInZoomOutDiscrepancy = success and result | |
end | |
local Util = _CameraUtils() | |
local ZoomController = _ZoomController() | |
local CameraToggleStateController = _CameraToggleStateController() | |
local CameraInput = _CameraInput() | |
local CameraUI = _CameraUI() | |
--[[ Roblox Services ]]-- | |
local Players = game:GetService("Players") | |
local UserInputService = game:GetService("UserInputService") | |
local StarterGui = game:GetService("StarterGui") | |
local GuiService = game:GetService("GuiService") | |
local ContextActionService = game:GetService("ContextActionService") | |
local VRService = game:GetService("VRService") | |
local UserGameSettings = UserSettings():GetService("UserGameSettings") | |
local player = Players.LocalPlayer | |
--[[ The Module ]]-- | |
local BaseCamera = {} | |
BaseCamera.__index = BaseCamera | |
function BaseCamera.new() | |
local self = setmetatable({}, BaseCamera) | |
-- So that derived classes have access to this | |
self.FIRST_PERSON_DISTANCE_THRESHOLD = FIRST_PERSON_DISTANCE_THRESHOLD | |
self.cameraType = nil | |
self.cameraMovementMode = nil | |
self.lastCameraTransform = nil | |
self.rotateInput = ZERO_VECTOR2 | |
self.userPanningCamera = false | |
self.lastUserPanCamera = tick() | |
self.humanoidRootPart = nil | |
self.humanoidCache = {} | |
-- Subject and position on last update call | |
self.lastSubject = nil | |
self.lastSubjectPosition = Vector3.new(0,5,0) | |
-- These subject distance members refer to the nominal camera-to-subject follow distance that the camera | |
-- is trying to maintain, not the actual measured value. | |
-- The default is updated when screen orientation or the min/max distances change, | |
-- to be sure the default is always in range and appropriate for the orientation. | |
self.defaultSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) | |
self.currentSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) | |
self.inFirstPerson = false | |
self.inMouseLockedMode = false | |
self.portraitMode = false | |
self.isSmallTouchScreen = false | |
-- Used by modules which want to reset the camera angle on respawn. | |
self.resetCameraAngle = true | |
self.enabled = false | |
-- Input Event Connections | |
self.inputBeganConn = nil | |
self.inputChangedConn = nil | |
self.inputEndedConn = nil | |
self.startPos = nil | |
self.lastPos = nil | |
self.panBeginLook = nil | |
self.panEnabled = true | |
self.keyPanEnabled = true | |
self.distanceChangeEnabled = true | |
self.PlayerGui = nil | |
self.cameraChangedConn = nil | |
self.viewportSizeChangedConn = nil | |
self.boundContextActions = {} | |
-- VR Support | |
self.shouldUseVRRotation = false | |
self.VRRotationIntensityAvailable = false | |
self.lastVRRotationIntensityCheckTime = 0 | |
self.lastVRRotationTime = 0 | |
self.vrRotateKeyCooldown = {} | |
self.cameraTranslationConstraints = Vector3.new(1, 1, 1) | |
self.humanoidJumpOrigin = nil | |
self.trackingHumanoid = nil | |
self.cameraFrozen = false | |
self.subjectStateChangedConn = nil | |
-- Gamepad support | |
self.activeGamepad = nil | |
self.gamepadPanningCamera = false | |
self.lastThumbstickRotate = nil | |
self.numOfSeconds = 0.7 | |
self.currentSpeed = 0 | |
self.maxSpeed = 6 | |
self.vrMaxSpeed = 4 | |
self.lastThumbstickPos = Vector2.new(0,0) | |
self.ySensitivity = 0.65 | |
self.lastVelocity = nil | |
self.gamepadConnectedConn = nil | |
self.gamepadDisconnectedConn = nil | |
self.currentZoomSpeed = 1.0 | |
self.L3ButtonDown = false | |
self.dpadLeftDown = false | |
self.dpadRightDown = false | |
-- Touch input support | |
self.isDynamicThumbstickEnabled = false | |
self.fingerTouches = {} | |
self.dynamicTouchInput = nil | |
self.numUnsunkTouches = 0 | |
self.inputStartPositions = {} | |
self.inputStartTimes = {} | |
self.startingDiff = nil | |
self.pinchBeginZoom = nil | |
self.userPanningTheCamera = false | |
self.touchActivateConn = nil | |
-- Mouse locked formerly known as shift lock mode | |
self.mouseLockOffset = ZERO_VECTOR3 | |
-- [[ NOTICE ]] -- | |
-- Initialization things used to always execute at game load time, but now these camera modules are instantiated | |
-- when needed, so the code here may run well after the start of the game | |
if player.Character then | |
self:OnCharacterAdded(player.Character) | |
end | |
player.CharacterAdded:Connect(function(char) | |
self:OnCharacterAdded(char) | |
end) | |
if self.cameraChangedConn then self.cameraChangedConn:Disconnect() end | |
self.cameraChangedConn = workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(function() | |
self:OnCurrentCameraChanged() | |
end) | |
self:OnCurrentCameraChanged() | |
if self.playerCameraModeChangeConn then self.playerCameraModeChangeConn:Disconnect() end | |
self.playerCameraModeChangeConn = player:GetPropertyChangedSignal("CameraMode"):Connect(function() | |
self:OnPlayerCameraPropertyChange() | |
end) | |
if self.minDistanceChangeConn then self.minDistanceChangeConn:Disconnect() end | |
self.minDistanceChangeConn = player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(function() | |
self:OnPlayerCameraPropertyChange() | |
end) | |
if self.maxDistanceChangeConn then self.maxDistanceChangeConn:Disconnect() end | |
self.maxDistanceChangeConn = player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(function() | |
self:OnPlayerCameraPropertyChange() | |
end) | |
if self.playerDevTouchMoveModeChangeConn then self.playerDevTouchMoveModeChangeConn:Disconnect() end | |
self.playerDevTouchMoveModeChangeConn = player:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(function() | |
self:OnDevTouchMovementModeChanged() | |
end) | |
self:OnDevTouchMovementModeChanged() -- Init | |
if self.gameSettingsTouchMoveMoveChangeConn then self.gameSettingsTouchMoveMoveChangeConn:Disconnect() end | |
self.gameSettingsTouchMoveMoveChangeConn = UserGameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(function() | |
self:OnGameSettingsTouchMovementModeChanged() | |
end) | |
self:OnGameSettingsTouchMovementModeChanged() -- Init | |
UserGameSettings:SetCameraYInvertVisible() | |
UserGameSettings:SetGamepadCameraSensitivityVisible() | |
self.hasGameLoaded = game:IsLoaded() | |
if not self.hasGameLoaded then | |
self.gameLoadedConn = game.Loaded:Connect(function() | |
self.hasGameLoaded = true | |
self.gameLoadedConn:Disconnect() | |
self.gameLoadedConn = nil | |
end) | |
end | |
self:OnPlayerCameraPropertyChange() | |
return self | |
end | |
function BaseCamera:GetModuleName() | |
return "BaseCamera" | |
end | |
function BaseCamera:OnCharacterAdded(char) | |
self.resetCameraAngle = self.resetCameraAngle or self:GetEnabled() | |
self.humanoidRootPart = nil | |
if UserInputService.TouchEnabled then | |
self.PlayerGui = player:WaitForChild("PlayerGui") | |
for _, child in ipairs(char:GetChildren()) do | |
if child:IsA("Tool") then | |
self.isAToolEquipped = true | |
end | |
end | |
char.ChildAdded:Connect(function(child) | |
if child:IsA("Tool") then | |
self.isAToolEquipped = true | |
end | |
end) | |
char.ChildRemoved:Connect(function(child) | |
if child:IsA("Tool") then | |
self.isAToolEquipped = false | |
end | |
end) | |
end | |
end | |
function BaseCamera:GetHumanoidRootPart() | |
if not self.humanoidRootPart then | |
if player.Character then | |
local humanoid = player.Character:FindFirstChildOfClass("Humanoid") | |
if humanoid then | |
self.humanoidRootPart = humanoid.RootPart | |
end | |
end | |
end | |
return self.humanoidRootPart | |
end | |
function BaseCamera:GetBodyPartToFollow(humanoid, isDead) | |
-- If the humanoid is dead, prefer the head part if one still exists as a sibling of the humanoid | |
if humanoid:GetState() == Enum.HumanoidStateType.Dead then | |
local character = humanoid.Parent | |
if character and character:IsA("Model") then | |
return character:FindFirstChild("Head") or humanoid.RootPart | |
end | |
end | |
return humanoid.RootPart | |
end | |
function BaseCamera:GetSubjectPosition() | |
local result = self.lastSubjectPosition | |
local camera = game.Workspace.CurrentCamera | |
local cameraSubject = camera and camera.CameraSubject | |
if cameraSubject then | |
if cameraSubject:IsA("Humanoid") then | |
local humanoid = cameraSubject | |
local humanoidIsDead = humanoid:GetState() == Enum.HumanoidStateType.Dead | |
if VRService.VREnabled and humanoidIsDead and humanoid == self.lastSubject then | |
result = self.lastSubjectPosition | |
else | |
local bodyPartToFollow = humanoid.RootPart | |
-- If the humanoid is dead, prefer their head part as a follow target, if it exists | |
if humanoidIsDead then | |
if humanoid.Parent and humanoid.Parent:IsA("Model") then | |
bodyPartToFollow = humanoid.Parent:FindFirstChild("Head") or bodyPartToFollow | |
end | |
end | |
if bodyPartToFollow and bodyPartToFollow:IsA("BasePart") then | |
local heightOffset | |
if humanoid.RigType == Enum.HumanoidRigType.R15 then | |
if humanoid.AutomaticScalingEnabled then | |
heightOffset = R15_HEAD_OFFSET | |
if bodyPartToFollow == humanoid.RootPart then | |
local rootPartSizeOffset = (humanoid.RootPart.Size.Y/2) - (HUMANOID_ROOT_PART_SIZE.Y/2) | |
heightOffset = heightOffset + Vector3.new(0, rootPartSizeOffset, 0) | |
end | |
else | |
heightOffset = R15_HEAD_OFFSET_NO_SCALING | |
end | |
else | |
heightOffset = HEAD_OFFSET | |
end | |
if humanoidIsDead then | |
heightOffset = ZERO_VECTOR3 | |
end | |
result = bodyPartToFollow.CFrame.p + bodyPartToFollow.CFrame:vectorToWorldSpace(heightOffset + humanoid.CameraOffset) | |
end | |
end | |
elseif cameraSubject:IsA("VehicleSeat") then | |
local offset = SEAT_OFFSET | |
if VRService.VREnabled then | |
offset = VR_SEAT_OFFSET | |
end | |
result = cameraSubject.CFrame.p + cameraSubject.CFrame:vectorToWorldSpace(offset) | |
elseif cameraSubject:IsA("SkateboardPlatform") then | |
result = cameraSubject.CFrame.p + SEAT_OFFSET | |
elseif cameraSubject:IsA("BasePart") then | |
result = cameraSubject.CFrame.p | |
elseif cameraSubject:IsA("Model") then | |
if cameraSubject.PrimaryPart then | |
result = cameraSubject:GetPrimaryPartCFrame().p | |
else | |
result = cameraSubject:GetModelCFrame().p | |
end | |
end | |
else | |
-- cameraSubject is nil | |
-- Note: Previous RootCamera did not have this else case and let self.lastSubject and self.lastSubjectPosition | |
-- both get set to nil in the case of cameraSubject being nil. This function now exits here to preserve the | |
-- last set valid values for these, as nil values are not handled cases | |
return | |
end | |
self.lastSubject = cameraSubject | |
self.lastSubjectPosition = result | |
return result | |
end | |
function BaseCamera:UpdateDefaultSubjectDistance() | |
if self.portraitMode then | |
self.defaultSubjectDistance = math.clamp(PORTRAIT_DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) | |
else | |
self.defaultSubjectDistance = math.clamp(DEFAULT_DISTANCE, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) | |
end | |
end | |
function BaseCamera:OnViewportSizeChanged() | |
local camera = game.Workspace.CurrentCamera | |
local size = camera.ViewportSize | |
self.portraitMode = size.X < size.Y | |
self.isSmallTouchScreen = UserInputService.TouchEnabled and (size.Y < 500 or size.X < 700) | |
self:UpdateDefaultSubjectDistance() | |
end | |
-- Listener for changes to workspace.CurrentCamera | |
function BaseCamera:OnCurrentCameraChanged() | |
if UserInputService.TouchEnabled then | |
if self.viewportSizeChangedConn then | |
self.viewportSizeChangedConn:Disconnect() | |
self.viewportSizeChangedConn = nil | |
end | |
local newCamera = game.Workspace.CurrentCamera | |
if newCamera then | |
self:OnViewportSizeChanged() | |
self.viewportSizeChangedConn = newCamera:GetPropertyChangedSignal("ViewportSize"):Connect(function() | |
self:OnViewportSizeChanged() | |
end) | |
end | |
end | |
-- VR support additions | |
if self.cameraSubjectChangedConn then | |
self.cameraSubjectChangedConn:Disconnect() | |
self.cameraSubjectChangedConn = nil | |
end | |
local camera = game.Workspace.CurrentCamera | |
if camera then | |
self.cameraSubjectChangedConn = camera:GetPropertyChangedSignal("CameraSubject"):Connect(function() | |
self:OnNewCameraSubject() | |
end) | |
self:OnNewCameraSubject() | |
end | |
end | |
function BaseCamera:OnDynamicThumbstickEnabled() | |
if UserInputService.TouchEnabled then | |
self.isDynamicThumbstickEnabled = true | |
end | |
end | |
function BaseCamera:OnDynamicThumbstickDisabled() | |
self.isDynamicThumbstickEnabled = false | |
end | |
function BaseCamera:OnGameSettingsTouchMovementModeChanged() | |
if player.DevTouchMovementMode == Enum.DevTouchMovementMode.UserChoice then | |
if (UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.DynamicThumbstick | |
or UserGameSettings.TouchMovementMode == Enum.TouchMovementMode.Default) then | |
self:OnDynamicThumbstickEnabled() | |
else | |
self:OnDynamicThumbstickDisabled() | |
end | |
end | |
end | |
function BaseCamera:OnDevTouchMovementModeChanged() | |
if player.DevTouchMovementMode.Name == "DynamicThumbstick" then | |
self:OnDynamicThumbstickEnabled() | |
else | |
self:OnGameSettingsTouchMovementModeChanged() | |
end | |
end | |
function BaseCamera:OnPlayerCameraPropertyChange() | |
-- This call forces re-evaluation of player.CameraMode and clamping to min/max distance which may have changed | |
self:SetCameraToSubjectDistance(self.currentSubjectDistance) | |
end | |
function BaseCamera:GetCameraHeight() | |
if VRService.VREnabled and not self.inFirstPerson then | |
return math.sin(VR_ANGLE) * self.currentSubjectDistance | |
end | |
return 0 | |
end | |
function BaseCamera:InputTranslationToCameraAngleChange(translationVector, sensitivity) | |
if not FFlagUserDontAdjustSensitvityForPortrait then | |
local camera = game.Workspace.CurrentCamera | |
if camera and camera.ViewportSize.X > 0 and camera.ViewportSize.Y > 0 and (camera.ViewportSize.Y > camera.ViewportSize.X) then | |
-- Screen has portrait orientation, swap X and Y sensitivity | |
return translationVector * Vector2.new( sensitivity.Y, sensitivity.X) | |
end | |
end | |
return translationVector * sensitivity | |
end | |
function BaseCamera:Enable(enable) | |
if self.enabled ~= enable then | |
self.enabled = enable | |
if self.enabled then | |
self:ConnectInputEvents() | |
self:BindContextActions() | |
if player.CameraMode == Enum.CameraMode.LockFirstPerson then | |
self.currentSubjectDistance = 0.5 | |
if not self.inFirstPerson then | |
self:EnterFirstPerson() | |
end | |
end | |
else | |
self:DisconnectInputEvents() | |
self:UnbindContextActions() | |
-- Clean up additional event listeners and reset a bunch of properties | |
self:Cleanup() | |
end | |
end | |
end | |
function BaseCamera:GetEnabled() | |
return self.enabled | |
end | |
function BaseCamera:OnInputBegan(input, processed) | |
if input.UserInputType == Enum.UserInputType.Touch then | |
self:OnTouchBegan(input, processed) | |
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then | |
self:OnMouse2Down(input, processed) | |
elseif input.UserInputType == Enum.UserInputType.MouseButton3 then | |
self:OnMouse3Down(input, processed) | |
end | |
end | |
function BaseCamera:OnInputChanged(input, processed) | |
if input.UserInputType == Enum.UserInputType.Touch then | |
self:OnTouchChanged(input, processed) | |
elseif input.UserInputType == Enum.UserInputType.MouseMovement then | |
self:OnMouseMoved(input, processed) | |
end | |
end | |
function BaseCamera:OnInputEnded(input, processed) | |
if input.UserInputType == Enum.UserInputType.Touch then | |
self:OnTouchEnded(input, processed) | |
elseif input.UserInputType == Enum.UserInputType.MouseButton2 then | |
self:OnMouse2Up(input, processed) | |
elseif input.UserInputType == Enum.UserInputType.MouseButton3 then | |
self:OnMouse3Up(input, processed) | |
end | |
end | |
function BaseCamera:OnPointerAction(wheel, pan, pinch, processed) | |
if processed then | |
return | |
end | |
if pan.Magnitude > 0 then | |
local inversionVector = Vector2.new(1, UserGameSettings:GetCameraYInvertValue()) | |
local rotateDelta = self:InputTranslationToCameraAngleChange(PAN_SENSITIVITY*pan, MOUSE_SENSITIVITY)*inversionVector | |
self.rotateInput = self.rotateInput + rotateDelta | |
end | |
local zoom = self.currentSubjectDistance | |
local zoomDelta = -(wheel + pinch) | |
if abs(zoomDelta) > 0 then | |
local newZoom | |
if self.inFirstPerson and zoomDelta > 0 then | |
newZoom = FIRST_PERSON_DISTANCE_THRESHOLD | |
else | |
if FFlagUserFixZoomInZoomOutDiscrepancy then | |
if (zoomDelta > 0) then | |
newZoom = zoom + zoomDelta*(1 + zoom*ZOOM_SENSITIVITY_CURVATURE) | |
else | |
newZoom = (zoom + zoomDelta) / (1 - zoomDelta*ZOOM_SENSITIVITY_CURVATURE) | |
end | |
else | |
newZoom = zoom + zoomDelta*(1 + zoom*ZOOM_SENSITIVITY_CURVATURE) | |
end | |
end | |
self:SetCameraToSubjectDistance(newZoom) | |
end | |
end | |
function BaseCamera:ConnectInputEvents() | |
self.pointerActionConn = UserInputService.PointerAction:Connect(function(wheel, pan, pinch, processed) | |
self:OnPointerAction(wheel, pan, pinch, processed) | |
end) | |
self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) | |
self:OnInputBegan(input, processed) | |
end) | |
self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed) | |
self:OnInputChanged(input, processed) | |
end) | |
self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed) | |
self:OnInputEnded(input, processed) | |
end) | |
self.menuOpenedConn = GuiService.MenuOpened:connect(function() | |
self:ResetInputStates() | |
end) | |
self.gamepadConnectedConn = UserInputService.GamepadDisconnected:connect(function(gamepadEnum) | |
if self.activeGamepad ~= gamepadEnum then return end | |
self.activeGamepad = nil | |
self:AssignActivateGamepad() | |
end) | |
self.gamepadDisconnectedConn = UserInputService.GamepadConnected:connect(function(gamepadEnum) | |
if self.activeGamepad == nil then | |
self:AssignActivateGamepad() | |
end | |
end) | |
self:AssignActivateGamepad() | |
if not FFlagUserCameraToggle then | |
self:UpdateMouseBehavior() | |
end | |
end | |
function BaseCamera:BindContextActions() | |
self:BindGamepadInputActions() | |
self:BindKeyboardInputActions() | |
end | |
function BaseCamera:AssignActivateGamepad() | |
local connectedGamepads = UserInputService:GetConnectedGamepads() | |
if #connectedGamepads > 0 then | |
for i = 1, #connectedGamepads do | |
if self.activeGamepad == nil then | |
self.activeGamepad = connectedGamepads[i] | |
elseif connectedGamepads[i].Value < self.activeGamepad.Value then | |
self.activeGamepad = connectedGamepads[i] | |
end | |
end | |
end | |
if self.activeGamepad == nil then -- nothing is connected, at least set up for gamepad1 | |
self.activeGamepad = Enum.UserInputType.Gamepad1 | |
end | |
end | |
function BaseCamera:DisconnectInputEvents() | |
if self.inputBeganConn then | |
self.inputBeganConn:Disconnect() | |
self.inputBeganConn = nil | |
end | |
if self.inputChangedConn then | |
self.inputChangedConn:Disconnect() | |
self.inputChangedConn = nil | |
end | |
if self.inputEndedConn then | |
self.inputEndedConn:Disconnect() | |
self.inputEndedConn = nil | |
end | |
end | |
function BaseCamera:UnbindContextActions() | |
for i = 1, #self.boundContextActions do | |
ContextActionService:UnbindAction(self.boundContextActions[i]) | |
end | |
self.boundContextActions = {} | |
end | |
function BaseCamera:Cleanup() | |
if self.pointerActionConn then | |
self.pointerActionConn:Disconnect() | |
self.pointerActionConn = nil | |
end | |
if self.menuOpenedConn then | |
self.menuOpenedConn:Disconnect() | |
self.menuOpenedConn = nil | |
end | |
if self.mouseLockToggleConn then | |
self.mouseLockToggleConn:Disconnect() | |
self.mouseLockToggleConn = nil | |
end | |
if self.gamepadConnectedConn then | |
self.gamepadConnectedConn:Disconnect() | |
self.gamepadConnectedConn = nil | |
end | |
if self.gamepadDisconnectedConn then | |
self.gamepadDisconnectedConn:Disconnect() | |
self.gamepadDisconnectedConn = nil | |
end | |
if self.subjectStateChangedConn then | |
self.subjectStateChangedConn:Disconnect() | |
self.subjectStateChangedConn = nil | |
end | |
if self.viewportSizeChangedConn then | |
self.viewportSizeChangedConn:Disconnect() | |
self.viewportSizeChangedConn = nil | |
end | |
if self.touchActivateConn then | |
self.touchActivateConn:Disconnect() | |
self.touchActivateConn = nil | |
end | |
self.turningLeft = false | |
self.turningRight = false | |
self.lastCameraTransform = nil | |
self.lastSubjectCFrame = nil | |
self.userPanningTheCamera = false | |
self.rotateInput = Vector2.new() | |
self.gamepadPanningCamera = Vector2.new(0,0) | |
-- Reset input states | |
self.startPos = nil | |
self.lastPos = nil | |
self.panBeginLook = nil | |
self.isRightMouseDown = false | |
self.isMiddleMouseDown = false | |
self.fingerTouches = {} | |
self.dynamicTouchInput = nil | |
self.numUnsunkTouches = 0 | |
self.startingDiff = nil | |
self.pinchBeginZoom = nil | |
-- Unlock mouse for example if right mouse button was being held down | |
if UserInputService.MouseBehavior ~= Enum.MouseBehavior.LockCenter then | |
UserInputService.MouseBehavior = Enum.MouseBehavior.Default | |
end | |
end | |
-- This is called when settings menu is opened | |
function BaseCamera:ResetInputStates() | |
self.isRightMouseDown = false | |
self.isMiddleMouseDown = false | |
self:OnMousePanButtonReleased() -- this function doesn't seem to actually need parameters | |
if UserInputService.TouchEnabled then | |
--[[menu opening was causing serious touch issues | |
this should disable all active touch events if | |
they're active when menu opens.]] | |
for inputObject in pairs(self.fingerTouches) do | |
self.fingerTouches[inputObject] = nil | |
end | |
self.dynamicTouchInput = nil | |
self.panBeginLook = nil | |
self.startPos = nil | |
self.lastPos = nil | |
self.userPanningTheCamera = false | |
self.startingDiff = nil | |
self.pinchBeginZoom = nil | |
self.numUnsunkTouches = 0 | |
end | |
end | |
function BaseCamera:GetGamepadPan(name, state, input) | |
if input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.Thumbstick2 then | |
-- if self.L3ButtonDown then | |
-- -- L3 Thumbstick is depressed, right stick controls dolly in/out | |
-- if (input.Position.Y > THUMBSTICK_DEADZONE) then | |
-- self.currentZoomSpeed = 0.96 | |
-- elseif (input.Position.Y < -THUMBSTICK_DEADZONE) then | |
-- self.currentZoomSpeed = 1.04 | |
-- else | |
-- self.currentZoomSpeed = 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 | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
function BaseCamera:DoKeyboardPanTurn(name, state, input) | |
if not self.hasGameLoaded and VRService.VREnabled then | |
return Enum.ContextActionResult.Pass | |
end | |
if state == Enum.UserInputState.Cancel then | |
self.turningLeft = false | |
self.turningRight = false | |
return Enum.ContextActionResult.Sink | |
end | |
if self.panBeginLook == nil and self.keyPanEnabled then | |
if input.KeyCode == Enum.KeyCode.Left then | |
self.turningLeft = state == Enum.UserInputState.Begin | |
elseif input.KeyCode == Enum.KeyCode.Right then | |
self.turningRight = state == Enum.UserInputState.Begin | |
end | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
function BaseCamera:DoPanRotateCamera(rotateAngle) | |
local angle = Util.RotateVectorByAngleAndRound(self:GetCameraLookVector() * Vector3.new(1,0,1), rotateAngle, math.pi*0.25) | |
if angle ~= 0 then | |
self.rotateInput = self.rotateInput + Vector2.new(angle, 0) | |
self.lastUserPanCamera = tick() | |
self.lastCameraTransform = nil | |
end | |
end | |
function BaseCamera:DoGamepadZoom(name, state, input) | |
if input.UserInputType == self.activeGamepad then | |
if input.KeyCode == Enum.KeyCode.ButtonR3 then | |
if state == Enum.UserInputState.Begin then | |
if self.distanceChangeEnabled then | |
local dist = self:GetCameraToSubjectDistance() | |
if dist > (GAMEPAD_ZOOM_STEP_2 + GAMEPAD_ZOOM_STEP_3)/2 then | |
self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_2) | |
elseif dist > (GAMEPAD_ZOOM_STEP_1 + GAMEPAD_ZOOM_STEP_2)/2 then | |
self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_1) | |
else | |
self:SetCameraToSubjectDistance(GAMEPAD_ZOOM_STEP_3) | |
end | |
end | |
end | |
elseif input.KeyCode == Enum.KeyCode.DPadLeft then | |
self.dpadLeftDown = (state == Enum.UserInputState.Begin) | |
elseif input.KeyCode == Enum.KeyCode.DPadRight then | |
self.dpadRightDown = (state == Enum.UserInputState.Begin) | |
end | |
if self.dpadLeftDown then | |
self.currentZoomSpeed = 1.04 | |
elseif self.dpadRightDown then | |
self.currentZoomSpeed = 0.96 | |
else | |
self.currentZoomSpeed = 1.00 | |
end | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
-- elseif input.UserInputType == self.activeGamepad and input.KeyCode == Enum.KeyCode.ButtonL3 then | |
-- if (state == Enum.UserInputState.Begin) then | |
-- self.L3ButtonDown = true | |
-- elseif (state == Enum.UserInputState.End) then | |
-- self.L3ButtonDown = false | |
-- self.currentZoomSpeed = 1.00 | |
-- end | |
-- end | |
end | |
function BaseCamera:DoKeyboardZoom(name, state, input) | |
if not self.hasGameLoaded and VRService.VREnabled then | |
return Enum.ContextActionResult.Pass | |
end | |
if state ~= Enum.UserInputState.Begin then | |
return Enum.ContextActionResult.Pass | |
end | |
if self.distanceChangeEnabled and player.CameraMode ~= Enum.CameraMode.LockFirstPerson then | |
if input.KeyCode == Enum.KeyCode.I then | |
self:SetCameraToSubjectDistance( self.currentSubjectDistance - 5 ) | |
elseif input.KeyCode == Enum.KeyCode.O then | |
self:SetCameraToSubjectDistance( self.currentSubjectDistance + 5 ) | |
end | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
function BaseCamera:BindAction(actionName, actionFunc, createTouchButton, ...) | |
table.insert(self.boundContextActions, actionName) | |
ContextActionService:BindActionAtPriority(actionName, actionFunc, createTouchButton, | |
CAMERA_ACTION_PRIORITY, ...) | |
end | |
function BaseCamera:BindGamepadInputActions() | |
self:BindAction("BaseCameraGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end, | |
false, Enum.KeyCode.Thumbstick2) | |
self:BindAction("BaseCameraGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end, | |
false, Enum.KeyCode.DPadLeft, Enum.KeyCode.DPadRight, Enum.KeyCode.ButtonR3) | |
end | |
function BaseCamera:BindKeyboardInputActions() | |
self:BindAction("BaseCameraKeyboardPanArrowKeys", function(name, state, input) return self:DoKeyboardPanTurn(name, state, input) end, | |
false, Enum.KeyCode.Left, Enum.KeyCode.Right) | |
self:BindAction("BaseCameraKeyboardZoom", function(name, state, input) return self:DoKeyboardZoom(name, state, input) end, | |
false, Enum.KeyCode.I, Enum.KeyCode.O) | |
end | |
local function isInDynamicThumbstickArea(input) | |
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 | |
local frameCornerTopLeft = thumbstickFrame.AbsolutePosition | |
local frameCornerBottomRight = frameCornerTopLeft + thumbstickFrame.AbsoluteSize | |
if input.Position.X >= frameCornerTopLeft.X and input.Position.Y >= frameCornerTopLeft.Y then | |
if input.Position.X <= frameCornerBottomRight.X and input.Position.Y <= frameCornerBottomRight.Y then | |
return true | |
end | |
end | |
return false | |
end | |
---Adjusts the camera Y touch Sensitivity when moving away from the center and in the TOUCH_SENSITIVTY_ADJUST_AREA | |
function BaseCamera:AdjustTouchSensitivity(delta, sensitivity) | |
local cameraCFrame = game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame | |
if not cameraCFrame then | |
return sensitivity | |
end | |
local currPitchAngle = cameraCFrame:ToEulerAnglesYXZ() | |
local multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y | |
if currPitchAngle > TOUCH_ADJUST_AREA_UP and delta.Y < 0 then | |
local fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_UP)/(MAX_Y - TOUCH_ADJUST_AREA_UP) | |
fractionAdjust = 1 - (1 - fractionAdjust)^3 | |
multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * ( | |
TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y) | |
elseif currPitchAngle < TOUCH_ADJUST_AREA_DOWN and delta.Y > 0 then | |
local fractionAdjust = (currPitchAngle - TOUCH_ADJUST_AREA_DOWN)/(MIN_Y - TOUCH_ADJUST_AREA_DOWN) | |
fractionAdjust = 1 - (1 - fractionAdjust)^3 | |
multiplierY = TOUCH_SENSITIVTY_ADJUST_MAX_Y - fractionAdjust * ( | |
TOUCH_SENSITIVTY_ADJUST_MAX_Y - TOUCH_SENSITIVTY_ADJUST_MIN_Y) | |
end | |
return Vector2.new( | |
sensitivity.X, | |
sensitivity.Y * multiplierY | |
) | |
end | |
function BaseCamera:OnTouchBegan(input, processed) | |
local canUseDynamicTouch = self.isDynamicThumbstickEnabled and not processed | |
if canUseDynamicTouch then | |
if self.dynamicTouchInput == nil and isInDynamicThumbstickArea(input) then | |
-- First input in the dynamic thumbstick area should always be ignored for camera purposes | |
-- Even if the dynamic thumbstick does not process it immediately | |
self.dynamicTouchInput = input | |
return | |
end | |
self.fingerTouches[input] = processed | |
self.inputStartPositions[input] = input.Position | |
self.inputStartTimes[input] = tick() | |
self.numUnsunkTouches = self.numUnsunkTouches + 1 | |
end | |
end | |
function BaseCamera:OnTouchChanged(input, processed) | |
if self.fingerTouches[input] == nil then | |
if self.isDynamicThumbstickEnabled then | |
return | |
end | |
self.fingerTouches[input] = processed | |
if not processed then | |
self.numUnsunkTouches = self.numUnsunkTouches + 1 | |
end | |
end | |
if self.numUnsunkTouches == 1 then | |
if self.fingerTouches[input] == false then | |
self.panBeginLook = self.panBeginLook or self:GetCameraLookVector() | |
self.startPos = self.startPos or input.Position | |
self.lastPos = self.lastPos or self.startPos | |
self.userPanningTheCamera = true | |
local delta = input.Position - self.lastPos | |
delta = Vector2.new(delta.X, delta.Y * UserGameSettings:GetCameraYInvertValue()) | |
if self.panEnabled then | |
local adjustedTouchSensitivity = TOUCH_SENSITIVTY | |
self:AdjustTouchSensitivity(delta, TOUCH_SENSITIVTY) | |
local desiredXYVector = self:InputTranslationToCameraAngleChange(delta, adjustedTouchSensitivity) | |
self.rotateInput = self.rotateInput + desiredXYVector | |
end | |
self.lastPos = input.Position | |
end | |
else | |
self.panBeginLook = nil | |
self.startPos = nil | |
self.lastPos = nil | |
self.userPanningTheCamera = false | |
end | |
if self.numUnsunkTouches == 2 then | |
local unsunkTouches = {} | |
for touch, wasSunk in pairs(self.fingerTouches) do | |
if not wasSunk then | |
table.insert(unsunkTouches, touch) | |
end | |
end | |
if #unsunkTouches == 2 then | |
local difference = (unsunkTouches[1].Position - unsunkTouches[2].Position).magnitude | |
if self.startingDiff and self.pinchBeginZoom then | |
local scale = difference / math.max(0.01, self.startingDiff) | |
local clampedScale = math.clamp(scale, 0.1, 10) | |
if self.distanceChangeEnabled then | |
self:SetCameraToSubjectDistance(self.pinchBeginZoom / clampedScale) | |
end | |
else | |
self.startingDiff = difference | |
self.pinchBeginZoom = self:GetCameraToSubjectDistance() | |
end | |
end | |
else | |
self.startingDiff = nil | |
self.pinchBeginZoom = nil | |
end | |
end | |
function BaseCamera:OnTouchEnded(input, processed) | |
if input == self.dynamicTouchInput then | |
self.dynamicTouchInput = nil | |
return | |
end | |
if self.fingerTouches[input] == false then | |
if self.numUnsunkTouches == 1 then | |
self.panBeginLook = nil | |
self.startPos = nil | |
self.lastPos = nil | |
self.userPanningTheCamera = false | |
elseif self.numUnsunkTouches == 2 then | |
self.startingDiff = nil | |
self.pinchBeginZoom = nil | |
end | |
end | |
if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then | |
self.numUnsunkTouches = self.numUnsunkTouches - 1 | |
end | |
self.fingerTouches[input] = nil | |
self.inputStartPositions[input] = nil | |
self.inputStartTimes[input] = nil | |
end | |
function BaseCamera:OnMouse2Down(input, processed) | |
if processed then return end | |
self.isRightMouseDown = true | |
self:OnMousePanButtonPressed(input, processed) | |
end | |
function BaseCamera:OnMouse2Up(input, processed) | |
self.isRightMouseDown = false | |
self:OnMousePanButtonReleased(input, processed) | |
end | |
function BaseCamera:OnMouse3Down(input, processed) | |
if processed then return end | |
self.isMiddleMouseDown = true | |
self:OnMousePanButtonPressed(input, processed) | |
end | |
function BaseCamera:OnMouse3Up(input, processed) | |
self.isMiddleMouseDown = false | |
self:OnMousePanButtonReleased(input, processed) | |
end | |
function BaseCamera:OnMouseMoved(input, processed) | |
if not self.hasGameLoaded and VRService.VREnabled then | |
return | |
end | |
local inputDelta = input.Delta | |
inputDelta = Vector2.new(inputDelta.X, inputDelta.Y * UserGameSettings:GetCameraYInvertValue()) | |
local isInputPanning = FFlagUserCameraToggle and CameraInput.getPanning() | |
local isBeginLook = self.startPos and self.lastPos and self.panBeginLook | |
local isPanning = isBeginLook or self.inFirstPerson or self.inMouseLockedMode or isInputPanning | |
if self.panEnabled and isPanning then | |
local desiredXYVector = self:InputTranslationToCameraAngleChange(inputDelta, MOUSE_SENSITIVITY) | |
self.rotateInput = self.rotateInput + desiredXYVector | |
end | |
if self.startPos and self.lastPos and self.panBeginLook then | |
self.lastPos = self.lastPos + input.Delta | |
end | |
end | |
function BaseCamera:OnMousePanButtonPressed(input, processed) | |
if processed then return end | |
if not FFlagUserCameraToggle then | |
self:UpdateMouseBehavior() | |
end | |
self.panBeginLook = self.panBeginLook or self:GetCameraLookVector() | |
self.startPos = self.startPos or input.Position | |
self.lastPos = self.lastPos or self.startPos | |
self.userPanningTheCamera = true | |
end | |
function BaseCamera:OnMousePanButtonReleased(input, processed) | |
if not FFlagUserCameraToggle then | |
self:UpdateMouseBehavior() | |
end | |
if not (self.isRightMouseDown or self.isMiddleMouseDown) then | |
self.panBeginLook = nil | |
self.startPos = nil | |
self.lastPos = nil | |
self.userPanningTheCamera = false | |
end | |
end | |
function BaseCamera:UpdateMouseBehavior() | |
if FFlagUserCameraToggle and self.isCameraToggle then | |
CameraUI.setCameraModeToastEnabled(true) | |
CameraInput.enableCameraToggleInput() | |
CameraToggleStateController(self.inFirstPerson) | |
else | |
if FFlagUserCameraToggle then | |
CameraUI.setCameraModeToastEnabled(false) | |
CameraInput.disableCameraToggleInput() | |
end | |
-- first time transition to first person mode or mouse-locked third person | |
if self.inFirstPerson or self.inMouseLockedMode then | |
--UserGameSettings.RotationType = Enum.RotationType.CameraRelative | |
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter | |
else | |
UserGameSettings.RotationType = Enum.RotationType.MovementRelative | |
if self.isRightMouseDown or self.isMiddleMouseDown then | |
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition | |
else | |
UserInputService.MouseBehavior = Enum.MouseBehavior.Default | |
end | |
end | |
end | |
end | |
function BaseCamera:UpdateForDistancePropertyChange() | |
-- Calling this setter with the current value will force checking that it is still | |
-- in range after a change to the min/max distance limits | |
self:SetCameraToSubjectDistance(self.currentSubjectDistance) | |
end | |
function BaseCamera:SetCameraToSubjectDistance(desiredSubjectDistance) | |
local lastSubjectDistance = self.currentSubjectDistance | |
-- By default, camera modules will respect LockFirstPerson and override the currentSubjectDistance with 0 | |
-- regardless of what Player.CameraMinZoomDistance is set to, so that first person can be made | |
-- available by the developer without needing to allow players to mousewheel dolly into first person. | |
-- Some modules will override this function to remove or change first-person capability. | |
if player.CameraMode == Enum.CameraMode.LockFirstPerson then | |
self.currentSubjectDistance = 0.5 | |
if not self.inFirstPerson then | |
self:EnterFirstPerson() | |
end | |
else | |
local newSubjectDistance = math.clamp(desiredSubjectDistance, player.CameraMinZoomDistance, player.CameraMaxZoomDistance) | |
if newSubjectDistance < FIRST_PERSON_DISTANCE_THRESHOLD then | |
self.currentSubjectDistance = 0.5 | |
if not self.inFirstPerson then | |
self:EnterFirstPerson() | |
end | |
else | |
self.currentSubjectDistance = newSubjectDistance | |
if self.inFirstPerson then | |
self:LeaveFirstPerson() | |
end | |
end | |
end | |
-- Pass target distance and zoom direction to the zoom controller | |
ZoomController.SetZoomParameters(self.currentSubjectDistance, math.sign(desiredSubjectDistance - lastSubjectDistance)) | |
-- Returned only for convenience to the caller to know the outcome | |
return self.currentSubjectDistance | |
end | |
function BaseCamera:SetCameraType( cameraType ) | |
--Used by derived classes | |
self.cameraType = cameraType | |
end | |
function BaseCamera:GetCameraType() | |
return self.cameraType | |
end | |
-- Movement mode standardized to Enum.ComputerCameraMovementMode values | |
function BaseCamera:SetCameraMovementMode( cameraMovementMode ) | |
self.cameraMovementMode = cameraMovementMode | |
end | |
function BaseCamera:GetCameraMovementMode() | |
return self.cameraMovementMode | |
end | |
function BaseCamera:SetIsMouseLocked(mouseLocked) | |
self.inMouseLockedMode = mouseLocked | |
if not FFlagUserCameraToggle then | |
self:UpdateMouseBehavior() | |
end | |
end | |
function BaseCamera:GetIsMouseLocked() | |
return self.inMouseLockedMode | |
end | |
function BaseCamera:SetMouseLockOffset(offsetVector) | |
self.mouseLockOffset = offsetVector | |
end | |
function BaseCamera:GetMouseLockOffset() | |
return self.mouseLockOffset | |
end | |
function BaseCamera:InFirstPerson() | |
return self.inFirstPerson | |
end | |
function BaseCamera:EnterFirstPerson() | |
-- Overridden in ClassicCamera, the only module which supports FirstPerson | |
end | |
function BaseCamera:LeaveFirstPerson() | |
-- Overridden in ClassicCamera, the only module which supports FirstPerson | |
end | |
-- Nominal distance, set by dollying in and out with the mouse wheel or equivalent, not measured distance | |
function BaseCamera:GetCameraToSubjectDistance() | |
return self.currentSubjectDistance | |
end | |
-- Actual measured distance to the camera Focus point, which may be needed in special circumstances, but should | |
-- never be used as the starting point for updating the nominal camera-to-subject distance (self.currentSubjectDistance) | |
-- since that is a desired target value set only by mouse wheel (or equivalent) input, PopperCam, and clamped to min max camera distance | |
function BaseCamera:GetMeasuredDistanceToFocus() | |
local camera = game.Workspace.CurrentCamera | |
if camera then | |
return (camera.CoordinateFrame.p - camera.Focus.p).magnitude | |
end | |
return nil | |
end | |
function BaseCamera:GetCameraLookVector() | |
return game.Workspace.CurrentCamera and game.Workspace.CurrentCamera.CFrame.lookVector or UNIT_Z | |
end | |
-- Replacements for RootCamera:RotateCamera() which did not actually rotate the camera | |
-- suppliedLookVector is not normally passed in, it's used only by Watch camera | |
function BaseCamera:CalculateNewLookCFrame(suppliedLookVector) | |
local currLookVector = suppliedLookVector or self:GetCameraLookVector() | |
local currPitchAngle = math.asin(currLookVector.y) | |
local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle) | |
local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta) | |
local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) | |
local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0) | |
return newLookCFrame | |
end | |
function BaseCamera:CalculateNewLookVector(suppliedLookVector) | |
local newLookCFrame = self:CalculateNewLookCFrame(suppliedLookVector) | |
return newLookCFrame.lookVector | |
end | |
function BaseCamera:CalculateNewLookVectorVR() | |
local subjectPosition = self:GetSubjectPosition() | |
local vecToSubject = (subjectPosition - game.Workspace.CurrentCamera.CFrame.p) | |
local currLookVector = (vecToSubject * X1_Y0_Z1).unit | |
local vrRotateInput = Vector2.new(self.rotateInput.x, 0) | |
local startCFrame = CFrame.new(ZERO_VECTOR3, currLookVector) | |
local yawRotatedVector = (CFrame.Angles(0, -vrRotateInput.x, 0) * startCFrame * CFrame.Angles(-vrRotateInput.y,0,0)).lookVector | |
return (yawRotatedVector * X1_Y0_Z1).unit | |
end | |
function BaseCamera:GetHumanoid() | |
local character = player and player.Character | |
if character then | |
local resultHumanoid = self.humanoidCache[player] | |
if resultHumanoid and resultHumanoid.Parent == character then | |
return resultHumanoid | |
else | |
self.humanoidCache[player] = nil -- Bust Old Cache | |
local humanoid = character:FindFirstChildOfClass("Humanoid") | |
if humanoid then | |
self.humanoidCache[player] = humanoid | |
end | |
return humanoid | |
end | |
end | |
return nil | |
end | |
function BaseCamera:GetHumanoidPartToFollow(humanoid, humanoidStateType) | |
if humanoidStateType == Enum.HumanoidStateType.Dead then | |
local character = humanoid.Parent | |
if character then | |
return character:FindFirstChild("Head") or humanoid.Torso | |
else | |
return humanoid.Torso | |
end | |
else | |
return humanoid.Torso | |
end | |
end | |
function BaseCamera:UpdateGamepad() | |
local gamepadPan = self.gamepadPanningCamera | |
if gamepadPan and (self.hasGameLoaded or not VRService.VREnabled) then | |
gamepadPan = Util.GamepadLinearToCurve(gamepadPan) | |
local currentTime = tick() | |
if gamepadPan.X ~= 0 or gamepadPan.Y ~= 0 then | |
self.userPanningTheCamera = true | |
elseif gamepadPan == ZERO_VECTOR2 then | |
self.lastThumbstickRotate = nil | |
if self.lastThumbstickPos == ZERO_VECTOR2 then | |
self.currentSpeed = 0 | |
end | |
end | |
local finalConstant = 0 | |
if self.lastThumbstickRotate then | |
if VRService.VREnabled then | |
self.currentSpeed = self.vrMaxSpeed | |
else | |
local elapsedTime = (currentTime - self.lastThumbstickRotate) * 10 | |
self.currentSpeed = self.currentSpeed + (self.maxSpeed * ((elapsedTime*elapsedTime)/self.numOfSeconds)) | |
if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end | |
if self.lastVelocity then | |
local velocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate) | |
local velocityDeltaMag = (velocity - self.lastVelocity).magnitude | |
if velocityDeltaMag > 12 then | |
self.currentSpeed = self.currentSpeed * (20/velocityDeltaMag) | |
if self.currentSpeed > self.maxSpeed then self.currentSpeed = self.maxSpeed end | |
end | |
end | |
end | |
finalConstant = UserGameSettings.GamepadCameraSensitivity * self.currentSpeed | |
self.lastVelocity = (gamepadPan - self.lastThumbstickPos)/(currentTime - self.lastThumbstickRotate) | |
end | |
self.lastThumbstickPos = gamepadPan | |
self.lastThumbstickRotate = currentTime | |
return Vector2.new( gamepadPan.X * finalConstant, gamepadPan.Y * finalConstant * self.ySensitivity * UserGameSettings:GetCameraYInvertValue()) | |
end | |
return ZERO_VECTOR2 | |
end | |
-- [[ VR Support Section ]] -- | |
function BaseCamera:ApplyVRTransform() | |
if not VRService.VREnabled then | |
return | |
end | |
--we only want this to happen in first person VR | |
local rootJoint = self.humanoidRootPart and self.humanoidRootPart:FindFirstChild("RootJoint") | |
if not rootJoint then | |
return | |
end | |
local cameraSubject = game.Workspace.CurrentCamera.CameraSubject | |
local isInVehicle = cameraSubject and cameraSubject:IsA("VehicleSeat") | |
if self.inFirstPerson and not isInVehicle then | |
local vrFrame = VRService:GetUserCFrame(Enum.UserCFrame.Head) | |
local vrRotation = vrFrame - vrFrame.p | |
rootJoint.C0 = CFrame.new(vrRotation:vectorToObjectSpace(vrFrame.p)) * CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) | |
else | |
rootJoint.C0 = CFrame.new(0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 1, 0) | |
end | |
end | |
function BaseCamera:IsInFirstPerson() | |
return self.inFirstPerson | |
end | |
function BaseCamera:ShouldUseVRRotation() | |
if not VRService.VREnabled then | |
return false | |
end | |
if not self.VRRotationIntensityAvailable and tick() - self.lastVRRotationIntensityCheckTime < 1 then | |
return false | |
end | |
local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) | |
self.VRRotationIntensityAvailable = success and vrRotationIntensity ~= nil | |
self.lastVRRotationIntensityCheckTime = tick() | |
self.shouldUseVRRotation = success and vrRotationIntensity ~= nil and vrRotationIntensity ~= "Smooth" | |
return self.shouldUseVRRotation | |
end | |
function BaseCamera:GetVRRotationInput() | |
local vrRotateSum = ZERO_VECTOR2 | |
local success, vrRotationIntensity = pcall(function() return StarterGui:GetCore("VRRotationIntensity") end) | |
if not success then | |
return | |
end | |
local vrGamepadRotation = self.GamepadPanningCamera or ZERO_VECTOR2 | |
local delayExpired = (tick() - self.lastVRRotationTime) >= self:GetRepeatDelayValue(vrRotationIntensity) | |
if math.abs(vrGamepadRotation.x) >= self:GetActivateValue() then | |
if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2]) then | |
local sign = 1 | |
if vrGamepadRotation.x < 0 then | |
sign = -1 | |
end | |
vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) * sign | |
self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = true | |
end | |
elseif math.abs(vrGamepadRotation.x) < self:GetActivateValue() - 0.1 then | |
self.vrRotateKeyCooldown[Enum.KeyCode.Thumbstick2] = nil | |
end | |
if self.turningLeft then | |
if delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Left] then | |
vrRotateSum = vrRotateSum - self:GetRotateAmountValue(vrRotationIntensity) | |
self.vrRotateKeyCooldown[Enum.KeyCode.Left] = true | |
end | |
else | |
self.vrRotateKeyCooldown[Enum.KeyCode.Left] = nil | |
end | |
if self.turningRight then | |
if (delayExpired or not self.vrRotateKeyCooldown[Enum.KeyCode.Right]) then | |
vrRotateSum = vrRotateSum + self:GetRotateAmountValue(vrRotationIntensity) | |
self.vrRotateKeyCooldown[Enum.KeyCode.Right] = true | |
end | |
else | |
self.vrRotateKeyCooldown[Enum.KeyCode.Right] = nil | |
end | |
if vrRotateSum ~= ZERO_VECTOR2 then | |
self.lastVRRotationTime = tick() | |
end | |
return vrRotateSum | |
end | |
function BaseCamera:CancelCameraFreeze(keepConstraints) | |
if not keepConstraints then | |
self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 1, self.cameraTranslationConstraints.z) | |
end | |
if self.cameraFrozen then | |
self.trackingHumanoid = nil | |
self.cameraFrozen = false | |
end | |
end | |
function BaseCamera:StartCameraFreeze(subjectPosition, humanoidToTrack) | |
if not self.cameraFrozen then | |
self.humanoidJumpOrigin = subjectPosition | |
self.trackingHumanoid = humanoidToTrack | |
self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, 0, self.cameraTranslationConstraints.z) | |
self.cameraFrozen = true | |
end | |
end | |
function BaseCamera:OnNewCameraSubject() | |
if self.subjectStateChangedConn then | |
self.subjectStateChangedConn:Disconnect() | |
self.subjectStateChangedConn = nil | |
end | |
local humanoid = workspace.CurrentCamera and workspace.CurrentCamera.CameraSubject | |
if self.trackingHumanoid ~= humanoid then | |
self:CancelCameraFreeze() | |
end | |
if humanoid and humanoid:IsA("Humanoid") then | |
self.subjectStateChangedConn = humanoid.StateChanged:Connect(function(oldState, newState) | |
if VRService.VREnabled and newState == Enum.HumanoidStateType.Jumping and not self.inFirstPerson then | |
self:StartCameraFreeze(self:GetSubjectPosition(), humanoid) | |
elseif newState ~= Enum.HumanoidStateType.Jumping and newState ~= Enum.HumanoidStateType.Freefall then | |
self:CancelCameraFreeze(true) | |
end | |
end) | |
end | |
end | |
function BaseCamera:GetVRFocus(subjectPosition, timeDelta) | |
local lastFocus = self.LastCameraFocus or subjectPosition | |
if not self.cameraFrozen then | |
self.cameraTranslationConstraints = Vector3.new(self.cameraTranslationConstraints.x, math.min(1, self.cameraTranslationConstraints.y + 0.42 * timeDelta), self.cameraTranslationConstraints.z) | |
end | |
local newFocus | |
if self.cameraFrozen and self.humanoidJumpOrigin and self.humanoidJumpOrigin.y > lastFocus.y then | |
newFocus = CFrame.new(Vector3.new(subjectPosition.x, math.min(self.humanoidJumpOrigin.y, lastFocus.y + 5 * timeDelta), subjectPosition.z)) | |
else | |
newFocus = CFrame.new(Vector3.new(subjectPosition.x, lastFocus.y, subjectPosition.z):lerp(subjectPosition, self.cameraTranslationConstraints.y)) | |
end | |
if self.cameraFrozen then | |
-- No longer in 3rd person | |
if self.inFirstPerson then -- not VRService.VREnabled | |
self:CancelCameraFreeze() | |
end | |
-- This case you jumped off a cliff and want to keep your character in view | |
-- 0.5 is to fix floating point error when not jumping off cliffs | |
if self.humanoidJumpOrigin and subjectPosition.y < (self.humanoidJumpOrigin.y - 0.5) then | |
self:CancelCameraFreeze() | |
end | |
end | |
return newFocus | |
end | |
function BaseCamera:GetRotateAmountValue(vrRotationIntensity) | |
vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") | |
if vrRotationIntensity then | |
if vrRotationIntensity == "Low" then | |
return VR_LOW_INTENSITY_ROTATION | |
elseif vrRotationIntensity == "High" then | |
return VR_HIGH_INTENSITY_ROTATION | |
end | |
end | |
return ZERO_VECTOR2 | |
end | |
function BaseCamera:GetRepeatDelayValue(vrRotationIntensity) | |
vrRotationIntensity = vrRotationIntensity or StarterGui:GetCore("VRRotationIntensity") | |
if vrRotationIntensity then | |
if vrRotationIntensity == "Low" then | |
return VR_LOW_INTENSITY_REPEAT | |
elseif vrRotationIntensity == "High" then | |
return VR_HIGH_INTENSITY_REPEAT | |
end | |
end | |
return 0 | |
end | |
function BaseCamera:Update(dt) | |
error("BaseCamera:Update() This is a virtual function that should never be getting called.", 2) | |
end | |
BaseCamera.UpCFrame = CFrame.new() | |
function BaseCamera:UpdateUpCFrame(cf) | |
self.UpCFrame = cf | |
end | |
local ZERO = Vector3.new(0, 0, 0) | |
function BaseCamera:CalculateNewLookCFrame(suppliedLookVector) | |
local currLookVector = suppliedLookVector or self:GetCameraLookVector() | |
currLookVector = self.UpCFrame:VectorToObjectSpace(currLookVector) | |
local currPitchAngle = math.asin(currLookVector.y) | |
local yTheta = math.clamp(self.rotateInput.y, -MAX_Y + currPitchAngle, -MIN_Y + currPitchAngle) | |
local constrainedRotateInput = Vector2.new(self.rotateInput.x, yTheta) | |
local startCFrame = CFrame.new(ZERO, currLookVector) | |
local newLookCFrame = CFrame.Angles(0, -constrainedRotateInput.x, 0) * startCFrame * CFrame.Angles(-constrainedRotateInput.y,0,0) | |
return newLookCFrame | |
end | |
return BaseCamera | |
end | |
function _BaseOcclusion() | |
--[[ 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 BaseOcclusion: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 | |
end | |
function _Popper() | |
local Players = game:GetService("Players") | |
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 getTotalTransparency(part) | |
return 1 - (1 - part.Transparency)*(1 - part.LocalTransparencyModifier) | |
end | |
local function eraseFromEnd(t, toSize) | |
for i = #t, toSize + 1, -1 do | |
t[i] = nil | |
end | |
end | |
local nearPlaneZ, 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() | |
nearPlaneZ = camera.NearPlaneZ | |
camera:GetPropertyChangedSignal("NearPlaneZ"):Connect(function() | |
nearPlaneZ = camera.NearPlaneZ | |
end) | |
end | |
local blacklist = {} do | |
local charMap = {} | |
local function refreshIgnoreList() | |
local n = 1 | |
blacklist = {} | |
for _, 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 | |
Players.PlayerAdded:Connect(playerAdded) | |
Players.PlayerRemoving:Connect(playerRemoving) | |
for _, player in ipairs(Players:GetPlayers()) do | |
playerAdded(player) | |
end | |
refreshIgnoreList() | |
end | |
-------------------------------------------------------------------------------------------- | |
-- Popper uses the level geometry find an upper bound on subject-to-camera distance. | |
-- | |
-- Hard limits are applied immediately and unconditionally. They are generally caused | |
-- when level geometry intersects with the near plane (with exceptions, see below). | |
-- | |
-- Soft limits are only applied under certain conditions. | |
-- They are 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 subjectRoot | |
local subjectPart | |
camera:GetPropertyChangedSignal("CameraSubject"):Connect(function() | |
local subject = camera.CameraSubject | |
if subject:IsA("Humanoid") then | |
subjectPart = subject.RootPart | |
elseif subject:IsA("BasePart") then | |
subjectPart = subject | |
else | |
subjectPart = nil | |
end | |
end) | |
local function canOcclude(part) | |
-- Occluders must be: | |
-- 1. Opaque | |
-- 2. Interactable | |
-- 3. Not in the same assembly as the subject | |
return | |
getTotalTransparency(part) < 0.25 and | |
part.CanCollide and | |
subjectRoot ~= (part:GetRootPart() or 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) | |
local originalSize = #blacklist | |
repeat | |
local hitPart, hitPoint = workspace:FindPartOnRayWithIgnoreList( | |
ray(origin, dir), blacklist, false, true | |
) | |
if hitPart then | |
if hitPart.CanCollide 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 + nearPlaneZ | |
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, true) | |
local lim = (entryPos - origin).Magnitude | |
if exitPart then | |
local promote = false | |
if lastPos then | |
promote = | |
workspace:FindPartOnRayWithWhitelist(ray(lastPos, target - lastPos), wl, true) or | |
workspace:FindPartOnRayWithWhitelist(ray(target, lastPos - target), wl, 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 - unitDir*1e-3 | |
end | |
until hardLimit < inf or not entryPart | |
eraseFromEnd(blacklist, originalSize) | |
debug.profileend() | |
return softLimit - nearPlaneZ, hardLimit - nearPlaneZ | |
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 + nearPlaneZ*(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 testPromotion(focus, dist, focusExtrapolation) | |
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 maxDist = (getCollisionPoint(fP, focusExtrapolation.posVelocity*SAMPLE_MAX_T) - fP).Magnitude | |
-- Metric that decides how many samples to take | |
local combinedSpeed = focusExtrapolation.posVelocity.magnitude | |
for dt = 0, min(SAMPLE_MAX_T, focusExtrapolation.rotVelocity.magnitude + maxDist/combinedSpeed), SAMPLE_DT do | |
local cfDt = focusExtrapolation.extrapolate(dt) -- Extrapolated CFrame at time dt | |
if queryPoint(cfDt.p, -cfDt.lookVector, 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") | |
for _, offset in ipairs(SCAN_SAMPLE_OFFSETS) do | |
local scaledOffset = offset | |
local pos = getCollisionPoint(fP, fX*scaledOffset.x + fY*scaledOffset.y) | |
if queryPoint(pos, (fP + fZ*dist - pos).Unit, dist) == inf then | |
return false | |
end | |
end | |
debug.profileend() | |
end | |
debug.profileend() | |
return true | |
end | |
local function Popper(focus, targetDist, focusExtrapolation) | |
debug.profilebegin("popper") | |
subjectRoot = subjectPart and subjectPart:GetRootPart() or subjectPart | |
local dist = targetDist | |
local soft, hard = queryViewport(focus, targetDist) | |
if hard < dist then | |
dist = hard | |
end | |
if soft < dist and testPromotion(focus, targetDist, focusExtrapolation) then | |
dist = soft | |
end | |
subjectRoot = nil | |
debug.profileend() | |
return dist | |
end | |
return Popper | |
end | |
function _ZoomController() | |
local ZOOM_STIFFNESS = 4.5 | |
local ZOOM_DEFAULT = 12.5 | |
local ZOOM_ACCELERATION = 0.0375 | |
local MIN_FOCUS_DIST = 0.5 | |
local DIST_OPAQUE = 1 | |
local Popper = _Popper() | |
local clamp = math.clamp | |
local exp = math.exp | |
local min = math.min | |
local max = math.max | |
local pi = math.pi | |
local cameraMinZoomDistance, cameraMaxZoomDistance do | |
local Player = game:GetService("Players").LocalPlayer | |
local function updateBounds() | |
cameraMinZoomDistance = Player.CameraMinZoomDistance | |
cameraMaxZoomDistance = Player.CameraMaxZoomDistance | |
end | |
updateBounds() | |
Player:GetPropertyChangedSignal("CameraMinZoomDistance"):Connect(updateBounds) | |
Player:GetPropertyChangedSignal("CameraMaxZoomDistance"):Connect(updateBounds) | |
end | |
local ConstrainedSpring = {} do | |
ConstrainedSpring.__index = ConstrainedSpring | |
function ConstrainedSpring.new(freq, x, minValue, maxValue) | |
x = clamp(x, minValue, maxValue) | |
return setmetatable({ | |
freq = freq, -- Undamped frequency (Hz) | |
x = x, -- Current position | |
v = 0, -- Current velocity | |
minValue = minValue, -- Minimum bound | |
maxValue = maxValue, -- Maximum bound | |
goal = x, -- Goal position | |
}, ConstrainedSpring) | |
end | |
function ConstrainedSpring:Step(dt) | |
local freq = self.freq*2*pi -- Convert from Hz to rad/s | |
local x = self.x | |
local v = self.v | |
local minValue = self.minValue | |
local maxValue = self.maxValue | |
local goal = self.goal | |
-- Solve the spring ODE for position and velocity after time t, assuming critical damping: | |
-- 2*f*x'[t] + x''[t] = f^2*(g - x[t]) | |
-- Knowns are x[0] and x'[0]. | |
-- Solve for x[t] and x'[t]. | |
local offset = goal - x | |
local step = freq*dt | |
local decay = exp(-step) | |
local x1 = goal + (v*dt - offset*(step + 1))*decay | |
local v1 = ((offset*freq - v)*step + v)*decay | |
-- Constrain | |
if x1 < minValue then | |
x1 = minValue | |
v1 = 0 | |
elseif x1 > maxValue then | |
x1 = maxValue | |
v1 = 0 | |
end | |
self.x = x1 | |
self.v = v1 | |
return x1 | |
end | |
end | |
local zoomSpring = ConstrainedSpring.new(ZOOM_STIFFNESS, ZOOM_DEFAULT, MIN_FOCUS_DIST, cameraMaxZoomDistance) | |
local function stepTargetZoom(z, dz, zoomMin, zoomMax) | |
z = clamp(z + dz*(1 + z*ZOOM_ACCELERATION), zoomMin, zoomMax) | |
if z < DIST_OPAQUE then | |
z = dz <= 0 and zoomMin or DIST_OPAQUE | |
end | |
return z | |
end | |
local zoomDelta = 0 | |
local Zoom = {} do | |
function Zoom.Update(renderDt, focus, extrapolation) | |
local poppedZoom = math.huge | |
if zoomSpring.goal > DIST_OPAQUE then | |
-- Make a pessimistic estimate of zoom distance for this step without accounting for poppercam | |
local maxPossibleZoom = max( | |
zoomSpring.x, | |
stepTargetZoom(zoomSpring.goal, zoomDelta, cameraMinZoomDistance, cameraMaxZoomDistance) | |
) | |
-- Run the Popper algorithm on the feasible zoom range, [MIN_FOCUS_DIST, maxPossibleZoom] | |
poppedZoom = Popper( | |
focus*CFrame.new(0, 0, MIN_FOCUS_DIST), | |
maxPossibleZoom - MIN_FOCUS_DIST, | |
extrapolation | |
) + MIN_FOCUS_DIST | |
end | |
zoomSpring.minValue = MIN_FOCUS_DIST | |
zoomSpring.maxValue = min(cameraMaxZoomDistance, poppedZoom) | |
return zoomSpring:Step(renderDt) | |
end | |
function Zoom.SetZoomParameters(targetZoom, newZoomDelta) | |
zoomSpring.goal = targetZoom | |
zoomDelta = newZoomDelta | |
end | |
end | |
return Zoom | |
end | |
function _MouseLockController() | |
--[[ Constants ]]-- | |
local DEFAULT_MOUSE_LOCK_CURSOR = "rbxasset://textures/MouseLockedCursor.png" | |
local CONTEXT_ACTION_NAME = "MouseLockSwitchAction" | |
local MOUSELOCK_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value | |
--[[ Services ]]-- | |
local PlayersService = game:GetService("Players") | |
local ContextActionService = game:GetService("ContextActionService") | |
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.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 _, keyEnum in pairs(Enum.KeyCode:GetEnumItems()) do | |
if token == keyEnum.Name then | |
self.boundKeys[#self.boundKeys+1] = keyEnum | |
break | |
end | |
end | |
end | |
self:UnbindContextActions() | |
self:BindContextActions() | |
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:DoMouseLockSwitch(name, state, input) | |
if state == Enum.UserInputState.Begin then | |
self:OnMouseLockToggled() | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
function MouseLockController:BindContextActions() | |
ContextActionService:BindActionAtPriority(CONTEXT_ACTION_NAME, function(name, state, input) | |
return self:DoMouseLockSwitch(name, state, input) | |
end, false, MOUSELOCK_ACTION_PRIORITY, unpack(self.boundKeys)) | |
end | |
function MouseLockController:UnbindContextActions() | |
ContextActionService:UnbindAction(CONTEXT_ACTION_NAME) | |
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 | |
self:BindContextActions() | |
else | |
-- Disabling | |
-- Restore mouse cursor | |
if Mouse.Icon~="" then | |
Mouse.Icon = "" | |
end | |
self:UnbindContextActions() | |
-- 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 | |
end | |
function _TransparencyController() | |
local MAX_TWEEN_RATE = 2.8 -- per second | |
local Util = _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 = math.clamp(deltaTransparency, -maxDelta, maxDelta) | |
end | |
transparency = self.lastTransparency + deltaTransparency | |
else | |
self.transparencyDirty = true | |
end | |
transparency = math.clamp(Util.Round(transparency, 2), 0, 1) | |
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 | |
end | |
function _Poppercam() | |
local ZoomController = _ZoomController() | |
local TransformExtrapolator = {} do | |
TransformExtrapolator.__index = TransformExtrapolator | |
local CF_IDENTITY = CFrame.new() | |
local function cframeToAxis(cframe) | |
local axis, angle = cframe:toAxisAngle() | |
return axis*angle | |
end | |
local function axisToCFrame(axis) | |
local angle = axis.magnitude | |
if angle > 1e-5 then | |
return CFrame.fromAxisAngle(axis, angle) | |
end | |
return CF_IDENTITY | |
end | |
local function extractRotation(cf) | |
local _, _, _, xx, yx, zx, xy, yy, zy, xz, yz, zz = cf:components() | |
return CFrame.new(0, 0, 0, xx, yx, zx, xy, yy, zy, xz, yz, zz) | |
end | |
function TransformExtrapolator.new() | |
return setmetatable({ | |
lastCFrame = nil, | |
}, TransformExtrapolator) | |
end | |
function TransformExtrapolator:Step(dt, currentCFrame) | |
local lastCFrame = self.lastCFrame or currentCFrame | |
self.lastCFrame = currentCFrame | |
local currentPos = currentCFrame.p | |
local currentRot = extractRotation(currentCFrame) | |
local lastPos = lastCFrame.p | |
local lastRot = extractRotation(lastCFrame) | |
-- Estimate velocities from the delta between now and the last frame | |
-- This estimation can be a little noisy. | |
local dp = (currentPos - lastPos)/dt | |
local dr = cframeToAxis(currentRot*lastRot:inverse())/dt | |
local function extrapolate(t) | |
local p = dp*t + currentPos | |
local r = axisToCFrame(dr*t)*currentRot | |
return r + p | |
end | |
return { | |
extrapolate = extrapolate, | |
posVelocity = dp, | |
rotVelocity = dr, | |
} | |
end | |
function TransformExtrapolator:Reset() | |
self.lastCFrame = nil | |
end | |
end | |
--[[ The Module ]]-- | |
local BaseOcclusion = _BaseOcclusion() | |
local Poppercam = setmetatable({}, BaseOcclusion) | |
Poppercam.__index = Poppercam | |
function Poppercam.new() | |
local self = setmetatable(BaseOcclusion.new(), Poppercam) | |
self.focusExtrapolator = TransformExtrapolator.new() | |
return self | |
end | |
function Poppercam:GetOcclusionMode() | |
return Enum.DevCameraOcclusionMode.Zoom | |
end | |
function Poppercam:Enable(enable) | |
self.focusExtrapolator:Reset() | |
end | |
function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController) | |
local rotatedFocus = CFrame.new(desiredCameraFocus.p, desiredCameraCFrame.p)*CFrame.new( | |
0, 0, 0, | |
-1, 0, 0, | |
0, 1, 0, | |
0, 0, -1 | |
) | |
local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus) | |
local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation) | |
return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus | |
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 | |
local ZoomController = _ZoomController() | |
function Poppercam:Update(renderDt, desiredCameraCFrame, desiredCameraFocus, cameraController) | |
local rotatedFocus = desiredCameraFocus * (desiredCameraCFrame - desiredCameraCFrame.p) | |
local extrapolation = self.focusExtrapolator:Step(renderDt, rotatedFocus) | |
local zoom = ZoomController.Update(renderDt, rotatedFocus, extrapolation) | |
return rotatedFocus*CFrame.new(0, 0, zoom), desiredCameraFocus | |
end | |
return Poppercam | |
end | |
function _Invisicam() | |
--[[ Top Level Roblox Services ]]-- | |
local PlayersService = game:GetService("Players") | |
--[[ 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 = _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 | |
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 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 _, 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 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 | |
end | |
function _LegacyCamera() | |
local ZERO_VECTOR2 = Vector2.new(0,0) | |
local Util = _CameraUtils() | |
--[[ Services ]]-- | |
local PlayersService = game:GetService('Players') | |
--[[ The Module ]]-- | |
local BaseCamera = _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 | |
--[[ 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 | |
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 | |
end | |
function _OrbitalCamera() | |
-- Local private variables and constants | |
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 | |
--[[ 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 = _CameraUtils() | |
--[[ Services ]]-- | |
local PlayersService = game:GetService('Players') | |
local VRService = game:GetService("VRService") | |
--[[ The Module ]]-- | |
local BaseCamera = _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 = math.clamp(desiredSubjectDistance, self.minDistance, self.maxDistance) | |
-- 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 = math.clamp(xyRotateVector.y, currPitchAngle - math.rad(MAX_ALLOWED_ELEVATION_DEG), currPitchAngle - math.rad(MIN_ALLOWED_ELEVATION_DEG)) | |
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 | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
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 | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
function OrbitalCamera:BindGamepadInputActions() | |
self:BindAction("OrbitalCamGamepadPan", function(name, state, input) return self:GetGamepadPan(name, state, input) end, | |
false, Enum.KeyCode.Thumbstick2) | |
self:BindAction("OrbitalCamGamepadZoom", function(name, state, input) return self:DoGamepadZoom(name, state, input) end, | |
false, Enum.KeyCode.ButtonR3, 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 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 | |
self.lastUserPanCamera = tick() | |
end | |
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 = math.clamp(self.curAzimuthRad, self.minAzimuthAbsoluteRad, self.maxAzimuthAbsoluteRad) | |
else | |
self.curAzimuthRad = (self.curAzimuthRad ~= 0) and (math.sign(self.curAzimuthRad) * (math.abs(self.curAzimuthRad) % TAU)) or 0 | |
end | |
self.curElevationRad = math.clamp(self.curElevationRad + self.rotateInput.y, self.minElevationRad, self.maxElevationRad) | |
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 | |
end | |
function _ClassicCamera() | |
-- 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 INITIAL_CAMERA_ANGLE = CFrame.fromOrientation(math.rad(-15), 0, 0) | |
local FFlagUserCameraToggle do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserCameraToggle") | |
end) | |
FFlagUserCameraToggle = success and result | |
end | |
--[[ Services ]]-- | |
local PlayersService = game:GetService('Players') | |
local VRService = game:GetService("VRService") | |
local CameraInput = _CameraInput() | |
local Util = _CameraUtils() | |
--[[ The Module ]]-- | |
local BaseCamera = _BaseCamera() | |
local ClassicCamera = setmetatable({}, BaseCamera) | |
ClassicCamera.__index = ClassicCamera | |
function ClassicCamera.new() | |
local self = setmetatable(BaseCamera.new(), ClassicCamera) | |
self.isFollowCamera = false | |
self.isCameraToggle = false | |
self.lastUpdate = tick() | |
self.cameraToggleSpring = Util.Spring.new(5, 0) | |
return self | |
end | |
function ClassicCamera:GetCameraToggleOffset(dt) | |
assert(FFlagUserCameraToggle) | |
if self.isCameraToggle then | |
local zoom = self.currentSubjectDistance | |
if CameraInput.getTogglePan() then | |
self.cameraToggleSpring.goal = math.clamp(Util.map(zoom, 0.5, self.FIRST_PERSON_DISTANCE_THRESHOLD, 0, 1), 0, 1) | |
else | |
self.cameraToggleSpring.goal = 0 | |
end | |
local distanceOffset = math.clamp(Util.map(zoom, 0.5, 64, 0, 1), 0, 1) + 1 | |
return Vector3.new(0, self.cameraToggleSpring:step(dt)*distanceOffset, 0) | |
end | |
return Vector3.new() | |
end | |
-- Movement mode standardized to Enum.ComputerCameraMovementMode values | |
function ClassicCamera:SetCameraMovementMode(cameraMovementMode) | |
BaseCamera.SetCameraMovementMode(self, cameraMovementMode) | |
self.isFollowCamera = cameraMovementMode == Enum.ComputerCameraMovementMode.Follow | |
self.isCameraToggle = cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle | |
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 overrideCameraLookVector = nil | |
if self.resetCameraAngle then | |
local rootPart = self:GetHumanoidRootPart() | |
if rootPart then | |
overrideCameraLookVector = (rootPart.CFrame * INITIAL_CAMERA_ANGLE).lookVector | |
else | |
overrideCameraLookVector = INITIAL_CAMERA_ANGLE.lookVector | |
end | |
self.resetCameraAngle = false | |
end | |
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 | |
local cameraHeight = self:GetCameraHeight() | |
-- 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(overrideCameraLookVector) | |
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 = math.clamp(tweenSpeed + tweenAcceleration * timeDelta, 0, tweenMaxSpeed) | |
local percent = math.clamp(tweenSpeed * timeDelta, 0, 1) | |
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 | |
if VREnabled then | |
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta) | |
else | |
newCameraFocus = CFrame.new(subjectPosition) | |
end | |
local cameraFocusP = newCameraFocus.p | |
if VREnabled and not self:IsInFirstPerson() then | |
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(overrideCameraLookVector) | |
self.rotateInput = ZERO_VECTOR2 | |
newCameraCFrame = CFrame.new(cameraFocusP - (zoom * newLookVector), cameraFocusP) | |
end | |
else -- is FollowCamera | |
local newLookVector = self:CalculateNewLookVector(overrideCameraLookVector) | |
self.rotateInput = ZERO_VECTOR2 | |
if VRService.VREnabled then | |
newCameraFocus = self:GetVRFocus(subjectPosition, timeDelta) | |
else | |
newCameraFocus = CFrame.new(subjectPosition) | |
end | |
newCameraCFrame = CFrame.new(newCameraFocus.p - (zoom * newLookVector), newCameraFocus.p) + Vector3.new(0, cameraHeight, 0) | |
end | |
if FFlagUserCameraToggle then | |
local toggleOffset = self:GetCameraToggleOffset(timeDelta) | |
newCameraFocus = newCameraFocus + toggleOffset | |
newCameraCFrame = newCameraCFrame + toggleOffset | |
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 | |
end | |
function _CameraUtils() | |
local CameraUtils = {} | |
local FFlagUserCameraToggle do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserCameraToggle") | |
end) | |
FFlagUserCameraToggle = success and result | |
end | |
local function round(num) | |
return math.floor(num + 0.5) | |
end | |
-- Critically damped spring class for fluid motion effects | |
local Spring = {} do | |
Spring.__index = Spring | |
-- Initialize to a given undamped frequency and default position | |
function Spring.new(freq, pos) | |
return setmetatable({ | |
freq = freq, | |
goal = pos, | |
pos = pos, | |
vel = 0, | |
}, Spring) | |
end | |
-- Advance the spring simulation by `dt` seconds | |
function Spring:step(dt) | |
local f = self.freq*2*math.pi | |
local g = self.goal | |
local p0 = self.pos | |
local v0 = self.vel | |
local offset = p0 - g | |
local decay = math.exp(-f*dt) | |
local p1 = (offset*(1 + f*dt) + v0*dt)*decay + g | |
local v1 = (v0*(1 - f*dt) - offset*(f*f*dt))*decay | |
self.pos = p1 | |
self.vel = v1 | |
return p1 | |
end | |
end | |
CameraUtils.Spring = Spring | |
-- map a value from one range to another | |
function CameraUtils.map(x, inMin, inMax, outMin, outMax) | |
return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin | |
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 = math.clamp(t, -1, 1) | |
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 math.clamp(point, -1, 1) | |
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 | |
if FFlagUserCameraToggle then | |
if enumValue == Enum.ComputerCameraMovementMode.CameraToggle or | |
enumValue == Enum.DevComputerCameraMovementMode.CameraToggle then | |
return Enum.ComputerCameraMovementMode.CameraToggle | |
end | |
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 | |
end | |
function _CameraModule() | |
local CameraModule = {} | |
CameraModule.__index = CameraModule | |
local FFlagUserCameraToggle do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserCameraToggle") | |
end) | |
FFlagUserCameraToggle = success and result | |
end | |
local FFlagUserRemoveTheCameraApi do | |
local success, result = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserRemoveTheCameraApi") | |
end) | |
FFlagUserRemoveTheCameraApi = success and result | |
end | |
-- 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 UserGameSettings = UserSettings():GetService("UserGameSettings") | |
-- Camera math utility library | |
local CameraUtils = _CameraUtils() | |
-- Load Roblox Camera Controller Modules | |
local ClassicCamera = _ClassicCamera() | |
local OrbitalCamera = _OrbitalCamera() | |
local LegacyCamera = _LegacyCamera() | |
-- Load Roblox Occlusion Modules | |
local Invisicam = _Invisicam() | |
local Poppercam = _Poppercam() | |
-- Load the near-field character transparency controller and the mouse lock "shift lock" controller | |
local TransparencyController = _TransparencyController() | |
local MouseLockController = _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") | |
PlayerScripts:RegisterTouchCameraMovementMode(Enum.TouchCameraMovementMode.Default) | |
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) | |
if FFlagUserCameraToggle then | |
PlayerScripts:RegisterComputerCameraMovementMode(Enum.ComputerCameraMovementMode.CameraToggle) | |
end | |
end | |
CameraModule.FFlagUserCameraToggle = FFlagUserCameraToggle | |
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 | |
-- Adds CharacterAdded and CharacterRemoving event handlers for all current players | |
for _,player in pairs(Players:GetPlayers()) do | |
self:OnPlayerAdded(player) | |
end | |
-- Adds CharacterAdded and CharacterRemoving event handlers for all players who join in the future | |
Players.PlayerAdded:Connect(function(player) | |
self:OnPlayerAdded(player) | |
end) | |
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 = UserInputService:GetLastInputType() | |
UserInputService.LastInputTypeChanged:Connect(function(newLastInputType) | |
self.lastInputType = newLastInputType | |
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 | |
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 | |
-- When Poppercam is enabled, we send it all existing player characters for its 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 or | |
(FFlagUserCameraToggle and cameraMovementMode == Enum.ComputerCameraMovementMode.CameraToggle) 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 | |
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) | |
if self.activeCameraController then | |
if FFlagUserCameraToggle then | |
self.activeCameraController:UpdateMouseBehavior() | |
end | |
local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt) | |
self.activeCameraController:ApplyVRTransform() | |
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 | |
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.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 | |
--begin edit | |
local Camera = CameraModule | |
local IDENTITYCF = CFrame.new() | |
local lastUpCFrame = IDENTITYCF | |
Camera.UpVector = Vector3.new(0, 1, 0) | |
Camera.TransitionRate = 0.15 | |
Camera.UpCFrame = IDENTITYCF | |
function Camera:GetUpVector(oldUpVector) | |
return oldUpVector | |
end | |
local function getRotationBetween(u, v, axis) | |
local dot, uxv = u:Dot(v), u:Cross(v) | |
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end | |
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot) | |
end | |
function Camera:CalculateUpCFrame() | |
local oldUpVector = self.UpVector | |
local newUpVector = self:GetUpVector(oldUpVector) | |
local backup = game.Workspace.CurrentCamera.CFrame.RightVector | |
local transitionCF = getRotationBetween(oldUpVector, newUpVector, backup) | |
local vecSlerpCF = IDENTITYCF:Lerp(transitionCF, self.TransitionRate) | |
self.UpVector = vecSlerpCF * oldUpVector | |
self.UpCFrame = vecSlerpCF * self.UpCFrame | |
lastUpCFrame = self.UpCFrame | |
end | |
function Camera:Update(dt) | |
if self.activeCameraController then | |
if Camera.FFlagUserCameraToggle then | |
self.activeCameraController:UpdateMouseBehavior() | |
end | |
local newCameraCFrame, newCameraFocus = self.activeCameraController:Update(dt) | |
self.activeCameraController:ApplyVRTransform() | |
self:CalculateUpCFrame() | |
self.activeCameraController:UpdateUpCFrame(self.UpCFrame) | |
-- undo shift-lock offset | |
local lockOffset = Vector3.new(0, 0, 0) | |
if (self.activeMouseLockController and self.activeMouseLockController:GetIsMouseLocked()) then | |
lockOffset = self.activeMouseLockController:GetMouseLockOffset() | |
end | |
local offset = newCameraFocus:ToObjectSpace(newCameraCFrame) | |
local camRotation = self.UpCFrame * offset | |
newCameraFocus = newCameraFocus - newCameraCFrame:VectorToWorldSpace(lockOffset) + camRotation:VectorToWorldSpace(lockOffset) | |
newCameraCFrame = newCameraFocus * camRotation | |
--local offset = newCameraFocus:Inverse() * newCameraCFrame | |
--newCameraCFrame = newCameraFocus * self.UpCFrame * offset | |
if (self.activeCameraController.lastCameraTransform) then | |
self.activeCameraController.lastCameraTransform = newCameraCFrame | |
self.activeCameraController.lastCameraFocus = newCameraFocus | |
end | |
if self.activeOcclusionModule then | |
newCameraCFrame, newCameraFocus = self.activeOcclusionModule:Update(dt, newCameraCFrame, newCameraFocus) | |
end | |
game.Workspace.CurrentCamera.CFrame = newCameraCFrame | |
game.Workspace.CurrentCamera.Focus = newCameraFocus | |
if self.activeTransparencyController then | |
self.activeTransparencyController:Update() | |
end | |
end | |
end | |
function Camera:IsFirstPerson() | |
if self.activeCameraController then | |
return self.activeCameraController:InFirstPerson() | |
end | |
return false | |
end | |
function Camera:IsMouseLocked() | |
if self.activeCameraController then | |
return self.activeCameraController:GetIsMouseLocked() | |
end | |
return false | |
end | |
function Camera:IsToggleMode() | |
if self.activeCameraController then | |
return self.activeCameraController.isCameraToggle | |
end | |
return false | |
end | |
function Camera:IsCamRelative() | |
return self:IsMouseLocked() or self:IsFirstPerson() | |
--return self:IsToggleMode(), self:IsMouseLocked(), self:IsFirstPerson() | |
end | |
-- | |
local Utils = _CameraUtils() | |
function Utils.GetAngleBetweenXZVectors(v1, v2) | |
local upCFrame = lastUpCFrame | |
v1 = upCFrame:VectorToObjectSpace(v1) | |
v2 = upCFrame:VectorToObjectSpace(v2) | |
return math.atan2(v2.X*v1.Z-v2.Z*v1.X, v2.X*v1.X+v2.Z*v1.Z) | |
end | |
--end edit | |
local cameraModuleObject = CameraModule.new() | |
local cameraApi = {} | |
return cameraModuleObject | |
end | |
function _ClickToMoveDisplay() | |
local ClickToMoveDisplay = {} | |
local FAILURE_ANIMATION_ID = "rbxassetid://2874840706" | |
local TrailDotIcon = "rbxasset://textures/ui/traildot.png" | |
local EndWaypointIcon = "rbxasset://textures/ui/waypoint.png" | |
local WaypointsAlwaysOnTop = false | |
local WAYPOINT_INCLUDE_FACTOR = 2 | |
local LAST_DOT_DISTANCE = 3 | |
local WAYPOINT_BILLBOARD_SIZE = UDim2.new(0, 1.68 * 25, 0, 2 * 25) | |
local ENDWAYPOINT_SIZE_OFFSET_MIN = Vector2.new(0, 0.5) | |
local ENDWAYPOINT_SIZE_OFFSET_MAX = Vector2.new(0, 1) | |
local FAIL_WAYPOINT_SIZE_OFFSET_CENTER = Vector2.new(0, 0.5) | |
local FAIL_WAYPOINT_SIZE_OFFSET_LEFT = Vector2.new(0.1, 0.5) | |
local FAIL_WAYPOINT_SIZE_OFFSET_RIGHT = Vector2.new(-0.1, 0.5) | |
local FAILURE_TWEEN_LENGTH = 0.125 | |
local FAILURE_TWEEN_COUNT = 4 | |
local TWEEN_WAYPOINT_THRESHOLD = 5 | |
local TRAIL_DOT_PARENT_NAME = "ClickToMoveDisplay" | |
local TrailDotSize = Vector2.new(1.5, 1.5) | |
local TRAIL_DOT_MIN_SCALE = 1 | |
local TRAIL_DOT_MIN_DISTANCE = 10 | |
local TRAIL_DOT_MAX_SCALE = 2.5 | |
local TRAIL_DOT_MAX_DISTANCE = 100 | |
local PlayersService = game:GetService("Players") | |
local TweenService = game:GetService("TweenService") | |
local RunService = game:GetService("RunService") | |
local Workspace = game:GetService("Workspace") | |
local LocalPlayer = PlayersService.LocalPlayer | |
local function CreateWaypointTemplates() | |
local TrailDotTemplate = Instance.new("Part") | |
TrailDotTemplate.Size = Vector3.new(1, 1, 1) | |
TrailDotTemplate.Anchored = true | |
TrailDotTemplate.CanCollide = false | |
TrailDotTemplate.Name = "TrailDot" | |
TrailDotTemplate.Transparency = 1 | |
local TrailDotImage = Instance.new("ImageHandleAdornment") | |
TrailDotImage.Name = "TrailDotImage" | |
TrailDotImage.Size = TrailDotSize | |
TrailDotImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1) | |
TrailDotImage.AlwaysOnTop = WaypointsAlwaysOnTop | |
TrailDotImage.Image = TrailDotIcon | |
TrailDotImage.Adornee = TrailDotTemplate | |
TrailDotImage.Parent = TrailDotTemplate | |
local EndWaypointTemplate = Instance.new("Part") | |
EndWaypointTemplate.Size = Vector3.new(2, 2, 2) | |
EndWaypointTemplate.Anchored = true | |
EndWaypointTemplate.CanCollide = false | |
EndWaypointTemplate.Name = "EndWaypoint" | |
EndWaypointTemplate.Transparency = 1 | |
local EndWaypointImage = Instance.new("ImageHandleAdornment") | |
EndWaypointImage.Name = "TrailDotImage" | |
EndWaypointImage.Size = TrailDotSize | |
EndWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1) | |
EndWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop | |
EndWaypointImage.Image = TrailDotIcon | |
EndWaypointImage.Adornee = EndWaypointTemplate | |
EndWaypointImage.Parent = EndWaypointTemplate | |
local EndWaypointBillboard = Instance.new("BillboardGui") | |
EndWaypointBillboard.Name = "EndWaypointBillboard" | |
EndWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE | |
EndWaypointBillboard.LightInfluence = 0 | |
EndWaypointBillboard.SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MIN | |
EndWaypointBillboard.AlwaysOnTop = true | |
EndWaypointBillboard.Adornee = EndWaypointTemplate | |
EndWaypointBillboard.Parent = EndWaypointTemplate | |
local EndWaypointImageLabel = Instance.new("ImageLabel") | |
EndWaypointImageLabel.Image = EndWaypointIcon | |
EndWaypointImageLabel.BackgroundTransparency = 1 | |
EndWaypointImageLabel.Size = UDim2.new(1, 0, 1, 0) | |
EndWaypointImageLabel.Parent = EndWaypointBillboard | |
local FailureWaypointTemplate = Instance.new("Part") | |
FailureWaypointTemplate.Size = Vector3.new(2, 2, 2) | |
FailureWaypointTemplate.Anchored = true | |
FailureWaypointTemplate.CanCollide = false | |
FailureWaypointTemplate.Name = "FailureWaypoint" | |
FailureWaypointTemplate.Transparency = 1 | |
local FailureWaypointImage = Instance.new("ImageHandleAdornment") | |
FailureWaypointImage.Name = "TrailDotImage" | |
FailureWaypointImage.Size = TrailDotSize | |
FailureWaypointImage.SizeRelativeOffset = Vector3.new(0, 0, -0.1) | |
FailureWaypointImage.AlwaysOnTop = WaypointsAlwaysOnTop | |
FailureWaypointImage.Image = TrailDotIcon | |
FailureWaypointImage.Adornee = FailureWaypointTemplate | |
FailureWaypointImage.Parent = FailureWaypointTemplate | |
local FailureWaypointBillboard = Instance.new("BillboardGui") | |
FailureWaypointBillboard.Name = "FailureWaypointBillboard" | |
FailureWaypointBillboard.Size = WAYPOINT_BILLBOARD_SIZE | |
FailureWaypointBillboard.LightInfluence = 0 | |
FailureWaypointBillboard.SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER | |
FailureWaypointBillboard.AlwaysOnTop = true | |
FailureWaypointBillboard.Adornee = FailureWaypointTemplate | |
FailureWaypointBillboard.Parent = FailureWaypointTemplate | |
local FailureWaypointFrame = Instance.new("Frame") | |
FailureWaypointFrame.BackgroundTransparency = 1 | |
FailureWaypointFrame.Size = UDim2.new(0, 0, 0, 0) | |
FailureWaypointFrame.Position = UDim2.new(0.5, 0, 1, 0) | |
FailureWaypointFrame.Parent = FailureWaypointBillboard | |
local FailureWaypointImageLabel = Instance.new("ImageLabel") | |
FailureWaypointImageLabel.Image = EndWaypointIcon | |
FailureWaypointImageLabel.BackgroundTransparency = 1 | |
FailureWaypointImageLabel.Position = UDim2.new( | |
0, -WAYPOINT_BILLBOARD_SIZE.X.Offset/2, 0, -WAYPOINT_BILLBOARD_SIZE.Y.Offset | |
) | |
FailureWaypointImageLabel.Size = WAYPOINT_BILLBOARD_SIZE | |
FailureWaypointImageLabel.Parent = FailureWaypointFrame | |
return TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate | |
end | |
local TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() | |
local function getTrailDotParent() | |
local camera = Workspace.CurrentCamera | |
local trailParent = camera:FindFirstChild(TRAIL_DOT_PARENT_NAME) | |
if not trailParent then | |
trailParent = Instance.new("Model") | |
trailParent.Name = TRAIL_DOT_PARENT_NAME | |
trailParent.Parent = camera | |
end | |
return trailParent | |
end | |
local function placePathWaypoint(waypointModel, position) | |
local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0)) | |
local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList( | |
ray, | |
{ Workspace.CurrentCamera, LocalPlayer.Character } | |
) | |
if hitPart then | |
waypointModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal) | |
waypointModel.Parent = getTrailDotParent() | |
end | |
end | |
local TrailDot = {} | |
TrailDot.__index = TrailDot | |
function TrailDot:Destroy() | |
self.DisplayModel:Destroy() | |
end | |
function TrailDot:NewDisplayModel(position) | |
local newDisplayModel = TrailDotTemplate:Clone() | |
placePathWaypoint(newDisplayModel, position) | |
return newDisplayModel | |
end | |
function TrailDot.new(position, closestWaypoint) | |
local self = setmetatable({}, TrailDot) | |
self.DisplayModel = self:NewDisplayModel(position) | |
self.ClosestWayPoint = closestWaypoint | |
return self | |
end | |
local EndWaypoint = {} | |
EndWaypoint.__index = EndWaypoint | |
function EndWaypoint:Destroy() | |
self.Destroyed = true | |
self.Tween:Cancel() | |
self.DisplayModel:Destroy() | |
end | |
function EndWaypoint:NewDisplayModel(position) | |
local newDisplayModel = EndWaypointTemplate:Clone() | |
placePathWaypoint(newDisplayModel, position) | |
return newDisplayModel | |
end | |
function EndWaypoint:CreateTween() | |
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, -1, true) | |
local tween = TweenService:Create( | |
self.DisplayModel.EndWaypointBillboard, | |
tweenInfo, | |
{ SizeOffset = ENDWAYPOINT_SIZE_OFFSET_MAX } | |
) | |
tween:Play() | |
return tween | |
end | |
function EndWaypoint:TweenInFrom(originalPosition) | |
local currentPositon = self.DisplayModel.Position | |
local studsOffset = originalPosition - currentPositon | |
self.DisplayModel.EndWaypointBillboard.StudsOffset = Vector3.new(0, studsOffset.Y, 0) | |
local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) | |
local tween = TweenService:Create( | |
self.DisplayModel.EndWaypointBillboard, | |
tweenInfo, | |
{ StudsOffset = Vector3.new(0, 0, 0) } | |
) | |
tween:Play() | |
return tween | |
end | |
function EndWaypoint.new(position, closestWaypoint, originalPosition) | |
local self = setmetatable({}, EndWaypoint) | |
self.DisplayModel = self:NewDisplayModel(position) | |
self.Destroyed = false | |
if originalPosition and (originalPosition - position).magnitude > TWEEN_WAYPOINT_THRESHOLD then | |
self.Tween = self:TweenInFrom(originalPosition) | |
coroutine.wrap(function() | |
self.Tween.Completed:Wait() | |
if not self.Destroyed then | |
self.Tween = self:CreateTween() | |
end | |
end)() | |
else | |
self.Tween = self:CreateTween() | |
end | |
self.ClosestWayPoint = closestWaypoint | |
return self | |
end | |
local FailureWaypoint = {} | |
FailureWaypoint.__index = FailureWaypoint | |
function FailureWaypoint:Hide() | |
self.DisplayModel.Parent = nil | |
end | |
function FailureWaypoint:Destroy() | |
self.DisplayModel:Destroy() | |
end | |
function FailureWaypoint:NewDisplayModel(position) | |
local newDisplayModel = FailureWaypointTemplate:Clone() | |
placePathWaypoint(newDisplayModel, position) | |
local ray = Ray.new(position + Vector3.new(0, 2.5, 0), Vector3.new(0, -10, 0)) | |
local hitPart, hitPoint, hitNormal = Workspace:FindPartOnRayWithIgnoreList( | |
ray, { Workspace.CurrentCamera, LocalPlayer.Character } | |
) | |
if hitPart then | |
newDisplayModel.CFrame = CFrame.new(hitPoint, hitPoint + hitNormal) | |
newDisplayModel.Parent = getTrailDotParent() | |
end | |
return newDisplayModel | |
end | |
function FailureWaypoint:RunFailureTween() | |
wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore starting tweening | |
-- Tween out from center | |
local tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) | |
local tweenLeft = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo, | |
{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_LEFT }) | |
tweenLeft:Play() | |
local tweenLeftRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo, | |
{ Rotation = 10 }) | |
tweenLeftRoation:Play() | |
tweenLeft.Completed:wait() | |
-- Tween back and forth | |
tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, | |
FAILURE_TWEEN_COUNT - 1, true) | |
local tweenSideToSide = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo, | |
{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_RIGHT}) | |
tweenSideToSide:Play() | |
-- Tween flash dark and roate left and right | |
tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, | |
FAILURE_TWEEN_COUNT - 1, true) | |
local tweenFlash = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame.ImageLabel, tweenInfo, | |
{ ImageColor3 = Color3.new(0.75, 0.75, 0.75)}) | |
tweenFlash:Play() | |
local tweenRotate = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo, | |
{ Rotation = -10 }) | |
tweenRotate:Play() | |
tweenSideToSide.Completed:wait() | |
-- Tween back to center | |
tweenInfo = TweenInfo.new(FAILURE_TWEEN_LENGTH/2, Enum.EasingStyle.Sine, Enum.EasingDirection.Out) | |
local tweenCenter = TweenService:Create(self.DisplayModel.FailureWaypointBillboard, tweenInfo, | |
{ SizeOffset = FAIL_WAYPOINT_SIZE_OFFSET_CENTER }) | |
tweenCenter:Play() | |
local tweenRoation = TweenService:Create(self.DisplayModel.FailureWaypointBillboard.Frame, tweenInfo, | |
{ Rotation = 0 }) | |
tweenRoation:Play() | |
tweenCenter.Completed:wait() | |
wait(FAILURE_TWEEN_LENGTH) -- Delay one tween length betfore removing | |
end | |
function FailureWaypoint.new(position) | |
local self = setmetatable({}, FailureWaypoint) | |
self.DisplayModel = self:NewDisplayModel(position) | |
return self | |
end | |
local failureAnimation = Instance.new("Animation") | |
failureAnimation.AnimationId = FAILURE_ANIMATION_ID | |
local lastHumanoid = nil | |
local lastFailureAnimationTrack = nil | |
local function getFailureAnimationTrack(myHumanoid) | |
if myHumanoid == lastHumanoid then | |
return lastFailureAnimationTrack | |
end | |
lastFailureAnimationTrack = myHumanoid:LoadAnimation(failureAnimation) | |
lastFailureAnimationTrack.Priority = Enum.AnimationPriority.Action | |
lastFailureAnimationTrack.Looped = false | |
return lastFailureAnimationTrack | |
end | |
local function findPlayerHumanoid() | |
local character = LocalPlayer.Character | |
if character then | |
return character:FindFirstChildOfClass("Humanoid") | |
end | |
end | |
local function createTrailDots(wayPoints, originalEndWaypoint) | |
local newTrailDots = {} | |
local count = 1 | |
for i = 1, #wayPoints - 1 do | |
local closeToEnd = (wayPoints[i].Position - wayPoints[#wayPoints].Position).magnitude < LAST_DOT_DISTANCE | |
local includeWaypoint = i % WAYPOINT_INCLUDE_FACTOR == 0 and not closeToEnd | |
if includeWaypoint then | |
local trailDot = TrailDot.new(wayPoints[i].Position, i) | |
newTrailDots[count] = trailDot | |
count = count + 1 | |
end | |
end | |
local newEndWaypoint = EndWaypoint.new(wayPoints[#wayPoints].Position, #wayPoints, originalEndWaypoint) | |
table.insert(newTrailDots, newEndWaypoint) | |
local reversedTrailDots = {} | |
count = 1 | |
for i = #newTrailDots, 1, -1 do | |
reversedTrailDots[count] = newTrailDots[i] | |
count = count + 1 | |
end | |
return reversedTrailDots | |
end | |
local function getTrailDotScale(distanceToCamera, defaultSize) | |
local rangeLength = TRAIL_DOT_MAX_DISTANCE - TRAIL_DOT_MIN_DISTANCE | |
local inRangePoint = math.clamp(distanceToCamera - TRAIL_DOT_MIN_DISTANCE, 0, rangeLength)/rangeLength | |
local scale = TRAIL_DOT_MIN_SCALE + (TRAIL_DOT_MAX_SCALE - TRAIL_DOT_MIN_SCALE)*inRangePoint | |
return defaultSize * scale | |
end | |
local createPathCount = 0 | |
-- originalEndWaypoint is optional, causes the waypoint to tween from that position. | |
function ClickToMoveDisplay.CreatePathDisplay(wayPoints, originalEndWaypoint) | |
createPathCount = createPathCount + 1 | |
local trailDots = createTrailDots(wayPoints, originalEndWaypoint) | |
local function removePathBeforePoint(wayPointNumber) | |
-- kill all trailDots before and at wayPointNumber | |
for i = #trailDots, 1, -1 do | |
local trailDot = trailDots[i] | |
if trailDot.ClosestWayPoint <= wayPointNumber then | |
trailDot:Destroy() | |
trailDots[i] = nil | |
else | |
break | |
end | |
end | |
end | |
local reiszeTrailDotsUpdateName = "ClickToMoveResizeTrail" ..createPathCount | |
local function resizeTrailDots() | |
if #trailDots == 0 then | |
RunService:UnbindFromRenderStep(reiszeTrailDotsUpdateName) | |
return | |
end | |
local cameraPos = Workspace.CurrentCamera.CFrame.p | |
for i = 1, #trailDots do | |
local trailDotImage = trailDots[i].DisplayModel:FindFirstChild("TrailDotImage") | |
if trailDotImage then | |
local distanceToCamera = (trailDots[i].DisplayModel.Position - cameraPos).magnitude | |
trailDotImage.Size = getTrailDotScale(distanceToCamera, TrailDotSize) | |
end | |
end | |
end | |
RunService:BindToRenderStep(reiszeTrailDotsUpdateName, Enum.RenderPriority.Camera.Value - 1, resizeTrailDots) | |
local function removePath() | |
removePathBeforePoint(#wayPoints) | |
end | |
return removePath, removePathBeforePoint | |
end | |
local lastFailureWaypoint = nil | |
function ClickToMoveDisplay.DisplayFailureWaypoint(position) | |
if lastFailureWaypoint then | |
lastFailureWaypoint:Hide() | |
end | |
local failureWaypoint = FailureWaypoint.new(position) | |
lastFailureWaypoint = failureWaypoint | |
coroutine.wrap(function() | |
failureWaypoint:RunFailureTween() | |
failureWaypoint:Destroy() | |
failureWaypoint = nil | |
end)() | |
end | |
function ClickToMoveDisplay.CreateEndWaypoint(position) | |
return EndWaypoint.new(position) | |
end | |
function ClickToMoveDisplay.PlayFailureAnimation() | |
local myHumanoid = findPlayerHumanoid() | |
if myHumanoid then | |
local animationTrack = getFailureAnimationTrack(myHumanoid) | |
animationTrack:Play() | |
end | |
end | |
function ClickToMoveDisplay.CancelFailureAnimation() | |
if lastFailureAnimationTrack ~= nil and lastFailureAnimationTrack.IsPlaying then | |
lastFailureAnimationTrack:Stop() | |
end | |
end | |
function ClickToMoveDisplay.SetWaypointTexture(texture) | |
TrailDotIcon = texture | |
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() | |
end | |
function ClickToMoveDisplay.GetWaypointTexture() | |
return TrailDotIcon | |
end | |
function ClickToMoveDisplay.SetWaypointRadius(radius) | |
TrailDotSize = Vector2.new(radius, radius) | |
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() | |
end | |
function ClickToMoveDisplay.GetWaypointRadius() | |
return TrailDotSize.X | |
end | |
function ClickToMoveDisplay.SetEndWaypointTexture(texture) | |
EndWaypointIcon = texture | |
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() | |
end | |
function ClickToMoveDisplay.GetEndWaypointTexture() | |
return EndWaypointIcon | |
end | |
function ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop) | |
WaypointsAlwaysOnTop = alwaysOnTop | |
TrailDotTemplate, EndWaypointTemplate, FailureWaypointTemplate = CreateWaypointTemplates() | |
end | |
function ClickToMoveDisplay.GetWaypointsAlwaysOnTop() | |
return WaypointsAlwaysOnTop | |
end | |
return ClickToMoveDisplay | |
end | |
function _BaseCharacterController() | |
local ZERO_VECTOR3 = Vector3.new(0,0,0) | |
--[[ The Module ]]-- | |
local BaseCharacterController = {} | |
BaseCharacterController.__index = BaseCharacterController | |
function BaseCharacterController.new() | |
local self = setmetatable({}, BaseCharacterController) | |
self.enabled = false | |
self.moveVector = ZERO_VECTOR3 | |
self.moveVectorIsCameraRelative = true | |
self.isJumping = false | |
return self | |
end | |
function BaseCharacterController:OnRenderStepped(dt) | |
-- By default, nothing to do | |
end | |
function BaseCharacterController:GetMoveVector() | |
return self.moveVector | |
end | |
function BaseCharacterController:IsMoveVectorCameraRelative() | |
return self.moveVectorIsCameraRelative | |
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 | |
end | |
function _VehicleController() | |
local ContextActionService = game:GetService("ContextActionService") | |
--[[ 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) | |
local AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE = 35 | |
-- Note that VehicleController does not derive from BaseCharacterController, it is a special case | |
local VehicleController = {} | |
VehicleController.__index = VehicleController | |
function VehicleController.new(CONTROL_ACTION_PRIORITY) | |
local self = setmetatable({}, VehicleController) | |
self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY | |
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 | |
self.autoPilot = {} | |
self.autoPilot.MaxSpeed = 0 | |
self.autoPilot.MaxSteeringAngle = 0 | |
return self | |
end | |
function VehicleController:BindContextActions() | |
if useTriggersForThrottle then | |
ContextActionService:BindActionAtPriority("throttleAccel", (function(actionName, inputState, inputObject) | |
self:OnThrottleAccel(actionName, inputState, inputObject) | |
return Enum.ContextActionResult.Pass | |
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonR2) | |
ContextActionService:BindActionAtPriority("throttleDeccel", (function(actionName, inputState, inputObject) | |
self:OnThrottleDeccel(actionName, inputState, inputObject) | |
return Enum.ContextActionResult.Pass | |
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonL2) | |
end | |
ContextActionService:BindActionAtPriority("arrowSteerRight", (function(actionName, inputState, inputObject) | |
self:OnSteerRight(actionName, inputState, inputObject) | |
return Enum.ContextActionResult.Pass | |
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Right) | |
ContextActionService:BindActionAtPriority("arrowSteerLeft", (function(actionName, inputState, inputObject) | |
self:OnSteerLeft(actionName, inputState, inputObject) | |
return Enum.ContextActionResult.Pass | |
end), false, self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.Left) | |
end | |
function VehicleController:Enable(enable, vehicleSeat) | |
if enable == self.enabled and vehicleSeat == self.vehicleSeat then | |
return | |
end | |
self.enabled = enable | |
self.vehicleMoveVector = ZERO_VECTOR3 | |
if enable then | |
if vehicleSeat then | |
self.vehicleSeat = vehicleSeat | |
self:SetupAutoPilot() | |
self:BindContextActions() | |
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) | |
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then | |
self.acceleration = 0 | |
else | |
self.acceleration = -1 | |
end | |
self.throttle = self.acceleration + self.decceleration | |
end | |
function VehicleController:OnThrottleDeccel(actionName, inputState, inputObject) | |
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then | |
self.decceleration = 0 | |
else | |
self.decceleration = 1 | |
end | |
self.throttle = self.acceleration + self.decceleration | |
end | |
function VehicleController:OnSteerRight(actionName, inputState, inputObject) | |
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then | |
self.turningRight = 0 | |
else | |
self.turningRight = 1 | |
end | |
self.steer = self.turningRight + self.turningLeft | |
end | |
function VehicleController:OnSteerLeft(actionName, inputState, inputObject) | |
if inputState == Enum.UserInputState.End or inputState == Enum.UserInputState.Cancel then | |
self.turningLeft = 0 | |
else | |
self.turningLeft = -1 | |
end | |
self.steer = self.turningRight + self.turningLeft | |
end | |
-- Call this from a function bound to Renderstep with Input Priority | |
function VehicleController:Update(moveVector, cameraRelative, usingGamepad) | |
if self.vehicleSeat then | |
if cameraRelative then | |
-- This is the default steering mode | |
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 | |
else | |
-- This is the path following mode | |
local localMoveVector = self.vehicleSeat.Occupant.RootPart.CFrame:VectorToObjectSpace(moveVector) | |
self.vehicleSeat.ThrottleFloat = self:ComputeThrottle(localMoveVector) | |
self.vehicleSeat.SteerFloat = self:ComputeSteer(localMoveVector) | |
return ZERO_VECTOR3, true | |
end | |
end | |
return moveVector, false | |
end | |
function VehicleController:ComputeThrottle(localMoveVector) | |
if localMoveVector ~= ZERO_VECTOR3 then | |
local throttle = -localMoveVector.Z | |
return throttle | |
else | |
return 0.0 | |
end | |
end | |
function VehicleController:ComputeSteer(localMoveVector) | |
if localMoveVector ~= ZERO_VECTOR3 then | |
local steerAngle = -math.atan2(-localMoveVector.x, -localMoveVector.z) * (180 / math.pi) | |
return steerAngle / self.autoPilot.MaxSteeringAngle | |
else | |
return 0.0 | |
end | |
end | |
function VehicleController:SetupAutoPilot() | |
-- Setup default | |
self.autoPilot.MaxSpeed = self.vehicleSeat.MaxSpeed | |
self.autoPilot.MaxSteeringAngle = AUTO_PILOT_DEFAULT_MAX_STEERING_ANGLE | |
-- VehicleSeat should have a MaxSteeringAngle as well. | |
-- Or we could look for a child "AutoPilotConfigModule" to find these values | |
-- Or allow developer to set them through the API as like the CLickToMove customization API | |
end | |
return VehicleController | |
end | |
function _TouchJump() | |
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 = _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(1, 146) | |
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) | |
if parentFrame then | |
self.parentUIFrame = parentFrame | |
end | |
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 | |
end | |
function _ClickToMoveController() | |
--[[ Roblox Services ]]-- | |
local UserInputService = game:GetService("UserInputService") | |
local PathfindingService = game:GetService("PathfindingService") | |
local Players = game:GetService("Players") | |
local DebrisService = game:GetService('Debris') | |
local StarterGui = game:GetService("StarterGui") | |
local Workspace = game:GetService("Workspace") | |
local CollectionService = game:GetService("CollectionService") | |
local GuiService = game:GetService("GuiService") | |
--[[ Configuration ]] | |
local ShowPath = true | |
local PlayFailureAnimation = true | |
local UseDirectPath = false | |
local UseDirectPathForVehicle = true | |
local AgentSizeIncreaseFactor = 1.0 | |
local UnreachableWaypointTimeout = 8 | |
--[[ Constants ]]-- | |
local movementKeys = { | |
[Enum.KeyCode.W] = true; | |
[Enum.KeyCode.A] = true; | |
[Enum.KeyCode.S] = true; | |
[Enum.KeyCode.D] = true; | |
[Enum.KeyCode.Up] = true; | |
[Enum.KeyCode.Down] = true; | |
} | |
local FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess, FFlagUserNavigationClickToMoveSkipPassedWaypointsResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNavigationClickToMoveSkipPassedWaypoints") end) | |
local FFlagUserNavigationClickToMoveSkipPassedWaypoints = FFlagUserNavigationClickToMoveSkipPassedWaypointsSuccess and FFlagUserNavigationClickToMoveSkipPassedWaypointsResult | |
local Player = Players.LocalPlayer | |
local ClickToMoveDisplay = _ClickToMoveDisplay() | |
local ZERO_VECTOR3 = Vector3.new(0,0,0) | |
local ALMOST_ZERO = 0.000001 | |
--------------------------UTIL LIBRARY------------------------------- | |
local Utility = {} | |
do | |
local function FindCharacterAncestor(part) | |
if part then | |
local humanoid = part:FindFirstChildOfClass("Humanoid") | |
if humanoid then | |
return part, humanoid | |
else | |
return FindCharacterAncestor(part.Parent) | |
end | |
end | |
end | |
Utility.FindCharacterAncestor = FindCharacterAncestor | |
local function Raycast(ray, ignoreNonCollidable, ignoreList) | |
ignoreList = ignoreList or {} | |
local hitPart, hitPos, hitNorm, hitMat = Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList) | |
if hitPart then | |
if ignoreNonCollidable and hitPart.CanCollide == false then | |
-- We always include character parts so a user can click on another character | |
-- to walk to them. | |
local _, humanoid = FindCharacterAncestor(hitPart) | |
if humanoid == nil then | |
table.insert(ignoreList, hitPart) | |
return Raycast(ray, ignoreNonCollidable, ignoreList) | |
end | |
end | |
return hitPart, hitPos, hitNorm, hitMat | |
end | |
return nil, nil | |
end | |
Utility.Raycast = Raycast | |
end | |
local humanoidCache = {} | |
local function findPlayerHumanoid(player) | |
local character = player and player.Character | |
if character then | |
local resultHumanoid = humanoidCache[player] | |
if resultHumanoid and resultHumanoid.Parent == character then | |
return resultHumanoid | |
else | |
humanoidCache[player] = nil -- Bust Old Cache | |
local humanoid = character:FindFirstChildOfClass("Humanoid") | |
if humanoid then | |
humanoidCache[player] = humanoid | |
end | |
return humanoid | |
end | |
end | |
end | |
--------------------------CHARACTER CONTROL------------------------------- | |
local CurrentIgnoreList | |
local CurrentIgnoreTag = nil | |
local TaggedInstanceAddedConnection = nil | |
local TaggedInstanceRemovedConnection = nil | |
local function GetCharacter() | |
return Player and Player.Character | |
end | |
local function UpdateIgnoreTag(newIgnoreTag) | |
if newIgnoreTag == CurrentIgnoreTag then | |
return | |
end | |
if TaggedInstanceAddedConnection then | |
TaggedInstanceAddedConnection:Disconnect() | |
TaggedInstanceAddedConnection = nil | |
end | |
if TaggedInstanceRemovedConnection then | |
TaggedInstanceRemovedConnection:Disconnect() | |
TaggedInstanceRemovedConnection = nil | |
end | |
CurrentIgnoreTag = newIgnoreTag | |
CurrentIgnoreList = {GetCharacter()} | |
if CurrentIgnoreTag ~= nil then | |
local ignoreParts = CollectionService:GetTagged(CurrentIgnoreTag) | |
for _, ignorePart in ipairs(ignoreParts) do | |
table.insert(CurrentIgnoreList, ignorePart) | |
end | |
TaggedInstanceAddedConnection = CollectionService:GetInstanceAddedSignal( | |
CurrentIgnoreTag):Connect(function(ignorePart) | |
table.insert(CurrentIgnoreList, ignorePart) | |
end) | |
TaggedInstanceRemovedConnection = CollectionService:GetInstanceRemovedSignal( | |
CurrentIgnoreTag):Connect(function(ignorePart) | |
for i = 1, #CurrentIgnoreList do | |
if CurrentIgnoreList[i] == ignorePart then | |
CurrentIgnoreList[i] = CurrentIgnoreList[#CurrentIgnoreList] | |
table.remove(CurrentIgnoreList) | |
break | |
end | |
end | |
end) | |
end | |
end | |
local function getIgnoreList() | |
if CurrentIgnoreList then | |
return CurrentIgnoreList | |
end | |
CurrentIgnoreList = {} | |
table.insert(CurrentIgnoreList, GetCharacter()) | |
return CurrentIgnoreList | |
end | |
-----------------------------------PATHER-------------------------------------- | |
local function Pather(endPoint, surfaceNormal, overrideUseDirectPath) | |
local this = {} | |
local directPathForHumanoid | |
local directPathForVehicle | |
if overrideUseDirectPath ~= nil then | |
directPathForHumanoid = overrideUseDirectPath | |
directPathForVehicle = overrideUseDirectPath | |
else | |
directPathForHumanoid = UseDirectPath | |
directPathForVehicle = UseDirectPathForVehicle | |
end | |
this.Cancelled = false | |
this.Started = false | |
this.Finished = Instance.new("BindableEvent") | |
this.PathFailed = Instance.new("BindableEvent") | |
this.PathComputing = false | |
this.PathComputed = false | |
this.OriginalTargetPoint = endPoint | |
this.TargetPoint = endPoint | |
this.TargetSurfaceNormal = surfaceNormal | |
this.DiedConn = nil | |
this.SeatedConn = nil | |
this.BlockedConn = nil | |
this.TeleportedConn = nil | |
this.CurrentPoint = 0 | |
this.HumanoidOffsetFromPath = ZERO_VECTOR3 | |
this.CurrentWaypointPosition = nil | |
this.CurrentWaypointPlaneNormal = ZERO_VECTOR3 | |
this.CurrentWaypointPlaneDistance = 0 | |
this.CurrentWaypointNeedsJump = false; | |
this.CurrentHumanoidPosition = ZERO_VECTOR3 | |
this.CurrentHumanoidVelocity = 0 | |
this.NextActionMoveDirection = ZERO_VECTOR3 | |
this.NextActionJump = false | |
this.Timeout = 0 | |
this.Humanoid = findPlayerHumanoid(Player) | |
this.OriginPoint = nil | |
this.AgentCanFollowPath = false | |
this.DirectPath = false | |
this.DirectPathRiseFirst = false | |
local rootPart = this.Humanoid and this.Humanoid.RootPart | |
if rootPart then | |
-- Setup origin | |
this.OriginPoint = rootPart.CFrame.p | |
-- Setup agent | |
local agentRadius = 2 | |
local agentHeight = 5 | |
local agentCanJump = true | |
local seat = this.Humanoid.SeatPart | |
if seat and seat:IsA("VehicleSeat") then | |
-- Humanoid is seated on a vehicle | |
local vehicle = seat:FindFirstAncestorOfClass("Model") | |
if vehicle then | |
-- Make sure the PrimaryPart is set to the vehicle seat while we compute the extends. | |
local tempPrimaryPart = vehicle.PrimaryPart | |
vehicle.PrimaryPart = seat | |
-- For now, only direct path | |
if directPathForVehicle then | |
local extents = vehicle:GetExtentsSize() | |
agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z) | |
agentHeight = AgentSizeIncreaseFactor * extents.Y | |
agentCanJump = false | |
this.AgentCanFollowPath = true | |
this.DirectPath = directPathForVehicle | |
end | |
-- Reset PrimaryPart | |
vehicle.PrimaryPart = tempPrimaryPart | |
end | |
else | |
local extents = GetCharacter():GetExtentsSize() | |
agentRadius = AgentSizeIncreaseFactor * 0.5 * math.sqrt(extents.X * extents.X + extents.Z * extents.Z) | |
agentHeight = AgentSizeIncreaseFactor * extents.Y | |
agentCanJump = (this.Humanoid.JumpPower > 0) | |
this.AgentCanFollowPath = true | |
this.DirectPath = directPathForHumanoid | |
this.DirectPathRiseFirst = this.Humanoid.Sit | |
end | |
-- Build path object | |
this.pathResult = PathfindingService:CreatePath({AgentRadius = agentRadius, AgentHeight = agentHeight, AgentCanJump = agentCanJump}) | |
end | |
function this:Cleanup() | |
if this.stopTraverseFunc then | |
this.stopTraverseFunc() | |
this.stopTraverseFunc = nil | |
end | |
if this.MoveToConn then | |
this.MoveToConn:Disconnect() | |
this.MoveToConn = nil | |
end | |
if this.BlockedConn then | |
this.BlockedConn:Disconnect() | |
this.BlockedConn = nil | |
end | |
if this.DiedConn then | |
this.DiedConn:Disconnect() | |
this.DiedConn = nil | |
end | |
if this.SeatedConn then | |
this.SeatedConn:Disconnect() | |
this.SeatedConn = nil | |
end | |
if this.TeleportedConn then | |
this.TeleportedConn:Disconnect() | |
this.TeleportedConn = nil | |
end | |
this.Started = false | |
end | |
function this:Cancel() | |
this.Cancelled = true | |
this:Cleanup() | |
end | |
function this:IsActive() | |
return this.AgentCanFollowPath and this.Started and not this.Cancelled | |
end | |
function this:OnPathInterrupted() | |
-- Stop moving | |
this.Cancelled = true | |
this:OnPointReached(false) | |
end | |
function this:ComputePath() | |
if this.OriginPoint then | |
if this.PathComputed or this.PathComputing then return end | |
this.PathComputing = true | |
if this.AgentCanFollowPath then | |
if this.DirectPath then | |
this.pointList = { | |
PathWaypoint.new(this.OriginPoint, Enum.PathWaypointAction.Walk), | |
PathWaypoint.new(this.TargetPoint, this.DirectPathRiseFirst and Enum.PathWaypointAction.Jump or Enum.PathWaypointAction.Walk) | |
} | |
this.PathComputed = true | |
else | |
this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint) | |
this.pointList = this.pathResult:GetWaypoints() | |
this.BlockedConn = this.pathResult.Blocked:Connect(function(blockedIdx) this:OnPathBlocked(blockedIdx) end) | |
this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success | |
end | |
end | |
this.PathComputing = false | |
end | |
end | |
function this:IsValidPath() | |
this:ComputePath() | |
return this.PathComputed and this.AgentCanFollowPath | |
end | |
this.Recomputing = false | |
function this:OnPathBlocked(blockedWaypointIdx) | |
local pathBlocked = blockedWaypointIdx >= this.CurrentPoint | |
if not pathBlocked or this.Recomputing then | |
return | |
end | |
this.Recomputing = true | |
if this.stopTraverseFunc then | |
this.stopTraverseFunc() | |
this.stopTraverseFunc = nil | |
end | |
this.OriginPoint = this.Humanoid.RootPart.CFrame.p | |
this.pathResult:ComputeAsync(this.OriginPoint, this.TargetPoint) | |
this.pointList = this.pathResult:GetWaypoints() | |
if #this.pointList > 0 then | |
this.HumanoidOffsetFromPath = this.pointList[1].Position - this.OriginPoint | |
end | |
this.PathComputed = this.pathResult.Status == Enum.PathStatus.Success | |
if ShowPath then | |
this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList) | |
end | |
if this.PathComputed then | |
this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. | |
this:OnPointReached(true) -- Move to first point | |
else | |
this.PathFailed:Fire() | |
this:Cleanup() | |
end | |
this.Recomputing = false | |
end | |
function this:OnRenderStepped(dt) | |
if this.Started and not this.Cancelled then | |
-- Check for Timeout (if a waypoint is not reached within the delay, we fail) | |
this.Timeout = this.Timeout + dt | |
if this.Timeout > UnreachableWaypointTimeout then | |
this:OnPointReached(false) | |
return | |
end | |
-- Get Humanoid position and velocity | |
this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath | |
this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity | |
-- Check if it has reached some waypoints | |
while this.Started and this:IsCurrentWaypointReached() do | |
this:OnPointReached(true) | |
end | |
-- If still started, update actions | |
if this.Started then | |
-- Move action | |
this.NextActionMoveDirection = this.CurrentWaypointPosition - this.CurrentHumanoidPosition | |
if this.NextActionMoveDirection.Magnitude > ALMOST_ZERO then | |
this.NextActionMoveDirection = this.NextActionMoveDirection.Unit | |
else | |
this.NextActionMoveDirection = ZERO_VECTOR3 | |
end | |
-- Jump action | |
if this.CurrentWaypointNeedsJump then | |
this.NextActionJump = true | |
this.CurrentWaypointNeedsJump = false -- Request jump only once | |
else | |
this.NextActionJump = false | |
end | |
end | |
end | |
end | |
function this:IsCurrentWaypointReached() | |
local reached = false | |
-- Check we do have a plane, if not, we consider the waypoint reached | |
if this.CurrentWaypointPlaneNormal ~= ZERO_VECTOR3 then | |
-- Compute distance of Humanoid from destination plane | |
local dist = this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidPosition) - this.CurrentWaypointPlaneDistance | |
-- Compute the component of the Humanoid velocity that is towards the plane | |
local velocity = -this.CurrentWaypointPlaneNormal:Dot(this.CurrentHumanoidVelocity) | |
-- Compute the threshold from the destination plane based on Humanoid velocity | |
local threshold = math.max(1.0, 0.0625 * velocity) | |
-- If we are less then threshold in front of the plane (between 0 and threshold) or if we are behing the plane (less then 0), we consider we reached it | |
reached = dist < threshold | |
else | |
reached = true | |
end | |
if reached then | |
this.CurrentWaypointPosition = nil | |
this.CurrentWaypointPlaneNormal = ZERO_VECTOR3 | |
this.CurrentWaypointPlaneDistance = 0 | |
end | |
return reached | |
end | |
function this:OnPointReached(reached) | |
if reached and not this.Cancelled then | |
-- First, destroyed the current displayed waypoint | |
if this.setPointFunc then | |
this.setPointFunc(this.CurrentPoint) | |
end | |
local nextWaypointIdx = this.CurrentPoint + 1 | |
if nextWaypointIdx > #this.pointList then | |
-- End of path reached | |
if this.stopTraverseFunc then | |
this.stopTraverseFunc() | |
end | |
this.Finished:Fire() | |
this:Cleanup() | |
else | |
local currentWaypoint = this.pointList[this.CurrentPoint] | |
local nextWaypoint = this.pointList[nextWaypointIdx] | |
-- If airborne, only allow to keep moving | |
-- if nextWaypoint.Action ~= Jump, or path mantains a direction | |
-- Otherwise, wait until the humanoid gets to the ground | |
local currentState = this.Humanoid:GetState() | |
local isInAir = currentState == Enum.HumanoidStateType.FallingDown | |
or currentState == Enum.HumanoidStateType.Freefall | |
or currentState == Enum.HumanoidStateType.Jumping | |
if isInAir then | |
local shouldWaitForGround = nextWaypoint.Action == Enum.PathWaypointAction.Jump | |
if not shouldWaitForGround and this.CurrentPoint > 1 then | |
local prevWaypoint = this.pointList[this.CurrentPoint - 1] | |
local prevDir = currentWaypoint.Position - prevWaypoint.Position | |
local currDir = nextWaypoint.Position - currentWaypoint.Position | |
local prevDirXZ = Vector2.new(prevDir.x, prevDir.z).Unit | |
local currDirXZ = Vector2.new(currDir.x, currDir.z).Unit | |
local THRESHOLD_COS = 0.996 -- ~cos(5 degrees) | |
shouldWaitForGround = prevDirXZ:Dot(currDirXZ) < THRESHOLD_COS | |
end | |
if shouldWaitForGround then | |
this.Humanoid.FreeFalling:Wait() | |
-- Give time to the humanoid's state to change | |
-- Otherwise, the jump flag in Humanoid | |
-- will be reset by the state change | |
wait(0.1) | |
end | |
end | |
-- Move to the next point | |
if FFlagUserNavigationClickToMoveSkipPassedWaypoints then | |
this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx) | |
else | |
if this.setPointFunc then | |
this.setPointFunc(nextWaypointIdx) | |
end | |
if nextWaypoint.Action == Enum.PathWaypointAction.Jump then | |
this.Humanoid.Jump = true | |
end | |
this.Humanoid:MoveTo(nextWaypoint.Position) | |
this.CurrentPoint = nextWaypointIdx | |
end | |
end | |
else | |
this.PathFailed:Fire() | |
this:Cleanup() | |
end | |
end | |
function this:MoveToNextWayPoint(currentWaypoint, nextWaypoint, nextWaypointIdx) | |
-- Build next destination plane | |
-- (plane normal is perpendicular to the y plane and is from next waypoint towards current one (provided the two waypoints are not at the same location)) | |
-- (plane location is at next waypoint) | |
this.CurrentWaypointPlaneNormal = currentWaypoint.Position - nextWaypoint.Position | |
this.CurrentWaypointPlaneNormal = Vector3.new(this.CurrentWaypointPlaneNormal.X, 0, this.CurrentWaypointPlaneNormal.Z) | |
if this.CurrentWaypointPlaneNormal.Magnitude > ALMOST_ZERO then | |
this.CurrentWaypointPlaneNormal = this.CurrentWaypointPlaneNormal.Unit | |
this.CurrentWaypointPlaneDistance = this.CurrentWaypointPlaneNormal:Dot(nextWaypoint.Position) | |
else | |
-- Next waypoint is the same as current waypoint so no plane | |
this.CurrentWaypointPlaneNormal = ZERO_VECTOR3 | |
this.CurrentWaypointPlaneDistance = 0 | |
end | |
-- Should we jump | |
this.CurrentWaypointNeedsJump = nextWaypoint.Action == Enum.PathWaypointAction.Jump; | |
-- Remember next waypoint position | |
this.CurrentWaypointPosition = nextWaypoint.Position | |
-- Move to next point | |
this.CurrentPoint = nextWaypointIdx | |
-- Finally reset Timeout | |
this.Timeout = 0 | |
end | |
function this:Start(overrideShowPath) | |
if not this.AgentCanFollowPath then | |
this.PathFailed:Fire() | |
return | |
end | |
if this.Started then return end | |
this.Started = true | |
ClickToMoveDisplay.CancelFailureAnimation() | |
if ShowPath then | |
if overrideShowPath == nil or overrideShowPath then | |
this.stopTraverseFunc, this.setPointFunc = ClickToMoveDisplay.CreatePathDisplay(this.pointList, this.OriginalTargetPoint) | |
end | |
end | |
if #this.pointList > 0 then | |
-- Determine the humanoid offset from the path's first point | |
-- Offset of the first waypoint from the path's origin point | |
this.HumanoidOffsetFromPath = Vector3.new(0, this.pointList[1].Position.Y - this.OriginPoint.Y, 0) | |
-- As well as its current position and velocity | |
this.CurrentHumanoidPosition = this.Humanoid.RootPart.Position + this.HumanoidOffsetFromPath | |
this.CurrentHumanoidVelocity = this.Humanoid.RootPart.Velocity | |
-- Connect to events | |
this.SeatedConn = this.Humanoid.Seated:Connect(function(isSeated, seat) this:OnPathInterrupted() end) | |
this.DiedConn = this.Humanoid.Died:Connect(function() this:OnPathInterrupted() end) | |
this.TeleportedConn = this.Humanoid.RootPart:GetPropertyChangedSignal("CFrame"):Connect(function() this:OnPathInterrupted() end) | |
-- Actually start | |
this.CurrentPoint = 1 -- The first waypoint is always the start location. Skip it. | |
this:OnPointReached(true) -- Move to first point | |
else | |
this.PathFailed:Fire() | |
if this.stopTraverseFunc then | |
this.stopTraverseFunc() | |
end | |
end | |
end | |
--We always raycast to the ground in the case that the user clicked a wall. | |
local offsetPoint = this.TargetPoint + this.TargetSurfaceNormal*1.5 | |
local ray = Ray.new(offsetPoint, Vector3.new(0,-1,0)*50) | |
local newHitPart, newHitPos = Workspace:FindPartOnRayWithIgnoreList(ray, getIgnoreList()) | |
if newHitPart then | |
this.TargetPoint = newHitPos | |
end | |
this:ComputePath() | |
return this | |
end | |
------------------------------------------------------------------------- | |
local function CheckAlive() | |
local humanoid = findPlayerHumanoid(Player) | |
return humanoid ~= nil and humanoid.Health > 0 | |
end | |
local function GetEquippedTool(character) | |
if character ~= nil then | |
for _, child in pairs(character:GetChildren()) do | |
if child:IsA('Tool') then | |
return child | |
end | |
end | |
end | |
end | |
local ExistingPather = nil | |
local ExistingIndicator = nil | |
local PathCompleteListener = nil | |
local PathFailedListener = nil | |
local function CleanupPath() | |
if ExistingPather then | |
ExistingPather:Cancel() | |
ExistingPather = nil | |
end | |
if PathCompleteListener then | |
PathCompleteListener:Disconnect() | |
PathCompleteListener = nil | |
end | |
if PathFailedListener then | |
PathFailedListener:Disconnect() | |
PathFailedListener = nil | |
end | |
if ExistingIndicator then | |
ExistingIndicator:Destroy() | |
end | |
end | |
local function HandleMoveTo(thisPather, hitPt, hitChar, character, overrideShowPath) | |
if ExistingPather then | |
CleanupPath() | |
end | |
ExistingPather = thisPather | |
thisPather:Start(overrideShowPath) | |
PathCompleteListener = thisPather.Finished.Event:Connect(function() | |
CleanupPath() | |
if hitChar then | |
local currentWeapon = GetEquippedTool(character) | |
if currentWeapon then | |
currentWeapon:Activate() | |
end | |
end | |
end) | |
PathFailedListener = thisPather.PathFailed.Event:Connect(function() | |
CleanupPath() | |
if overrideShowPath == nil or overrideShowPath then | |
local shouldPlayFailureAnim = PlayFailureAnimation and not (ExistingPather and ExistingPather:IsActive()) | |
if shouldPlayFailureAnim then | |
ClickToMoveDisplay.PlayFailureAnimation() | |
end | |
ClickToMoveDisplay.DisplayFailureWaypoint(hitPt) | |
end | |
end) | |
end | |
local function ShowPathFailedFeedback(hitPt) | |
if ExistingPather and ExistingPather:IsActive() then | |
ExistingPather:Cancel() | |
end | |
if PlayFailureAnimation then | |
ClickToMoveDisplay.PlayFailureAnimation() | |
end | |
ClickToMoveDisplay.DisplayFailureWaypoint(hitPt) | |
end | |
function OnTap(tapPositions, goToPoint, wasTouchTap) | |
-- Good to remember if this is the latest tap event | |
local camera = Workspace.CurrentCamera | |
local character = Player.Character | |
if not CheckAlive() then return end | |
-- This is a path tap position | |
if #tapPositions == 1 or goToPoint then | |
if camera then | |
local unitRay = camera:ScreenPointToRay(tapPositions[1].x, tapPositions[1].y) | |
local ray = Ray.new(unitRay.Origin, unitRay.Direction*1000) | |
local myHumanoid = findPlayerHumanoid(Player) | |
local hitPart, hitPt, hitNormal = Utility.Raycast(ray, true, getIgnoreList()) | |
local hitChar, hitHumanoid = Utility.FindCharacterAncestor(hitPart) | |
if wasTouchTap and hitHumanoid and StarterGui:GetCore("AvatarContextMenuEnabled") then | |
local clickedPlayer = Players:GetPlayerFromCharacter(hitHumanoid.Parent) | |
if clickedPlayer then | |
CleanupPath() | |
return | |
end | |
end | |
if goToPoint then | |
hitPt = goToPoint | |
hitChar = nil | |
end | |
if hitPt and character then | |
-- Clean up current path | |
CleanupPath() | |
local thisPather = Pather(hitPt, hitNormal) | |
if thisPather:IsValidPath() then | |
HandleMoveTo(thisPather, hitPt, hitChar, character) | |
else | |
-- Clean up | |
thisPather:Cleanup() | |
-- Feedback here for when we don't have a good path | |
ShowPathFailedFeedback(hitPt) | |
end | |
end | |
end | |
elseif #tapPositions >= 2 then | |
if camera then | |
-- Do shoot | |
local currentWeapon = GetEquippedTool(character) | |
if currentWeapon then | |
currentWeapon:Activate() | |
end | |
end | |
end | |
end | |
local function DisconnectEvent(event) | |
if event then | |
event:Disconnect() | |
end | |
end | |
--[[ The ClickToMove Controller Class ]]-- | |
local KeyboardController = _Keyboard() | |
local ClickToMove = setmetatable({}, KeyboardController) | |
ClickToMove.__index = ClickToMove | |
function ClickToMove.new(CONTROL_ACTION_PRIORITY) | |
local self = setmetatable(KeyboardController.new(CONTROL_ACTION_PRIORITY), ClickToMove) | |
self.fingerTouches = {} | |
self.numUnsunkTouches = 0 | |
-- PC simulation | |
self.mouse1Down = tick() | |
self.mouse1DownPos = Vector2.new() | |
self.mouse2DownTime = tick() | |
self.mouse2DownPos = Vector2.new() | |
self.mouse2UpTime = tick() | |
self.keyboardMoveVector = ZERO_VECTOR3 | |
self.tapConn = nil | |
self.inputBeganConn = nil | |
self.inputChangedConn = nil | |
self.inputEndedConn = nil | |
self.humanoidDiedConn = nil | |
self.characterChildAddedConn = nil | |
self.onCharacterAddedConn = nil | |
self.characterChildRemovedConn = nil | |
self.renderSteppedConn = nil | |
self.menuOpenedConnection = nil | |
self.running = false | |
self.wasdEnabled = false | |
return self | |
end | |
function ClickToMove:DisconnectEvents() | |
DisconnectEvent(self.tapConn) | |
DisconnectEvent(self.inputBeganConn) | |
DisconnectEvent(self.inputChangedConn) | |
DisconnectEvent(self.inputEndedConn) | |
DisconnectEvent(self.humanoidDiedConn) | |
DisconnectEvent(self.characterChildAddedConn) | |
DisconnectEvent(self.onCharacterAddedConn) | |
DisconnectEvent(self.renderSteppedConn) | |
DisconnectEvent(self.characterChildRemovedConn) | |
DisconnectEvent(self.menuOpenedConnection) | |
end | |
function ClickToMove:OnTouchBegan(input, processed) | |
if self.fingerTouches[input] == nil and not processed then | |
self.numUnsunkTouches = self.numUnsunkTouches + 1 | |
end | |
self.fingerTouches[input] = processed | |
end | |
function ClickToMove:OnTouchChanged(input, processed) | |
if self.fingerTouches[input] == nil then | |
self.fingerTouches[input] = processed | |
if not processed then | |
self.numUnsunkTouches = self.numUnsunkTouches + 1 | |
end | |
end | |
end | |
function ClickToMove:OnTouchEnded(input, processed) | |
if self.fingerTouches[input] ~= nil and self.fingerTouches[input] == false then | |
self.numUnsunkTouches = self.numUnsunkTouches - 1 | |
end | |
self.fingerTouches[input] = nil | |
end | |
function ClickToMove:OnCharacterAdded(character) | |
self:DisconnectEvents() | |
self.inputBeganConn = UserInputService.InputBegan:Connect(function(input, processed) | |
if input.UserInputType == Enum.UserInputType.Touch then | |
self:OnTouchBegan(input, processed) | |
end | |
-- Cancel path when you use the keyboard controls if wasd is enabled. | |
if self.wasdEnabled and processed == false and input.UserInputType == Enum.UserInputType.Keyboard | |
and movementKeys[input.KeyCode] then | |
CleanupPath() | |
ClickToMoveDisplay.CancelFailureAnimation() | |
end | |
if input.UserInputType == Enum.UserInputType.MouseButton1 then | |
self.mouse1DownTime = tick() | |
self.mouse1DownPos = input.Position | |
end | |
if input.UserInputType == Enum.UserInputType.MouseButton2 then | |
self.mouse2DownTime = tick() | |
self.mouse2DownPos = input.Position | |
end | |
end) | |
self.inputChangedConn = UserInputService.InputChanged:Connect(function(input, processed) | |
if input.UserInputType == Enum.UserInputType.Touch then | |
self:OnTouchChanged(input, processed) | |
end | |
end) | |
self.inputEndedConn = UserInputService.InputEnded:Connect(function(input, processed) | |
if input.UserInputType == Enum.UserInputType.Touch then | |
self:OnTouchEnded(input, processed) | |
end | |
if input.UserInputType == Enum.UserInputType.MouseButton2 then | |
self.mouse2UpTime = tick() | |
local currPos = input.Position | |
-- We allow click to move during path following or if there is no keyboard movement | |
local allowed = ExistingPather or self.keyboardMoveVector.Magnitude <= 0 | |
if self.mouse2UpTime - self.mouse2DownTime < 0.25 and (currPos - self.mouse2DownPos).magnitude < 5 and allowed then | |
local positions = {currPos} | |
OnTap(positions) | |
end | |
end | |
end) | |
self.tapConn = UserInputService.TouchTap:Connect(function(touchPositions, processed) | |
if not processed then | |
OnTap(touchPositions, nil, true) | |
end | |
end) | |
self.menuOpenedConnection = GuiService.MenuOpened:Connect(function() | |
CleanupPath() | |
end) | |
local function OnCharacterChildAdded(child) | |
if UserInputService.TouchEnabled then | |
if child:IsA('Tool') then | |
child.ManualActivationOnly = true | |
end | |
end | |
if child:IsA('Humanoid') then | |
DisconnectEvent(self.humanoidDiedConn) | |
self.humanoidDiedConn = child.Died:Connect(function() | |
if ExistingIndicator then | |
DebrisService:AddItem(ExistingIndicator.Model, 1) | |
end | |
end) | |
end | |
end | |
self.characterChildAddedConn = character.ChildAdded:Connect(function(child) | |
OnCharacterChildAdded(child) | |
end) | |
self.characterChildRemovedConn = character.ChildRemoved:Connect(function(child) | |
if UserInputService.TouchEnabled then | |
if child:IsA('Tool') then | |
child.ManualActivationOnly = false | |
end | |
end | |
end) | |
for _, child in pairs(character:GetChildren()) do | |
OnCharacterChildAdded(child) | |
end | |
end | |
function ClickToMove:Start() | |
self:Enable(true) | |
end | |
function ClickToMove:Stop() | |
self:Enable(false) | |
end | |
function ClickToMove:CleanupPath() | |
CleanupPath() | |
end | |
function ClickToMove:Enable(enable, enableWASD, touchJumpController) | |
if enable then | |
if not self.running then | |
if Player.Character then -- retro-listen | |
self:OnCharacterAdded(Player.Character) | |
end | |
self.onCharacterAddedConn = Player.CharacterAdded:Connect(function(char) | |
self:OnCharacterAdded(char) | |
end) | |
self.running = true | |
end | |
self.touchJumpController = touchJumpController | |
if self.touchJumpController then | |
self.touchJumpController:Enable(self.jumpEnabled) | |
end | |
else | |
if self.running then | |
self:DisconnectEvents() | |
CleanupPath() | |
-- Restore tool activation on shutdown | |
if UserInputService.TouchEnabled then | |
local character = Player.Character | |
if character then | |
for _, child in pairs(character:GetChildren()) do | |
if child:IsA('Tool') then | |
child.ManualActivationOnly = false | |
end | |
end | |
end | |
end | |
self.running = false | |
end | |
if self.touchJumpController and not self.jumpEnabled then | |
self.touchJumpController:Enable(true) | |
end | |
self.touchJumpController = nil | |
end | |
-- Extension for initializing Keyboard input as this class now derives from Keyboard | |
if UserInputService.KeyboardEnabled and enable ~= self.enabled then | |
self.forwardValue = 0 | |
self.backwardValue = 0 | |
self.leftValue = 0 | |
self.rightValue = 0 | |
self.moveVector = ZERO_VECTOR3 | |
if enable then | |
self:BindContextActions() | |
self:ConnectFocusEventListeners() | |
else | |
self:UnbindContextActions() | |
self:DisconnectFocusEventListeners() | |
end | |
end | |
self.wasdEnabled = enable and enableWASD or false | |
self.enabled = enable | |
end | |
function ClickToMove:OnRenderStepped(dt) | |
-- Reset jump | |
self.isJumping = false | |
-- Handle Pather | |
if ExistingPather then | |
-- Let the Pather update | |
ExistingPather:OnRenderStepped(dt) | |
-- If we still have a Pather, set the resulting actions | |
if ExistingPather then | |
-- Setup move (NOT relative to camera) | |
self.moveVector = ExistingPather.NextActionMoveDirection | |
self.moveVectorIsCameraRelative = false | |
-- Setup jump (but do NOT prevent the base Keayboard class from requesting jumps as well) | |
if ExistingPather.NextActionJump then | |
self.isJumping = true | |
end | |
else | |
self.moveVector = self.keyboardMoveVector | |
self.moveVectorIsCameraRelative = true | |
end | |
else | |
self.moveVector = self.keyboardMoveVector | |
self.moveVectorIsCameraRelative = true | |
end | |
-- Handle Keyboard's jump | |
if self.jumpRequested then | |
self.isJumping = true | |
end | |
end | |
-- Overrides Keyboard:UpdateMovement(inputState) to conditionally consider self.wasdEnabled and let OnRenderStepped handle the movement | |
function ClickToMove:UpdateMovement(inputState) | |
if inputState == Enum.UserInputState.Cancel then | |
self.keyboardMoveVector = ZERO_VECTOR3 | |
elseif self.wasdEnabled then | |
self.keyboardMoveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue) | |
end | |
end | |
-- Overrides Keyboard:UpdateJump() because jump is handled in OnRenderStepped | |
function ClickToMove:UpdateJump() | |
-- Nothing to do (handled in OnRenderStepped) | |
end | |
--Public developer facing functions | |
function ClickToMove:SetShowPath(value) | |
ShowPath = value | |
end | |
function ClickToMove:GetShowPath() | |
return ShowPath | |
end | |
function ClickToMove:SetWaypointTexture(texture) | |
ClickToMoveDisplay.SetWaypointTexture(texture) | |
end | |
function ClickToMove:GetWaypointTexture() | |
return ClickToMoveDisplay.GetWaypointTexture() | |
end | |
function ClickToMove:SetWaypointRadius(radius) | |
ClickToMoveDisplay.SetWaypointRadius(radius) | |
end | |
function ClickToMove:GetWaypointRadius() | |
return ClickToMoveDisplay.GetWaypointRadius() | |
end | |
function ClickToMove:SetEndWaypointTexture(texture) | |
ClickToMoveDisplay.SetEndWaypointTexture(texture) | |
end | |
function ClickToMove:GetEndWaypointTexture() | |
return ClickToMoveDisplay.GetEndWaypointTexture() | |
end | |
function ClickToMove:SetWaypointsAlwaysOnTop(alwaysOnTop) | |
ClickToMoveDisplay.SetWaypointsAlwaysOnTop(alwaysOnTop) | |
end | |
function ClickToMove:GetWaypointsAlwaysOnTop() | |
return ClickToMoveDisplay.GetWaypointsAlwaysOnTop() | |
end | |
function ClickToMove:SetFailureAnimationEnabled(enabled) | |
PlayFailureAnimation = enabled | |
end | |
function ClickToMove:GetFailureAnimationEnabled() | |
return PlayFailureAnimation | |
end | |
function ClickToMove:SetIgnoredPartsTag(tag) | |
UpdateIgnoreTag(tag) | |
end | |
function ClickToMove:GetIgnoredPartsTag() | |
return CurrentIgnoreTag | |
end | |
function ClickToMove:SetUseDirectPath(directPath) | |
UseDirectPath = directPath | |
end | |
function ClickToMove:GetUseDirectPath() | |
return UseDirectPath | |
end | |
function ClickToMove:SetAgentSizeIncreaseFactor(increaseFactorPercent) | |
AgentSizeIncreaseFactor = 1.0 + (increaseFactorPercent / 100.0) | |
end | |
function ClickToMove:GetAgentSizeIncreaseFactor() | |
return (AgentSizeIncreaseFactor - 1.0) * 100.0 | |
end | |
function ClickToMove:SetUnreachableWaypointTimeout(timeoutInSec) | |
UnreachableWaypointTimeout = timeoutInSec | |
end | |
function ClickToMove:GetUnreachableWaypointTimeout() | |
return UnreachableWaypointTimeout | |
end | |
function ClickToMove:SetUserJumpEnabled(jumpEnabled) | |
self.jumpEnabled = jumpEnabled | |
if self.touchJumpController then | |
self.touchJumpController:Enable(jumpEnabled) | |
end | |
end | |
function ClickToMove:GetUserJumpEnabled() | |
return self.jumpEnabled | |
end | |
function ClickToMove:MoveTo(position, showPath, useDirectPath) | |
local character = Player.Character | |
if character == nil then | |
return false | |
end | |
local thisPather = Pather(position, Vector3.new(0, 1, 0), useDirectPath) | |
if thisPather and thisPather:IsValidPath() then | |
HandleMoveTo(thisPather, position, nil, character, showPath) | |
return true | |
end | |
return false | |
end | |
return ClickToMove | |
end | |
function _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 = _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 | |
self.moveVector = ZERO_VECTOR3 | |
self.isJumping = false | |
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 = ZERO_VECTOR3 | |
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 | |
end | |
function _DynamicThumbstick() | |
local ZERO_VECTOR3 = Vector3.new(0,0,0) | |
local TOUCH_CONTROLS_SHEET = "rbxasset://textures/ui/Input/TouchControlsSheetV2.png" | |
local DYNAMIC_THUMBSTICK_ACTION_NAME = "DynamicThumbstickAction" | |
local DYNAMIC_THUMBSTICK_ACTION_PRIORITY = Enum.ContextActionPriority.High.Value | |
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 ContextActionService = game:GetService("ContextActionService") | |
local RunService = game:GetService("RunService") | |
local TweenService = game:GetService("TweenService") | |
local LocalPlayer = Players.LocalPlayer | |
if not LocalPlayer then | |
Players:GetPropertyChangedSignal("LocalPlayer"):Wait() | |
LocalPlayer = Players.LocalPlayer | |
end | |
--[[ The Module ]]-- | |
local BaseCharacterController = _BaseCharacterController() | |
local DynamicThumbstick = setmetatable({}, BaseCharacterController) | |
DynamicThumbstick.__index = DynamicThumbstick | |
function DynamicThumbstick.new() | |
local self = setmetatable(BaseCharacterController.new(), DynamicThumbstick) | |
self.moveTouchObject = nil | |
self.moveTouchLockedIn = false | |
self.moveTouchFirstChanged = false | |
self.moveTouchStartPosition = nil | |
self.startImage = nil | |
self.endImage = nil | |
self.middleImages = {} | |
self.startImageFadeTween = nil | |
self.endImageFadeTween = nil | |
self.middleImageFadeTweens = {} | |
self.isFirstTouch = true | |
self.thumbstickFrame = 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 | |
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: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:BindContextActions() | |
else | |
ContextActionService:UnbindAction(DYNAMIC_THUMBSTICK_ACTION_NAME) | |
-- Disable | |
self:OnInputEnded() -- Cleanup | |
end | |
self.enabled = enable | |
self.thumbstickFrame.Visible = enable | |
end | |
-- Was called OnMoveTouchEnded in previous version | |
function DynamicThumbstick:OnInputEnded() | |
self.moveTouchObject = nil | |
self.moveVector = ZERO_VECTOR3 | |
self:FadeThumbstick(false) | |
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:InputInFrame(inputObject) | |
local frameCornerTopLeft = self.thumbstickFrame.AbsolutePosition | |
local frameCornerBottomRight = frameCornerTopLeft + self.thumbstickFrame.AbsoluteSize | |
local inputPosition = inputObject.Position | |
if inputPosition.X >= frameCornerTopLeft.X and inputPosition.Y >= frameCornerTopLeft.Y then | |
if inputPosition.X <= frameCornerBottomRight.X and inputPosition.Y <= frameCornerBottomRight.Y then | |
return true | |
end | |
end | |
return false | |
end | |
function DynamicThumbstick:DoFadeInBackground() | |
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 = 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 | |
function DynamicThumbstick:DoMove(direction) | |
local currentMoveVector = direction | |
-- Scaled Radial Dead Zone | |
local inputAxisMagnitude = currentMoveVector.magnitude | |
if inputAxisMagnitude < self.radiusOfDeadZone then | |
currentMoveVector = ZERO_VECTOR3 | |
else | |
currentMoveVector = currentMoveVector.unit*( | |
1 - math.max(0, (self.radiusOfMaxSpeed - currentMoveVector.magnitude)/self.radiusOfMaxSpeed) | |
) | |
currentMoveVector = Vector3.new(currentMoveVector.x, 0, currentMoveVector.y) | |
end | |
self.moveVector = currentMoveVector | |
end | |
function DynamicThumbstick:LayoutMiddleImages(startPos, endPos) | |
local startDist = (self.thumbstickSize / 2) + self.middleSize | |
local vector = endPos - startPos | |
local distAvailable = vector.magnitude - (self.thumbstickRingSize / 2) - self.middleSize | |
local direction = vector.unit | |
local distNeeded = self.middleSpacing * NUM_MIDDLE_IMAGES | |
local spacing = self.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, self.middleSize * exposedFraction, 0, self.middleSize * exposedFraction) | |
else | |
image.Visible = false | |
end | |
end | |
end | |
function DynamicThumbstick:MoveStick(pos) | |
local vector2StartPosition = Vector2.new(self.moveTouchStartPosition.X, self.moveTouchStartPosition.Y) | |
local startPos = vector2StartPosition - 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) | |
self:LayoutMiddleImages(startPos, endPos) | |
end | |
function DynamicThumbstick:BindContextActions() | |
local function inputBegan(inputObject) | |
if self.moveTouchObject then | |
return Enum.ContextActionResult.Pass | |
end | |
if not self:InputInFrame(inputObject) then | |
return Enum.ContextActionResult.Pass | |
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, self.thumbstickSize, 0, self.thumbstickSize), ImageColor3 = Color3.new(0,0,0)} | |
):Play() | |
end | |
self.moveTouchLockedIn = false | |
self.moveTouchObject = inputObject | |
self.moveTouchStartPosition = inputObject.Position | |
self.moveTouchFirstChanged = true | |
if FADE_IN_OUT_BACKGROUND then | |
self:DoFadeInBackground() | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
local function inputChanged(inputObject) | |
if inputObject == self.moveTouchObject then | |
if self.moveTouchFirstChanged then | |
self.moveTouchFirstChanged = false | |
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) | |
self:MoveStick(inputObject.Position) | |
end | |
self.moveTouchLockedIn = true | |
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 | |
self:DoMove(direction) | |
self:MoveStick(inputObject.Position) | |
end | |
return Enum.ContextActionResult.Sink | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
local function inputEnded(inputObject) | |
if inputObject == self.moveTouchObject then | |
self:OnInputEnded() | |
if self.moveTouchLockedIn then | |
return Enum.ContextActionResult.Sink | |
end | |
end | |
return Enum.ContextActionResult.Pass | |
end | |
local function handleInput(actionName, inputState, inputObject) | |
if inputState == Enum.UserInputState.Begin then | |
return inputBegan(inputObject) | |
elseif inputState == Enum.UserInputState.Change then | |
return inputChanged(inputObject) | |
elseif inputState == Enum.UserInputState.End then | |
return inputEnded(inputObject) | |
elseif inputState == Enum.UserInputState.Cancel then | |
self:OnInputEnded() | |
end | |
end | |
ContextActionService:BindActionAtPriority( | |
DYNAMIC_THUMBSTICK_ACTION_NAME, | |
handleInput, | |
false, | |
DYNAMIC_THUMBSTICK_ACTION_PRIORITY, | |
Enum.UserInputType.Touch) | |
end | |
function DynamicThumbstick:Create(parentFrame) | |
if self.thumbstickFrame then | |
self.thumbstickFrame:Destroy() | |
self.thumbstickFrame = nil | |
if self.onRenderSteppedConn then | |
self.onRenderSteppedConn:Disconnect() | |
self.onRenderSteppedConn = nil | |
end | |
end | |
self.thumbstickSize = 45 | |
self.thumbstickRingSize = 20 | |
self.middleSize = 10 | |
self.middleSpacing = self.middleSize + 4 | |
self.radiusOfDeadZone = 2 | |
self.radiusOfMaxSpeed = 20 | |
local screenSize = parentFrame.AbsoluteSize | |
local isBigScreen = math.min(screenSize.x, screenSize.y) > 500 | |
if isBigScreen then | |
self.thumbstickSize = self.thumbstickSize * 2 | |
self.thumbstickRingSize = self.thumbstickRingSize * 2 | |
self.middleSize = self.middleSize * 2 | |
self.middleSpacing = self.middleSpacing * 2 | |
self.radiusOfDeadZone = self.radiusOfDeadZone * 2 | |
self.radiusOfMaxSpeed = self.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("Frame") | |
self.thumbstickFrame.BorderSizePixel = 0 | |
self.thumbstickFrame.Name = "DynamicThumbstickFrame" | |
self.thumbstickFrame.Visible = false | |
self.thumbstickFrame.BackgroundTransparency = 1.0 | |
self.thumbstickFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0) | |
self.thumbstickFrame.Active = false | |
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, self.thumbstickRingSize * 3.3, 1, -self.thumbstickRingSize * 2.8) | |
self.startImage.Size = UDim2.new(0, self.thumbstickRingSize * 3.7, 0, self.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, self.thumbstickSize * 0.8, 0, self.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 = {} | |
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 = 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() | |
self.fadeInAndOutHalfDuration = 2.5 | |
self.fadeInAndOutBalance = 0.05 | |
self.tweenInAlphaStart = tick() | |
end | |
playerGuiChangedConn = playerGui:GetPropertyChangedSignal("CurrentScreenOrientation"):Connect(function() | |
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) | |
self.thumbstickFrame.Parent = parentFrame | |
if game:IsLoaded() then | |
longShowBackground() | |
else | |
coroutine.wrap(function() | |
game.Loaded:Wait() | |
longShowBackground() | |
end)() | |
end | |
end | |
return DynamicThumbstick | |
end | |
function _Gamepad() | |
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 = _BaseCharacterController() | |
local Gamepad = setmetatable({}, BaseCharacterController) | |
Gamepad.__index = Gamepad | |
function Gamepad.new(CONTROL_ACTION_PRIORITY) | |
local self = setmetatable(BaseCharacterController.new(), Gamepad) | |
self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY | |
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 | |
self.isJumping = false | |
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 handleJumpAction = function(actionName, inputState, inputObject) | |
self.isJumping = (inputState == Enum.UserInputState.Begin) | |
return Enum.ContextActionResult.Sink | |
end | |
local handleThumbstickInput = function(actionName, inputState, inputObject) | |
if inputState == Enum.UserInputState.Cancel then | |
self.moveVector = ZERO_VECTOR3 | |
return Enum.ContextActionResult.Sink | |
end | |
if self.activeGamepad ~= inputObject.UserInputType then | |
return Enum.ContextActionResult.Pass | |
end | |
if inputObject.KeyCode ~= Enum.KeyCode.Thumbstick1 then 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 | |
return Enum.ContextActionResult.Sink | |
end | |
ContextActionService:BindActivate(self.activeGamepad, Enum.KeyCode.ButtonR2) | |
ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false, | |
self.CONTROL_ACTION_PRIORITY, Enum.KeyCode.ButtonA) | |
ContextActionService:BindActionAtPriority("moveThumbstick", handleThumbstickInput, false, | |
self.CONTROL_ACTION_PRIORITY, 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 | |
self:UnbindContextActions() | |
end | |
self.activeGamepad = bestGamepad | |
self:BindContextActions() | |
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 | |
end | |
function _Keyboard() | |
--[[ 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 = _BaseCharacterController() | |
local Keyboard = setmetatable({}, BaseCharacterController) | |
Keyboard.__index = Keyboard | |
function Keyboard.new(CONTROL_ACTION_PRIORITY) | |
local self = setmetatable(BaseCharacterController.new(), Keyboard) | |
self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY | |
self.textFocusReleasedConn = nil | |
self.textFocusGainedConn = nil | |
self.windowFocusReleasedConn = nil | |
self.forwardValue = 0 | |
self.backwardValue = 0 | |
self.leftValue = 0 | |
self.rightValue = 0 | |
self.jumpEnabled = true | |
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 | |
self.moveVector = ZERO_VECTOR3 | |
self.jumpRequested = false | |
self:UpdateJump() | |
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:UpdateJump() | |
self.isJumping = self.jumpRequested | |
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. | |
-- We return ContextActionResult.Pass here for legacy reasons. | |
-- Many games rely on gameProcessedEvent being false on UserInputService.InputBegan for these control actions. | |
local handleMoveForward = function(actionName, inputState, inputObject) | |
self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 | |
self:UpdateMovement(inputState) | |
return Enum.ContextActionResult.Pass | |
end | |
local handleMoveBackward = function(actionName, inputState, inputObject) | |
self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 | |
self:UpdateMovement(inputState) | |
return Enum.ContextActionResult.Pass | |
end | |
local handleMoveLeft = function(actionName, inputState, inputObject) | |
self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0 | |
self:UpdateMovement(inputState) | |
return Enum.ContextActionResult.Pass | |
end | |
local handleMoveRight = function(actionName, inputState, inputObject) | |
self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0 | |
self:UpdateMovement(inputState) | |
return Enum.ContextActionResult.Pass | |
end | |
local handleJumpAction = function(actionName, inputState, inputObject) | |
self.jumpRequested = self.jumpEnabled and (inputState == Enum.UserInputState.Begin) | |
self:UpdateJump() | |
return Enum.ContextActionResult.Pass | |
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:BindActionAtPriority("moveForwardAction", handleMoveForward, false, | |
self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterForward) | |
ContextActionService:BindActionAtPriority("moveBackwardAction", handleMoveBackward, false, | |
self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterBackward) | |
ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false, | |
self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft) | |
ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false, | |
self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight) | |
ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false, | |
self.CONTROL_ACTION_PRIORITY, 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.jumpRequested = false | |
self:UpdateJump() | |
end | |
local function onTextFocusGained(textboxFocused) | |
self.jumpRequested = false | |
self:UpdateJump() | |
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 | |
end | |
function _ControlModule() | |
local ControlModule = {} | |
ControlModule.__index = ControlModule | |
--[[ Roblox Services ]]-- | |
local Players = game:GetService("Players") | |
local RunService = game:GetService("RunService") | |
local UserInputService = game:GetService("UserInputService") | |
local Workspace = game:GetService("Workspace") | |
local UserGameSettings = UserSettings():GetService("UserGameSettings") | |
-- Roblox User Input Control Modules - each returns a new() constructor function used to create controllers as needed | |
local Keyboard = _Keyboard() | |
local Gamepad = _Gamepad() | |
local DynamicThumbstick = _DynamicThumbstick() | |
local FFlagUserMakeThumbstickDynamic do | |
local success, value = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserMakeThumbstickDynamic") | |
end) | |
FFlagUserMakeThumbstickDynamic = success and value | |
end | |
local TouchThumbstick = FFlagUserMakeThumbstickDynamic and DynamicThumbstick or _TouchThumbstick() | |
-- These controllers handle only walk/run movement, jumping is handled by the | |
-- TouchJump controller if any of these are active | |
local ClickToMove = _ClickToMoveController() | |
local TouchJump = _TouchJump() | |
local VehicleController = _VehicleController() | |
local CONTROL_ACTION_PRIORITY = Enum.ContextActionPriority.Default.Value | |
-- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching | |
local movementEnumToModuleMap = { | |
[Enum.TouchMovementMode.DPad] = DynamicThumbstick, | |
[Enum.DevTouchMovementMode.DPad] = DynamicThumbstick, | |
[Enum.TouchMovementMode.Thumbpad] = DynamicThumbstick, | |
[Enum.DevTouchMovementMode.Thumbpad] = DynamicThumbstick, | |
[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] = DynamicThumbstick, | |
[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, | |
} | |
local lastInputType | |
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 | |
-- For Roblox self.vehicleController | |
self.humanoidSeatedConn = nil | |
self.vehicleController = nil | |
self.touchControlFrame = nil | |
self.vehicleController = VehicleController.new(CONTROL_ACTION_PRIORITY) | |
Players.LocalPlayer.CharacterAdded:Connect(function(char) self:OnCharacterAdded(char) end) | |
Players.LocalPlayer.CharacterRemoving:Connect(function(char) self:OnCharacterRemoving(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) | |
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 Vector3.new(0,0,0) | |
end | |
function ControlModule:GetActiveController() | |
return self.activeController | |
end | |
function ControlModule:EnableActiveControlModule() | |
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 jump. | |
self.activeController:Enable( | |
true, | |
Players.LocalPlayer.DevComputerMovementMode == Enum.DevComputerMovementMode.UserChoice, | |
self.touchJumpController | |
) | |
elseif self.touchControlFrame then | |
self.activeController:Enable(true, self.touchControlFrame) | |
else | |
self.activeController:Enable(true) | |
end | |
end | |
function ControlModule:Enable(enable) | |
if not self.activeController then | |
return | |
end | |
if enable == nil then | |
enable = true | |
end | |
if enable then | |
self:EnableActiveControlModule() | |
else | |
self:Disable() | |
end | |
end | |
-- For those who prefer distinct functions | |
function ControlModule:Disable() | |
if self.activeController then | |
self.activeController:Enable(false) | |
if self.moveFunction then | |
self.moveFunction(Players.LocalPlayer, Vector3.new(0,0,0), true) | |
end | |
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 | |
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 | |
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 | |
local function calculateRawMoveVector(humanoid, cameraRelativeMoveVector) | |
local camera = Workspace.CurrentCamera | |
if not camera then | |
return cameraRelativeMoveVector | |
end | |
if humanoid:GetState() == Enum.HumanoidStateType.Swimming then | |
return camera.CFrame:VectorToWorldSpace(cameraRelativeMoveVector) | |
end | |
local c, s | |
local _, _, _, R00, R01, R02, _, _, R12, _, _, R22 = camera.CFrame:GetComponents() | |
if R12 < 1 and R12 > -1 then | |
-- X and Z components from back vector. | |
c = R22 | |
s = R02 | |
else | |
-- In this case the camera is looking straight up or straight down. | |
-- Use X components from right and up vectors. | |
c = R00 | |
s = -R01*math.sign(R12) | |
end | |
local norm = math.sqrt(c*c + s*s) | |
return Vector3.new( | |
(c*cameraRelativeMoveVector.x + s*cameraRelativeMoveVector.z)/norm, | |
0, | |
(c*cameraRelativeMoveVector.z - s*cameraRelativeMoveVector.x)/norm | |
) | |
end | |
function ControlModule:OnRenderStepped(dt) | |
if self.activeController and self.activeController.enabled and self.humanoid then | |
-- Give the controller a chance to adjust its state | |
self.activeController:OnRenderStepped(dt) | |
-- Now retrieve info from the controller | |
local moveVector = self.activeController:GetMoveVector() | |
local cameraRelative = self.activeController:IsMoveVectorCameraRelative() | |
local clickToMoveController = self:GetClickToMoveController() | |
if self.activeController ~= clickToMoveController then | |
if moveVector.magnitude > 0 then | |
-- Clean up any developer started MoveTo path | |
clickToMoveController:CleanupPath() | |
else | |
-- Get move vector for developer started MoveTo | |
clickToMoveController:OnRenderStepped(dt) | |
moveVector = clickToMoveController:GetMoveVector() | |
cameraRelative = clickToMoveController:IsMoveVectorCameraRelative() | |
end | |
end | |
-- Are we driving a vehicle ? | |
local vehicleConsumedInput = false | |
if self.vehicleController then | |
moveVector, vehicleConsumedInput = self.vehicleController:Update(moveVector, cameraRelative, self.activeControlModule==Gamepad) | |
end | |
-- If not, move the player | |
-- Verification 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 | |
if cameraRelative then | |
moveVector = calculateRawMoveVector(self.humanoid, moveVector) | |
end | |
self.moveFunction(Players.LocalPlayer, moveVector, false) | |
--end | |
-- And make them jump if needed | |
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(CONTROL_ACTION_PRIORITY) | |
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.touchGui then | |
self.touchGui.Enabled = true | |
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 | |
if self.touchGui then | |
self.touchGui.Enabled = false | |
end | |
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(CONTROL_ACTION_PRIORITY) | |
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 and (self.activeControlModule == ClickToMove | |
or self.activeControlModule == TouchThumbstick | |
or self.activeControlModule == DynamicThumbstick) then | |
if not self.controllers[TouchJump] then | |
self.controllers[TouchJump] = TouchJump.new() | |
end | |
self.touchJumpController = self.controllers[TouchJump] | |
self.touchJumpController:Enable(true, self.touchControlFrame) | |
else | |
if self.touchJumpController then | |
self.touchJumpController:Enable(false) | |
end | |
end | |
self:EnableActiveControlModule() | |
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.touchGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling | |
self.touchGui.Enabled = self.humanoid ~= nil | |
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 | |
function ControlModule:GetClickToMoveController() | |
if not self.controllers[ClickToMove] then | |
self.controllers[ClickToMove] = ClickToMove.new(CONTROL_ACTION_PRIORITY) | |
end | |
return self.controllers[ClickToMove] | |
end | |
function ControlModule:IsJumping() | |
if self.activeController then | |
return self.activeController:GetIsJumping() or (self.touchJumpController and self.touchJumpController:GetIsJumping()) | |
end | |
return false | |
end | |
return ControlModule.new() | |
end | |
function _PlayerModule() | |
local PlayerModule = {} | |
PlayerModule.__index = PlayerModule | |
function PlayerModule.new() | |
local self = setmetatable({},PlayerModule) | |
self.cameras = _CameraModule() | |
self.controls = _ControlModule() | |
return self | |
end | |
function PlayerModule:GetCameras() | |
return self.cameras | |
end | |
function PlayerModule:GetControls() | |
return self.controls | |
end | |
function PlayerModule:GetClickToMoveController() | |
return self.controls:GetClickToMoveController() | |
end | |
return PlayerModule.new() | |
end | |
function _sounds() | |
local SetState = Instance.new("BindableEvent",script) | |
local Players = game:GetService("Players") | |
local RunService = game:GetService("RunService") | |
local SOUND_DATA = { | |
Climbing = { | |
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3", | |
Looped = true, | |
}, | |
Died = { | |
SoundId = "rbxasset://sounds/uuhhh.mp3", | |
}, | |
FreeFalling = { | |
SoundId = "rbxasset://sounds/action_falling.mp3", | |
Looped = true, | |
}, | |
GettingUp = { | |
SoundId = "rbxasset://sounds/action_get_up.mp3", | |
}, | |
Jumping = { | |
SoundId = "rbxasset://sounds/action_jump.mp3", | |
}, | |
Landing = { | |
SoundId = "rbxasset://sounds/action_jump_land.mp3", | |
}, | |
Running = { | |
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3", | |
Looped = true, | |
Pitch = 1.85, | |
}, | |
Splash = { | |
SoundId = "rbxasset://sounds/impact_water.mp3", | |
}, | |
Swimming = { | |
SoundId = "rbxasset://sounds/action_swim.mp3", | |
Looped = true, | |
Pitch = 1.6, | |
}, | |
} | |
-- wait for the first of the passed signals to fire | |
local function waitForFirst(...) | |
local shunt = Instance.new("BindableEvent") | |
local slots = {...} | |
local function fire(...) | |
for i = 1, #slots do | |
slots[i]:Disconnect() | |
end | |
return shunt:Fire(...) | |
end | |
for i = 1, #slots do | |
slots[i] = slots[i]:Connect(fire) | |
end | |
return shunt.Event:Wait() | |
end | |
-- map a value from one range to another | |
local function map(x, inMin, inMax, outMin, outMax) | |
return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin | |
end | |
local function playSound(sound) | |
sound.TimePosition = 0 | |
sound.Playing = true | |
end | |
local function stopSound(sound) | |
sound.Playing = false | |
sound.TimePosition = 0 | |
end | |
local function shallowCopy(t) | |
local out = {} | |
for k, v in pairs(t) do | |
out[k] = v | |
end | |
return out | |
end | |
local function initializeSoundSystem(player, humanoid, rootPart) | |
local sounds = {} | |
-- initialize sounds | |
for name, props in pairs(SOUND_DATA) do | |
local sound = Instance.new("Sound") | |
sound.Name = name | |
-- set default values | |
sound.Archivable = false | |
sound.EmitterSize = 5 | |
sound.MaxDistance = 150 | |
sound.Volume = 0.65 | |
for propName, propValue in pairs(props) do | |
sound[propName] = propValue | |
end | |
sound.Parent = rootPart | |
sounds[name] = sound | |
end | |
local playingLoopedSounds = {} | |
local function stopPlayingLoopedSounds(except) | |
for sound in pairs(shallowCopy(playingLoopedSounds)) do | |
if sound ~= except then | |
sound.Playing = false | |
playingLoopedSounds[sound] = nil | |
end | |
end | |
end | |
-- state transition callbacks | |
local stateTransitions = { | |
[Enum.HumanoidStateType.FallingDown] = function() | |
stopPlayingLoopedSounds() | |
end, | |
[Enum.HumanoidStateType.GettingUp] = function() | |
stopPlayingLoopedSounds() | |
playSound(sounds.GettingUp) | |
end, | |
[Enum.HumanoidStateType.Jumping] = function() | |
stopPlayingLoopedSounds() | |
playSound(sounds.Jumping) | |
end, | |
[Enum.HumanoidStateType.Swimming] = function() | |
local verticalSpeed = math.abs(rootPart.Velocity.Y) | |
if verticalSpeed > 0.1 then | |
sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1) | |
playSound(sounds.Splash) | |
end | |
stopPlayingLoopedSounds(sounds.Swimming) | |
sounds.Swimming.Playing = true | |
playingLoopedSounds[sounds.Swimming] = true | |
end, | |
[Enum.HumanoidStateType.Freefall] = function() | |
sounds.FreeFalling.Volume = 0 | |
stopPlayingLoopedSounds(sounds.FreeFalling) | |
playingLoopedSounds[sounds.FreeFalling] = true | |
end, | |
[Enum.HumanoidStateType.Landed] = function() | |
stopPlayingLoopedSounds() | |
local verticalSpeed = math.abs(rootPart.Velocity.Y) | |
if verticalSpeed > 75 then | |
sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1) | |
playSound(sounds.Landing) | |
end | |
end, | |
[Enum.HumanoidStateType.Running] = function() | |
stopPlayingLoopedSounds(sounds.Running) | |
sounds.Running.Playing = true | |
playingLoopedSounds[sounds.Running] = true | |
end, | |
[Enum.HumanoidStateType.Climbing] = function() | |
local sound = sounds.Climbing | |
if math.abs(rootPart.Velocity.Y) > 0.1 then | |
sound.Playing = true | |
stopPlayingLoopedSounds(sound) | |
else | |
stopPlayingLoopedSounds() | |
end | |
playingLoopedSounds[sound] = true | |
end, | |
[Enum.HumanoidStateType.Seated] = function() | |
stopPlayingLoopedSounds() | |
end, | |
[Enum.HumanoidStateType.Dead] = function() | |
stopPlayingLoopedSounds() | |
playSound(sounds.Died) | |
end, | |
} | |
-- updaters for looped sounds | |
local loopedSoundUpdaters = { | |
[sounds.Climbing] = function(dt, sound, vel) | |
sound.Playing = vel.Magnitude > 0.1 | |
end, | |
[sounds.FreeFalling] = function(dt, sound, vel) | |
if vel.Magnitude > 75 then | |
sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1) | |
else | |
sound.Volume = 0 | |
end | |
end, | |
[sounds.Running] = function(dt, sound, vel) | |
sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5 | |
end, | |
} | |
-- state substitutions to avoid duplicating entries in the state table | |
local stateRemap = { | |
[Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running, | |
} | |
local activeState = stateRemap[humanoid:GetState()] or humanoid:GetState() | |
local activeConnections = {} | |
local stateChangedConn = humanoid.StateChanged:Connect(function(_, state) | |
state = stateRemap[state] or state | |
if state ~= activeState then | |
local transitionFunc = stateTransitions[state] | |
if transitionFunc then | |
transitionFunc() | |
end | |
activeState = state | |
end | |
end) | |
local customStateChangedConn = SetState.Event:Connect(function(state) | |
state = stateRemap[state] or state | |
if state ~= activeState then | |
local transitionFunc = stateTransitions[state] | |
if transitionFunc then | |
transitionFunc() | |
end | |
activeState = state | |
end | |
end) | |
local steppedConn = RunService.Stepped:Connect(function(_, worldDt) | |
-- update looped sounds on stepped | |
for sound in pairs(playingLoopedSounds) do | |
local updater = loopedSoundUpdaters[sound] | |
if updater then | |
updater(worldDt, sound, rootPart.Velocity) | |
end | |
end | |
end) | |
local humanoidAncestryChangedConn | |
local rootPartAncestryChangedConn | |
local characterAddedConn | |
local function terminate() | |
stateChangedConn:Disconnect() | |
customStateChangedConn:Disconnect() | |
steppedConn:Disconnect() | |
humanoidAncestryChangedConn:Disconnect() | |
rootPartAncestryChangedConn:Disconnect() | |
characterAddedConn:Disconnect() | |
end | |
humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(_, parent) | |
if not parent then | |
terminate() | |
end | |
end) | |
rootPartAncestryChangedConn = rootPart.AncestryChanged:Connect(function(_, parent) | |
if not parent then | |
terminate() | |
end | |
end) | |
characterAddedConn = player.CharacterAdded:Connect(terminate) | |
end | |
local function playerAdded(player) | |
local function characterAdded(character) | |
-- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications: | |
-- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically. | |
-- ** must use a waitForFirst on everything and listen for hierarchy changes. | |
-- * the character might not be in the dm by the time CharacterAdded fires | |
-- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again | |
-- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented. | |
-- * RootPart probably won't exist immediately. | |
-- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented. | |
if not character.Parent then | |
waitForFirst(character.AncestryChanged, player.CharacterAdded) | |
end | |
if player.Character ~= character or not character.Parent then | |
return | |
end | |
local humanoid = character:FindFirstChildOfClass("Humanoid") | |
while character:IsDescendantOf(game) and not humanoid do | |
waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded) | |
humanoid = character:FindFirstChildOfClass("Humanoid") | |
end | |
if player.Character ~= character or not character:IsDescendantOf(game) then | |
return | |
end | |
-- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals | |
local rootPart = character:FindFirstChild("HumanoidRootPart") | |
while character:IsDescendantOf(game) and not rootPart do | |
waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded) | |
rootPart = character:FindFirstChild("HumanoidRootPart") | |
end | |
if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then | |
initializeSoundSystem(player, humanoid, rootPart) | |
end | |
end | |
if player.Character then | |
characterAdded(player.Character) | |
end | |
player.CharacterAdded:Connect(characterAdded) | |
end | |
Players.PlayerAdded:Connect(playerAdded) | |
for _, player in ipairs(Players:GetPlayers()) do | |
playerAdded(player) | |
end | |
return SetState | |
end | |
function _StateTracker() | |
local EPSILON = 0.1 | |
local SPEED = { | |
["onRunning"] = true, | |
["onClimbing"] = true | |
} | |
local INAIR = { | |
["onFreeFall"] = true, | |
["onJumping"] = true | |
} | |
local STATEMAP = { | |
["onRunning"] = Enum.HumanoidStateType.Running, | |
["onJumping"] = Enum.HumanoidStateType.Jumping, | |
["onFreeFall"] = Enum.HumanoidStateType.Freefall | |
} | |
local StateTracker = {} | |
StateTracker.__index = StateTracker | |
function StateTracker.new(humanoid, soundState) | |
local self = setmetatable({}, StateTracker) | |
self.Humanoid = humanoid | |
self.HRP = humanoid.RootPart | |
self.Speed = 0 | |
self.State = "onRunning" | |
self.Jumped = false | |
self.JumpTick = tick() | |
self.SoundState = soundState | |
self._ChangedEvent = Instance.new("BindableEvent") | |
self.Changed = self._ChangedEvent.Event | |
return self | |
end | |
function StateTracker:Destroy() | |
self._ChangedEvent:Destroy() | |
end | |
function StateTracker:RequestedJump() | |
self.Jumped = true | |
self.JumpTick = tick() | |
end | |
function StateTracker:OnStep(gravityUp, grounded, isMoving) | |
local cVelocity = self.HRP.Velocity | |
local gVelocity = cVelocity:Dot(gravityUp) | |
local oldState, oldSpeed = self.State, self.Speed | |
local newState | |
local newSpeed = cVelocity.Magnitude | |
if (not grounded) then | |
if (gVelocity > 0) then | |
if (self.Jumped) then | |
newState = "onJumping" | |
else | |
newState = "onFreeFall" | |
end | |
else | |
if (self.Jumped) then | |
self.Jumped = false | |
end | |
newState = "onFreeFall" | |
end | |
else | |
if (self.Jumped and tick() - self.JumpTick > 0.1) then | |
self.Jumped = false | |
end | |
newSpeed = (cVelocity - gVelocity*gravityUp).Magnitude | |
newState = "onRunning" | |
end | |
newSpeed = isMoving and newSpeed or 0 | |
if (oldState ~= newState or (SPEED[newState] and math.abs(oldSpeed - newSpeed) > EPSILON)) then | |
self.State = newState | |
self.Speed = newSpeed | |
self.SoundState:Fire(STATEMAP[newState]) | |
self._ChangedEvent:Fire(self.State, self.Speed) | |
end | |
end | |
return StateTracker | |
end | |
function _InitObjects() | |
local model = workspace:FindFirstChild("objects") or game:GetObjects("rbxassetid://5045408489")[1] | |
local SPHERE = model:WaitForChild("Sphere") | |
local FLOOR = model:WaitForChild("Floor") | |
local VFORCE = model:WaitForChild("VectorForce") | |
local BGYRO = model:WaitForChild("BodyGyro") | |
local function initObjects(self) | |
local hrp = self.HRP | |
local humanoid = self.Humanoid | |
local sphere = SPHERE:Clone() | |
sphere.Parent = self.Character | |
local floor = FLOOR:Clone() | |
floor.Parent = self.Character | |
local isR15 = (humanoid.RigType == Enum.HumanoidRigType.R15) | |
local height = isR15 and (humanoid.HipHeight + 0.05) or 2 | |
local weld = Instance.new("Weld") | |
weld.C0 = CFrame.new(0, -height, 0.1) | |
weld.Part0 = hrp | |
weld.Part1 = sphere | |
weld.Parent = sphere | |
local weld2 = Instance.new("Weld") | |
weld2.C0 = CFrame.new(0, -(height + 1.5), 0) | |
weld2.Part0 = hrp | |
weld2.Part1 = floor | |
weld2.Parent = floor | |
local gyro = BGYRO:Clone() | |
gyro.CFrame = hrp.CFrame | |
gyro.Parent = hrp | |
local vForce = VFORCE:Clone() | |
vForce.Attachment0 = isR15 and hrp:WaitForChild("RootRigAttachment") or hrp:WaitForChild("RootAttachment") | |
vForce.Parent = hrp | |
return sphere, gyro, vForce, floor | |
end | |
return initObjects | |
end | |
local plr = game.Players.LocalPlayer | |
local ms = plr:GetMouse() | |
local char | |
plr.CharacterAdded:Connect(function(c) | |
char = c | |
end) | |
function _R6() | |
function r6() | |
local Figure = char | |
local Torso = Figure:WaitForChild("Torso") | |
local RightShoulder = Torso:WaitForChild("Right Shoulder") | |
local LeftShoulder = Torso:WaitForChild("Left Shoulder") | |
local RightHip = Torso:WaitForChild("Right Hip") | |
local LeftHip = Torso:WaitForChild("Left Hip") | |
local Neck = Torso:WaitForChild("Neck") | |
local Humanoid = Figure:WaitForChild("Humanoid") | |
local pose = "Standing" | |
local currentAnim = "" | |
local currentAnimInstance = nil | |
local currentAnimTrack = nil | |
local currentAnimKeyframeHandler = nil | |
local currentAnimSpeed = 1.0 | |
local animTable = {} | |
local animNames = { | |
idle = { | |
{ id = "http://www.roblox.com/asset/?id=180435571", weight = 9 }, | |
{ id = "http://www.roblox.com/asset/?id=180435792", weight = 1 } | |
}, | |
walk = { | |
{ id = "http://www.roblox.com/asset/?id=180426354", weight = 10 } | |
}, | |
run = { | |
{ id = "run.xml", weight = 10 } | |
}, | |
jump = { | |
{ id = "http://www.roblox.com/asset/?id=125750702", weight = 10 } | |
}, | |
fall = { | |
{ id = "http://www.roblox.com/asset/?id=180436148", weight = 10 } | |
}, | |
climb = { | |
{ id = "http://www.roblox.com/asset/?id=180436334", weight = 10 } | |
}, | |
sit = { | |
{ id = "http://www.roblox.com/asset/?id=178130996", weight = 10 } | |
}, | |
toolnone = { | |
{ id = "http://www.roblox.com/asset/?id=182393478", weight = 10 } | |
}, | |
toolslash = { | |
{ id = "http://www.roblox.com/asset/?id=129967390", weight = 10 } | |
-- { id = "slash.xml", weight = 10 } | |
}, | |
toollunge = { | |
{ id = "http://www.roblox.com/asset/?id=129967478", weight = 10 } | |
}, | |
wave = { | |
{ id = "http://www.roblox.com/asset/?id=128777973", weight = 10 } | |
}, | |
point = { | |
{ id = "http://www.roblox.com/asset/?id=128853357", weight = 10 } | |
}, | |
dance1 = { | |
{ id = "http://www.roblox.com/asset/?id=182435998", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=182491037", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=182491065", weight = 10 } | |
}, | |
dance2 = { | |
{ id = "http://www.roblox.com/asset/?id=182436842", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=182491248", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=182491277", weight = 10 } | |
}, | |
dance3 = { | |
{ id = "http://www.roblox.com/asset/?id=182436935", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=182491368", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=182491423", weight = 10 } | |
}, | |
laugh = { | |
{ id = "http://www.roblox.com/asset/?id=129423131", weight = 10 } | |
}, | |
cheer = { | |
{ id = "http://www.roblox.com/asset/?id=129423030", weight = 10 } | |
}, | |
} | |
local dances = {"dance1", "dance2", "dance3"} | |
-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote | |
local emoteNames = { wave = false, point = false, dance1 = true, dance2 = true, dance3 = true, laugh = false, cheer = false} | |
function configureAnimationSet(name, fileList) | |
if (animTable[name] ~= nil) then | |
for _, connection in pairs(animTable[name].connections) do | |
connection:disconnect() | |
end | |
end | |
animTable[name] = {} | |
animTable[name].count = 0 | |
animTable[name].totalWeight = 0 | |
animTable[name].connections = {} | |
-- check for config values | |
local config = script:FindFirstChild(name) | |
if (config ~= nil) then | |
-- print("Loading anims " .. name) | |
table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) | |
table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) | |
local idx = 1 | |
for _, childPart in pairs(config:GetChildren()) do | |
if (childPart:IsA("Animation")) then | |
table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) | |
animTable[name][idx] = {} | |
animTable[name][idx].anim = childPart | |
local weightObject = childPart:FindFirstChild("Weight") | |
if (weightObject == nil) then | |
animTable[name][idx].weight = 1 | |
else | |
animTable[name][idx].weight = weightObject.Value | |
end | |
animTable[name].count = animTable[name].count + 1 | |
animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight | |
-- print(name .. " [" .. idx .. "] " .. animTable[name][idx].anim.AnimationId .. " (" .. animTable[name][idx].weight .. ")") | |
idx = idx + 1 | |
end | |
end | |
end | |
-- fallback to defaults | |
if (animTable[name].count <= 0) then | |
for idx, anim in pairs(fileList) do | |
animTable[name][idx] = {} | |
animTable[name][idx].anim = Instance.new("Animation") | |
animTable[name][idx].anim.Name = name | |
animTable[name][idx].anim.AnimationId = anim.id | |
animTable[name][idx].weight = anim.weight | |
animTable[name].count = animTable[name].count + 1 | |
animTable[name].totalWeight = animTable[name].totalWeight + anim.weight | |
-- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")") | |
end | |
end | |
end | |
-- Setup animation objects | |
function scriptChildModified(child) | |
local fileList = animNames[child.Name] | |
if (fileList ~= nil) then | |
configureAnimationSet(child.Name, fileList) | |
end | |
end | |
script.ChildAdded:connect(scriptChildModified) | |
script.ChildRemoved:connect(scriptChildModified) | |
for name, fileList in pairs(animNames) do | |
configureAnimationSet(name, fileList) | |
end | |
-- ANIMATION | |
-- declarations | |
local toolAnim = "None" | |
local toolAnimTime = 0 | |
local jumpAnimTime = 0 | |
local jumpAnimDuration = 0.3 | |
local toolTransitionTime = 0.1 | |
local fallTransitionTime = 0.3 | |
local jumpMaxLimbVelocity = 0.75 | |
-- functions | |
function stopAllAnimations() | |
local oldAnim = currentAnim | |
-- return to idle if finishing an emote | |
if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then | |
oldAnim = "idle" | |
end | |
currentAnim = "" | |
currentAnimInstance = nil | |
if (currentAnimKeyframeHandler ~= nil) then | |
currentAnimKeyframeHandler:disconnect() | |
end | |
if (currentAnimTrack ~= nil) then | |
currentAnimTrack:Stop() | |
currentAnimTrack:Destroy() | |
currentAnimTrack = nil | |
end | |
return oldAnim | |
end | |
function setAnimationSpeed(speed) | |
if speed ~= currentAnimSpeed then | |
currentAnimSpeed = speed | |
currentAnimTrack:AdjustSpeed(currentAnimSpeed) | |
end | |
end | |
function keyFrameReachedFunc(frameName) | |
if (frameName == "End") then | |
local repeatAnim = currentAnim | |
-- return to idle if finishing an emote | |
if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then | |
repeatAnim = "idle" | |
end | |
local animSpeed = currentAnimSpeed | |
playAnimation(repeatAnim, 0.0, Humanoid) | |
setAnimationSpeed(animSpeed) | |
end | |
end | |
-- Preload animations | |
function playAnimation(animName, transitionTime, humanoid) | |
local roll = math.random(1, animTable[animName].totalWeight) | |
local origRoll = roll | |
local idx = 1 | |
while (roll > animTable[animName][idx].weight) do | |
roll = roll - animTable[animName][idx].weight | |
idx = idx + 1 | |
end | |
-- print(animName .. " " .. idx .. " [" .. origRoll .. "]") | |
local anim = animTable[animName][idx].anim | |
-- switch animation | |
if (anim ~= currentAnimInstance) then | |
if (currentAnimTrack ~= nil) then | |
currentAnimTrack:Stop(transitionTime) | |
currentAnimTrack:Destroy() | |
end | |
currentAnimSpeed = 1.0 | |
-- load it to the humanoid; get AnimationTrack | |
currentAnimTrack = humanoid:LoadAnimation(anim) | |
currentAnimTrack.Priority = Enum.AnimationPriority.Core | |
-- play the animation | |
currentAnimTrack:Play(transitionTime) | |
currentAnim = animName | |
currentAnimInstance = anim | |
-- set up keyframe name triggers | |
if (currentAnimKeyframeHandler ~= nil) then | |
currentAnimKeyframeHandler:disconnect() | |
end | |
currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) | |
end | |
end | |
------------------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------------------- | |
local toolAnimName = "" | |
local toolAnimTrack = nil | |
local toolAnimInstance = nil | |
local currentToolAnimKeyframeHandler = nil | |
function toolKeyFrameReachedFunc(frameName) | |
if (frameName == "End") then | |
-- print("Keyframe : ".. frameName) | |
playToolAnimation(toolAnimName, 0.0, Humanoid) | |
end | |
end | |
function playToolAnimation(animName, transitionTime, humanoid, priority) | |
local roll = math.random(1, animTable[animName].totalWeight) | |
local origRoll = roll | |
local idx = 1 | |
while (roll > animTable[animName][idx].weight) do | |
roll = roll - animTable[animName][idx].weight | |
idx = idx + 1 | |
end | |
-- print(animName .. " * " .. idx .. " [" .. origRoll .. "]") | |
local anim = animTable[animName][idx].anim | |
if (toolAnimInstance ~= anim) then | |
if (toolAnimTrack ~= nil) then | |
toolAnimTrack:Stop() | |
toolAnimTrack:Destroy() | |
transitionTime = 0 | |
end | |
-- load it to the humanoid; get AnimationTrack | |
toolAnimTrack = humanoid:LoadAnimation(anim) | |
if priority then | |
toolAnimTrack.Priority = priority | |
end | |
-- play the animation | |
toolAnimTrack:Play(transitionTime) | |
toolAnimName = animName | |
toolAnimInstance = anim | |
currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) | |
end | |
end | |
function stopToolAnimations() | |
local oldAnim = toolAnimName | |
if (currentToolAnimKeyframeHandler ~= nil) then | |
currentToolAnimKeyframeHandler:disconnect() | |
end | |
toolAnimName = "" | |
toolAnimInstance = nil | |
if (toolAnimTrack ~= nil) then | |
toolAnimTrack:Stop() | |
toolAnimTrack:Destroy() | |
toolAnimTrack = nil | |
end | |
return oldAnim | |
end | |
------------------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------------------- | |
function onRunning(speed) | |
if speed > 0.01 then | |
playAnimation("walk", 0.1, Humanoid) | |
if currentAnimInstance and currentAnimInstance.AnimationId == "http://www.roblox.com/asset/?id=180426354" then | |
setAnimationSpeed(speed / 14.5) | |
end | |
pose = "Running" | |
else | |
if emoteNames[currentAnim] == nil then | |
playAnimation("idle", 0.1, Humanoid) | |
pose = "Standing" | |
end | |
end | |
end | |
function onDied() | |
pose = "Dead" | |
end | |
function onJumping() | |
playAnimation("jump", 0.1, Humanoid) | |
jumpAnimTime = jumpAnimDuration | |
pose = "Jumping" | |
end | |
function onClimbing(speed) | |
playAnimation("climb", 0.1, Humanoid) | |
setAnimationSpeed(speed / 12.0) | |
pose = "Climbing" | |
end | |
function onGettingUp() | |
pose = "GettingUp" | |
end | |
function onFreeFall() | |
if (jumpAnimTime <= 0) then | |
playAnimation("fall", fallTransitionTime, Humanoid) | |
end | |
pose = "FreeFall" | |
end | |
function onFallingDown() | |
pose = "FallingDown" | |
end | |
function onSeated() | |
pose = "Seated" | |
end | |
function onPlatformStanding() | |
pose = "PlatformStanding" | |
end | |
function onSwimming(speed) | |
if speed > 0 then | |
pose = "Running" | |
else | |
pose = "Standing" | |
end | |
end | |
function getTool() | |
for _, kid in ipairs(Figure:GetChildren()) do | |
if kid.className == "Tool" then return kid end | |
end | |
return nil | |
end | |
function getToolAnim(tool) | |
for _, c in ipairs(tool:GetChildren()) do | |
if c.Name == "toolanim" and c.className == "StringValue" then | |
return c | |
end | |
end | |
return nil | |
end | |
function animateTool() | |
if (toolAnim == "None") then | |
playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) | |
return | |
end | |
if (toolAnim == "Slash") then | |
playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) | |
return | |
end | |
if (toolAnim == "Lunge") then | |
playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) | |
return | |
end | |
end | |
function moveSit() | |
RightShoulder.MaxVelocity = 0.15 | |
LeftShoulder.MaxVelocity = 0.15 | |
RightShoulder:SetDesiredAngle(3.14 /2) | |
LeftShoulder:SetDesiredAngle(-3.14 /2) | |
RightHip:SetDesiredAngle(3.14 /2) | |
LeftHip:SetDesiredAngle(-3.14 /2) | |
end | |
local lastTick = 0 | |
function move(time) | |
local amplitude = 1 | |
local frequency = 1 | |
local deltaTime = time - lastTick | |
lastTick = time | |
local climbFudge = 0 | |
local setAngles = false | |
if (jumpAnimTime > 0) then | |
jumpAnimTime = jumpAnimTime - deltaTime | |
end | |
if (pose == "FreeFall" and jumpAnimTime <= 0) then | |
playAnimation("fall", fallTransitionTime, Humanoid) | |
elseif (pose == "Seated") then | |
playAnimation("sit", 0.5, Humanoid) | |
return | |
elseif (pose == "Running") then | |
playAnimation("walk", 0.1, Humanoid) | |
elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then | |
-- print("Wha " .. pose) | |
stopAllAnimations() | |
amplitude = 0.1 | |
frequency = 1 | |
setAngles = true | |
end | |
if (setAngles) then | |
local desiredAngle = amplitude * math.sin(time * frequency) | |
RightShoulder:SetDesiredAngle(desiredAngle + climbFudge) | |
LeftShoulder:SetDesiredAngle(desiredAngle - climbFudge) | |
RightHip:SetDesiredAngle(-desiredAngle) | |
LeftHip:SetDesiredAngle(-desiredAngle) | |
end | |
-- Tool Animation handling | |
local tool = getTool() | |
if tool and tool:FindFirstChild("Handle") then | |
local animStringValueObject = getToolAnim(tool) | |
if animStringValueObject then | |
toolAnim = animStringValueObject.Value | |
-- message recieved, delete StringValue | |
animStringValueObject.Parent = nil | |
toolAnimTime = time + .3 | |
end | |
if time > toolAnimTime then | |
toolAnimTime = 0 | |
toolAnim = "None" | |
end | |
animateTool() | |
else | |
stopToolAnimations() | |
toolAnim = "None" | |
toolAnimInstance = nil | |
toolAnimTime = 0 | |
end | |
end | |
local events = {} | |
local eventHum = Humanoid | |
local function onUnhook() | |
for i = 1, #events do | |
events[i]:Disconnect() | |
end | |
events = {} | |
end | |
local function onHook() | |
onUnhook() | |
pose = eventHum.Sit and "Seated" or "Standing" | |
events = { | |
eventHum.Died:connect(onDied), | |
eventHum.Running:connect(onRunning), | |
eventHum.Jumping:connect(onJumping), | |
eventHum.Climbing:connect(onClimbing), | |
eventHum.GettingUp:connect(onGettingUp), | |
eventHum.FreeFalling:connect(onFreeFall), | |
eventHum.FallingDown:connect(onFallingDown), | |
eventHum.Seated:connect(onSeated), | |
eventHum.PlatformStanding:connect(onPlatformStanding), | |
eventHum.Swimming:connect(onSwimming) | |
} | |
end | |
onHook() | |
-- setup emote chat hook | |
game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) | |
local emote = "" | |
if msg == "/e dance" then | |
emote = dances[math.random(1, #dances)] | |
elseif (string.sub(msg, 1, 3) == "/e ") then | |
emote = string.sub(msg, 4) | |
elseif (string.sub(msg, 1, 7) == "/emote ") then | |
emote = string.sub(msg, 8) | |
end | |
if (pose == "Standing" and emoteNames[emote] ~= nil) then | |
playAnimation(emote, 0.1, Humanoid) | |
end | |
end) | |
-- main program | |
-- initialize to idle | |
playAnimation("idle", 0.1, Humanoid) | |
pose = "Standing" | |
spawn(function() | |
while Figure.Parent ~= nil do | |
local _, time = wait(0.1) | |
move(time) | |
end | |
end) | |
return { | |
onRunning = onRunning, | |
onDied = onDied, | |
onJumping = onJumping, | |
onClimbing = onClimbing, | |
onGettingUp = onGettingUp, | |
onFreeFall = onFreeFall, | |
onFallingDown = onFallingDown, | |
onSeated = onSeated, | |
onPlatformStanding = onPlatformStanding, | |
onHook = onHook, | |
onUnhook = onUnhook | |
} | |
end | |
return r6() | |
end | |
function _R15() | |
local function r15() | |
local Character = char | |
local Humanoid = Character:WaitForChild("Humanoid") | |
local pose = "Standing" | |
local userNoUpdateOnLoopSuccess, userNoUpdateOnLoopValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoUpdateOnLoop") end) | |
local userNoUpdateOnLoop = userNoUpdateOnLoopSuccess and userNoUpdateOnLoopValue | |
local userAnimationSpeedDampeningSuccess, userAnimationSpeedDampeningValue = pcall(function() return UserSettings():IsUserFeatureEnabled("UserAnimationSpeedDampening") end) | |
local userAnimationSpeedDampening = userAnimationSpeedDampeningSuccess and userAnimationSpeedDampeningValue | |
local animateScriptEmoteHookFlagExists, animateScriptEmoteHookFlagEnabled = pcall(function() | |
return UserSettings():IsUserFeatureEnabled("UserAnimateScriptEmoteHook") | |
end) | |
local FFlagAnimateScriptEmoteHook = animateScriptEmoteHookFlagExists and animateScriptEmoteHookFlagEnabled | |
local AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent") | |
local HumanoidHipHeight = 2 | |
local EMOTE_TRANSITION_TIME = 0.1 | |
local currentAnim = "" | |
local currentAnimInstance = nil | |
local currentAnimTrack = nil | |
local currentAnimKeyframeHandler = nil | |
local currentAnimSpeed = 1.0 | |
local runAnimTrack = nil | |
local runAnimKeyframeHandler = nil | |
local animTable = {} | |
local animNames = { | |
idle = { | |
{ id = "http://www.roblox.com/asset/?id=507766666", weight = 1 }, | |
{ id = "http://www.roblox.com/asset/?id=507766951", weight = 1 }, | |
{ id = "http://www.roblox.com/asset/?id=507766388", weight = 9 } | |
}, | |
walk = { | |
{ id = "http://www.roblox.com/asset/?id=507777826", weight = 10 } | |
}, | |
run = { | |
{ id = "http://www.roblox.com/asset/?id=507767714", weight = 10 } | |
}, | |
swim = { | |
{ id = "http://www.roblox.com/asset/?id=507784897", weight = 10 } | |
}, | |
swimidle = { | |
{ id = "http://www.roblox.com/asset/?id=507785072", weight = 10 } | |
}, | |
jump = { | |
{ id = "http://www.roblox.com/asset/?id=507765000", weight = 10 } | |
}, | |
fall = { | |
{ id = "http://www.roblox.com/asset/?id=507767968", weight = 10 } | |
}, | |
climb = { | |
{ id = "http://www.roblox.com/asset/?id=507765644", weight = 10 } | |
}, | |
sit = { | |
{ id = "http://www.roblox.com/asset/?id=2506281703", weight = 10 } | |
}, | |
toolnone = { | |
{ id = "http://www.roblox.com/asset/?id=507768375", weight = 10 } | |
}, | |
toolslash = { | |
{ id = "http://www.roblox.com/asset/?id=522635514", weight = 10 } | |
}, | |
toollunge = { | |
{ id = "http://www.roblox.com/asset/?id=522638767", weight = 10 } | |
}, | |
wave = { | |
{ id = "http://www.roblox.com/asset/?id=507770239", weight = 10 } | |
}, | |
point = { | |
{ id = "http://www.roblox.com/asset/?id=507770453", weight = 10 } | |
}, | |
dance = { | |
{ id = "http://www.roblox.com/asset/?id=507771019", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=507771955", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=507772104", weight = 10 } | |
}, | |
dance2 = { | |
{ id = "http://www.roblox.com/asset/?id=507776043", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=507776720", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=507776879", weight = 10 } | |
}, | |
dance3 = { | |
{ id = "http://www.roblox.com/asset/?id=507777268", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=507777451", weight = 10 }, | |
{ id = "http://www.roblox.com/asset/?id=507777623", weight = 10 } | |
}, | |
laugh = { | |
{ id = "http://www.roblox.com/asset/?id=507770818", weight = 10 } | |
}, | |
cheer = { | |
{ id = "http://www.roblox.com/asset/?id=507770677", weight = 10 } | |
}, | |
} | |
-- Existance in this list signifies that it is an emote, the value indicates if it is a looping emote | |
local emoteNames = { wave = false, point = false, dance = true, dance2 = true, dance3 = true, laugh = false, cheer = false} | |
local PreloadAnimsUserFlag = false | |
local PreloadedAnims = {} | |
local successPreloadAnim, msgPreloadAnim = pcall(function() | |
PreloadAnimsUserFlag = UserSettings():IsUserFeatureEnabled("UserPreloadAnimations") | |
end) | |
if not successPreloadAnim then | |
PreloadAnimsUserFlag = false | |
end | |
math.randomseed(tick()) | |
function findExistingAnimationInSet(set, anim) | |
if set == nil or anim == nil then | |
return 0 | |
end | |
for idx = 1, set.count, 1 do | |
if set[idx].anim.AnimationId == anim.AnimationId then | |
return idx | |
end | |
end | |
return 0 | |
end | |
function configureAnimationSet(name, fileList) | |
if (animTable[name] ~= nil) then | |
for _, connection in pairs(animTable[name].connections) do | |
connection:disconnect() | |
end | |
end | |
animTable[name] = {} | |
animTable[name].count = 0 | |
animTable[name].totalWeight = 0 | |
animTable[name].connections = {} | |
local allowCustomAnimations = true | |
local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end) | |
if not success then | |
allowCustomAnimations = true | |
end | |
-- check for config values | |
local config = script:FindFirstChild(name) | |
if (allowCustomAnimations and config ~= nil) then | |
table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) | |
table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) | |
local idx = 0 | |
for _, childPart in pairs(config:GetChildren()) do | |
if (childPart:IsA("Animation")) then | |
local newWeight = 1 | |
local weightObject = childPart:FindFirstChild("Weight") | |
if (weightObject ~= nil) then | |
newWeight = weightObject.Value | |
end | |
animTable[name].count = animTable[name].count + 1 | |
idx = animTable[name].count | |
animTable[name][idx] = {} | |
animTable[name][idx].anim = childPart | |
animTable[name][idx].weight = newWeight | |
animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight | |
table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) | |
table.insert(animTable[name].connections, childPart.ChildAdded:connect(function(property) configureAnimationSet(name, fileList) end)) | |
table.insert(animTable[name].connections, childPart.ChildRemoved:connect(function(property) configureAnimationSet(name, fileList) end)) | |
end | |
end | |
end | |
-- fallback to defaults | |
if (animTable[name].count <= 0) then | |
for idx, anim in pairs(fileList) do | |
animTable[name][idx] = {} | |
animTable[name][idx].anim = Instance.new("Animation") | |
animTable[name][idx].anim.Name = name | |
animTable[name][idx].anim.AnimationId = anim.id | |
animTable[name][idx].weight = anim.weight | |
animTable[name].count = animTable[name].count + 1 | |
animTable[name].totalWeight = animTable[name].totalWeight + anim.weight | |
end | |
end | |
-- preload anims | |
if PreloadAnimsUserFlag then | |
for i, animType in pairs(animTable) do | |
for idx = 1, animType.count, 1 do | |
if PreloadedAnims[animType[idx].anim.AnimationId] == nil then | |
Humanoid:LoadAnimation(animType[idx].anim) | |
PreloadedAnims[animType[idx].anim.AnimationId] = true | |
end | |
end | |
end | |
end | |
end | |
------------------------------------------------------------------------------------------------------------ | |
function configureAnimationSetOld(name, fileList) | |
if (animTable[name] ~= nil) then | |
for _, connection in pairs(animTable[name].connections) do | |
connection:disconnect() | |
end | |
end | |
animTable[name] = {} | |
animTable[name].count = 0 | |
animTable[name].totalWeight = 0 | |
animTable[name].connections = {} | |
local allowCustomAnimations = true | |
local success, msg = pcall(function() allowCustomAnimations = game:GetService("StarterPlayer").AllowCustomAnimations end) | |
if not success then | |
allowCustomAnimations = true | |
end | |
-- check for config values | |
local config = script:FindFirstChild(name) | |
if (allowCustomAnimations and config ~= nil) then | |
table.insert(animTable[name].connections, config.ChildAdded:connect(function(child) configureAnimationSet(name, fileList) end)) | |
table.insert(animTable[name].connections, config.ChildRemoved:connect(function(child) configureAnimationSet(name, fileList) end)) | |
local idx = 1 | |
for _, childPart in pairs(config:GetChildren()) do | |
if (childPart:IsA("Animation")) then | |
table.insert(animTable[name].connections, childPart.Changed:connect(function(property) configureAnimationSet(name, fileList) end)) | |
animTable[name][idx] = {} | |
animTable[name][idx].anim = childPart | |
local weightObject = childPart:FindFirstChild("Weight") | |
if (weightObject == nil) then | |
animTable[name][idx].weight = 1 | |
else | |
animTable[name][idx].weight = weightObject.Value | |
end | |
animTable[name].count = animTable[name].count + 1 | |
animTable[name].totalWeight = animTable[name].totalWeight + animTable[name][idx].weight | |
idx = idx + 1 | |
end | |
end | |
end | |
-- fallback to defaults | |
if (animTable[name].count <= 0) then | |
for idx, anim in pairs(fileList) do | |
animTable[name][idx] = {} | |
animTable[name][idx].anim = Instance.new("Animation") | |
animTable[name][idx].anim.Name = name | |
animTable[name][idx].anim.AnimationId = anim.id | |
animTable[name][idx].weight = anim.weight | |
animTable[name].count = animTable[name].count + 1 | |
animTable[name].totalWeight = animTable[name].totalWeight + anim.weight | |
-- print(name .. " [" .. idx .. "] " .. anim.id .. " (" .. anim.weight .. ")") | |
end | |
end | |
-- preload anims | |
if PreloadAnimsUserFlag then | |
for i, animType in pairs(animTable) do | |
for idx = 1, animType.count, 1 do | |
Humanoid:LoadAnimation(animType[idx].anim) | |
end | |
end | |
end | |
end | |
-- Setup animation objects | |
function scriptChildModified(child) | |
local fileList = animNames[child.Name] | |
if (fileList ~= nil) then | |
configureAnimationSet(child.Name, fileList) | |
end | |
end | |
script.ChildAdded:connect(scriptChildModified) | |
script.ChildRemoved:connect(scriptChildModified) | |
for name, fileList in pairs(animNames) do | |
configureAnimationSet(name, fileList) | |
end | |
-- ANIMATION | |
-- declarations | |
local toolAnim = "None" | |
local toolAnimTime = 0 | |
local jumpAnimTime = 0 | |
local jumpAnimDuration = 0.31 | |
local toolTransitionTime = 0.1 | |
local fallTransitionTime = 0.2 | |
local currentlyPlayingEmote = false | |
-- functions | |
function stopAllAnimations() | |
local oldAnim = currentAnim | |
-- return to idle if finishing an emote | |
if (emoteNames[oldAnim] ~= nil and emoteNames[oldAnim] == false) then | |
oldAnim = "idle" | |
end | |
if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then | |
oldAnim = "idle" | |
currentlyPlayingEmote = false | |
end | |
currentAnim = "" | |
currentAnimInstance = nil | |
if (currentAnimKeyframeHandler ~= nil) then | |
currentAnimKeyframeHandler:disconnect() | |
end | |
if (currentAnimTrack ~= nil) then | |
currentAnimTrack:Stop() | |
currentAnimTrack:Destroy() | |
currentAnimTrack = nil | |
end | |
-- clean up walk if there is one | |
if (runAnimKeyframeHandler ~= nil) then | |
runAnimKeyframeHandler:disconnect() | |
end | |
if (runAnimTrack ~= nil) then | |
runAnimTrack:Stop() | |
runAnimTrack:Destroy() | |
runAnimTrack = nil | |
end | |
return oldAnim | |
end | |
function getHeightScale() | |
if Humanoid then | |
if not Humanoid.AutomaticScalingEnabled then | |
return 1 | |
end | |
local scale = Humanoid.HipHeight / HumanoidHipHeight | |
if userAnimationSpeedDampening then | |
if AnimationSpeedDampeningObject == nil then | |
AnimationSpeedDampeningObject = script:FindFirstChild("ScaleDampeningPercent") | |
end | |
if AnimationSpeedDampeningObject ~= nil then | |
scale = 1 + (Humanoid.HipHeight - HumanoidHipHeight) * AnimationSpeedDampeningObject.Value / HumanoidHipHeight | |
end | |
end | |
return scale | |
end | |
return 1 | |
end | |
local smallButNotZero = 0.0001 | |
function setRunSpeed(speed) | |
local speedScaled = speed * 1.25 | |
local heightScale = getHeightScale() | |
local runSpeed = speedScaled / heightScale | |
if runSpeed ~= currentAnimSpeed then | |
if runSpeed < 0.33 then | |
currentAnimTrack:AdjustWeight(1.0) | |
runAnimTrack:AdjustWeight(smallButNotZero) | |
elseif runSpeed < 0.66 then | |
local weight = ((runSpeed - 0.33) / 0.33) | |
currentAnimTrack:AdjustWeight(1.0 - weight + smallButNotZero) | |
runAnimTrack:AdjustWeight(weight + smallButNotZero) | |
else | |
currentAnimTrack:AdjustWeight(smallButNotZero) | |
runAnimTrack:AdjustWeight(1.0) | |
end | |
currentAnimSpeed = runSpeed | |
runAnimTrack:AdjustSpeed(runSpeed) | |
currentAnimTrack:AdjustSpeed(runSpeed) | |
end | |
end | |
function setAnimationSpeed(speed) | |
if currentAnim == "walk" then | |
setRunSpeed(speed) | |
else | |
if speed ~= currentAnimSpeed then | |
currentAnimSpeed = speed | |
currentAnimTrack:AdjustSpeed(currentAnimSpeed) | |
end | |
end | |
end | |
function keyFrameReachedFunc(frameName) | |
if (frameName == "End") then | |
if currentAnim == "walk" then | |
if userNoUpdateOnLoop == true then | |
if runAnimTrack.Looped ~= true then | |
runAnimTrack.TimePosition = 0.0 | |
end | |
if currentAnimTrack.Looped ~= true then | |
currentAnimTrack.TimePosition = 0.0 | |
end | |
else | |
runAnimTrack.TimePosition = 0.0 | |
currentAnimTrack.TimePosition = 0.0 | |
end | |
else | |
local repeatAnim = currentAnim | |
-- return to idle if finishing an emote | |
if (emoteNames[repeatAnim] ~= nil and emoteNames[repeatAnim] == false) then | |
repeatAnim = "idle" | |
end | |
if FFlagAnimateScriptEmoteHook and currentlyPlayingEmote then | |
if currentAnimTrack.Looped then | |
-- Allow the emote to loop | |
return | |
end | |
repeatAnim = "idle" | |
currentlyPlayingEmote = false | |
end | |
local animSpeed = currentAnimSpeed | |
playAnimation(repeatAnim, 0.15, Humanoid) | |
setAnimationSpeed(animSpeed) | |
end | |
end | |
end | |
function rollAnimation(animName) | |
local roll = math.random(1, animTable[animName].totalWeight) | |
local origRoll = roll | |
local idx = 1 | |
while (roll > animTable[animName][idx].weight) do | |
roll = roll - animTable[animName][idx].weight | |
idx = idx + 1 | |
end | |
return idx | |
end | |
local function switchToAnim(anim, animName, transitionTime, humanoid) | |
-- switch animation | |
if (anim ~= currentAnimInstance) then | |
if (currentAnimTrack ~= nil) then | |
currentAnimTrack:Stop(transitionTime) | |
currentAnimTrack:Destroy() | |
end | |
if (runAnimTrack ~= nil) then | |
runAnimTrack:Stop(transitionTime) | |
runAnimTrack:Destroy() | |
if userNoUpdateOnLoop == true then | |
runAnimTrack = nil | |
end | |
end | |
currentAnimSpeed = 1.0 | |
-- load it to the humanoid; get AnimationTrack | |
currentAnimTrack = humanoid:LoadAnimation(anim) | |
currentAnimTrack.Priority = Enum.AnimationPriority.Core | |
-- play the animation | |
currentAnimTrack:Play(transitionTime) | |
currentAnim = animName | |
currentAnimInstance = anim | |
-- set up keyframe name triggers | |
if (currentAnimKeyframeHandler ~= nil) then | |
currentAnimKeyframeHandler:disconnect() | |
end | |
currentAnimKeyframeHandler = currentAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) | |
-- check to see if we need to blend a walk/run animation | |
if animName == "walk" then | |
local runAnimName = "run" | |
local runIdx = rollAnimation(runAnimName) | |
runAnimTrack = humanoid:LoadAnimation(animTable[runAnimName][runIdx].anim) | |
runAnimTrack.Priority = Enum.AnimationPriority.Core | |
runAnimTrack:Play(transitionTime) | |
if (runAnimKeyframeHandler ~= nil) then | |
runAnimKeyframeHandler:disconnect() | |
end | |
runAnimKeyframeHandler = runAnimTrack.KeyframeReached:connect(keyFrameReachedFunc) | |
end | |
end | |
end | |
function playAnimation(animName, transitionTime, humanoid) | |
local idx = rollAnimation(animName) | |
local anim = animTable[animName][idx].anim | |
switchToAnim(anim, animName, transitionTime, humanoid) | |
currentlyPlayingEmote = false | |
end | |
function playEmote(emoteAnim, transitionTime, humanoid) | |
switchToAnim(emoteAnim, emoteAnim.Name, transitionTime, humanoid) | |
currentlyPlayingEmote = true | |
end | |
------------------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------------------- | |
local toolAnimName = "" | |
local toolAnimTrack = nil | |
local toolAnimInstance = nil | |
local currentToolAnimKeyframeHandler = nil | |
function toolKeyFrameReachedFunc(frameName) | |
if (frameName == "End") then | |
playToolAnimation(toolAnimName, 0.0, Humanoid) | |
end | |
end | |
function playToolAnimation(animName, transitionTime, humanoid, priority) | |
local idx = rollAnimation(animName) | |
local anim = animTable[animName][idx].anim | |
if (toolAnimInstance ~= anim) then | |
if (toolAnimTrack ~= nil) then | |
toolAnimTrack:Stop() | |
toolAnimTrack:Destroy() | |
transitionTime = 0 | |
end | |
-- load it to the humanoid; get AnimationTrack | |
toolAnimTrack = humanoid:LoadAnimation(anim) | |
if priority then | |
toolAnimTrack.Priority = priority | |
end | |
-- play the animation | |
toolAnimTrack:Play(transitionTime) | |
toolAnimName = animName | |
toolAnimInstance = anim | |
currentToolAnimKeyframeHandler = toolAnimTrack.KeyframeReached:connect(toolKeyFrameReachedFunc) | |
end | |
end | |
function stopToolAnimations() | |
local oldAnim = toolAnimName | |
if (currentToolAnimKeyframeHandler ~= nil) then | |
currentToolAnimKeyframeHandler:disconnect() | |
end | |
toolAnimName = "" | |
toolAnimInstance = nil | |
if (toolAnimTrack ~= nil) then | |
toolAnimTrack:Stop() | |
toolAnimTrack:Destroy() | |
toolAnimTrack = nil | |
end | |
return oldAnim | |
end | |
------------------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------------------- | |
-- STATE CHANGE HANDLERS | |
function onRunning(speed) | |
if speed > 0.75 then | |
local scale = 16.0 | |
playAnimation("walk", 0.2, Humanoid) | |
setAnimationSpeed(speed / scale) | |
pose = "Running" | |
else | |
if emoteNames[currentAnim] == nil and not currentlyPlayingEmote then | |
playAnimation("idle", 0.2, Humanoid) | |
pose = "Standing" | |
end | |
end | |
end | |
function onDied() | |
pose = "Dead" | |
end | |
function onJumping() | |
playAnimation("jump", 0.1, Humanoid) | |
jumpAnimTime = jumpAnimDuration | |
pose = "Jumping" | |
end | |
function onClimbing(speed) | |
local scale = 5.0 | |
playAnimation("climb", 0.1, Humanoid) | |
setAnimationSpeed(speed / scale) | |
pose = "Climbing" | |
end | |
function onGettingUp() | |
pose = "GettingUp" | |
end | |
function onFreeFall() | |
if (jumpAnimTime <= 0) then | |
playAnimation("fall", fallTransitionTime, Humanoid) | |
end | |
pose = "FreeFall" | |
end | |
function onFallingDown() | |
pose = "FallingDown" | |
end | |
function onSeated() | |
pose = "Seated" | |
end | |
function onPlatformStanding() | |
pose = "PlatformStanding" | |
end | |
------------------------------------------------------------------------------------------- | |
------------------------------------------------------------------------------------------- | |
function onSwimming(speed) | |
if speed > 1.00 then | |
local scale = 10.0 | |
playAnimation("swim", 0.4, Humanoid) | |
setAnimationSpeed(speed / scale) | |
pose = "Swimming" | |
else | |
playAnimation("swimidle", 0.4, Humanoid) | |
pose = "Standing" | |
end | |
end | |
function animateTool() | |
if (toolAnim == "None") then | |
playToolAnimation("toolnone", toolTransitionTime, Humanoid, Enum.AnimationPriority.Idle) | |
return | |
end | |
if (toolAnim == "Slash") then | |
playToolAnimation("toolslash", 0, Humanoid, Enum.AnimationPriority.Action) | |
return | |
end | |
if (toolAnim == "Lunge") then | |
playToolAnimation("toollunge", 0, Humanoid, Enum.AnimationPriority.Action) | |
return | |
end | |
end | |
function getToolAnim(tool) | |
for _, c in ipairs(tool:GetChildren()) do | |
if c.Name == "toolanim" and c.className == "StringValue" then | |
return c | |
end | |
end | |
return nil | |
end | |
local lastTick = 0 | |
function stepAnimate(currentTime) | |
local amplitude = 1 | |
local frequency = 1 | |
local deltaTime = currentTime - lastTick | |
lastTick = currentTime | |
local climbFudge = 0 | |
local setAngles = false | |
if (jumpAnimTime > 0) then | |
jumpAnimTime = jumpAnimTime - deltaTime | |
end | |
if (pose == "FreeFall" and jumpAnimTime <= 0) then | |
playAnimation("fall", fallTransitionTime, Humanoid) | |
elseif (pose == "Seated") then | |
playAnimation("sit", 0.5, Humanoid) | |
return | |
elseif (pose == "Running") then | |
playAnimation("walk", 0.2, Humanoid) | |
elseif (pose == "Dead" or pose == "GettingUp" or pose == "FallingDown" or pose == "Seated" or pose == "PlatformStanding") then | |
stopAllAnimations() | |
amplitude = 0.1 | |
frequency = 1 | |
setAngles = true | |
end | |
-- Tool Animation handling | |
local tool = Character:FindFirstChildOfClass("Tool") | |
if tool and tool:FindFirstChild("Handle") then | |
local animStringValueObject = getToolAnim(tool) | |
if animStringValueObject then | |
toolAnim = animStringValueObject.Value | |
-- message recieved, delete StringValue | |
animStringValueObject.Parent = nil | |
toolAnimTime = currentTime + .3 | |
end | |
if currentTime > toolAnimTime then | |
toolAnimTime = 0 | |
toolAnim = "None" | |
end | |
animateTool() | |
else | |
stopToolAnimations() | |
toolAnim = "None" | |
toolAnimInstance = nil | |
toolAnimTime = 0 | |
end | |
end | |
-- connect events | |
local events = {} | |
local eventHum = Humanoid | |
local function onUnhook() | |
for i = 1, #events do | |
events[i]:Disconnect() | |
end | |
events = {} | |
end | |
local function onHook() | |
onUnhook() | |
pose = eventHum.Sit and "Seated" or "Standing" | |
events = { | |
eventHum.Died:connect(onDied), | |
eventHum.Running:connect(onRunning), | |
eventHum.Jumping:connect(onJumping), | |
eventHum.Climbing:connect(onClimbing), | |
eventHum.GettingUp:connect(onGettingUp), | |
eventHum.FreeFalling:connect(onFreeFall), | |
eventHum.FallingDown:connect(onFallingDown), | |
eventHum.Seated:connect(onSeated), | |
eventHum.PlatformStanding:connect(onPlatformStanding), | |
eventHum.Swimming:connect(onSwimming) | |
} | |
end | |
onHook() | |
-- setup emote chat hook | |
game:GetService("Players").LocalPlayer.Chatted:connect(function(msg) | |
local emote = "" | |
if (string.sub(msg, 1, 3) == "/e ") then | |
emote = string.sub(msg, 4) | |
elseif (string.sub(msg, 1, 7) == "/emote ") then | |
emote = string.sub(msg, 8) | |
end | |
if (pose == "Standing" and emoteNames[emote] ~= nil) then | |
playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid) | |
end | |
end) | |
--[[ emote bindable hook | |
if FFlagAnimateScriptEmoteHook then | |
script:WaitForChild("PlayEmote").OnInvoke = function(emote) | |
-- Only play emotes when idling | |
if pose ~= "Standing" then | |
return | |
end | |
if emoteNames[emote] ~= nil then | |
-- Default emotes | |
playAnimation(emote, EMOTE_TRANSITION_TIME, Humanoid) | |
return true | |
elseif typeof(emote) == "Instance" and emote:IsA("Animation") then | |
-- Non-default emotes | |
playEmote(emote, EMOTE_TRANSITION_TIME, Humanoid) | |
return true | |
end | |
-- Return false to indicate that the emote could not be played | |
return false | |
end | |
end | |
]] | |
-- initialize to idle | |
playAnimation("idle", 0.1, Humanoid) | |
pose = "Standing" | |
-- loop to handle timed state transitions and tool animations | |
spawn(function() | |
while Character.Parent ~= nil do | |
local _, currentGameTime = wait(0.1) | |
stepAnimate(currentGameTime) | |
end | |
end) | |
return { | |
onRunning = onRunning, | |
onDied = onDied, | |
onJumping = onJumping, | |
onClimbing = onClimbing, | |
onGettingUp = onGettingUp, | |
onFreeFall = onFreeFall, | |
onFallingDown = onFallingDown, | |
onSeated = onSeated, | |
onPlatformStanding = onPlatformStanding, | |
onHook = onHook, | |
onUnhook = onUnhook | |
} | |
end | |
return r15() | |
end | |
while true do | |
wait(.1) | |
if plr.Character ~= nil then | |
char = plr.Character | |
break | |
end | |
end | |
function _Controller() | |
local humanoid = char:WaitForChild("Humanoid") | |
local animFuncs = {} | |
if (humanoid.RigType == Enum.HumanoidRigType.R6) then | |
animFuncs = _R6() | |
else | |
animFuncs = _R15() | |
end | |
print("Animation succes") | |
return animFuncs | |
end | |
function _AnimationHandler() | |
local AnimationHandler = {} | |
AnimationHandler.__index = AnimationHandler | |
function AnimationHandler.new(humanoid, animate) | |
local self = setmetatable({}, AnimationHandler) | |
self._AnimFuncs = _Controller() | |
self.Humanoid = humanoid | |
return self | |
end | |
function AnimationHandler:EnableDefault(bool) | |
if (bool) then | |
self._AnimFuncs.onHook() | |
else | |
self._AnimFuncs.onUnhook() | |
end | |
end | |
function AnimationHandler:Run(name, ...) | |
self._AnimFuncs[name](...) | |
end | |
return AnimationHandler | |
end | |
function _GravityController() | |
local ZERO = Vector3.new(0, 0, 0) | |
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 VEC_XY = Vector3.new(1, 0, 1) | |
local IDENTITYCF = CFrame.new() | |
local JUMPMODIFIER = 1.2 | |
local TRANSITION = 0.15 | |
local WALKF = 200 / 3 | |
local UIS = game:GetService("UserInputService") | |
local RUNSERVICE = game:GetService("RunService") | |
local InitObjects = _InitObjects() | |
local AnimationHandler = _AnimationHandler() | |
local StateTracker = _StateTracker() | |
-- Class | |
local GravityController = {} | |
GravityController.__index = GravityController | |
-- Private Functions | |
local function getRotationBetween(u, v, axis) | |
local dot, uxv = u:Dot(v), u:Cross(v) | |
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end | |
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot) | |
end | |
local function lookAt(pos, forward, up) | |
local r = forward:Cross(up) | |
local u = r:Cross(forward) | |
return CFrame.fromMatrix(pos, r.Unit, u.Unit) | |
end | |
local function getMass(array) | |
local mass = 0 | |
for _, part in next, array do | |
if (part:IsA("BasePart")) then | |
mass = mass + part:GetMass() | |
end | |
end | |
return mass | |
end | |
-- Public Constructor | |
local ExecutedPlayerModule = _PlayerModule() | |
local ExecutedSounds = _sounds() | |
function GravityController.new(player) | |
local self = setmetatable({}, GravityController) | |
--[[ Camera | |
local loaded = player.PlayerScripts:WaitForChild("PlayerScriptsLoader"):WaitForChild("Loaded") | |
if (not loaded.Value) then | |
--loaded.Changed:Wait() | |
end | |
]] | |
local playerModule = ExecutedPlayerModule | |
self.Controls = playerModule:GetControls() | |
self.Camera = playerModule:GetCameras() | |
-- Player and character | |
self.Player = player | |
self.Character = player.Character | |
self.Humanoid = player.Character:WaitForChild("Humanoid") | |
self.HRP = player.Character:WaitForChild("HumanoidRootPart") | |
-- Animation | |
self.AnimationHandler = AnimationHandler.new(self.Humanoid, self.Character:WaitForChild("Animate")) | |
self.AnimationHandler:EnableDefault(false) | |
local ssss = game:GetService("Players").LocalPlayer.PlayerScripts:FindFirstChild("SetState") or Instance.new("BindableEvent",game:GetService("Players").LocalPlayer.PlayerScripts) | |
local soundState = ExecutedSounds | |
ssss.Name = "SetState" | |
self.StateTracker = StateTracker.new(self.Humanoid, soundState) | |
self.StateTracker.Changed:Connect(function(name, speed) | |
self.AnimationHandler:Run(name, speed) | |
end) | |
-- Collider and forces | |
local collider, gyro, vForce, floor = InitObjects(self) | |
floor.Touched:Connect(function() end) | |
collider.Touched:Connect(function() end) | |
self.Collider = collider | |
self.VForce = vForce | |
self.Gyro = gyro | |
self.Floor = floor | |
-- Attachment to parts | |
self.LastPart = workspace.Terrain | |
self.LastPartCFrame = IDENTITYCF | |
-- Gravity properties | |
self.GravityUp = UNIT_Y | |
self.Ignores = {self.Character} | |
function self.Camera.GetUpVector(this, oldUpVector) | |
return self.GravityUp | |
end | |
-- Events etc | |
self.Humanoid.PlatformStand = true | |
self.CharacterMass = getMass(self.Character:GetDescendants()) | |
self.Character.AncestryChanged:Connect(function() self.CharacterMass = getMass(self.Character:GetDescendants()) end) | |
self.JumpCon = RUNSERVICE.RenderStepped:Connect(function(dt) | |
if (self.Controls:IsJumping()) then | |
self:OnJumpRequest() | |
end | |
end) | |
self.DeathCon = self.Humanoid.Died:Connect(function() self:Destroy() end) | |
self.SeatCon = self.Humanoid.Seated:Connect(function(active) if (active) then self:Destroy() end end) | |
self.HeartCon = RUNSERVICE.Heartbeat:Connect(function(dt) self:OnHeartbeatStep(dt) end) | |
RUNSERVICE:BindToRenderStep("GravityStep", Enum.RenderPriority.Input.Value + 1, function(dt) self:OnGravityStep(dt) end) | |
return self | |
end | |
-- Public Methods | |
function GravityController:Destroy() | |
self.JumpCon:Disconnect() | |
self.DeathCon:Disconnect() | |
self.SeatCon:Disconnect() | |
self.HeartCon:Disconnect() | |
RUNSERVICE:UnbindFromRenderStep("GravityStep") | |
self.Collider:Destroy() | |
self.VForce:Destroy() | |
self.Gyro:Destroy() | |
self.StateTracker:Destroy() | |
self.Humanoid.PlatformStand = false | |
self.AnimationHandler:EnableDefault(true) | |
self.GravityUp = UNIT_Y | |
end | |
function GravityController:GetGravityUp(oldGravity) | |
return oldGravity | |
end | |
function GravityController:IsGrounded(isJumpCheck) | |
if (not isJumpCheck) then | |
local parts = self.Floor:GetTouchingParts() | |
for _, part in next, parts do | |
if (not part:IsDescendantOf(self.Character)) then | |
return true | |
end | |
end | |
else | |
if (self.StateTracker.Jumped) then | |
return false | |
end | |
-- 1. check we are touching something with the collider | |
local valid = {} | |
local parts = self.Collider:GetTouchingParts() | |
for _, part in next, parts do | |
if (not part:IsDescendantOf(self.Character)) then | |
table.insert(valid, part) | |
end | |
end | |
if (#valid > 0) then | |
-- 2. do a decently long downwards raycast | |
local max = math.cos(self.Humanoid.MaxSlopeAngle) | |
local ray = Ray.new(self.Collider.Position, -10 * self.GravityUp) | |
local hit, pos, normal = workspace:FindPartOnRayWithWhitelist(ray, valid, true) | |
-- 3. use slope to decide on jump | |
if (hit and max <= self.GravityUp:Dot(normal)) then | |
return true | |
end | |
end | |
end | |
return false | |
end | |
function GravityController:OnJumpRequest() | |
if (not self.StateTracker.Jumped and self:IsGrounded(true)) then | |
local hrpVel = self.HRP.Velocity | |
self.HRP.Velocity = hrpVel + self.GravityUp*self.Humanoid.JumpPower*JUMPMODIFIER | |
self.StateTracker:RequestedJump() | |
end | |
end | |
function GravityController:GetMoveVector() | |
return self.Controls:GetMoveVector() | |
end | |
function GravityController:OnHeartbeatStep(dt) | |
local ray = Ray.new(self.Collider.Position, -1.1*self.GravityUp) | |
local hit, pos, normal = workspace:FindPartOnRayWithIgnoreList(ray, self.Ignores) | |
local lastPart = self.LastPart | |
if (hit and lastPart and lastPart == hit) then | |
local offset = self.LastPartCFrame:ToObjectSpace(self.HRP.CFrame) | |
self.HRP.CFrame = hit.CFrame:ToWorldSpace(offset) | |
end | |
self.LastPart = hit | |
self.LastPartCFrame = hit and hit.CFrame | |
end | |
function GravityController:OnGravityStep(dt) | |
-- update gravity up vector | |
local oldGravity = self.GravityUp | |
local newGravity = self:GetGravityUp(oldGravity) | |
local rotation = getRotationBetween(oldGravity, newGravity, workspace.CurrentCamera.CFrame.RightVector) | |
rotation = IDENTITYCF:Lerp(rotation, TRANSITION) | |
self.GravityUp = rotation * oldGravity | |
-- get world move vector | |
local camCF = workspace.CurrentCamera.CFrame | |
local fDot = camCF.LookVector:Dot(newGravity) | |
local cForward = math.abs(fDot) > 0.5 and -math.sign(fDot)*camCF.UpVector or camCF.LookVector | |
local left = cForward:Cross(-newGravity).Unit | |
local forward = -left:Cross(newGravity).Unit | |
local move = self:GetMoveVector() | |
local worldMove = forward*move.z - left*move.x | |
worldMove = worldMove:Dot(worldMove) > 1 and worldMove.Unit or worldMove | |
local isInputMoving = worldMove:Dot(worldMove) > 0 | |
-- get the desired character cframe | |
local hrpCFLook = self.HRP.CFrame.LookVector | |
local charF = hrpCFLook:Dot(forward)*forward + hrpCFLook:Dot(left)*left | |
local charR = charF:Cross(newGravity).Unit | |
local newCharCF = CFrame.fromMatrix(ZERO, charR, newGravity, -charF) | |
local newCharRotation = IDENTITYCF | |
if (isInputMoving) then | |
newCharRotation = IDENTITYCF:Lerp(getRotationBetween(charF, worldMove, newGravity), 0.7) | |
end | |
-- calculate forces | |
local g = workspace.Gravity | |
local gForce = g * self.CharacterMass * (UNIT_Y - newGravity) | |
local cVelocity = self.HRP.Velocity | |
local tVelocity = self.Humanoid.WalkSpeed * worldMove | |
local gVelocity = cVelocity:Dot(newGravity)*newGravity | |
local hVelocity = cVelocity - gVelocity | |
if (hVelocity:Dot(hVelocity) < 1) then | |
hVelocity = ZERO | |
end | |
local dVelocity = tVelocity - hVelocity | |
local walkForceM = math.min(10000, WALKF * self.CharacterMass * dVelocity.Magnitude / (dt*60)) | |
local walkForce = walkForceM > 0 and dVelocity.Unit*walkForceM or ZERO | |
-- mouse lock | |
local charRotation = newCharRotation * newCharCF | |
if (self.Camera:IsCamRelative()) then | |
local lv = workspace.CurrentCamera.CFrame.LookVector | |
local hlv = lv - charRotation.UpVector:Dot(lv)*charRotation.UpVector | |
charRotation = lookAt(ZERO, hlv, charRotation.UpVector) | |
end | |
-- get state | |
self.StateTracker:OnStep(self.GravityUp, self:IsGrounded(), isInputMoving) | |
-- update values | |
self.VForce.Force = walkForce + gForce | |
self.Gyro.CFrame = charRotation | |
end | |
return GravityController | |
end | |
function _Draw3D() | |
local module = {} | |
-- Style Guide | |
module.StyleGuide = { | |
Point = { | |
Thickness = 0.5; | |
Color = Color3.new(0, 1, 0); | |
}, | |
Line = { | |
Thickness = 0.1; | |
Color = Color3.new(1, 1, 0); | |
}, | |
Ray = { | |
Thickness = 0.1; | |
Color = Color3.new(1, 0, 1); | |
}, | |
Triangle = { | |
Thickness = 0.05; | |
}; | |
CFrame = { | |
Thickness = 0.1; | |
RightColor3 = Color3.new(1, 0, 0); | |
UpColor3 = Color3.new(0, 1, 0); | |
BackColor3 = Color3.new(0, 0, 1); | |
PartProperties = { | |
Material = Enum.Material.SmoothPlastic; | |
}; | |
} | |
} | |
-- CONSTANTS | |
local WEDGE = Instance.new("WedgePart") | |
WEDGE.Material = Enum.Material.SmoothPlastic | |
WEDGE.Anchored = true | |
WEDGE.CanCollide = false | |
local PART = Instance.new("Part") | |
PART.Size = Vector3.new(0.1, 0.1, 0.1) | |
PART.Anchored = true | |
PART.CanCollide = false | |
PART.TopSurface = Enum.SurfaceType.Smooth | |
PART.BottomSurface = Enum.SurfaceType.Smooth | |
PART.Material = Enum.Material.SmoothPlastic | |
-- Functions | |
local function draw(properties, style) | |
local part = PART:Clone() | |
for k, v in next, properties do | |
part[k] = v | |
end | |
if (style) then | |
for k, v in next, style do | |
if (k ~= "Thickness") then | |
part[k] = v | |
end | |
end | |
end | |
return part | |
end | |
function module.Draw(parent, properties) | |
properties.Parent = parent | |
return draw(properties, nil) | |
end | |
function module.Point(parent, cf_v3) | |
local thickness = module.StyleGuide.Point.Thickness | |
return draw({ | |
Size = Vector3.new(thickness, thickness, thickness); | |
CFrame = (typeof(cf_v3) == "CFrame" and cf_v3 or CFrame.new(cf_v3)); | |
Parent = parent; | |
}, module.StyleGuide.Point) | |
end | |
function module.Line(parent, a, b) | |
local thickness = module.StyleGuide.Line.Thickness | |
return draw({ | |
CFrame = CFrame.new((a + b)/2, b); | |
Size = Vector3.new(thickness, thickness, (b - a).Magnitude); | |
Parent = parent; | |
}, module.StyleGuide.Line) | |
end | |
function module.Ray(parent, origin, direction) | |
local thickness = module.StyleGuide.Ray.Thickness | |
return draw({ | |
CFrame = CFrame.new(origin + direction/2, origin + direction); | |
Size = Vector3.new(thickness, thickness, direction.Magnitude); | |
Parent = parent; | |
}, module.StyleGuide.Ray) | |
end | |
function module.Triangle(parent, a, b, c) | |
local ab, ac, bc = b - a, c - a, c - b | |
local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc) | |
if (abd > acd and abd > bcd) then | |
c, a = a, c | |
elseif (acd > bcd and acd > abd) then | |
a, b = b, a | |
end | |
ab, ac, bc = b - a, c - a, c - b | |
local right = ac:Cross(ab).Unit | |
local up = bc:Cross(right).Unit | |
local back = bc.Unit | |
local height = math.abs(ab:Dot(up)) | |
local width1 = math.abs(ab:Dot(back)) | |
local width2 = math.abs(ac:Dot(back)) | |
local thickness = module.StyleGuide.Triangle.Thickness | |
local w1 = WEDGE:Clone() | |
w1.Size = Vector3.new(thickness, height, width1) | |
w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back) | |
w1.Parent = parent | |
local w2 = WEDGE:Clone() | |
w2.Size = Vector3.new(thickness, height, width2) | |
w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back) | |
w2.Parent = parent | |
for k, v in next, module.StyleGuide.Triangle do | |
if (k ~= "Thickness") then | |
w1[k] = v | |
w2[k] = v | |
end | |
end | |
return w1, w2 | |
end | |
function module.CFrame(parent, cf) | |
local origin = cf.Position | |
local r = cf.RightVector | |
local u = cf.UpVector | |
local b = -cf.LookVector | |
local thickness = module.StyleGuide.CFrame.Thickness | |
local right = draw({ | |
CFrame = CFrame.new(origin + r/2, origin + r); | |
Size = Vector3.new(thickness, thickness, r.Magnitude); | |
Color = module.StyleGuide.CFrame.RightColor3; | |
Parent = parent; | |
}, module.StyleGuide.CFrame.PartProperties) | |
local up = draw({ | |
CFrame = CFrame.new(origin + u/2, origin + u); | |
Size = Vector3.new(thickness, thickness, r.Magnitude); | |
Color = module.StyleGuide.CFrame.UpColor3; | |
Parent = parent; | |
}, module.StyleGuide.CFrame.PartProperties) | |
local back = draw({ | |
CFrame = CFrame.new(origin + b/2, origin + b); | |
Size = Vector3.new(thickness, thickness, u.Magnitude); | |
Color = module.StyleGuide.CFrame.BackColor3; | |
Parent = parent; | |
}, module.StyleGuide.CFrame.PartProperties) | |
return right, up, back | |
end | |
-- Return | |
return module | |
end | |
function _Draw2D() | |
local module = {} | |
-- Style Guide | |
module.StyleGuide = { | |
Point = { | |
BorderSizePixel = 0; | |
Size = UDim2.new(0, 4, 0, 4); | |
BorderColor3 = Color3.new(0, 0, 0); | |
BackgroundColor3 = Color3.new(0, 1, 0); | |
}, | |
Line = { | |
Thickness = 1; | |
BorderSizePixel = 0; | |
BorderColor3 = Color3.new(0, 0, 0); | |
BackgroundColor3 = Color3.new(0, 1, 0); | |
}, | |
Ray = { | |
Thickness = 1; | |
BorderSizePixel = 0; | |
BorderColor3 = Color3.new(0, 0, 0); | |
BackgroundColor3 = Color3.new(0, 1, 0); | |
}, | |
Triangle = { | |
ImageTransparency = 0; | |
ImageColor3 = Color3.new(0, 1, 0); | |
} | |
} | |
-- CONSTANTS | |
local HALF = Vector2.new(0.5, 0.5) | |
local RIGHT = "rbxassetid://2798177521" | |
local LEFT = "rbxassetid://2798177955" | |
local IMG = Instance.new("ImageLabel") | |
IMG.BackgroundTransparency = 1 | |
IMG.AnchorPoint = HALF | |
IMG.BorderSizePixel = 0 | |
local FRAME = Instance.new("Frame") | |
FRAME.BorderSizePixel = 0 | |
FRAME.Size = UDim2.new(0, 0, 0, 0) | |
FRAME.BackgroundColor3 = Color3.new(1, 1, 1) | |
-- Functions | |
function draw(properties, style) | |
local frame = FRAME:Clone() | |
for k, v in next, properties do | |
frame[k] = v | |
end | |
if (style) then | |
for k, v in next, style do | |
if (k ~= "Thickness") then | |
frame[k] = v | |
end | |
end | |
end | |
return frame | |
end | |
function module.Draw(parent, properties) | |
properties.Parent = parent | |
return draw(properties, nil) | |
end | |
function module.Point(parent, v2) | |
return draw({ | |
AnchorPoint = HALF; | |
Position = UDim2.new(0, v2.x, 0, v2.y); | |
Parent = parent; | |
}, module.StyleGuide.Point) | |
end | |
function module.Line(parent, a, b) | |
local v = (b - a) | |
local m = (a + b)/2 | |
return draw({ | |
AnchorPoint = HALF; | |
Position = UDim2.new(0, m.x, 0, m.y); | |
Size = UDim2.new(0, module.StyleGuide.Line.Thickness, 0, v.magnitude); | |
Rotation = math.deg(math.atan2(v.y, v.x)) - 90; | |
BackgroundColor3 = Color3.new(1, 1, 0); | |
Parent = parent; | |
}, module.StyleGuide.Line) | |
end | |
function module.Ray(parent, origin, direction) | |
local a, b = origin, origin + direction | |
local v = (b - a) | |
local m = (a + b)/2 | |
return draw({ | |
AnchorPoint = HALF; | |
Position = UDim2.new(0, m.x, 0, m.y); | |
Size = UDim2.new(0, module.StyleGuide.Ray.Thickness, 0, v.magnitude); | |
Rotation = math.deg(math.atan2(v.y, v.x)) - 90; | |
Parent = parent; | |
}, module.StyleGuide.Ray) | |
end | |
function module.Triangle(parent, a, b, c) | |
local ab, ac, bc = b - a, c - a, c - b | |
local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc) | |
if (abd > acd and abd > bcd) then | |
c, a = a, c | |
elseif (acd > bcd and acd > abd) then | |
a, b = b, a | |
end | |
ab, ac, bc = b - a, c - a, c - b | |
local unit = bc.unit | |
local height = unit:Cross(ab) | |
local flip = (height >= 0) | |
local theta = math.deg(math.atan2(unit.y, unit.x)) + (flip and 0 or 180) | |
local m1 = (a + b)/2 | |
local m2 = (a + c)/2 | |
local w1 = IMG:Clone() | |
w1.Image = flip and RIGHT or LEFT | |
w1.AnchorPoint = HALF | |
w1.Size = UDim2.new(0, math.abs(unit:Dot(ab)), 0, height) | |
w1.Position = UDim2.new(0, m1.x, 0, m1.y) | |
w1.Rotation = theta | |
w1.Parent = parent | |
local w2 = IMG:Clone() | |
w2.Image = flip and LEFT or RIGHT | |
w2.AnchorPoint = HALF | |
w2.Size = UDim2.new(0, math.abs(unit:Dot(ac)), 0, height) | |
w2.Position = UDim2.new(0, m2.x, 0, m2.y) | |
w2.Rotation = theta | |
w2.Parent = parent | |
for k, v in next, module.StyleGuide.Triangle do | |
w1[k] = v | |
w2[k] = v | |
end | |
return w1, w2 | |
end | |
-- Return | |
return module | |
end | |
function _DrawClass() | |
local Draw2DModule = _Draw2D() | |
local Draw3DModule = _Draw3D() | |
-- | |
local DrawClass = {} | |
local DrawClassStorage = setmetatable({}, {__mode = "k"}) | |
DrawClass.__index = DrawClass | |
function DrawClass.new(parent) | |
local self = setmetatable({}, DrawClass) | |
self.Parent = parent | |
DrawClassStorage[self] = {} | |
self.Draw3D = {} | |
for key, func in next, Draw3DModule do | |
self.Draw3D[key] = function(...) | |
local returns = {func(self.Parent, ...)} | |
for i = 1, #returns do | |
table.insert(DrawClassStorage[self], returns[i]) | |
end | |
return unpack(returns) | |
end | |
end | |
self.Draw2D = {} | |
for key, func in next, Draw2DModule do | |
self.Draw2D[key] = function(...) | |
local returns = {func(self.Parent, ...)} | |
for i = 1, #returns do | |
table.insert(DrawClassStorage[self], returns[i]) | |
end | |
return unpack(returns) | |
end | |
end | |
return self | |
end | |
-- | |
function DrawClass:Clear() | |
local t = DrawClassStorage[self] | |
while (#t > 0) do | |
local part = table.remove(t) | |
if (part) then | |
part:Destroy() | |
end | |
end | |
DrawClassStorage[self] = {} | |
end | |
-- | |
return DrawClass | |
end | |
--END TEST | |
local PLAYERS = game:GetService("Players") | |
local GravityController = _GravityController() | |
local Controller = GravityController.new(PLAYERS.LocalPlayer) | |
local DrawClass = _DrawClass() | |
local PI2 = math.pi*2 | |
local ZERO = Vector3.new(0, 0, 0) | |
local LOWER_RADIUS_OFFSET = 3 | |
local NUM_DOWN_RAYS = 24 | |
local ODD_DOWN_RAY_START_RADIUS = 3 | |
local EVEN_DOWN_RAY_START_RADIUS = 2 | |
local ODD_DOWN_RAY_END_RADIUS = 1.66666 | |
local EVEN_DOWN_RAY_END_RADIUS = 1 | |
local NUM_FEELER_RAYS = 9 | |
local FEELER_LENGTH = 2 | |
local FEELER_START_OFFSET = 2 | |
local FEELER_RADIUS = 3.5 | |
local FEELER_APEX_OFFSET = 1 | |
local FEELER_WEIGHTING = 8 | |
function GetGravityUp(self, oldGravityUp) | |
local ignoreList = {} | |
for i, player in next, PLAYERS:GetPlayers() do | |
ignoreList[i] = player.Character | |
end | |
-- get the normal | |
local hrpCF = self.HRP.CFrame | |
local isR15 = (self.Humanoid.RigType == Enum.HumanoidRigType.R15) | |
local origin = isR15 and hrpCF.p or hrpCF.p + 0.35*oldGravityUp | |
local radialVector = math.abs(hrpCF.LookVector:Dot(oldGravityUp)) < 0.999 and hrpCF.LookVector:Cross(oldGravityUp) or hrpCF.RightVector:Cross(oldGravityUp) | |
local centerRayLength = 25 | |
local centerRay = Ray.new(origin, -centerRayLength * oldGravityUp) | |
local centerHit, centerHitPoint, centerHitNormal = workspace:FindPartOnRayWithIgnoreList(centerRay, ignoreList) | |
--[[disable | |
DrawClass:Clear() | |
DrawClass.Draw3D.Ray(centerRay.Origin, centerRay.Direction) | |
]] | |
local downHitCount = 0 | |
local totalHitCount = 0 | |
local centerRayHitCount = 0 | |
local evenRayHitCount = 0 | |
local oddRayHitCount = 0 | |
local mainDownNormal = ZERO | |
if (centerHit) then | |
mainDownNormal = centerHitNormal | |
centerRayHitCount = 0 | |
end | |
local downRaySum = ZERO | |
for i = 1, NUM_DOWN_RAYS do | |
local dtheta = PI2 * ((i-1)/NUM_DOWN_RAYS) | |
local angleWeight = 0.25 + 0.75 * math.abs(math.cos(dtheta)) | |
local isEvenRay = (i%2 == 0) | |
local startRadius = isEvenRay and EVEN_DOWN_RAY_START_RADIUS or ODD_DOWN_RAY_START_RADIUS | |
local endRadius = isEvenRay and EVEN_DOWN_RAY_END_RADIUS or ODD_DOWN_RAY_END_RADIUS | |
local downRayLength = centerRayLength | |
local offset = CFrame.fromAxisAngle(oldGravityUp, dtheta) * radialVector | |
local dir = (LOWER_RADIUS_OFFSET * -oldGravityUp + (endRadius - startRadius) * offset) | |
local ray = Ray.new(origin + startRadius * offset, downRayLength * dir.unit) | |
local hit, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList) | |
--[[disable | |
DrawClass.Draw3D.Ray(ray.Origin, ray.Direction) | |
]] | |
if (hit) then | |
downRaySum = downRaySum + angleWeight * hitNormal | |
downHitCount = downHitCount + 1 | |
if isEvenRay then | |
evenRayHitCount = evenRayHitCount + 1 | |
else | |
oddRayHitCount = oddRayHitCount + 1 | |
end | |
end | |
end | |
local feelerHitCount = 0 | |
local feelerNormalSum = ZERO | |
for i = 1, NUM_FEELER_RAYS do | |
local dtheta = 2 * math.pi * ((i-1)/NUM_FEELER_RAYS) | |
local angleWeight = 0.25 + 0.75 * math.abs(math.cos(dtheta)) | |
local offset = CFrame.fromAxisAngle(oldGravityUp, dtheta) * radialVector | |
local dir = (FEELER_RADIUS * offset + LOWER_RADIUS_OFFSET * -oldGravityUp).unit | |
local feelerOrigin = origin - FEELER_APEX_OFFSET * -oldGravityUp + FEELER_START_OFFSET * dir | |
local ray = Ray.new(feelerOrigin, FEELER_LENGTH * dir) | |
local hit, hitPoint, hitNormal = workspace:FindPartOnRayWithIgnoreList(ray, ignoreList) | |
--[[disable | |
DrawClass.Draw3D.Ray(ray.Origin, ray.Direction) | |
]] | |
if (hit) then | |
feelerNormalSum = feelerNormalSum + FEELER_WEIGHTING * angleWeight * hitNormal --* hitDistSqInv | |
feelerHitCount = feelerHitCount + 1 | |
end | |
end | |
if (centerRayHitCount + downHitCount + feelerHitCount > 0) then | |
local normalSum = mainDownNormal + downRaySum + feelerNormalSum | |
if (normalSum ~= ZERO) then | |
return normalSum.unit | |
end | |
end | |
return oldGravityUp | |
end | |
Controller.GetGravityUp = GetGravityUp | |
-- E is toggle | |
game:GetService("ContextActionService"):BindAction("Toggle", function(action, state, input) | |
if not (state == Enum.UserInputState.Begin) then | |
return | |
end | |
if (Controller) then | |
Controller:Destroy() | |
Controller = nil | |
else | |
Controller = GravityController.new(PLAYERS.LocalPlayer) | |
Controller.GetGravityUp = GetGravityUp | |
end | |
end, false, Enum.KeyCode.Z) | |
print("end") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment