Grass System: looking for advice on how to lower script activity

This script clones grass stems, then raycasts to find floor, then positions grass on the stem and finally simulates windy grass that constantly changes based on wind flow values found in the workspace.

As I’ve tested things it had increased and decreased.

Current Script Activity averages at around 9.8%, so I’m looking for it to be brought down a lot.

--// Create Storage for Grass
local GrassStorage = game.ReplicatedStorage.PlayerObjects.Foliage.Grass.GrassStorage:Clone()
GrassStorage.Parent = script.Parent

--// Tables
local GrassFolder = {}
local initalStemCFrameFolder = {}
local XVelFolder = {}
local ZVelFolder = {}
local XUpFolder = {}
local ZUpFolder = {}
--local newRotFolder = {}
--local rotReachedFolder = {}
local XNewRotFolder = {}
local ZNewRotFolder = {}
local LastGrassBladePosition = {}
local LastStemPosition = {}
local GrassDespawnDebounce = {}
local GrassSpawnDebounce = {}
local GrassReady = {}
local TickActive = {}
local TickNum = {}
local TickRate = {}
local MaxVelX = {}
local MinVelX = {}
local MaxVelZ = {}
local MinVelZ = {}
local VelChangeRateX = {}
local VelChangeRateZ = {}

--// Create Grass Spawner
local GrassSpawner = game.ReplicatedStorage.PlayerObjects.Foliage.Grass.GrassSpawners.GrassSpawn1:Clone()
GrassSpawner.Parent = script.Parent

--// Itentify services
local PhysicsService = game:GetService("PhysicsService")
local CollectionService = game:GetService("CollectionService")

--// Set Values
local SetGrassAmount = 300
local GrassAmount = 0

--// Other Values
local grassfolder = "Grass"
local initalStemCFrame = game.ReplicatedStorage.PlayerObjects.Foliage.Grass.GrassBlade.Stem.CFrame
local InitialSpawnDebounce = false
local SpawnDebounce = false
local DeSpawnDebounce = false
local DistanceTickNum, DistanceTickRate, DistanceTickActive = 0,2,true
local CheckDebounce = false

--// Pre-Register all operators
local CFrame_new = CFrame.new
local CFrame_Angles = CFrame.Angles
local math_random = math.random
local math_rad = math.rad
local Vector3_new = Vector3.new
local RaycastParams_new = RaycastParams.new
local Ray_New = Ray.new
local math_floor = math.floor
local math_sqrt = math.sqrt
local math_abs = math.abs

local function round(n)
	return math_floor(n + 0.5)
end

function SpawnGrass(part, amount, setting) -- Settings: "Random", "Distance", "Rows"
	local distance = nil
	--local maximum = part.
	for i = 1, amount, 1 do
		local Grass = game.ReplicatedStorage.PlayerObjects.Foliage.Grass.GrassBlade:Clone()
		Grass.Parent = GrassStorage
		local grasstype = math_random(1,3)
		if grasstype == 1 then
			Grass.GrassBlade2:Destroy()
			Grass.GrassBlade3:Destroy()
			Grass.GrassBlade1.Name = "GrassBlade"
			Grass.GrassBlade.GrassBlade1Front.Name = "GrassBladeFront"
			Grass.GrassBlade.GrassBlade1Back.Name = "GrassBladeBack"
			PhysicsService:SetPartCollisionGroup(Grass.GrassBlade.GrassBladeFront, grassfolder)
			PhysicsService:SetPartCollisionGroup(Grass.GrassBlade.GrassBladeBack, grassfolder)
			PhysicsService:SetPartCollisionGroup(Grass.Stem, grassfolder)
		elseif grasstype == 2 then
			Grass.GrassBlade1:Destroy()
			Grass.GrassBlade3:Destroy()
			Grass.GrassBlade2.Name = "GrassBlade"
			Grass.GrassBlade.GrassBlade2Front.Name = "GrassBladeFront"
			Grass.GrassBlade.GrassBlade2Back.Name = "GrassBladeBack"
			PhysicsService:SetPartCollisionGroup(Grass.GrassBlade.GrassBladeFront, grassfolder)
			PhysicsService:SetPartCollisionGroup(Grass.GrassBlade.GrassBladeBack, grassfolder)
			PhysicsService:SetPartCollisionGroup(Grass.Stem, grassfolder)
		elseif grasstype == 3 then
			Grass.GrassBlade1:Destroy()
			Grass.GrassBlade2:Destroy()
			Grass.GrassBlade3.Name = "GrassBlade"
			Grass.GrassBlade.GrassBlade3Front.Name = "GrassBladeFront"
			Grass.GrassBlade.GrassBlade3Back.Name = "GrassBladeBack"
			PhysicsService:SetPartCollisionGroup(Grass.GrassBlade.GrassBladeFront, grassfolder)
			PhysicsService:SetPartCollisionGroup(Grass.GrassBlade.GrassBladeBack, grassfolder)
			PhysicsService:SetPartCollisionGroup(Grass.Stem, grassfolder)
		end
		if setting == "Random" then
			local X1 = round(part.Position.X + part.Size.X/2)
			local X2 = round(part.Position.X - part.Size.X/2)
			local Z1 = round(part.Position.Z + part.Size.Z/2)
			local Z2 = round(part.Position.Z - part.Size.Z/2)
			local XRandom = math_random(X2, X1)
			local ZRandom = math_random(Z2, Z1)
			XRandom = XRandom + math_random(-90,90)/100
			ZRandom = ZRandom + math_random(-90,90)/100
			Grass.Stem.Position = Vector3_new(XRandom, part.Position.Y - 1, ZRandom)
		end
		lookforplatform(Grass.Stem)
	end
