一、前言
中秋佳节又快到了,这是中国传统文化中非常重要的一个节日。在这个团圆的日子里,家人团聚,共享月光、赏悦月色,感受中秋的浪漫与温馨。
在此特别推出一个中秋版的推箱子小游戏 - 《兔子推着月饼上月球》。游戏以可爱的中秋元素为主题,控制小兔子挑战十关推箱子关卡,最终把月饼送到月亮指定位置,完成月球之旅。
通过这个小游戏,希望大家在度过一个温馨欢乐的中秋佳节的同时,也感受到一点古老的中秋文化的魅力。以下就让我们开始这趟奇妙的中秋之旅吧!
提前祝大家中秋快乐!愿这个小游戏能让你感受一点中秋的乐趣与魅力。
二、准备素材
我是在阿里巴巴的iconfont寻找一些素材
游戏图片
-
墙体:灯笼🏮、烟花🎆
-
角色:兔子🐰、嫦娥
-
箱子:月饼🥮、甜甜圈🍩
-
目的地:月球🌕
-
空:烟花🎆、星星🌟
游戏背景音乐
-
卡农
-
龙珠
三、编写代码
编码环境
环境名称 | 版本 |
---|---|
python | 3.10 |
pygame | 2.5.1 |
安装依赖
pip install pygame
准备游戏地图
python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 游戏地图 }
# @Date: 2023/09/13 12:10
from typing import Dict, List
EMPTY_FLAG = 0 # 表示空
WALL_FLAG = 1 # 表示墙
BOX_FLAG = 2 # 表示月饼
PLAYER_FLAG = 3 # 表示兔子
DEST_FLAG = 4 # 表示目的地 (月亮)
FINISH_BOX_FLAG = 5 # 表示已完成的箱子
PLAYER_DEST_FLAG = 6 # 表示角色与目的地重合
BOX_DEST_FLAG = 7 # 表示箱子与目的地重合
BG_FLAG = 9 # 表示墙外面 其他
# 游戏地图 level => 二维列表地图
GAME_MAP: Dict[int, List[List[int]]] = {
1: [
[9, 9, 1, 1, 1, 9, 9, 9],
[9, 9, 1, 4, 1, 9, 9, 9],
[9, 9, 1, 0, 1, 1, 1, 1],
[1, 1, 1, 2, 0, 2, 4, 1],
[1, 4, 0, 2, 3, 1, 1, 1],
[1, 1, 1, 1, 2, 1, 9, 9],
[9, 9, 9, 1, 4, 1, 9, 9],
[9, 9, 9, 1, 1, 1, 9, 9]
],
2: [
[9, 9, 1, 1, 1, 1, 9, 9],
[9, 9, 1, 4, 4, 1, 9, 9],
[9, 1, 1, 0, 4, 1, 1, 9],
[9, 1, 0, 0, 2, 4, 1, 9],
[1, 1, 0, 2, 3, 0, 1, 1],
[1, 0, 0, 1, 2, 2, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
],
3: [
[9, 9, 1, 1, 1, 1, 9, 9],
[9, 1, 1, 0, 0, 1, 9, 9],
[9, 1, 3, 2, 0, 1, 9, 9],
[9, 1, 1, 2, 0, 1, 1, 9],
[9, 1, 1, 0, 2, 0, 1, 9],
[9, 1, 4, 2, 0, 0, 1, 9],
[9, 1, 4, 4, 6, 4, 1, 9],
[9, 1, 1, 1, 1, 1, 1, 9]
]
]
}
文章里就准备了三张地图,因为太占行数了,就不全部贴出来了。通过一个字典来当作推箱子的游戏地图,key 是游戏的关卡,value 为二维列表当做游戏地图。里面的数字代表游戏素材。
- 0 表示空
- 1 表示墙
- 2 表示月饼
- 3 表示兔子
- 4 表示目的地 (月亮)
- 5 表示已完成的箱子
- 6 表示角色与目的地重合
- 7 表示箱子与目的地重合
这个地图是推箱子的精髓,根据数字标识、坐标(x,y),来画游戏关卡地图非常方便,上下左右控制兔子🐰移动,也只需改变地图里数字标识然后重新绘制就能达到移动的效果。
pygame 渲染绘制
地图准备好了就可以通过pygame直接画了
python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 游戏入口模块 }
# @Date: 2023/09/13 12:20
import sys
import pygame
from pygame import Surface
from src.game_map import GAME_MAP, WALL_FLAG, RABBIT_FLAG, MOON_CAKE_FLAG, TERMINAL_FLAG
# 定义颜色 rgb
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
class RabbitBox(object):
GRID_SIZE = 64 # 单个方块大小
GAME_TITLE = "🐰兔子推着月饼🥮上月球🌕"
def __init__(self, game_level: int = 1):
self._init_game()
self.game_screen: Surface = None
self.game_level = min(game_level, len(GAME_MAP))
self.rabbit_pos: tuple = None
self.setup_game_screen()
def _init_game(self):
pygame.init()
pygame.display.set_caption(self.GAME_TITLE)
def setup_game_screen(self):
"""根据游戏地图配置游戏屏幕"""
rows = GAME_MAP[self.game_level]
row_num = len(rows)
col_num = len(rows[0])
self.game_screen = pygame.display.set_mode(size=[self.GRID_SIZE * row_num, self.GRID_SIZE * col_num])
def draw_map(self):
"""遍历地图数据绘制"""
# 获取游戏地图
game_map = GAME_MAP[self.game_level]
for y in range(len(game_map)):
for x in range(len(game_map[y])):
if game_map[y][x] == WALL_FLAG:
# 画墙
pygame.draw.rect(
surface=self.game_screen,
color=BLACK,
rect=[x * self.GRID_SIZE, y * self.GRID_SIZE, self.GRID_SIZE, self.GRID_SIZE]
)
elif game_map[y][x] == RABBIT_FLAG:
# 画兔子
pygame.draw.rect(
surface=self.game_screen, # 游戏屏幕
color=RED, # 颜色
rect=[
x * self.GRID_SIZE, # x 坐标
y * self.GRID_SIZE, # y 坐标
self.GRID_SIZE, # 宽 width
self.GRID_SIZE, # 高 height
]
)
self.rabbit_pos = (x, y) # 标识兔子坐标
elif game_map[y][x] == MOON_CAKE_FLAG:
# 画月饼
pygame.draw.circle(
surface=self.game_screen,
color=WHITE,
center=[x * self.GRID_SIZE + self.GRID_SIZE / 2, y * self.GRID_SIZE + self.GRID_SIZE / 2],
radius=self.GRID_SIZE / 3
)
elif game_map[y][x] == TERMINAL_FLAG:
# 画月饼的目的地(月球)
pygame.draw.circle(
surface=self.game_screen,
color=WHITE,
center=[x * self.GRID_SIZE + self.GRID_SIZE / 2, y * self.GRID_SIZE + self.GRID_SIZE / 2],
radius=self.GRID_SIZE / 2
)
def run_game(self):
# 主循环事件监听与渲染
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
# 监听键盘事件,上下左右控制兔子,记录方向
if event.key == pygame.K_UP:
pass
elif event.key == pygame.K_DOWN:
pass
elif event.key == pygame.K_LEFT:
pass
elif event.key == pygame.K_RIGHT:
pass
self.game_screen.fill(BLACK)
self.draw_map()
pygame.display.flip()
def main():
RabbitBox().run_game()
if __name__ == '__main__':
main()
-
pygame.init() 先初始化好pygame 游戏环境
-
RabbitBox() 初始化一些游戏属性
- 关卡
- 游戏屏幕screen
- 兔子坐标
-
pygame 游戏渲染 事件循环
-
绘制游戏屏幕
self.game_screen.fill(BLACK)
-
绘制游戏地图
self.draw_map()
-
事件处理
-
游戏刷新展示
pygame.display.flip()
-
当前绘制的一些矩形、圆等图形不是游戏素材图片,先大概看看地图的样子
添加游戏素材图片
python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author: Hui
# @Desc: { 游戏入口模块 }
# @Date: 2023/09/13 12:20
import os
import sys
import random
import pygame
from pygame import Surface
from src.game_map import GAME_MAP, WALL_FLAG, PLAYER_FLAG, BOX_FLAG, TERMINAL_FLAG, EMPTY_FLAG
# 定义颜色 rgb
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
class RabbitBox(object):
GRID_SIZE = 64 # 单个方块大小
GAME_TITLE = "🐰兔子推着月饼🥮上月球🌕"
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WALLS = [
pygame.image.load(os.path.join(BASE_DIR, "res/img/lantern.png"))
]
PLAYERS = [
pygame.image.load(os.path.join(BASE_DIR, "res/img/rabbit.png"))
]
BOXS = [
pygame.image.load(os.path.join(BASE_DIR, "res/img/moon_cake.png"))
]
TERMINAL_BOXS = [
pygame.image.load(os.path.join(BASE_DIR, "res/img/moon_02.png"))
]
EMPTY_BOXS = [
pygame.image.load(os.path.join(BASE_DIR, "res/img/fireworks.png")),
pygame.image.load(os.path.join(BASE_DIR, "res/img/fireworks_02.png")),
pygame.image.load(os.path.join(BASE_DIR, "res/img/star.png")),
]
def __init__(self, game_level: int = 1):
self._init_game()
self.game_screen: Surface = None
self.game_level = min(game_level, len(GAME_MAP))
self.player_pos: tuple = None
self.box: Surface = None
self.player: Surface = None
self.wall: Surface = None
self.terminal_box: Surface = None
self.finished_box: Surface = None
self.empty_box: Surface = None
self.setup_game_screen()
self.random_game_material()
def random_game_material(self):
"""随机游戏素材"""
self.wall = random.choice(self.WALLS)
self.player = random.choice(self.PLAYERS)
self.box = random.choice(self.BOXS)
self.terminal_box = random.choice(self.TERMINAL_BOXS)
self.empty_box = random.choice(self.EMPTY_BOXS)
def _init_game(self):
pygame.init()
pygame.display.set_caption(self.GAME_TITLE)
def setup_game_screen(self):
"""根据游戏地图配置游戏屏幕"""
rows = GAME_MAP[self.game_level]
row_num = len(rows)
col_num = len(rows[0])
self.game_screen = pygame.display.set_mode(size=[self.GRID_SIZE * row_num, self.GRID_SIZE * col_num])
def draw_map(self):
"""遍历地图数据绘制"""
# 获取游戏地图
game_map = GAME_MAP[self.game_level]
for i, row in enumerate(game_map):
for j, col in enumerate(row):
# 计算偏移坐标
offset_x = j * self.GRID_SIZE
offset_y = i * self.GRID_SIZE
# 判断标识
num_flag = game_map[i][j]
if num_flag == WALL_FLAG:
# 画墙 (灯笼)
self.game_screen.blit(source=self.wall, dest=(offset_x, offset_y))
elif num_flag == PLAYER_FLAG:
# 画角色(兔子)
self.game_screen.blit(source=self.player, dest=(offset_x, offset_y))
self.player_pos = (i, j) # 标识兔子坐标
elif num_flag == BOX_FLAG:
# 画箱子(月饼)
self.game_screen.blit(source=self.box, dest=(offset_x, offset_y))
elif num_flag == TERMINAL_FLAG:
# 画月饼的目的地(月球)
self.game_screen.blit(source=self.terminal_box, dest=(offset_x, offset_y))
elif num_flag == EMPTY_FLAG:
# 画空(背景)
self.game_screen.blit(source=self.empty_box, dest=(offset_x, offset_y))
return self.player_pos
def run_game(self):
# 主循环事件监听与渲染
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
self.game_screen.fill(BLACK)
self.draw_map()
pygame.display.flip()
def main():
RabbitBox().run_game()
if __name__ == '__main__':
main()
-
pygame.image.load(os.path.join(BASE_DIR, "res/img/rabbit.png"))
加载游戏图片 -
self.game_screen.blit(source=self.player, dest=(x, y))
绘制在游戏屏幕上- x, y 图片坐标,通过二维列表的位置 与 图片的大小进行计算
-
self.player = random.choice(self.PLAYERS)
随机游戏素材- 后面可以通过调节列表大小来控制出现的概率
事件处理
地图已经绘制完毕,接下来就是兔子移动事件监听与处理。
python
def move_up(self):
"""
玩家向上移动处理
"""
print("move_up")
i, j = self.player_pos
map_list = GAME_MAP[self.game_level]
def handle_player_dest():
"""人和目的地重合处理"""
if map_list[i][j] == PLAYER_DEST_FLAG:
# 当前位置是人和目的地重合处理
map_list[i][j] = DEST_FLAG # 把原来角色位置改成目的地
else:
map_list[i][j] = EMPTY_FLAG # 把原来角色位置改成空白
if map_list[i - 1][j] == BOX_FLAG and \
(map_list[i - 2][j] == EMPTY_FLAG or map_list[i - 2][j] == DEST_FLAG):
print('up box')
# 玩家上边(i - 1)是箱子
# 且箱子的上边只能是空白或者目的地才可向上
map_list[i - 1][j] = PLAYER_FLAG # 角色向上移动改变角色位置
# 人和目的地重合判断处理
handle_player_dest()
map_list[i - 2][j] = BOX_FLAG # 把箱子向上移改变位置
elif map_list[i - 1][j] == EMPTY_FLAG:
# 玩家上边(i - 1)是空白
print('up empty')
map_list[i - 1][j] = PLAYER_FLAG # 角色向上移动改变角色位置
handle_player_dest()
elif map_list[i - 1][j] == DEST_FLAG:
# 玩家上边是目的地
print('up destination')
map_list[i - 1][j] = PLAYER_DEST_FLAG # 让角色和目的地重合
handle_player_dest()
def _event_handle(self):
"""事件处理"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
key_pressed = pygame.key.get_pressed()
if key_pressed[K_a] or key_pressed[K_LEFT]:
# 玩家向左移动
self.move_lef()
elif key_pressed[K_d] or key_pressed[K_RIGHT]:
# 玩家向右移动
self.move_right()
elif key_pressed[K_w] or key_pressed[K_UP]:
# 玩家向上移动
self.move_up()
elif key_pressed[K_s] or key_pressed[K_DOWN]:
# 玩家上下移动
self.move_down()
def run_game(self):
# 主循环事件监听与渲染
while True:
# 设置游戏刷新帧率
pygame.time.Clock().tick(self.game_fps)
# 绘制地图
self.game_screen.fill(BLACK)
self.draw_map()
# 事件处理
self._event_handle()
pygame.display.flip()
移动逻辑其实都是相似的,这里以向上移动进行讲解下
-
玩家上方是箱子,箱子上方是空白或目的地
- 角色上移
map_list[i - 1][j] = PLAYER_FLAG
- 如果当前玩家与目的地重合要还原目的地,不是则是置为空白就行
- 箱子上移
map_list[i - 2][j] = BOX_FLAG
- 角色上移
-
玩家上方是空白
- 玩家与空白交换位置
- 玩家与目的地重合处理
-
玩家上方是目的地
- 让玩家和目的地重合
map_list[i - 1][j] = PLAYER_DEST_FLAG
- 让玩家和目的地重合
有一个特殊一点就是玩家和目的地重合要还原目的地
python
def handle_player_dest():
"""人和目的地重合处理"""
if map_list[i][j] == PLAYER_DEST_FLAG:
# 当前位置是人和目的地重合处理
map_list[i][j] = DEST_FLAG # 把原来角色位置改成目的地
else:
map_list[i][j] = EMPTY_FLAG # 把原来角色位置改成空白
但还是有点小问题就是箱子和目的重合判断没有做,发现上下左右都要做重合处理。看下统一处理下
python
def _player_move_event_handle(self, direction: MoveDirection):
"""
玩家上下左右移动事件处理
Args:
direction: 移动的方向
Returns:
"""
# 记录上下左右待判断的位置
# i,j 玩家原来位置 上下 m,k 左右 n,v
i, j = self.player_pos
m, n = self.player_pos
k, v = self.player_pos
map_list = GAME_MAP[self.game_level]
# 根据不同的移动方向确定判定条件
if direction == MoveDirection.UP: # 向上 (i - 1, j)、(i - 2, j)
m = i - 1
k = i - 2
elif direction == MoveDirection.DOWN: # 向下 (i + 1, j)、(i + 2, j)
m = i + 1
k = i + 2
elif direction == MoveDirection.LEFT: # 向左 (i, j - 1)、(i, j - 2)
n = j - 1
v = j - 2
elif direction == MoveDirection.RIGHT: # 向右 (i, j + 1)、(i, j + 2)
n = j + 1
v = j + 2
def handle_player_dest_coincide():
"""
角色和目的地重合判断处理
"""
if map_list[i][j] == PLAYER_DEST_FLAG:
map_list[i][j] = DEST_FLAG # 是,把原来角色位置改成目的地
else:
map_list[i][j] = EMPTY_FLAG # 不是,把原来角色位置改成空白
# 玩家(上下左右)边是箱子或者箱子和目的地重合
# 且箱子的(上下左右)边只能是空白或者目的地才可向上
if map_list[m][n] in [BOX_FLAG, BOX_DEST_FLAG] and \
map_list[k][v] in [EMPTY_FLAG, DEST_FLAG]:
if map_list[m][n] == BOX_DEST_FLAG: # 如果移动的位置是箱子与目的地的重合
map_list[m][n] = PLAYER_DEST_FLAG # 让角色和目的地重合
else:
map_list[m][n] = PLAYER_FLAG # 角色向上移动改变角色位置
# 判断当前位置是否是角色和目的地重合
handle_player_dest_coincide()
# 判断箱子是否与目的地重合
if map_list[k][v] == DEST_FLAG:
map_list[k][v] = BOX_DEST_FLAG # 标记箱子和目的地重合
else:
map_list[k][v] = BOX_FLAG # 把箱子向上移改变位置
elif map_list[m][n] == EMPTY_FLAG: # 判断(上下左右)边是否空白
map_list[m][n] = PLAYER_FLAG # 角色向上移动改变角色位置
# 判断当前位置是否是角色和目的地重合
handle_player_dest_coincide()
elif map_list[m][n] == DEST_FLAG: # 判断(上下左右)边是否是目的地
map_list[m][n] = PLAYER_DEST_FLAG # 让角色和目的地重合
# 判断当前位置是否是角色和目的地重合
handle_player_dest_coincide()
def _event_handle(self):
"""事件处理"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
key_pressed = pygame.key.get_pressed()
if key_pressed[K_a] or key_pressed[K_LEFT]:
# 玩家向左移动
self._player_move_event_handle(direction=MoveDirection.LEFT)
# self.move_lef()
elif key_pressed[K_d] or key_pressed[K_RIGHT]:
# 玩家向右移动
self._player_move_event_handle(direction=MoveDirection.RIGHT)
# self.move_right()
elif key_pressed[K_w] or key_pressed[K_UP]:
# 玩家向上移动
self._player_move_event_handle(direction=MoveDirection.UP)
# self.move_up()
elif key_pressed[K_s] or key_pressed[K_DOWN]:
# 玩家上下移动
self._player_move_event_handle(direction=MoveDirection.DOWN)
# self.move_down()
这里通用标识移动方向统一处理上下左右逻辑,减少了代码冗余,但阅读没有这么清晰了。
最后就是去除了画格子当背景,而是通过绘制全局的背景图这样更好看些。然后补齐了游戏通关判断。
python
def _handle_game_finish(self):
"""
游戏过关判断处理
"""
exist_dest = False # 标明是否存在目的地
map_list = GAME_MAP[self.game_level]
for row in map_list:
if DEST_FLAG in row or PLAYER_DEST_FLAG in row:
exist_dest = True
# 不存在目的地游戏通关
if not exist_dest:
print('game pass %s' % self.game_level)
self.game_level = self.game_level + 1
self.random_game_material()
if self.game_level > len(GAME_MAP):
self.game_level = 1
print("全部游戏关卡已完成")
四、源代码
大家可以下载源码自己调节自己喜欢的游戏素材,以及配置游戏背景音乐。
也可以继续扩展游戏功能
- 重玩、回退策略
- 游戏移动步数、计分
- 每关使用时间