一、效果图

二、代码
python
# -*- coding:utf-8 -*-
"""
作者:杨桃清
日期: 2025年04日28 02:50:40
"""
import pygame
import tkinter as tk
from tkinter import ttk, messagebox
import random
import json
import os
import sys
from typing import Dict, List, Tuple, Optional
# 常量定义
SCREEN_SIZE = (400, 600) # 统一使用游戏界面尺寸
RECORD_FILE = "flappy_bird_record.json"
DEFAULT_SETTINGS = {
"gravity": 0.2,
"bird_speed": -7,
"pipe_frequency": 1500,
"pipe_gap_min": 120,
"pipe_gap_max": 200,
"pipe_speed": 2,
"difficulty_step": 0.1,
}
class GameSettingsWindow:
"""参数设置窗口"""
def __init__(self, parent):
self.top = tk.Toplevel(parent)
self.top.title(" 游戏参数设置")
self.top.resizable(False, False)
self.settings = DEFAULT_SETTINGS.copy()
self._create_widgets()
def _create_widgets(self):
# 重力设置
ttk.Label(self.top, text="重力:").grid(row=0, column=0, padx=5, pady=5, sticky="e")
self.gravity_entry = ttk.Entry(self.top)
self.gravity_entry.insert(0, str(self.settings["gravity"]))
self.gravity_entry.grid(row=0, column=1, padx=5, pady=5)
# 跳跃力度
ttk.Label(self.top, text="跳跃力度:").grid(row=1, column=0, padx=5, pady=5, sticky="e")
self.bird_speed_entry = ttk.Entry(self.top)
self.bird_speed_entry.insert(0, str(self.settings["bird_speed"]))
self.bird_speed_entry.grid(row=1, column=1, padx=5, pady=5)
# 柱子生成频率
ttk.Label(self.top, text="柱子频率(ms):").grid(row=2, column=0, padx=5, pady=5, sticky="e")
self.pipe_freq_entry = ttk.Entry(self.top)
self.pipe_freq_entry.insert(0, str(self.settings["pipe_frequency"]))
self.pipe_freq_entry.grid(row=2, column=1, padx=5, pady=5)
# 柱子间隙
ttk.Label(self.top, text="柱子最小间隙:").grid(row=3, column=0, padx=5, pady=5, sticky="e")
self.pipe_gap_min_entry = ttk.Entry(self.top)
self.pipe_gap_min_entry.insert(0, str(self.settings["pipe_gap_min"]))
self.pipe_gap_min_entry.grid(row=3, column=1, padx=5, pady=5)
ttk.Label(self.top, text="柱子最大间隙:").grid(row=4, column=0, padx=5, pady=5, sticky="e")
self.pipe_gap_max_entry = ttk.Entry(self.top)
self.pipe_gap_max_entry.insert(0, str(self.settings["pipe_gap_max"]))
self.pipe_gap_max_entry.grid(row=4, column=1, padx=5, pady=5)
# 柱子移动速度
ttk.Label(self.top, text="柱子速度:").grid(row=5, column=0, padx=5, pady=5, sticky="e")
self.pipe_speed_entry = ttk.Entry(self.top)
self.pipe_speed_entry.insert(0, str(self.settings["pipe_speed"]))
self.pipe_speed_entry.grid(row=5, column=1, padx=5, pady=5)
# 难度增幅
ttk.Label(self.top, text="难度增幅:").grid(row=6, column=0, padx=5, pady=5, sticky="e")
self.difficulty_step_entry = ttk.Entry(self.top)
self.difficulty_step_entry.insert(0, str(self.settings["difficulty_step"]))
self.difficulty_step_entry.grid(row=6, column=1, padx=5, pady=5)
# 按钮区域
btn_frame = ttk.Frame(self.top)
btn_frame.grid(row=7, column=0, columnspan=2, pady=10)
ttk.Button(btn_frame, text="保存", command=self._save_settings).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="恢复默认", command=self._reset_defaults).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="取消", command=self.top.destroy).pack(side=tk.RIGHT, padx=5)
def _save_settings(self):
"""保存参数设置"""
try:
self.settings = {
"gravity": float(self.gravity_entry.get()),
"bird_speed": float(self.bird_speed_entry.get()),
"pipe_frequency": int(self.pipe_freq_entry.get()),
"pipe_gap_min": int(self.pipe_gap_min_entry.get()),
"pipe_gap_max": int(self.pipe_gap_max_entry.get()),
"pipe_speed": int(self.pipe_speed_entry.get()),
"difficulty_step": float(self.difficulty_step_entry.get()),
}
self.top.destroy()
except ValueError:
messagebox.showerror(" 错误", "请输入有效的数值参数")
def _reset_defaults(self):
"""恢复默认设置"""
self.settings = DEFAULT_SETTINGS.copy()
self.gravity_entry.delete(0, tk.END)
self.gravity_entry.insert(0, str(self.settings["gravity"]))
self.bird_speed_entry.delete(0, tk.END)
self.bird_speed_entry.insert(0, str(self.settings["bird_speed"]))
self.pipe_freq_entry.delete(0, tk.END)
self.pipe_freq_entry.insert(0, str(self.settings["pipe_frequency"]))
self.pipe_gap_min_entry.delete(0, tk.END)
self.pipe_gap_min_entry.insert(0, str(self.settings["pipe_gap_min"]))
self.pipe_gap_max_entry.delete(0, tk.END)
self.pipe_gap_max_entry.insert(0, str(self.settings["pipe_gap_max"]))
self.pipe_speed_entry.delete(0, tk.END)
self.pipe_speed_entry.insert(0, str(self.settings["pipe_speed"]))
self.difficulty_step_entry.delete(0, tk.END)
self.difficulty_step_entry.insert(0, str(self.settings["difficulty_step"]))
def get_settings(self) -> Dict:
"""获取当前设置"""
return self.settings
class MainMenu:
"""游戏主菜单"""
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode(SCREEN_SIZE)
pygame.display.set_caption("Flappy Bird")
self.clock = pygame.time.Clock()
self.settings = DEFAULT_SETTINGS.copy()
self._load_resources()
def _load_resources(self):
"""加载游戏资源"""
# 字体
try:
self.title_font = pygame.font.SysFont("simhei", 48)
self.menu_font = pygame.font.SysFont("simhei", 36)
self.info_font = pygame.font.SysFont("simhei", 24)
except:
self.title_font = pygame.font.SysFont(None, 48)
self.menu_font = pygame.font.SysFont(None, 36)
self.info_font = pygame.font.SysFont(None, 24)
# 颜色
self.colors = {
"background": (240, 240, 240),
"title": (30, 30, 30),
"button": (70, 130, 180),
"button_hover": (100, 160, 210),
"text": (255, 255, 255),
"shadow": (100, 100, 100),
}
# 加载记录
self.max_distance = self._load_record()
def _load_record(self) -> float:
"""加载历史记录"""
if os.path.exists(RECORD_FILE):
try:
with open(RECORD_FILE, "r") as f:
return json.load(f).get("max_distance", 0)
except:
return 0
return 0
def _draw_menu(self):
"""绘制主菜单"""
self.screen.fill(self.colors["background"])
# 绘制标题
title = self.title_font.render("Flappy Bird", True, self.colors["title"])
title_rect = title.get_rect(center=(SCREEN_SIZE[0] // 2, 120))
self.screen.blit(title, title_rect)
# 绘制按钮
button_data = [
("开始游戏", (SCREEN_SIZE[0] // 2, 250)),
("退出游戏", (SCREEN_SIZE[0] // 2, 350)),
("设置", (SCREEN_SIZE[0] - 80, 40)),
]
self.buttons = []
mouse_pos = pygame.mouse.get_pos()
for text, pos in button_data:
if text == "设置":
button_rect = pygame.Rect(0, 0, 80, 40)
else:
button_rect = pygame.Rect(0, 0, 200, 60)
button_rect.center = pos
# 按钮状态
hover = button_rect.collidepoint(mouse_pos)
color = self.colors["button_hover"] if hover else self.colors["button"]
# 绘制按钮
pygame.draw.rect(self.screen, color, button_rect, border_radius=10)
pygame.draw.rect(self.screen, self.colors["shadow"], button_rect, 2, border_radius=10)
# 绘制按钮文字
if text == "设置":
text_surf = self.info_font.render(text, True, self.colors["text"])
else:
text_surf = self.menu_font.render(text, True, self.colors["text"])
text_rect = text_surf.get_rect(center=button_rect.center)
self.screen.blit(text_surf, text_rect)
self.buttons.append((text, button_rect))
# 显示最高记录
if self.max_distance > 0:
record_text = self.info_font.render(f" 最远记录: {int(self.max_distance)} 米", True, self.colors["title"])
record_rect = record_text.get_rect(midleft=(20, SCREEN_SIZE[1] - 30))
self.screen.blit(record_text, record_rect)
pygame.display.flip()
def run(self):
"""运行主菜单循环"""
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEBUTTONDOWN:
mouse_pos = pygame.mouse.get_pos()
for text, rect in self.buttons:
if rect.collidepoint(mouse_pos):
if text == "开始游戏":
self._start_game()
elif text == "退出游戏":
running = False
elif text == "设置":
self._open_settings()
self._draw_menu()
self.clock.tick(60)
pygame.quit()
sys.exit()
def _open_settings(self):
"""打开设置窗口"""
root = tk.Tk()
root.withdraw() # 隐藏主窗口
settings_window = GameSettingsWindow(root)
root.wait_window(settings_window.top)
if hasattr(settings_window, 'settings'):
self.settings = settings_window.settings
def _start_game(self):
"""启动游戏"""
game = GameEngine(self.settings)
game.run()
# 更新记录
new_record = self._load_record()
if new_record > self.max_distance:
self.max_distance = new_record
class GameEngine:
"""游戏引擎"""
def __init__(self, settings: Dict):
self.settings = settings
self.screen = pygame.display.set_mode(SCREEN_SIZE)
pygame.display.set_caption("Flappy Bird - 游戏中")
self.clock = pygame.time.Clock()
# 游戏状态
self.bird = pygame.Rect(100, 250, 30, 30)
self.bird_speed = 0
self.pipes = []
self.distance = 0
self.score = 0
self.last_pipe_time = 0
self.max_distance = self._load_record()
# 资源加载
self._load_resources()
def _load_resources(self):
"""加载游戏资源"""
# 字体
try:
self.font = pygame.font.SysFont("simhei", 24)
self.large_font = pygame.font.SysFont("simhei", 36)
except:
self.font = pygame.font.SysFont(None, 24)
self.large_font = pygame.font.SysFont(None, 36)
# 颜色
self.colors = {
"background": (255, 255, 255),
"bird": (0, 0, 255),
"pipe": (0, 180, 0),
"text": (0, 0, 0),
"game_over_bg": (240, 240, 240),
"button": (70, 130, 180),
"button_hover": (100, 160, 210),
"button_text": (255, 255, 255),
"overlay": (0, 0, 0, 180),
}
def _load_record(self) -> float:
"""加载历史记录"""
if os.path.exists(RECORD_FILE):
try:
with open(RECORD_FILE, "r") as f:
return json.load(f).get("max_distance", 0)
except:
return 0
return 0
def _save_record(self):
"""保存新记录"""
if self.distance > self.max_distance:
self.max_distance = self.distance
with open(RECORD_FILE, "w") as f:
json.dump({"max_distance": self.max_distance}, f)
def _handle_events(self) -> bool:
"""处理游戏事件"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
self.bird_speed = self.settings["bird_speed"]
return True
def _update_game_state(self):
"""更新游戏状态"""
# 更新小鸟位置
self.bird_speed += self.settings["gravity"]
self.bird.y += self.bird_speed
self.distance += 0.05
# 更新记录
if self.distance > self.max_distance:
self._save_record()
# 动态难度调整
difficulty = 1 + self.settings["difficulty_step"] * (self.distance // 100)
pipe_freq = max(500, self.settings["pipe_frequency"] / difficulty)
gap_min = max(80, self.settings["pipe_gap_min"] - (difficulty - 1) * 10)
gap_max = max(150, self.settings["pipe_gap_max"] - (difficulty - 1) * 15)
pipe_speed = min(5, self.settings["pipe_speed"] + (difficulty - 1) * 0.5)
# 生成新柱子
current_time = pygame.time.get_ticks()
if current_time - self.last_pipe_time > pipe_freq:
gap = random.randint(int(gap_min), int(gap_max))
height = random.randint(50, SCREEN_SIZE[1] - gap - 50)
self.pipes.append([
pygame.Rect(SCREEN_SIZE[0], 0, 50, height),
pygame.Rect(SCREEN_SIZE[0], height + gap, 50, SCREEN_SIZE[1] - height - gap)
])
self.last_pipe_time = current_time
# 移动柱子
for pipe in self.pipes[:]:
pipe[0].x -= pipe_speed
pipe[1].x -= pipe_speed
if pipe[0].x < -50:
self.pipes.remove(pipe)
self.score += 1
def _check_collisions(self) -> bool:
"""检测碰撞"""
if self.bird.y <= 0 or self.bird.y >= SCREEN_SIZE[1]:
return True
return any(
self.bird.colliderect(pipe[0]) or self.bird.colliderect(pipe[1])
for pipe in self.pipes
)
def _draw_game(self):
"""绘制游戏画面"""
self.screen.fill(self.colors["background"])
# 绘制小鸟
pygame.draw.rect(self.screen, self.colors["bird"], self.bird)
# 绘制柱子
for pipe in self.pipes:
pygame.draw.rect(self.screen, self.colors["pipe"], pipe[0])
pygame.draw.rect(self.screen, self.colors["pipe"], pipe[1])
# 显示游戏信息
info = [
f"得分: {self.score}",
f"距离: {int(self.distance)} 米",
f"记录: {int(self.max_distance)} 米"
]
for i, text in enumerate(info):
text_surface = self.font.render(text, True, self.colors["text"])
self.screen.blit(text_surface, (10, 10 + i * 30))
pygame.display.flip()
def _show_game_over(self) -> bool:
"""显示结束界面并返回是否重玩"""
overlay = pygame.Surface(SCREEN_SIZE, pygame.SRCALPHA)
overlay.fill(self.colors["overlay"])
self.screen.blit(overlay, (0, 0))
# 游戏结果文本
texts = [
("游戏结束!", 36, (SCREEN_SIZE[0] // 2, 150)),
(f"本次得分: {self.score}", 30, (SCREEN_SIZE[0] // 2, 220)),
(f"飞行距离: {int(self.distance)} 米", 30, (SCREEN_SIZE[0] // 2, 270)),
(f"最远记录: {int(self.max_distance)} 米", 30, (SCREEN_SIZE[0] // 2, 320))
]
for text, size, pos in texts:
text_surface = self.large_font.render(text, True, (255, 255, 255))
text_rect = text_surface.get_rect(center=pos)
self.screen.blit(text_surface, text_rect)
# 创建按钮
buttons = [
("重新开始", (SCREEN_SIZE[0] // 2 - 110, 400)),
("返回菜单", (SCREEN_SIZE[0] // 2 + 110, 400))
]
button_rects = []
mouse_pos = pygame.mouse.get_pos()
for text, pos in buttons:
rect = pygame.Rect(0, 0, 150, 50)
rect.center = pos
# 按钮悬停效果
btn_color = self.colors["button_hover"] if rect.collidepoint(mouse_pos) else self.colors["button"]
pygame.draw.rect(self.screen, btn_color, rect, border_radius=10)
pygame.draw.rect(self.screen, (255, 255, 255), rect, 2, border_radius=10)
text_surface = self.font.render(text, True, self.colors["button_text"])
text_rect = text_surface.get_rect(center=rect.center)
self.screen.blit(text_surface, text_rect)
button_rects.append((text, rect))
pygame.display.flip()
# 处理按钮点击
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos()
for text, rect in button_rects:
if rect.collidepoint(pos):
if text == "重新开始":
return True
else:
return False
self.clock.tick(60)
def run(self):
"""运行游戏主循环"""
running = True
while running:
self.clock.tick(60)
if not self._handle_events():
break
self._update_game_state()
if self._check_collisions():
if self._show_game_over():
# 重新开始游戏
self.__init__(self.settings)
continue
else:
break
self._draw_game()
pygame.display.set_caption("Flappy Bird")
return False
if __name__ == "__main__":
menu = MainMenu()
menu.run()