作为刚接触编程的大一新生,总觉得代码太抽象?试试用 Pygame 做个推箱子游戏吧!这个经典小游戏逻辑清晰、代码量适中,特别适合新手练手。

我花了两天时间从 0 到 1 实现,过程中踩了不少 "新手专属" 的坑(比如箱子推不动、人物穿墙)。这篇文章会按 "问题拆解" 的思路,带你一步步做出能玩的推箱子,最后还会分享几个 "轻量化拓展" 技巧,让你的游戏更有特色。

一、先搞懂:推箱子的核心逻辑是什么?

新手做游戏最容易犯的错是 "一上来就写代码"。先想清楚推箱子的核心要素:

  1. 地图元素

    • 墙壁(不能穿过)
    • 空地(可以走)
    • 箱子(可以被推,只能推到空地或目标点)
    • 目标点(箱子推到这里就算完成)
    • 玩家(可以上下左右移动,能推箱子)
  2. 核心规则

    • 玩家只能推箱子时,玩家只能移动到箱子旁边,再向箱子方向推
    • 箱子只能推到空地上,不能推到墙上
    • 所有箱子推到目标点,游戏胜利

二、准备工作:3 分钟搭好开发环境

1. 安装 Pygame(避坑版)

直接用 pip 安装可能会遇到版本兼容问题,大一新生建议这样做:

bash

# 先确认Python版本(3.8+最佳)
python --version
# 安装指定版本(稳定性好)
pip install pygame==2.1.2

2. 项目结构(别堆成一锅粥)

新手容易把所有代码写在一个文件里,推荐简单分个类:

plaintext

push_box/
├── main.py       # 主程序
└── maps.py       # 地图数据(方便以后加新关卡)

三、从零开始写代码:按 "元素→逻辑→整合" 的顺序来

第一步:定义游戏元素(用类封装更清晰)

1. 先画个游戏窗口(解决 "窗口闪屏" 问题)

刚开始我直接在主循环里画元素,画面总是闪。后来才知道要加个 "双缓冲"—— 用一个临时画布先画好,再一次性显示到屏幕上。

python

运行

import pygame
import sys

# 初始化Pygame
pygame.init()

# 游戏常量(大写区分,方便修改)
CELL_SIZE = 50  # 每个格子的大小(像素)
SCREEN_WIDTH = 10 * CELL_SIZE  # 10列
SCREEN_HEIGHT = 10 * CELL_SIZE  # 10行

# 创建窗口和临时画布(解决闪屏)
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("大一新手的推箱子")
canvas = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))  # 临时画布

# 颜色定义(用RGB值)
COLOR_WALL = (0, 0, 255)    # 蓝色墙壁
COLOR_FLOOR = (220, 220, 220)  # 灰色地板
COLOR_BOX = (139, 69, 19)   # 棕色箱子
COLOR_TARGET = (0, 255, 0)  # 绿色目标点
COLOR_PLAYER = (255, 0, 0)  # 红色玩家
2. 定义玩家类(解决 "人物穿墙" 问题)

玩家移动时必须判断 "下一步是不是墙",刚开始我没加判断,结果人物能直接穿墙。

python

运行

class Player:
    def __init__(self, x, y):
        self.x = x  # 格子坐标(不是像素)
        self.y = y  # 格子坐标(不是像素)

    def move(self, dx, dy, map_data):
        """
        移动玩家
        dx: x方向变化(-1左,1右,0不动)
        dy: y方向变化(-1上,1下,0不动)
        map_data: 地图数据(用于判断是否能移动)
        """
        # 计算新位置
        new_x = self.x + dx
        new_y = self.y + dy
        
        # 1. 先判断新位置是不是墙(map_data[y][x] == 1 表示墙)
        if map_data[new_y][new_x] == 1:
            return False  # 是墙,不能移动
        
        # 2. 如果新位置是箱子,尝试推动箱子
        if map_data[new_y][new_x] == 2:  # 2表示箱子
            # 箱子要移动到的位置
            box_new_x = new_x + dx
            box_new_y = new_y + dy
            # 检查箱子的新位置是否合法(不是墙也不是其他箱子)
            if map_data[box_new_y][box_new_x] in (0, 3):  # 0是空地,3是目标点
                # 推动箱子(更新地图数据)
                map_data[new_y][new_x] = 0 if (map_data[new_y][new_x] == 2) else 3
                map_data[box_new_y][box_new_x] = 2
            else:
                return False  # 箱子推不动
        
        # 3. 移动玩家
        self.x = new_x
        self.y = new_y
        return True
3. 地图处理(用数字代表不同元素)

用数字矩阵表示地图,比直接画图形更灵活。0 = 空地,1 = 墙,2 = 箱子,3 = 目标点,4 = 玩家初始位置。

python

运行

# maps.py
def get_level(level=1):
    """返回指定关卡的地图数据和玩家初始位置"""
    if level == 1:
        # 10x10的地图(行x列)
        map_data = [
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            [1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
            [1, 0, 2, 0, 1, 0, 0, 2, 0, 1],
            [1, 0, 0, 0, 1, 1, 1, 0, 0, 1],
            [1, 1, 0, 0, 0, 2, 0, 0, 1, 1],
            [1, 0, 0, 0, 1, 0, 1, 0, 0, 1],
            [1, 0, 2, 0, 1, 0, 1, 0, 0, 1],
            [1, 1, 1, 0, 1, 0, 1, 1, 0, 1],
            [1, 0, 3, 3, 3, 3, 0, 0, 0, 1],
            [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
        ]
        player_x, player_y = 1, 1  # 玩家初始位置(第2行第2列)
        return map_data, player_x, player_y
    # 可以在这里加更多关卡...

第二步:实现核心游戏逻辑(重点是碰撞检测)

1. 初始化游戏数据

python

运行

from maps import get_level

# 加载第一关
map_data, player_x, player_y = get_level(1)
player = Player(player_x, player_y)

# 计算目标点数量(用于判断胜利)
target_count = sum(row.count(3) for row in map_data)
2. 判断游戏是否胜利(所有箱子都在目标点上)

python

运行

def check_win(map_data, target_count):
    """检查是否所有目标点都有箱子"""
    # 统计"箱子在目标点上"的数量
    box_on_target = 0
    for y in range(len(map_data)):
        for x in range(len(map_data[y])):
            # 箱子(2)在目标点(3)上时,我们把它记为2(推动时会处理)
            # 这里统计所有在目标点上的箱子
            if map_data[y][x] == 2 and original_map[y][x] == 3:
                box_on_target += 1
    return box_on_target == target_count

# 保存原始地图(用于判断箱子是否在目标点上)
original_map = [row.copy() for row in map_data]

第三步:写主循环(把所有逻辑串起来)

主循环是游戏的 "心脏",负责处理输入、更新状态、绘制画面。

python

运行

# 游戏主循环
clock = pygame.time.Clock()
running = True
win = False

while running:
    # 1. 处理事件(按键、关闭窗口等)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        
        # 处理按键(只在没胜利时响应)
        if event.type == pygame.KEYDOWN and not win:
            if event.key == pygame.K_UP:
                player.move(0, -1, map_data)  # 上移
            elif event.key == pygame.K_DOWN:
                player.move(0, 1, map_data)   # 下移
            elif event.key == pygame.K_LEFT:
                player.move(-1, 0, map_data)  # 左移
            elif event.key == pygame.K_RIGHT:
                player.move(1, 0, map_data)   # 右移
            elif event.key == pygame.K_r:  # 按R键重新开始
                map_data, px, py = get_level(1)
                player = Player(px, py)
                win = False
        
        # 检查是否胜利
        if not win:
            win = check_win(map_data, target_count)
    
    # 2. 绘制画面(先画在临时画布上)
    canvas.fill(COLOR_FLOOR)  # 填充背景
    
    # 绘制地图元素
    for y in range(len(map_data)):
        for x in range(len(map_data[y])):
            # 计算像素坐标(格子坐标×格子大小)
            pixel_x = x * CELL_SIZE
            pixel_y = y * CELL_SIZE
            
            # 绘制墙壁
            if map_data[y][x] == 1:
                pygame.draw.rect(canvas, COLOR_WALL, 
                                (pixel_x, pixel_y, CELL_SIZE, CELL_SIZE))
            # 绘制目标点(即使上面有箱子也要显示)
            if original_map[y][x] == 3:
                # 画一个小一点的正方形作为目标点
                pygame.draw.rect(canvas, COLOR_TARGET, 
                                (pixel_x+10, pixel_y+10, 
                                 CELL_SIZE-20, CELL_SIZE-20))
            # 绘制箱子
            if map_data[y][x] == 2:
                pygame.draw.rect(canvas, COLOR_BOX, 
                                (pixel_x+5, pixel_y+5, 
                                 CELL_SIZE-10, CELL_SIZE-10))
    
    # 绘制玩家
    pygame.draw.circle(canvas, COLOR_PLAYER, 
                      (player.x*CELL_SIZE + CELL_SIZE//2, 
                       player.y*CELL_SIZE + CELL_SIZE//2), 
                      CELL_SIZE//2 - 5)
    
    # 绘制胜利文字
    if win:
        font = pygame.font.Font(None, 72)
        text = font.render("胜利!按R重来", True, (255, 0, 0))
        canvas.blit(text, (SCREEN_WIDTH//2 - text.get_width()//2, 
                          SCREEN_HEIGHT//2 - text.get_height()//2))
    
    # 3. 把临时画布显示到屏幕上(解决闪屏)
    screen.blit(canvas, (0, 0))
    pygame.display.flip()
    
    # 控制帧率(60帧/秒)
    clock.tick(60)

# 退出游戏
pygame.quit()
sys.exit()

四、新手最容易踩的 6 个坑(我全中了)

  1. 坐标搞混:把格子坐标(x=1,y=1)和像素坐标(x=50,y=50)弄混,导致元素画错位。解决:始终用像素坐标 = 格子坐标 × 格子大小转换。

  2. 推动逻辑错误:推箱子时只判断了玩家能不能动,没判断箱子能不能动,导致箱子能穿墙。解决:先算箱子要去的位置,合法才能推。

  3. 地图复制错误:重置关卡时直接new_map = old_map,结果改新地图时旧地图也变了。解决:用[row.copy() for row in old_map]深复制。

  4. 画面闪屏:每帧直接在屏幕上画元素,导致闪屏。解决:用临时画布Surface先画好,再一次性显示。

  5. 胜利判断错误:只检查箱子数量,没检查是否在目标点上。解决:保存原始地图,对比箱子位置是否在目标点上。

  6. 边界没处理:玩家走到地图边缘时会报错。解决:在地图外围加一圈墙(像示例地图那样)。

五、3 个轻量化拓展(让你的游戏更酷)

1. 加个关卡选择(5 分钟搞定)

maps.py里加几个地图,再用数字键 1、2、3 切换:

python

运行

# 在事件处理里加
if event.key == pygame.K_1:
    map_data, px, py = get_level(1)
    player = Player(px, py)
    win = False
elif event.key == pygame.K_2:
    map_data, px, py = get_level(2)
    player = Player(px, py)
    win = False

2. 加移动音效(1 行代码)

python

运行

# 初始化时加载音效
move_sound = pygame.mixer.Sound("move.wav")  # 需要一个wav文件
push_sound = pygame.mixer.Sound("push.wav")

# 玩家移动成功时播放
if player.move(0, -1, map_data):
    move_sound.play()

3. 显示步数统计(记录玩家操作)

python

运行

# 初始化时加个步数变量
steps = 0

# 每次移动成功时加1
if player.move(0, -1, map_data):
    steps += 1

# 绘制步数
font = pygame.font.Font(None, 36)
text = font.render(f"步数: {steps}", True, (0, 0, 0))
canvas.blit(text, (10, 10))

最后:完整代码和学习建议

作为大一新生,我的体会是:做游戏比刷题库有趣多了!推箱子虽然简单,但包含了 "坐标转换"" 碰撞检测 ""状态判断" 等编程核心思想。刚开始写不出完整代码很正常,先复制我的代码跑起来,再一点点改参数(比如格子大小、颜色),慢慢就有感觉了。

如果这篇文章帮到了你,欢迎在评论区分享你的修改版本~ 祝大家都能在编程中找到乐趣!

更多推荐