Snow effect in Unity

I am making a game where you are supposed to sweep snow off the floor to protect passersby.

Let it snow

Tiny Spheres with RigidBody

My first idea was to make a tiny sphere called Snowflake. I made it white, gave it a Rigidbody and a large drag to make it fall slowly and stop on the ground. Then I dragged it into my Assets to turn it into a Prefab.

Next I created an empty object and added an Environment script to it. In the script, I made a loop that ran every second, and instantiated thousands of Snowflakes.

And it looked great! Except it started slowing to a crawl after a few seconds of running. Unity couldn't handle so many particles.

Particle system

1000 new particles per second

My next idea was to use a particle system. With a small max particle size of 0.04, I get something that looks like snow. Originally, the particles go up from a single spot like water from a sprinkler. To make it look like snowflakes fall from a large area, I changed the shape to a flat Rectangle.

z: -1 to make the particles fall.

To make the particles accumulate on the ground, I increased the start lifetime to 240s.

Finally to make it slow a lot, I set emissions to something like 1000.

However after letting it run for a while, it started to slow down again. If I set emissions to something smaller, then the ground isn't saturated with snowflakes, and the ground doesn't look all white like I'd like it to be. On the other hand, if I increase the size of snowflakes too much, then it looks like it's snowing snowballs, not snowflakes.

Growth over time

In order to reduce emissions, while still making the floor whiten over time, I made the snowflakes grow over time. Since they will grow slowly, long after they've landed on the ground, it isn't noticeable to the eye.

After they land, they will grow over time, making the ground "fill" with snow.

Future improvements

There are a bunch of videos on YouTube showing how to make more realistic looking snow. I'll use them to tweak my snow effect, once the other parts of the game are more settled.

Sweeping the snow

Player script

How do I make my character sweep snow? By destroying any particle that the player gets close to.

This post explains you should set the remainingLifetime to zero. The official doc also says so. Adjusting for my use case, it looks like this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.ParticleSystem;

public class Player : MonoBehaviour
{
  public ParticleSystem snowParticleSystem;
  Particle[] particles;

  // Start is called before the first frame update
  void Start()
  {
    // Allocate memory once.
    particles = new Particle[snowParticleSystem.main.maxParticles];
  }

  // Update is called once per frame
  void Update()
  {
    int particleCount = snowParticleSystem.GetParticles(particles, particles.Length);
    for (int i = 0; i < particleCount; i++)
    {
      Particle particle = particles[i];
      float distance = (transform.position - snowParticleSystem.transform.TransformPoint(particle.position)).magnitude;
      bool closeEnough = distance < 5;
      if (closeEnough)
      {
        // Delete the particle.
        particle.remainingLifetime = 0; // <== bug here. see fix below.
      }
    }
    snowParticleSystem.SetParticles(particles, particleCount);
  }
}

I was stuck for a bit because of the particle position. Initially I was computing the distance between the player and the particle like this: (transform.position - particle.position).magnitude, but that won't work since the particle.position is in the Particle System's referential. You need to convert it to world coordinates, just as transform.position is the position of the player in world coordinates.

Finally, with the distance being computed correctly, it found which particles to delete. However, setting remainingLifetime to 0 did nothing! How come?

It turns out I had expected Particle to be a class, but it's actually a struct. If it's a struct, then initializing Particle particle = particles[i] actually makes a copy of the particle. So I wasn't modifying the array at all. So I really should have killed the particle like this instead:

particles[i].remainingLifetime = 0;

Collision module

Another post mentions that you could set up Lifetime Loss to 1, and it'll destroy the particle on impact.

Setting Lifetime Loss to 1 will destroy the particle on collision.

Unfortunately it won't work in my use case, since I want the particle to collide with the Walkable layer, but get destroyed when it collides with the Player layer.

Also, the advantage of using the Player script, is that I get to define the radius of the sweeping.

The player (the capsule) swept some snow.

Walking animation

Using my cheat sheet and https://docs.unity3d.com/Manual/nav-CouplingAnimationAndNavigation.html, I imported a farmer mesh with its animations.

Then in my script, I pass the NavMeshAgent's velocity to the Animator's Speed_f float.

Basic broom

I made a broom out of a cylinder and a flattened cube in the model's left hand. The last step was to rotate the broom, but still leave it in the left hand. To do so I had to switch the rotation pivot to a point in the left hand, instead of the broom's center. This post shows how to do it.

Rotate the broom around the hand, instead of the broom's center.

NPCs

The game wouldn't be fun if there was no goal. I introduced NPCs who try to cross the snowy area. If there is too much snow around them, they stop walking.

To walk or not to walk

To do this, I implemented a function that counts the snowflakes in a circle around them. If it's above a certain density, I stop the nav mesh agent:

void Update()
{
  float density = GetSnowDensity();
  navMeshAgent.isStopped = density > maxSnowDensity;
}

private float GetSnowDensity()
{
  int particleCount = snowParticleSystem.GetParticles(particles, particles.Length);
  int particlesInRadius = 0;
  for (int i = 0; i < particleCount; i++)
  {
    Particle particle = particles[i];
    float distance = (transform.position - snowParticleSystem.transform.TransformPoint(particle.position)).magnitude;
    bool closeEnough = distance < sweepDistance;
    if (closeEnough)
    {
      particlesInRadius++;
    }
  }
  return particlesInRadius / sweepDistance * Mathf.PI * Mathf.PI;
}

Spawn location

The NPC needs a start and destination. Ideally, the NPC would spawn at the edge of the scene, then cross the central snowy area. The central snowy area was delimited by 4 walls. To pick a random spawn location, I could have hard-coded something like this:

wall = get a random int between 0 and 4
position on wall = get a random float between 0 and length(wall)
position = wall position + position * wall vector

This code is fine, but it assumes the walls are neatly positioned and don't intersect. However in my scene, I placed the walls quickly. They were longer than they could have been, and they intersected. So the snippet wouldn't have worked.

Instead, I used ray tracing. I pick a random direction from the center, then trace the ray, and find the intersection of that ray with one of the 4 walls.

const float MaxRayTracingDistance = 100;
public LayerMask wallsLayer;

private Vector3 GetRandomWallPoint()
{
  RaycastHit hit;
  Vector3 randomDirection = new Vector3(Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f));
  Ray ray = new Ray(Vector3.zero, randomDirection);
  if (Physics.Raycast(ray, out hit, MaxRayTracingDistance, wallsLayer.value))
  {
    // Make sure we're 2 meters away from the wall, inside the walls.
    return hit.point - 2 * randomDirection;
  }
  throw new UnityException();
}

Finally, the spawned NPC should look towards the inside of the area. We could have made it very random by picking another wall location, but it gives bad results. Sometimes it'll try to walk along the same wall, or cross the area in a way that doesn't cross the snowy area. Instead, it's easier to make the NPC walk the opposite way:

private void SpawnOnePasserby()
{
  // Somewhere on the walls.
  Vector3 initialPosition = GetRandomWallPoint();
  // Look back.
  Quaternion initialRotation = Quaternion.LookRotation(-initialPosition);
  npcs.Add(Instantiate(npcPrefab, initialPosition, initialRotation));
}

GameManager

The GameManager is responsible for spawning NPCs at regular intervals, and destroying them once they reach their destination. When they reach their destination, the GameManager will also increment the number of people saved. This acts as a game score.

Health bar

Another mechanic to make the game more interesting and give a sense of urgency is to give NPCs health. When it reaches zero, they die. To make a health bar, most people would make a 2D Canvas, make a green image on top of a full width red image, and scale the green image down to make it look like they lose health. It works ok in 2D games, but it's harder in 3D. Why? Because you have to make that canvas face the camera, no matter the direction the NPC is facing. See it explained here.

So instead, I designed my own health bar, which will be easily visible no matter the direction the NPC is facing. To do this, I built the bar with vertical 3D cylinders.

Upon death, I set the Death_b boolean to true on the animator, and play a death groan sound.

Mobile

Converting it to a Mobile game is a topic in itself, so I'll write about it in a separate article. See http://blog.wafrat.com/publishing-a-game-made-in-unity-to-the-play-store-in-2022/.