end

function lookforplatform(Stem)
	-- Set an origin and directional vector
	local ray = Ray.new(Stem.Position, Vector3.new(0, -50, 0))
	local rayOrigin = Stem.Position
	local rayDirection = Vector3_new(0, -100, 0)

	-- Build a "RaycastParams" object and cast the ray
	local raycastParams = RaycastParams_new()
	raycastParams.FilterDescendantsInstances = {Stem.Parent}
	raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
	local raycastResult = workspace:Raycast(rayOrigin, rayDirection, raycastParams)
	
	local part, pos, _, material = workspace:FindPartOnRayWithIgnoreList(ray, {Stem.Parent})

	if part then
		--print(part.Name)
		if material == Enum.Material.Grass then
			--print"landed"
			GrassAmount += 1
		else
			Stem.Parent:Destroy()
			return
		end
	end

	if raycastResult then
		--printRegion(raycastResult.Position)
		--print(raycastResult.Parent.Name)
		local distance = Stem.Position.Y - raycastResult.Position.Y
		Stem.Position = raycastResult.Position
		--local GrassType = Stem.GrassMainScript:GetAttribute("GrassType")
		local grass = Stem.Parent.GrassBlade.GrassBladeFront
		Stem.Parent.GrassBlade.GrassBladeFront.Position = Vector3_new(grass.Position.X, grass.Position.Y - distance, grass.Position.Z)
		Stem.Parent.GrassBlade.GrassBladeBack.Position = Stem.Parent.GrassBlade.GrassBladeFront.Position
	end
	setBladePosition(Stem)
end

