How do I make my boat float?

So this is a little bit more difficult since I made custom water and also a boat vehicle in a ‘unique’ way. My boat works on land but doesnt float to the top of the custom water I made. I tried to make it but I just couldn’t figure out any way to do this. Any ideas how to do this based on the scripts? Thanks!

Boat Seat Script:

local seat    = script.Parent
local Players = game:GetService("Players")

seat:GetPropertyChangedSignal("Occupant"):Connect(function()
	local humanoid = seat.Occupant
	if humanoid then
		local player = Players:GetPlayerFromCharacter(humanoid.Parent)
		if player then
			seat:SetNetworkOwner(player)
		end
	else
		seat:SetNetworkOwnershipAuto()
	end
end)

Boat Local Script in StarterPlayerScripts:

local Players       = game:GetService("Players")
local UIS           = game:GetService("UserInputService")
local RS            = game:GetService("RunService")

local player        = Players.LocalPlayer
local character     = player.Character or player.CharacterAdded:Wait()
local humanoid      = character:WaitForChild("Humanoid")

local maxSpeed      = 16
local maxReverse    = maxSpeed / 2
local maxTurnSpeed  = 90
local accelTime     = 1
local decelTime     = 2

local movingForward  = false
local movingBackward = false
local turningLeft    = false
local turningRight   = false
local currentSeat

local speedVel = 0
local turnVel  = 0

local boat = workspace:WaitForChild("Boat")
local seat = boat:WaitForChild("Seat")
if not boat.PrimaryPart then boat.PrimaryPart = seat end

humanoid.Seated:Connect(function(active, seatPart)
	if active then
		currentSeat = seatPart
	else
		currentSeat       = nil
		movingForward     = false
		movingBackward    = false
		turningLeft       = false
		turningRight      = false
		speedVel          = 0
		turnVel           = 0
	end
end)

UIS.InputBegan:Connect(function(input, gp)
	if gp or not currentSeat then return end
	if input.KeyCode == Enum.KeyCode.W then
		movingForward = true
	elseif input.KeyCode == Enum.KeyCode.S then
		movingBackward = true
	elseif input.KeyCode == Enum.KeyCode.A then
		turningLeft = true
	elseif input.KeyCode == Enum.KeyCode.D then
		turningRight = true
	end
end)

UIS.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.W then
		movingForward = false
	elseif input.KeyCode == Enum.KeyCode.S then
		movingBackward = false
	elseif input.KeyCode == Enum.KeyCode.A then
		turningLeft = false
	elseif input.KeyCode == Enum.KeyCode.D then
		turningRight = false
	end
end)

RS.RenderStepped:Connect(function(dt)
	if currentSeat and currentSeat == seat and seat.Occupant then
		local targetSpeed = movingForward and maxSpeed or movingBackward and -maxReverse or 0
		local tTime = (math.abs(targetSpeed) > math.abs(speedVel)) and accelTime or decelTime
		speedVel = speedVel + (targetSpeed - speedVel) * math.clamp(dt / tTime, 0, 1)
		local moveDelta = boat.PrimaryPart.CFrame.LookVector * speedVel * dt

		if boat.PrimaryPart.Anchored then
			boat:SetPrimaryPartCFrame(boat.PrimaryPart.CFrame + moveDelta)
		else
			boat.PrimaryPart.AssemblyLinearVelocity = boat.PrimaryPart.CFrame.LookVector * speedVel
		end

		local targetTurn = turningLeft and maxTurnSpeed or turningRight and -maxTurnSpeed or 0
		local tTimeTurn = (math.abs(targetTurn) > math.abs(turnVel)) and accelTime or decelTime
		turnVel = turnVel + (targetTurn - turnVel) * math.clamp(dt / tTimeTurn, 0, 1)
		boat:SetPrimaryPartCFrame(boat.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(turnVel * dt), 0))
	end
end)

Wave Local Script in StarterPlayerScripts:

local Wave = require(script.Wave)
local Plane = workspace:WaitForChild("Wave"):WaitForChild("Plane")
local Settings = {
	WaveLength    = 90,
	Direction     = Vector2.new(0, 0),
	Steepness     = 0.6,
	TimeModifier  = 8,
	MaxDistance   = 1500,
}
local TestWave = Wave.new(Plane, Settings)
TestWave:ConnectRenderStepped()

Module Script | Child of the Local Wave Script:

local Wave = {}
Wave.__index = Wave

