Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Server Script: Fast chunk loading + progressive block exposure + block break handling
- local Players = game:GetService("Players")
- local RunService = game:GetService("RunService")
- local Workspace = game:GetService("Workspace")
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- -- Config
- local BLOCK_SIZE = 3
- local CHUNK_SIZE = 16
- local HEIGHT_SCALE = 7
- local BASE_HEIGHT = 20
- local DIRT_LAYERS = 3
- local RENDER_DISTANCE_CHUNKS = 3 -- 3 chunks radius = 7x7 chunks total
- local ORE_CHANCES = {
- Emerald = 15,
- Diamond = 50,
- Ruby = 120,
- }
- local CAVE_CHANCE = 0.015
- local RAVINE_CHANCE = 0.001
- local RUBY_MAX_Y = 10
- local TREE_CHANCE = 0.05
- -- References
- local Blocks = Workspace:WaitForChild("Blocks")
- local Trees = Workspace:WaitForChild("Trees")
- local TerrainFolder = Workspace:FindFirstChild("GeneratedTerrain") or Instance.new("Folder", Workspace)
- TerrainFolder.Name = "GeneratedTerrain"
- local BreakBlockEvent = ReplicatedStorage:FindFirstChild("BreakBlockEvent")
- if not BreakBlockEvent then
- BreakBlockEvent = Instance.new("RemoteEvent")
- BreakBlockEvent.Name = "BreakBlockEvent"
- BreakBlockEvent.Parent = ReplicatedStorage
- end
- local noise = math.noise
- local Random = Random.new()
- -- Data store for chunks: key -> chunkData
- -- chunkData.blocks = {[posStr] = {blockName, rendered, part}}
- local Chunks = {}
- -- Helpers
- local function posToStr(x,y,z)
- return x..","..y..","..z
- end
- local function strToPos(s)
- local x,y,z = s:match("([^,]+),([^,]+),([^,]+)")
- return tonumber(x), tonumber(y), tonumber(z)
- end
- local function getChunkCoords(x,z)
- return math.floor(x/CHUNK_SIZE), math.floor(z/CHUNK_SIZE)
- end
- local function getChunkKey(cx, cz)
- return cx .. "," .. cz
- end
- local function worldToChunkBlockPos(x,y,z)
- local cx, cz = getChunkCoords(x,z)
- local bx = x - cx*CHUNK_SIZE
- local bz = z - cz*CHUNK_SIZE
- return cx, cz, bx, y, bz
- end
- local function createBlockPart(blockName, pos)
- local template = Blocks:FindFirstChild(blockName)
- if not template then return nil end
- local part = template:Clone()
- part:PivotTo(CFrame.new(pos))
- part.Parent = TerrainFolder
- return part
- end
- -- Coroutine-friendly chunk generation (yielding every few blocks to avoid lag)
- local function generateChunk(cx, cz)
- local chunkData = {blocks = {}}
- for bx = 0, CHUNK_SIZE - 1 do
- for bz = 0, CHUNK_SIZE - 1 do
- local wx = cx * CHUNK_SIZE + bx
- local wz = cz * CHUNK_SIZE + bz
- local height = math.floor(noise(wx / 25, wz / 25) * HEIGHT_SCALE + BASE_HEIGHT)
- for y = 0, height do
- -- Cave & ravine
- if y < height - 2 and noise(wx / 10, y / 10, wz / 10) > 0.55 and Random:NextNumber() < CAVE_CHANCE then
- continue
- end
- if Random:NextNumber() < RAVINE_CHANCE and wz % 20 == 0 and y < BASE_HEIGHT then
- continue
- end
- local blockName
- if y == height then
- blockName = "Grass"
- elseif y >= height - DIRT_LAYERS then
- blockName = "Dirt"
- else
- local placedOre = false
- for ore, chance in pairs(ORE_CHANCES) do
- if ore == "Ruby" and y > RUBY_MAX_Y then
- -- skip ruby
- elseif Random:NextInteger(1, chance) == 1 then
- blockName = ore
- placedOre = true
- break
- end
- end
- if not placedOre then
- blockName = "Stone"
- end
- end
- local worldPos = Vector3.new(wx * BLOCK_SIZE, y * BLOCK_SIZE, wz * BLOCK_SIZE)
- local key = posToStr(wx, y, wz)
- chunkData.blocks[key] = {
- blockName = blockName,
- rendered = false,
- part = nil
- }
- -- Render surface blocks only at generation
- if y == height then
- local part = createBlockPart(blockName, worldPos)
- chunkData.blocks[key].rendered = true
- chunkData.blocks[key].part = part
- end
- end
- end
- -- Yield every row of bz to avoid lag
- coroutine.yield()
- end
- -- Generate trees on grass surface blocks
- for key, data in pairs(chunkData.blocks) do
- if data.blockName == "Grass" and data.rendered then
- local x, y, z = strToPos(key)
- if Random:NextNumber() < TREE_CHANCE then
- local treeList = Trees:GetChildren()
- if #treeList > 0 then
- local tree = treeList[Random:NextInteger(1, #treeList)]:Clone()
- local treePos = Vector3.new(x * BLOCK_SIZE, (y + 1) * BLOCK_SIZE, z * BLOCK_SIZE)
- tree:PivotTo(CFrame.new(treePos))
- tree.Parent = TerrainFolder
- end
- end
- end
- end
- Chunks[getChunkKey(cx, cz)] = chunkData
- end
- local function renderBlock(x,y,z)
- local cx, cz, bx, by, bz = worldToChunkBlockPos(x,y,z)
- local key = getChunkKey(cx, cz)
- local chunk = Chunks[key]
- if not chunk then return end
- local blockKey = posToStr(x,y,z)
- local blockData = chunk.blocks[blockKey]
- if not blockData or blockData.rendered then return end
- local pos = Vector3.new(x * BLOCK_SIZE, y * BLOCK_SIZE, z * BLOCK_SIZE)
- local part = createBlockPart(blockData.blockName, pos)
- if part then
- blockData.rendered = true
- blockData.part = part
- end
- end
- local function exposeBlockAndNeighbors(x,y,z)
- -- Expose target block
- renderBlock(x,y,z)
- -- Expose all 6 neighbors (up/down/left/right/front/back)
- local neighbors = {
- {x+1, y, z}, {x-1, y, z},
- {x, y+1, z}, {x, y-1, z},
- {x, y, z+1}, {x, y, z-1}
- }
- for _, pos in ipairs(neighbors) do
- renderBlock(pos[1], pos[2], pos[3])
- end
- end
- local function breakBlock(x,y,z)
- local cx, cz = getChunkCoords(x,z)
- local chunkKey = getChunkKey(cx, cz)
- local chunk = Chunks[chunkKey]
- if not chunk then return end
- local blockKey = posToStr(x,y,z)
- local blockData = chunk.blocks[blockKey]
- if not blockData or not blockData.rendered then return end
- -- Remove block visual
- if blockData.part then
- blockData.part:Destroy()
- end
- -- Remove from chunk data
- chunk.blocks[blockKey] = nil
- -- Expose neighbors
- exposeBlockAndNeighbors(x,y,z)
- end
- local function getPlayerChunkPos(player)
- local hrp = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
- if hrp then
- local pos = hrp.Position
- local cx, cz = getChunkCoords(math.floor(pos.X / BLOCK_SIZE), math.floor(pos.Z / BLOCK_SIZE))
- return cx, cz
- end
- return nil, nil
- end
- local ACTIVE_CHUNKS = {}
- local function loadChunksAroundPlayer(player)
- local cx, cz = getPlayerChunkPos(player)
- if not cx or not cz then return end
- local keysToKeep = {}
- for dx = -RENDER_DISTANCE_CHUNKS, RENDER_DISTANCE_CHUNKS do
- for dz = -RENDER_DISTANCE_CHUNKS, RENDER_DISTANCE_CHUNKS do
- local ncx, ncz = cx + dx, cz + dz
- local key = getChunkKey(ncx, ncz)
- keysToKeep[key] = true
- if not Chunks[key] then
- -- Generate chunk in coroutine to avoid lag
- local co = coroutine.create(generateChunk)
- local success, err = coroutine.resume(co, ncx, ncz)
- while coroutine.status(co) ~= "dead" do
- coroutine.resume(co)
- task.wait() -- yield to prevent blocking
- end
- end
- -- Parent all rendered blocks' parts for active chunks
- if not ACTIVE_CHUNKS[key] then
- for _, blockData in pairs(Chunks[key].blocks) do
- if blockData.rendered and blockData.part and blockData.part.Parent ~= TerrainFolder then
- blockData.part.Parent = TerrainFolder
- end
- end
- ACTIVE_CHUNKS[key] = true
- end
- end
- end
- -- Unload distant chunks
- for key in pairs(ACTIVE_CHUNKS) do
- if not keysToKeep[key] then
- local chunk = Chunks[key]
- if chunk then
- for _, blockData in pairs(chunk.blocks) do
- if blockData.rendered and blockData.part then
- blockData.part.Parent = nil -- remove from workspace but keep data
- end
- end
- end
- ACTIVE_CHUNKS[key] = nil
- end
- end
- end
- -- Player Join Handler
- Players.PlayerAdded:Connect(function(player)
- -- Periodic chunk loading for player
- spawn(function()
- while player.Parent do
- loadChunksAroundPlayer(player)
- wait(1)
- end
- end)
- end)
- -- Handle block breaking event
- BreakBlockEvent.OnServerEvent:Connect(function(player, bx, by, bz)
- breakBlock(bx, by, bz)
- end)
- print("🌲 Chunk-based terrain generator (server) initialized.")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement