Hi
I have created a snowing effect that uses ParticleEmitters in conjunction with RayCasts to dynamically change the LifeTime of the particles. This way snow doesn’t fall underneath a roof.
Pictures:
The Emitter is constructed of different (15x15) tiles. Each of these tiles has a ParticleEmitter that has it’s LifeTime changed based on the tile’s distance from the ground.
The entire construction moves along with the player (it doesn’t rotate with the player!). This way the snow is always above the player’s character.
As this uses ParticleEmitters it also comes with their disadvantages being:
-
LifeTime is limited, so particles might not completely reach the ground.
-
Amount of emitted particles change on lower graphics settings.
Other inconveniences with this system:
- A change to the ParticleEmitters must be done multiple times for every emitter
A command to automate this:
for _, v in pairs(workspace.SnowEmitter:GetDescendants()) do
if v:IsA("ParticleEmitter") then
v.Size = 2
end
end
- Possible performance issues on lower-end devices (change RAYCAST_FRAME_UPDATE to be higher or BATCH_UPDATE_RAYCAST to be lower).
If you quickly want to browse the code:
local RAYCAST_FRAME_UPDATE = 10 -- Number of frames before every raycast (batch) update
local BATCH_UPDATE_RAYCAST = 1 -- Number of parts to raycast from every raycast update
local snowEmitter = workspace.SnowEmitter
local LocalPlayer = game:GetService("Players").LocalPlayer
local partTable = {}
for _, v in pairs(snowEmitter:GetDescendants()) do
if v:IsA("BasePart") and v.Name ~= "RootPart" then
table.insert(partTable, v)
end
end
local newTable = {}
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
raycastParams.FilterDescendantsInstances = partTable
raycastParams.IgnoreWater = false
local function copyTable(src, dest)
for index, value in pairs(src) do
dest[index] = value
end
end
local raycastFrameUpdateCounter = 0
game:GetService("RunService").Heartbeat:Connect(function()
-- Move model
local char = LocalPlayer.Character
if char and char:FindFirstChild("HumanoidRootPart") then
local position = char.HumanoidRootPart.Position
snowEmitter:SetPrimaryPartCFrame(CFrame.new(0, 30, 0) + position)
end
-- Raycast
raycastFrameUpdateCounter += 1
if raycastFrameUpdateCounter >= RAYCAST_FRAME_UPDATE then
raycastFrameUpdateCounter = 0
local used = {}
-- Raycast (in batch of BATCH_UPDATE_RAYCAST)
if #newTable <= 0 then
copyTable(partTable, newTable)
end
local batchCounter = 0
for _, v in pairs(newTable) do
batchCounter += 1
if batchCounter >= BATCH_UPDATE_RAYCAST then
local particleEmitter = v.ParticleEmitter
local raycastResult = workspace:Raycast(v.Position, Vector3.new(0, -100, 0), raycastParams)
if raycastResult and raycastResult.Position then
local lifeTime = (raycastResult.Position - v.Position).Magnitude / particleEmitter.Speed.Max
particleEmitter.Lifetime = NumberRange.new(lifeTime - 5, lifeTime)
table.insert(used, v)
end
end
end
-- Remove used parts from table
for _, v in pairs(used) do
local found = table.find(newTable, v)
if found then
table.remove(newTable, found)
end
end
end
end)
This code assumes the model named “SnowEmitter” is inside of workspace. This should be a LocalScript!
Here is a place file that should be ready to use:
ParticleSnowDemo.rbxl (26.4 KB)
Keep in mind that some scripting knowledge is needed to implement this effect (aka: enable, disable). There is no API provided, only the script and the model.
Feel free to use this inside of your game, no need to give me credit. I would love to see what you make with this! Please post any bugs, issues, or suggestions to improve this system down below.