function setBladePosition(stem)
	local CPoint = stem.Position
	local Gf = nil
	Gf = stem.Parent.GrassBlade.GrassBladeFront
	local Gb = stem.Parent.GrassBlade.GrassBladeBack
	Gf.Position = Vector3_new((CPoint.X + (Gf.Size.X/2)), (CPoint.Y + (Gf.Size.Y/2)), (CPoint.Z + (Gf.Size.Z/2)))
	Gb.Position = Gf.Position
	local GBWeld = Instance.new("Weld", stem.Parent.GrassBlade.GrassBladeFront)
	GBWeld.Part0 = stem.Parent.GrassBlade.GrassBladeFront
	GBWeld.Part1 = stem.Parent.GrassBlade.GrassBladeBack
	local GBM6D = stem.GBM6D
	GBM6D.Name = "GBM6D"
	GBM6D.Part0 = stem.Parent.GrassBlade.GrassBladeFront
	GBM6D.Part1 = stem
	local offset = CFrame_new(Gf.Size.X/2,-(Gf.Size.Y/2),0)
	GBM6D.C0 = GBM6D.Part0.CFrame:inverse() * GBM6D.Part1.CFrame * offset
	GBM6D.C1 = CFrame_Angles(0,math_random(-180,180),0)
	table.insert(XVelFolder, 0)
	table.insert(ZVelFolder, 0)
	table.insert(XUpFolder, false)
	table.insert(ZUpFolder, false)
	--table.insert(newRotFolder, 0)
	--table.insert(rotReachedFolder, true)
	table.insert(XNewRotFolder, 0)
	table.insert(ZNewRotFolder, 0)
	table.insert(GrassFolder, stem)
	table.insert(GrassDespawnDebounce, false)
	table.insert(GrassSpawnDebounce, false)
	table.insert(GrassReady, true)
	table.insert(TickNum, 1)
	table.insert(TickRate, 5)
	table.insert(TickActive, false)
	table.insert(MaxVelX, 0.5)
	table.insert(MinVelX, -0.5)
	table.insert(MaxVelZ, 0.5)
	table.insert(MinVelZ, -0.5)
	table.insert(VelChangeRateX, 0.01)
	table.insert(VelChangeRateZ, 0.01)
	table.insert(initalStemCFrameFolder, stem.CFrame)
end

local distance = 0

local debounce1 = false

local Camera = game:GetService("Workspace").CurrentCamera
local CameraLocation = nil

local distanceX = 0
local distanceY = 0
local distanceZ = 0

Below is the cause of the script activity. Its the code that constantly moves all the grass stems.

local function UpdateDistance(object, i, CameraLocation)
	distance = (object.Position - CameraLocation).Magnitude
	
	if i >= 1 then
		if distance >= 200 then
			if GrassDespawnDebounce[i] == false then
				GrassDespawnDebounce[i] = true
				GrassReady[i] = false
				local TheParent = GrassFolder[i].Parent
				TheParent.Parent = game.Lighting.GrassCache
				GrassSpawnDebounce[i] = false
			end
			TickActive[i] = false
		else
			GrassDespawnDebounce[i] = false
			if GrassSpawnDebounce[i] == false then
				GrassSpawnDebounce[i] = true
				GrassReady[i] = true
				local TheParent = GrassFolder[i].Parent
				TheParent.Parent = script.Parent.GrassStorage
			end
		end
		
		if distance <= 40 then
			TickRate[i] = 0
			VelChangeRateX[i] = 0.01
			VelChangeRateZ[i] = 0.01
			MaxVelX[i] = 0.4
			MinVelX[i] = -0.4
			MaxVelZ[i] = 0.4
			MinVelZ[i] = -0.4
		elseif distance > 40 and distance <= 60 then
			TickRate[i] = 1
			VelChangeRateX[i] = 0.03
			VelChangeRateZ[i] = 0.03
			MaxVelX[i] = 0.7
			MinVelX[i] = -0.7
			MaxVelZ[i] = 0.7
			MinVelZ[i] = -0.7
		elseif distance > 60 and distance <= 75 then
			TickRate[i] = 4
			VelChangeRateX[i] = 0.1
			VelChangeRateZ[i] = 0.1
			MaxVelX[i] = 1
			MinVelX[i] = -1
			MaxVelZ[i] = 1
			MinVelZ[i] = -1
		elseif distance > 75 and distance <= 90 then
			TickRate[i] = 7
			VelChangeRateX[i] = 0.15
			VelChangeRateZ[i] = 0.15
			MaxVelX[i] = 1.2
			MinVelX[i] = -1.2
			MaxVelZ[i] = 1.2
			MinVelZ[i] = -1.2
		end
		
		return distance, TickRate[i], MaxVelX[i], MinVelX[i], MaxVelZ[i], MinVelZ[i], VelChangeRateX[i], VelChangeRateZ[i], GrassDespawnDebounce[i], GrassSpawnDebounce[i], GrassReady[i]
	else
		return distance
	end
end

