How would I add a wind system to my plant

so recently I made a plant that tweens in the direction you walked into it from and I wanted to to look better. I would like to add a wind system to that it sways and looks more alive. I have no idea on how I would implement something like this because I would not be able to have two tweens run at the same time because it would bug out the touch tween. does anybody know how i could make this work

here is my script

local PS = game:GetService('Players')
local CLS = game:GetService('CollectionService')
local RPS = game:GetService('ReplicatedStorage')
local TWS = game:GetService('TweenService')
local RS = game:GetService('RunService')

local hitboxFolder : Folder = Instance.new('Folder') do
	hitboxFolder.Name = 'HitboxFolder'
	hitboxFolder.Parent = workspace
end

local player : Player = PS.LocalPlayer
local camera : Camera = workspace.CurrentCamera
local hitbox : BasePart = RPS.Hitbox

local grassData : {[BasePart] : {Top : Vector3, Pivot : Vector3, IsEligible : boolean, ActivationUpdated : boolean}} = {}
local hitboxes : {[BasePart] : {IsEligible : boolean, ActivationUpdated : boolean}} = {}
local activeParts : {BasePart} = {}

local playerConnections : {[string] : {CharacterAdded : RBXScriptConnection, CharacterRemoving : RBXScriptConnection}} = {}
local physicsConnections : {[BasePart] : {Touched : RBXScriptConnection?, TouchEnded : RBXScriptConnection}} = {}  
local hitboxConnections : {[BasePart] : RBXScriptConnection} = {}

local rotationCFrame : CFrame = CFrame.Angles(0, math.rad(-90), math.rad(90))
local upAxis : Vector3 = Vector3.FromNormalId(Enum.NormalId.Back)
local tweenInfo : TweenInfo = TweenInfo.new(3.5, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out, 0, false, 0)
local root3 : number = math.sqrt(3)

local timeCount : number = 0
local updateTimeCount : number = 0
local timeInterval : number = 0.1
local updateInterval : number = timeInterval * 2
local updateCount : number = 0
local activationRadius : number = 200

local eligibilityChanged : boolean = false
local hitboxEligibilityChanged : boolean = false
local lastCFrame : CFrame = camera.CFrame

local function tween(name : string, mesh : MeshPart, newCFrame : CFrame)
	if mesh:FindFirstChild(name) and mesh[name]:IsA('Tween') then
		mesh[name]:Destroy()
	end

	local tween = TWS:Create(mesh, tweenInfo, {CFrame = newCFrame})
	tween.Name = name
	tween.Parent = mesh
	tween:Play()
	tween.Completed:Connect(function()
		tween:Destroy()
		tween = nil
	end)
end

