ThePythonGameBook

learn Python. Create Games

User Tools

Site Tools


en:pygame:step022

Step 022 - Scrolling and mini-map

video

description

screenshot of step022minimap.py In this example you can control the 2 famous tanks from the previous step. But some features were added: The map or “playground” is larger than the screen resolution. . The player sprite(s) can be steered around the whole map (but not over the map edge). The position of all Tank, Bullet and Tracer sprites and the current visible area of the map can be seen in the minimap or radar-map. The minimap is always visible on the top right corner of the screen. The minimap itself is also a Sprite, called Radarmap. The dimensions of screen, the big map and the radarmap are all visible inside the Config class. Minimaps or Radar-maps are very common in a lot of games, and de-facto standard in real-time-strategy games (rts-games).

code discussion

In the Code examples of this part of the book, all sprites have a self.pos attribute, a list of two coordinates [x,y]. This self.pos variable is used for all calculations (angle, movement etc.). Until now, the self.pos variable was identical with the self.rect position (self.rect.centerx, self.rect.centery), the only difference being that self.rect demands integer values while the self.pos variables could handle decimal points.

But now with the bigmap being by far larger as the visible screen area, the math gets more complex. While the self.pos variables stay more or less untouched, the position of the visible screen area in realation to the big map is to be calculated when blitting the sprites at the self.rect.center position.

Config.cornerpoint shows the way

In this example, the offset of the visible screen to the bigmap is calculated with the aid of the variable Config.cornerpoint. It's a list of 2 coordinates [x,y] and stored inside the Config-class so that other classes can comfortably access it. It's values are the coordinates of the topleft corner of the visible screen area. At the beginning, those are set to [0,0] and there is no difference between the self.rect coordinates and the self.pos coordinates.

As soon as the player moves the visible area with the Cursor - Keys, the value of Config.cornerpoint is changed (this is done inside the mainloop):

  #...
        # -------- scroll the big map ----------
        scrollx = 0
        scrolly = 0
        pressedkeys = pygame.key.get_pressed()
        # --- handle Cursor keys to scroll map ----
        if pressedkeys[pygame.K_LEFT]:
             scrollx -= Config.scrollstepx
        if pressedkeys[pygame.K_RIGHT]:
             scrollx += Config.scrollstepx
        if pressedkeys[pygame.K_UP]:
             scrolly -= Config.scrollstepy
        if pressedkeys[pygame.K_DOWN]:
             scrolly += Config.scrollstepy
        # -------- scroll the visible part of the map ------
        Config.cornerpoint[0] += scrollx
        Config.cornerpoint[1] += scrolly  
  #...
Config.scrollstepx and Config.scrollstepy are variables to control how many pixels the visible area should scroll each frame. Do not make those values too big, or the scrolling look jumpy.

Spritely Tank Class

Now, let's take a look inside a typical Sprite class, like the Tank class. At the end of its update(self)-function, there is the blitting of the self.rect.center variables. Here the values of Config.cornerpoint are subtracted. Change the - into a + for a funny effect:

  #... 
        self.rect.centerx = round(self.pos[0] - Config.cornerpoint[0], 0) #x
        self.rect.centery = round(self.pos[1] - Config.cornerpoint[1], 0) #y    
  #...

Bulleting Sprites

Also note that in contrast to the previous step, in this code example the Bullet sprite class has it's own book dictionary to store all Bullet (and Tracer) sprites. This is necessary so that the Radarmap sprite can iterate throug all Tank sprites (in the Tank book) and all Bullets (in the Bullet book) to draw little dots. Inside the Radarmap class, self.factorx and self.factory are variables indication the relation between bigmap to radarmap.

#... inside Radarmap class , update function...
        for tanknumber in Tank.book: # tank are circles with radius 4
            pos = Tank.book[tanknumber].pos
            color = Tank.book[tanknumber].color
            pygame.draw.circle(self.image,color, (round(pos[0] * self.factorx,0),
                                                  round(pos[1] * self.factory,0)), 4 )
        for bulletnumber in Bullet.book:
            if Bullet.book[bulletnumber].tracer:
                dotlength = 2 # bullets are rectangles with sidelength 4 (bullets) or 2 (tracer)
            else:
                dotlength = 4 # rect with length 1 is not visible
            pos = Bullet.book[bulletnumber].pos
            color = Bullet.book[bulletnumber].color
            pygame.draw.rect(self.image, color,(round(pos[0] * self.factorx,0),
                                                round(pos[1] * self.factory,0),
                                                dotlength, dotlength))
