内容目录

引言

Flappy Bird是一款经典的移动游戏,以其简单但极具挑战性的玩法吸引了全球玩家。本文将介绍如何使用Python和Pygame制作一个Flappy Bird克隆版游戏,并添加一些有趣的功能,如道具系统和在线排行榜。

准备工作

在开始之前,你需要安装以下工具和库:
Python
Pygame
Flask(用于后端)
SQLite(数据库)
requests(HTTP请求库)
可以使用以下命令安装这些库:

pip install pygame flask flask_sqlalchemy requests

图片资源





音效资源

项目结构

创建一个新的项目文件夹,并按照以下结构组织你的文件:

flappy_bird/
    ├── app.py
    ├── main.py
    ├── bird.py
    ├── pipe.py
    ├── power_up.py
    ├── settings.py
    └── assets/
        ├── bird.png
        ├── pipe.png
        ├── background.png
        ├── power_up.png
        └── invincibility_power_up.png

第一步:基础游戏实现

首先,我们需要定义游戏的基本元素:小鸟和管道。

bird.py

import pygame
from settings import BIRD_IMAGE_PATH, FLAP_STRENGTH, GRAVITY

class Bird(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.image.load(BIRD_IMAGE_PATH)
        self.rect = self.image.get_rect()
        self.rect.center = (x, y)
        self.velocity = 0

    def update(self):
        self.velocity += GRAVITY
        self.rect.y += self.velocity

    def flap(self):
        self.velocity = FLAP_STRENGTH

pipe.py

import pygame
import random
from settings import PIPE_IMAGE_PATH, PIPE_WIDTH, PIPE_HEIGHT, PIPE_GAP, PIPE_SPEED, SCREEN_WIDTH

class Pipe(pygame.sprite.Sprite):
    def __init__(self, x, y, position):
        super().__init__()
        self.image = pygame.image.load(PIPE_IMAGE_PATH)
        self.image = pygame.transform.scale(self.image, (PIPE_WIDTH, PIPE_HEIGHT))
        self.rect = self.image.get_rect()
        if position == 'top':
            self.rect.bottomleft = (x, y - PIPE_GAP // 2)
        else:
            self.rect.topleft = (x, y + PIPE_GAP // 2)
        self.passed = False  # 添加passed属性

    def update(self):
        self.rect.x -= PIPE_SPEED
        if self.rect.right < 0:
            self.kill()

def create_pipe_pair(x):
    y = random.randint(PIPE_GAP, SCREEN_WIDTH - PIPE_GAP)
    top_pipe = Pipe(x, y, 'top')
    bottom_pipe = Pipe(x, y, 'bottom')
    return top_pipe, bottom_pipe

settings.py

# settings.py

# Screen dimensions
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 600

# Colors
WHITE = (255, 255, 255)

# Game settings
GRAVITY = 0.25
FLAP_STRENGTH = -5
PIPE_GAP = 150
PIPE_WIDTH = 50
PIPE_HEIGHT = 320
PIPE_SPEED = 3

# Paths to assets
BIRD_IMAGE_PATH = 'assets/bird.png'
PIPE_IMAGE_PATH = 'assets/pipe.png'
BACKGROUND_IMAGE_PATH = 'assets/background.png'

# Path to power-up image
POWER_UP_IMAGE_PATH = 'assets/power_up.png'
INVINCIBILITY_POWER_UP_IMAGE_PATH = 'assets/invincibility_power_up.png'

# Power-up settings
POWER_UP_WIDTH = 30
POWER_UP_HEIGHT = 30
POWER_UP_SPEED = 3
POWER_UP_DURATION = 5000  # in milliseconds
INVINCIBILITY_DURATION = 5000  # in milliseconds

# Power-up end message settings
POWER_UP_END_MESSAGE = "Power-up ended!"
POWER_UP_END_MESSAGE_DURATION = 2000  # in milliseconds

# Menu and end screen settings
MENU_BACKGROUND_COLOR = (0, 0, 0)
MENU_TEXT_COLOR = (255, 255, 255)
MENU_FONT_SIZE = 48
END_BACKGROUND_COLOR = (0, 0, 0)
END_TEXT_COLOR = (255, 0, 0)
END_FONT_SIZE = 48

第二步:添加道具系统

为了让游戏更有趣,我们可以添加道具系统。当小鸟获得道具时,可以触发不同的效果,例如无敌和加速。

power_up.py

import pygame
import random
from settings import POWER_UP_IMAGE_PATH, POWER_UP_WIDTH, POWER_UP_HEIGHT, POWER_UP_SPEED, SCREEN_WIDTH, SCREEN_HEIGHT, PIPE_GAP, INVINCIBILITY_POWER_UP_IMAGE_PATH

class PowerUp(pygame.sprite.Sprite):
    def __init__(self, x, y, power_up_type='default'):
        super().__init__()
        if power_up_type == 'invincibility':
            self.image = pygame.image.load(INVINCIBILITY_POWER_UP_IMAGE_PATH)
        else:
            self.image = pygame.image.load(POWER_UP_IMAGE_PATH)
        self.image = pygame.transform.scale(self.image, (POWER_UP_WIDTH, POWER_UP_HEIGHT))
        self.rect = self.image.get_rect()
        self.rect.topleft = (x, y)
        self.power_up_type = power_up_type

    def update(self):
        self.rect.x -= POWER_UP_SPEED
        if self.rect.right < 0:
            self.kill()

def create_power_up(pipe_y):
    x = SCREEN_WIDTH
    # 确保道具在管道之间生成
    safe_zone_top = pipe_y + PIPE_GAP // 2
    safe_zone_bottom = safe_zone_top + PIPE_GAP // 2 - POWER_UP_HEIGHT
    y = random.randint(safe_zone_top, safe_zone_bottom)
    power_up_type = random.choice(['default', 'invincibility'])  # 随机选择道具类型
    return PowerUp(x, y, power_up_type)

第三步:添加在线排行榜

为了增加竞争性,我们可以添加一个在线排行榜,用于存储和显示玩家的最高分。

app.py (Flask 后端)

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///scores.db'
db = SQLAlchemy(app)

class Score(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    score = db.Column(db.Integer, nullable=False)

@app.route('/add_score', methods=['POST'])
def add_score():
    data = request.get_json()
    new_score = Score(name=data['name'], score=data['score'])
    db.session.add(new_score)
    db.session.commit()
    return jsonify({'message': 'Score added successfully!'})

@app.route('/get_scores', methods=['GET'])
def get_scores():
    scores = Score.query.order_by(Score.score.desc()).limit(10).all()
    scores_list = [{'name': score.name, 'score': score.score} for score in scores]
    return jsonify(scores_list)

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

main.py

import pygame
import sys
import random
import requests
import pygame_textinput
from settings import (
    SCREEN_WIDTH, SCREEN_HEIGHT, WHITE, BACKGROUND_IMAGE_PATH, FONT_PATH, FONT_SIZE, FONT_COLOR,
    MENU_BACKGROUND_COLOR, MENU_TEXT_COLOR, MENU_FONT_SIZE, END_BACKGROUND_COLOR, END_TEXT_COLOR, END_FONT_SIZE,
    PASS_PIPE_SOUND_PATH, POWER_UP_DURATION, POWER_UP_END_MESSAGE, POWER_UP_END_MESSAGE_DURATION, INVINCIBILITY_DURATION
)
from bird import Bird
from pipe import create_pipe_pair
from power_up import create_power_up

API_URL = "http://127.0.0.1:5000"  # 后端服务的URL

def show_menu(screen):
    font = pygame.font.Font(FONT_PATH, MENU_FONT_SIZE)
    title_text = font.render("Flappy Bird", True, MENU_TEXT_COLOR)
    start_text = font.render("Press SPACE to Start", True, MENU_TEXT_COLOR)

    while True:
        screen.fill(MENU_BACKGROUND_COLOR)
        screen.blit(title_text, (SCREEN_WIDTH // 2 - title_text.get_width() // 2, SCREEN_HEIGHT // 2 - title_text.get_height() // 2 - 50))
        screen.blit(start_text, (SCREEN_WIDTH // 2 - start_text.get_width() // 2, SCREEN_HEIGHT // 2 - start_text.get_height() // 2 + 50))

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    return

        pygame.display.flip()

def show_end_screen(screen, score):
    font = pygame.font.Font(FONT_PATH, END_FONT_SIZE)
    small_font = pygame.font.Font(FONT_PATH, FONT_SIZE - 10)
    end_text = font.render("Game Over", True, END_TEXT_COLOR)
    score_text = small_font.render(f"Score: {score}", True, END_TEXT_COLOR)
    restart_text = small_font.render("Press SPACE to Restart", True, END_TEXT_COLOR)

    textinput = pygame_textinput.TextInputVisualizer()
    textinput.font_color = END_TEXT_COLOR
    textinput.font_size = FONT_SIZE
    textinput.font_object = pygame.font.Font(FONT_PATH, FONT_SIZE)

    add_score_screen = True
    high_scores = get_high_scores()

    while True:
        screen.fill(END_BACKGROUND_COLOR)
        screen.blit(end_text, (SCREEN_WIDTH // 2 - end_text.get_width() // 2, SCREEN_HEIGHT // 2 - end_text.get_height() // 2 - 150))
        screen.blit(score_text, (SCREEN_WIDTH // 2 - score_text.get_width() // 2, SCREEN_HEIGHT // 2 - score_text.get_height() // 2 - 100))

        if add_score_screen:
            name_text = small_font.render("Enter your name:", True, END_TEXT_COLOR)
            screen.blit(name_text, (SCREEN_WIDTH // 2 - name_text.get_width() // 2, SCREEN_HEIGHT // 2 - name_text.get_height() // 2 - 50))
            events = pygame.event.get()
            textinput.update(events)
            screen.blit(textinput.surface, (SCREEN_WIDTH // 2 - textinput.surface.get_width() // 2, SCREEN_HEIGHT // 2))
            for event in events:
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RETURN:
                        add_score(score, textinput.value)
                        add_score_screen = False
        else:
            screen.blit(restart_text, (SCREEN_WIDTH // 2 - restart_text.get_width() // 2, SCREEN_HEIGHT // 2 - restart_text.get_height() // 2))
            high_scores_text = small_font.render("High Scores", True, END_TEXT_COLOR)
            screen.blit(high_scores_text, (SCREEN_WIDTH // 2 - high_scores_text.get_width() // 2, SCREEN_HEIGHT // 2 + 50))
            for i, hs in enumerate(high_scores):
                hs_text = small_font.render(f"{hs['name']}: {hs['score']}", True, END_TEXT_COLOR)
                screen.blit(hs_text, (SCREEN_WIDTH // 2 - hs_text.get_width() // 2, SCREEN_HEIGHT // 2 + 80 + i * 30))

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        return

        pygame.display.flip()

def add_score(score, name="Player"):
    data = {"name": name, "score": score}
    try:
        response = requests.post(f"{API_URL}/add_score", json=data)
        if response.status_code == 200:
            print("Score added successfully!")
        else:
            print("Failed to add score!")
    except Exception as e:
        print(f"Error: {e}")

def get_high_scores():
    try:
        response = requests.get(f"{API_URL}/get_scores")
        if response.status_code == 200:
            return response.json()
        else:
            print("Failed to fetch high scores!")
            return []
    except Exception as e:
        print(f"Error: {e}")
        return []

def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    clock = pygame.time.Clock()
    background = pygame.image.load(BACKGROUND_IMAGE_PATH)

    # 加载音效
    pass_pipe_sound = pygame.mixer.Sound(PASS_PIPE_SOUND_PATH)

    while True:
        show_menu(screen)  # 显示菜单界面

        bird = Bird(100, SCREEN_HEIGHT // 2)
        bird_group = pygame.sprite.Group()
        bird_group.add(bird)

        pipe_group = pygame.sprite.Group()
        power_up_group = pygame.sprite.Group()
        pygame.time.set_timer(pygame.USEREVENT, 1500)

        # 初始化分数
        score = 0
        font = pygame.font.Font(FONT_PATH, FONT_SIZE)
        end_message_font = pygame.font.Font(FONT_PATH, 30)

        # 初始化道具效果
        power_up_active = False
        power_up_start_time = 0
        invincibility_active = False
        invincibility_start_time = 0
        show_power_up_end_message = False
        power_up_end_message_start_time = 0

        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        bird.flap()
                if event.type == pygame.USEREVENT:
                    pipes = create_pipe_pair(SCREEN_WIDTH)
                    pipe_group.add(pipes[0])
                    pipe_group.add(pipes[1])
                    if random.randint(1, 10) == 1:  # 10%的概率生成道具
                        power_up = create_power_up(pipes[0].rect.bottom)
                        power_up_group.add(power_up)

            # 检测鸟是否通过了管道
            for pipe in pipe_group:
                if pipe.rect.right < bird.rect.left and not getattr(pipe, 'passed', False):
                    score += 1
                    pipe.passed = True
                    pass_pipe_sound.play()  # 播放音效

            # 检测鸟是否获得了道具
            for power_up in pygame.sprite.spritecollide(bird, power_up_group, True):
                if power_up.power_up_type == 'invincibility':
                    invincibility_active = True
                    invincibility_start_time = pygame.time.get_ticks()
                else:
                    power_up_active = True
                    power_up_start_time = pygame.time.get_ticks()

            # 更新和绘制
            screen.blit(background, (0, 0))
            bird_group.update()
            bird_group.draw(screen)
            pipe_group.update()
            pipe_group.draw(screen)
            power_up_group.update()
            power_up_group.draw(screen)

            # 显示当前分数
            score_text = font.render(f"Score: {score}", True, FONT_COLOR)
            screen.blit(score_text, (10, 10))

            # 处理道具效果
            current_time = pygame.time.get_ticks()
            if power_up_active:
                if current_time - power_up_start_time > POWER_UP_DURATION:
                    power_up_active = False
                    show_power_up_end_message = True
                    power_up_end_message_start_time = current_time
            if invincibility_active:
                if current_time - invincibility_start_time > INVINCIBILITY_DURATION:
                    invincibility_active = False
                    show_power_up_end_message = True
                    power_up_end_message_start_time = current_time

            # 显示道具效果结束提示
            if show_power_up_end_message:
                power_up_end_text = end_message_font.render(POWER_UP_END_MESSAGE, True, (255, 0, 0))
                screen.blit(power_up_end_text, (SCREEN_WIDTH // 2 - power_up_end_text.get_width() // 2, SCREEN_HEIGHT // 2 - power_up_end_text.get_height() // 2))
                if current_time - power_up_end_message_start_time > POWER_UP_END_MESSAGE_DURATION:
                    show_power_up_end_message = False

            # 检测碰撞(如果没有激活无敌效果)
            if not invincibility_active and not power_up_active:
                if pygame.sprite.spritecollideany(bird, pipe_group) or bird.rect.top <= 0 or bird.rect.bottom >= SCREEN_HEIGHT:
                    running = False  # 碰撞或飞出屏幕时结束游戏

            pygame.display.flip()
            clock.tick(60)

        show_end_screen(screen, score)  # 显示结束界面

if __name__ == "__main__":
    main()

通过这些步骤,你将能够创建一个功能丰富且具有挑战性的Flappy Bird克隆版游戏。希望你在开发过程中享受乐趣,并能够通过这些示例和建议进一步扩展游戏功能!

发表评论