Trying to create a custom swimming system

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.

Where should I start?

2 Likes

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.

2 Likes

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!

2 Likes

Good idea! I’ll try this, and see where I get.

1 Like

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)```
1 Like

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.

1 Like

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.

1 Like

To add more insight, this is what is happening.

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.

1 Like
  • 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.
task.wait(0.1)
rootPart.Position = swimRaycast.Position
playerVelocity.Enabled = true
playerVelocity.VectorVelocity = Vector3.new(0, -0.1, 0)

And lastly, since you’re using a debug block already; try to visualize where the Raycast hits, and adjust properly if it isn’t proper.

SIDE NOTE I woke up just now, so I apologize if I did any mistakes.

1 Like

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.

1 Like

1. Detecting the Torso

  • 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.

2 Likes

Check out the documentation of RemoteEvents if you want further explanation.

1 Like

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

1 Like

Hey! Thanks for your suggestion, I went with something similar, but I got it working myself! Already have it a little bit polished up.

Thank you all for your help!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.