Lab 5: Reactors/Animations

For this lab, we suggest you have a shared Google Doc between you and your partner for written answers, and a Pyret file for your code.

Learning Objective

The objective of this lab is to gain familiarity with creating animations, specifically with one of Pyret's built in tools, the reactor.

We will practice breaking down the problem of creating an animation step by step using the skills that we've developed throughtout the semester so far. After that, we'll be able to see how Pyret can help us automate some of the process of creating an animation.

If you don't feel more comfortable with the concept of a reactor after working on this lab, come to TA hours! The content of this lab will be important for Project 2, and your TAs would love to help in any way they can.

Problem 1 โ€“ Cloudy with a Chance of Cupcakes

In this lab, we're going to animate this gif of a cupcake falling onto a plate in Pyret โ€“ perhaps you could imagine that you're an animation programmer at Sony Pictures Animation working on the critically acclaimed film, Cloudy with a Chance of Meatballs.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

Take a few minutes to understand this gif:

  1. What elements do you see on the screen?
  2. What is moving? What is not moving?
  3. How do you think the cupcake is moving?
  4. Why/how does the cupcake reappear at the top after it lands on the plate?
  5. How does the program know when the cupcake is on the plate?

Write down your answers to these questions in your Google Doc.

Problem 1.1 โ€“ Frame by Frame

Let's look at that gif again, but this time, we'll look at some sequential frames individually.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

We can see that our fixed pieces โ€“ the white background, the plate at the bottom โ€“ look the same in each frame. However, our moving part โ€“ the falling cupcake โ€“ is at a different vertical position in each frame.

If we gave each of the elements in a given frame a coordinate value, then we could draw the image for that frame. If we could create a whole collection of images, and get Pyret to flip through them quickly, we'd get an animation!

Problem 1.2 โ€“ Your First Frame

The first thing you need to get started is the white background. That's just a rectangle (using the image library).

Now we need the plate and cupcake. Pyret has a useful function image-url for loading images when you donโ€™t want to build an entire graphic from scratch (URL stands for "Universal Resource Locator"โ€ฆ a web address!). image-url takes in the url of a picture from the web and outputs it as a Pyret Image. Once itโ€™s loaded, you can manipulate it the way you would any other Image from the image library.

We used these images:

include image

PLATE-URL = "https://imgur.com/wneOB5v.png"
DESSERT-URL = "https://imgur.com/rLw09co.png"

dessert = image-url(DESSERT-URL)
plate = image-url(PLATE-URL)

HEIGHT = 500
WIDTH = 750
background = rectangle(WIDTH, HEIGHT, "solid", "white")

Youโ€™re welcome to find (or build!) your own, or just copy our code to get started.

Task: Write an expression to create the first frame/image in the animation, where the plate is fixed at the bottom-middle of the frame, and the cupcake is currently at the top-middle of the frame.

Hint: You can use the scale function to make your dessert/plate larger or smaller.
Hint: You might find the function place-image more useful than overlay-xy, as it allows you to fix one image (e.g., the background) and put another (e.g., the plate) on top of it using a coordinate. You can nest calls to place-image, just as you have before with overlay-xy.
Note: The origin of the background grid (the point at (0,0)) is at the top left corner of the image. Therefore, increasing 'y' values is equivalent to moving down on the image.


CHECKPOINT:

Call a TA over once you've made your first frame.


Problem 1.3 โ€“ Another One

Task: Now that you've got your first frame, write an expression to create the next frame/image in the animation, where the plate is fixed in the same place, and the cupcake is currently at the same x value but its y value has increased. Just use concrete numbers for your new coordinates (don't try to build them off the previous expression).

Problem 1.4 โ€“ And Another One

Task: Now that you've got two frames, write an expression to create the next frame/image in the animation, where the plate is fixed at in the same place, and the cupcake is currently at the same x value but its y value has increased yet again.

Problem 1.5 โ€“ Reducing Repetition

You could make a lot of images this way. Then you'd just need to get Pyret to flip through them.

But creating these images is repetitive โ€“ do you really have to copy and paste all the time? What do we usually do when we find we are repeating the same code multiple times?

Task: Stop and discuss your response with your partner.

Task: Create a function called falling-dessert that takes two Number inputs (for x and y) and produces the frame image with the cupcake at those coordinates.

Now, we can create an entire sequence of images more quickly, for example by writing:

falling-dessert(375, 0)
falling-dessert(375, 20)
falling-dessert(375, 40)
falling-dessert(375, 60)
falling-dessert(375, 80)
...

Problem 2 โ€“ Connecting the Images

Creating falling-dessert saved us work in creating individual images, but there's still work to do to generate enough images for an entire animation. Wouldn't it be nice to automate that as well?

Task: Look at the sequence of falling-dessert calls just above. Do you see any sort of pattern across them that we might be able to use to automate the generation of successive frames? Discuss with your partner.

In our sequence, the y coordinate is increasing by 20 from one image to the next. The same computation (increase by 20) over and over? That also sounds like a function! For example:

fun update-coord(y :: Number) -> Number:
  doc: "generate y coordinate for next image"
  y + 20
end

This is the right idea, but we need to generalize it a bit to be able to combine update-coord and falling-dessert to generate our full sequence of images.

As a first step, we have to have falling-dessert and update-coord work with Coordinates rather than individual numbers (we can explain why later)

Setup

Make sure you include this at the top of your program:

include reactors
include image

data Coord:
    | coord(x :: Number, y :: Number)
end

Coord here is a special datatype that turns two numbers into a single coordinate. For now, just rely on your intuition that a coordinate is made up of two numbers (x and y). Don't worry about what the rest of the code here does.

To create a new Coord named my-coord with an x value of 3 and a y value of 4:
my-coord = coord(3, 4)

To access the x and y values of my-coord:

>> my-coord.x
3
>> my-coord.y
4

Task: Edit falling-dessert so that its input is now a single Coord (rather than two numbers). In the body of the function, extract x and y from the Coord input.

Task: Re-create the sequence of images that we showed earlier, this time using Coord instead of number inputs.

Task: Similarly, edit update-coord to take a Coord as an input and produce a Coord as output. The produced Coord should have the same x value as the input Coord, while the produced y value still increases by 10 (as it did before).


CHECKPOINT:

Call over a TA once you reach this point.


Problem 3 โ€“ Putting it Together: The Reactor

We now have two functions that work with coordinates: falling-dessert produces an image at one coordinate, while update-coord produces the next coordinate at which to draw an image. If we can make these two functions work together, we can get an animation without us having to create images by hand.

Luckily, Pyret has something called a Reactor that coordinates these functions for us. Add the following code to your file, then run it:

my-reactor = reactor:
  init: coord(375, 0),
  to-draw: falling-dessert,
  on-tick: update-coord
end

interact(my-reactor)

You should now see something like the first animation we showed you! (the cupcake will fly past the plate and go off the bottom of the screen โ€“ we'll fix that later)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More โ†’

What's a Reactor?

A reactor is a special value in Pyret that has different components that play a part in creating the animation you see when you call interact on the reactor. Let's look at what the components of the reactor are doing:

  • init: coord(375, 0) โ€“ the initial coordinate for our reactor; this is the initial value of what will be changing frame by frame
  • to-draw: falling-dessert โ€“ the function that returns an image based on the current coordinate
  • on-tick: update-coord โ€“ the function that produces the next coordinate for our reactor, frame by frame (tick by tick). Its first input will be the init value.

Here's a visual of what the reactor is doing:

Breakdown of a reactor.

Task: Look at the Reactor diagram and the code. Note any observations or things you are curious about regarding reactors.

Task: Try to explain why we had to change the contract (types) of update-coord from Number -> Number to Coord -> Coord to make the Reactor work.


CHECKPOINT:

When you've finished the above tasks, submit this Google Form with your answer to the second task.


Problem 3.1: From Animations to Games

So far, our reactor uses update-coord to move the cupcake every few milliseconds. Let's make a game from our animation, where the plate is drawn at a random position and the player's goal is to move the falling cupcake to land it on the plate?

Task: Edit your code to make the plate start at a random position. (HINT: num-random(n) could be helpful!)

Next, we want to have a human player control the movement of the cupcake. You will press keys on your keyboard (such as the arrow keys) to generate a new Coord (rather than only have update-coord do that automatically).

Task: Decide how you would like each of the following keys to change the coordinate for the cupcake:

  • Left arrow
  • Right arrow
  • Up arrow
  • Down arrow

Task: Write a function move-dessert that takes in a Coord and a String and returns a new Coord. The String names the pressed key, the input Coord represents the current position of the cupcake, and the output Coord represents the cupcakeโ€™s new position after the keypress. The strings for the arrow keys are "left", "right", "up", and "down".

(If you want to add more keys later, you can use a keyโ€™s literal character as the string for that key. For example, โ€œaโ€ is the a key, โ€œbโ€ is the b key, etc.)

Task: Add the move-dessert function to your reactor like this:

my-reactor = reactor:
  init: init-coord,
  to-draw: falling-dessert,
  on-tick: update-coord,
  on-key: move-dessert
end

Try playing your new game!


CHECKPOINT:

Call over a TA once you reach this point.


Problem 3.2: Additional Features

Here are some additional features you can implement if you have time. You could also come up with your own (remember, you can include more behaviors with additional keys).

Wrap-around:
What happens if the cupcake misses the plate? Would the cupcake tragically fall to the ground? (Try this out using your current reactor.)

Modify your on-tick function so that if the cupcake goes off the bottom of the screen, it starts again from the top (PHEW!).

Collisions:
Write a function found-plate that takes in a Coord representing the position of the cupcake and returns a Boolean indicating whether the cupcake has landed on (or collided with) the dessert.

Add it to your reactor like this:

my-reactor = reactor:
  init: init-coord,
  to-draw: falling-dessert,
  on-tick: update-coord,
  on-key: move-dessert,
  stop-when: found-plate
end

Next, congratulate yourself for successfully scoring a dessert on your plate by modifying to-draw so that if there is a collision (found-plate returns true), the image becomes a congratulatory image of your choice.


CHECKPOINT:

Call over a TA once you reach this point.


Problem 3.3: Advanced Features

What if we wanted more than one changing feature of our animation? For example, what if we wanted to rain down desserts from the sky?

Currently, our program is using a single Coord to capture the information that is changing from frame to frame. But a reactor can use ANY type to connect the draw and update functions, as long as that type is consistent across the two functions and the initial value.

For example, rather than a single Coord, your changing information could be an entire List of Coord, where each Coord is information about a separate cupcake.

Task: Rewrite the reactor and its component functions to handle multiple cupcakes through List. You can choose which images respond to your on-key function โ€“ be creative!


CHECKPOINT:

Call over a TA once you reach this point.


Cupcake saved!

Thanks to your excellent cupcake-navigating skills, you were able to rescue the falling cupcake from its demise! Safely on your plate, you can now sit back, relax, and enjoy.