Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

preface

In this issue, we will take you to further reproduce our Magic Tower mini-game, the main content includes the definition of hero class and the realization of basic actions, and the switching of different layers in the process of action.

Without further ado, let’s begin happily

The development tools

Python version: 3.7.4

Related modules:

Pygame module;

And some modules that come with Python.

Environment set up

Install Python and add it to the environment variables. PIP installs the required related modules.

Introduction of the principle

Last time, we implemented the basic screen definition for the game, something like this:

Careful friends must have noticed, how can there be no warriors in the map? How can we save the princess without him? Don’t worry, this time we will take you to realize that part.

First, let’s define our hero warrior category:

"Define our hero warrior."
class Hero(pygame.sprite.Sprite) :
    def __init__(self, imagepaths, blocksize, position, fontpath=None) :
        pygame.sprite.Sprite.__init__(self)
        Set the base properties
        self.font = pygame.font.Font(fontpath, 40)
        # Load the corresponding image
        self.images = {}
        for key, value in imagepaths.items():
            self.images[key] = pygame.transform.scale(pygame.image.load(value), (blocksize, blocksize))
        self.image = self.images['down']
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = position
        Set the level and other information
        self.level = 1
        self.life_value = 1000
        self.attack_power = 10
        self.defense_power = 10
        self.num_coins = 0
        self.experience = 0
        self.num_yellow_keys = 0
        self.num_purple_keys = 0
        self.num_red_keys = 0
    Bind the warrior to the screen.
    def draw(self, screen) :
        screen.blit(self.image, self.rect)
Copy the code

Here’s what it looks like when tied to the game’s main screen:

Does something look wrong? Yes, the left side of the original text shows the warrior’s current status ah! It’s all gone now! We’ll write a few lines of code to display the hero information in the left pane:

font_renders = [
    self.font.render(str(self.level), True, (255.255.255)),
    self.font.render(str(self.life_value), True, (255.255.255)),
    self.font.render(str(self.attack_power), True, (255.255.255)),
    self.font.render(str(self.defense_power), True, (255.255.255)),
    self.font.render(str(self.num_coins), True, (255.255.255)),
    self.font.render(str(self.experience), True, (255.255.255)),
    self.font.render(str(self.num_yellow_keys), True, (255.255.255)),
    self.font.render(str(self.num_purple_keys), True, (255.255.255)),
    self.font.render(str(self.num_red_keys), True, (255.255.255)),
]
rects = [fr.get_rect() for fr in font_renders]
rects[0].topleft = (160.80)
for idx in range(1.6):
    rects[idx].topleft = 160.127 + 42 * (idx - 1)
for idx in range(6.9):
    rects[idx].topleft = 160.364 + 55 * (idx - 6)
for fr, rect in zip(font_renders, rects):
    screen.blit(fr, rect)
Copy the code

It looks like this:

Having completed the basic definition of the warrior class, it is time to make it move. Specifically, we will first implement a warrior action class function:

"' action ' ' '
def move(self, direction) :
    assert direction in self.images
    self.image = self.images[direction]
    move_vector = {
        'left': (-self.blocksize, 0),
        'right': (self.blocksize, 0),
        'up': (0, -self.blocksize),
        'down': (0, self.blocksize),
    }[direction]
    self.rect.left += move_vector[0]
    self.rect.top += move_vector[1]
Copy the code

Then write a button detection and determine the warrior’s direction of action based on the key value the player presses:

key_pressed = pygame.key.get_pressed()
if key_pressed[pygame.K_w] or key_pressed[pygame.K_UP]:
    self.hero.move('up')
elif key_pressed[pygame.K_s] or key_pressed[pygame.K_DOWN]:
    self.hero.move('down')
elif key_pressed[pygame.K_a] or key_pressed[pygame.K_LEFT]:
    self.hero.move('left')
elif key_pressed[pygame.K_d] or key_pressed[pygame.K_RIGHT]:
    self.hero.move('right')
Copy the code

If you think I’ve done all right, you’re done, there are two problems with this.

First of all, this will cause the player to press the up button once, and the warrior will move a lot of squares, which will make it difficult for the player to control the position of the warrior. In this case, we can add an action cooling variable:

# Action cooling
self.move_cooling_count = 0
self.move_cooling_time = 5
self.freeze_move_flag = False
Copy the code

