Module

Sand Particle

Progress 3/5
3/5

Now that we can draw sand particles, let’s make them fall!

Inheritance

In programming, classes are like blueprints for creating objects. An object is a collection of data (properties) and actions (methods). In our project, we’ll use classes to represent different types of particles, like Sand.

If you are confused about what classes are and how they work check out this breakdown

Inheritance is a powerful concept in object-oriented programming where a new class (called a subclass or derived class) can inherit properties and methods from an existing class (called a superclass or base class). This helps us write more organized and reusable code.

In our particles.js file, you’ll see a base Particle class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * Base particle class
 */
class Particle {
    constructor() {
        this.color = "";
        this.type = "";
    }

    /**
     * Returns true if the particle should swap with other when trying
     * to move onto the same grid location as {@link other}.
     *
     * EX: Let sand sink below water
     *
     * @param {Particle} other
     * @returns {boolean} Should the particle swap
     */
    swap(other) {
        return false;
    }

    /**
     * Update the particle at location (row, col)
     *
     * @param {number} row
     * @param {number} col
     */
    update(row, col) {

    }
}

This is our base blueprint for all particles. It has a constructor that initializes the color and type properties, and it has two methods: swap and update.

Next, you’ll see the Sand class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * Sand particle
 */
export class Sand extends Particle {
    constructor() {
        super(); // Calls the constructor of the parent class (Particle)
        this.color = "orange";
        this.type = "sand";
    }

    swap(other) {
        // TODO make sand fall under the water
    }

    update(row, col) {
        // TODO update sand
    }
}

Notice the extends Particle keyword. This tells JavaScript that the Sand class inherits from the Particle class. This means that the Sand class automatically gets the color, type, swap, and update properties and methods from the Particle class.

The update(row, col) method is where we’ll define how a sand particle behaves over time. We need to modify this to move the sand down one row in the grid every time the update is called.

First we need a helper function to move a particle in the grid. Open the canvas.js file and find the moveParticle function. Replace the TODO comment with the following code:

1
2
3
4
export function moveParticle(row, col, newRow, newCol, swap) {
    grid[newRow][newCol] = grid[row][col];
    return true;
}

This function takes the current row and column (row, col) of a particle and the new row and column (newRow, newCol), where we want to move it. It then simply copies the particle from its old position in the grid to its new position (ignore the return true; for now).

Now, open the particles.js file and find the update method inside the Sand class. Replace the TODO comment with the following code (ignore this.swap we will be using it later):

1
2
3
update(row, col) {
    moveParticle(row, col, row+1, col, this.swap);
}

Make sure you put this code in the update function of the Sand class not the base Particle class.

This code calls the moveParticle function to move the sand particle from its current row and col to the row below it.

Issues with moving

There are two errors with the current implementation, they should become apparent as soon as you run this code and click on the screen. Lets fix them.

When the sand reaches the bottom of the canvas, you might start seeing errors in the console again. This is because we are trying to move the sand to a row that doesn’t exist (outside the bounds of our grid).

To fix this, we need to update the checkBounds function in canvas.js to checks if a given row and column are within the valid bounds of our grid.

TASK 1: Modify the checkBounds function so it returns true if (row, col) is within the bounds of grid and false otherwise. After writing the function, use it in moveParticle to prevent particles from being moved out of bounds.

Task 1: Hint
Think about the dimensions of our grid. How can you check if a given row is within the valid range of rows? What about the column?

Try to complete the task before moving on

Answer (click to unblur):

1
2
3
export function checkBounds(row, col) {
    return row < grid.length && row >= 0 && col < grid[0].length && col >= 0;
}

moveParticle in canvas.js should look something like this:

1
2
3
4
5
6
7
8
9
10
export function moveParticle(row, col, newRow, newCol, swap) {
    // 👇 Put the following code here 👇
    if (!checkBounds(row, col) || !checkBounds(newRow, newCol)) {
        return false;
    }
    // 👆 Put the following code here 👆


    // Rest of moveParticle
}

You might notice that when the sand moves down, it leaves a trail behind it. This is because we are only copying the sand particle to the new position and not removing it from its old position.

TASK 2: Modify the moveParticle function in canvas.js to stop the particles from streaking as they fall.

Task 2: Hint
What do we use to represent an empty particle
Hint 2
Are we moving the particle or just making a new one

Try to complete the task before moving on

Answer (click to unblur):

