“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?