#...

ideas

<note tip>How powerful is your computer ? Change all those variables in the Config class to make a big map with a big screen resolution. See if the framerate is dropping.</note> <note tip>Remember loading images (step007) ? try to load an existing image to use as bigmap</note> <note tip>What of building obstacles for the tanks to navigate around?</note> <note tip>What about lobbing projectiles over obstacles by calculating kinetic energy required to move an object that weighs X amount Y distance?</note>

additional resources

no additional resources necessary

source code on github

To run this example you need:

file in folder download
022_minimap.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/022_minimap.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
022_minimap.py
demo of tank game with rotating turrets and minimap
url: http://thepythongamebook.com/en:part2:pygame:step022
author: horst.jens@spielend-programmieren.at
licence: gpl, see http://www.gnu.org/licenses/gpl.html

the minimap displays tanks, traces and bullets even for elements
currently not visible on the playfield.

works with python3.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
    bigmapwidth = 1024
    bigmapheight = 800
    fps = 60
    xtiles = 15 # how many grid tiles for x axis
    ytiles = 15 # how many grid tiles for y axis
    title = "Esc: quit"
    scrollstepx = 3 # how many pixels to scroll when pressing cursor key
    scrollstepy = 3 # how many pixels to scroll when pressing cursor key
    cornerpoint = [0,0] # left upper edge of visible screen rect inside bigmap
    radarmapwidth = 200
    radarmapheight = 150

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
    book = {}
    number = 0
    def __init__(self, boss):
        pygame.sprite.Sprite.__init__(self, self.groups) # THE most important line !
        self.boss = boss
        self.number = Bullet.number 
        Bullet.number += 1
        Bullet.book[self.number] = self # add myself into the bulletbook
        self.dx = 0
        self.dy = 0
        self.angle = 0
        self.tracer = False # the tracer class will have self.tracer = True
        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
        pygame.draw.circle(image, (0,0,0), (int(Bullet.side * 1.5) , Bullet.side // 2) , 2) # point 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 kill(self):
        # overwriting this function to kill myself out of the bullet book correctly
        del Bullet.book[self.number] # delete myself out of the book
        pygame.sprite.Sprite.kill(self) # call the kill function
 
    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.bigmapwidth:
            self.kill()
        if self.pos[1] < 0:
            self.kill()
        elif self.pos[1] > Config.bigmapheight:
            self.kill()
        #------- move -------
        self.rect.centerx = round(self.pos[0] - Config.cornerpoint[0],0)
        self.rect.centery = round(self.pos[1] - Config.cornerpoint[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 
        self.tracer = True
        
    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 Radarmap(pygame.sprite.Sprite):
    """a classic radarmap of the bigmap, 
    to be displayed alsways in the upper right corner 
    of the screen. With colored dots for the tanks
    and a rect for the visible screen area"""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self, self.groups) # THE most important line !
        self.image = pygame.Surface((Config.radarmapwidth, Config.radarmapheight))
        self.paintmap() # self image's color is not defined, therfore it remains black
        self.rect = self.image.get_rect()
        self.rect.topleft = (Config.width - Config.radarmapwidth, 0) # topleft is a pygame variable
        self.factorx = Config.radarmapwidth  * 1.0 / Config.bigmapwidth # 1.0 to force decimapl point calculation 
        self.factory = Config.radarmapheight *1.0 / Config.bigmapheight
        
    def paintmap(self):
        self.image.fill((0,0,0))
        pygame.draw.rect(self.image, (150,0,0), (0,0, Config.radarmapwidth, Config.radarmapheight),1)    
    
    def update(self, seconds):
        self.paintmap() # redraw black map # outcomment for funny painting effect
        # draw a withe rect to show the visible area of the bigmap
        pygame.draw.rect(self.image, (255,255,255), (round(Config.cornerpoint[0] * self.factorx,0),
                                                     round(Config.cornerpoint[1] * self.factory,0),
                                                     round(Config.width * self.factorx, 0),
                                                     round(Config.height * self.factory, 0)),1)
        for tanknumber in Tank.book: # tank are circles with radius 4
            pos = Tank.book[tanknumber].pos
            color = Tank.book[tanknumber].color
            pygame.draw.circle(self.image,color, (int(pos[0] * self.factorx),
                                                  int(pos[1] * self.factory)), 4 )
        for bulletnumber in Bullet.book:
            if Bullet.book[bulletnumber].tracer:
                dotlength = 2 # bullets are rectangles with sidelength 4 (bullets) or 2 (tracer)
            else:
                dotlength = 4 # rect with length 1 is not visible
            pos = Bullet.book[bulletnumber].pos
            color = Bullet.book[bulletnumber].color
            pygame.draw.rect(self.image, color,(int(pos[0] * self.factorx),
                                                int(pos[1] * self.factory),
                                                dotlength, dotlength))
    
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 = 50 # turret
    tankTurnSpeed = 80 # tank
    movespeed = 80
    #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_KP0)
    mgfirekey = (pygame.K_LCTRL, pygame.K_KP_ENTER)
    mg2firekey = (pygame.K_i, pygame.K_KP_PLUS)
    turretLeftkey = (pygame.K_j, pygame.K_KP7)
    turretRightkey = (pygame.K_l, pygame.K_KP9)
    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), (100,100,255))
    #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.color = Tank.color[self.number]
        self.turretAngle = angle #turret facing
        self.tankAngle = angle # tank facing
        self.msg =  "tank%i: x:%i y:%i facing: turret:%i tank:%i"  % (self.number, self.pos[0], self.pos[1], self.turretAngle, self.tankAngle )
        #Text((Config.width/2, 30+20*self.number), self.msg) # create status line text sprite
        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 self.number == 1:   # only for tank2
            self.aim_at_player()       # default aim at player0
        else:
            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[0] + self.side/2 >= Config.bigmapwidth:
            self.pos[0] = Config.bigmawidth - self.side/2
            self.dx = 0 # crash into border
        elif self.pos[0] -self.side/2 <= 0:
            self.pos[0] = 0 + self.side/2
            self.dx = 0
        if self.pos[1] + self.side/2 >= Config.bigmapheight:
            self.pos[1] = Config.bigmapheight - 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    
        
        self.rect.centerx = round(self.pos[0] - Config.cornerpoint[0], 0) #x
        self.rect.centery = round(self.pos[1] - Config.cornerpoint[1], 0) #y    
        #self.msg =  "tank%i: x:%i y:%i facing: turret:%i tank:%i"  % (self.number, self.pos[0], self.pos[1], self.turretAngle, self.tankAngle )
                    
    def aim_at_player(self, targetnumber=0):
        deltax = Tank.book[targetnumber].pos[0] - self.pos[0]
        deltay = Tank.book[targetnumber].pos[1] - self.pos[1]
        angle =   math.atan2(-deltax, -deltay)/math.pi*180.0    
        
        diff = (angle - self.turretAngle - 90) %360 #reset at 360
        diff -= 180
        # to avoid a jittering canon introduce a tolerance range of 4 degrees
        if abs(diff) < 2:
            self.turndirection = 0
        elif diff > 0:
            self.turndirection = 1
        else:
            self.turndirection = -1
        # return diff
     
     
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,255,255))
    mytext = mytext.convert_alpha()
    return mytext        

