前言
这一期我们会带大家进一步复现我们的魔塔小游戏,主要内容包括英雄类的定义与其基础行动的实现,行动过程中触发不同层的切换等功能。
废话不多说,让我们愉快地开始吧~
开发工具
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 = 1self.life_value = 1000self.attack_power = 10self.defense_power = 10self.num_coins = 0self.experience = 0self.num_yellow_keys = 0self.num_purple_keys = 0self.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.imagesself.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 = 0self.move_cooling_time = 5self.freeze_move_flag = False
在冷却中的时候进行计数:
if self.freeze_move_flag:self.move_cooling_count += 1if self.move_cooling_count > self.move_cooling_time:self.move_cooling_count = 0self.freeze_move_flag = False
计数完成后英雄方可恢复行动能力。于是move可以重写成:
'''行动'''def move(self, direction):if self.freeze_move_flag: returnassert direction in self.imagesself.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: returnassert direction in self.imagesself.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_positionelif 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 = Falseif elem == '2' and self.num_yellow_keys > 0:self.num_yellow_keys -= 1flag = Trueelif elem == '3' and self.num_purple_keys > 0:self.num_purple_keys -= 1flag = Trueelif elem == '4' and self.num_red_keys > 0:self.num_red_keys -= 1flag = Trueif 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 += 1elif elem == '7': self.num_purple_keys += 1elif elem == '8': self.num_red_keys += 1map_parser.map_matrix[block_position[1]][block_position[0]] = '0'return True# 捡到宝石elif elem in ['9', '10']:if elem == '9': self.defense_power += 3elif elem == '10': self.attack_power += 3map_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] = elemmap_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: returnassert direction in self.imagesself.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 += 1self.loadmap()elif event == 'downstairs':self.map_level_pointer -= 1self.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 += 1self.loadmap()self.hero.placenexttostairs(self.map_parser, 'down')elif event == 'downstairs':self.map_level_pointer -= 1self.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 - 1elif 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 + 1elif col_idx > 0 and map_parser.map_matrix[row_idx][col_idx - 1] == '00':self.block_position = col_idx - 1, row_idxelif 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_idxself.rect.left, self.rect.top = self.block_position[0] * self.blocksize + self.offset[0], self.block_position[1] * self.blocksize + self.offset[1]
重新测试一下看看:
总结一下,主要就是实现了我们的勇士角色,以及他和地图中一些元素相遇后需要发生的一些简单的事件响应。本期完整源代码可以私信获取
文章到这里就结束了,感谢你的观看,Python29个小游戏系列,下篇文章分享魔塔小游戏呀(3)
为了感谢读者们,我想把我最近收藏的一些编程干货分享给大家,回馈每一个读者,希望能帮到你们。
往期回顾
Python从零开始带大家实现一个魔塔小游戏(1)