Fork/Clone Ch.11 Animation Jedi Training
We are getting pretty advanced in Python now as we continue forward to making games. The last chapter on the topic of Classes was crucial going forward and was also rather challenging so hopefully you took good notes and got a lot of practice. The Arcade library has a built-in Window() class that we will be using to represent our program. We can create our own child class and override functions to do a lot of things.
Up to this point, when making our graphics, we have used a function called open_window to open a window. Here’s some sample code to remind you how we did this. SW and SH are the variables Screen Width and Screen Height in case you were wondering.
import arcade
SW = 640
SH = 480
def main():
arcade.open_window(SW, SH, "Drawing Example")
arcade.run()
if __name__=="__main__":
main()
Now that we know about classes, we are going to do this differently. We are going to call a built-in Window() class from the arcade library instead and set it to a variable that I will call window. You can call it whatever you want.
import arcade
SW = 640
SH = 480
def main():
window = arcade.Window(SW, SH, "Drawing Example")
arcade.run()
if __name__=="__main__":
main()
Function calls, and calls to create an instance of an object look very similar. The obvious clue that we are creating an instance of an object in the second example is the fact that Window is capitalized, which means it is a Class.
Arcade’s Window class has a lot of built-in methods that are automatically called when needed. Methods for drawing, for responding to the keyboard, the mouse, and more. You can see all the methods by looking at the Window Class Documentation. But by default, these methods don’t do anything. We need to change that.
As we learned from the prior chapter, we can extend the functionality of a class by creating a child class. Therefore, we can extend the Window class by creating a child class of it. Let's call our child class MyGame. In our constructor for the MyGame class we call the Window constructor using super(). In the main() function we instantiate a MyGame object called window and pass it the width, height and title.
import arcade
SW = 640
SH = 480
class MyGame(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
def main():
window = MyGame(SW, SH, "Drawing Example")
arcade.run()
if __name__=="__main__":
main()
The Arcade Window class has a default method called on_draw which will eventually be updated 60 times/second. We need to override this method by creating our own on_draw method. All of our drawing code will be put within this method. Let's call the .start_render() method and then draw a ball in this section.
We also need to set the background color of the window. Since we only need to do this once, when the window object is initialized we will do it in the __init__ method. There's no point of doing that in the on_draw method and resetting the background 60 times/second when it isn’t changing. We will also initialize our starting x and y coordinates of the ball.
import arcade
SW = 640
SH = 480
class MyGame(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
arcade.set_background_color(arcade.color.ASH_GREY)
self.x=320
self.y=240
def on_draw(self):
arcade.start_render()
arcade.draw_circle_filled(self.x, self.y, 15, arcade.color.AUBURN)
def main():
window = MyGame(SW, SH, "Drawing Example")
arcade.run()
if __name__=="__main__":
main()
If you run our newest code you will see a static ball, since it was drawn one time. How do we get it to move and then be redrawn?
The Arcade Window class has another default method called on_update which takes a delta_time parameter which we will shorten to dt. Again, we need to override this method by creating our own on_update method. By overriding the on_update method, we can update our ball position and animate our scene. In this section, we will add 1 to both the x and y positions of the ball. The dt parameter that is passed to the on_update method is 1/60 second so whatever is in the on_update method will happen 60 times/second. So for example if you add 3 to the x position of a ball in the on_update method, the "velocity" of the ball will be traveling 60 x 3 = 180 pixels/second across the screen in the x direction.
import arcade
SW = 640
SH = 480
class MyGame(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
arcade.set_background_color(arcade.color.ASH_GREY)
self.x=320
self.y=240
def on_draw(self):
arcade.start_render()
arcade.draw_circle_filled(self.x, self.y, 15, arcade.color.AUBURN)
def on_update(self, dt):
self.x += 1
self.y += 1
def main():
window = MyGame(SW, SH, "Drawing Example")
arcade.run()
if __name__=="__main__":
main()
It doesn’t take much imagination to realize that adding more parameters to the ball, getting it to bounce, or even having several balls on the screen would make our MyGame class very complex. We should now know a way to encapsulate all that “ball” stuff together. It's called classes!
Here is a more complex example, but all the logic for the ball has been moved into a new Ball class. In the update_ball method of the Ball class we check to see if the pos_x and pos_y get to the edge of the screen and if so we multiply the dx and dy by -1 respectively to change the direction.
There is a lot going on here in this code. Discuss anything you are not sure about with your instructor. Your Take Home Test asks you 10 questions about this code. You will reuse this code a lot in your animation projects and games you will eventually make.
import arcade
SW = 640
SH = 480
class Ball:
def __init__(self):
self.pos_x = SW / 2
self.pos_y = SH / 2
self.dx = 3
self.dy = 2
self.rad = 15
self.col = arcade.color.AUBURN
def draw_ball(self):
arcade.draw_circle_filled(self.pos_x, self.pos_y, self.rad, self.col)
def update_ball(self):
self.pos_y += self.dy
self.pos_x += self.dx
#bounce off edge of screen
if self.pos_x < self.rad or self.pos_x > SW - self.rad:
self.dx *= -1
if self.pos_y < self.rad or self.pos_y > SH - self.rad:
self.dy *= -1
class MyGame(arcade.Window):
def __init__(self, width, height, title):
super().__init__(width, height, title)
arcade.set_background_color(arcade.color.ASH_GREY)
self.ball = Ball()
def on_draw(self):
arcade.start_render()
self.ball.draw_ball()
def on_update(self, dt):
self.ball.update_ball()
def main():
window = MyGame(SW, SH, "Drawing Example")
arcade.run()
if __name__=="__main__":
main()
What if we want to spawn multiple objects, all with different starting values? Instead of hard-coding attributes in the Ball( ) class you would randomize the attributes and then do something like the following:
self.ball_list=[] #Create Empty list to hold all ball objects
for i in range(100):
self.ball = Ball() #Instantiate ball
self.ball_list.append(self.ball) #Append ball to list
But how would we draw and update each object? We will need to iterate through the ball_list and call the "draw_ball" and "update_ball" methods on each ball within your "on_draw" and "on_update" methods of your MyGame class.
def on_draw(self):
arcade.start_render()
for item in self.ball_list:
item.draw_ball()
def on_update(self):
for item in self.ball_list:
item.update_ball()
30 Box Bounce Program
Snowfall Program
Bouncing BB8
Hickory Dickory Dock
Inner Solar System