To count while cooling:

if self.freeze_move_flag:
    self.move_cooling_count += 1
    if self.move_cooling_count > self.move_cooling_time:
        self.move_cooling_count = 0
        self.freeze_move_flag = False
Copy the code

The hero will not regain action until the count is complete. So move can be rewritten as:

"' action ' ' '
def move(self, direction) :
    if self.freeze_move_flag: return
    assert direction in self.images
    self.image = self.images[direction]
    move_vector = {
        'left': (-self.blocksize, 0),
        'right': (self.blocksize, 0),
        'up': (0, -self.blocksize),
        'down': (0, self.blocksize),
    }[direction]
    self.rect.left += move_vector[0]
    self.rect.top += move_vector[1]
    self.freeze_move_flag = True
Copy the code

If you’re interested, you can remove this code and actually see if there’s a difference between keyboard manipulation of our warriors.

The other problem, the most serious problem, is that the action will be illegal, such as warriors in positions like this:

Therefore, we need to add an additional judgment as to whether the move is legal:

"' action ' ' '
def move(self, direction, map_parser) :
    if self.freeze_move_flag: return
    assert direction in self.images
    self.image = self.images[direction]
    move_vector = {'left': (-1.0), 'right': (1.0), 'up': (0, -1), 'down': (0.1)}[direction]
    block_position = self.block_position[0] + move_vector[0], self.block_position[1] + move_vector[1]
    if block_position[0] > =0 and block_position[0] < map_parser.map_size[1] and \
            block_position[1] > =0 and block_position[1] < map_parser.map_size[0] :if map_parser.map_matrix[block_position[1]][block_position[0]] in ['0']:
            self.block_position = block_position
        elif map_parser.map_matrix[block_position[1]][block_position[0]] in ['24']:
            self.dealcollideevent(
                elem=map_parser.map_matrix[block_position[1]][block_position[0]],
                block_position=block_position,
                map_parser=map_parser,
            )
    self.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]
    self.freeze_move_flag = True
Copy the code

Here, for the sake of judgment, we changed the pixel coordinates to the block coordinates of the elements in the game map (that is, the index of the position of each number in the map matrix in the game map designed in the last phase). The other thing we need to think about is that in future iterations of the game, we need to respond to the collision between the warrior and some elements of the map, such as the duel between the warrior and the monster, picking up the key, etc., So we also embedded dealCollideEvent in the move function above to handle this situation. A simple effect is shown below:

Of course, according to the theory of the original game, there should be a dialog box for the background story, which we will implement in the next phase. In this phase, we mainly implement some basic functions, such as triggering some simple events, including meeting the door and picking up the key, etc. :

Handling impact Events
def dealcollideevent(self, elem, block_position, map_parser) :
    # When you meet a door of different colors, open it if you have a key, otherwise you can't advance
    if elem in ['2'.'3'.'4']:
        flag = False
        if elem == '2' and self.num_yellow_keys > 0:
            self.num_yellow_keys -= 1
            flag = True
        elif elem == '3' and self.num_purple_keys > 0:
            self.num_purple_keys -= 1
            flag = True
        elif elem == '4' and self.num_red_keys > 0:
            self.num_red_keys -= 1
            flag = True
        if flag: map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return flag
    # Pick up keys of different colors
    elif elem in ['6'.'7'.'8'] :if elem == '6': self.num_yellow_keys += 1
        elif elem == '7': self.num_purple_keys += 1
        elif elem == '8': self.num_red_keys += 1
        map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return True
    # Found gems
    elif elem in ['9'.'10'] :if elem == '9': self.defense_power += 3
        elif elem == '10': self.attack_power += 3
        map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return True
    # Meet the fairy, have a conversation, and move one space to the left
    elif elem in ['24']:
        map_parser.map_matrix[block_position[1]][block_position[0] - 1] = elem
        map_parser.map_matrix[block_position[1]][block_position[0]] = '0'
        return False
Copy the code

Finally, let’s implement the effect of switching the current game map while the warrior is walking up and down the stairs. This may sound a bit tricky at first, but it’s not, simply return the command for the walking up and down stairs event to the main loop:

