“What do you want to achieve?”
My game uses a modified version of Okeanskiy’s Terrain Generation system, where:
- The terrain colors range in the orange-yellow spectrum.
- Instead of trees, garbage of random shapes, sizes, and material, spawn.
So far, this modified version of the terrain generation system should generate a landscape like this, not counting the custom sky-box:
Many destroyed and battered-up trash litter the dusty landscape of the Scrapyard realm. You might find some of the trash to be familiar objects, such as the enemy insects/arachnids you defeated in Bee Swarm Simulator, that one player’s plate that did not make it in Plates of Fate, or a prop from a long-gone game from your childhood, dating from the early months of ROBLOX.
However, there is a flaw in the modification: the trash is not consistent to all clients, as I stated last time. For example, to take the first notice of the problem, one player could be looking at a ruined spawn location, while another would perceive the same garbage as a mantis corpse.
What I want to achieve is the direct opposite of the flaw: trash that is consistent to all clients, so that if one player sees a wrecked tank, the other player perceives it as a wrecked tank, and nothing else.
“What is the issue?”
The problem actually lies in making the props consistent to all clients. SquarePapyrus12 suggested that I set up “a seed system and all clients should have the same seed”. However, my first few attempts on doing so ended up backfiring, returning a landscape devoid of any trash.
“What solutions have you tried so far?”
For reference, here is the original Chunk ModuleScript, complete with the modifications to add trash. (This very same ModuleScript was involved in generating the old landscape above.)
local TERRAIN_HEIGHT_COLORS = {
[-50] = Color3.fromRGB(169,88,33); -- Dark orange-ish
[-10] = Color3.fromRGB(197,140,69); -- Orange-ish
[0] = Color3.fromRGB(212,173,97); -- Yellow orange-ish
[75] = Color3.fromRGB(223,210,158) -- Pale yellowish
}
local X, Z = 4,4
local WIDTH_SCALE = 16
local HEIGHT_SCALE = 100
local TERRAIN_SMOOTHNESS = 20
local MIN_TRASH_SPAWN_HEIGHT = -15
local MAX_TRASH_SPAWN_HEIGHT = 30
local TRASH_DENSITY = 0.5 -- Number between 0 and 1
local SEED = game.Workspace.SEED.Value
local TerrainModel = game.Workspace.TerrainModel
local TrashModels = game.Workspace.TrashModels
local Terrain = game.Workspace.Terrain
local wedge = Instance.new("WedgePart")
wedge.Anchored = true
wedge.TopSurface = Enum.SurfaceType.Smooth
wedge.BottomSurface = Enum.SurfaceType.Smooth
local function draw3DTriangle(a, b, c)
local ab,ac,bc = b-a, c-a, c-b
local abd,acd,bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc)
if (abd > acd) and (abd > bcd) then
c,a = a,c
elseif (acd > bcd) and (acd > abd) then
a,b = b,a
end
ab,ac,bc = b-a, c-a, c-b
local right = ac:Cross(ab).unit
local up = bc:Cross(right).unit
local back = bc.unit
local height = math.abs(ab:Dot(up))
local w1 = wedge:Clone()
w1.Size = Vector3.new(0,height,math.abs(ab:Dot(back)))
w1.CFrame = CFrame.fromMatrix((a+b)/2, right, up, back)
w1.Parent = TerrainModel
local w2 = wedge:Clone()
w2.Size = Vector3.new(0,height,math.abs(ac:Dot(back)))
w2.CFrame = CFrame.fromMatrix((a+c)/2, -(right), up, -(back))
w2.Parent = TerrainModel
return w1, w2
end
local function getHeight(chunkPosX, chunkPosZ, x, z)
local height = math.noise(
(X/TERRAIN_SMOOTHNESS * chunkPosX) + x/TERRAIN_SMOOTHNESS,
(Z/TERRAIN_SMOOTHNESS * chunkPosZ) + z/TERRAIN_SMOOTHNESS,
SEED
) * HEIGHT_SCALE
if height > 20 then
local difference = height - 20
height += (difference * 1.2)
end
if height < -20 then
local difference = height - -20
height += (difference * 1.2)
end
return height
end
local function getPosition(chunkPosX, chunkPosZ, x, z)
return Vector3.new(
(chunkPosX*X*WIDTH_SCALE) + x*WIDTH_SCALE,
getHeight(chunkPosX, chunkPosZ, x, z),
(chunkPosZ*Z*WIDTH_SCALE) + z*WIDTH_SCALE
)
end
local function paintWedge(wedge)
local wedgeHeight = wedge.Position.Y
local color
local lowerColorHeight
local higherColorHeight
for height,heightColor in pairs(TERRAIN_HEIGHT_COLORS) do
if wedgeHeight == height then
color = heightColor
break
end
if (wedgeHeight < height) and (not higherColorHeight or height < higherColorHeight) then
higherColorHeight = height
end
if (wedgeHeight > height) and (not lowerColorHeight or height > lowerColorHeight) then
lowerColorHeight = height
end
end
if not color then
if higherColorHeight == nil then
color = TERRAIN_HEIGHT_COLORS[lowerColorHeight]
elseif lowerColorHeight == nil then
color = TERRAIN_HEIGHT_COLORS[higherColorHeight]
else
local alpha = (wedgeHeight - lowerColorHeight) / (higherColorHeight - lowerColorHeight)
local lowerColor = TERRAIN_HEIGHT_COLORS[lowerColorHeight]
local higherColor = TERRAIN_HEIGHT_COLORS[higherColorHeight]
color = lowerColor:lerp(higherColor, alpha)
end
end
wedge.Material = Enum.Material.Sand
wedge.Color = color
end
local function addWater(chunk)
local cframe = CFrame.new(
(chunk.x + 0.5) * (chunk.WIDTH_SIZE_X),
-70,
(chunk.z + 0.5) * (chunk.WIDTH_SIZE_Z)
)
local size = Vector3.new(
chunk.WIDTH_SIZE_X,
100,
chunk.WIDTH_SIZE_Z
)
game.Workspace.Terrain:FillBlock(cframe, size, Enum.Material.Water)
chunk.waterCFrame = cframe
chunk.waterSize = size
end
local function addTrash(chunk)
local posGrid = chunk.positionGrid
local instances = chunk.instances
local chunkPosX = chunk.x
local chunkPosZ = chunk.z
for x = 0, X-1 do
for z = 0, Z-1 do
local pos = posGrid[x][z]
if pos.Y >= MIN_TRASH_SPAWN_HEIGHT and pos.Y <= MAX_TRASH_SPAWN_HEIGHT then
math.randomseed((x * (chunkPosX+SEED))+(z * (chunkPosZ+SEED)))
if math.random() < TRASH_DENSITY then
local garbage = game.ReplicatedStorage.Garbage:GetChildren()
local trash = garbage[math.random(1,#garbage)]:Clone()
local cframe = CFrame.new(pos)
* CFrame.new(
math.random()*math.random(-10,10),
0,
math.random()*math.random(-10,10)
)
* CFrame.Angles(
math.rad(math.random()*math.random(-30,30)),
2*math.pi*math.random(),
math.rad(math.random()*math.random(-30,30))
)
trash:SetPrimaryPartCFrame(cframe)
trash.Parent = TrashModels
table.insert(instances, trash)
end
end
end
end
end
local Chunk = {}
Chunk.__index = Chunk
Chunk.WIDTH_SIZE_X = X * WIDTH_SCALE
Chunk.WIDTH_SIZE_Z = Z * WIDTH_SCALE
function Chunk.new(chunkPosX, chunkPosZ)
local chunk = {
instances = {};
positionGrid = {};
x = chunkPosX;
z = chunkPosZ;
}
setmetatable(chunk,Chunk)
local positionGrid = chunk.positionGrid
for x = 0, X do
positionGrid[x] = {}
for z = 0, Z do
positionGrid[x][z] = getPosition(chunkPosX,chunkPosZ,x,z)
end
end
for x = 0, X-1 do
for z = 0, Z-1 do
local a = positionGrid[x][z]
local b = positionGrid[x+1][z]
local c = positionGrid[x][z+1]
local d = positionGrid[x+1][z+1]
local wedgeA, wedgeB = draw3DTriangle(a,b,c)
local wedgeC, wedgeD = draw3DTriangle(b,c,d)
paintWedge(wedgeA)
paintWedge(wedgeB)
paintWedge(wedgeC)
paintWedge(wedgeD)
table.insert(chunk.instances, wedgeA)
table.insert(chunk.instances, wedgeB)
table.insert(chunk.instances, wedgeC)
table.insert(chunk.instances, wedgeD)
--[[
Terrain:FillBlock(wedgeA.CFrame,wedgeA.Size,Enum.Material.Sand)
Terrain:FillBlock(wedgeB.CFrame,wedgeB.Size,Enum.Material.Sand)
Terrain:FillBlock(wedgeC.CFrame,wedgeC.Size,Enum.Material.Sand)
Terrain:FillBlock(wedgeD.CFrame,wedgeD.Size,Enum.Material.Sand)
]]--
end
end
addWater(chunk)
addTrash(chunk)
return chunk
end
function Chunk:Destroy()
for index, instance in ipairs(self.instances) do
instance:Destroy()
end
game.Workspace.Terrain:FillBlock(self.waterCFrame, self.waterSize, Enum.Material.Air)
end
return Chunk
One solution I tried was to modify the addTrash function so that it takes a NumberValue, “TRASH_SEED”, and increments it in hundredths for every time the block was run.
TRASH_SEED = game.Workspace.TRASH_SEED.Value
local function addTrash(chunk)
local posGrid = chunk.positionGrid
local instances = chunk.instances
local chunkPosX = chunk.x
local chunkPosZ = chunk.z
for x = 0, X-1 do
for z = 0, Z-1 do
local pos = posGrid[x][z]
if pos.Y >= MIN_TRASH_SPAWN_HEIGHT and pos.Y <= MAX_TRASH_SPAWN_HEIGHT then
math.randomseed((x * (chunkPosX+SEED))+(z * (chunkPosZ+SEED)))
if math.random() < TRASH_DENSITY then
local p = math.noise(TRASH_SEED) + 0.25
local garbage = game.ReplicatedStorage.Garbage:GetChildren()
local q = (p/0.75) * #garbage
local r = math.ceil(q+1)
if r then
continue
else
r = #garbage
end
local trash = garbage[r]:Clone()
local cframe = CFrame.new(pos)
* CFrame.new(
math.random()*math.random(-10,10),
0,
math.random()*math.random(-10,10)
)
* CFrame.Angles(
math.rad(math.random()*math.random(-30,30)),
2*math.pi*math.random(),
math.rad(math.random()*math.random(-30,30))
)
trash:SetPrimaryPartCFrame(cframe)
trash.Parent = TrashModels
table.insert(instances, trash)
TRASH_SEED += 0.01
end
end
end
end
end
This modification instead caused the terrain generation to stop generating trash on the terrain; I must have done something wrong.
I tried setting the randomseed to the TRASH_SEED, but the trash still generated differently for each player.
if math.random() < TRASH_DENSITY then
math.randomseed(TRASH_SEED)
local garbage = game.ReplicatedStorage.Garbage:GetChildren()
-- ...
end
Another solution I tried was setting the network owner of each and every BasePart in the model:
for _,v in pairs(trash:GetDescendants()) do
if v:IsA("BasePart") then
v:SetNetworkOwner()
end
end
As you would expect, I got an error that "Network Ownership API can only be called from the Server."
It seems that these solutions I had tried only ended up in failure of generating client-consistent trash; either the trash is different for each client, or just does not generate.
What other better solutions can you provide?
