Python 2048游戏代码实现(Pygame仿真带移动动画)
前些天发现没有博主用Pygame实现比较漂亮的2048游戏,于是就自己做了一版。功能及特色包括:全套经典配色和数字块图片、记分牌(分数和历史最高分)以及移动动画效果。效果图如下:
本文全套代码和资源在这里付费获取:2048游戏源码(Pygame仿真带移动动画)「恰饭需要,恳请各位支持!博主必定尽力为大家奉上最详尽的讲解文章。」
总目录
-
- 主函数
- 载入图片:load_images()
- 读入最高分:read_best()
- 初始化地图:load_images()
- 随机加入块:add_randnum(map)
- 绘制窗口:draw_all(screen, map, images)
-
- 绘制背景
- 绘制标题栏
- 绘制数字块
- 移动函数
-
- 向上移动:move_up(screen, map, images)
- 向下移动:move_down(screen, map, images)
- 向左移动:move_left(screen, map, images)
- 向右移动:move_right(screen, map, images)
- 附录:程序开头定义的常量
主函数
我把大部分功能的实现都封装为函数了,所以主函数里的内容还是比较简洁的:
def main():# 初始化部分 global best_score pygame.init() #初始化pyamge库 images = load_images() #载入数字块图片 best_score = read_best()#读入历史最高分数 screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) #新建窗口对象 pygame.display.set_caption('2048') #设置窗口标题 map = init_map()#初始化游戏地图二维列表 map = add_randnum(map) #随机加入一个新数字块 draw_all(screen, map, images) #绘制窗口 # 主循环部分 while True: for event in pygame.event.get(): # 获取事件(鼠标、键盘等) if event.type == pygame.QUIT: # 如果按下关闭按钮 pygame.quit() # 退出pygame窗口 sys.exit() # 结束主循环 if event.type == pygame.KEYDOWN: #鼠标事件 if event.key == pygame.K_UP or event.key == pygame.K_w: #按下向上键或w键 map = move_up(screen, map, images) #获取移动后的地图,函数中完成移动动画效果(下同) if event.key == pygame.K_DOWN or event.key == pygame.K_s: #按下向下键或s键 map = move_down(screen, map, images) if event.key == pygame.K_LEFT or event.key == pygame.K_a: #按下向左键或a键 map = move_left(screen, map, images) if event.key == pygame.K_RIGHT or event.key == pygame.K_d: #按下向右键或d键 map = move_right(screen, map, images)
下面我把主程序里面用到的函数按顺序盘点一遍,方便大家参考借鉴。
载入图片:load_images()
将数字块图片都放在 block\
文件夹下,图片 n.png
表示的是 2n 的数字块:
将图片用 pygame.image.load()
函数载入后存入列表,再使用 pygame.transform.scale()
函数调整图片大小以适应游戏界面。然后返回图片列表。
def load_images(): images = [0] for i in range(1, 16): images.append(pygame.image.load("block/"+str(i)+".png")) images[i] = pygame.transform.scale(images[i], (int(BOLCK_WIDTH), int(BOLCK_WIDTH))) return images
读入最高分:read_best()
将最高分数存储在当前目录下的 best
文件,在载入游戏时读取。
def read_best(): f = open('best', 'r') score = int(f.read()) return score
初始化地图:load_images()
首先需要放出我定义的数字块类 numBlock
(因为平常没怎么用过类与对象,所以可能这个类抽象得有点生涩,欢迎大家多多指教):
class numBlock: #数字块类 def __init__(self): self.num = 0 #数字 self.rect = pygame.Rect(0, 0, BOLCK_WIDTH, BOLCK_WIDTH)#所在矩形 def set_position(self, x, y): #根据数组下标设置矩形位置 self.rect.x = GAMEBOX_LEFT+UNIT_WIDTH*(8*x+1) self.rect.y = GAMEBOX_TOP+UNIT_WIDTH*(8*y+1)
这个类里面有两个属性:数字块的数字和所在矩形。矩形使用的是 pygame 的 Rect
类,可以表示矩形的左上坐标和长宽。
另外还有一个函数 set_position(x, y)
,可以根据数字块的行列坐标将其移动到对应位置。
而 load_images()
函数就是用 numBlock
类生成了一个二维列表用来表示游戏地图:
def init_map(): map = [] for i in range(4): line = [] for j in range(4): line.append(numBlock()) map.append(line) return map
随机加入块:add_randnum(map)
随机选取地图上一个没有数字的位置,填入数字 1 或 2。需要注意,在我的数组里,n 表示的是 2n ,即随机加入的数字块是 2 或者 4。
def add_randnum(map): x = random.randint(0, 3) y = random.randint(0, 3) while map[y][x].num != 0: x = random.randint(0, 3) y = random.randint(0, 3) map[y][x].num = random.randint(1, 2) map[y][x].set_position(x, y) return map
绘制窗口:draw_all(screen, map, images)
我在这个函数里封装了三部分的绘制。首先先给出这个函数的定义:
def draw_all(screen, map, images): draw_bg(screen) draw_titlebar(screen) draw_blocks(screen, map, images) pygame.display.update()
看函数名就可以得知,这三部分分别是:绘制背景、绘制标题栏 和 绘制数字块。绘制完成后调用 pygame.display.update()
刷新窗口。
绘制背景
先填充背景色为浅黄色,然后绘制褐色游戏底盘,最后绘制八个浅褐色的数字框。相关颜色常量和位置常量的定义放在文章的最后。
def draw_bg(screen): screen.fill(YELLOW_LIGHT) gamebox_rect = pygame.Rect(GAMEBOX_LEFT, GAMEBOX_TOP, GAMEBOX_WIDTH, GAMEBOX_WIDTH) pygame.draw.rect(screen, BROWN_NORMAL, gamebox_rect, 0, 5) for i in range(4): for j in range(4): block_rect = pygame.Rect(GAMEBOX_LEFT+UNIT_WIDTH*(8*i+1), GAMEBOX_TOP+UNIT_WIDTH*(8*j+1), BOLCK_WIDTH, BOLCK_WIDTH) pygame.draw.rect(screen, BROWN_LIGHT, block_rect, 0, 5)
绘制标题栏
标题栏包括:2048 logo、记分牌底板、记分牌标题 和 记分牌数字。
def draw_titlebar(screen): #绘制2048标题 title_font = pygame.font.Font('font/ClearSansBold.woff.ttf', TITLE_SIZE) title_text = title_font.render('2048', True, BROWN_DEEP) screen.blit(title_text, (GAMEBOX_LEFT, GAMEBOX_TOP/2-TITLE_SIZE*3/4)) #绘制记分牌背板 scoreboard_rect = pygame.Rect(WINDOW_WIDTH-GAMEBOX_LEFT-SCOREBOARD_WIDTH*15/7,GAMEBOX_TOP/2-SCOREBOARD_HEIGHT*3/4, SCOREBOARD_WIDTH, SCOREBOARD_HEIGHT) pygame.draw.rect(screen, BROWN_NORMAL, scoreboard_rect, 0, 3) bestboard_rect = pygame.Rect(WINDOW_WIDTH - GAMEBOX_LEFT - SCOREBOARD_WIDTH, GAMEBOX_TOP / 2 - SCOREBOARD_HEIGHT * 3 / 4, SCOREBOARD_WIDTH, SCOREBOARD_HEIGHT) pygame.draw.rect(screen, BROWN_NORMAL, bestboard_rect, 0, 3) #绘制记分牌标题文字 scoretitle_font = pygame.font.SysFont('Arial', SCORETITLE_SIZE, True) scoretitle_text = scoretitle_font.render('SCORE', True, BROWN_MORE_LIGHT) scoretitle_rect = scoretitle_text.get_rect() scoretitle_rect.center = (WINDOW_WIDTH-GAMEBOX_LEFT-SCOREBOARD_WIDTH*(15/7-1/2), GAMEBOX_TOP/2-SCOREBOARD_HEIGHT/2) screen.blit(scoretitle_text, scoretitle_rect) besttitle_font = pygame.font.SysFont('Arial', SCORETITLE_SIZE, True) besttitle_text = besttitle_font.render('BEST', True, BROWN_MORE_LIGHT) besttitle_rect = besttitle_text.get_rect() besttitle_rect.center = (WINDOW_WIDTH-GAMEBOX_LEFT-SCOREBOARD_WIDTH/2, GAMEBOX_TOP/2-SCOREBOARD_HEIGHT/2) screen.blit(besttitle_text, besttitle_rect) #绘制分数 global score, best_score score_font = pygame.font.Font('font/ClearSansBold.woff.ttf', SCORE_SIZE) score_text = score_font.render(str(score), True, WHITE) score_rect = score_text.get_rect() score_rect.center = (WINDOW_WIDTH - GAMEBOX_LEFT - SCOREBOARD_WIDTH * (15 / 7 - 1 / 2), GAMEBOX_TOP / 2 - SCOREBOARD_HEIGHT / 10) screen.blit(score_text, score_rect) best_font = pygame.font.Font('font/ClearSansBold.woff.ttf', SCORE_SIZE) best_text = best_font.render(str(best_score), True, WHITE) best_rect = best_text.get_rect() best_rect.center = (WINDOW_WIDTH - GAMEBOX_LEFT - SCOREBOARD_WIDTH / 2, GAMEBOX_TOP / 2 - SCOREBOARD_HEIGHT / 10) screen.blit(best_text, best_rect)
绘制数字块
遍历二维列表,如果数字不为0,则根据元素的 rect
所定义位置绘制出数字块。
def draw_blocks(screen, map, images): for i in range(4): for j in range(4): if map[i][j].num != 0: screen.blit(images[map[i][j].num], (map[i][j].rect.x,map[i][j].rect.y))
移动函数
按下方向键时的执行的程序我封装为了四个函数,这四个函数大概是这个游戏的代码的精髓吧。这是我根据自己的思路写的,如有问题请大家不吝指正。
下面仅以 move_up() 函数为例来讲解实现原理。其他三个方向的代码类似,只贴出完整代码,不做讲解。
向上移动:move_up(screen, map, images)
先贴上完整代码,然后再分块说明:
def move_up(screen, map, images): global score, best_score pre_map = copy.deepcopy(map) map = copy.deepcopy(map) move_step = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] for j in range(4): k = 0 pre_num = numBlock() for i in range(4): if map[i][j].num == 0: k += 1 else: if map[i][j].num == pre_num.num: pre_num.num += 1 score += pow(2, pre_num.num) if best_score < score: best_score = score write_best(score) map[i][j] = numBlock() k += 1 move_step[i][j] = k else: t = map[i][j] map[i][j] = numBlock() map[i - k][j] = t map[i - k][j].set_position(j, i - k) move_step[i][j] = k pre_num = map[i - k][j] is_moving = True k = 0 while is_moving: is_moving = False k += 1 for i in range(4): for j in range(4): if move_step[i][j] > 0.1: is_moving = True move_step[i][j] -= 1/10 pre_map[i][j].set_position(j, i-k*1/10) draw_all(screen, pre_map, images) if not equal_map(map, pre_map): add_randnum(map) draw_all(screen, map, images) return map
在这段程序的开头先引入全局变量 score
和 best_score
,因为每次移动完都需要计分。然后将地图用深拷贝 copy.deepcopy()
复制两份进行后续操作。初始化列表 move_step
用于存储每个数字块将要移动的格数,这个数组是为移动动画准备的。
global score, best_score pre_map = copy.deepcopy(map) map = copy.deepcopy(map) move_step = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
这里是游戏规则的核心算法,用到一个双重循环来模拟获得出移动后的新地图。对于每一列,从上到下遍历这一列,用变量k
表示当前数字块需要上移几格,然后上移。
过程中判断如果当前数字与前一个数字相同,则删除该块并给上一块升级,即合块。
当然,计分也是在这里完成的。每次合块后,增加的分数为合块后的数字。
for j in range(4): k = 0 pre_num = numBlock() for i in range(4): if map[i][j].num == 0: k += 1 else: if map[i][j].num == pre_num.num: pre_num.num += 1 score += pow(2, pre_num.num) if best_score < score: best_score = score write_best(score) map[i][j] = numBlock() k += 1 move_step[i][j] = k else: t = map[i][j] map[i][j] = numBlock() map[i - k][j] = t map[i - k][j].set_position(j, i - k) move_step[i][j] = k pre_num = map[i - k][j]
接着是完成移动动画,循环小步移动需要移动的数字块,每移动一小步使用之前定义的 draw_all()
函数刷新窗口,直至移动完成。
is_moving = True k = 0 while is_moving: is_moving = False k += 1 for i in range(4): for j in range(4): if move_step[i][j] > 0.1: is_moving = True move_step[i][j] -= 1/10 pre_map[i][j].set_position(j, i-k*1/10) draw_all(screen, pre_map, images)
最后,判断移动地图是否相等,即移动前的地图是否可以移动。如果没有移动则不用增加新的随机数字块。
if not equal_map(map, pre_map): add_randnum(map)
这里的 equal_map(map, pre_map)
函数定义如下:
def equal_map(map1, map2): equal = True for i in range(4): for j in range(4): if map1[i][j].num != map2[i][j].num: equal = False break return equal
向下移动:move_down(screen, map, images)
def move_down(screen, map, images): global score, best_score pre_map = copy.deepcopy(map) map = copy.deepcopy(map) move_step = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] for j in range(4): k = 0 pre_num = numBlock() for i in range(3, -1, -1): if map[i][j].num == 0: k += 1 else: if map[i][j].num == pre_num.num: pre_num.num += 1 score += pow(2, pre_num.num) if best_score < score: best_score = score write_best(score) map[i][j] = numBlock() k += 1 move_step[i][j] = k else: t = map[i][j] map[i][j] = numBlock() map[i + k][j] = t map[i + k][j].set_position(j, i + k) move_step[i][j] = k pre_num = map[i + k][j] is_moving = True k = 0 while is_moving: is_moving = False k += 1 for i in range(4): for j in range(4): if move_step[i][j] > 0.1: is_moving = True move_step[i][j] -= 1/10 pre_map[i][j].set_position(j, i+k*1/10) draw_all(screen, pre_map, images) if not equal_map(map, pre_map): add_randnum(map) draw_all(screen, map, images) return map
向左移动:move_left(screen, map, images)
def move_left(screen, map, images): global score, best_score pre_map = copy.deepcopy(map) map = copy.deepcopy(map) move_step = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] for i in range(4): k = 0 pre_num = numBlock() for j in range(4): if map[i][j].num == 0: k += 1 else: if map[i][j].num == pre_num.num: pre_num.num += 1 score += pow(2, pre_num.num) if best_score < score: best_score = score write_best(score) map[i][j] = numBlock() k += 1 move_step[i][j] = k else: t = map[i][j] map[i][j] = numBlock() map[i][j - k] = t map[i][j - k].set_position(j - k, i) move_step[i][j] = k pre_num = map[i][j - k] is_moving = True k = 0 while is_moving: is_moving = False k += 1 for i in range(4): for j in range(4): if move_step[i][j] > 0.1: is_moving = True move_step[i][j] -= 1/10 pre_map[i][j].set_position(j-k*1/10, i) draw_all(screen, pre_map, images) if not equal_map(map, pre_map): add_randnum(map) draw_all(screen, map, images) return map
向右移动:move_right(screen, map, images)
def move_right(screen, map, images): global score, best_score pre_map = copy.deepcopy(map) map = copy.deepcopy(map) move_step = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] for i in range(4): k = 0 pre_num = numBlock() for j in range(3, -1, -1): if map[i][j].num == 0: k += 1 else: if map[i][j].num == pre_num.num: pre_num.num += 1 score += pow(2, pre_num.num) if best_score < score: best_score = score write_best(score) map[i][j] = numBlock() k += 1 move_step[i][j] = k else: t = map[i][j] map[i][j] = numBlock() map[i][j + k] = t map[i][j + k].set_position(j + k, i) move_step[i][j] = k pre_num = map[i][j + k] is_moving = True k = 0 while is_moving: is_moving = False k += 1 for i in range(4): for j in range(4): if move_step[i][j] > 0.1: is_moving = True move_step[i][j] -= 1/10 pre_map[i][j].set_position(j+k*1/10, i) draw_all(screen, pre_map, images) if not equal_map(map, pre_map): add_randnum(map) draw_all(screen, map, images) return map
附录:程序开头定义的常量
# 位置常量WINDOW_WIDTH = 600WINDOW_HEIGHT = 750GAMEBOX_LEFT = 50GAMEBOX_TOP = 200GAMEBOX_WIDTH = 500UNIT_WIDTH = GAMEBOX_WIDTH/33BOLCK_WIDTH = UNIT_WIDTH*7SCOREBOARD_WIDTH = 120SCOREBOARD_HEIGHT = 55# 字体大小TITLE_SIZE = 60SCORETITLE_SIZE = 14SCORE_SIZE = 24# 颜色常量BROWN_DEEP = (119, 110, 101)BROWN_NORMAL = (187, 173, 160)BROWN_LIGHT = (205, 193, 180)BROWN_MORE_LIGHT = (238, 228, 218)YELLOW_LIGHT = (250, 248, 239)WHITE = (255, 255, 255)