local function activateHitbox(hitboxInstance : BasePart)
	physicsConnections[hitboxInstance] = {}

	physicsConnections[hitboxInstance].Touched = hitboxInstance.Touched:Connect(function(partTouched : BasePart)
		if not (grassData[partTouched]) or not (grassData[partTouched].IsEligible) then return end
		if (table.find(activeParts, partTouched)) then return end

		activeParts[#activeParts + 1] = partTouched

		local data = grassData[partTouched]

		local direction : Vector3 = ((hitboxInstance.Position - partTouched.Position) * -Vector3.new(1, 0, 1)).Unit * partTouched.Size.Y/root3
		local newLookAt : Vector3 = (data.Top + direction)
		local newPosition : Vector3 = data.Pivot + ((newLookAt - data.Pivot).Unit * partTouched.Size.Y/2)

		local finalCFrame = CFrame.lookAt(newPosition, newLookAt, upAxis) * rotationCFrame
		tween('pivot', partTouched.Parent, finalCFrame)
	end)

	physicsConnections[hitboxInstance].TouchEnded = hitboxInstance.TouchEnded:Connect(function(partTouched : BasePart)
		local index = table.find(activeParts, partTouched)
		if not (grassData[partTouched]) or not (index) then return end
		if not (grassData[partTouched].IsEligible) then return end

		table.remove(activeParts, index)
		tween('pivot', partTouched.Parent, partTouched.CFrame)
	end)

	hitboxInstance.BrickColor = BrickColor.new('Magenta')
end

local function deactivateHitbox(hitboxInstance : BasePart)
	if (physicsConnections[hitboxInstance]) then
		physicsConnections[hitboxInstance].Touched:Disconnect()
		physicsConnections[hitboxInstance].TouchEnded:Disconnect()
		physicsConnections[hitboxInstance] = nil
	end

	hitboxInstance.BrickColor = BrickColor.new('Really red')
end

local function activateGrass(grassInstance : MeshPart)
	grassInstance.Hitbox.CanTouch = true
	grassInstance.BrickColor = BrickColor.new('Camo')
end

local function deactivateGrass(grassInstance : MeshPart)
	grassInstance.Hitbox.CanTouch = false

	if (grassInstance:FindFirstChild('pivot')) then
		grassInstance.pivot:Cancel()
		grassInstance.CFrame = grassInstance.Hitbox.CFrame
	end

	local index = table.find(activeParts, grassInstance.Hitbox)
	if (index) then
		table.remove(activeParts, index)
		grassInstance.CFrame = grassInstance.Hitbox.CFrame
	end

	grassInstance.BrickColor = BrickColor.new('New Yeller')
end

local function setupGrass(grassInstance : BasePart)
	local hasHitbox = grassInstance:FindFirstChild('Hitbox')
	local grassHitbox = hasHitbox or Instance.new('Part')

	grassData[grassHitbox] = {
		Top = grassInstance.Position + Vector3.new(0, grassInstance.Size.Y/2, 0),
		Pivot = grassInstance.Position - Vector3.new(0, grassInstance.Size.Y/2, 0),
		IsEligible = true,
		ActivationUpdated = false,
	}

	if not (hasHitbox) then
		grassInstance.CFrame = CFrame.lookAt(grassInstance.Position, grassData[grassHitbox].Top, upAxis) * rotationCFrame
		grassHitbox.CFrame = grassInstance.CFrame
		grassHitbox.Size = grassInstance.Size
		grassHitbox.CanCollide = false
		grassHitbox.CanQuery = false
		grassHitbox.Anchored = true
		grassHitbox.Massless = true
		grassHitbox.CastShadow = false
		grassHitbox.Transparency = 1
		grassHitbox.Name = 'Hitbox'
		grassHitbox.Parent = grassInstance
	end
end

local function removeGrass(grassInstance : BasePart)
	deactivateGrass(grassInstance)

	if (grassData[grassInstance.Hitbox]) then
		grassData[grassInstance.Hitbox] = nil
	end

	grassInstance.Hitbox:Destroy()
end

local function setupHitbox(hitboxInstance : BasePart)
	if not (hitboxInstance:IsDescendantOf(workspace)) then return end
	hitboxes[hitboxInstance] = {IsEligible = true, ActivationUpdated = false}

	activateHitbox(hitboxInstance)
end

local function removeHitbox(hitboxInstance : BasePart)
	deactivateHitbox(hitboxInstance)

	if hitboxes[hitboxInstance] then
		hitboxes[hitboxInstance] = nil
	end

	if (hitboxConnections[hitboxInstance]) then
		hitboxConnections[hitboxInstance]:Disconnect()
		hitboxConnections[hitboxInstance] = nil
	end
end

local function cameraCFrameChanged(hitboxOnly : boolean)
	local controlVector : Vector3 = lastCFrame.LookVector
	local threshold : number = camera.FieldOfView + 2

	task.desynchronize()

	if not (hitboxOnly) then
		for hitbox, data in grassData do

			local directionVector : Vector3 = (hitbox.Position - lastCFrame.Position)
			local angle : number = math.floor(math.deg(directionVector.Unit:Angle(controlVector)))

			-- If the distance is not within the radius, we check to see if its in the camera's viewport, and activate if its inside the viewport.
			if (angle <= threshold) then

				local directionVector2 : Vector3? = player.Character and player.Character.PrimaryPart and (hitbox.Position - player.Character.PrimaryPart.Position) 
				local xzDistance : number? = directionVector2 and (Vector3.new(directionVector2.X, 0, directionVector2.Z)).Magnitude

				-- If the distance is within the activationRadius, then the grass will be activated regardless of if the camera has it in its viewport or not.
				if (xzDistance) and (xzDistance <= activationRadius) then
					if not (data.IsEligible) then
						eligibilityChanged = true
						data.IsEligible = true
						data.ActivationUpdated = false
					end 
				else
					if (data.IsEligible) then
						eligibilityChanged = true
						data.IsEligible = false
						data.ActivationUpdated = false
					end
				end
			else
				if (data.IsEligible) then
					eligibilityChanged = true
					data.IsEligible = false
					data.ActivationUpdated = false
				end
			end
		end
	end

	for hitbox, data in hitboxes do
		local directionVector : Vector3 = (hitbox.Position - lastCFrame.Position)
		local angle : number = math.floor(math.deg(directionVector.Unit:Angle(controlVector)))

		if (angle <= threshold) then
			local directionVector2 : Vector3? = player.Character and player.Character.PrimaryPart and (hitbox.Position - player.Character.PrimaryPart.Position) 
			local xzDistance : number? = directionVector2 and (Vector3.new(directionVector2.X, 0, directionVector2.Z)).Magnitude

			-- If the distance is within the activationRadius, then the grass will be activated regardless of if the camera has it in its viewport or not.
			if (xzDistance) and (xzDistance <= activationRadius) then
				if not (data.IsEligible) then
					hitboxEligibilityChanged = true
					data.IsEligible = true
					data.ActivationUpdated = false
				end 
			else
				if (data.IsEligible) then
					hitboxEligibilityChanged = true
					data.IsEligible = false
					data.ActivationUpdated = false
				end
			end
		else
			if (data.IsEligible) then
				hitboxEligibilityChanged = true
				data.IsEligible = false
				data.ActivationUpdated = false
			end
		end
	end

	task.synchronize()
end

local function characterAdded(character : Model)
	local primaryPart : BasePart = character:WaitForChild('HumanoidRootPart')
	local newHitbox : BasePart = hitbox:Clone()

	local attachment = Instance.new('Attachment')
	attachment.Parent = newHitbox

	local alignPosition : AlignPosition = Instance.new('AlignPosition') do
		alignPosition.RigidityEnabled = true
		alignPosition.Mode = Enum.PositionAlignmentMode.OneAttachment
		alignPosition.Attachment0 = attachment
		alignPosition.Enabled = true
		alignPosition.Parent = attachment

		hitboxConnections[newHitbox] = RS.Stepped:Connect(function()
			alignPosition.Position = primaryPart.Position
		end)
	end

	newHitbox.Position = primaryPart.Position
	newHitbox.Parent = hitboxFolder
	CLS:AddTag(newHitbox, 'Hitbox')
end

local function characterRemoving(character : Model)
	character.Hitbox:Destroy()
end

local function updateCamera(dt : number)
	timeCount += dt

	if (timeCount > updateInterval) then
		timeCount = 0

		if (camera.CFrame.Position - lastCFrame.Position).Magnitude > 1 or (camera.CFrame.Rotation ~= lastCFrame.Rotation) then
			updateCount = 0
			lastCFrame = camera.CFrame
			task.spawn(cameraCFrameChanged)
		else
			updateCount += 1
			if (updateCount > 4) then
				updateCount = 0
				task.spawn(cameraCFrameChanged, true)
			end
		end
	end
end

local function updateActivation(dt : number)
	updateTimeCount += dt

	if (updateTimeCount > (updateInterval)) then
		updateTimeCount = 0

		-- Update activation for grass
		if (eligibilityChanged) then
			eligibilityChanged = false

			for hitbox, data in grassData do
				if (data.IsEligible) then

					if not (data.ActivationUpdated) then
						data.ActivationUpdated = true
						activateGrass(hitbox.Parent)
					end

				else
					if not (data.ActivationUpdated) then
						data.ActivationUpdated = true
						deactivateGrass(hitbox.Parent)
					end
				end
			end
		end

		-- Update activation for hitboxes
		if (hitboxEligibilityChanged) then
			hitboxEligibilityChanged = false

			for hitbox, data in hitboxes do
				if (data.IsEligible) then
					if not (data.ActivationUpdated) then
						data.ActivationUpdated = true
						activateHitbox(hitbox)
					end
				else
					if not (data.ActivationUpdated) then
						data.ActivationUpdated = true
						deactivateHitbox(hitbox)
					end
				end
			end
		end
	end
end

PS.PlayerAdded:Connect(function(player : Player)
	playerConnections[player.Name] = {
		CharacterAdded = player.CharacterAdded:Connect(characterAdded),
		CharacterRemoving = player.CharacterRemoving:Connect(characterRemoving)
	}

	if (player.Character) then
		characterAdded(player.Character)
	end
end)

PS.PlayerRemoving:Connect(function(player : Player)
	playerConnections[player.Name].CharacterAdded:Disconnect()
	playerConnections[player.Name].CharacterRemoving:Disconnect()
	playerConnections[player.Name] = nil
end)

CLS:GetInstanceAddedSignal('Grass'):Connect(setupGrass)
CLS:GetInstanceRemovedSignal('Grass'):Connect(removeGrass)

CLS:GetInstanceAddedSignal('Hitbox'):Connect(setupHitbox)
CLS:GetInstanceRemovedSignal('Hitbox'):Connect(removeHitbox)

RS:BindToRenderStep('cameraUpdate', Enum.RenderPriority.Camera.Value, updateCamera)
RS:BindToRenderStep('activationUpdate', Enum.RenderPriority.Camera.Value + 2, updateActivation)

for i, grassInstance in (CLS:GetTagged('Grass')) do
	setupGrass(grassInstance)
end

for i, hitboxInstance in (CLS:GetTagged('Hitbox')) do
	setupHitbox(hitboxInstance)
end

for i,player in (PS:GetPlayers()) do
	playerConnections[player.Name] = {
		CharacterAdded = player.CharacterAdded:Connect(characterAdded),
		CharacterRemoving = player.CharacterRemoving:Connect(characterRemoving)
	}

	if (player.Character) then
		characterAdded(player.Character)
	end
end
1 Like

You could use an open-sourced module like Wind Shake by boatbomber:

It’s really good I recommend it :+1:

1 Like