Project Magic School Bus

Gloria Julien
23 min readMay 30, 2019

--

Prototyping Interactions I (SDEG-600)

This content is part of a collection of works completed during my studies as a UX/IxD Master’s student at Thomas Jefferson University.

Here, I have documented my progress while taking the Prototyping Interactions I course during Summer 2019. It’s part learning how to code in javascript using the p5.js API and part learning designing for real-world interactions.

Follow me on LinkedIn.

Let’s make the Magic School Bus shoot smelly objects!

Goals

Objective:

Recreate a scene from a children’s book. Display a statement listing the design goals (e.g. subject, intent, aesthetics, interaction…) and how to achieve them (e.g. function, narrative, process, technique).

Methods:

Apply randomness. Use loops and arrays. Use a logical statement.

Steps:

  1. Decide on project functionality.
  2. Setup the drawing.
  3. Create the Magic School Bus.
  4. Add the laser beam.
  5. Create a laser beam when mouse is clicked.
  6. Create smells. // Checkpoint A //
  7. Create an interaction between the laser beam and smells.
  8. Refine the interaction of “hits”.
  9. Remove the laser beam when it hits a smell.
  10. Count number of hits, display it after a certain amount of time.
  11. Change the ellipses to smell images.
  12. Fix the laser beam removal.
  13. Add project information and Improvements list.
  14. Final code and some changes made.

Step 1. Decide on project functionality.

Some of my favorite children’s books when I was younger included: Elmer, The Magic School Bus, and Officer Buckle and Gloria.

Books from my childhood

While I’d love to into why I loved each of these books, this is not a book review. So, I settled on the Magic School Bus story where the class learned about smell in the story called, “The Magic School Bus Makes A Stink” (click for the episode video, active as of 5/25/2019).

The part of the story which I’m looking to create occurs towards the end of the class’s journey. At the coveted Smell Search competition, each classroom participated by presenting their best smells to Flora, the judge. Ms. Frizzle’s smell sample unfortunately gets sabotaged with Janet adding skunk to the mix. Therefore, the class and the Magic School Bus shrink down to molecular size, zapping away all the bad smells in the batch before it reaches Flora’s nose, winning them the Smell Search competition.

My goal is to recreate this scene, with a shrunken down Magic School Bus, shooting a laser beam at the stinky smells it sees, before it reaches Flora’s nose. The user will be able to control the position of the school bus using their mouse and when the user clicks, a laser beam shoots out towards the smells. If one of the beams hits the smell object, the smell disappears.

The user points start at 0. If a good smell is shot, a point is subtracted. If a bad smell is shot, a point is added. The goal is to shoot only all the bad smells.

Let’s see how I did.

Step 2. Setup the drawing.

I wanted to use a full canvas for this project, so I set up the drawing as such:

function setup() {
createCanvas(windowWidth,windowHeight);
}

Step 3. Create the Magic School Bus.

To create a version of the Magic School Bus, I started with a plain rectangle that you could move around the screen. Then, I wanted to make sure the bus could not overstep a certain threshold (only the first 1/4 of my canvas in the x-direction), so I controlled its position with the constrain( ) function. Here’s my code:

function setup() {
createCanvas(windowWidth,windowHeight);
}
function draw() {
background(244, 248, 252);
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX,mouseY,120,70,5);
}

Output:

Step 3. Create the Magic School Bus.

Step 4. Add the laser beam.

The laser beam was somewhat tricky for a student at my level, so I searched the web for some help and stumbled upon a tutorial for making Space Invaders in p5.js: https://www.youtube.com/watch?v=biN3v3ef-Y0. (Thank you for making this youtube@thecodingtrain!)

Some useful techniques I utilized from the tutorial included setting my objects into different functions and calling upon them using the existing show( ) and user-created move( ) methods. At the same time, I learned about the new operator (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new). In addition, I created the variable schoolbus to store the information of my new school bus. Then, I created my schoolbus in the setup( ) function. And finally, making sure my schoolbus shows up, I called the show( ) method in the draw( ) function. The keyword, this, is used to make sure it is referring to the object that is currently executing the code. https://codeburst.io/all-about-this-and-new-keywords-in-javascript-38039f71780c

Output 1: (after cleaning up code)

var schoolbus;function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
}
function draw() {
background(244, 248, 252);
schoolbus.show();
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX,mouseY,120,70,5);
}
}