local newCFrame       = CFrame.new
local IdentityCFrame  = newCFrame()
local EmptyVector2    = Vector2.new()
local math_noise      = math.noise
local Stepped         = game:GetService("RunService").RenderStepped
local Player          = game:GetService("Players").LocalPlayer

local default = {
	WaveLength    = 85,
	Gravity       = 1.5,
	Direction     = Vector2.new(1, 0),
	PushPoint     = nil,
	Steepness     = 1,
	TimeModifier  = 4,
	MaxDistance   = 1500,
}

local function Gerstner(pos, wavelength, direction, steepness, gravity, time)
	local k    = (2 * math.pi) / wavelength
	local a    = steepness / k
	local d    = direction.Unit
	local c    = math.sqrt(gravity / k)
	local f    = k * d:Dot(Vector2.new(pos.X, pos.Z)) - c * time
	local cosF = math.cos(f)

	local dX = d.X * (a * cosF)
	local dY = a * math.sin(f)
	local dZ = d.Y * (a * cosF)
	return Vector3.new(dX, dY, dZ)
end

local function CreateSettings(s, o)
	o = o or {}
	s = s or default
	return {
		WaveLength   = s.WaveLength   or o.WaveLength   or default.WaveLength,
		Gravity      = s.Gravity      or o.Gravity      or default.Gravity,
		Direction    = s.Direction    or o.Direction    or default.Direction,
		PushPoint    = s.PushPoint    or o.PushPoint    or default.PushPoint,
		Steepness    = s.Steepness    or o.Steepness    or default.Steepness,
		TimeModifier = s.TimeModifier or o.TimeModifier or default.TimeModifier,
		MaxDistance  = s.MaxDistance  or o.MaxDistance  or default.MaxDistance,
	}
end

local function GetDirection(settings, worldPos)
	if settings.PushPoint then
		local target = settings.PushPoint:IsA("Attachment")
			and settings.PushPoint.WorldPosition
			or settings.PushPoint.Position
		local dir3 = (target - worldPos).Unit
		return Vector2.new(dir3.X, dir3.Z)
	end
	return settings.Direction
end

function Wave.new(instance, waveSettings, bones)
	bones = bones or {}
	for _, v in pairs(instance:GetDescendants()) do
		if v:IsA("Bone") then
			table.insert(bones, v)
		end
	end

	return setmetatable({
		_instance  = instance,
		_bones     = bones,
		_time      = 0,
		_connections = {},
		_settings  = CreateSettings(waveSettings),
	}, Wave)
end

function Wave:Update()
	local center = self._instance.Position
	local settings = self._settings

	for _, bone in pairs(self._bones) do
		local worldPos = bone.WorldPosition
		local dir = settings.Direction == EmptyVector2
			and Vector2.new(math_noise(worldPos.X, worldPos.Z, 1), math_noise(worldPos.X, worldPos.Z, 0))
			or GetDirection(settings, worldPos)

		local dist = (worldPos - center).Magnitude
		local minSteep, maxSteep = 0.1, 0.2
		local steepness = dist <= 100
			and (minSteep + (maxSteep - minSteep) * (dist / 100))
			or settings.Steepness

		bone.Transform = newCFrame(
			Gerstner(worldPos, settings.WaveLength, dir, steepness, settings.Gravity, self._time)
		)
	end
end

function Wave:Refresh()
	for _, bone in pairs(self._bones) do
		bone.Transform = IdentityCFrame
	end
end

function Wave:UpdateSettings(waveSettings)
	self._settings = CreateSettings(waveSettings, self._settings)
end

function Wave:ConnectRenderStepped()
	local conn = Stepped:Connect(function()
		if not game:IsLoaded() then return end
		local char = Player.Character
		if not char or (char.PrimaryPart.Position - self._instance.Position).Magnitude < self._settings.MaxDistance then
			self._time = (DateTime.now().UnixTimestampMillis / 1000) / self._settings.TimeModifier
			self:Update()
		else
			self:Refresh()
		end
	end)
	table.insert(self._connections, conn)
	return conn
end

function Wave:Destroy()
	self._instance = nil
	for _, conn in pairs(self._connections) do
		pcall(function() conn:Disconnect() end)
	end
	self._bones = {}
	self._settings = {}
end

return Wave

I’ve tried to and failed miserably to do this and just genuinely cant figure out a solution. I’m hoping someone could help me do this or just give any recommendation on what to do based on how I made it. Thanks!

2 Likes

