Table of Contents
Snowflakes Simulation
A simple snowflakes simulation to show snowfall in LOVE2D. Snowflakes are generated using LOVE2D's particle system.
This simulation is heavily inspired by Dan Shiffman's video about snowflakes "Coding Challenge #88: Snowfall".
This youtube video shows a demo of the simulation
The code for the project is at https://github.com/abhishekmishra/explorations/tree/main/snowflakes
To run this yourself clone the repo and run it using love2d.
# clone the repo
git clone https://github.com/abhishekmishra/explorations.git
# goto the snowflakes folder
cd explorations/snowflakes
# run love2d in the current folder
love .
Credits
Artwork used in the simulation
Pixel Art for the Snowflakes
Artist: alxl
Artwork Page: pixel-art-snowflakes
License: CC-0
Background Music
Artist: caliderium
Artwork Page: a-lucid-dream
License: CC-BY 3.0
Code
snowflake.lua
--- snowflake.lua -- Snowflake class
--
-- date: 09/04/2024
-- author: Abhishek Mishra
local Class = require 'middleclass'
-- Load the sprite sheet
-- The sprite sheet contains 6 tiles in a row and 3 tiles in a column
-- Each tile is 9x9 pixels
-- The sprite sheet is available at
-- https://opengameart.org/content/pixel-art-snowflakes
-- made by https://opengameart.org/users/alxl
local spriteSheet = love.graphics.newImage("pixel_snowflakes.png")
-- Define the size of each tile
local tileWidth, tileHeight = 9, 9
-- Define the number of tiles per row and column
local tilesPerRow, tilesPerColumn = 6, 3
-- Create a table to hold the quads
local quads = {}
-- Create quads for each tile in the sprite sheet
for y = 0, tilesPerColumn - 1 do
for x = 0, tilesPerRow - 1 do
-- Calculate the position of the tile in the sprite sheet
local quad = love.graphics.newQuad(x * tileWidth, y * tileHeight,
tileWidth, tileHeight, spriteSheet:getDimensions())
table.insert(quads, quad)
end
end
-- Define the SnowFlake class
local SnowFlake = Class('SnowFlake')
function SnowFlake:initialize(x, y, size)
self.origx = x
self.origy = y
self.x = self.origx
self.y = self.origy
self.size = size
self.speed = math.random(50, 100)
self.direction = math.random() * 2 * math.pi
self:setParticleSystem()
end
--- SnowFlake:setParticleSystem: Set the particle system for the snowflake
function SnowFlake:setParticleSystem()
-- use a random quad from the sprite sheet
self.quad = quads[math.random(1, #quads)]
self.psystem = love.graphics.newParticleSystem(spriteSheet, 32)
self.psystem:setQuads(self.quad)
-- particles live at least 1s and at most 5-20s
self.psystem:setParticleLifetime(1, math.random(5, 20))
-- set emission rate to 1 particle per second
self.psystem:setEmissionRate(1)
-- only emit for 1.5s which means only 1 particle will be emitted
self.psystem:setEmitterLifetime(1.5)
-- full size variation (1 is max)
self.psystem:setSizeVariation(1)
-- set linear acceleration to -20, 20 on x-axis
-- and -10, 50 on y-axis
-- Random movement in all directions.
self.psystem:setLinearAcceleration(-20, -10, 20, 50)
-- set radial acceleration to -10, 10
-- Random acceleration towards/away the center.
self.psystem:setRadialAcceleration(-10, 10)
-- set spin to -2, 2
-- Random spin.
self.psystem:setSpin(-2, 2)
-- set colors to white
-- Fade to transparency.
self.psystem:setColors(1, 1, 1, 1, 1, 1, 1, 0)
-- set sizes to 1, 1.5, 1.1, 0.75, 0.25
-- Random sizes for the snowflake.
self.psystem:setSizes(1, 1.5, 1.1, 0.75, 0.25, 2, 3.3, 4)
end
function SnowFlake:update(dt)
self.y = self.y + self.speed * dt
self.x = self.x + math.sin(self.direction) * self.size * dt
--if the x position crosses the screen boundaries, wrap it around
if self.x < 0 then
self.x = love.graphics.getWidth()
elseif self.x > love.graphics.getWidth() then
self.x = 0
end
-- if the snowflake is beyond the bottom of the screen, reset it to
-- origx, origy
if self:beyondScreen(love.graphics.getHeight()) then
self.x = self.origx
self.y = self.origy
self:setParticleSystem()
end
self.psystem:update(dt)
end
function SnowFlake:draw()
love.graphics.draw(self.psystem, self.x, self.y)
end
--- SnowFlake:beyondScreen: Check if the snowflake is beyond the bottom of the
-- screen
-- @param height The height of the screen
function SnowFlake:beyondScreen(height)
return self.y > height
end
--- SnowFlake:stop: Stop the particle system
function SnowFlake:stop()
self.psystem:stop()
end
return SnowFlake
main.lua
--- main.lua: Snowflakes Simulation in LÖVE
-- date: 09/04/2024
-- author: Abhishek Mishra
local SnowFlake = require 'snowflake'
-- table to hold all the snowflakes
local snowflakes
-- font for credits
local creditsFont = love.graphics.newFont(12)
-- background music
local music = love.audio.newSource("A Lucid Dream.ogg", "stream")
-- number of snowflakes to create
local NUM_SNOWFLAKES = 5000
-- release rate per second
local RELEASE_RATE = 500
-- total released
local totalReleased = 0
--- createSnowflake: Create a new snowflake
-- @return SnowFlake: A new snowflake object
local function createSnowflake()
return SnowFlake:new(
math.random(0, love.graphics.getWidth()),
math.random(-love.graphics.getHeight(),
love.graphics.getHeight() * 0.05),
math.random(1, 3))
end
--- restart: Restart the simulation
local function restart()
-- stop all existing snowflakes
if snowflakes then
for _, snowflake in ipairs(snowflakes) do
snowflake:stop()
end
end
-- create a new table to hold snowflakes
snowflakes = {}
totalReleased = 0
-- if there is already music playing, stop it
love.audio.stop()
-- load music and play
music:setLooping(true)
love.audio.play(music)
end
--- love.load: Called once at the start of the simulation
function love.load()
restart()
end
--- love.update: Called every frame, updates the simulation
function love.update(dt)
if totalReleased < NUM_SNOWFLAKES then
local toRelease = math.floor(RELEASE_RATE * dt)
-- create new snowflakes
for i = 1, toRelease do
table.insert(snowflakes, createSnowflake())
totalReleased = totalReleased + 1
end
end
for _, snowflake in ipairs(snowflakes) do
snowflake:update(dt)
end
end
--- love.draw: Called every frame, draws the simulation
function love.draw()
love.graphics.setColor(0.8, 0.9, 1)
for _, snowflake in ipairs(snowflakes) do
snowflake:draw()
end
-- draw text at the bottom of the screen to show credits
love.graphics.setColor(0.7, 0.5, 0.5, 0.8)
-- set credits font
love.graphics.setFont(creditsFont)
love.graphics.print("Snowflakes Simulation in LÖVE by ne0l4t3r4l (https://neolateral.in)", 10,
love.graphics.getHeight() - 50)
love.graphics.print("Pixel-art snowflakes by https://opengameart.org/users/alxl", 10, love.graphics.getHeight() - 35)
love.graphics.print("Background Score - \"The Lucid Dream\" by https://opengameart.org/users/caliderium", 10,
love.graphics.getHeight() - 20)
-- draw text at the bottom-right of the screen to show FPS
-- love.graphics.setColor(0.5, 0.4, 0.4)
-- love.graphics.print("FPS: " .. love.timer.getFPS(), love.graphics.getWidth() - 50, love.graphics.getHeight() - 20)
end
-- escape to exit
function love.keypressed(key)
if key == "escape" then
love.event.quit()
end
if key == 'r' then
restart()
end
end
conf.lua
function love.conf(t)
-- set the window title
t.window.title = "Snowflakes Simulation"
-- run fullscreen
t.window.fullscreen = true
-- disable unused modules for performance
t.modules.joystick = false
t.modules.physics = false
t.modules.touch = false
end