# Up and down the stairs
elif elem in ['13'.'14'] :if elem == '13': events = ['upstairs']
    elif elem == '14': events = ['downstairs']
    return True, events

"' action ' ' '
def move(self, direction, map_parser) :
    # Decide whether to freeze or not
    if self.freeze_move_flag: return
    assert direction in self.images
    self.image = self.images[direction]
    # Mobile Warrior
    move_vector = {'left': (-1.0), 'right': (1.0), 'up': (0, -1), 'down': (0.1)}[direction]
    block_position = self.block_position[0] + move_vector[0], self.block_position[1] + move_vector[1]
    # Check whether the movement is valid and trigger the corresponding event
    events = []
    if block_position[0] > =0 and block_position[0] < map_parser.map_size[1] and \
            block_position[1] > =0 and block_position[1] < map_parser.map_size[0] :# -- Legal movement
        if map_parser.map_matrix[block_position[1]][block_position[0]] in ['0']:
            self.block_position = block_position
        # -- Trigger event
        elif map_parser.map_matrix[block_position[1]][block_position[0]] in ['2'.'3'.'4'.'6'.'7'.'8'.'9'.'10'.'13'.'14'.'24']:
            flag, events = self.dealcollideevent(
                elem=map_parser.map_matrix[block_position[1]][block_position[0]],
                block_position=block_position,
                map_parser=map_parser,
            )
            if flag: self.block_position = block_position
    # Reset warrior position
    self.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]
    # Freezing
    self.freeze_move_flag = True
    Return the event that needs to be fired in the main loop
    return events
Copy the code

Then respond in the main loop:

# -- Trigger game events
for event in move_events:
    if event == 'upstairs':
        self.map_level_pointer += 1
        self.loadmap()
    elif event == 'downstairs':
        self.map_level_pointer -= 1
        self.loadmap()
Copy the code

The effect is as follows:

I don’t know if you have noticed a problem, that is, the location of the warriors after going upstairs is actually wrong, theoretically it should be near the bottom of the stairway of the current map, not the last game map where the warriors went upstairs, so how should this part be implemented? A simple solution is to define a 00 variable at the top of the stairs when defining the game map:

When drawing a game map, use the 0 element:

if elem in self.element_images:
    image = self.element_images[elem][self.image_pointer]
    image = pygame.transform.scale(image, (self.blocksize, self.blocksize))
    screen.blit(image, position)
elif elem in ['00'.'hero']:
    image = self.element_images['0'][self.image_pointer]
    image = pygame.transform.scale(image, (self.blocksize, self.blocksize))
    screen.blit(image, position)    
Copy the code

However, when going up and down the stairs to switch the game map, we can use this identifier to reset the character’s position:

# -- Trigger game events
for event in move_events:
    if event == 'upstairs':
        self.map_level_pointer += 1
        self.loadmap()
        self.hero.placenexttostairs(self.map_parser, 'down')
    elif event == 'downstairs':
        self.map_level_pointer -= 1
        self.loadmap()
        self.hero.placenexttostairs(self.map_parser, 'up')
Copy the code

Where the function to reset the position is implemented as follows:

Place next to upper/lower stairs.
def placenexttostairs(self, map_parser, stairs_type='up') :
    assert stairs_type in ['up'.'down']
    for row_idx, row in enumerate(map_parser.map_matrix):
        for col_idx, elem in enumerate(row):
            if (stairs_type == 'up' and elem == '13') or (stairs_type == 'down' and elem == '14') :if row_idx > 0 and map_parser.map_matrix[row_idx - 1][col_idx] == '00':
                    self.block_position = col_idx, row_idx - 1
                elif row_idx < map_parser.map_size[0] - 1 and map_parser.map_matrix[row_idx + 1][col_idx] == '00':
                    self.block_position = col_idx, row_idx + 1
                elif col_idx > 0 and map_parser.map_matrix[row_idx][col_idx - 1] = ='00':
                    self.block_position = col_idx - 1, row_idx
                elif col_idx < map_parser.map_size[1] - 1 and map_parser.map_matrix[row_idx][col_idx + 1] = ='00':
                    self.block_position = col_idx + 1, row_idx
    self.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]
Copy the code

Retest it and see:

To sum up, it’s basically about implementing our warrior character and some simple event responses that need to happen when he meets some elements in the map.