I am trying to create a custom swimming system, which is going to be inside of a custom water part, which is just a series of layers that look like this:
I want to make it so that once you touch, roughly the third/fourth layer, you will rise back up to the top and start to swim. You cannot move up or down, only around the surface of the water, and will gradually sink over time.
I tried a couple of solutions that would be done mostly on the server, but I just couldnt crack it. I thought about using a Raycast, but that ended up falling somewhat flat.
For added context, I also found a forum post that highlights most of what I am trying to do, but it utilizes the actual water block built into roblox, which I do not want to do.
Hey! This is definitely a advanced script… I would suggest looking into something called Linear Velocity to achieve the player being pushed up in the water. If I were making this that’s where I would start!
Okay, so I am kind of getting somewhere here. I have it MOSTLY working, the problem that is confusing me, is that if you land on the water from the top, it doesnt work for some reason? Im very confused and don’t know why this is happening.
You will see in the code that im also teleporting a block around, its just for debugging, to visualize a couple of things.
local part = script.Parent
local debugPart = workspace.RaycastBlock
local debounce = {}
part.Touched:Connect(function(player)
local character = player:FindFirstAncestorOfClass("Model")
if character then
if character:FindFirstChild("Humanoid") then
if not debounce[character.Name] then
debounce[character.Name] = true
end
end
end
local rootPart = player.Parent.HumanoidRootPart
local playerAttachment = Instance.new("Attachment", rootPart)
local playerVelocity = Instance.new("LinearVelocity", playerAttachment)
playerVelocity.MaxForce = math.huge
playerVelocity.Attachment0 = playerAttachment
playerVelocity.Enabled = false
local swimRaycast = workspace:Raycast(rootPart.Position + Vector3.new(0, 5, 0), Vector3.new(0, -10, 0))
debugPart.Position = rootPart.Position + Vector3.new(0, 5, 0)
if swimRaycast and swimRaycast.Material == Enum.Material.Glass then
--print(swimRaycast.Position)
debugPart.Position = swimRaycast.Position
rootPart.Position = swimRaycast.Position
playerVelocity.Enabled = true
playerVelocity.VectorVelocity = Vector3.new(0, -0.1, 0)
end
end)
part.TouchEnded:Connect(function(player)
local character = player:FindFirstAncestorOfClass("Model")
if character then
if character:FindFirstChild("Humanoid") then
debounce[character.Name] = nil
end
end
end)```
You could use Touched events on each layer to find out when the player reaches the third or fourth layer.
Or… If you want to improve precision, use a downward Raycast from the player’s position to detect the exact layer they are on.
But once the player touches the third or fourth layer, you can apply an upward force (for example using LinearVelocity or a custom script) to move the player back to the surface. But ensure that this force is only applied when the player is below a certain threshold!
When they get up to the surface, lock the player’s Y-axis position; allowing movement only on the X and Z axes. The player can sink if you slowly reduce the Y-axis over time. You can use a while loop or TweenService for smooth transitions. Use RemoteEvents to sync the server and the client for more smoothness.
You actually almost perfectly described what I did! The layer that they are on doesnt actually matter, as the layers are just there for style, I added a touch event to a layer that the player will ALWAYS be touching, no matter what, if they are trying to swim. That way, they dont start swimming just by touching the water at all. Smoothness, youre right about, I just dont care about that yet until I get it fully working, which im still trying to figure out why the raycast doesnt work in some cases.
When landing on it from the top, the touch even is triggered, and the raycast happens. For some reason though, it just insists on hitting nothing when coming at it from the top.
Make sure that the debounce is reset appropriately. It might be helpful to reset it immediately after TouchEnded or maybe add some additional checks to guarantee the logic only runs when necessary.
You will want to check the player’s vertical velocity before executing the rest of the logic. If the player is falling really fast, you might have to wait until they slow down to a certain threshold before executing the swimming script (or logic, whatever you understand, ill just be calling it logic).
if rootPart.AssemblyLinearVelocity.Y < -10 then
return
end
Adjust the raycast to start slightly above the players position or extend its range to make sure it properly detects the water layer. Also, ensure the Raycast isn’t blocked by other objects.
local swimRaycast = workspace:Raycast(rootPart.Position + Vector3.new(0, 10, 0), Vector3.new(0, -20, 0)) -- These positions might be incorrect, I'm not sure; so adjust them before using it.
Add a small delay before applying LinearVelocity or changing the player’s position to make sure the physics engine has settled.
I have some debugging to do anyway, I want to change it so that the touch event only triggers if the players torso touches it, there are just a number of issues here. I need to make a lot of this happen on the client side for it to work properly, but the problem is that I dont know how to get a remote event to fire on the client when using a Touch event. It always throws the error that the player argument must be a player object.
Ensure that the Touched event is only triggered if the player’s torso (or HumanoidRootPart for R15 rigs) touches the water.
This can be done by checking if the hit part is a child of a player’s character model.
2. Sending a Remote Event to the Client
Once you confirm that the torso has touched the water, find the corresponding player object using Players:GetPlayerFromCharacter(). Then, use a RemoteEvent to communicate with the client.
3. Client-Side Handling
The client-side script will listen for this remote event and handle the swimming mechanics accordingly.
Here’s an example, with your code used. (Serverscript!)
local part = script.Parent
local debounce = {}
-- Reference to the RemoteEvent
local swimRemote = game.ReplicatedStorage:WaitForChild("SwimRemote")
part.Touched:Connect(function(hit)
local character = hit.Parent
local player = game.Players:GetPlayerFromCharacter(character)
if player and hit.Name == "HumanoidRootPart" then
if not debounce[player.Name] then
debounce[player.Name] = true
-- Sends a remote event to the client
swimRemote:FireClient(player)
end
end
end)
part.TouchEnded:Connect(function(hit)
local character = hit.Parent
local player = game.Players:GetPlayerFromCharacter(character)
if player and hit.Name == "HumanoidRootPart" then
debounce[player.Name] = nil
end
end)
And the client sided script:
local swimRemote = game.ReplicatedStorage:WaitForChild("SwimRemote")
swimRemote.OnClientEvent:Connect(function()
local player = game.Players.LocalPlayer
local character = player.Character
local rootPart = character:WaitForChild("HumanoidRootPart")
-- Handle client-side swimming logic here
print("Player has started swimming!")
-- Example: Apply custom effects or physics, I'm a bit tired to do that and the swimming logic too. :(
-- rootPart.Position = someNewPosition
end)
Explanation Below!
Server Script:
The Touched event checks if the hit part belongs to a player’s character and whether it’s the HumanoidRootPart.
If these conditions are met, it uses Players:GetPlayerFromCharacter() to retrieve the player object.
It then triggers the SwimRemote event, sending the player object to the client.
Client Script:
The client listens for the SwimRemote event and, when triggered, runs the swimming logic on the client-side.
This can include adjusting the player’s position, applying forces, or changing animations.
Common errors you may encounter which include:
Debounce Issues: Make sure the debounce logic is correctly set up so that the event doesn’t fire multiple times unnecessarily.
Remote Event Setup: Ensure that the SwimRemote is properly set up in ReplicatedStorage, and that both the server and client scripts correctly reference it.
I ended up doing something a bit different, which worked out as I got almost exactly what I was looking for.
Server side code:
local part = script.Parent
local startSwimOnClient = game.ReplicatedStorage.StartSwimOnClient
local debounce = {}
part.Touched:Connect(function(hit)
local character = hit.Parent
local player = game.Players:GetPlayerFromCharacter(character)
if player and hit.Name == "Head" then
if not debounce[player.Name] then
debounce[player.Name] = true
startSwimOnClient:FireClient(player)
end
end
end)
part.TouchEnded:Connect(function(hit)
local character = hit.Parent
local player = game.Players:GetPlayerFromCharacter(character)
if player and hit.Name == "HumanoidRootPart" then
wait(2)
debounce[player.Name] = nil
end
end)
Client Side Code
isSwimming = false
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local startSwimOnClient = game.ReplicatedStorage.StartSwimOnClient
local swimTweenInfo = TweenInfo.new(1, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, 0)
local Player = game.Players.LocalPlayer
local character = Player.Character or Player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local root = character:WaitForChild("HumanoidRootPart")
local rootAttachment = root:WaitForChild("RootAttachment")
local linearVelocity = Instance.new("LinearVelocity", root)
local partDebug = game.Workspace.RaycastBlock
linearVelocity.MaxForce = math.huge
linearVelocity.Attachment0 = rootAttachment
linearVelocity.Enabled = false
startSwimOnClient.OnClientEvent:Connect(function()
if isSwimming == false then
local swimRaycast = workspace:Raycast(root.Position + Vector3.new(0, 10, 0), Vector3.new(0, -5, 0))
print("Starting Raycast")
if swimRaycast and swimRaycast.Material == Enum.Material.Glass then
isSwimming = true
partDebug.Position = swimRaycast.Position
local swimTween = TweenService:Create(root, swimTweenInfo, {CFrame = partDebug.CFrame + Vector3.new(0, -0.5, 0)})
swimTween:Play()
--root.CFrame = partDebug.CFrame
linearVelocity.VectorVelocity = Vector3.new(0, -0.08, 0)
linearVelocity.Enabled = true
task.wait(30)
linearVelocity.Enabled = false
isSwimming = false
end
end
end)
This works exactly as I want it to, the only problem is that I cannot move around while sinking, because of linear velocity.
Video of what im dealing with here, I KNOW why I cant move, I just dont know how to resolve it. On line 32 I am aware that my X and Z values are set to zero, I just dont know what to replace it with so that I can move in any X and Z direction.
You’ll probably have to set the X and Z values of the linear velocity when the user hits W, A, S, or D with UserInputService. Instead of LinearVelocity, although deprecated a BodyPosition may work and you just set the Y value of the position. That’s how I’ve done it before.
Keep X and Z values to put into the LinearVelocity, and make them 1, 0, or -1 I guess depending on what key is pressed