How to make a terrain modifier

Terrain Modifier

In this tutorial I will go over the basics of terrain modification in game. I will also go over how you can add a terrain gun into your game in the simplest way possible.

Let’s start

Terrain Methods

There are a bunch of things you have to know when modifying terrain. First, terrain in Roblox is based on a 4x4x4 Voxel Grid based system. Internally, Roblox handles the smoothing and “mixing” of the terrain too.

There are multiple ways to add and subtract of this terrain. Two terrain functions actually do some calculation for you already. These are:

  • Terrain:FillBall()
  • Terrain:FillBlock()

There is and extra method called Terrain:WriteVoxel() but we will get into that later.

When we finish our script we will learn how to do this.

https://gyazo.com/353b07086c2488166b057be5070d11b9

Ugh lag

So let’s get started.

Now there are a couple of ways to do this however we are going to use RunService combined with a simple voxel modification system.

Each Voxel has a material and an occuopancy of how much that materiel fills it’s cell and arround its cell too.

https://gyazo.com/1aa171527ca0bfa7752efcc82d405fea

Here we can see the size of a voxel with a default 4x2x1 part.

How?

So, what we will do is when we use the tool we will connect a RunService stepped event and fire the server every frame.

We do this to have the best out of replication and security. If you would like to get the best compromise between performance, replication and security there are a couple of other ways you can do this.

Alternative ways

Only fire the event 1 every X frames. Better performance less replication so another player might see a little delay.

Fire the event when the client finishes modifying the terrain then send the entire terrain data. Very good for performance but the replication only happens at the very end. Probably do not use this when you are considering fast paced or interactive game play. Maybe on building games okay. Kinda like when you see on some building games you can see the target before you place the item down.

We will be using Terrain:WriteVoxel() as it is the most reliable option we have interms of editability.

There are a couple of different tools that we can try an replicate in-game
image

Let’s focus on Erode for now.

Framework

We will be using a simple Tool to get player input and a couple of RemoteEvents for this. No need for RemoteFunctions. What is also important is to limit the amount of Voxels the player can edit every minute as Remotes can only fire definite amount of times every minute before they are dropped to prevent RE spam.

NOTE: I will NOT include any anti exploits here. All checks are done client side because I want to keep this simple for now. It is up to you to figure out any sanity checks you would want on this.

I will put some explanation and foot notes below

----[[SERVER CODE]]----
-- Services -- 1*
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- References
local Terrain = workspace.Terrain
local ErodeTerrainEvent = ReplicatedStorage.RemoteEvents.ErodeTerrain -- 2*

-- Variable
local CurrentTerrainVoxels = {} -- 3*

-- Function
local function ErodeTerrain(Player,Voxels: Region3) -- 4*
	Voxels = Voxels:ExpandToGrid(4)
	CurrentTerrainVoxels.Material, CurrentTerrainVoxels.Occupancy = Terrain:ReadVoxels(Voxels,4) -- 5*
	for X = 1, CurrentTerrainVoxels.Material.Size.X do -- 6*
		for Y = 1, CurrentTerrainVoxels.Material.Size.Y do
			for Z = 1, CurrentTerrainVoxels.Material.Size.Z do	
				CurrentTerrainVoxels.Occupancy[X][Y][Z] -= 0.2 -- 7*	
			end
		end
	end
	Terrain:WriteVoxels(Voxels,4,CurrentTerrainVoxels.Material,CurrentTerrainVoxels.Occupancy) -- 8*
end


-- Events
ErodeTerrainEvent.OnServerEvent:Connect(ErodeTerrain) -- 9*
----[[CLIENT CODE]]----

-- Services -- 1*
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- Constants -- 2*
local Tool = script.Parent
local HALF_VOXEL_SIZE = Vector3.new(2,2,2) 
local TOOL_MAXIMUM_CAPACITY = 600 

-- References
local ErodeTerrainEvent: RemoteEvent = ReplicatedStorage.RemoteEvents:WaitForChild("ErodeTerrain") -- 3*

-- Variables -- 4*
local Connection: RBXScriptConnection = nil
local ToolCapacity = TOOL_MAXIMUM_CAPACITY 
local Debounce = false
local MouseRegion3: Region3
local MouseMin: Vector3

-- Functions
local function ToolDeactivate() -- 5*
	Connection:Disconnect()
	task.wait(3)
	Debounce = false
end

