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.