Short description of the issue: The first players first to join the game everything has always worked works as intended. As new players join, an unexplainable “shaking” occurs in their character.
Videos:
Intended behavior:
Unintended behavior:
MY SETUP
-
The Character Model
When a player joins, the client will verify if they are in VR mode. If so, the server will delete everything in their character. A folder named “VRFolder” is created and parented inside their Character model. I set up a ChildAdded event to delete anything new added that isn’t the VR folder. This is nice, because when the player respawns or leaves, this is automatically cleaned up for me.
-
The Base
To make each VR character work, I have a “Base” model which is just three unanchored 1x1x1 blocks that represent the position of the headset, right controller, and left controller. They are all unanchored, cancollide is true, and massless is false.
-
The Head
I have a few different head models that are all set up in the same exact way. They are all unanchored parts that are welded (with a weldconstraint) to the center head part. The “eyes” are the only parts that have cancollide false.
-
The Hands
I have two different sets of hands: small and large. The small hands are set up in the exact same way the large ones are, just half the size. The “wrist” is welded (with a weldconstraint) to the palm, and each finger is its own model and connected with Motor6D’s, which allows for finger movement.
-
Putting it all together
When the Player is ready to create their character, the base is cloned, a head is selected and cloned, and a set of hands is cloned. The head and hands are parented to the Base, and the Base is parented to the VR folder. Attachments are created for the HeadCF and Head (the part inside the model) and then an AlignPosition and AlignOrientation are created and parented to the HeadCF. The hands are also set up in the same manner, using the (Right/Left)HandCF and the Palm in the respective hand model. Details of the AlignPosition and AlignOrientation are attached:
Finally, I loop through every part of the Base model (and its descendants, so that includes the hands/head) and set the network ownership of the parts to the Player using SetNetworkOwner. This gives the client full control over the character, and allows me to update the CFrame of the HeadCF/RightHandCF/LeftHandCF on the client and since the AlignPosition and AlignOrientations are set up, when the CFrame is updated all the parts move.
Solutions I have tried:
I have tried creating the hands solely on the server and making the client request to update the positions and while a lot less common, this bug still occurs- and the latency makes it horrifying to play with.
Note: I am using Knit.
Server Code
function Service.Client:JoinTeam(Player: Player, TeamName: string)
local VRFolder = Player.Character:FindFirstChild("VRFolder")
if not VRFolder and not VRFolder:FindFirstChild("Base") or typeof(TeamName) ~= "string" then return end
local Team = Teams:FindFirstChild(TeamName)
if not Team then return end
Player.Team = Team
local Base = VRAssets.Base:Clone()
local HeadScale
local Head
if Team.Name == "Ascended" then
if Player.UserId == game.CreatorId then HeadScale = 8 Head = VRAssets.Heads.VRBrickGod:Clone() else HeadScale = 4 Head = VRAssets.Heads.VRGoodDemiGod:Clone() end
else
if Player.UserId == 114316837 then HeadScale = 8 Head = VRAssets.Heads.VREvilBrickGod:Clone() else HeadScale = 4 Head = VRAssets.Heads.VREvilDemiGod:Clone() end
end
local RightHand = HeadScale == 8 and VRAssets.Hands.LargeRH:Clone() or VRAssets.Hands.SmallRH:Clone()
local LeftHand = HeadScale == 8 and VRAssets.Hands.LargeLH:Clone() or VRAssets.Hands.SmallLH:Clone()
local function CreateAttachment(Part: Part)
local NewAttachment = Instance.new("Attachment")
NewAttachment.Parent = Part
return NewAttachment
end
local function CreateAlignPosition(Part: Part, Attachment0Part: Part, Attachment1Part: Part)
local AlignPosition = Instance.new("AlignPosition")
AlignPosition.MaxForce = 100000
AlignPosition.MaxVelocity = math.huge
AlignPosition.Responsiveness = 100
AlignPosition.Attachment0 = Attachment0Part
AlignPosition.Attachment1 = Attachment1Part
AlignPosition.Parent = Part
end
local function CreateAlignOrientation(Part: Part, Attachment0Part: Part, Attachment1Part: Part)
local AlignOrientation = Instance.new("AlignOrientation")
AlignOrientation.MaxAngularVelocity = math.huge
AlignOrientation.MaxTorque = 100000
AlignOrientation.Responsiveness = 50
AlignOrientation.Attachment0 = Attachment0Part
AlignOrientation.Attachment1 = Attachment1Part
AlignOrientation.Parent = Part
end
local function SetupAlignments(CFPart: Part, PrimaryPart: Part)
local CFAttachment = CreateAttachment(CFPart)
local PrimaryAttachment = CreateAttachment(PrimaryPart)
CreateAlignPosition(PrimaryPart, PrimaryAttachment, CFAttachment)
CreateAlignOrientation(PrimaryPart, PrimaryAttachment, CFAttachment)
end
local function SetCollisionGroup(Model: Model)
for _, Part in ipairs(Model:GetDescendants()) do
if not Part:IsA("BasePart") then continue end
PhysicsService:SetPartCollisionGroup(Part, 'VRNonCollide')
end
end
local function Blink()
local Eyes = {}
local function TweenEye(Eye, NewSize)
local Tween = TweenService:Create(Eye, TweenInfo.new(0.2), {Size = NewSize})
Tween:Play()
Tween = nil
end
local function Break()
local VRFolder = Player.Character and Player.Character:FindFirstChild("VRFolder")
if not VRFolder and not VRFolder:FindFirstChild("Base") then return true else return false end
end
for _, Eye in ipairs(Head:GetChildren()) do
if not Eye.Name:find("Eye") then continue end
Eyes[Eye] = Eye.Size
end
task.spawn(function()
while true do
if Break() then break end
task.wait(math.random(2, 4))
for Eye, OriginalSize in pairs(Eyes) do
TweenEye(Eye, Vector3.new(OriginalSize.X, OriginalSize.Y/2, OriginalSize.Z))
delay(0.2, function()
if Break() then return end
TweenEye(Eye, OriginalSize)
end)
end
end
end)
end
local function SetOwnership(Model: Model)
for _, Part in ipairs(Model:GetDescendants()) do
if not Part:IsA("Part") or Part.Anchored then continue end
Part:SetNetworkOwner(Player)
end
end
local function SetNameGui(NameGui)
NameGui.Frame.DisplayName.Text = Player.DisplayName
NameGui.Frame.Username.Text = "("..Player.Name..")"
end
SetupAlignments(Base.RightHandCF, RightHand.Palm)
SetupAlignments(Base.LeftHandCF, LeftHand.Palm)
SetupAlignments(Base.HeadCF, Head.Head)
local NameGui = Head.Head:FindFirstChild("NameGui")
if NameGui then SetNameGui(NameGui) end
RightHand.Name = "RightHand"
LeftHand.Name = "LeftHand"
Head.Parent = Base
RightHand.Parent = Base
LeftHand.Parent = Base
Base.Parent = VRFolder
task.wait()
SetCollisionGroup(VRFolder)
SetOwnership(VRFolder)
Blink()
self.SetupCharacter:Fire(Player, HeadScale, Base) -- This fires to the client to setup client events
end```
Client Code
function Controller:SetupControls(HeadScale: number, Base: Model)
local function GetDirection(MoveX: number, MoveY: number)
local DirectionX = math.abs(MoveX)
local DirectionY = math.abs(MoveY)
if DirectionX > DirectionY then return "X" else return "Y" end
end
local function MoveFingers(Pressure: number, FingerCode: StringValue)
local Hand = (FingerCode == "R1" or FingerCode == "R2" or FingerCode == "A") and Base.RightHand or Base.LeftHand
local FingerGroup = ((FingerCode == "R1" or FingerCode == "L1") and "Grip") or ((FingerCode == "R2" or FingerCode == "L2") and "Trigger") or "Thumb"
local function UpdateFingers()
local function SetFingerM6Ds(FingerName)
for _, M6D in ipairs(Hand:GetDescendants()) do
if not M6D:IsA("Motor6D") or (M6D.Name ~= FingerName and M6D.Parent.Parent.Name ~= FingerName) then continue end
M6D.DesiredAngle = (70 * Pressure) * math.pi/180
end
end
if FingerGroup == "Thumb" then
SetFingerM6Ds("Thumb")
elseif FingerGroup == "Trigger" then
SetFingerM6Ds("Index")
else
SetFingerM6Ds("MiddleFinger")
SetFingerM6Ds("Ring")
SetFingerM6Ds("Pinky")
end
end
UpdateFingers()
end
local function LeftThumbstick(MoveX: number, MoveY: number, Direction: string)
-- Direction Y moves forward/back while Direction X moves to the left/right
local MoveDirection = Direction == "Y" and MoveY or MoveX
local NewVector = Direction == "Y" and Base.HeadCF.CFrame.LookVector or Base.HeadCF.CFrame.RightVector
NewVector = Vector3.new(NewVector.X, 0, NewVector.Z)
Camera.CFrame = Camera.CFrame + NewVector * MoveDirection
end
local function RightThumbstick(MoveX: number, MoveY: number, Direction: string)
-- Direction Y moves up/down while Direction X turns left/right
if Direction == "Y" then Camera.CFrame = Camera.CFrame * CFrame.new(0, MoveY, 0) else Camera.CFrame = Camera.CFrame * CFrame.fromEulerAnglesXYZ(0, -MoveX * 0.02, 0) end
end
local function ThumbstickMoved(Input: InputObject, ThumbstickCode: string)
local MoveY = Input.Position.Y
local MoveX = Input.Position.X
local Direction = GetDirection(MoveX, MoveY)
if ThumbstickCode == "T1" then LeftThumbstick(MoveX, MoveY, Direction) else RightThumbstick(MoveX, MoveY, Direction) end
end
Camera.HeadScale = HeadScale
Base.HeadCF.Anchored = true
Base.RightHandCF.Anchored = true
Base.LeftHandCF.Anchored = true
for _, Object in ipairs(Base:GetDescendants()) do
if Object:IsA("BasePart") and Object.Parent:FindFirstChild("Head") then Object.Transparency = 1 end
if Object:IsA("ParticleEmitter") or Object:IsA("BillboardGui") then Object.Enabled = false end
end
Camera.HeadScale = HeadScale
if Player.Team.Name == "Ascended" then Camera.CFrame = workspace.GoodGodSpawn.CFrame else Camera.CFrame = workspace.EvilGodSpawn.CFrame end
VRService:RecenterUserHeadCFrame()
UserInputService.UserCFrameChanged:Connect(function(BodyPart: UserCFrame, NewCF: CFrame)
local Part = (BodyPart == Enum.UserCFrame.Head and Base.HeadCF) or (BodyPart == Enum.UserCFrame.RightHand and Base.RightHandCF) or (BodyPart == Enum.UserCFrame.LeftHand and Base.LeftHandCF) or nil
if not Part then return end
local ScaledCF = Camera.CFrame * CFrame.new(NewCF.Position * HeadScale) * NewCF.Rotation
Part.CFrame = ScaledCF
local PairedPart = (Part == Base.HeadCF and Base:FindFirstChild("Head", true)) or (Part == Base.RightHandCF and Base.RightHand.Palm) or Base.LeftHand.Palm
if PairedPart and (Part.Position - PairedPart.Position).Magnitude > 30 then PairedPart.CFrame = Part.CFrame end
end)
UserInputService.InputChanged:Connect(function(Input: InputObject, GameProcessed: boolean)
local FingerCode = (Input.KeyCode == Enum.KeyCode.ButtonR2 and "R2") or (Input.KeyCode == Enum.KeyCode.ButtonL2 and "L2") or (Input.KeyCode == Enum.KeyCode.ButtonR1 and "R1") or (Input.KeyCode == Enum.KeyCode.ButtonL1 and "L1") or nil
if FingerCode then local Pressure = Input.Position.Z MoveFingers(Pressure, FingerCode) end
local ThumbstickCode = (Input.KeyCode == Enum.KeyCode.Thumbstick1 and "T1") or (Input.KeyCode == Enum.KeyCode.Thumbstick2 and "T2") or nil
if ThumbstickCode then ThumbstickMoved(Input, ThumbstickCode) end
end)
UserInputService.InputBegan:Connect(function(Input: InputObject, GameProcessed: boolean)
local FingerCode = (Input.KeyCode == Enum.KeyCode.ButtonA and "A") or (Input.KeyCode == Enum.KeyCode.ButtonX and "X") or nil
if FingerCode then MoveFingers(1, FingerCode) end
if Input.KeyCode ~= Enum.KeyCode.ButtonSelect then return end
local Head = Base:FindFirstChild("Head", true)
if Head then Head.CFrame = Base.HeadCF.CFrame end
Base.RightHand.Palm.CFrame = Base.RightHandCF.CFrame
Base.LeftHand.Palm.CFrame = Base.LeftHandCF.CFrame
VRService:RecenterUserHeadCFrame()
end)
UserInputService.InputEnded:Connect(function(Input: InputObject, GameProcessed: boolean)
local FingerCode = (Input.KeyCode == Enum.KeyCode.ButtonA and "A") or (Input.KeyCode == Enum.KeyCode.ButtonX and "X") or nil
if FingerCode then MoveFingers(0, FingerCode) end
end)
end```
Please let me know if you have any solutions I can try. Thanks for reading