local function UpdateVel(UpValue, VelValue, VelChangeRate, MaxVelValue, MinVelValue, i, Type)
	if UpValue[i] == true then
		if VelValue[i] > MaxVelValue[i] then
			VelValue[i] = MaxVelValue[i]
		else
			VelValue[i] += VelChangeRate[i]
		end
	elseif XUpFolder[i] == false then
		if VelValue[i] < MinVelValue[i] then
			VelValue[i] = MinVelValue[i]
		else
			VelValue[i] -= VelChangeRate[i]
		end
	end
	
	if Type == "X" then
		return XUpFolder[i] == UpValue, XVelFolder[i] == VelValue, VelChangeRateX[i] == VelChangeRate
	elseif Type == "Z" then
		return ZUpFolder[i] == UpValue, ZVelFolder[i] == VelValue, VelChangeRateZ[i] == VelChangeRate
	end
end

local function UpdateGrass(i, GWX, GWZ)
	local GrassOrientationX = GrassFolder[i].Orientation.X
	local GrassOrientationZ = GrassFolder[i].Orientation.Z
	
	UpdateVel(XUpFolder, XVelFolder, VelChangeRateX, MaxVelX, MinVelX, i, "X")
	UpdateVel(ZUpFolder, ZVelFolder, VelChangeRateZ, MaxVelZ, MinVelZ, i, "Z")
	
	if (XUpFolder[i] == true and GrassOrientationX >= XNewRotFolder[i]) or (ZUpFolder[i] == true and GrassOrientationZ >= ZNewRotFolder[i]) 
		or (XUpFolder[i] == false and GrassOrientationX <= XNewRotFolder[i]) or (ZUpFolder[i] == false and GrassOrientationZ <= ZNewRotFolder[i]) then
		--local pressure = math_random(1,5)
		XNewRotFolder[i] = GWX + math_random(-3,3)
		ZNewRotFolder[i] = GWZ + math_random(-3,3)
		if XNewRotFolder[i] >= GrassOrientationX then
			XUpFolder[i] = true
		else
			XUpFolder[i] = false
		end
		if ZNewRotFolder[i] >= GrassOrientationZ then
			ZUpFolder[i] = true
		else
			ZUpFolder[i] = false
		end
	end
	
	GrassFolder[i].CFrame = initalStemCFrameFolder[i] * CFrame_Angles(math_rad(GrassOrientationX + XVelFolder[i]),0,math_rad(GrassOrientationZ + ZVelFolder[i]))

	return XUpFolder[i], ZUpFolder[i], XVelFolder[i], ZVelFolder[i]
end

game:GetService("RunService").RenderStepped:Connect(function(Step)
	if InitialSpawnDebounce == false then
		InitialSpawnDebounce = true
		SpawnGrass(GrassSpawner, SetGrassAmount, "Random")
	end

	Camera = workspace.CurrentCamera
	
	CameraLocation = Vector3_new(Camera.CFrame.Position.X, Camera.CFrame.Position.Y, Camera.CFrame.Position.Z)

	UpdateDistance(GrassSpawner, -1, CameraLocation)
	
	if distance <= 220 then
		if DistanceTickActive then
			for i = 1, GrassAmount, 1 do
				UpdateDistance(GrassFolder[i], i, CameraLocation)
			end
		end
		if DistanceTickNum < DistanceTickRate then
			DistanceTickNum += 1
			DistanceTickActive = false
		elseif DistanceTickNum >= DistanceTickRate then
			DistanceTickNum = 0
			DistanceTickActive = true
		end
		CheckDebounce = false
	end
	if distance > 270 then
		if CheckDebounce == false then
			local GrassCache = game.Lighting.GrassCache
			for i = 1, GrassAmount, 1 do
				if GrassFolder[i].Parent.Parent.Name == "GrassStorage" then
					GrassFolder[i].Parent.Parent = GrassCache
				end
			end
			CheckDebounce = true
		end
	end
	
	for i = 1, GrassAmount, 1 do
		UpdateTickrate(i, distance)
		
		if TickActive[i] == true and GrassReady[i] == true then
			local GWX = workspace.ServerStats.Weather.GeneralWindX.Value
			local GWZ = workspace.ServerStats.Weather.GeneralWindZ.Value
			UpdateGrass(i, GWX, GWZ)
		end
	end
end)

I appreciate and will look forward to any help you can provide on this issue!

7 Likes

I would suggest looking into the Wind Shake module by @boatbomber: Wind Shake: High performance wind effect for leaves and foliage