local function ToolEquipped(Mouse: Mouse) -- 6*
	Tool.Activated:Connect(function()
		if Debounce then return end
		Debounce = true
		Connection = RunService.Stepped:Connect(function()
			if ToolCapacity < 1 then -- 7*
				ToolDeactivate() 
				return
			end
			ToolCapacity -= 1
			MouseRegion3 = Region3.new(Mouse.Hit.Position - HALF_VOXEL_SIZE, Mouse.Hit.Position + HALF_VOXEL_SIZE) -- 8*
			ErodeTerrainEvent:FireServer(MouseRegion3) 
		end)
		Tool.Deactivated:Wait() -- 9*
		ToolDeactivate()
	end)
end

-- Events
Tool.Equipped:Connect(ToolEquipped) -- 10*

Explanation

Server

  1. We always declare our services. In this case will will need Replicated Storage

  2. We need references to the objects for readability and performance especially if we are going to be using them a lot which we will be.

  3. We will setup an object to store all our current terrain voxels that we are editing. This might be a little less performant however it makes the code a lot more readable.

  4. Let’s setup our method for eroding terrain. It will take in two parameters although you can pass in more. We are only going to be passing in a region3 extra as the location needed to be eroded. If you want to make a terrain gun that adds terrain you may also pass a material enum value as a parameter. You can do this by doing `Enum.Material..Value . Also remember to include checks just incase Voxels is not a region3

  5. Terrain:ReadVoxels() is a function that we should use to get the current terrain data in the region that we are editing. It passes in Occupancy and Material as a 3D array. We edit this array then write the voxels back into the grid at the same location. NOTE: You have to Expand the voxels to a 4x4x4 grid BEFORE using this as Voxels is a 4x4x4 grid in nature. You also have to put resolution 4 as Roblox does not allow any other resolution at the moment.

  6. I did make a variable for the size of the array however I thought it would be better to just put it straight into the loop. What we are doing here is looping through the 3D array. We can use the Material or the Occupancy Size as it is all the same,

  7. Here is the real stuff. We are simply looping through the entire 3d array and taking away a little bit of occupancy. If the cell occupancy is < 0 when we write it in, Roblox willl change the cell to contain Air. This is the SIMPLEST terrain tool you can use. You may use AddBall along with RunService for the add effect which is also another simple tool. One thing you might want to know is that if you use the Add tool for a long time the Terrain usually clips the camera and in some cases the player. That is why I choose Erode for this.

  8. Write the voxels back in.

Client

  1. We need to declare services like the server

  2. We will need several constants for this. We will need the tool max cap to function sort of like ammo. We will also need a voxel size I will get into why in a moment

  3. Reference…

  4. We need variables such as debounce a connection variable to keep track of that and a mouse region3 where we will store the Region3 around the point where the mouse hits the terrain.

  5. We have a cooldown function here. This just clears up the debounce and resets the guns ammo. (Deactivate might not be the best name here but oh well)

  6. Tool equipped function. It’s also handy that the clients mouse is passed in which we can also get from this too.

  7. Check whethere we are out of ammo if so cooldown

  8. Here we create a 4x4x4 Region3 around the mouse. Due to floating point math it will never align with the actual voxel grid. I decided that I was going to have a 2x2x2 voxel grid size for the region that the player is editing. If you want less or more simply change the region size around the mouse to something larger than 4x4x4

  9. We are checking for the clients deactivation of the tool. Pretty handy event. Don’t forget it exists.

  10. Listen to tool equipped

Outcome

https://gyazo.com/bd988f6d721e1fa49470654c85ff46ba

I am not really that good at explaining or scripting. I only made this because I made something I will never use and thought it would be better to show someone else who might need it.

The systems can easily be adapted to do paint, add and subctract. Flatten, Grow and Smooth require extra logic to replicate. I won’t show you how because I don’t actually know how :laughing:

End notes

You can add special effects, sounds particles and a better tool model to make it look nice. It was slightly inspired by astroneer. If you have ever played or seen it you might know what I mean by it. Feel free to use the code!

Happy coding!

15 Likes

Smooth terrain Minecraft here we come…

2 Likes

I couldn’t make it work like you did.
What I did was:

  • Put the server code into a Script in ServerScriptService

  • Created a Tool in StarterPlayerScripts with a Part named Handle and the client code into a LocalScript.

Aaaaand nothing happened.
What am I doing wrong?

Did you create a RemoteEvent for the communication between Client and Server?

1 Like

No, how should I do that? Thought the tool script would trigger the event

Well it does. But you have to make sure that you have made an remote event first.

Then you need to reference it and then the code includes the FireServer function already

I referenced it here. But you need to reference it wherever you put your RemoteEvent. I put mine in ReplicatedStorage and I called it “ErodeTerrain”

1 Like