CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Animation Part 2: Time-Based Animations in Tkinter


  1. Updated Starter Code
  2. Controller: timerFired
  3. Example: Changing Colors
  4. Example: Bouncing and Pausing Square
  5. Example: Snake
  6. Snake and MVC

Notes:
  1. To run these examples, first download cmu_112_graphics.py and be sure it is in the same folder as the file you are running.
  2. That file has version numbers. As we release updates, be sure you are using the most-recent version!
  3. This is a relatively new animation framework (since Fall 2019), which means it may still have some bugs that we will fix as we go. Also, it is similar to previous semesters, but different in important ways. Be aware of this if you are reviewing previous semesters' materials (especially prior to Fall 2019)!
  4. As with Tkinter graphics, the examples here will not run using Brython in your browser.

  1. Controller: timerFired
    # Timer fired is called every timerDelay milliseconds # We can use this function to effectively loop over time, # thereby simulating movement over time def timerFired(app): app.timerCalls += 1 def appStarted(app): # Change app.timerDelay to affect how often timerFired is called app.timerDelay = 100 # 100 millisecond == 0.1 seconds app.timerCalls = 0 def mousePressed(app, event): pass def keyPressed(app, event): pass def redrawAll(app, canvas): canvas.create_text(app.width/2, app.height/2, text="Timer Calls: " + str(app.timerCalls))
    Result:



  2. Example: Changing Colors
    # Changes the color of the square every second def appStarted(app): app.timerDelay = 1000 # 1 second app.squareColor = "red" def mousePressed(app, event): pass def keyPressed(app, event): pass def timerFired(app): if app.squareColor == "red": app.squareColor = "green" elif app.squareColor == "green": app.squareColor = "blue" elif app.squareColor == "blue": app.squareColor = "red" def redrawAll(app, canvas): size = 50 canvas.create_rectangle(app.width/2 - size, app.height/2 - size, app.width/2 + size, app.height/2 + size, fill=app.squareColor)
    Result:



  3. Example: Bouncing and Pausing Square
    # Draws a bouncing square which can be paused def appStarted(app): app.squareLeft = 50 app.squareTop = 50 app.squareFill = "yellow" app.squareSize = 25 app.squareSpeed = 20 app.headingRight = True app.headingDown = True app.isPaused = False app.timerDelay = 50 def mousePressed(app, event): pass def keyPressed(app, event): if (event.char == "p"): app.isPaused = not app.isPaused elif (event.char == "s"): doStep(app) def timerFired(app): if (not app.isPaused): doStep(app) def doStep(app): # Move vertically if (app.headingRight == True): if (app.squareLeft + app.squareSize > app.width): app.headingRight = False else: app.squareLeft += app.squareSpeed else: if (app.squareLeft < 0): app.headingRight = True else: app.squareLeft -= app.squareSpeed # Move horizontally if (app.headingDown == True): if (app.squareTop + app.squareSize > app.height): app.headingDown = False else: app.squareTop += app.squareSpeed else: if (app.squareTop < 0): app.headingDown = True else: app.squareTop -= app.squareSpeed def redrawAll(app, canvas): # draw the square canvas.create_rectangle(app.squareLeft, app.squareTop, app.squareLeft + app.squareSize, app.squareTop + app.squareSize, fill=app.squareFill) # draw the text canvas.create_text(app.width/2, 20, text="Pressing 'p' pauses/unpauses timer") canvas.create_text(app.width/2, 40, text="Pressing 's' steps the timer once")
    Result:



  4. Example: Snake
    Here is a 4-part video explaining how to write this version of Snake:
    1. Draw the board and the Snake
    2. Add motion and gameOver
    3. Add food and self-collision
    4. Add the timer and finish the game
    from cmu_112_graphics import * import random def appStarted(app): app.rows = 10 app.cols = 10 app.margin = 5 # margin around grid app.timerDelay = 250 initSnakeAndFood(app) app.waitingForFirstKeyPress = True def initSnakeAndFood(app): app.snake = [(0,0)] app.direction = (0, +1) # (drow, dcol) placeFood(app) app.gameOver = False # getCellBounds from grid-demo.py def getCellBounds(app, row, col): # aka 'modelToView' # returns (x0, y0, x1, y1) corners/bounding box of given cell in grid gridWidth = app.width - 2*app.margin gridHeight = app.height - 2*app.margin x0 = app.margin + gridWidth * col / app.cols x1 = app.margin + gridWidth * (col+1) / app.cols y0 = app.margin + gridHeight * row / app.rows y1 = app.margin + gridHeight * (row+1) / app.rows return (x0, y0, x1, y1) def keyPressed(app, event): if (app.waitingForFirstKeyPress): app.waitingForFirstKeyPress = False elif (event.key == 'r'): initSnakeAndFood(app) elif app.gameOver: return elif (event.key == 'Up'): app.direction = (-1, 0) elif (event.key == 'Down'): app.direction = (+1, 0) elif (event.key == 'Left'): app.direction = (0, -1) elif (event.key == 'Right'): app.direction = (0, +1) # elif (event.key == 's'): # this was only here for debugging, before we turned on the timer # takeStep(app) def timerFired(app): if app.gameOver or app.waitingForFirstKeyPress: return takeStep(app) def takeStep(app): (drow, dcol) = app.direction (headRow, headCol) = app.snake[0] (newRow, newCol) = (headRow+drow, headCol+dcol) if ((newRow < 0) or (newRow >= app.rows) or (newCol < 0) or (newCol >= app.cols) or ((newRow, newCol) in app.snake)): app.gameOver = True else: app.snake.insert(0, (newRow, newCol)) if (app.foodPosition == (newRow, newCol)): placeFood(app) else: # didn't eat, so remove old tail (slither forward) app.snake.pop() def placeFood(app): # Keep trying random positions until we find one that is not in # the snake. Note: there are more sophisticated ways to do this. while True: row = random.randint(0, app.rows-1) col = random.randint(0, app.cols-1) if (row,col) not in app.snake: app.foodPosition = (row, col) return def drawBoard(app, canvas): for row in range(app.rows): for col in range(app.cols): (x0, y0, x1, y1) = getCellBounds(app, row, col) canvas.create_rectangle(x0, y0, x1, y1, fill='white') def drawSnake(app, canvas): for (row, col) in app.snake: (x0, y0, x1, y1) = getCellBounds(app, row, col) canvas.create_oval(x0, y0, x1, y1, fill='blue') def drawFood(app, canvas): if (app.foodPosition != None): (row, col) = app.foodPosition (x0, y0, x1, y1) = getCellBounds(app, row, col) canvas.create_oval(x0, y0, x1, y1, fill='green') def drawGameOver(app, canvas): if (app.gameOver): canvas.create_text(app.width/2, app.height/2, text='Game over!', font='Arial 26 bold') canvas.create_text(app.width/2, app.height/2+40, text='Press r to restart!', font='Arial 26 bold') def redrawAll(app, canvas): if (app.waitingForFirstKeyPress): canvas.create_text(app.width/2, app.height/2, text='Press any key to start!', font='Arial 26 bold') else: drawBoard(app, canvas) drawSnake(app, canvas) drawFood(app, canvas) drawGameOver(app, canvas) runApp(width=400, height=400)

  5. Snake and MVC
    Model View Controller
    app.rows redrawAll() keyPressed()
    app.cols drawGameOver() timerFired()
    app.margin drawFood() takeStep()
    app.waitingForFirstKeyPress drawSnake() placeFood()
    app.snake drawBoard()
    app.direction
    app.foodPosition
    + all game state + all drawing functions + all event-triggered actions