I have a skinned mesh ocean script. I wanted to get a part to match the exact height of the mesh however it’s slightly off. Am I doing something wrong in the script? Is there anything I should have added? Please help me get an idea of what’s wrong and how to fix it. Thanks!
Server Script:
local oceanPart = workspace:WaitForChild("OceanTest")
local meshPart = workspace:WaitForChild("OceanMesh")
local RunService = game:GetService("RunService")
local WaveLength = 200
local Gravity = 9.81
local Steepness = 0.25
local TimeModifier = 20
local ShallowThreshold = 100
local IntermediateThreshold = 250
local origin = Vector2.new(0, 0)
local waveDirX = 1
local waveDirZ = 0.5
local function GetWaveHeightAtPos(x, z, time)
local pos2D = Vector2.new(x, z)
local distance = (pos2D - origin).Magnitude
local k = (2 * math.pi) / WaveLength
local speed = math.sqrt(Gravity / k)
local steepness = Steepness
if distance < ShallowThreshold then
steepness = steepness * 0.4
speed = speed * 0.5
elseif distance < IntermediateThreshold then
steepness = steepness * 0.7
speed = speed * 0.75
end
local a = steepness / k
local phase = k * (waveDirX * x + waveDirZ * z) - speed * time
local y = a * math.sin(phase)
return y
end
RunService.Heartbeat:Connect(function()
local time = tick() / TimeModifier
local pos = oceanPart.Position
local waveY = GetWaveHeightAtPos(pos.X, pos.Z, time)
oceanPart.Position = Vector3.new(pos.X, meshPart.Position.Y + waveY, pos.Z)
end)
Local Script:
local meshPart = workspace:WaitForChild("OceanMesh")
local bones = {}
for _, obj in pairs(meshPart:GetDescendants()) do
if obj:IsA("Bone") then
table.insert(bones, obj)
end
end
local RunService = game:GetService("RunService")
local origin = Vector2.new(0, 0)
local WaveLength = 200
local Gravity = 9.81
local Steepness = 0.25
local TimeModifier = 20
local ShallowThreshold = 100
local IntermediateThreshold = 250
local waveDirX = 1
local waveDirZ = 0.5
local function GetWaveHeightAtPos(x, z, time)
local pos2D = Vector2.new(x, z)
local distance = (pos2D - origin).Magnitude
local k = (2 * math.pi) / WaveLength
local speed = math.sqrt(Gravity / k)
local steepness = Steepness
if distance < ShallowThreshold then
steepness = steepness * 0.4
speed = speed * 0.5
elseif distance < IntermediateThreshold then
steepness = steepness * 0.7
speed = speed * 0.75
end
local a = steepness / k
local phase = k * (waveDirX * x + waveDirZ * z) - speed * time
local y = a * math.sin(phase)
return y
end
RunService.RenderStepped:Connect(function()
local time = tick() / TimeModifier
for _, bone in pairs(bones) do
local worldPos = meshPart.CFrame:PointToWorldSpace(bone.Position)
local y = GetWaveHeightAtPos(worldPos.X, worldPos.Z, time)
bone.Transform = CFrame.new(0, y, 0)
end
end)
GetServerTimeNow() returns the client’s best approximation of the current time on the server. This is useful for creating synchronized experiences as every client will get about the same results regardless of their timezone or local clock.
Returns how much time has elapsed, in seconds, since the Unix epoch, on the current local session’s computer. The Unix epoch is represented by 00:00:00 on 1 January 1970.
tick() isn’t officially deprecated, but has a variety of issues. It can be off by up to one second and returns inconsistent results across time zones and operating systems. Use os.time(), os.clock(), or time() instead. Also consider DateTime.UnixTimestamp and DateTime.UnixTimestampMillis.
Possibly use meshPart.Position.Y as the base height, then add your wave height offset on top of that to keep the part aligned with the mesh surface. Something like;
local time = workspace:GetServerTimeNow() / TimeModifier
and it was much further off then when using ticks, I then switched it to this:
local time = workspace:GetServerTimeNow() % 1000 / TimeModifier
And it was about as accurate as when I was using ticks. Is there something I did wrong or should have also done. Sorry for responding late, I had dev forum open all day but it just showed me the notifcation.
How you measuring the difference, is it from the server or the client, and roughly how far off is it?
I suspect the issue is network latency. Each update the server makes to oceanPart.Position takes a variable amount of time to replicate to every client, which is problematic, since it is very likely the latency was greater than the time between each heartbeat on the server, i.e. the client is seeing an old state.
Unfortunately there is no perfect solution, but you can compensate by having each client predict the position on the server, i.e. each client models their replication latency and adds that to the time used to compute the ocean height.
Another issue might be that GetServerTimeNow does not have millisecond accuracy, but this would only matter if the ocean’s change in height is substantial enough over a several millisecond time interval.
You do not want to use tick or os.time because these functions are based on the machine’s timezone. e.g. A server in California and a client in Maine would have a 3 hour difference.
It uses server time to handle latency and is much more accurate now but there’s still a small problem. The part mostly follows the waves but goes a bit higher and lower than the waves can go by about a stud or less why?
Server Script:
local oceanPart = workspace:WaitForChild("OceanTest")
local meshPart = workspace:WaitForChild("OceanMesh")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local latencyPing = ReplicatedStorage:FindFirstChild("LatencyPing") or Instance.new("RemoteFunction")
latencyPing.Name = "LatencyPing"
latencyPing.Parent = ReplicatedStorage
latencyPing.OnServerInvoke = function()
return true
end
local WaveLength = 200
local Gravity = 9.81
local Steepness = 0.25
local TimeModifier = 20
local ShallowThreshold = 100
local IntermediateThreshold = 250
local origin = Vector2.new(0, 0)
local waveDirX = 1
local waveDirZ = 0.5
local function GetWaveHeightAtPos(x, z, time)
local pos2D = Vector2.new(x, z)
local distance = (pos2D - origin).Magnitude
local k = (2 * math.pi) / WaveLength
local speed = math.sqrt(Gravity / k)
local steepness = Steepness
if distance < ShallowThreshold then
steepness = steepness * 0.4
speed = speed * 0.5
elseif distance < IntermediateThreshold then
steepness = steepness * 0.7
speed = speed * 0.75
end
local a = steepness / k
local phase = k * (waveDirX * x + waveDirZ * z) - speed * time
local y = a * math.sin(phase)
return y
end
RunService.Heartbeat:Connect(function()
local time = workspace:GetServerTimeNow() / TimeModifier
local pos = oceanPart.Position
local waveY = GetWaveHeightAtPos(pos.X, pos.Z, time)
oceanPart.Position = Vector3.new(pos.X, meshPart.Position.Y + waveY, pos.Z)
end)
Local Script:
local meshPart = workspace:WaitForChild("OceanMesh")
local bones = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local latencyPing = ReplicatedStorage:WaitForChild("LatencyPing")
local function getLatency()
local startTime = tick()
latencyPing:InvokeServer()
return (tick() - startTime) / 2
end
local latency = getLatency()
for _, obj in pairs(meshPart:GetDescendants()) do
if obj:IsA("Bone") then
table.insert(bones, obj)
end
end
local origin = Vector2.new(0, 0)
local WaveLength = 200
local Gravity = 9.81
local Steepness = 0.25
local TimeModifier = 20
local ShallowThreshold = 100
local IntermediateThreshold = 250
local waveDirX = 1
local waveDirZ = 0.5
local function GetWaveHeightAtPos(x, z, time)
local pos2D = Vector2.new(x, z)
local distance = (pos2D - origin).Magnitude
local k = (2 * math.pi) / WaveLength
local speed = math.sqrt(Gravity / k)
local steepness = Steepness
if distance < ShallowThreshold then
steepness = steepness * 0.4
speed = speed * 0.5
elseif distance < IntermediateThreshold then
steepness = steepness * 0.7
speed = speed * 0.75
end
local a = steepness / k
local phase = k * (waveDirX * x + waveDirZ * z) - speed * time
local y = a * math.sin(phase)
return y
end
RunService.RenderStepped:Connect(function()
local time = (workspace:GetServerTimeNow() + latency) / TimeModifier
for _, bone in pairs(bones) do
local worldPos = meshPart.CFrame:PointToWorldSpace(bone.Position)
local y = GetWaveHeightAtPos(worldPos.X, worldPos.Z, time)
bone.Transform = CFrame.new(0, y - bone.Position.Y, 0)
end
end)
Thanks for your help so far, it’s improved my script a lot!
If the height is too high or too low then try multiplying Y by .95 (or whatever works) to decrease where you’ve got it at the 100% limit of your calculation.
Yes, but you still need to compensate for the replication latency of oceanPart.Position. Additionally, I am unaware if it is ideal to call GetServerTimeNow every frame, but the documentation does not give a clear answer and I have not profiled it’s performance myself.
GetServerTimeNow() is expensive to call compared to DateTime.now(), and is less precise than os.clock(), so it should be used to make sure an event starts at the right real world time or to adjust things periodically to keep a series of events in sync.
Also, I am curious if you really need to position this on the server. If not, then each client should position the part instead, as they will see zero error. Moreover, each client would see approximately the same thing if their clocks are synced with GetServerTimeNow.
I switched it to just client side and local rather than server. It had the same issue of going to high above where it should be and too far below. However when I took @Scottifly’s suggestion of multiplying the Y by 0.95 (Although I used 0.7) it worked perfectly. However when I would I move it to anywhere other than 0,0 it was off again. Is there a way to fix this with or without multiplying the the Y? Am I missing anything? Thanks!
Local Script:
local oceanPart = workspace:WaitForChild("OceanTest")
local meshPart = workspace:WaitForChild("OceanMesh")
local bones = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local latencyPing = ReplicatedStorage:WaitForChild("LatencyPing")
local function getLatency()
local startTime = tick()
latencyPing:InvokeServer()
return (tick() - startTime) / 2
end
local latency = getLatency()
for _, obj in pairs(meshPart:GetDescendants()) do
if obj:IsA("Bone") then
table.insert(bones, obj)
end
end
local origin = Vector2.new(0, 0)
local WaveLength = 200
local Gravity = 9.81
local Steepness = 0.25
local TimeModifier = 20
local ShallowThreshold = 100
local IntermediateThreshold = 250
local waveDirX = 1
local waveDirZ = 0.5
local function GetWaveHeightAtPos(x, z, time)
local pos2D = Vector2.new(x, z)
local distance = (pos2D - origin).Magnitude
local k = (2 * math.pi) / WaveLength
local speed = math.sqrt(Gravity / k)
local steepness = Steepness
if distance < ShallowThreshold then
steepness = steepness * 0.4
speed = speed * 0.5
elseif distance < IntermediateThreshold then
steepness = steepness * 0.7
speed = speed * 0.75
end
local a = steepness / k
local phase = k * (waveDirX * x + waveDirZ * z) - speed * time
local y = a * math.sin(phase)
return y
end
RunService.RenderStepped:Connect(function()
local time = (workspace:GetServerTimeNow() + latency) / TimeModifier
local pos = oceanPart.Position
local waveY = GetWaveHeightAtPos(pos.X, pos.Z, time)
oceanPart.Position = Vector3.new(pos.X, meshPart.Position.Y + waveY * 0.7, pos.Z)
for _, bone in pairs(bones) do
local worldPos = meshPart.CFrame:PointToWorldSpace(bone.Position)
local y = GetWaveHeightAtPos(worldPos.X, worldPos.Z, time)
bone.Transform = CFrame.new(0, y - bone.Position.Y, 0)
end
end)
I just realized I might be misunderstanding your issue. Are you saying there is a one stud difference between the maximum y position that the part and bone can attain, or there is up to a one stud difference between their current positions? Additionally, can you clarify how you measured the error? Did you visually look at them while they are moving?
Since your height function computes only the phase as a function of time, there will be no difference in amplitude (maximum y position) no matter how different the client and server clocks are, so I had assumed you meant something else.
If you meant that the amplitude was different:
Can you measure the maximum y position of the part and the ocean to see if their amplitudes are actually different? I am confused why you are seeing a fix by modifying the amplitude of the oceanPart since GetWaveHeightAtPos should compute a signal with an identical amplitude.
Additionally, can you test setting the bone and the oceanPart to same position (e.g. a static one like Vector3.new(0, 0, 0) ), without updating it, and see if they look like they are in different places? Perhaps the way the bone’s position affects the mesh is not what you were expecting, since the only way the amplitudes could really be different is if there is a disconnect between the bone’s visual position and the position property.
Also, you do not need to compensate for latency if you are going to update the oceanPart.Position on the client now because the time will be identical. It seems GetServerTimeNow is performant enough that you can call it each frame. (Besides that, when you measure latency you should be doing it more than once since it changes a lot, which I suggest doing by checking when the client detects an update to the part’s position, not by making a remote call.)
So I took some of what you said and it’s experiencing kind of the same issue, where it’s perfect at 0,0 but off anywhere else. I genuinely have no idea what the issue is.
Local Script:
local meshPart = workspace:WaitForChild("OceanMesh")
local bones = {}
local RunService = game:GetService("RunService")
for _, obj in pairs(meshPart:GetDescendants()) do
if obj:IsA("Bone") then
table.insert(bones, obj)
end
end
local origin = Vector2.new(0, 0)
local WaveLength = 200
local Gravity = 9.81
local Steepness = 0.25
local TimeModifier = 20
local ShallowThreshold = 100
local IntermediateThreshold = 250
local waveDirX = 1
local waveDirZ = 0.5
local function GetWaveHeightAtPos(x, z, time)
local pos2D = Vector2.new(x, z)
local distance = (pos2D - origin).Magnitude
local k = (2 * math.pi) / WaveLength
local speed = math.sqrt(Gravity / k)
local steepness = Steepness
if distance < ShallowThreshold then
steepness = steepness * 0.4
speed = speed * 0.5
elseif distance < IntermediateThreshold then
steepness = steepness * 0.7
speed = speed * 0.75
end
local a = steepness / k
local phase = k * (waveDirX * x + waveDirZ * z) - speed * time
local y = a * math.sin(phase)
return y
end
RunService.RenderStepped:Connect(function()
local time = workspace:GetServerTimeNow() / TimeModifier
for _, bone in pairs(bones) do
local localPos = bone.Position
local worldPos = meshPart.CFrame:PointToWorldSpace(localPos)
local y = GetWaveHeightAtPos(worldPos.X, worldPos.Z, time)
local localY = localPos.Y
bone.Transform = CFrame.new(0, y - localY, 0)
end
end)
Try what @DrKittyWaffles said to do.
When you are calculating the bone and Part Positions to World CFrame, print each one.
It’ll tell you exactly how much the difference is.
From there you may be able to troubleshoot what is going on.
Since it works at 0, 0 just continue using the local space
local time = workspace:GetServerTimeNow() / TimeModifier
for _, bone in bones do
local localPos = bone.Position
local y = GetWaveHeightAtPos(localPos.X, localPos.Z, time)
bone.Transform = CFrame.new(0, y - localPos.Y, 0)
end
No, I made a typo, but I should have clarified that I made an assumption about how you compute the new oceanPart.Position since the most recent script you posted did not contain it,.
Can you show how you are computing oceanPart.Position?
Can you show how you are computing oceanPart.Position so it sits on the surface of the ocean? Additionally, if you changed the code for bone.Position, post that too.