[ Jocelyn Ireson-Paine's Home Page | Ducks animation as an OGV video | Ducks animation as an animated GIF ]

Flying Duck à l'Orange

This is a Pyret program which I wrote as an experiment. I was teaching an Oxford Institute summer course for post-GCSE students, and wanted to demonstrate how animations and computer games work, but without getting lost in the minutiae of Java method syntax, graphics libraries, and the rest. I'd been looking at older languages for reactive animation, including the one that (as far as I know) started it, Fran. Fran looks awfully tempting, and so does Flapjax. But Fran is no longer available, and Flapjax requires knowledge of JavaScript — though admittedly, perhaps not very much. Further searching turned up Pyret. As it's a language designed specifically for teaching, I decided to try it.

I based the program below on the Pyret flight lander game. However, I've chosen variable names that I think easier to understand. I've also extended the world state so that it contains some other types of information, and I've animated one of my cartoons, to show that there's nothing special about the images used in animations. Copy the code below and paste it into the online Pyret editor, and you should be able to run it. You'll need to click the "Start Coding" button on the above page. There's a nice explanation of how Pyret worlds and animations work in http://www.bootstrapworld.org/materials/spring2016/courses/bs2/units/unit4/index.html, "Unit 4: Welcome to the World".

The top of this page links to a video of an animation, shown both as a .ogv file and an animated GIF. To make the .ogv file, I used RecordMyDesktop as described in its user guide, delineating Pyret's "Big Bang" window (which is where it shows animations) as the region to be videoed. I then converted the .ogv file to JPEGs and then an animated GIF as described in the AskUbuntu thread "How to create animated GIF images of a screencast?", using these commands:

mplayer -ao null ducks.ogv -vo jpeg:outdir=output
convert output/* -layers Optimize output.gif

Here, finally, is my Pyret program:

import image as I
import world as W
#
# These import the libraries that
# provide operations for handling
# images and worlds. See 
# https://www.pyret.org/docs/latest/Tutorial__A_Flight_Lander_Game.html#%28part._.Preliminaries%29 .


DUCK1-URL =
  "http://www.j-paine.org/pyret/duck1.png"
DUCK1 = I.image-url( DUCK1-URL )
#
# The URL of our picture of duck 1,
# and the picture itself. I've taken this
# and the other pictures (apart from the
# "GULP!" caption) from 
# http://www.jocelyns-cartoons.co.uk/cartoons2/duck_and_orange.html .


DUCK2-URL =
  "http://www.j-paine.org/pyret/duck2.png"
DUCK2 = I.image-url( DUCK2-URL )
#
# The URL of our picture of duck 2,
# and the picture itself.


ORANGE-ON-BRANCH-URL =
  "http://www.j-paine.org/pyret/orange.png"
ORANGE-ON-BRANCH = I.image-url( ORANGE-ON-BRANCH-URL )
#
# The orange on a branch.


EATEN-ORANGE-ON-BRANCH-URL =
  "http://www.j-paine.org/pyret/eaten_orange.png"
EATEN-ORANGE-ON-BRANCH = I.image-url( EATEN-ORANGE-ON-BRANCH-URL )
#
# The branch once the orange has been eaten.


GULP-URL =
  "http://www.j-paine.org/pyret/gulp.png"
GULP = I.image-url( GULP-URL )
#
# The "GULP!" caption.


BACKGROUND-URL = 
  "http://www.j-paine.org/pyret/background.png"
BACKGROUND = I.image-url( BACKGROUND-URL )
#
# The background, consisting of two islands in a 
# pond.


DUCK1-X-INIT = 220
DUCK1-Y-INIT = 280
#
# Duck 1's initial coordinates. These are the coordinates
# of the centre of the rectangle enclosing the duck
# in http://www.jocelyns-cartoons.co.uk/cartoons2/duck_and_orange.html .


DUCK1-X-CHANGE = 0.3
DUCK1-Y-CHANGE = -0.3
#
# How much duck 1 moves to the right
# and up on every clock tick.


DUCK2-X-INIT = 515
DUCK2-Y-INIT = 289
#
# Duck 2's initial coordinates.


ORANGE-X = 387
ORANGE-Y = 104
#
# The coordinates of the orange.


EATEN-ORANGE-X = 392
EATEN-ORANGE-Y = 90
#
# The coordinates of the eaten orange.


GULP-X = 387
GULP-Y = 104
#
# The coordinates of the "GULP!" caption.


data WorldState:
  | world-state( duck1-x, duck1-y, duck1-time-since-starts-eating, 
duck1-is-gulping, duck2-x, duck2-y, orange-is-there )
end
#
# The world state. This contains: duck 1's coordinates;
# the time in ticks since it starts eating the
# orange, or the string "N/A" if it hasn't yet;
# whether it is saying "GULP!"; duck 2's
# coordinates; and whether the orange is there
# (i.e. hasn't been eaten).


fun numbers-are-near( x1, x2 ):
  delta = x1 - x2
  num-abs( delta ) < 100
end
#
# An auxiliary function called by 'is-near'. Returns
# true if the difference between x1 and x2 is less
# than 100.


fun is-near( x1, y1, x2, y2 ):
  numbers-are-near( x1, y1 ) and numbers-are-near( y1, y2 )
end
#
# Called to determine whether the duck is near the
# orange. Returns true if its x and y coordinates
# are both less than 100 from the orange's.


fun duck1-is-at-orange( ws ):
  is-near( ws.duck1-x, ws.duck1-y, ORANGE-X, ORANGE-Y )
end
#
# Given the world state, returns true if duck1
# is roughly at the same position as the orange,
# false otherwise.


fun new-world-state( ws ):
  starts-eating = ws.orange-is-there and duck1-is-at-orange( ws )
  
  orange-still-there = ws.orange-is-there and not( starts-eating )
  
  time-since-starts-eating = if starts-eating: 
    0
  else if ws.duck1-time-since-starts-eating == "N/A":
    "N/A"
  else:
    ws.duck1-time-since-starts-eating + 1
  end
  
  gulping = not( time-since-starts-eating == "N/A" ) and ( 
time-since-starts-eating < 200 )
  
  world-state( ws.duck1-x + DUCK1-X-CHANGE,
    ws.duck1-y + DUCK1-Y-CHANGE,
    time-since-starts-eating,
    gulping,
    ws.duck2-x,
    ws.duck2-y,
    orange-still-there  )
end
#
# Given the world state on one
# tick, returns it updated. Updates are to:
# the position of the ducks (in this version,
# duck 2 doesn't move); the time since
# duck 1 started eating; whether it is
# saying "GULP!"; and whether the orange
# is still there. 


fun draw-world-state( ws ):
  background-with-duck1 = I.place-image( DUCK1,
    ws.duck1-x,
    ws.duck1-y,
    BACKGROUND )
  
  background-with-ducks = I.place-image( DUCK2,
    ws.duck2-x,
    ws.duck2-y,
    background-with-duck1 )
  
  background-with-ducks-and-orange = if ws.orange-is-there:   
    I.place-image( ORANGE-ON-BRANCH,
      ORANGE-X,
      ORANGE-Y,
      background-with-ducks )
  else:
    I.place-image( EATEN-ORANGE-ON-BRANCH,
      EATEN-ORANGE-X,
      EATEN-ORANGE-Y,
      background-with-ducks )
  end
  
  background-with-ducks-and-orange-and-gulp = if ws.duck1-is-gulping:
    I.place-image( GULP, GULP-X, GULP-Y, background-with-ducks-and-orange 
)
  else:
   background-with-ducks-and-orange
  end
  
  I.scale( 0.50, background-with-ducks-and-orange-and-gulp )
end
#
# Given the world state, returns a
# picture of it. This entails: placing the ducks in their
# correct positions; placing either the orange or the
# eaten orange; and placing the "GULP!" if duck 1 is
# saying it.


W.big-bang( world-state(DUCK1-X-INIT,DUCK1-Y-INIT,"N/A", false, 
DUCK2-X-INIT,DUCK2-Y-INIT,true),
  [list: W.on-tick( new-world-state ),
    W.to-draw( draw-world-state ) 
            ] 
          )
#
# Makes an animation. Initially, this
# makes a world state which is 0. Then
# on every clock tick, it makes a new
# world state by calling new-world-state
# on the old one. It also calls 
# draw-world-state to 
# convert the state to an image and
# make it the next frame in our 
# animation. See 
# https://www.pyret.org/docs/latest/Tutorial__A_Flight_Lander_Game.html#%28part._.Observing_.Time__and_.Combining_the_.Pieces_%29 .