So I modified Egomoose’s WallStick gravity controller so I can use it in my game and I found something really interesting.
Im working on a planet exploration game and when these planets are set in 0,0,0 the controller works just fine as shown in the video below:
However when the planet’s primarypart CFrame starts getting farther away from 0,0,0 the controller starts giving weird and off CFrames:
I think the issue might be in these two codes:
Code 1: (Module)
–[[
This is a class that provides custom physics that allows things like walking upside down or on moving vehicle like planes, etc.
API:
Constructors and static functions:
WallStick.new(Player player)
> Creates a WallStick object for the given player.
WallStick.WaitForAppearance(Player player)
> Yields until the player’s appearance has fully loaded.
Methods:
WallStick:Destroy()
> Destroys the WallStick object. This returns the player back to the standard humanoid based character controller.
WallStick:SetCameraMode(mode)
> Modes are “Custom” which rotates with the character. “Default” which is the standard camera (keep in mind default camera
will NOT properly calculate the humanoid move direction). “Debug” is used to view the physics character. Keep in mind
DEBUG_TRANSPARENCY should not be set to 1 so you can actually see stuff.
WallStick:GetFloorAndNormal()
> This is a developer set method. You use it to define the current part the player is standing on and the object space normal
that they should be oriented to on that part.
> By default this returns nil, nil and thus does not change what the player is oriented to.
WallStick:Set(part, objectNormal, newCF)
> This method is used to manually set the part and object normal that the player is oriented to. The newCF parameter is optional
and can be used to CFrame the player to a new position.
Properties:
WallStick.Part
> The current part the player is relative to.
WallStick.Normal
> The current object space normal the player is oriented to.
WallStick.PhysicsFloor
> The floor part that the physics character is currently on top of.
> This is useful for converting values from the physics space to the real space and back.
WallStick.Character
> The player’s Character
WallStick.Humanoid
> The player’s Humanoid
WallStick.HRP
> The player’s HumanoidRootPart
WallStick.PhysicsCharacter
> The player’s physics Character
WallStick.PhysicsHumanoid
> The player’s physics Humanoid
WallStick.PhysicsHRP
> The player’s physics HumanoidRootPart
Enjoy!
- EgoMoose
–]]
local TERRAIN = game:GetService(“Workspace”):WaitForChild(“Terrain”)
local PLAYERS = game:GetService(“Players”)
local RUNSERVICE = game:GetService(“RunService”)
local PHYSSERVICE = game:GetService(“PhysicsService”)
local COLLIDE_WITH_PLAYERS = true
local WORLD_CENTER = Vector3.new(10000, 0, 0)
local COLLIDER_SIZE2 = Vector3.new(64, 64, 64) / 2
local WALLSTICKCHARID = PHYSSERVICE:GetCollisionGroupId(“WallStickCharacters”)
local DEBUG = false
local DEBUG_TRANSPARENCY = DEBUG and 0 or 1
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_XZ = Vector3.new(1, 0, 1)
local VEC_YZ = Vector3.new(0, 1, 1)
–
local CharacterAppearanceLoaded = script:WaitForChild(“CharacterAppearanceLoaded”)
local PhysicsReplicationEvent = script:WaitForChild(“PhysicsReplicationEvent”)
local PhysicsCharacter = require(script:WaitForChild(“PhysicsCharacter”))
local AnimationReplicator = require(script:WaitForChild(“AnimationReplicator”))
– 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 setCollisionGroupId(array, id)
for _, part in next, array do
if (part:IsA(“BasePart”)) then
part.CollisionGroupId = id
end
end
end
– Class
local WallStick = {}
WallStick.__index = WallStick
– Public Constructors
function WallStick.new(player)
local self = setmetatable({}, WallStick)
-- fix slow load bug
local loaded = player.PlayerScripts:WaitForChild("PlayerScriptsLoader"):WaitForChild("Loaded")
if (not loaded.Value) then
loaded.Changed:Wait()
end
--
self.Part = nil
self.Normal = UNIT_Y -- Object Space
self.PhysicsFloor = nil
self.CollisionParts = {}
self.LastTick = tick()
self.IsOtherFrame = false
--
self.PhysicsWorld = Instance.new("Model")
self.PhysicsWorld.Name = "PhysicsWorld"
self.PhysicsWorld.Parent = game.Workspace
self.PhysicsCollision = Instance.new("Model")
self.PhysicsCollision.Name = "PhysicsCollision"
self.PhysicsCollision.Parent = self.PhysicsWorld
--
self.Player = player
self.Character = player.Character
self.Humanoid = self.Character:WaitForChild("Humanoid")
self.HRP = self.Character:WaitForChild("HumanoidRootPart")
self.PhysicsCharacter = PhysicsCharacter(self.Character, DEBUG_TRANSPARENCY)
self.PhysicsHumanoid = self.PhysicsCharacter:WaitForChild("Humanoid")
self.PhysicsHRP = self.PhysicsCharacter:WaitForChild("HumanoidRootPart")
self.PhysicsCharacter.Parent = self.PhysicsWorld
self.Gyro = script:WaitForChild("BodyGyro"):Clone()
setCollisionGroupId(self.Character:GetChildren(), WALLSTICKCHARID)
--
local playerScripts = require(player.PlayerScripts:WaitForChild("PlayerModule"))
self.Controls = playerScripts:GetControls()
self.Camera = playerScripts:GetCameras()
self.Camera.TransitionRate = 1
self.CameraUp = UNIT_Y
self:SetCameraMode(DEBUG and "Debug" or "Custom")
function self.Camera.GetUpVector(this, oldUpVector)
return self.CameraUp
end
--
self.AnimationReplicator = AnimationReplicator.new(self.Character)
self.AnimationReplicator:SetRepHumanoid(self.PhysicsHumanoid)
--
self.Humanoid.PlatformStand = true
self.DeathConnection = self.Humanoid.Died:Connect(function()
self:Destroy()
end)
self.AncestryConnection = self.Character.AncestryChanged:Connect(function(_, parent)
if (not parent) then
self:Destroy()
end
end)
--
RUNSERVICE:BindToRenderStep("WallStickUpdate", Enum.RenderPriority.Camera.Value - 1, function(dt)
self:OnStep(dt)
self:OnCollisionStep(dt)
self:OnCharacterMovement(dt)
end)
self:Set(TERRAIN, UNIT_Y)
return self
end
function WallStick.WaitForAppearance(player)
CharacterAppearanceLoaded:InvokeServer()
end
– Public Methods
function WallStick:Destroy()
setCollisionGroupId(self.Character:GetChildren(), 0)
PhysicsReplicationEvent:FireServer(nil, nil, true)
self.Humanoid.PlatformStand = false
self.HRP.Velocity = self.Part.CFrame:VectorToWorldSpace(self.PhysicsFloor.CFrame:VectorToObjectSpace(self.PhysicsHRP.Velocity))
self.HRP.RotVelocity = self.Part.CFrame:VectorToWorldSpace(self.PhysicsFloor.CFrame:VectorToObjectSpace(self.PhysicsHRP.RotVelocity))
self.AnimationReplicator:SetRepHumanoid(self.Humanoid)
self:SetCameraMode("Default")
self.CameraUp = UNIT_Y
self.DeathConnection:Disconnect()
self.AncestryConnection:Disconnect()
RUNSERVICE:UnbindFromRenderStep("WallStickUpdate")
self.PhysicsWorld:Destroy()
end
function WallStick:SetCameraMode(mode)
local cam = workspace.CurrentCamera
if (mode == "Custom") then
cam.CameraSubject = self.Humanoid
elseif (mode == "Default") then
cam.CameraSubject = self.Humanoid
self.CameraUp = UNIT_Y
self.Camera:SetSpinPart(TERRAIN)
elseif (mode == "Debug") then
cam.CameraSubject = self.PhysicsHumanoid
end
self.CameraMode = mode
end
function WallStick:GetFloorAndNormal(lastPart, lastNormal, lastTick)
– written by the dev using the controlller
– return part, objectSpaceNormal
end
function WallStick:Set(newPart, newNormal, newCF)
local lastPart = self.Part
local lastNormal = self.Normal
self.Part = newPart
self.Normal = newNormal
local physVel = self.PhysicsHRP.Velocity
local physRotVel = self.PhysicsHRP.RotVelocity
-- create a new/updated physics floor
if (self.PhysicsFloor) then
self.PhysicsFloor:Destroy()
end
local isTerrain = (newPart.ClassName == "Terrain")
local isSpawn = (newPart.ClassName == "SpawnLocation")
local isSeat = (newPart.ClassName == "Seat" or newPart.ClassName == "VehicleSeat")
local floor
if (isTerrain or isSpawn or isSeat) then
floor = Instance.new("Part")
floor.CanCollide = isSpawn and newPart.CanCollide or false
floor.Size = not isTerrain and newPart.Size or ZERO
else
floor = newPart:Clone()
floor:ClearAllChildren()
end
floor.Name = "PhysicsFloor"
floor.Transparency = DEBUG_TRANSPARENCY
floor.Anchored = true
floor.Velocity = ZERO
floor.RotVelocity = ZERO
floor.CFrame = CFrame.new(WORLD_CENTER) * getRotationBetween(self.Normal, UNIT_Y, UNIT_X)
floor.Parent = self.PhysicsWorld
self.PhysicsFloor = floor
local camera = workspace.CurrentCamera
local cameraOffset = self.PhysicsHRP.CFrame:ToObjectSpace(camera.CFrame)
local focusOffset = self.PhysicsHRP.CFrame:ToObjectSpace(camera.Focus)
-- character/camera positioning
if (self.CollisionParts[newPart]) then
self.CollisionParts[newPart]:Destroy()
self.CollisionParts[newPart] = nil
end
-- adjust this
local vel = self.PhysicsHRP.CFrame:VectorToObjectSpace(self.PhysicsHRP.Velocity)
local rVel = self.PhysicsHRP.CFrame:VectorToObjectSpace(self.PhysicsHRP.RotVelocity)
self.PhysicsHRP.CFrame = self.PhysicsFloor.CFrame:ToWorldSpace(newPart.CFrame:ToObjectSpace(newCF or self.HRP.CFrame))
self.PhysicsHRP.Velocity = self.PhysicsHRP.CFrame:VectorToWorldSpace(vel)
self.PhysicsHRP.RotVelocity = self.PhysicsHRP.CFrame:VectorToWorldSpace(rVel)
-- update the camera
if (self.CameraMode == "Custom") then
self.Camera:SetSpinPart(newPart)
elseif (self.CameraMode == "Debug") then
camera.CFrame = self.PhysicsHRP.CFrame:ToWorldSpace(cameraOffset)
camera.Focus = self.PhysicsHRP.CFrame:ToWorldSpace(focusOffset)
end
-- update the tick
self.LastTick = tick()
end
–
function WallStick:OnCollisionStep(dt)
local parts = workspace:FindPartsInRegion3(Region3.new(
self.HRP.Position - COLLIDER_SIZE2,
self.HRP.Position + COLLIDER_SIZE2
), self.Character, 1000)
local newCollisionParts = {}
local collisionParts = self.CollisionParts
local stickPart = self.Part
local stickPartCF = stickPart.CFrame
local physicsFloorCF = self.PhysicsFloor.CFrame
for _, part in next, parts do
if (collisionParts[part]) then
local cPart = collisionParts[part]
cPart.CFrame = physicsFloorCF:ToWorldSpace(stickPartCF:ToObjectSpace(part.CFrame))
cPart.CanCollide = part.CanCollide
newCollisionParts[part] = cPart
elseif (part ~= stickPart and not part:IsDescendantOf(self.Character) and not part:IsDescendantOf(self.PhysicsWorld) and part.CanCollide and (COLLIDE_WITH_PLAYERS or not PLAYERS:GetPlayerFromCharacter(part.Parent))) then
local cPart
local isSeat = (part.ClassName == "Seat" or part.ClassName == "VehicleSeat")
if (part.ClassName == "SpawnLocation" or isSeat) then
cPart = Instance.new("Part")
cPart.CanCollide = part.CanCollide
cPart.Size = part.Size
else
cPart = part:Clone()
cPart:ClearAllChildren()
end
cPart.Transparency = DEBUG_TRANSPARENCY
cPart.Anchored = true
cPart.Velocity = ZERO
cPart.RotVelocity = ZERO
cPart.Parent = self.PhysicsCollision
newCollisionParts[part] = cPart
self.CollisionParts[part] = cPart
end
end
if (self.PhysicsFloor) then
self.PhysicsFloor.CanCollide = stickPart.CanCollide
end
for part, cPart in next, self.CollisionParts do
if (not newCollisionParts[part]) then
self.CollisionParts[part] = nil
cPart:Destroy()
end
end
end
function WallStick:OnStep(dt)
self.HRP.Velocity = ZERO
self.HRP.RotVelocity = ZERO
local hrpOffset = self.PhysicsFloor.CFrame:ToObjectSpace(self.PhysicsHRP.CFrame)
self.HRP.CFrame = self.Part.CFrame:ToWorldSpace(hrpOffset)
local newPart, newNormal = self:GetFloorAndNormal(self.Part, self.Normal, self.LastTick)
if (newPart and newNormal) then
self:Set(newPart, newNormal)
elseif (not self.Part.Parent) then
self:Set(TERRAIN, UNIT_Y)
end
-- server replication
self.OtherFrame = not self.OtherFrame
if (self.OtherFrame) then
local hrpOffset = self.PhysicsFloor.CFrame:ToObjectSpace(self.PhysicsHRP.CFrame)
PhysicsReplicationEvent:FireServer(self.Part, hrpOffset, false)
end
self.PhysicsHumanoid.WalkSpeed = self.Humanoid.WalkSpeed
self.PhysicsHumanoid.JumpPower = self.Humanoid.JumpPower
self.PhysicsHumanoid.Jump = self.Humanoid.Jump
-- camera
if (self.CameraMode == "Custom") then
self.CameraUp = self.Part.CFrame:VectorToWorldSpace(self.Normal)
end
-- mouse lock
local pHRPCF = self.PhysicsHRP.CFrame
local pCamCF = pHRPCF * self.HRP.CFrame:ToObjectSpace(workspace.CurrentCamera.CFrame)
self.Gyro.CFrame = CFrame.new(pHRPCF.p, pHRPCF.p + pCamCF.LookVector * VEC_XZ)
self.Gyro.Parent = self.Camera:IsCamRelative() and self.PhysicsHRP or nil
end
function WallStick:OnCharacterMovement(dt)
local move = self.Controls:GetMoveVector()
if (self.CameraMode ~= "Debug") then
local physCamCF = self.PhysicsHRP.CFrame * self.HRP.CFrame:ToObjectSpace(workspace.CurrentCamera.CFrame)
local c, s
local _, _, _, R00, R01, R02, _, R11, R12, _, _, R22 = physCamCF:GetComponents()
local q = math.sign(R11)
if R12 < 1 and R12 > -1 then
c = R22
s = R02
else
c = R00
s = -R01*math.sign(R12)
end
local norm = math.sqrt(c*c + s*s)
local physMove = Vector3.new(
(c*move.x*q + s*move.z)/norm,
0,
(c*move.z - s*move.x*q)/norm
)
self.PhysicsHumanoid:Move(physMove, false)
else
self.PhysicsHumanoid:Move(move, true)
end
end
–
return WallStick
Code 2: (LocalScript)
– CONSTANTS
local COLLECTION = game:GetService(“CollectionService”)
local REPSTORAGE = game:GetService(“ReplicatedStorage”)
local RUNSERVICE = game:GetService(“RunService”)
local PLAYERS = game:GetService(“Players”)
local LOCALPLAYER = PLAYERS.LocalPlayer
local TERRAIN = workspace.Terrain
local DISABLE_STATES = {
[Enum.HumanoidStateType.Physics] = true;
[Enum.HumanoidStateType.Seated] = true;
}
local WallStickClass = require(REPSTORAGE:WaitForChild(“WallStick”))
local Raycast = require(script:WaitForChild(“Raycast”))
local Character = script.Parent
local Humanoid = Character:WaitForChild(“Humanoid”)
local HRP = Character:WaitForChild(“HumanoidRootPart”)
– Special tags for different parts
local function getTags(tag)
local dict = {}
for _, object in next, COLLECTION:GetTagged(tag) do
dict[object] = true
for _, child in next, object:GetDescendants() do
dict[child] = true
end
end
return dict
end
local fastTransitionParts = getTags(“FastTransitionParts”)
local ignoreParts = getTags(“IgnoreParts”)
local maintainParts = getTags(“MaintainParts”)
– Wallstick control
local wallstick = nil
local targetRate = 1
local floor = TERRAIN
local lNormal = Vector3.new(0, 1, 0)
local function lerp(a, b, t)
return (b - a)*t + a
end
local function clampCloseToOne(t, margin)
if (math.abs(1 - t) < margin) then
return 1
end
return t
end
local function raycastFromCharacter()
local characters = {}
for i, player in next, PLAYERS:GetPlayers() do
characters[i] = player.Character
end
local hrpCF = HRP.CFrame
local height = Character:GetExtentsSize().y * 0.75
local ray = Ray.new(hrpCF.p, -height*hrpCF.UpVector)
return Raycast.FindPartOnRayWithCallbackWithIgnoreList(ray, characters, false, true, 5, function(hit, position, normal, material)
if (not hit) then
return Raycast.CallbackResult.Finished
elseif (not hit.CanCollide) then
return Raycast.CallbackResult.Finished
else
table.insert(characters, hit)
return Raycast.CallbackResult.Continue
end
end)
end
local function getFloorAndNormal(self)
if (tick() - self.LastTick < 0.3) then
return
end
if (floor ~= self.Part) then
return floor, lNormal
end
end
local function setWallstickEnabled(bool)
if (DISABLE_STATES[Humanoid:GetState()]) then
bool = false
end
if (not bool and wallstick) then
wallstick:Destroy()
wallstick = nil
elseif (bool and not wallstick) then
wallstick = WallStickClass.new(LOCALPLAYER)
wallstick.GetFloorAndNormal = getFloorAndNormal
targetRate = wallstick.Camera.TransitionRate
end
end
local function onPhysicsStep(dt)
local hit, pos, normal = raycastFromCharacter()
setWallstickEnabled(hit ~= TERRAIN)
if (not wallstick) then return end
-- handle camera transitioning
if (fastTransitionParts[hit] or (hit and not hit.Anchored)) then
targetRate = 1
else
wallstick.Camera.TransitionRate = 0.15
targetRate = 0.15
end
wallstick.Camera.TransitionRate = clampCloseToOne(lerp(wallstick.Camera.TransitionRate, targetRate, 0.1), 1E-4)
-- set the new floor and it's relative up vector (unless it's an ignore part)
if (hit and not hit.CanCollide and not ignoreParts[hit]) then
local rNormal = hit.CFrame:VectorToObjectSpace(normal)
if (maintainParts[hit]) then
local n = wallstick.Part.CFrame:VectorToWorldSpace(wallstick.Normal)
rNormal = hit.CFrame:VectorToObjectSpace(n)
end
if (hit ~= wallstick.Part) then
floor, lNormal = hit, rNormal
end
end
if (not floor:IsDescendantOf(workspace)) then
floor = TERRAIN
lNormal = Vector3.new(0, 1, 0)
end
end
– Init
WallStickClass.WaitForAppearance(LOCALPLAYER)
setWallstickEnabled(true)
Humanoid.StateChanged:Connect(function(oldState, newState)
if (DISABLE_STATES[newState]) then
setWallstickEnabled(false)
end
end)
RUNSERVICE.Heartbeat:Connect(onPhysicsStep)
game.ReplicatedStorage:WaitForChild(“TP”).OnClientEvent:Connect(function()
wallstick:Destroy()
wallstick = nil
end)
I’d really appreciate some help as I’ve been trying to solve this since september so yeah.
Anyways any reply will result incredibly helpful