To add the laser beam, I had to think how I wanted it to look. In the end, I wanted a line of a certain length to run across the screen from the left to right. Each time the mouse was clicked, a new beam would show up and add to the picture (we’ll get to this click action in Step 5). For now, let’s create one laser beam on our canvas.

So, I started by creating a function dedicated to the laser beams. Giving them a black color and adding variables where the position of the beam would change as the draw() continuously executes. This meant replacing the x-coordinate of my beam rectangle with a value altering as the screen redraws. I named this “beamX”. In addition, I made my y-coordinate change as well, since any new beam would be drawn where the school bus would be currently located. I mapped this to mouseY+35.

To make the laser beam move across the screen, I added an increment to the function, beamX +=10. To test this code, I’ve set the starting x-position of our beam to 0 and the y-position will follow the mouse position with mouseY.

var schoolbus;
var beams;
var beamX=0;
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
beams = new beam(beamX,mouseY+35);
frameRate(10)
//test move slowly to check each action
}
function draw() {
background(244, 248, 252);
schoolbus.show();
beams.show();
beams.move();

}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX,mouseY,120,70,5);
}
}
function beam(x,y) {
this.show = function(){
fill(0);
rect(beamX,mouseY+35,50,5);
}
this.move = function(){
beamX += 10;
}
}

Output 2:

Step 4. Add the laser beam.

It’s getting there. There’s a few problems to fix including: making sure the laser beam doesn’t change positions along with the mouse movement and changing the laser beam starting position to be the nose of our schools (which I’ll designate as the right-most edge, vertically-centered).

Step 5. Create a laser beam when mouse is clicked.

In addition to fixing the problems from Step 4, I also want to create laser beams only when the mouse is clicked. To do that, I need to use an array to store every existing laser beam as they shoot out including all new ones that are made. Each laser beam will move linearly only at the position the mouse was clicked at, relative to where our schoolbus was.

I’ll start with my loop. I make sure that the loop runs only until the max length of the beam array, let’s call this lightbeams = [];. I created a for loop to achieve this, using the indices to iterate through the array and using the show( ) and move( ) methods again.

Then, I want to make javascript listen to when I click the mouse using the mouseClicked( ) function. Once the mouse is clicked, I want to create a new beam and store this value into the array, “lightbeams”, I created earlier. To do so, I use the push( ) method to simply add on the newest beam data. If you view the value of lightbeams after several clicks (console.log(lightbeams)), each object in the array will be a different input value for beam (e.g. lightbeams=[beam(100,200), beam(100,250)] ).

I also had to change my beam( ) function to ensure that the x and y positions start when the mouse is clicked. Using this.x and this.y to point at the executing function, I was able to achieve this. This way I pass in only the values of the mouse click event into the beam( ) function. Also, to prevent any laser beams from forming outside of my schoolbus, I used the same constrain( ) function to control where the laser beams could start. This region equals to the same region the schoolbus could occupy.

I removed the variables beams and beamX. “beams” was no longer useful since I wasn’t creating a new beam at the start of the canvas drawing in setup(). “beamX” wasn’t useful anymore as I need to set the starting position of my beams to whereever my schoolbus was.

var schoolbus;
var lightbeams = [];
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
}
function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
}

}
function mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots+120,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.move = function(){
this.x += 10;
}
}

Output:

Step 5. Create a laser beam when mouse is clicked.

Step 6. Create smells.

Notable smells mentioned in the Magic School Bus episode include: gym socks, cinnamon, mint, banana, rose, rotten eggs, and skunk. I’ll add these in as image files and place them randomly across my screen.

To check that my idea of creating scattered objects on the screen would work, I used ellipses to start.

There’s a few things I know I have to do. The first which is to only draw the ellipses once. To draw 20 ellipses randomly on the screen, without redrawing it, this means I need to enter this loop in the setup() function. I created a blank array, smellItems, to hold each of the new ellipses that were drawn. This loop recreates the array smellItems until it reaches a length of 20, the same count as the loop.

Next, I know that I need to show the same number of items made in the setup() function, but this time in the draw() function. As opposed to making the loop directly in the draw function, the setup function allows me to draw it once then access this drawing through the show( ) function I will make in the new smell( ) function dedicated for making new smell objects. I access each of these ellipses from the setup() function by using show( ) in the draw() function.

Finally, to create each of my ellipses, I’ll create a new function called smell( ). I will store my x and y values created during the setup() function into this.x and this.y, so I may use the same values as coordinates to my ellipses. Since there is a loop going through and a random number generator, I will be receiving new x and y values each time the loop iterates.

I made some minor adjustments to the position of my bus and where the new ellipses can be drawn, so that they never overlap.

var schoolbus;
var lightbeams = [];
var smellItems = [];
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<20;i++){
smellItems[i]=new smell(random(width/4,width),random(height))
}

}
function draw() {
background(244, 248, 252);
schoolbus.show();
for(var i=0;i<20;i++){
smellItems[i].show();
}

for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
}
}
function mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.move = function(){
this.x += 10;
}
}
function smell(x,y){
this.x = x;
this.y = y;
this.show = function() {
fill(0)
ellipse(this.x+120,this.y,35,35)
}

}

Output 1:

Step 6. Create smells. (1. scattered smells)

What if I defined the project structure differently so that instead of spitting out a bunch of random dots on the screen, I instead create new dots along the x-most edge (“width”), then have these move towards the schoolbus? Then, a new column of dots form on the screen once the first column has moved to a certain distance away from the furthest edge?

Let’s give this a shot, too.

To create this, I need to add a move( ) function to my smell( ) function. This will allow the ellipses (my “smells”) to move across the screen as they float by the schoolbus.

In the setup() function, my loop for smellItems ends when the index reach 5. This same number 5 indicates how many smells I will be creating. When I create the smells, I want to make them start at the right-most edge of my screen (“width”), but not too far off that the ellipse’ center falls off the screen. So, I adjusted the value accordingly.

Below, in the draw() function, I will make my index go until the length of the array “smellItems” has been reached. In addition, I need to make sure to activate the move( ) function while I’m in the draw() function.

var schoolbus;
var lightbeams = [];
var smellItems = [];
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<5;i++){
smellItems[i]=new smell(random(width-50,width-25), random(height))
}
}
function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
}
for (var i=0;i<smellItems.length;i++){
smellItems[i].show();
smellItems[i].move();
}
}
function mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.move = function(){
this.x += 10;
}
}
function smell(x,y){
this.x = x;
this.y = y;
this.show = function() {
fill(0)
ellipse(this.x,this.y,35,35)
}
this.move = function() {
this.x-=5;
}

}

Output 2:

Step 6. Create smells. (2. random column of smells)

I like this better! Now, I have moving objects towards my Magic School Bus. Next, let’s add a few more columns as the smell objects pass through the screen.

Checkpoint A. Verify my objects values “x” and “y”

Verifying the values of my objects will help me later on when I will evaluate the intersection of my laser beam with the smells. Here, I am documenting my understanding of how the code is run and what information it is gathering.

When the first laser beam and smell is made, they start at a given value denoted by the lines written with “new” then the function, either “beam” or “smell”:

for(var i=0;i<5;i++){
smellItems[i]=new smell(random(width-50,width-25), random(height)) // this line
}
// AND FOR LASER BEAMSfunction mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35) // this line
lightbeams.push(beamshot);
}

For each function, I also created a move( ) function, which allows me to translate the object left or right. Each new movement (it’s new input “x” and “y” values) is stored into the array (“smellItems” or “lightbeams”). So, if I call a position in the array and its “x” value, then I should obtain the referring number associated.

To test this, I prevented movement of my objects and sought out the values of my objects to verify my thinking.

var schoolbus;
var lightbeams = [];
var smellItems = [];
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<5;i++){
smellItems[i]=new smell(random(width-50,width-25), random(height))
}

}
function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
// lightbeams[i].move();
}
for (var i=0;i<smellItems.length;i++){
smellItems[i].show();
// smellItems[i].move();
}
}function mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.move = function(){
this.x += 10;
}
}
function smell(x,y){
this.x = x;
this.y = y;
this.show = function() {
fill(0)
ellipse(this.x,this.y,35,35)
}
this.move = function() {
this.x-=2;
}
}

Output:

Checkpoint A. Verify my objects values “x” and “y”

Console:

Checkpoint A. Console log for checking “x” and “y”

The “x” and “y” values called in the console log are showing either the smell() function or beam() function’s “x” and “y” input. Alternatively, I could have named my inputs to the smell() and beam() functions something easier to follow such as: beam(beamXpos,beamYpos) and smell(smellXpos,smellYpos).

Step 7. Create an interaction between the laser beam and smells.

First, let’s check to see that our code will reply when our laser beam hits the smellItem. To do this, I’ll create a new function within beam( ) to handle the interaction. In layman’s terms, I will be checking to see that the distance between my laser beam and the smellItem is less than distance between each of their x-coordinates if the items touched.

Here’s a visual of the math:

My new function hits( ) will handle the input: each item in the array of smellItems. In my main code, within the draw( ) function, I want to compare the x and y values from each position of my arrays (lightbeams and smellItems) to see what the distance between the coordinate are of my laser beam and my smell. I’ll measure this distance using the dist( ) function. Because I want to compare the each lightbeam created with each new smellItems, I need a nested loop to run through my lightbeams array, then a loop to run through my smellItems array.

This will satisfy the check system. If I’ve done this right, then for now, the console will log “I’m hit!” when the laser beam hits the smellItem.

var schoolbus;
var lightbeams = [];
var smellItems = [];
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<5;i++){
smellItems[i]=new smell(random(width-50,width-25),random(height))
}
}
function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
for (var j=0;j<smellItems.length;j++){
if(lightbeams[i].hits(smellItems[j])){
console.log("i'm hit!")
}
}

}
for (var i=0;i<smellItems.length;i++){
smellItems[i].show();
// smellItems[i].move();
}
}function mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.hits = function(smellcheck){
var d = dist(this.x,this.y,smellcheck.x,smellcheck.y)
if (d < (50+20)){
//rect width plus radius of ellipse
return true;
} else{
return false;
}
}
this.move = function(){
this.x += 10;
}
}function smell(x,y){
this.x = x;
this.y = y;
this.show = function() {
fill(0)
ellipse(this.x,this.y,40,40) // use 20 for distance from x
}
this.move = function() {
this.x-=2;
}
}

Output:

Step 7. Create an interaction between the laser beam and smells.

For now, I’ve prevented the smells from moving just so we can check the interactions. Looks like it’s working!

However, the number of “hits” occurring is more than one. Mathematically, this is correct because as the laser beam moves past the smell object, the distance (d) between each of their “x” and “y”coordinates remains less than my condition (50+20).

Step 8. Refine the interaction of “hits”.

Next, I wanted to remove the smell from my screen if “hits” is true. To do this, I simply change my line from a console.log function to a remove function. In this case, I will use the splice( ) function. The splice( ) function allows me to pinpoint the portion of my smellItems array and removed the currently accessed item to be removed from the array.

Here’s the additional code added to my draw( ) function:

function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
for (var j=0;j<smellItems.length;j++){
if(lightbeams[i].hits(smellItems[j])){
console.log("i'm hit!")
var removedSmells = smellItems.splice(j,1) // added code
}
}
}

In this output, I’ve also modified my smellItems to show up in greater numbers, but reveal themselves once they scroll past right edge of the screen. To do that, I modified the range in which a smell could be created in my setup( ) function.

function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<20;i++){ // increased number of smells
smellItems[i]=new smell(random(width-50, width*2), floor(random(0,height)))
// modified smell forming positions
}
}

Output:

Step 8. Refine the interaction of “hits”.

Looks good! A few things I could include are:

  1. Removing the laser beam once it hits the smell object.
  2. Identifying if a smell hits the schoolbus, send up an alert.

Step 9. Remove the laser beam when it hits a smell.

To do this next part, I thought I could easily add in a splice( ) function in addition to my smell removal. But, this was my result:

if(lightbeams[i].hits(smellItems[j])){
console.log("i'm hit!")
var removedSmells = smellItems.splice(j,1)
var removedBeams = lightbeams.splice(i,1) // added this
}

Output 1:

Step 9. The code breaks if I add in the splice( ) function for my lightbeams array.

As you can see, the code breaks when I added in my splice( ) function for the lightbeams array. The error thrown is found at the start of my comparison loop. When I removed the lightbeam item from my array, with only an array length of 1, I also removed the loop’s capability to compare since there is no longer any length to the array “lightbeams” (I removed it all). So, the removal of my lightbeam needs to be done differently. In other words, I need to decrement in my for loop, not increment.

Let’s try this again.

First, I set the function for removing the lightbeams to exist outside of our comparison loop for the lightbeam and smells. I called this removeBeam( ). When the lightbeam hits a smell, it activates the removeBeam( ) function for the current index location. This then goes into the beam( ) function and activates removeBeam( ). All it does is change the condition to delete (to then run through deleting loop) to “true”. This is called toDelete( ) method. Since I don’t want to run the toDelete( ) method all the time, I set toDelete( ) to start as “false” in the beam( ) function.

var schoolbus;
var lightbeams = [];
var smellItems = [];
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<20;i++){
smellItems[i]=new smell(random(width-50, width*2), floor(random(0,height))) // modified to draw in other locations
}
}
function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
for (var j=0;j<smellItems.length;j++){
if(lightbeams[i].hits(smellItems[j])){
console.log("i'm hit!")
var removedSmells = smellItems.splice(j,1)
lightbeams[i].removeBeam(); // activate the removeBeam function within "beam"; turns toDelete to true; added this code
}
}
}
for (var i=0;i<smellItems.length;i++){
smellItems[i].show();
smellItems[i].move();
}
for (var b=lightbeams.length-1;b>=0;b--){ // remove the lightbeam from the array by decrementing
if(lightbeams[b].toDelete){
// when toDelete is true, then run the code below; added this line of code
lightbeams.splice(b,1);
}

}
} //end draw() functionfunction mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.toDelete = false; // added this line of code
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.hits = function(smellcheck){
var d = dist(this.x,this.y,smellcheck.x,smellcheck.y)
if (d < (50+20)){
return true;
} else{
return false;
}
}
this.removeBeam = function(){ // added this line of code
this.toDelete = true;
}
this.move = function(){
this.x += 10;
}
}function smell(x,y){
this.x = x;
this.y = y;
this.show = function() {
fill(0)
ellipse(this.x,this.y,40,40)
}
this.move = function() {
this.x-=5;
}
}

Output:

Step 9. Remove the laser beam when it hits a smell.

Step 10. Count number of hits, display it after a certain amount of time.

Now, I want to track the number of times my laser beam destroys a smell. I’ll store this count into countSmells.

Next, I’ll need to figure out where to add my counts. Since the number of lightbeams destroyed is the same as my count, I’ll add my count there, under the beam( ) function and within the .removeBeam method. This way, I’ll also only access the count when a smell is destroyed. I don’t need to create any new code to check when hits is true.

var schoolbus;
var lightbeams = [];
var smellItems = [];
var countSmells = 0;
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<20;i++){
smellItems[i]=new smell(random(width-50,width*2), floor(random(0,height)))
}
}function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
for (var j=0;j<smellItems.length;j++){
if(lightbeams[i].hits(smellItems[j])){
console.log("i'm hit!")
var removedSmells = smellItems.splice(j,1);
lightbeams[i].removeBeam();
}
}
}
for (var i=0;i<smellItems.length;i++){
smellItems[i].show();
smellItems[i].move();
}
for (var b=lightbeams.length-1;b>=0;b--){
if(lightbeams[b].toDelete){
lightbeams.splice(b,1);
}
}
var time = millis();
text("Time elapsed "+time+"ms",5,40)
if (time>13500){
alert("Congratulations! You removed "+countSmells+" bad smells!")
noLoop()
}

} //end draw() function
function mouseClicked(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,120,70,5);
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.toDelete = false;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.hits = function(smellcheck){
var d = dist(this.x,this.y,smellcheck.x,smellcheck.y)
if (d < (50+20)){
return true;
} else{
return false;
}
}
this.removeBeam = function(){
this.toDelete = true;
countSmells += 1;
}
this.move = function(){
this.x += 10;
}
}function smell(x,y){
this.x = x;
this.y = y;
this.show = function() {
fill(0)
ellipse(this.x,this.y,40,40)
}
this.move = function() {
this.x-=5;
}
}

Output:

Step 10. Count number of hits, display it after a certain amount of time.
Step 10. Time elasped to 13500ms and countSmells total removed.

Step 11. Change the ellipses to smell images.

See my article “Code Test: Removing Images” for how I learned about image arrays.

Now that everything is moving the way I wanted it to, I need to swap out my ellipses to images. To do that I have to setup my images so they all have the same filename, different number, and sequential (smell0.png, smell1png, …, smell6.png). Then, I need to load these images into my code through the preload( ) function. After that, I need to change my smell( ) function to handle the image in the way I want it to. Finally, I’ll need to check that my call to create new smells in the setup( ) function will match the new code.

With some trial and error, I added or changed these lines of code:

