相信大家小时候 一定都玩过一个经典游戏 推箱子 今天,我们就一起来学习 推箱子的姐妹版游戏 推星星 这款游戏可以说是进阶版 不仅在视觉效果上进行了增进 关卡设计也相对较难 你,准备好了吗? 游戏介绍 推星星游戏中,玩家在一个地上有许多星星、砖块、箱子的小房间里,他需将所有星星找出来并推到带有星标方块的方格中;当星星推到靠墙或者碰到另外一个星星的时候,玩家就不能再继续推动当前星星了,唯一的办法只能是重启关卡;当所有星星被推上星标方块后,这一关就完成了并进入下一关。 TIPS 游戏中的每一关都由许多2D网格方块组成,所有方块元素都能放在彼此的旁边,所以我们可以通过增加一些障碍方块来创造出许多困难有趣的关卡。 The Initial Setup(初始设置) 1. # 码趣学院CodingMarch 2. # 游戏分享之五:推星星 3. # 文末扫码或添加微信,获取游戏源代码 4. # 更多游戏添加公众号:maquxueyuan 5. 6. import random, sys, copy, os, pygame 7. from pygame.locals import * 8. 9. FPS = 30 # 帧数 10. WINWIDTH = 800 # 游戏窗口宽度(像素) 11. WINHEIGHT = 600 # 游戏窗口高度(像素) 12. HALF_WINWIDTH = int(WINWIDTH / 2) 13. HALF_WINHEIGHT = int(WINHEIGHT / 2) 14. 15. # 每个方块大小(像素). 16. TILEWIDTH = 50 17. TILEHEIGHT = 85 18. TILEFLOORHEIGHT = 45 19. 20. CAM_MOVE_SPEED = 5 # 镜头移动时每一帧的像素 21. 22. 23. # 地图外有额外装饰的方块比例 24. OUTSIDE_DECORATION_PCT = 20 25. 26. BRIGHTBLUE = ( 0, 170, 255) 27. WHITE = (255, 255, 255) 28. BGCOLOR = BRIGHTBLUE 29. TEXTCOLOR = WHITE 30. 31. UP = 'up' 32. DOWN = 'down' 33. LEFT = 'left' 34. RIGHT = 'right' 这些常量将用在程序的各个部分中。 TILEWIDTH和TILEHEIGHT变量将方块设置为50像素宽和85像素高。然而这些方块在屏幕上显示的时候会重叠起来(稍后解释) TILEFLOORHEIGHT意味着充当地板的部分方块大小为45像素。请见下面这张对这些设置的图解: 在关卡房间外长草的方块将可能带有额外的装饰(E.g.树或石头)OUTSIDE_DECORATION_PCT常量表示这些方块将有多大比例获得这些随机装饰。 37. def main(): 38. global FPSCLOCK, DISPLAYSURF, IMAGESDICT, TILEMAPPING, OUTSIDEDECOMAPPING, BASICFONT, PLAYERIMAGES, currentImage 39. 40. # pygame初始化并创建全局变量 41. pygame.init() 42. FPSCLOCK = pygame.time.Clock() 43. 44. # 因为储存在DISPLAYSURF中的表层对象 45. # 是pygame.display.set_mode()函数的返回值 46. # 所以当函数pygame.display.update()被调出时 47. # 这个表层对象就正如实际屏幕一样 48. DISPLAYSURF = pygame.display.set_mode((WINWIDTH, WINHEIGHT)) 49. 50. pygame.display.set_caption('Star Pusher') 51. BASICFONT = pygame.font.Font('freesansbold.ttf', 18) 这是通常Pygame在程序最开始设置时需要做的 53. # 一个全局字典里的值将包含由pygame.image.load()函数返回的 54. # 所有Pygame表层对象 55. IMAGESDICT = {'uncovered goal': pygame.image.load('RedSelector.png'), 56. 'covered goal': pygame.image.load('Selector.png'), 57. 'star': pygame.image.load('Star.png'), 58. 'corner': pygame.image.load('Wall Block Tall.png'), 59. 'wall': pygame.image.load('Wood Block Tall.png'), 60. 'inside floor': pygame.image.load('Plain Block.png'), 61. 'outside floor': pygame.image.load('Grass Block.png'), 62. 'title': pygame.image.load('star_title.png'), 63. 'solved': pygame.image.load('star_solved.png'), 64. 'princess': pygame.image.load('princess.png'), 65. 'boy': pygame.image.load('boy.png'), 66. 'catgirl': pygame.image.load('catgirl.png'), 67. 'horngirl': pygame.image.load('horngirl.png'), 68. 'pinkgirl': pygame.image.load('pinkgirl.png'), 69. 'rock': pygame.image.load('Rock.png'), 70. 'short tree': pygame.image.load('Tree_Short.png'), 71. 'tall tree': pygame.image.load('Tree_Tall.png'), 72. 'ugly tree': pygame.image.load('Tree_Ugly.png')} IMAGESDICT是储存所有已读图片的字典。 而这将使其他函数使用这些图片时方便许多,因为只有IMGAESDICT需要变为全局变量。 如果我们将这些图片分别储存在不同的变量里,那么这18个变量(游戏中使用了18张图片)都需要改为全局变量。而字典就能将所有带有图片的表层对象储存起来,使用起来将方便很多。 (我是游戏里的小草堆,可爱嘛!) 74. # 字典里的值都是全局值, 75. # 并规划字符在关卡文件中所代表的表层对象 76. TILEMAPPING = {'x': IMAGESDICT['corner'], 77. '#': IMAGESDICT['wall'], 78. 'o': IMAGESDICT['inside floor'], 79. ' ': IMAGESDICT['outside floor']} 地图的数据结构只是一系列的2D字符串。TILEMAPPING字典将地图数据结构中使用的字符与他们所代表的图片链接起来。(在drawMap()函数的解释里将显得更直观易懂) 80. OUTSIDEDECOMAPPING = {'1': IMAGESDICT['rock'], 81. '2': IMAGESDICT['short tree'], 82. '3': IMAGESDICT['tall tree'], 83. '4': IMAGESDICT['ugly tree']} OUTSIDEDECOMAPPING也是一个将地图数据结构中使用的字符与读取图片链接起来的字典。 “外部装饰”图将绘制在草地方块上方。 85. # PLAYERIMAGES是一个列表包括所有玩家可以变成的字符 86. # currentImage 是玩家当前使用的角色图片的索引 87. currentImage = 0 88. PLAYERIMAGES = [IMAGESDICT['princess'], 89. IMAGESDICT['boy'], 90. IMAGESDICT['catgirl'], 91. IMAGESDICT['horngirl'], 92. IMAGESDICT['pinkgirl']] PLAYERIMAGES列表储存用来改变控制角色形象的图片。 currentImage变量跟随当前使用的角色图片的序号。举个例子,当currentImage设置为0那么PLAYERIMAGES也为[0],而角色就会显示”“公主”这一形象的图片 (我是游戏里的人物,快来选我!) 94. startScreen() # 显示标题画面直到玩家按下任意一个键 95. 96. # 读取关卡文件里的关卡 97. # 通过readLevelsFile()来获取关卡文件格式的细节并了解如何编写自己的关卡 98. levels = readLevelsFile('starPusherLevels.txt') 99. currentLevelIndex = 0 startScreen()函数将会持续显示初始画面(这个画面带有游戏教程)直到玩家按下任意一个键。当玩家按下任意键后,startScreen()函数将返回并通过关卡文件读取关卡。玩家将从第一关开始,而这第一关就是在列表中序号为0的关卡。 (开始界面) 101. # 主要的游戏循环,这个循环在单个关卡运行 102. # 当玩家完成这个关卡,循环将读取上/下一关 103. while True: # 主要游戏循环 104. # 运行关卡至开始游戏阶段: 105. result = runLevel(levels, currentLevelIndex) runLevel()函数为游戏处理所有行为。这个函数通过一系列关卡对象并决定运行哪个整数序号关卡。 当玩家完成关卡,runLevel()将反馈以下字符串:’solved’意味着玩家达成关卡目标,’next’意味着玩家想要跳关,’back’意味着玩家想返回上一关,’reset’意味着玩家想重启关卡。 107. if result in ('solved', 'next'): 108. # 进入下一关 109. currentLevelIndex += 1 110. if currentLevelIndex >= len(levels): 111. # 如果之后没有关卡则返回第一关 112. currentLevelIndex = 0 113. elif result == 'back': 114. # 返回上一关 115. currentLevelIndex -= 1 116. if currentLevelIndex <> 117. # 如果没有上一关可以返回了则回到最后一关 118. currentLevelIndex = len(levels)-1 如果runLevel()反馈的字符串为’solved’或者’next’,那么我们需要给增长量levelNum增加1。如果levelNum超过了关卡总数,那么levelNum设定回0。 反过来同理,如果反馈的是’back’,levelNum就减1。如果减到低于0,那么就会设定回最后一关 ![]() 119. elif result == 'reset': 120. pass # 用来重新调用runLevel()重启关卡 如果返回值为’reset’,代码不会起任何作用。 pass语句在这里不起任何作用(更像一个评论),然而在Python编译器里是需要的,因为在elif语句后规定需要一行代码。 我们完全可以移除119和120行,而程序不会受到影响。而没有删除的原因是为了程序的可读性,因为这样我们就不会忘记runLevel()同样能反馈字符串’reset’。 123. def runLevel(levels, levelNum): 124. global currentImage 125. levelObj = levels[levelnum] 126. mapObj = decorateMap(levelObj['mapObj'], levelObj['startState']['player']) 127. gameStateObj = copy.deepcopy(levelObj['startState']) 关卡列表包括所有从关卡文件里读取的关卡。当前关卡对象(levelNum设定的)将储存在levelObj变量里。一个对象映射(区分地图内外方块以及装饰)将由函数decorateMap()反馈。而为了监测玩家所玩关卡的游戏状态,我们可以通过copy.deepcopy()函数从levelObj中复制一个游戏状态的对象用以监测。 复制游戏状态对象是因为储存在levelObj[‘startState’]中的对象意味着游戏刚开始的状态,而我们并不需要去修改它。而如果我们修改了,玩家在重启关卡时将无法读取初始游戏状态。 在这里使用copy.deepcopy()函数因为游戏状态对象是一个包含元组的字典。在这里使用一个命令语句来复制字典,我们所得到的不是字典所引用的值而是字典引用的对象。所以复制和初始的库仍将引用相同的元组。 copy.deepcopy()函数通过复制字典里确实存在的元组来解决这一问题。通过这个方法我们能保证在改变单个字典时不会对另一个字典造成影响。 ![]() (推星星第一关) 128. mapNeedsRedraw = True # 设置为True来调用drawmap() 129. levelSurf = BASICFONT.render('Level %s of %s' % (levelObj['levelNum'] + 1, totalNumOfLevels), 1, TEXTCOLOR) 130. levelRect = levelSurf.get_rect() 131. levelRect.bottomleft = (20, WINHEIGHT - 35) 132. mapWidth = len(mapObj) * TILEWIDTH 133. mapHeight = (len(mapObj[0]) - 1) * (TILEHEIGHT - TILEFLOORHEIGHT) + TILEHEIGHT 134. MAX_CAM_X_PAN = abs(HALF_WINHEIGHT - int(mapHeight / 2)) + TILEWIDTH 135. MAX_CAM_Y_PAN = abs(HALF_WINWIDTH - int(mapWidth / 2)) + TILEHEIGHT 136. 137. levelIsComplete = False 138. # 跟踪镜头的移动 139. cameraOffsetX = 0 140. cameraOffsetY = 0 141. # 监测是否按下了移动镜头的键 142. cameraUp = False 143. cameraDown = False 144. cameraLeft = False 145. cameraRight = False 现在更多变量将在关卡开始前设置好。 mapWidth和mapHeight变量是用像素来设置地图大小的变量。 用来计算mapHeight的表达式有一点复杂因为所有方块互相重合。只有底排的方块能展示完整的高度(所以加上了TILEHEIGHT这段表达式),而其他所有排的方块((len(mapObj[0]-1)))都有一部分重合。 这意味着他们都(TILEHEIGHT - TILEFLOORHEIGHT)像素高。 而在推星星游戏中的镜头可以随着玩家的控制独立移动。这也是为什么镜头需要它自己的移动变量cameraUp, cameraDown, cameraLeft, 和cameraRight. cameraOffsetX 和cameraOffsetY变量监测镜头的位置。 147. while True: # 主要游戏循环 148. # 重设变量: 149. playerMoveTo = None 150. keyPressed = False 151. 152. for event in pygame.event.get(): # 处理其他情况的循环 153. if event.type == QUIT: 154. # 玩家按下游戏窗口的右上角的X 155. terminate() 156. playerMoveTo变量将设置为玩家想要控制角色移动方向的方向常量。 keyPressed变量监测在游戏循环中是否有任意一个键是按下的,而这个变量将在玩家通过关卡后检查。 157. elif event.type == KEYDOWN: 158. # 处理按键 159. keyPressed = True 160. if event.key == K_LEFT: 161. playerMoveTo = LEFT 162. elif event.key == K_RIGHT: 163. playerMoveTo = RIGHT 164. elif event.key == K_UP: 165. playerMoveTo = UP 166. elif event.key == K_DOWN: 167. playerMoveTo = DOWN 168. 169. # 设定镜头移动模式. 170. elif event.key == K_a: 171. cameraLeft = True 172. elif event.key == K_d: 173. cameraRight = True 174. elif event.key == K_w: 175. cameraUp = True 176. elif event.key == K_s: 177. cameraDown = True 178. 179. elif event.key == K_n: 180. return 'next' 181. elif event.key == K_b: 182. return 'back' 183. 184. elif event.key == K_ESCAPE: 185. terminate() # ESC = 关闭. 186. elif event.key == K_BACKSPACE: 187. return 'reset' # 重启关卡. 188. elif event.key == K_p: 189. # 更改为下一个角色图片 190. currentImage += 1 191. if currentImage >= len(PLAYERIMAGES): 192. # 在使用最后一个玩家图片时选择更换将返回第一张 193. currentImage = 0 194. mapNeedsRedraw = True 195. 196. elif event.type == KEYUP: 197. # 重设镜头移动模式 198. if event.key == K_a: 199. cameraLeft = False 200. elif event.key == K_d: 201. cameraRight = False 202. elif event.key == K_w: 203. cameraUp = False 204. elif event.key == K_s: 205. cameraDown = False 这段代码将处理多个按键一起按下的情况 207. if playerMoveTo != None and not levelIsComplete: 208. # 随着玩家按键而移动 209. # 推动所有可推动的星星. 210. moved = makeMove(mapObj, gameStateObj, playerMoveTo) 211. 212. if moved: 213. # 记录移动步数. 214. gameStateObj['stepCounter'] += 1 215. mapNeedsRedraw = True 216. 217. if isLevelFinished(levelObj, gameStateObj): 218. # 当关卡通关时显示solve画面 219. levelIsComplete = True 220. keyPressed = False 如果playerMoveTo变量没有设置为None,那么我们就能知道玩家想要移动。这时调用函数makeMove()来改变玩家所控制的角色在gameStateObj中的XY坐标,包括推动星星。 而makeMove() 的反馈值将储存在moved中。如果这个值是True,那么玩家的角色将会向所按方向键移动,反之则玩家移动的方向有障碍物。 ![]() (推星星第二关) 222. DISPLAYSURF.fill(BGCOLOR) 223. 224. if mapNeedsRedraw: 225. mapSurf = drawMap(mapObj, gameStateObj, levelObj['goals']) 226. mapNeedsRedraw = False 地图不需要通过游戏循环而重新绘制。事实上,这个重复绘制程序很复杂,而且这么做也会让游戏轻微的变慢(但是很明显)。当地图中的东西发生改变的时候,地图在这时候才需要重新绘制(玩家移动或者星星被推动)。 所以当mapNeedsRedraw变量设置为True时,mapSurf变量中的表层对象只能通过调用drawMap()函数来更新 225行,当地图被绘制后,mapNeedsRedraw变量设置为Flase。如果你想要查看通过游戏循环儿重新绘制地图导致的程序缓慢,标注行226,并且重新运行程序。 228. if cameraUp and cameraOffsetY <> 229. cameraOffsetY += CAM_MOVE_SPEED 230. elif cameraDown and cameraOffsetY > -MAX_CAM_X_PAN: 231. cameraOffsetY -= CAM_MOVE_SPEED 232. if cameraLeft and cameraOffsetX <> 233. cameraOffsetX += CAM_MOVE_SPEED 234. elif cameraRight and cameraOffsetX > -MAX_CAM_Y_PAN: 235. cameraOffsetX -= CAM_MOVE_SPEED 如果镜头移动变量设置为True,镜头没有通过(如平移通过)由MAX_CAM_X_PAN and MAX_CAM_Y_PAN,设置的边界,那么镜头的位置(被储存在cameraOffsetX and和cameraOffsetY)应该移动CAM_MOVE_SPEED像素。 请注意,在行228中有if 和 elif的说明,在行230中有镜头的上升和下降,,在行232和行234中有区分if 和elif的说明。 这样,使用者可以在同一时间控制控制镜头的垂直与水平的方向。如果行232是elif说明,那么控制镜头的方向就不可能实现了。 237. # 将mapSurf对象基于摄像头设置 238. mapSurfRect = mapSurf.get_rect() 239. mapSurfRect.center = (HALF_WINWIDTH + cameraOffsetX, HALF_WINHEIGHT + cameraOffsetY) 240. 241. # 将mapSurf置入DISPLAYSURF表层对象 242. DISPLAYSURF.blit(mapSurf, mapSurfRect) 243. 244. DISPLAYSURF.blit(levelSurf, levelRect) 245. stepSurf = BASICFONT.render('Steps: %s' % (gameStateObj['stepCounter']), 1, TEXTCOLOR) 246. stepRect = stepSurf.get_rect() 247. stepRect.bottomleft = (20, WINHEIGHT - 10) 248. DISPLAYSURF.blit(stepSurf, stepRect) 249. 250. if levelIsComplete: 251. # 过关显示SOLVED 252. # 直到按下任意键 253. solvedRect = IMAGESDICT['solved'].get_rect() 254. solvedRect.center = (HALF_WINWIDTH, HALF_WINHEIGHT) 255. DISPLAYSURF.blit(IMAGESDICT['solved'], solvedRect) 256. 257. if keyPressed: 258. return 'solved' 259. 260. pygame.display.update() # 将DISPLAYSURF置入屏幕 261. FPSCLOCK.tick() 262. 263. 行237至261,定位镜头,在DISPLAYSURF的显示表层对象中绘制地图和其他图形。如果游戏的,关卡过关,那么游戏胜利的图案会在任何图像之上。如果使用者在迭代期间按下键,keyPressed变量被设置为True,此时runLevel()函数会返回。 264. def isWall(mapObj, x, y): 265. '''反馈True如果在地图上的坐标是墙 266. 反之False''' 267. if x < 0="" or="" x="">= len(mapObj) or y < 0="" or="" y="">= len(mapObj[x]): 268. return False # XY坐标不再地图上 269. elif mapObj[x][y] in ('#', 'x'): 270. return True # 被墙挡住 271. return False 如果传递函数的XY坐标上的地图对象上又障碍物,则isWall()函数返回True。障碍物对象在地图对象中表示为“x”或“#”字符串。 274. def decorateMap(mapObj, startxy): 275. '''复制并更改地图对象 276. 下面是步骤: 277. * 形成角落的墙转换为角落墙 278. * 区分内外方块 279. * 装饰随机添加给外部方块 280. 281. 返回地图对象''' 282. 283. startx, starty = startxy # 语法糖 284. 285. # 复制地图对象这样我们就不用更改代码了 286. mapObjCopy = copy.deepcopy(mapObj) decorateMap()函数更改了mapObj的数据结构所以它在显示时不会像在地图文件里一样。而decorateMap()更改的三个对象在顶部的评论里已经解释了 ![]() ![]() ![]() 288. # 将不是墙的字符从数据中移除 289. for x in range(len(mapObjCopy)): 290. for y in range(len(mapObjCopy[0])): 291. if mapObjCopy[x][y] in ('$', '.', '@', '+', '*'): 292. mapObjCopy[x][y] = ' ' 地图对象是有性质的,是表示玩家,目标,和星星的位置的。对于地图对象这事非常重要的(当地图数据被阅读之后,会被存储到数据结构中去),所以他们转换为空白空间。 294. # Flood fill to determine inside/outside floor tiles. 295. floodFill(mapObjCopy, startx, starty, ' ', 'o') floodFill()函数会将方格中的障碍物从' '形状改成'o'的形状。它使用一个称为递归的编程概念。 ![]() 好啦!今天就到这里! 推星星的游戏编程代码有点复杂, 今天完成了一半, 请大家不要半途而废, 只有坚持不懈, 才会得到意想不到的收获。 明天我们就能完成整个游戏啦! **注:文中代码的行数如中断,则默认该行为空 如果想系统的学习Python,欢迎大家报名码趣学院的编程基础课,孩子在学习如何制作游戏和应用的同时,能体会到编程的无限乐趣。 |
|