Table of Contents
Step 017 - Rotating, shooting, inheritance, physics
Time to play! The source code example below is an actual playable game, demonstrating several different concepts:
With the source code example below you can shoot down little penguins (again). This time, you steer a fat penguin and can rotate and move him with the W,A,S,D keys. The movement is relative to the facing of the fat penguin; by pressing W he will not move upwards, but instead forward depending on his actual rotation. You can still move sidewards by pressing Q and R.
To calculate the forward movement of a rotated object in a cartesian (x,y) coordinate system some Trigonometric_functions are used. To make the game more interesting, some small penguins are also in the game. The player can shoot at them (using the SPACE key) and collide with them but should avoid the fragments when a small penguin explodes.
If a small penguin reaches the ground, he get some random “fuel”1) and moves upward (negative dy) until the “fuel” runs out. Gravity, that the player can turn on and off with the G, will suck penguins, shots and fragments down.
The small objects (penguins, shots) are pushed around by forces (boost, gravity, impact of shots and fragments or other penguins), leading to diagonal movement (dx and dy). While the rotation of the fat penguin dedicate it's movement, the movement of the small objects dedicate their rotation.
The goal of the game is too shoot down as many small penguins while trying to reach a 100% hit ratio. Each time one small penguin is killed a new one is created. The ratio of hits / misses will be calculated by the computer. The game ends if the
gametime runs out or the player was hit by too much red fragments.
- change the constants (all in CAPITAL LETTER) like GRAVITY, FRICTION etc.
- uncomment out-commented lines in
Bird.areacheckto let birds bounce off walls
- uncomment out-commented lines in
Bird.speedcheckto introduce a general speed limit
- give the small birds more “fuel” to fly higher: change
some trigonometry for pygame
|How to calculate sine and cosine for angle x|| Radiand vs. Grad: 360° = 2 * π
π (Pi) = 3.1418….
While python will take care of most mathematics involved in rotating sprites for you, it may be a good to refresh some school wisdom. In this example games, those trigometric functions are used:
- The sine: takes an angle (in radiant) as argument, returns the y coordinate
- The cosine: takes an angle (in radiant) as argument, returns the x coordinate
- The arctangent: takes a fraction as argument, returns an angle (in radiant)
As you can see in the drawing at the right side, sine is the vertical coordinate of a given point ( D in the diagram) on a unit circle2) while the cosine is the horizontal coordinate of the same point. The relation between those coordinates (vertical / horizontal) equals the tangent of the angle. The arctangent function is the inverse function of the tangent: arctangent takes the relation (y/x) as argument and returns the angle.
grad and radiant
There are several methods to measure the angle:
- divide a full circle into 360 degrees (grad) or
- divide a full circle into 2 times Pi 3)
While python's pygame module can handle basic rotation of sprites and surfaces using the 360 degree method, for calculating sine, cosine and arctangent, you need python's math module. Math functions use radians.
Two small functions help here:
import math def radians_to_degrees(radians): return (radians / math.pi) * 180.0 def degrees_to_radians(degrees): return degrees * (math.pi / 180.0)
calculating direction for a given rotation
The player's class, BigBird knows the rotation of the sprite and need the movement vectors
dy to calculate. Here is the code, not-so important bits are omitted:
import math GRAD = math.pi / 180 class BigBird(pygame.sprites.Sprites): #... def __init__(self): pressedkeys = pygame.key.get_pressed() self.ddx = 0.0 self.ddy = 0.0 if pressedkeys[pygame.K_w]: # forward self.ddx = -math.sin(self.angle*GRAD) self.ddy = -math.cos(self.angle*GRAD) if pressedkeys[pygame.K_s]: # backward self.ddx = +math.sin(self.angle*GRAD) self.ddy = +math.cos(self.angle*GRAD) if pressedkeys[pygame.K_e]: # right side self.ddx = +math.cos(self.angle*GRAD) self.ddy = -math.sin(self.angle*GRAD) if pressedkeys[pygame.K_q]: # left side self.ddx = -math.cos(self.angle*GRAD) self.ddy = +math.sin(self.angle*GRAD) #... self.dx += self.ddx * self.speed self.dy += self.ddy * self.speed #... self.pos += self.dx * seconds self.pos += self.dy * seconds #...
calculating rotation for a given direction
On the other hand, if you know dx and dy and need the fitting angle, you can use this code:
class Bullet(pygame.sprite.Sprite): #... def update(self, time): #... #--------- rotate into direction of movement ------------ #--- calculate with math.atan --- #if self.dx != 0 and self.dy!=0: # ratio = self.dy / self.dx # if self.dx > 0: # self.angle = -90-math.atan(ratio)/math.pi*180.0 # in grad # else: # self.angle = 90-math.atan(ratio)/math.pi*180.0 # in grad #--- or calculate with math.atan2 --- self.angle = math.atan2(-self.dx, -self.dy)/math.pi*180.0 self.image = pygame.transform.rotozoom(self.image0,self.angle,1.0)
Using math.atan2 function instead of math.atan save some code lines. You can view the documentation for the math module online at http://docs.python.org/library/math.html :
Return the arc tangent of x, in radians.
Return atan(y / x), in radians. The result is between -pi and pi. The vector in the plane from the origin to point (x, y) makes this angle with the positive X axis. The point of atan2() is that the signs of both inputs are known to it, so it can compute the correct quadrant for the angle. For example, atan(1) and atan2(1, 1) are both pi/4, but atan2(-1, -1) is -3*pi/4.
using class inheritance
The very useful Fragment class serves as a parent class for the BlueFragment class, the RedFragment class, the Smoke class and the Shot class.
Inside the game's mainloop is a tiny “physic engine” in use: It checks with a crashgroup if one bird is actually crashing into another bird. If so, both birds move away from each other.
This could be done with less sophisticated code like in the previous examples, like by simply giving the crashbird's new random values for dx and dy. However Lenoard Michlmayr was so nice to help me out here with some code for an elastic collision:
Note that to simplify the calculation, each bird is calculated as a disc. Also in the very special case that there is no speed (like if one bird is “beamed” into another bird) some random values for dx and dy are created.
# ... def elastic_collision(sprite1, sprite2): """elasitc collision between 2 sprites (calculated as disc's). The function alters the dx and dy movement vectors of both sprites. The sprites need the property .mass, .radius, .pos, .pos, .dx, dy pos is the x postion, pos the y position""" # here we do some physics: the elastic # collision # # first we get the direction of the push. # Let's assume that the sprites are disk # shaped, so the direction of the force is # the direction of the distance. dirx = sprite1.pos - sprite2.pos diry = sprite1.pos - sprite2.pos # # the velocity of the centre of mass sumofmasses = sprite1.mass + sprite2.mass sx = (sprite1.dx * sprite1.mass + sprite2.dx * sprite2.mass) / sumofmasses sy = (sprite1.dy * sprite1.mass + sprite2.dy * sprite2.mass) / sumofmasses # if we sutract the velocity of the centre # of mass from the velocity of the sprite, # we get it's velocity relative to the # centre of mass. And relative to the # centre of mass, it looks just like the # sprite is hitting a mirror. # bdxs = sprite2.dx - sx bdys = sprite2.dy - sy cbdxs = sprite1.dx - sx cbdys = sprite1.dy - sy # (dirx,diry) is perpendicular to the mirror # surface. We use the dot product to # project to that direction. distancesquare = dirx * dirx + diry * diry if distancesquare == 0: # no distance? this should not happen, # but just in case, we choose a random # direction dirx = random.randint(0,11) - 5.5 diry = random.randint(0,11) - 5.5 distancesquare = dirx * dirx + diry * diry dp = (bdxs * dirx + bdys * diry) # scalar product dp /= distancesquare # divide by distance * distance. cdp = (cbdxs * dirx + cbdys * diry) cdp /= distancesquare # We are done. (dirx * dp, diry * dp) is # the projection of the velocity # perpendicular to the virtual mirror # surface. Subtract it twice to get the # new direction. # # Only collide if the sprites are moving # towards each other: dp > 0 if dp > 0: sprite2.dx -= 2 * dirx * dp sprite2.dy -= 2 * diry * dp sprite1.dx -= 2 * dirx * cdp sprite1.dy -= 2 * diry * cdp
The function is called with 2 sprites as arguments during the mainloop. Note that the elastic_collision function has no return values but instead manipulates the .dx and .dy properties (the movement vectors) of both sprites.
# ... inside mainloop # ------ collision detection for bird in birdgroup: # test if a bird collides with another bird bird.crashing = False # make bird NOT blue # check the Bird.number to make sure the bird is not crashing with himself if not bird.waiting: # do not check birds outside the screen crashgroup = pygame.sprite.spritecollide(bird, birdgroup, False ) for crashbird in crashgroup: # test bird with other bird collision if crashbird.number > bird.number: #avoid checking twice bird.crashing = True # make bird blue crashbird.crashing = True # make other bird blue if not (bird.waiting or crashbird.waiting): elastic_collision(crashbird, bird) # change dx and dy of both birds
source code on github
To run this example you need:
|017_turning_and_physic.py|| || Download the whole Archive with all files from Github:
| babytux.png ||
| babytux_neg.png ||
| claws.ogg |
from Battle of Wesnoth
View/Edit/Download the file directly in Github: https://github.com/horstjens/ThePythonGameBook/blob/master/pygame/017_turning_and_physic.py
click reload in your browser if you see no code here:
comment this page