backlinks to this page:
Github:
Video link: http://www.youtube.com/watch?v=SDsomsEWl7E
In this example, two tanks can be controlled by the players (using both hands), moving forward and backward and rotating. Additionally, the turrets can rotate also. The turrets can shoot bullets out of the tanks's main cannon (please admire the recoil effect) and the tanks can fire tracer rounds from machine guns. Each gun has a machine gun at the turret and at its bow (see graphic at the right).
This source code examples teaches nothing new but demonstrate how to solve a specific problem: Creating bullet-sprites not at the center of it's launcher, but at the end (and some space away) from it. The most obvious solution for such a problem would be to create the bullet at the center of it's launcher (the Tank) and use the Layer system to make sure the Tank is drawn on top of the bullet.
But if you have a fine eye you will notice some ugly effects: if you rotate a cannon fast enough, it will look like the bullet exits at the side of the cannon instead of at it's end. Also creating one Sprite at the exact position of another sprite will trigger a collision detection, needing more code to make sure that a tank cannot shoot itself.
To deal with the problem of creating a bullet sprite at the exact end of a rotated cannon sprite, see the source code below. All you need is a little knowledge of the math.sin and math.cos function (remember to transform grad into radiant), like explained in step017 - rotating, shooting, inheritance.
If you like complicated explanations: What you do here is creating a vector and rotating it to find the coordinate of a point (the musszle). This is done in the methods calculate_origin of the Bullet and the Tracer class:
For the Bullet, shooting out of the muzzle of the tanks main gun the problem is this: It's boss sprite, the Tank turret, is rotated by turretAngle. Also the cannon is very long, nearly as long as the side of the Tank.
def calculate_origin(self): # - spawn bullet at end of turret barrel instead tank center - # cannon is around Tank.side long, calculatet from Tank center # later subtracted 20 pixel from this distance # so that bullet spawns closer to tank muzzle self.pos[0] += math.cos(degrees_to_radians(self.boss.turretAngle)) * (Tank.side-20) self.pos[1] += math.sin(degrees_to_radians(-self.boss.turretAngle)) * (Tank.side-20)
For the Tracer (shooting out of the red bow rectangle of the tank) the point of launching is the little red rectangle in the front of the Tank. Because the Tank can rotate it's turret independent of the tank's own rotation, the important variable here is tankangle. The bow machine gun rectangle is not so much distanced from the Tank's center (side/2), but it is a bit on the left side. De facto i created here an 2D-Vector, 30° from the Tank's center and with the lenght of Tank.side/2. This vector is rotated with the tankAngle to find the coordinates of the point of origin for the Tracer round.
def calculate_origin(self): """overwriting because another point of origin is needed""" # - spawn bullet at end of bow rect (and some extra distance) # the bow rect is in the middle -left from the tank center # calculatet by going -30° from the Tank center for the half tank side self.pos[0] += math.cos(degrees_to_radians(30+self.boss.tankAngle)) * (Tank.side/2) self.pos[1] += math.sin(degrees_to_radians(-30-self.boss.tankAngle)) * (Tank.side/2)
While playing you will notice that sometimes some keyboard command seem to be ignored when you press several keys at once. It also can happen that if you press some combinations of more than 2 keys at the same time, it seems like a different key was pressed (ghost)
To test it out:
This phenomenon is well known among game designer. Ultimately it is caused by the way how keyboards are constructed.
A possible solution is to design games with fewer keys to be pressed and use keys like SHIFT, ALT and CTRL because those keys are better recognized by design of the hardware. Also think about accepting input from Mouse and Joysticks (see pygame documentation) or writing network-games where each player has his own keyboard.
See the glossary entry keyboard for more information. You will find there a very cool program1) to test out how many keys you can press at the same time without confusing your keyboard.
no additional resources necessary.
<note tip>Can you add sound effects to the Tankdemo ? See step010 - using sound and music</note> <note tip>Can you create moving or stationary practice targets for the Tankdemo ? See step017 - rotating, shooting...</note>
To run this example you need:
file | in folder | download |
---|---|---|
020_shooting_from_tank.py | pygame | Download the whole Archive with all files from Github: https://github.com/horstjens/ThePythonGameBook/archives/master |
View/Edit/Download the file directly in Github: https://github.com/horstjens/ThePythonGameBook/blob/master/pygame/020_shooting_from_tank.py
#!/usr/bin/env python # -*- coding: utf-8 -*- """ 020_shooting_from_tank.py demo of tank game with rotating turrets url: http://thepythongamebook.com/en:part2:pygame:step020 author: horst.jens@spielend-programmieren.at licence: gpl, see http://www.gnu.org/licenses/gpl.html demo of 2 tanks shooting bullets at the end of it's cannon and shooting tracers at the end of it's bow Machine Gun and from the turret-machine gun (co-axial with main gun) works with pyhton3.4 and python2.7 """ #the next line is only needed for python2.x and not necessary for python3.x from __future__ import print_function, division import pygame import random import math GRAD = math.pi / 180 # 2 * pi / 360 # math module needs Radiant instead of Grad class Config(object): """ a class to hold all game constants that may be modded by the user""" fullscreen = False width = 640 height = 480 fps = 100 xtiles = 30 # how many grid tiles for x axis ytiles = 20 # how many grid tiles for y axis class Text(pygame.sprite.Sprite): """ a helper class to write text on the screen """ number = 0 book = {} def __init__(self, pos, msg): self.number = Text.number # get a unique number Text.number += 1 # prepare number for next Textsprite Text.book[self.number] = self # store myself into the book pygame.sprite.Sprite.__init__(self, self.groups) self.pos = [0.0,0.0] self.pos[0] = pos[0] self.pos[1] = pos[1] self.msg = msg self.changemsg(msg) def update(self, seconds): pass def changemsg(self,msg): self.msg = msg self.image = write(self.msg) self.rect = self.image.get_rect() self.rect.centerx = self.pos[0] self.rect.centery = self.pos[1] class Bullet(pygame.sprite.Sprite): """ a big projectile fired by the tank's main cannon""" side = 7 # small side of bullet rectangle vel = 180 # velocity mass = 50 maxlifetime = 10.0 # seconds def __init__(self, boss): pygame.sprite.Sprite.__init__(self, self.groups) # THE most important line ! self.boss = boss self.dx = 0 self.dy = 0 self.angle = 0 self.lifetime = 0.0 self.color = self.boss.color self.calculate_heading() # !!!!!!!!!!!!!!!!!!! self.dx += self.boss.dx self.dy += self.boss.dy # add boss movement self.pos = self.boss.pos[:] # copy (!!!) of boss position #self.pos = self.boss.pos # uncomment this linefor fun effect self.calculate_origin() self.update() # to avoid ghost sprite in upper left corner, # force position calculation. def calculate_heading(self): """ drawing the bullet and rotating it according to it's launcher""" self.radius = Bullet.side # for collision detection self.angle += self.boss.turretAngle self.mass = Bullet.mass self.vel = Bullet.vel image = pygame.Surface((Bullet.side * 2, Bullet.side)) # rect 2 x 1 image.fill((128,128,128)) # fill grey pygame.draw.rect(image, self.color, (0,0,int(Bullet.side * 1.5), Bullet.side)) # rectangle 1.5 length pygame.draw.circle(image, self.color, (int(self.side *1.5) ,self.side//2), self.side//2) # circle image.set_colorkey((128,128,128)) # grey transparent self.image0 = image.convert_alpha() self.image = pygame.transform.rotate(self.image0, self.angle) self.rect = self.image.get_rect() self.dx = math.cos(degrees_to_radians(self.boss.turretAngle)) * self.vel self.dy = math.sin(degrees_to_radians(-self.boss.turretAngle)) * self.vel def calculate_origin(self): # - spawn bullet at end of turret barrel instead tank center - # cannon is around Tank.side long, calculatet from Tank center # later subtracted 20 pixel from this distance # so that bullet spawns closer to tank muzzle self.pos[0] += math.cos(degrees_to_radians(self.boss.turretAngle)) * (Tank.side-20) self.pos[1] += math.sin(degrees_to_radians(-self.boss.turretAngle)) * (Tank.side-20) def update(self, seconds=0.0): # ---- kill if too old --- self.lifetime += seconds if self.lifetime > Bullet.maxlifetime: self.kill() # ------ calculate movement -------- self.pos[0] += self.dx * seconds self.pos[1] += self.dy * seconds # ----- kill if out of screen if self.pos[0] < 0: self.kill() elif self.pos[0] > Config.width: self.kill() if self.pos[1] < 0: self.kill() elif self.pos[1] > Config.height: self.kill() #------- move ------- self.rect.centerx = round(self.pos[0],0) self.rect.centery = round(self.pos[1],0) class Tracer(Bullet): """Tracer is nearly the same as Bullet, but smaller and with another origin (bow MG rect instead cannon. Tracer inherits all methods of Bullet, but i overwrite calculate_heading and calculate_origin""" side = 15 # long side of bullet rectangle vel = 200 # velocity mass = 10 color = (200,0,100) maxlifetime = 10.0 # seconds def __init__(self, boss, turret=False): self.turret = turret Bullet.__init__(self,boss ) # this line is important def calculate_heading(self): """overwriting the method because there are some differences between a tracer and a main gun bullet""" self.radius = Tracer.side # for collision detection self.angle = 0 self.angle += self.boss.tankAngle if self.turret: self.angle = self.boss.turretAngle self.mass = Tracer.mass self.vel = Tracer.vel image = pygame.Surface((Tracer.side, Tracer.side / 4)) # a line image.fill(self.boss.color) # fill yellow ? pygame.draw.rect(image, (0,0,0), (Tracer.side * .75, 0, Tracer.side, Tracer.side / 4)) # red dot at front image.set_colorkey((128,128,128)) # grey transparent self.image0 = image.convert_alpha() self.image = pygame.transform.rotate(self.image0, self.angle) self.rect = self.image.get_rect() if self.turret: # turret mg self.dx = math.cos(degrees_to_radians(self.boss.turretAngle)) * self.vel self.dy = math.sin(degrees_to_radians(-self.boss.turretAngle)) * self.vel else: # bow mg self.dx = math.cos(degrees_to_radians(self.boss.tankAngle)) * self.vel self.dy = math.sin(degrees_to_radians(-self.boss.tankAngle)) * self.vel def calculate_origin(self): """overwriting because another point of origin is needed""" # - spawn bullet at end of machine gun muzzle (bow or turret) if self.turret: self.pos[0] += math.cos(degrees_to_radians(-90+self.boss.turretAngle)) * 15 self.pos[1] += math.sin(degrees_to_radians(90-self.boss.turretAngle)) * 15 else: self.pos[0] += math.cos(degrees_to_radians(30+self.boss.tankAngle)) * (Tank.side/2) self.pos[1] += math.sin(degrees_to_radians(-30-self.boss.tankAngle)) * (Tank.side/2) class Tank(pygame.sprite.Sprite): """ A Tank, controlled by the Player with Keyboard commands. This Tank draw it's own Turret (including the main gun) and it's bow rectangle (slit for Tracer Machine Gun""" side = 100 # side of the quadratic tank sprite recoiltime = 0.75 # how many seconds the cannon is busy after firing one time mgrecoiltime = 0.2 # how many seconds the bow mg (machine gun) is idle turretTurnSpeed = 25 # turret tankTurnSpeed = 8 # tank movespeed = 25 maxrotate = 360 # maximum amount of degree the turret is allowed to rotate book = {} # a book of tanks to store all tanks number = 0 # each tank gets his own number # keys for tank control, expand if you need more tanks # player1, player2 etc firekey = (pygame.K_k, pygame.K_DOWN) mgfirekey = (pygame.K_LCTRL, pygame.K_KP_ENTER) mg2firekey = (pygame.K_i, pygame.K_UP) turretLeftkey = (pygame.K_j, pygame.K_LEFT) turretRightkey = (pygame.K_l, pygame.K_RIGHT) forwardkey = (pygame.K_w, pygame.K_KP8) backwardkey = (pygame.K_s, pygame.K_KP5) tankLeftkey = (pygame.K_a, pygame.K_KP4) tankRightkey = (pygame.K_d, pygame.K_KP6) color = ((200,200,0), (0,0,200)) msg = ["wasd LCTRL, ijkl", "Keypad: 4852, ENTER, cursor"] def __init__(self, startpos = (150,150), angle=0): self.number = Tank.number # now i have a unique tank number Tank.number += 1 # prepare number for next tank Tank.book[self.number] = self # store myself into the tank book pygame.sprite.Sprite.__init__(self, self.groups) # THE most important line ! self.pos = [startpos[0], startpos[1]] # x,y self.dx = 0 self.dy = 0 self.ammo = 30 # main gun self.mgammo = 500 # machinge gun self.msg = "player%i: ammo: %i/%i keys: %s" % (self.number+1, self.ammo, self.mgammo, Tank.msg[self.number]) Text((Config.width/2, 30+20*self.number), self.msg) # create status line text sprite self.color = Tank.color[self.number] self.turretAngle = angle #turret facing self.tankAngle = angle # tank facing self.firekey = Tank.firekey[self.number] # main gun self.mgfirekey = Tank.mgfirekey[self.number] # bow mg self.mg2firekey = Tank.mg2firekey[self.number] # turret mg self.turretLeftkey = Tank.turretLeftkey[self.number] # turret self.turretRightkey = Tank.turretRightkey[self.number] # turret self.forwardkey = Tank.forwardkey[self.number] # move tank self.backwardkey = Tank.backwardkey[self.number] # reverse tank self.tankLeftkey = Tank.tankLeftkey[self.number] # rotate tank self.tankRightkey = Tank.tankRightkey[self.number] # rotat tank # painting facing north, have to rotate 90° later image = pygame.Surface((Tank.side,Tank.side)) # created on the fly image.fill((128,128,128)) # fill grey if self.side > 10: pygame.draw.rect(image, self.color, (5,5,self.side-10, self.side-10)) #tank body, margin 5 pygame.draw.rect(image, (90,90,90), (0,0,self.side//6, self.side)) # track left pygame.draw.rect(image, (90,90,90), (self.side-self.side//6, 0, self.side,self.side)) # right track pygame.draw.rect(image, (255,0,0), (self.side//6+5 , 10, 10, 5)) # red bow rect left #pygame.draw.rect(image, (255,0,0), (self.side/2 - 5, 10, 10, 5)) # red bow rect middle pygame.draw.circle(image, (255,0,0), (self.side//2,self.side//2), self.side//3 , 2) # red circle for turret image = pygame.transform.rotate(image,-90) # rotate so to look east self.image0 = image.convert_alpha() self.image = image.convert_alpha() self.rect = self.image0.get_rect() #---------- turret ------------------ self.firestatus = 0.0 # time left until cannon can fire again self.mgfirestatus = 0.0 # time until mg can fire again self.mg2firestatus = 0.0 # time until turret mg can fire again self.turndirection = 0 # for turret self.tankturndirection = 0 self.movespeed = Tank.movespeed self.turretTurnSpeed = Tank.turretTurnSpeed self.tankTurnSpeed = Tank.tankTurnSpeed Turret(self) # create a Turret for this tank def update(self, seconds): # no need for seconds but the other sprites need it #-------- reloading, firestatus---------- if self.firestatus > 0: self.firestatus -= seconds # cannon will soon be ready again if self.firestatus <0: self.firestatus = 0 #avoid negative numbers if self.mgfirestatus > 0: self.mgfirestatus -= seconds # bow mg will soon be ready again if self.mgfirestatus <0: self.mgfirestatus = 0 #avoid negative numbers if self.mg2firestatus > 0: self.mg2firestatus -= seconds # turret mg will soon be ready again if self.mg2firestatus <0: self.mg2firestatus = 0 #avoid negative numbers # ------------ keyboard -------------- pressedkeys = pygame.key.get_pressed() # -------- turret manual rotate ---------- self.turndirection = 0 # left / right turret rotation if pressedkeys[self.turretLeftkey]: self.turndirection += 1 if pressedkeys[self.turretRightkey]: self.turndirection -= 1 #---------- tank rotation --------- self.tankturndirection = 0 # reset left/right rotation if pressedkeys[self.tankLeftkey]: self.tankturndirection += 1 if pressedkeys[self.tankRightkey]: self.tankturndirection -= 1 # ---------------- rotate tank --------------- self.tankAngle += self.tankturndirection * self.tankTurnSpeed * seconds # time-based turning of tank # angle etc from Tank (boss) oldcenter = self.rect.center oldrect = self.image.get_rect() # store current surface rect self.image = pygame.transform.rotate(self.image0, self.tankAngle) self.rect = self.image.get_rect() self.rect.center = oldcenter # if tank is rotating, turret is also rotating with tank ! # -------- turret autorotate ---------- self.turretAngle += self.tankturndirection * self.tankTurnSpeed * seconds + self.turndirection * self.turretTurnSpeed * seconds # time-based turning # ---------- fire cannon ----------- if (self.firestatus ==0) and (self.ammo > 0): if pressedkeys[self.firekey]: self.firestatus = Tank.recoiltime # seconds until tank can fire again Bullet(self) self.ammo -= 1 self.msg = "player%i: ammo: %i/%i keys: %s" % (self.number+1, self.ammo, self.mgammo, Tank.msg[self.number]) Text.book[self.number].changemsg(self.msg) # -------- fire bow mg --------------- if (self.mgfirestatus ==0) and (self.mgammo >0): if pressedkeys[self.mgfirekey]: self.mgfirestatus = Tank.mgrecoiltime Tracer(self, False) # turret mg = False self.mgammo -= 1 self.msg = "player%i: ammo: %i/%i keys: %s" % (self.number+1, self.ammo, self.mgammo, Tank.msg[self.number]) Text.book[self.number].changemsg(self.msg) # -------- fire turret mg --------------- if (self.mg2firestatus ==0) and (self.mgammo >0): if pressedkeys[self.mg2firekey]: self.mg2firestatus = Tank.mgrecoiltime # same recoiltime for both mg's Tracer(self, True) # turret mg = True self.mgammo -= 1 self.msg = "player%i: ammo: %i/%i keys: %s" % (self.number+1, self.ammo, self.mgammo, Tank.msg[self.number]) Text.book[self.number].changemsg(self.msg) # ---------- movement ------------ self.dx = 0 self.dy = 0 self.forward = 0 # movement calculator if pressedkeys[self.forwardkey]: self.forward += 1 if pressedkeys[self.backwardkey]: self.forward -= 1 # if both are pressed togehter, self.forward becomes 0 if self.forward == 1: self.dx = math.cos(degrees_to_radians(self.tankAngle)) * self.movespeed self.dy = -math.sin(degrees_to_radians(self.tankAngle)) * self.movespeed if self.forward == -1: self.dx = -math.cos(degrees_to_radians(self.tankAngle)) * self.movespeed self.dy = math.sin(degrees_to_radians(self.tankAngle)) * self.movespeed # ------------- check border collision --------------------- self.pos[0] += self.dx * seconds self.pos[1] += self.dy * seconds if self.pos[1] + self.side//2 >= Config.height: self.pos[1] = Config.height - self.side//2 self.dy = 0 # crash into border elif self.pos[1] -self.side/2 <= 0: self.pos[1] = 0 + self.side//2 self.dy = 0 # ---------- paint sprite at correct position --------- self.rect.centerx = round(self.pos[0], 0) #x self.rect.centery = round(self.pos[1], 0) #y class Turret(pygame.sprite.Sprite): """turret on top of tank""" def __init__(self, boss): pygame.sprite.Sprite.__init__(self, self.groups) # THE most important line ! self.boss = boss self.side = self.boss.side self.images = {} # how much recoil after shooting, reverse order of apperance self.images[0] = self.draw_cannon(0) # idle position self.images[1] = self.draw_cannon(1) self.images[2] = self.draw_cannon(2) self.images[3] = self.draw_cannon(3) self.images[4] = self.draw_cannon(4) self.images[5] = self.draw_cannon(5) self.images[6] = self.draw_cannon(6) self.images[7] = self.draw_cannon(7) self.images[8] = self.draw_cannon(8) # position of max recoil self.images[9] = self.draw_cannon(4) self.images[10] = self.draw_cannon(0) # idle position def update(self, seconds): # painting the correct image of cannon if self.boss.firestatus > 0: self.image = self.images[int(self.boss.firestatus // (Tank.recoiltime / 10.0))] else: self.image = self.images[0] # --------- rotating ------------- # angle etc from Tank (boss) oldrect = self.image.get_rect() # store current surface rect self.image = pygame.transform.rotate(self.image, self.boss.turretAngle) self.rect = self.image.get_rect() # ---------- move with boss --------- self.rect = self.image.get_rect() self.rect.center = self.boss.rect.center def draw_cannon(self, offset): # painting facing right, offset is the recoil image = pygame.Surface((self.boss.side * 2,self.boss.side * 2)) # created on the fly image.fill((128,128,128)) # fill grey pygame.draw.circle(image, (255,0,0), (self.side,self.side), 22, 0) # red circle pygame.draw.circle(image, (0,255,0), (self.side,self.side), 18, 0) # green circle pygame.draw.rect(image, (255,0,0), (self.side-10, self.side + 10, 15,2)) # turret mg rectangle pygame.draw.rect(image, (0,255,0), (self.side-20 - offset,self.side - 5, self.side - offset,10)) # green cannon pygame.draw.rect(image, (255,0,0), (self.side-20 - offset,self.side - 5, self.side - offset,10),1) # red rect image.set_colorkey((128,128,128)) return image # ---------------- End of classes -------------------- #------------ defs ------------------ def radians_to_degrees(radians): return (radians / math.pi) * 180.0 def degrees_to_radians(degrees): return degrees * (math.pi / 180.0) def write(msg="pygame is cool"): """helper function for the Text sprite""" myfont = pygame.font.SysFont("None", 28) mytext = myfont.render(msg, True, (255,0,0)) mytext = mytext.convert_alpha() return mytext def pressedKeysString(): """returns the pressed keys (for the player1 tank) to be displayd in the status line""" pressedkeys = pygame.key.get_pressed() line = "" if pressedkeys[pygame.K_a]: line += "a " if pressedkeys[pygame.K_d]: line += "d " if pressedkeys[pygame.K_w]: line += "w " if pressedkeys[pygame.K_s]: line += "s " if pressedkeys[pygame.K_LCTRL]: line += "LCTRL" return line def main(): """the game itself""" pygame.init() screen=pygame.display.set_mode((Config.width,Config.height)) background = pygame.Surface((screen.get_size())) background.fill((128,128,255)) # fill grey light blue:(128,128,255) # paint a grid of white lines for x in range(0,Config.width,Config.width//Config.xtiles): #start, stop, step pygame.draw.line(background, (255,255,255), (x,0), (x,Config.height)) for y in range(0,Config.height,Config.height//Config.ytiles): #start, stop, step pygame.draw.line(background, (255,255,255), (0,y), (Config.width,y)) # paint upper rectangle to have background for text pygame.draw.rect(background, (128,128,255), (0,0,Config.width, 70)) background = background.convert() screen.blit(background, (0,0)) # delete all clock = pygame.time.Clock() # create pygame clock object FPS = Config.fps # desired max. framerate playtime = 0 tankgroup = pygame.sprite.Group() bulletgroup = pygame.sprite.Group() allgroup = pygame.sprite.LayeredUpdates() Tank._layer = 4 # base layer Bullet._layer = 7 # to prove that Bullet is in top-layer Tracer._layer = 5 # above Tank, but below Turret Turret._layer = 6 # above Tank & Tracer Text._layer = 3 # below Tank #assign default groups to each sprite class Tank.groups = tankgroup, allgroup Turret.groups = allgroup Bullet.groups = bulletgroup, allgroup Text.groups = allgroup player1 = Tank((150,250), 90) # create first tank, looking north player2 = Tank((450,250), -90) # create second tank, looking south status3 = Text((Config.width//2, 10), "Tank Demo. Press ESC to quit") mainloop = True while mainloop: milliseconds = clock.tick(Config.fps) # milliseconds passed since last frame seconds = milliseconds / 1000.0 # seconds passed since last frame (float) playtime += seconds for event in pygame.event.get(): if event.type == pygame.QUIT: # pygame window closed by user mainloop = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: mainloop = False # exit game pygame.display.set_caption("FPS: %.2f keys: %s" % ( clock.get_fps(), pressedKeysString())) #screen.blit(background, (0,0)) # delete all allgroup.clear(screen, background) # funny effect if you outcomment this line allgroup.update(seconds) allgroup.draw(screen) pygame.display.flip() # flip the screen 30 times a second return 0 if __name__ == '__main__': main()