1
2
3
4
5
6
7
export function moveParticle(row, col, newRow, newCol, swap) {
    // Rest of moveParticle

    grid[newRow][newCol] = grid[row][col];
    grid[row][col] = null; // This line is the fix!
    return true;
}

Now, the errors when the sand hits the bottom and the streaking should be gone!

Sand physics

Let’s make our sand behave a bit more realistically. Currently, it just falls straight down. Lets add a check to make sure it doesn’t overwrite other sand if there is already sand below it.

TASK 3: Utilizing getParticle (which returns the particle at (row, col)), add a check in the moveParticle function in canvas.js to make sure a particle cannot move on top of another particle.

Remember if a gird location is empty it will contain the value null.

Answer (click to unblur):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function moveParticle(row, col, newRow, newCol, swap) {
    if (!checkBounds(row, col) || !checkBounds(newRow, newCol)) {
        return false;
    }

    // 👇 Add this check 👇
    if (getParticle(newRow, newCol)) {
        return false;
    }
    // 👆 Add this check 👆

    grid[newRow][newCol] = grid[row][col];
    grid[row][col] = null;
    return true;
}

You might have noticed that we are returning true and false from the moveParticle function. This is to indicate wether a particle was moved or not, lets add some code in particles.js that moves the particle left if there is something below it.

Open the particles.js file and modify the update function in the Sand class. We’ll use the return value of moveParticle to determine if the downward move was successful. If it was unable to move down, we’ll try to move left.

1
2
3
4
5
6
7
update(row, col) {
    // Try to move down
    if (!moveParticle(row, col, row+1, col, this.swap)) {
        // If cannot move down, try to move left
        moveParticle(row, col, row, col-1, this.swap);
    }
}

The sand looks better then before, but now once sand reaches the bottom it moves to the left until it hits the border of the canvas. Lets make sure that sand stops moving.

1
2
3
4
5
6
7
8
9
update(row, col) {
    // Fall due to gravity
    let newRow = row + 1;

    // If nothing below move down
    if (!moveParticle(row, col, newRow, col, this.swap)) {
        moveParticle(row, col, newRow, col-1, this.swap);
    }
}

This works because when we try to move sand to the left, we are also trying to move it down, this results in the sand attempting to move below the screen which gets prevented by our bounds check. This also creates a satisfying pyramid shape.

Now lets add another check to have it move right if it can’t move left.

1
2
3
4
5
6
7
8
9
10
11
12
update(row, col) {
    // Fall due to gravity
    let newRow = row + 1;

    // If nothing below move down
    if (!moveParticle(row, col, newRow, col)) {
        // Try to move left
        if (!moveParticle(row, col, newRow, col-1, this.swap)) {
            moveParticle(row, col, newRow, col+1, this.swap)
        }
    }
}

CHALLENGE: Mess around with the sand physics! What happens if you have the sand move two steps every update (row+2 or col+2), or if you try to move left and right first?

Completion & Discussion Checklist

Before joining the group discussion or moving on to the next module, ensure you have completed the tasks, investigated the bugs, and are ready to discuss the questions below:

Click to expand Completion & Discussion Checklist (7 Items)
#TypeItemPrompt Preview
1Bug HuntEmpty Grid Null CheckAccessing particle.color on an empty grid cell causes a null error. Verify that a particle exists at (row, col) before reading its properties.
2QuestionGrid Bounds & CoordinatesThink about the dimensions of our grid. How can you check if a given row is within the valid range of rows? What about the column?
3QuestionParticle Reference vs. CopyWhat do we use to represent an empty particle? Are we moving the particle or just making a new one?
4TaskImplement checkBounds()Modify the checkBounds function so it returns true if (row, col) is within the bounds of grid and false otherwise. Use it in moveParticle to prevent particles from moving out of bounds.
5TaskFix Particle StreakingModify the moveParticle function in canvas.js to stop the particles from streaking as they fall.
6TaskPrevent Particle OverwriteUtilizing getParticle, add a check in moveParticle in canvas.js to make sure a particle cannot move on top of another particle.
7ChallengeCustom Physics ExperimentationMess around with the sand physics! What happens if you have the sand move two steps every update (row+2 or col+2), or if you try to move left and right first?

Congratulations! You’ve completed the first part of the Falling Sand tutorial. You can now create and make sand particles fall and react to simple physics. In the next part, we’ll introduce more particle types and make them interact with each other.