How to make boat stick to surface of a skinned mesh?

I’m adding realistic water to my game and so I used a skin mesh. I wanted boats so I opted to just have a model called Boat stick to the surface of that mesh. It doesn’t work at all. I don’t know if it doesn’t know the location of the wave or if the scripts just have an issue but it isn’t working. Any ideas? Thanks:

Local Boat Script in StarterPlayerScripts:

local RunService = game:GetService("RunService")
local boat = workspace:WaitForChild("Boat")

RunService.RenderStepped:Connect(function()
	if _G.GetWaveHeightAt then
		local pos = boat.Position
		local waveY = _G.GetWaveHeightAt(pos)
		boat.Position = Vector3.new(pos.X, waveY, pos.Z)
	end
end)

Local Wave Script in StarterPlayerScripts:

local RunService = game:GetService("RunService")
local waveModel = workspace:WaitForChild("Wave")
local plane = waveModel:WaitForChild("Plane")
local bones = {}

for _, v in ipairs(plane:GetDescendants()) do
	if v:IsA("Bone") then
		table.insert(bones, v)
	end
end

local waves = {
	{dir = Vector2.new(1, 0).Unit, amplitude = 15, wavelength = 60, speed = 8},
	{dir = Vector2.new(0, 1).Unit, amplitude = 10, wavelength = 40, speed = 12},
	{dir = Vector2.new(1, 1).Unit, amplitude = 8, wavelength = 50, speed = 6},
	{dir = Vector2.new(-1, 1).Unit, amplitude = 6, wavelength = 70, speed = 5},
}

local function getWaveHeightAtXZ(x, z, t)
	local height = 0
	for _, wave in ipairs(waves) do
		local k = (2 * math.pi) / wave.wavelength
		local phase = k * (wave.dir.X * x + wave.dir.Y * z - wave.speed * t)
		height += math.sin(phase * 1.2) * wave.amplitude * 0.3
	end
	return height
end

local boneData = {}
for _, bone in ipairs(bones) do
	local pos = bone.CFrame.Position
	boneData[#boneData+1] = {
		bone = bone,
		originCFrame = bone.CFrame,
		originXZ = Vector2.new(pos.X, pos.Z)
	}
end

RunService.RenderStepped:Connect(function()
	local t = tick()
	for _, data in ipairs(boneData) do
		local xz = data.originXZ
		local totalOffset = Vector3.zero
		for _, wave in ipairs(waves) do
			local k = (2 * math.pi) / wave.wavelength
			local phase = k * (wave.dir.X * xz.X + wave.dir.Y * xz.Y - wave.speed * t)
			local offsetX = math.cos(phase) * wave.amplitude * 0.5 * wave.dir.X
			local offsetZ = math.cos(phase) * wave.amplitude * 0.5 * wave.dir.Y
			local offsetY = math.sin(phase * 1.2) * wave.amplitude * 0.3
			totalOffset += Vector3.new(offsetX, offsetY, offsetZ)
		end
		data.bone.CFrame = data.originCFrame * CFrame.new(totalOffset)
	end
end)

_G.GetWaveHeightAt = function(position)
	return getWaveHeightAtXZ(position.X, position.Z, tick())
end

Server Wave Script in ServerScrtipService:

local RunService = game:GetService("RunService")
local waveModel = workspace:WaitForChild("Wave")
local plane = waveModel:WaitForChild("Plane")
local bones = {}

for _, v in ipairs(plane:GetDescendants()) do
	if v:IsA("Bone") then
		table.insert(bones, v)
	end
end

local waves = {
	{dir = Vector2.new(1, 0).Unit, amplitude = 15, wavelength = 60, speed = 8},
	{dir = Vector2.new(0, 1).Unit, amplitude = 10, wavelength = 40, speed = 12},
	{dir = Vector2.new(1, 1).Unit, amplitude = 8, wavelength = 50, speed = 6},
	{dir = Vector2.new(-1, 1).Unit, amplitude = 6, wavelength = 70, speed = 5},
}

local function getWaveHeightAtXZ(x, z, t)
	local height = 0
	for _, wave in ipairs(waves) do
		local k = (2 * math.pi) / wave.wavelength
		local phase = k * (wave.dir.X * x + wave.dir.Y * z - wave.speed * t)
		height += math.sin(phase * 1.2) * wave.amplitude * 0.3
	end
	return height
end

local boneData = {}
for _, bone in ipairs(bones) do
	local pos = bone.CFrame.Position
	boneData[#boneData+1] = {
		bone = bone,
		originCFrame = bone.CFrame,
		originXZ = Vector2.new(pos.X, pos.Z)
	}
end

RunService.Heartbeat:Connect(function()
	local t = tick()
	for _, data in ipairs(boneData) do
		local xz = data.originXZ
		local totalOffset = Vector3.zero
		for _, wave in ipairs(waves) do
			local k = (2 * math.pi) / wave.wavelength
			local phase = k * (wave.dir.X * xz.X + wave.dir.Y * xz.Y - wave.speed * t)
			local offsetX = math.cos(phase) * wave.amplitude * 0.5 * wave.dir.X
			local offsetZ = math.cos(phase) * wave.amplitude * 0.5 * wave.dir.Y
			local offsetY = math.sin(phase * 1.2) * wave.amplitude * 0.3
			totalOffset += Vector3.new(offsetX, offsetY, offsetZ)
		end
		data.bone.CFrame = data.originCFrame * CFrame.new(totalOffset)
	end
end)

_G.GetWaveHeightAt = function(position)
	return getWaveHeightAtXZ(position.X, position.Z, tick())
end

Any ideas? Thanks!

2 Likes

I’ve seen other solved posts on this topic before: