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.
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.
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.
Take a few minutes to understand this gif:
Write down your answers to these questions in your Google Doc.
Let's look at that gif again, but this time, we'll look at some sequential frames individually.
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!
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:
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.
Call a TA over once you've made your first frame.
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).
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.
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:
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:
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)
Make sure you include this at the top of your program:
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
:
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).
Call over a TA once you reach this point.
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:
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)
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 frameto-draw: falling-dessert
โ the function that returns an image based on the current coordinateon-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:
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.
When you've finished the above tasks, submit this Google Form with your answer to the second task.
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:
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:
Try playing your new game!
Call over a TA once you reach this point.
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:
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.
Call over a TA once you reach this point.
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!
Call over a TA once you reach this point.
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.