这一期我们会带大家进一步复现我们的魔塔小游戏,主要内容包括英雄类的定义与其基础行动的实现,行动过程中触发不同层的切换等功能。
废话不多说,让我们愉快地开始吧~
Python版本: 3.7.4
相关模块:
pygame模块;
以及一些python自带的模块。
安装Python并添加到环境变量,pip安装需要的相关模块即可。
上一期,我们实现了游戏的基础画面定义,类似这样:
细心的小伙伴肯定发现了,地图里怎么没有我们的勇士呢?没有他我们还怎么去拯救公主呀~别急,这期就带大家来实现这部分内容。
首先,我们来定义一下我们的英雄勇士类: 源码分享
'''定义我们的主角勇士'''
class Hero(pygame.sprite.Sprite):
def __init__(self, imagepaths, blocksize, position, fontpath=None):
pygame.sprite.Sprite.__init__(self)
# 设置基础属性
self.font = pygame.font.Font(fontpath, 40)
# 加载对应的图片
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
# 设置等级等信息
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
'''将勇士绑定到屏幕上'''
def draw(self, screen):
screen.blit(self.image, self.rect)
复制代码
将其绑定到游戏主界面之后的效果如下:
看起来是不是哪里不对?没错,左边原来有文字显示勇士当前的状态呀!现在都没了!不过没关系,问题不大,我们写几行代码将英雄的信息显示在左边的面板上面即可:
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)
复制代码
效果是这样子的: 项目源码
完成了勇士类最基础的定义,接下来我们就该让他动起来啦,具体而言,我们先实现一个勇士行动的类函数:
'''行动'''
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]
复制代码
然后写个按键检测,并根据玩家按下的键值来决定勇士的行动方向:
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')
复制代码
如果你觉得我上面的代码写的没问题,大功告成了,这样写是有两个问题的。
首先,这样子写会导致玩家按一次上键,勇士就移动很多格,导致玩家不好控制勇士的位置,此时我们可以添加一个行动冷却变量:
# 行动冷却
self.move_cooling_count = 0
self.move_cooling_time = 5
self.freeze_move_flag = False
复制代码
在冷却中的时候进行计数:
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
复制代码
计数完成后英雄方可恢复行动能力。于是move可以重写成:
'''行动'''
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
复制代码
感兴趣的小伙伴可以自行去掉这段代码实际感受一下键盘操作我们的勇士时是否会存在区别。
另外一个问题,也是最严重的问题,那就是行动会不合法,比如勇士会出现在这样的位置:
因此,我们需要再添加额外的移动是否合法的判断:
'''行动'''
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
复制代码
这里,为了方便判断,我们将原来采用的像素坐标改成了游戏地图中的元素块坐标(即上一期设计的游戏地图里,每个数字在地图矩阵中的位置索引)。另外,这里我们还需要想到的一个点是未来进一步复现游戏的过程中,我们需要在勇士和地图中一些元素发生碰撞时作出对应的响应,例如勇士和怪物进行决斗,捡到钥匙等等事件,因此我们也在上面的move函数中嵌入了dealcollideevent来处理这样的情况,一个简单效果展示如下:
当然,理论上按照原版的游戏这里应该是有一个背景故事的对话框的,这部分我们下一期再实现,本期我们主要实现一些基础的功能,比如一些简单事件的触发,包括遇到门,捡到钥匙等等:
'''处理撞击事件'''
def dealcollideevent(self, elem, block_position, map_parser):
# 遇到不同颜色的门, 有钥匙则打开, 否则无法前进
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
# 捡到不同颜色的钥匙
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
# 捡到宝石
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
# 遇到仙女, 进行对话, 并左移一格
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
复制代码
最后,我们来实现一下勇士上下楼梯时切换当前游戏地图的效果,这咋听起来似乎有点难办,但其实不然,只需要将发生上下楼梯事件的命令返回到游戏主循环:
# 上下楼梯
elif elem in ['13', '14']:
if elem == '13': events = ['upstairs']
elif elem == '14': events = ['downstairs']
return True, events
'''行动'''
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]
# 判断该移动是否合法, 并触发对应的事件
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]:
# --合法移动
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 ['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
# 重新设置勇士位置
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
# 返回需要在主循环里触发的事件
return 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()
复制代码
效果如下:
不知道大家有没有发现一个问题,就是勇士上楼之后所在的位置其实不对,理论上应该是在当前地图的下楼梯口附近的,而不是上一张游戏地图里勇士最后上楼时所在的位置,那么这部分应该如何实现呢?其实很简单,一个简单的解决方案是在定义游戏地图的时候,在上下楼梯处定义一个00变量:
画游戏地图的时候还是按照0元素去画:
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)
复制代码
但是上下楼梯切换游戏地图时,我们可以利用该标识符重置角色所在的位置:
# --触发游戏事件
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')
复制代码
其中重置位置的函数实现如下:
'''放置到上/下楼梯口旁'''
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]
复制代码
重新测试一下看看:
总结一下,主要就是实现了我们的勇士角色,以及他和地图中一些元素相遇后需要发生的一些简单的事件响应。