前些天发现没有博主用Pygame实现比较漂亮的2048游戏,于是就自己做了一版。功能及特色包括:全套经典配色和数字块图片、记分牌(分数和历史最高分)以及移动动画效果。效果图如下:
本文全套代码和资源在这里付费获取:2048游戏源码(Pygame仿真带移动动画)「恰饭需要,恳请各位支持!博主必定尽力为大家奉上最详尽的讲解文章。」
总目录
主函数
我把大部分功能的实现都封装为函数了,所以主函数里的内容还是比较简洁的:
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 = 600
WINDOW_HEIGHT = 750
GAMEBOX_LEFT = 50
GAMEBOX_TOP = 200
GAMEBOX_WIDTH = 500
UNIT_WIDTH = GAMEBOX_WIDTH/33
BOLCK_WIDTH = UNIT_WIDTH*7
SCOREBOARD_WIDTH = 120
SCOREBOARD_HEIGHT = 55
# 字体大小
TITLE_SIZE = 60
SCORETITLE_SIZE = 14
SCORE_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)