How is Blox Fruits water system done?

I want to achieve movement in water like Blox Fruits

It looks like its walking the y position stays the same and

It still has the swimming animations and everything, how?

1 Like

Could you provide a video of how your expecting it to look to assist people who see this post.

1 Like

try experimenting with it a little first

if you can somehow salvage this garbage script I made during ancient times, feel free to tweak it to suit your use case

local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local Terrain = game.Workspace.Terrain

local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
local animator = humanoid:FindFirstChildWhichIsA("Animator")
humanoid:SetStateEnabled(Enum.HumanoidStateType.Swimming, false)

local position = character.PrimaryPart:FindFirstChildWhichIsA("BodyPosition") or Instance.new("BodyPosition")
position.Parent = character.PrimaryPart
position.Position = character.PrimaryPart.Position
position.MaxForce = Vector3.zero
local underwater = false
local entered = false
local rising = false
local height = character.PrimaryPart.Position.Y
local delta = 1/30 -- rate i guess idk lol

-- anims
local animations = character:WaitForChild("Animate", 5)
local current_anim: AnimationTrack = nil
local swim_idle: AnimationTrack = animator:LoadAnimation(animations:FindFirstChild("SwimIdle", true))
local swim_move: AnimationTrack = animator:LoadAnimation(animations:FindFirstChild("Swim", true))

local name = script.Name
local folder = player:FindFirstChild(name) or Instance.new("Folder", player)
folder.Name = name

-- clean folder on respawn
for _, thing in pairs(folder:GetChildren()) do
	thing:Destroy()
end

local function GetBoundingBox(character: Model): (CFrame, Vector3)
	character.Archivable = true
	local clone = character:Clone()
	clone.Parent = folder
	for _, v in pairs(clone:GetChildren()) do
		if not v:IsA("BasePart") then
			v:Destroy()
		end
	end
	local cframe, size = clone:GetBoundingBox()
	clone:Destroy()
	return cframe, size
end

-- for keyboard/controller players
local function rise(action: string, state: Enum.UserInputState)
	if state == Enum.UserInputState.Begin and underwater then
		rising = true
		return Enum.ContextActionResult.Sink
	end
	rising = false
	return Enum.ContextActionResult.Pass
end
ContextActionService:BindAction("rise", rise, false, Enum.PlayerActions.CharacterJump, Enum.KeyCode.ButtonA) -- not sure if characterjump will register on A presses

local function get_characters(): {}
	local characters = {}
	for _, plr in pairs(game:GetService("Players"):GetPlayers()) do
		table.insert(characters, plr.Character)
	end
	return characters
end

local function raycast(origin: Vector3, direction: Vector3): RaycastResult
	local params = RaycastParams.new()
	params.RespectCanCollide = true
	params.IgnoreWater = true
	params.FilterType = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = get_characters()
	local ray = game.Workspace:Raycast(origin, direction*50, params)
	return ray
end

local function lerp(a, b, alpha)
	return a + (b - a) * alpha
end

local function clamp(number: number, min: number, max: number): number
	if min > max then
		return math.clamp(number, max, min)
	else
		return math.clamp(number, min, max)
	end
end

-- for mobile players cuz playeractions dont work on mobile for some reason and there's no :IsKeyDown() equivalent for a button
local button: GuiButton
if UserInputService.TouchEnabled then
	button = player.PlayerGui:WaitForChild("TouchGui"):WaitForChild("TouchControlFrame"):WaitForChild("JumpButton")
	button.MouseButton1Down:Connect(function()
		rising = true
	end)
	button.MouseButton1Up:Connect(function()
		rising = false
	end)
end

local other: BodyPosition
local check: BodyGyro

while task.wait() do
	-- for admins
	check = character.PrimaryPart:FindFirstChildWhichIsA("BodyGyro")
	if check then
		if check.MaxTorque.Magnitude > 0 then
			underwater = false
			entered = false
			continue
		end
	end
	
	local bounds, size = GetBoundingBox(character)
	local region = Region3.new(bounds.Position, bounds.Position + (size/2)):ExpandToGrid(4)
	local material = Terrain:ReadVoxels(region, 4)[1][1][1]
	underwater = material == Enum.Material.Water
	
	if underwater then
		-- angular_velocity.Parent = character.PrimaryPart
		local min = raycast(character.PrimaryPart.Position, -character.PrimaryPart.CFrame.UpVector)
		local max = character.PrimaryPart.Position.Y + size.Y/2
		min = (min and min.Position.Y + size.Y/2) or character.PrimaryPart.Position.Y
		local increment = (rising and 1) or -1
		height = lerp(height, clamp(height + increment, min, max), delta*3)
		position.Position = Vector3.new(bounds.Position.X, height, bounds.Position.Z)
		position.MaxForce = Vector3.new(0, math.huge, 0) -- Vector3.new(0, game.Workspace.Gravity, 0)
		if not entered then
			entered = true
			height -= 1
		end
		if humanoid.MoveDirection.Magnitude > 0 and current_anim ~= swim_move then
			current_anim = swim_move
			swim_move:Play(0.5)
			swim_idle:Stop(0.5)
			humanoid.AutoRotate = true
		elseif humanoid.MoveDirection.Magnitude == 0 and current_anim ~= swim_idle then
			current_anim = swim_idle
			swim_idle:Play(0.5)
			swim_move:Stop(0.5)
		end
	else
		-- angular_velocity.Parent = folder
		if current_anim ~= nil then
			current_anim = nil
			swim_move:Stop()
			swim_idle:Stop()
		end
		if entered then
			entered = false
			if rising then
				humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
			end
		end
		height = character.PrimaryPart.Position.Y
		position.MaxForce = Vector3.zero
	end
end

idk what the blox fruits water looks like but you could make realistic water with a mesh and then changing the height of the water using bones

then for the swimming it could be invisible terrain water or a custom swimming system

1 Like