var smellImgs = [];  // added an empty array to handle imagesfunction preload() {
for (var i=0;i<7;i++){
smellImgs[i] = loadImage("smell"+i+".png");
// loads images
}

}
function setup() { // changed arguments to smell()
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<20;i++){
smellItems[i]=new smell(floor(random(0,height)))
}
}
// modifications to my smell() function
function smell(y){
this.y = y; //random height to place the image
this.x = floor(random(0,6));
// random integer to cycle through images
this.distX = random(width/2,width*2);
// random width to place the images
this.show = function() {
image(smellImgs[this.x], this.distX, this.y, smellImgs[this.x].width/2, smellImgs[this.x].height/2)
}
this.move = function() {
this.distX-=5;
}
}

Output:

Step 11. Change the ellipses to smell images.

Great! Changing the ellipses to smell images has worked, except now, the laser beam no longer interacts with the images and removes the laser beam. Let’s fix that.

Step 12. Fix the laser beam removal.

The first thing I will do is check the formula used for comparing the distances between my smell object and the laser beam.

// modified code for the for loop "new smell()" linefor(var i=0;i<20;i++){
smellItems[i]=new smell(floor(random(width/2,width*2)),floor(random(0,height)),floor(random(0,6)))
}

// modified .hits within beam()
this.hits = function(smellcheck){
var d = dist(this.x,this.y,smellcheck.distX,smellcheck.distY)
if (d < (100)){ //rect width
return true;
} else{
return false;
}
}

// modified smell() function
function smell(distX, distY, int){
this.distX = distX; // random width to place the images
this.distY = distY; //random height to place the image
this.int = int; // random integer to cycle through images
this.show = function() {
image(smellImgs[this.int],this.distX,this.distY,smellImgs[this.int].width/3,smellImgs[this.int].height/3)
}
this.move = function() {
this.distX-=5;
}
}

Output:

Step 12. Fix the laser beam removal.

Step 13. Add project details and Improvements list.

With the lines of code below, I added my project details and improvements.

// added right before closing draw()
// here goes the text!
fill(0)
rect(width-width/5,0,width/5,height)
textSize(height*.015)
textAlign(LEFT)
fill(255)
text("Project Goals:",width-width/5*.95,50)
text("Create the Magic School Bus.\nCreate incoming 'smells' to shoot away as the \nmouse is clicked.\nCount the number of smells removed.\nThe aesthetics will be simple.",width-width/5*.95,70)
text("Method:",width-width/5*.95,170)
text("Use loops, arrays, preload, functions, and \nmouse clicks.",width-width/5*.95,190)
text("Remaining Tasks:",width-width/5*.95,250)
text("Only add points when bad smells removed.\nIndicate which are bad smells.\nDisplay elapsed time to remove smells.\nStop when schoolbus is hit by a smell.",width-width/5*.95,270)
text("Created by: Gloria Julien, © 2019",width-width/5*.95,height-100)

Output:

Step 13. Add project details and Improvements list.

Step 14. Final code and some changes made.

Some changes made:

Change mouseClicked() to keyPressed() to allow hitting keys (I used a spacebar.) That’s it.. so far. I could go on for awhile playing in this code.

My p5.js code:

Remember to link the p5.js library in your index.html file! Or just download the complete library from https://p5js.org/download/.