def main():
    """the game itself"""
    pygame.init()
    screen=pygame.display.set_mode((Config.width,Config.height)) 
    # note that "map" is an pygame function and can not be used as a name for a variable
    bigmap = pygame.Surface((Config.bigmapwidth, Config.bigmapheight))
    # ----------------- create bigmap -------------------
    bigmap.fill((128,128,128)) # fill grey 
    # paint a grid of dark lines
    for x in range(0,Config.bigmapwidth,Config.bigmapwidth//Config.xtiles): #start, stop, step
        pygame.draw.line(bigmap, (64,64,64), (x,0), (x,Config.bigmapheight))
    for y in range(0,Config.bigmapheight,Config.bigmapheight//Config.ytiles): #start, stop, step
        pygame.draw.line(bigmap, (64,64,64), (0,y), (Config.bigmapwidth,y))
    pygame.draw.rect(bigmap, (255,0,0), (0,0,Config.bigmapwidth, Config.bigmapheight), 25) # red bigmap edge
    # paint thin red cross in the middle of the map
    pygame.draw.line(bigmap, (200,0,0), (Config.bigmapwidth /2, 0),( Config.bigmapwidth / 2, Config.bigmapheight),1)
    pygame.draw.line(bigmap, (200,0,0), (0, Config.bigmapheight/2),( Config.bigmapwidth , Config.bigmapheight/2),1)
    bigmap = bigmap.convert()
    # ------- background is a subsurface of bigmap ----------
    background = pygame.Surface((screen.get_size()))
    backgroundrect = background.get_rect()
    background = bigmap.subsurface((Config.cornerpoint[0],
                                    Config.cornerpoint[1],
                                    Config.width,
                                    Config.height)) # take snapshot of bigmap
    # -----------------------------------
    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
    Radarmap._layer = 3  # below Tank # better 9 ?
 
    #assign default groups to each sprite class
    Tank.groups = tankgroup, allgroup
    Turret.groups = allgroup 
    Bullet.groups = bulletgroup, allgroup
    Text.groups = allgroup # text does not move with map !
    Radarmap.groups = allgroup # radar does not move with map
    
    player1 = Tank((150,250), 90) # create  first tank, looking north
    player2 = Tank((450,250), 90) # create second tank, looking south
    Radarmap() # there is only one radarmap, so no variable assigned to it
     
    status3 = Text((155, 25), "cursor keys to scroll the map,")
    status3 = Text((155, 45), "tanks: keypad, wasd, ijkl, lctrl")
    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
            # teleport player1 tank if left mousebutton is pressed
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if pygame.mouse.get_pressed()[0]:
                    #left mousebutton was pressed
                    player1.pos[0]=pygame.mouse.get_pos()[0] + Config.cornerpoint[0]
                    player1.pos[1]=pygame.mouse.get_pos()[1] + Config.cornerpoint[1]
                    
        # -------- scroll the big map ----------
        scrollx = 0
        scrolly = 0
        pressedkeys = pygame.key.get_pressed()
        # --- handle Cursor keys to scroll map ----
        if pressedkeys[pygame.K_LEFT]:
             scrollx -= Config.scrollstepx
        if pressedkeys[pygame.K_RIGHT]:
             scrollx += Config.scrollstepx
        if pressedkeys[pygame.K_UP]:
             scrolly -= Config.scrollstepy
        if pressedkeys[pygame.K_DOWN]:
             scrolly += Config.scrollstepy
        # -------- scroll the visible part of the map ------
        Config.cornerpoint[0] += scrollx
        Config.cornerpoint[1] += scrolly
        #--------- do not scroll out of bigmap edge -----
        if Config.cornerpoint[0] < 0:
            Config.cornerpoint[0] = 0
            scrollx = 0
        elif Config.cornerpoint[0] > Config.bigmapwidth - Config.width:
            Config.cornerpoint[0] = Config.bigmapwidth - Config.width
            scrollx = 0
        if Config.cornerpoint[1] < 0:
            Config.cornerpoint[1] = 0
            scrolly = 0
        elif Config.cornerpoint[1] > Config.bigmapheight - Config.height:
            Config.cornerpoint[1] = Config.bigmapheight - Config.height
            scrolly = 0
        
        pygame.display.set_caption("%s FPS: %.2f playtime: %.1f " % ( Config.title,clock.get_fps(), playtime))
        #screen.blit(background, (0,0)) # delete all
        if scrollx == 0 and scrolly == 0:    # only necessery if there was no scrolling
            allgroup.clear(screen, background) # funny effect if you outcomment this line
        else:
            background = bigmap.subsurface((Config.cornerpoint[0],
                                            Config.cornerpoint[1],
                                            Config.width,
                                            Config.height)) # take snapshot of bigmap
            screen.blit(background, (0,0))
        allgroup.update(seconds) 
        allgroup.draw(screen)
        pygame.display.flip() # flip the screen 30 times a second
    return 0

if __name__ == '__main__':
    main()

/var/www/horst/thepythongamebook.com/data/pages/en/pygame/step022.txt · Last modified: 2020/05/15 22:57 by Horst JENS