From what I see, your water is just a visual effect. Correct me if I’m wrong, but I think your boat is simply falling because the water has no physics. You’ll need to code your own buoyancy system for it to float.

2 Likes

So I tried adding it and this is my first time ever using buoyancy and it kind of works but not really, and not well. Any idea what I can do to improve it:

Local Script:

local Wave = require(script.Wave)
local Plane = workspace:WaitForChild("Wave"):WaitForChild("Plane")
local Settings = {
	WaveLength    = 90,
	Direction     = Vector2.new(0, 0),
	Gravity       = 1.5,
	Steepness     = 0.6,
	TimeModifier  = 8,
	MaxDistance   = 1500,
}
local TestWave = Wave.new(Plane, Settings)
TestWave:ConnectRenderStepped()

local floats = {}

local function getWaveY(x, z)
	local t = (DateTime.now().UnixTimestampMillis / 1000) / Settings.TimeModifier
	local k = (2 * math.pi) / Settings.WaveLength
	local a = Settings.Steepness / k
	local d = Settings.Direction.Unit
	local c = math.sqrt(Settings.Gravity / k)
	local f = k * (d.X * x + d.Y * z) - c * t
	return Plane.Position.Y + a * math.sin(f)
end

workspace.Wave.Touched:Connect(function(part)
	if part:IsA("BasePart") and not part.Anchored and not floats[part] then
		local bp = Instance.new("BodyPosition", part)
		bp.MaxForce = Vector3.new(0, part:GetMass() * workspace.Gravity * 3, 0)
		bp.P = 2000
		bp.D = 50
		floats[part] = bp
	end
end)

workspace.Wave.TouchEnded:Connect(function(part)
	local bp = floats[part]
	if bp then
		bp:Destroy()
		floats[part] = nil
	end
end)

game:GetService("RunService").Heartbeat:Connect(function()
	for part, bp in pairs(floats) do
		if part and part.Parent then
			local waveY = getWaveY(part.Position.X, part.Position.Z)
			local halfY = part.Size.Y / 2
			bp.Position = Vector3.new(part.Position.X, waveY + halfY, part.Position.Z)
		else
			floats[part] = nil
		end
	end
end)

Module Script:

local Wave = {}
Wave.__index = Wave

local newCFrame       = CFrame.new
local IdentityCFrame  = newCFrame()
local EmptyVector2    = Vector2.new()
local math_noise      = math.noise
local Stepped         = game:GetService("RunService").RenderStepped
local Player          = game:GetService("Players").LocalPlayer

local default = {
	WaveLength    = 85,
	Gravity       = 1.5,
	Direction     = Vector2.new(1, 0),
	PushPoint     = nil,
	Steepness     = 1,
	TimeModifier  = 4,
	MaxDistance   = 1500,
}

local function Gerstner(pos, wavelength, direction, steepness, gravity, time)
	local k    = (2 * math.pi) / wavelength
	local a    = steepness / k
	local d    = direction.Unit
	local c    = math.sqrt(gravity / k)
	local f    = k * d:Dot(Vector2.new(pos.X, pos.Z)) - c * time
	local cosF = math.cos(f)

	local dX = d.X * (a * cosF)
	local dY = a * math.sin(f)
	local dZ = d.Y * (a * cosF)
	return Vector3.new(dX, dY, dZ)
end

local function CreateSettings(s, o)
	o = o or {}
	s = s or default
	return {
		WaveLength   = s.WaveLength   or o.WaveLength   or default.WaveLength,
		Gravity      = s.Gravity      or o.Gravity      or default.Gravity,
		Direction    = s.Direction    or o.Direction    or default.Direction,
		PushPoint    = s.PushPoint    or o.PushPoint    or default.PushPoint,
		Steepness    = s.Steepness    or o.Steepness    or default.Steepness,
		TimeModifier = s.TimeModifier or o.TimeModifier or default.TimeModifier,
		MaxDistance  = s.MaxDistance  or o.MaxDistance  or default.MaxDistance,
	}
end

local function GetDirection(settings, worldPos)
	if settings.PushPoint then
		local target = settings.PushPoint:IsA("Attachment")
			and settings.PushPoint.WorldPosition
			or settings.PushPoint.Position
		local dir3 = (target - worldPos).Unit
		return Vector2.new(dir3.X, dir3.Z)
	end
	return settings.Direction
end

