Antares
Spell Reviewer
- Joined
- Dec 13, 2009
- Messages
- 982
While doing 3D collisions, someone brought up to me the problem with terrain height not being synced between players, a problem I had completely forgotten about. I searched here on the forums if this is a problem that has been solved yet, but I found only posts around 2020 discussing the issue, but not providing any concrete solutions:
Synchronized Heightmap of GetLocationZ
[DESYNC] - 2 Possible causes found
Since there wasn't anything newer that I could find, I assume no one really tried to tackle that problem since then, at least not here on hive?
So, because syncing that much data in real-time doesn't work, I thought the next-best solution would be to simply precompute the height map.
You create the height map with a spacing of 128 on map initialization:
To get the terrain height at any location, you use this function:
This function is 100% accurate and is even twice as fast as the traditional method:
So, even if you're not worrying about desyncs, this is still nice to have.
This should take care at least of desyncs caused by terrain deformations.
But according to the one thread I linked, there could still be desyncs even with a height map calculated at map initialization because of Reforged. Therefore, I looked at how feasible it is to calculate the height map in single-player and write it to a file, then import that data into the map and use it to generate it for every subsequent map launch. I came up with this:
Basically, you start the map, and it encodes the height map into a string that is saved to a file with the Preload native. One then has to remove all the extra characters from it and simply copy/paste it back into the map, and the next time the map loads, it will decode the height map from the string. A mapper can simply generate the height map normally during development and then precompute it once for the release version.
I tested it for various maps and for a giant map like Divide and Conquer, the string size is ~60kb, so it's no problem at all. With some more optimization, I got it down to 44kb.
Tell me what you think! What's the current state of GetLocationZ desyncs?
Synchronized Heightmap of GetLocationZ
[DESYNC] - 2 Possible causes found
Since there wasn't anything newer that I could find, I assume no one really tried to tackle that problem since then, at least not here on hive?
So, because syncing that much data in real-time doesn't work, I thought the next-best solution would be to simply precompute the height map.
You create the height map with a spacing of 128 on map initialization:
Lua:
local function CreateHeightMap()
local xMin = (worldMinX // 128)*128
local yMin = (worldMinY // 128)*128
local xMax = (worldMaxX // 128)*128 + 1
local yMax = (worldMaxY // 128)*128 + 1
local x = xMin
local y
local i = 1
local j
while x <= xMax do
heightMap[i] = {}
y = yMin
j = 1
while y <= yMax do
heightMap[i][j] = GetLocZ(x,y)
j = j + 1
y = y + 128
end
i = i + 1
x = x + 128
end
iMax = i - 2
jMax = j - 2
end
To get the terrain height at any location, you use this function:
Lua:
---@param x number
---@param y number
---@return number
GetTerrainZ = function(x, y)
--Find the bottom-left corner of the tile that the coordinates are in.
local rx = (x - worldMinX)/128 + 1
local ry = (y - worldMinY)/128 + 1
local i = rx // 1
local j = ry // 1
rx = rx - i
ry = ry - j
--Safety check to prevent nil values if quering a location outside the world bounds.
if i < 1 then
i = 1
rx = 0
elseif i > iMax then
i = iMax
rx = 1
end
if j < 1 then
j = 1
ry = 0
elseif j > jMax then
j = jMax
ry = 1
end
--Find out in which triangle the coordinates are in, then interpolate based on three grid points.
if rx + ry > 1 then --In top-right triangle
return (rx + ry - 1)*heightMap[i+1][j+1] + ((1 - rx)*heightMap[i][j+1] + (1 - ry)*heightMap[i+1][j])
else
return (1 - rx - ry)*heightMap[i][j] + (rx*heightMap[i+1][j] + ry*heightMap[i][j+1])
end
end
This function is 100% accurate and is even twice as fast as the traditional method:
Lua:
local function GetLocZ(x, y)
MoveLocation(moveableLoc, x, y)
return GetLocationZ(moveableLoc)
end
This should take care at least of desyncs caused by terrain deformations.
But according to the one thread I linked, there could still be desyncs even with a height map calculated at map initialization because of Reforged. Therefore, I looked at how feasible it is to calculate the height map in single-player and write it to a file, then import that data into the map and use it to generate it for every subsequent map launch. I came up with this:
Lua:
local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
local MINIMUM_Z = -256
local function ReadHeightMap()
local charPos = 0
local numRepetitions = 0
local charValues = {}
for i = 1, string.len(chars) do
charValues[string.sub(chars, i, i)] = i - 1
end
local firstChar = nil
for i = 1, #heightMap do
for j = 1, #heightMap[i] do
if numRepetitions > 0 then
heightMap[i][j] = heightMap[i][j-1]
numRepetitions = numRepetitions - 1
else
local valueDetermined = false
while not valueDetermined do
charPos = charPos + 1
local char = string.sub(HeightMapCode, charPos, charPos)
if char == "-" then
charPos = charPos + 1
char = string.sub(HeightMapCode, charPos, charPos)
end
if tonumber(char) then
local k = 0
while tonumber(string.sub(HeightMapCode, charPos + k + 1, charPos + k + 1)) do
k = k + 1
end
numRepetitions = tonumber(string.sub(HeightMapCode, charPos, charPos + k)) - 1
charPos = charPos + k
valueDetermined = true
heightMap[i][j] = heightMap[i][j-1]
else
if char == nil or char == "" then
heightMap[i][j] = 0
end
if firstChar then
if charValues[firstChar] and charValues[char] then
heightMap[i][j] = charValues[firstChar]*52 + charValues[char] + MINIMUM_Z
else
heightMap[i][j] = 0
end
firstChar = nil
valueDetermined = true
else
firstChar = char
end
end
end
end
end
end
HeightMapCode = nil
end
local function WriteHeightMap(subfolder)
local preloadString = 'HeightMapCode = "'
PreloadGenClear()
PreloadGenStart()
local numRepetitions = 0
local firstChar
local secondChar
local stringLength = 0
for i = 1, #heightMap do
for j = 1, #heightMap[i] do
if j > 1 then
if math.abs(heightMap[i][j-1] - heightMap[i][j]) <= 0.5 then
numRepetitions = numRepetitions + 1
else
if numRepetitions > 0 then
preloadString = preloadString .. numRepetitions
end
numRepetitions = 0
firstChar = (heightMap[i][j] - MINIMUM_Z) // 52 + 1
secondChar = heightMap[i][j]//1 - MINIMUM_Z - (heightMap[i][j]//1 - MINIMUM_Z)//52*52 + 1
preloadString = preloadString .. string.sub(chars, firstChar, firstChar) .. string.sub(chars, secondChar, secondChar)
end
else
if numRepetitions > 0 then
preloadString = preloadString .. numRepetitions
end
numRepetitions = 0
preloadString = preloadString .. "-"
firstChar = (heightMap[i][j] - MINIMUM_Z) // 52 + 1
secondChar = heightMap[i][j]//1 - MINIMUM_Z - (heightMap[i][j]//1 - MINIMUM_Z)//52*52 + 1
preloadString = preloadString .. string.sub(chars, firstChar, firstChar) .. string.sub(chars, secondChar, secondChar)
end
stringLength = stringLength + 1
if stringLength == 100 then
Preload(preloadString)
stringLength = 0
preloadString = ""
end
end
end
if numRepetitions > 0 then
preloadString = preloadString .. numRepetitions
end
preloadString = preloadString .. '"'
Preload(preloadString)
PreloadGenEnd(subfolder .. "\\heightMap.txt")
print("Written Height Map to CustomMapData\\" .. subfolder .. "\\heightMap.txt")
end
Basically, you start the map, and it encodes the height map into a string that is saved to a file with the Preload native. One then has to remove all the extra characters from it and simply copy/paste it back into the map, and the next time the map loads, it will decode the height map from the string. A mapper can simply generate the height map normally during development and then precompute it once for the release version.
I tested it for various maps and for a giant map like Divide and Conquer, the string size is ~60kb, so it's no problem at all. With some more optimization, I got it down to 44kb.
Tell me what you think! What's the current state of GetLocationZ desyncs?