Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --!native
- local GrassPhysics = {}
- local originalParents = {}
- GrassPhysics.__index = GrassPhysics
- -- Services
- local Players = game:GetService("Players")
- local CollectionService = game:GetService("CollectionService")
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
- local TweenService = game:GetService("TweenService")
- local RunService = game:GetService("RunService")
- -- Constants
- local ROOT = workspace
- local CAMERA = ROOT.CurrentCamera
- GrassPhysics.Config = {
- WindShake = {
- Enabled = false, -- Enable/disable wind shake effect, ENABLING THIS DOES NOT ADD IT INTO YOUR CODE! You must set it up yourself, but turn this on if you have it shake the grass using wind.
- ScriptPath = nil, -- Path to the wind shake script, not the module, the script that requires it, example: Players.LocalPlayer.PlayerScripts.WindControllerClient
- },
- OcclusionCulling = {
- Enabled = false, -- Enable/disable occlusion culling, can improve performance but may cause bugs.
- },
- Sound = {
- Enable = true, -- Enable/disable sound effects
- Volume = 0.1, -- Volume of the plant sound effects (0.0 - 1.0)
- PitchRange = { -- Range for randomizing pitch of plant sounds
- Min = 0.8,
- Max = 1.0
- },
- EffectDuration = 0.5, -- Duration to play the sound effect before destroying
- Reverb = { -- Reverb settings for the sound
- Enable = false,
- DecayTime = 2.0,
- ReflectLevel = 0.5
- },
- Spatial = { -- Spatial audio settings
- Enable = true,
- RolloffMode = Enum.RollOffMode.Linear,
- MinDistance = 10,
- MaxDistance = 100
- }
- },
- Tween = {
- Duration = 1.6, -- Duration of animation tweens in seconds
- EasingStyle = Enum.EasingStyle.Elastic, -- Easing style for animation tweens
- EasingDirection = Enum.EasingDirection.Out, -- Easing direction for animation tweens
- TweenName = "pivotTween", -- Name for the tween instance
- },
- Physics = {
- HitboxRadius = 200, -- Radius around the hitbox within which effects will be activated
- Intervals = {
- UpdateInterval = 0.1, -- Update interval for checking activations and camera positions
- CameraUpdateFactor = 2, -- Factor to control how often camera updates are checked
- ActivationUpdateFactor = 2 -- Factor to control how often activation updates are checked
- },
- },
- Grass = {
- Tag = "Grass", -- Tag used to identify grass objects, changing this can break the plugin
- HitboxTag = "Hitbox", -- Tag used to identify hitbox objects, changing this can break the plugin
- Transparency = 1, -- Transparency level of the hitbox
- CollisionResponse = { -- Collision response settings for the grass
- Enable = true,
- Friction = 0.5,
- Bounce = 0.1
- },
- },
- Camera = {
- FieldOfViewThreshold = 2, -- Additional degrees to the camera's field of view for angle checks
- MovementThreshold = 1, -- Minimum movement in position or rotation to consider camera as moved
- },
- SoundAsset = {
- SoundEnabled = true,
- SoundMappings = {
- Grass = {
- SoundId = "rbxassetid://8008438167",
- VolumeRange = { Min = 0, Max = 0.1 }
- },
- --[[Add more sounds as needed, example
- Flower = {
- SoundId = "rbxassetid://0",
- VolumeRange = { Min = 0, Max = 0.1 }
- },]]
- }
- },
- Debug = {
- EnableLogging = false, -- Enable or disable debug logging
- ExtraLogging = false, -- Extra debug logging, may flood your output; must have Enable Logging set to true for this to work
- LogFrequency = 5, -- Number of updates between log entries
- }
- }
- --[[
- The following code is created by varuniemcfruity for the "Interactive Grass" plugin on ROBLOX.
- If you need to contact me, please send a private message through the developer forum or reach out to me via my profile.
- Please respect my work by not stealing or reselling this code. If you have purchased the plugin, you are welcome to use it in all of your games.
- However, redistribution for profit or sharing the code for free is prohibited.
- ]]--
- -- Internal Data
- local grassInfo = {}
- local hitboxInfo = {}
- local activeObjects = {}
- local playerEvents = {}
- local physicsEvents = {}
- local hitboxEvents = {}
- local timeTracker = 0
- local updateCounter = 0
- local lastCamCFrame = CAMERA.CFrame
- local hitboxFolder = Instance.new("Folder", ROOT)
- hitboxFolder.Name = "HitboxContainer"
- -- Constants based on Configurations
- local TW_INFO = TweenInfo.new(
- GrassPhysics.Config.Tween.Duration,
- GrassPhysics.Config.Tween.EasingStyle,
- GrassPhysics.Config.Tween.EasingDirection
- )
- local UP_VECTOR = Vector3.FromNormalId(Enum.NormalId.Back)
- local ROT_CFRAME = CFrame.Angles(0, math.rad(-90), math.rad(90))
- local SQRT_3 = math.sqrt(3)
- local INTERVAL = GrassPhysics.Config.Physics.Intervals.UpdateInterval
- local HITBOX_RADIUS = GrassPhysics.Config.Physics.HitboxRadius
- -- Helper Functions
- local function logDebug(message)
- if GrassPhysics.Config.Debug.EnableLogging then
- print("[GrassPhysics Debug]: " .. message)
- end
- end
- local function createTween(obj, properties, info)
- local tween = TweenService:Create(obj, info, properties)
- tween:Play()
- return tween
- end
- local function initiateTween(name, mesh, targetCFrame)
- if mesh:FindFirstChild(name) then
- mesh[name]:Destroy()
- end
- local tween = createTween(mesh, { CFrame = targetCFrame }, TW_INFO)
- tween.Name = name
- tween.Parent = mesh
- tween.Completed:Connect(function()
- tween:Destroy()
- end)
- end
- local function activateEffect(hitbox)
- physicsEvents[hitbox] = {
- Touched = hitbox.Touched:Connect(function(part)
- if not grassInfo[part] or not grassInfo[part].Eligible then return end
- if table.find(activeObjects, part) then return end
- local partType = part.Parent.Name
- local soundConfig = GrassPhysics.Config.SoundAsset.SoundMappings[partType]
- if not soundConfig then
- logDebug("No sound configuration found for part type: " .. partType)
- end
- local humanoid = Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
- table.insert(activeObjects, part)
- local rotation = part.Parent:GetAttribute("Rotation")
- local info = grassInfo[part]
- local direction = ((hitbox.Position - part.Position) * -Vector3.new(1, 0, 1)).Unit * part.Size.Y / SQRT_3
- local targetLook = info.Top + direction
- local targetPos = info.Base + (targetLook - info.Base).Unit * part.Size.Y / 2
- local finalCFrame = CFrame.lookAt(targetPos, targetLook, UP_VECTOR) * ROT_CFRAME * CFrame.Angles(rotation.X, rotation.Y, rotation.Z)
- initiateTween(GrassPhysics.Config.Tween.TweenName, part.Parent, finalCFrame)
- if GrassPhysics.Config.Sound.Enable and GrassPhysics.Config.SoundAsset.SoundMappings[partType] then
- local sound = Instance.new("Sound", hitbox)
- sound.SoundId = soundConfig.SoundId
- local pitchShift = Instance.new("PitchShiftSoundEffect", sound)
- pitchShift.Octave = math.random(
- GrassPhysics.Config.Sound.PitchRange.Min * 100,
- GrassPhysics.Config.Sound.PitchRange.Max * 100
- ) / 100
- sound.Volume = math.random(
- soundConfig.VolumeRange.Min * 100,
- soundConfig.VolumeRange.Max * 100
- ) / 100
- sound:Play()
- delay(GrassPhysics.Config.Sound.EffectDuration, function()
- sound:Destroy()
- end)
- if GrassPhysics.Config.Sound.Reverb.Enable then
- local reverb = Instance.new("ReverbSoundEffect", sound)
- reverb.DecayTime = GrassPhysics.Config.Sound.Reverb.DecayTime
- reverb.Density = GrassPhysics.Config.Sound.Reverb.ReflectLevel
- end
- if GrassPhysics.Config.Sound.Spatial.Enable then
- sound.RollOffMode = GrassPhysics.Config.Sound.Spatial.RolloffMode
- sound.MinDistance = GrassPhysics.Config.Sound.Spatial.MinDistance
- sound.MaxDistance = GrassPhysics.Config.Sound.Spatial.MaxDistance
- end
- end
- logDebug("Activated effect for part: " .. part.Name)
- if GrassPhysics.Config.WindShake.Enabled then
- GrassPhysics.Config.WindShake.ScriptPath.Enabled = false
- end
- end),
- TouchEnded = hitbox.TouchEnded:Connect(function(part)
- local idx = table.find(activeObjects, part)
- if not grassInfo[part] or not idx then return end
- table.remove(activeObjects, idx)
- initiateTween(GrassPhysics.Config.Tween.TweenName, part.Parent, part.CFrame)
- logDebug("Ended touch effect for part: " .. part.Name)
- local touchingParts = hitbox:GetTouchingParts()
- local stillTouching = false
- for _, touchingPart in ipairs(touchingParts) do
- if grassInfo[touchingPart] and table.find(activeObjects, touchingPart) then
- stillTouching = true
- break
- end
- end
- if not stillTouching and GrassPhysics.Config.WindShake.Enabled then
- GrassPhysics.Config.WindShake.ScriptPath.Enabled = true
- end
- end)
- }
- end
- local function deactivateEffect(hitbox)
- if physicsEvents[hitbox] then
- physicsEvents[hitbox].Touched:Disconnect()
- physicsEvents[hitbox].TouchEnded:Disconnect()
- physicsEvents[hitbox] = nil
- logDebug("Deactivated effect for hitbox: " .. hitbox.Name)
- end
- end
- local function enableGrass(grass)
- grass.Hitbox.CanTouch = true
- end
- local function disableGrass(grass)
- grass.Hitbox.CanTouch = false
- if grass:FindFirstChild(GrassPhysics.Config.Tween.TweenName) then
- grass[GrassPhysics.Config.Tween.TweenName]:Cancel()
- end
- grass.CFrame = grass.Hitbox.CFrame
- local idx = table.find(activeObjects, grass.Hitbox)
- if idx then
- table.remove(activeObjects, idx)
- end
- end
- local function resetGrass(grass)
- if grass:FindFirstChild(GrassPhysics.Config.Tween.TweenName) then
- grass[GrassPhysics.Config.Tween.TweenName]:Cancel()
- end
- grass.CFrame = grass.Hitbox.CFrame
- local idx = table.find(activeObjects, grass.Hitbox)
- if idx then
- table.remove(activeObjects, idx)
- end
- end
- local function setupGrassInstance(grass)
- grass:SetAttribute("Rotation", Vector3.new(math.rad(grass.Orientation.X), math.rad(grass.Orientation.Y), math.rad(grass.Orientation.Z)))
- local grassHitbox = grass:FindFirstChild("Hitbox") or Instance.new("Part", grass)
- grassInfo[grassHitbox] = {
- Top = grass.Position + Vector3.new(0, grass.Size.Y / 2, 0),
- Base = grass.Position - Vector3.new(0, grass.Size.Y / 2, 0),
- Eligible = true
- }
- if not grass:FindFirstChild("Hitbox") then
- grassHitbox.Size = grass.Size
- grassHitbox.CanCollide = false
- grassHitbox.Anchored = true
- grassHitbox.Transparency = GrassPhysics.Config.Grass.Transparency
- grassHitbox.Name = "Hitbox"
- grass.CFrame = CFrame.lookAt(grass.Position, grassInfo[grassHitbox].Top, UP_VECTOR) * ROT_CFRAME * CFrame.Angles(grass:GetAttribute("Rotation").X, grass:GetAttribute("Rotation").Y, grass:GetAttribute("Rotation").Z)
- grassHitbox.CFrame = grass.CFrame
- end
- logDebug("Setup grass instance: " .. grass.Name)
- end
- local function removeGrassInstance(grass)
- disableGrass(grass)
- if grassInfo[grass.Hitbox] then
- grassInfo[grass.Hitbox] = nil
- end
- grass.Hitbox:Destroy()
- logDebug("Removed grass instance: " .. grass.Name)
- end
- local function setupHitboxInstance(hitbox)
- if not hitbox:IsDescendantOf(ROOT) then return end
- hitboxInfo[hitbox] = { Eligible = true }
- print(hitboxInfo)
- activateEffect(hitbox)
- logDebug("Setup hitbox instance: " .. hitbox.Name)
- end
- local function removeHitboxInstance(hitbox)
- deactivateEffect(hitbox)
- hitboxInfo[hitbox] = nil
- if hitboxEvents[hitbox] then
- hitboxEvents[hitbox]:Disconnect()
- hitboxEvents[hitbox] = nil
- end
- logDebug("Removed hitbox instance: " .. hitbox.Name)
- end
- local function cameraPositionChanged(hitboxOnly)
- task.desynchronize()
- local controlVec = lastCamCFrame.LookVector
- local threshold = CAMERA.FieldOfView + GrassPhysics.Config.Camera.FieldOfViewThreshold
- for obj, data in (hitboxOnly and hitboxInfo or grassInfo) do
- local directionVec = obj.Position - lastCamCFrame.Position
- local angle = math.deg(directionVec.Unit:Angle(controlVec))
- local playerPosVec = Players.LocalPlayer.Character and Players.LocalPlayer.Character.PrimaryPart and (obj.Position - Players.LocalPlayer.Character.PrimaryPart.Position)
- local distance = playerPosVec and (Vector3.new(playerPosVec.X, 0, playerPosVec.Z)).Magnitude
- if angle <= threshold and distance and distance <= HITBOX_RADIUS then
- if not data.Eligible then
- data.Eligible = true
- data.Updated = false
- end
- else
- if data.Eligible then
- data.Eligible = false
- data.Updated = false
- end
- end
- end
- lastCamCFrame = CAMERA.CFrame
- if GrassPhysics.Config.Debug.ExtraLogging == true then logDebug("Camera position changed. Hitbox only: " .. tostring(hitboxOnly)) end
- task.synchronize()
- end
- local function handleCharacterAddition(character)
- local primaryPart = character:WaitForChild("HumanoidRootPart")
- local humanoid = character:WaitForChild("Humanoid")
- local newHitbox = ReplicatedStorage.InteractiveGrass.Hitbox:WaitForChild("Hitbox"):Clone()
- newHitbox.Parent = hitboxFolder
- local attachment = Instance.new("Attachment", newHitbox)
- local alignPosition = Instance.new("AlignPosition", attachment)
- alignPosition.RigidityEnabled = true
- alignPosition.Mode = Enum.PositionAlignmentMode.OneAttachment
- alignPosition.Attachment0 = attachment
- alignPosition.Enabled = true
- local alignOrientation = Instance.new("AlignOrientation", attachment)
- alignOrientation.RigidityEnabled = true
- alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
- alignOrientation.Attachment0 = attachment
- alignOrientation.Enabled = true
- hitboxEvents[newHitbox] = RunService.Stepped:Connect(function()
- alignPosition.Position = primaryPart.Position
- end)
- newHitbox.Position = primaryPart.Position
- CollectionService:AddTag(newHitbox, GrassPhysics.Config.Grass.HitboxTag)
- logDebug("Character added: " .. character.Name)
- humanoid.Died:Connect(function()
- for _, grass in ipairs(activeObjects) do
- resetGrass(grass.Parent)
- logDebug("Reset grass: " .. grass.Parent.Name)
- end
- activeObjects = {}
- logDebug("Player died, deactivated all grass and hitboxes")
- end)
- end
- local function handleCharacterRemoval(character)
- local hitboxPart = character:FindFirstChild("Hitbox")
- if hitboxPart then
- hitboxPart:Destroy()
- end
- logDebug("Character removed: " .. character.Name)
- end
- local function updateCameraCFrame(dt)
- timeTracker += dt
- if timeTracker > INTERVAL * GrassPhysics.Config.Physics.Intervals.CameraUpdateFactor then
- timeTracker = 0
- if (CAMERA.CFrame.Position - lastCamCFrame.Position).Magnitude > GrassPhysics.Config.Camera.MovementThreshold or CAMERA.CFrame.Rotation ~= lastCamCFrame.Rotation then
- updateCounter = 0
- task.spawn(cameraPositionChanged)
- else
- updateCounter += 1
- if updateCounter > GrassPhysics.Config.Debug.LogFrequency then
- updateCounter = 0
- task.spawn(cameraPositionChanged, true)
- end
- end
- end
- if GrassPhysics.Config.Debug.ExtraLogging == true then logDebug("Camera CFrame updated.") end
- end
- local function updateActivations(dt)
- timeTracker += dt
- if timeTracker > INTERVAL * GrassPhysics.Config.Physics.Intervals.ActivationUpdateFactor then
- timeTracker = 0
- for obj, data in pairs(grassInfo) do
- if data.Eligible then
- if not data.Updated then
- data.Updated = true
- enableGrass(obj.Parent)
- end
- else
- if not data.Updated then
- data.Updated = true
- disableGrass(obj.Parent)
- end
- end
- end
- for obj, data in pairs(hitboxInfo) do
- if data.Eligible then
- if not data.Updated then
- data.Updated = true
- activateEffect(obj)
- end
- else
- if not data.Updated then
- data.Updated = true
- deactivateEffect(obj)
- end
- end
- end
- if GrassPhysics.Config.Debug.ExtraLogging == true then logDebug("Activations updated.") end
- end
- end
- Players.PlayerAdded:Connect(function(player)
- playerEvents[player.Name] = {
- CharacterAdded = player.CharacterAdded:Connect(handleCharacterAddition),
- CharacterRemoving = player.CharacterRemoving:Connect(handleCharacterRemoval)
- }
- if player.Character then
- handleCharacterAddition(player.Character)
- end
- end)
- Players.PlayerRemoving:Connect(function(player)
- local events = playerEvents[player.Name]
- if events then
- events.CharacterAdded:Disconnect()
- events.CharacterRemoving:Disconnect()
- playerEvents[player.Name] = nil
- end
- end)
- CollectionService:GetInstanceAddedSignal(GrassPhysics.Config.Grass.Tag):Connect(setupGrassInstance)
- CollectionService:GetInstanceRemovedSignal(GrassPhysics.Config.Grass.Tag):Connect(removeGrassInstance)
- CollectionService:GetInstanceAddedSignal(GrassPhysics.Config.Grass.HitboxTag):Connect(setupHitboxInstance)
- CollectionService:GetInstanceRemovedSignal(GrassPhysics.Config.Grass.HitboxTag):Connect(removeHitboxInstance)
- RunService:BindToRenderStep("CameraUpdateStep", Enum.RenderPriority.Camera.Value, updateCameraCFrame)
- RunService:BindToRenderStep("ActivationUpdateStep", Enum.RenderPriority.Camera.Value + 2, updateActivations)
- for _, grass in ipairs(CollectionService:GetTagged(GrassPhysics.Config.Grass.Tag)) do
- setupGrassInstance(grass)
- end
- for _, hitbox in ipairs(CollectionService:GetTagged(GrassPhysics.Config.Grass.HitboxTag)) do
- setupHitboxInstance(hitbox)
- end
- for _, player in ipairs(Players:GetPlayers()) do
- playerEvents[player.Name] = {
- CharacterAdded = player.CharacterAdded:Connect(handleCharacterAddition),
- CharacterRemoving = player.CharacterRemoving:Connect(handleCharacterRemoval)
- }
- if player.Character then
- handleCharacterAddition(player.Character)
- end
- end
- local occlusionEnabled = GrassPhysics.Config.OcclusionCulling.Enabled
- script.Parent.Parent.Culling.GrassCulling.Enabled = occlusionEnabled
- return GrassPhysics
Add Comment
Please, Sign In to add comment