function Wave.new(instance, waveSettings, bones)
	bones = bones or {}
	for _, v in pairs(instance:GetDescendants()) do
		if v:IsA("Bone") then
			table.insert(bones, v)
		end
	end

	return setmetatable({
		_instance  = instance,
		_bones     = bones,
		_time      = 0,
		_connections = {},
		_settings  = CreateSettings(waveSettings),
	}, Wave)
end

function Wave:Update()
	local center = self._instance.Position
	local settings = self._settings

	for _, bone in pairs(self._bones) do
		local worldPos = bone.WorldPosition
		local dir = settings.Direction == EmptyVector2
			and Vector2.new(math_noise(worldPos.X, worldPos.Z, 1), math_noise(worldPos.X, worldPos.Z, 0))
			or GetDirection(settings, worldPos)

		local dist = (worldPos - center).Magnitude
		local minSteep, maxSteep = 0.1, 0.2
		local steepness = dist <= 100
			and (minSteep + (maxSteep - minSteep) * (dist / 100))
			or settings.Steepness

		bone.Transform = newCFrame(
			Gerstner(worldPos, settings.WaveLength, dir, steepness, settings.Gravity, self._time)
		)
	end
end

function Wave:Refresh()
	for _, bone in pairs(self._bones) do
		bone.Transform = IdentityCFrame
	end
end

function Wave:UpdateSettings(waveSettings)
	self._settings = CreateSettings(waveSettings, self._settings)
end

function Wave:ConnectRenderStepped()
	local conn = Stepped:Connect(function()
		if not game:IsLoaded() then return end
		local char = Player.Character
		if not char or (char.PrimaryPart.Position - self._instance.Position).Magnitude < self._settings.MaxDistance then
			self._time = (DateTime.now().UnixTimestampMillis / 1000) / self._settings.TimeModifier
			self:Update()
		else
			self:Refresh()
		end
	end)
	table.insert(self._connections, conn)
	return conn
end

function Wave:Destroy()
	self._instance = nil
	for _, conn in pairs(self._connections) do
		pcall(function() conn:Disconnect() end)
	end
	self._bones = {}
	self._settings = {}
end

return Wave

Thanks!

1 Like

Great! Could you send a video of what it looks like now so I can help?

1 Like

That makes sense. I thought the same thing. Regular water, a simple boat script from the creator store can be analyzed to figure out how to get a boat to float on terrain water. There is terrain water and then there is model created water. Yes, a visual effect.

2 Likes

@TheyCallMeFatbird
@Brianhi33alt

Video of Water And Boat:

1 Like

If the boat is on the water does it still fall?

Here’s an idea I came up with partially inspired by physics which I believe would be a good approximation

I would make a function to calculate the height of the water at a given point (Y coordinate of the water plane) and (optional) the normal of the surface. Then I’d take a point around the center of the boat (let’s call it O) and a number equal to about half the height of the boat (let’s call it h).

On physics tick, we get the height of the water at O (the Y coordinate is ignored, therefore giving us the height right below or above it).
Δh = 2 * h - (O.Y - waterheight + h) = h - O.Y + waterheight. This will represent (approximately) how much of the boat is under water (times h)
Then, we divide Δh by h and clamp it between 0 and 1: Δh ← clamp(Δh/2/h,0,1)
Finally, the force should be equal to a constant based on how bouyant the boat is (b) multiplied by the boat’s mass (m) multiplied by Δh (of course multiplied by deltatime). The direction should be the normal, or if that’s too intense and you don’t want your boat to move around, or you can’t figure out how to calculate the normal, just use up.

Velocity += b * m * Δh * Δt(deltatime) * waternormal

If the boat won’t stop bouncing, I’d recommend adding some air resistance (or even calculate water resistance if you want your physics to be fancy and you can figure that out). You can add air resistance by defining a constant like 0.9 by which your speed will be multiplied every second and to make it smooth (instead of multiplying once per second) you can instead multiply by it to the power of deltatime on tick

If it works, show me, I want to see it. Also, this is inspired by Acerola’s video on buoyancy so you should check that out the next time you’re watching YouTube :)

2 Likes

Why not use Roblox’s built in water, and change the density of the boat?

Yes. If I make the plane collidable then it just sits on the server side plane, which is conpletely flat and doesnt move

First, make sure all parts have welds and the boat model has a PrimaryPart.
Then try making the force of the boat stronger.

Last thing, could you show the BoatScript?

1 Like

I have an idea, you could also have a transparent block in the water area which the boat will not fall under. Of course you can’t go in the block otherwise everything falls. Transparent block is one way to keep a boat from falling. You can have the invisible block part but still keep the collision setting.