1 Like

Thanks! I’ll definitely look into it.

1 Like

I got it to work, but it wasn’t as smooth or natural as my previous code. It only knocked average activity down to 9%.
I think I’ll use WindShake for tree leaves and minor foliage like vines.
Outside of that I’ll keep looking for ways of resolving the issues I got with my grass.

Thanks for the suggestion, I appreciate it greatly!

1 Like

Did you replace the entire movement logic with the WindShake module?

Could you show what your code roughly looks like now?
The WindShake module should be pretty efficient…

What happened was I added all the grass to a collection group and ran the script off that. Even then the movement of the grass wasn’t how I wanted it. Its a great system but I don’t think it’ll work well for what I have in mind for the grass. Still gonna use it for the tree leaves tho.
*Didn’t use previous code when trying it, was only the module.

1 Like

Quick question, when your player moves does the grass far away despawn? Does new grass get cloned and set near where the player moves to?

I recently implemented that but it does freeze when it tries to spawn again. Looking into much better performance fixes on spawning grass.

I suggest creating a custom grass cache.

When you’re creating a handful of parts, it can really slow down the engine. So when the player moves far away from certain grass, instead of destroying it, I suggest parenting it to a folder called “Cache” and setting the primary CFrame of the grass to 99999.

At the line in your code where the script needs more grass, instead of making new grass everytime, check the Cache folder for old grass, then just set the CFrame to where it needs to be.

Bonus points if you do this all on the client, for ~2k parts my operation time (how long it took to generate) went from 0.035 seconds to 0.022. Also dropped my performance stats Receiving and Networking.

3 Likes

I’ll definitely try that out. Would moving it to Lighting or replicated storage also work? Or is it best to just set the CFrame to 99999 as you suggested?

I mean this can help a little bit.
You should cache your functions since you are going to be firing them a lot.

local math_random = math.random
local CFrame_Angles = CFrame.Angles

You can read up on this here


Access to external locals (that is, variables that are local to an enclosing
function) is not as fast as access to local variables, but it is still faster than
access to globals. Consider the next fragment:

function foo (x)
	for i = 1, 1000000 do
		x = x + math.sin(i)
	end
	return x
end
print(foo(10))

We can optimize it by declaring sin once, outside function foo:

local sin = math.sin
function foo (x)
	for i = 1, 1000000 do
		x = x + sin(i)
	end
	return x
end
print(foo(10))

This second code runs 30% faster than the original one.


3 Likes

Didn’t know that, will definitely try this since I use a lot of those throughout my code. Thanks for advice!

Doesn’t matter, I got it to work thanks to you man. It spawns and despawns without any frame drops now!
Thanks for the help!

1 Like

I was able to knock activity down from 15% to 13% so thanks for the advice! Will remember to do that in the future.

1 Like

You can do this but I found it to be a little slower. Setting CFrame to 99999 works better in my opinion.

1 Like

You might also do a global hash table of all the possible Angles for your CFrame and then only change the hash table if weather conditions change.

You should checkout this module: PartCache, for all your quick part-creation needs It should simplify the caching process.
It was created for efficiently “creating” many projectiles inside your game. But can be implemented for other stuff to.

You do realize that the optimizations you mentioned aren’t even relevant in Luau in the first place? Luau VM is way more optimized than Lua currently is.

Benchmark:

local sin = math.sin

function foo (x)
	for i = 1, 1000000 do
		x = x + math.sin(i)
	end
	return x
end

function foos (x)
	for i = 1, 1000000 do
		x = x + sin(i)
	end
	return x
end

e = os.clock()

foo(1)

print(("Not Cached: %s"):format(os.clock() - e))

e = os.clock()

foos(2)

print(("Not Cached: %s"):format(os.clock() - e))

Cached: 0.036631599999964
Not Cached: 0.036271200002375

1 Like

Could you make the grass script a plugin or open sourced? Because it looks cool. And Looks cooler than when I use boatbomber’s WindShake module on grass!

I’ll make it open sourced if I can get the script activity below 5%.
As it stands I’ve added better working grass since this post, but it runs at the cost of 15-17% script Activity.
Heavily researching how to reduce this number, but having very little luck at the moment.

1 Like