var schoolbus;
var lightbeams = [];
var smellItems = [];
var countSmells = 0;
var smellImgs = [];
function preload() {
for (var i=0;i<7;i++){
smellImgs[i] = loadImage("smell"+i+".png");
}
}
function setup() {
createCanvas(windowWidth,windowHeight);
schoolbus = new bus();
for(var i=0;i<20;i++){
smellItems[i]=new smell(floor(random(width/2,width*2)),floor(random(0,height-100)),floor(random(0,6)))
}
}
function draw() {
background(244, 248, 252);
schoolbus.show();
for (var i=0;i<lightbeams.length;i++){
lightbeams[i].show();
lightbeams[i].move();
for (var j=0;j<smellItems.length;j++){
if(lightbeams[i].hits(smellItems[j])){
console.log("i'm hit!")
var removedSmells = smellItems.splice(j,1);
lightbeams[i].removeBeam(); // activate the removeBeam function within "beam"; turns toDelete to true
}
}
}
for (var i=0;i<smellItems.length;i++){
smellItems[i].show();
smellItems[i].move();
}
for (var b=lightbeams.length-1;b>=0;b--){ // remove the lightbeam from the array
if(lightbeams[b].toDelete){ // when toDelete is true, then run the code below
lightbeams.splice(b,1);
}
}
var time = millis();
fill(0)
textSize(height*.01)
textAlign(LEFT)
text("Time elapsed "+time+"ms",5,40)
if (time>10000){
alert("Congratulations! You removed "+countSmells+" bad smells!")
noLoop()
}
// here goes the text!
fill(0)
rect(width-width/5,0,width/5,height)
textSize(height*.015)
textAlign(LEFT)
fill(255)
text("Project Goals:",width-width/5*.95,50)
text("Create the Magic School Bus.\nCreate incoming 'smells' to shoot away as the \nmouse is clicked.\nCount the number of smells removed.\nThe aesthetics will be simple.",width-width/5*.95,70)
text("Method:",width-width/5*.95,170)
text("Use loops, arrays, preload, functions, and \nmouse clicks.",width-width/5*.95,190)
text("Remaining Tasks:",width-width/5*.95,250)
text("Only add points when bad smells removed.\nIndicate which are bad smells.\nDisplay elapsed time to remove smells.\nRemove lives when the schoolbus is hit by a smell.",width-width/5*.95,270)
text("Created by: Gloria Julien, © 2019",width-width/5*.95,height-100)
} //end draw() functionfunction keyPressed(){
let allshots = constrain(mouseX,120,width/4)
var beamshot = new beam(allshots,mouseY+35)
lightbeams.push(beamshot);
}
// function mouseClicked(){
// let allshots = constrain(mouseX,120,width/4)
// var beamshot = new beam(allshots,mouseY+35)
// lightbeams.push(beamshot);
// }
function bus() {
this.show = function() {
let mcX = constrain(mouseX,0,width/4)
fill(255, 195, 0)
noStroke()
rect(mcX-120,mouseY,100,60,5); // rect.width = 100; rect.height=60
}
}
function beam(x,y) {
this.x = x;
this.y = y;
this.toDelete = false;
this.show = function(){
fill(0);
rect(this.x,this.y,50,5);
}
this.hits = function(smellcheck){ //hits is going to be a function of the items it is interacting with; the input to the function are the smellItems because it needs to check the interaction with the specific smellItem
var d = dist(this.x,this.y,smellcheck.distX,smellcheck.distY)
if (d < (100)){ //rect width
return true;
} else{
return false;
}
}
this.removeBeam = function(){
this.toDelete = true;
countSmells += 1;
}
this.move = function(){
this.x += 10;
}
}// next upload images to the file and scatter them across the page, between width/4 -> widthfunction smell(distX, distY, int){
this.distX = distX; // random width to place the images
this.distY = distY; //random height to place the image
this.int = int; // random integer to cycle through images
this.show = function() {
image(smellImgs[this.int],this.distX,this.distY,smellImgs[this.int].width/3,smellImgs[this.int].height/3)
}
this.move = function() {
this.distX-=5;
}
}

Lessons Learned:

The first thing I learned when I shared this with my professor was that I was unknowingly attacking object oriented programming (OOP). Very cool stuff there. It’s enough to get me itching to learn how to do more of it.

I learned how to utilize p5.js in new ways other than static drawings, like making shapes on the pages.

I learned how to use arrays to carry my data and later on, how to modify those arrays to make a new type of interaction on the screen.

Placing your code within the right functions (such as, preload(), setup(), or draw() ) was tricky for me only because I’m still fresh to p5.js and working on understanding how it works. At the very least, draw() redraws the canvas every time, unless you have a stopping function, like noLoop().

To help myself debug, I ran the frameRate() function and slowed down the processing such that I could see how my objects were refreshing. At the normal frame rate, sometimes I could not see what was happening in the background, so slowing it down truly helped my debug.

I got to use a time function! millis() came in handy to count the time passed since I first loaded the page.

New Methods Learned:

splice(), mouseClicked(), image(), noLoop(), millis(), frameRate()

More Opportunities:

  1. Provide directions on which smells are to be targeted.
  2. Display time spent shooting smells.
  3. Create a point system: count if a bad smell is destroyed.
  4. Apply negative points when the school bus is hit by a bad smell.
  5. Build levels.

Thanks for reading! Please leave a comment that you think might help me improve my skills. Maybe something new I could try. I’d love to give it a shot.

Follow me on LinkedIn.

--

--

Gloria Julien

I think a lot about people’s perceptions about life. Oh, and I’m a UX-er, who’s also turning into a Career Coach.