PyGame游戏开发(入门知识+组件拆分+历史存档/回放+人机策略)

前言:

本章实现游戏组件的复用解耦,以及使用配置文件替代原有硬编码形式,进而只需要改动配置文件即可实现整个游戏的难度和地图变化 ,同时增加历史记录功能 ,在配置文件开启后即可保存每一局的记录为json形式作为后续强化学习的数据源 ,同时也可通过json复现渲染 之前保存的游戏记录,并编写函数作为初期人机对抗策略 。(之前写的pygame文档上了热榜,在此感谢各位的支持,也特此加更代码,更新本章,改成了更适合面向对象/java开发体质的代码,甚至导师那边的事情都放了放 bushi)PyGame游戏开发(含源码+演示视频+开结题报告+设计文档)-CSDN博客https://blog.csdn.net/wlf2030/article/details/147878665?spm=1001.2014.3001.5502源码链接如下(持续更新,欢迎star):

wlf728050719/BallGamehttps://github.com/wlf728050719/BallGame


入门知识:

其实pygame作为早期的二维游戏开发库,上手比较简单,只需要掌握以下几点即可。

1.渲染和物理分离,

可以将一张图片看作游戏中的一个object,其surface对象负责渲染,其rect负责物理逻辑的判断。

获取两个对象一般使用下面的方式:

load加载路径图片获取surface,并用surface.get_rect获取rect对象,获取的rect大小默认和surface大小相同,且由于传入图片为矩形,所以rect对象也为矩形。

2.位置绑定

尽管surface对象和rect负责职责不同,但可以理解surface对象作为一个贴图始终跟随rect对象移动,所以只需要关注rect的位置即可。同时注意x,y轴增加的方向如下。

3.循环监听

游戏的渲染和逻辑一定是放在一个死循环中,非阻塞监听键盘事件并执行对应逻辑。

4.渲染覆盖

后渲染的图片会覆盖在先渲染的图片上,同时由于pygame不会自动清空已经渲染的画面,所以每一帧开始或结束需要重新使用背景图覆盖原有画面。


快速开始:

下载解压源码文件后,目录如下,其中history为创建的存储游玩记录的目录,origin为之前的代码,功能健全 ,(启动后,使用l,m,h三个按键选择难度,1,2,3选择地图,按enter进入游戏,双方使用ws和上下控制,空格暂停,esc退出。)但由于没有拆分组件几乎不能二次开发。

remake1目录下为改进后代码,入口pve,pvp,replay分别对应三种模式,pve,pvp只需要修改对应的json配置文件即可创建完全不同的游戏进程,同时replay也可加载不同的历史记录。

配置文件如下:(可选择组件图片,游戏模式,组件速度,分数上限,是否保存,保存路径,人机策略等)

python 复制代码
{
  "background_image": "../resources/img/page/game.png",
  "ball_images":
  [
  "../resources/img/component/ball/ball_0.png",
  "../resources/img/component/ball/ball_1.png",
  "../resources/img/component/ball/ball_2.png",
  "../resources/img/component/ball/ball_3.png",
  "../resources/img/component/ball/ball_4.png"
  ],
  "paddle_image": "../resources/img/component/paddle/paddle.png",
  "fps": 30,
  "mode": "PVE",
  "strategy_right": 5,
  "max_scores": 1,
  "ball_speed": 5,
  "paddle_speed": 5,
  "render": true,
  "save": true,
  "save_dir": "../history/pve"
}

组件:

将渲染和物理逻辑进行拆分方便后续不渲染画面快速获取游戏记录。并添加导出和加载状态的接口,从而实现记录保存和回放。

Ball:

python 复制代码
import pygame.image


class Ball:
    def __init__(self, images, x, y, speedx, speedy):
        self.origin_x = x
        self.origin_y = y
        self.origin_speedx = speedx
        self.origin_speedy = speedy
        self.surfaces = []
        for img_path in images:
            try:
                surface = pygame.image.load(img_path)
                self.surfaces.append(surface)
            except pygame.error as e:
                print(f"无法加载图片 {img_path}: {e}")
        self.rect = self.surfaces[0].get_rect()
        self.rect.x = x
        self.rect.y = y
        self.speedx = speedx
        self.speedy = speedy

    def move(self):
        self.rect.x += self.speedx
        self.rect.y += self.speedy

    def render(self, frame, screen):
        screen.blit(self.surfaces[(frame % len(self.surfaces))], self.rect)

    def is_hit(self, other_rect):
        return self.rect.colliderect(other_rect)

    def set_position(self, x, y):
        self.rect.x = x
        self.rect.y = y

    def set_speed(self, speedx, speedy):
        self.speedx = speedx
        self.speedy = speedy

    def reset(self):
        self.set_position(self.origin_x, self.origin_y)
        self.set_speed(self.origin_speedx, self.origin_speedy)

    def get_state(self):
        return {
            'x': self.rect.x,
            'y': self.rect.y,
            'speedx': self.speedx,
            'speedy': self.speedy
        }

    def load_state(self, state):
        self.rect.x = state["x"]
        self.rect.y = state["y"]
        self.speedx = state["speedx"]
        self.speedy = state["speedy"]

Paddle:

python 复制代码
import pygame

from remake1.constant.enums import Direction


class Paddle:
    def __init__(self, image, x, y, speed,height):
        self.surface = pygame.image.load(image)
        self.rect = self.surface.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.speed = speed
        self.height = height
    def move(self, direction):
        if direction == Direction.UP:
            self.rect.y -= self.speed
        elif direction == Direction.DOWN:
            self.rect.y += self.speed
        if self.rect.top < 0:
            self.rect.top = 0
        if self.rect.bottom > self.height:
            self.rect.bottom = self.height
    def render(self, screen):
        screen.blit(self.surface, self.rect)

    def set_position(self, y):
        self.rect.y = y

    def set_speed(self, speed):
        self.speed = speed

    def get_state(self):
        return {
            'x': self.rect.x,
            'y': self.rect.y,
            'speed': self.speed
        }

    def load_state(self, state):
        self.rect.x = state["x"]
        self.rect.y = state["y"]
        self.speed = state["speed"]

输入工具类:

规范允许输入按键,对长按和短按键进行区分。

python 复制代码
class InputUtil:
    def __init__(self, allowed_keys):
        self.allowed_keys = set(allowed_keys)   #支持的输入按键
        self.pressed_keys = set()   #所有按下
        self.released_keys = set()  #所有释放
        self.just_pressed = set()   #最新按下
        self.just_released = set()  #最新释放
    def press(self, key):
        if key in self.allowed_keys:
            if key not in self.pressed_keys:
                self.pressed_keys.add(key)
                self.just_pressed.add(key)
            if key in self.released_keys:
                self.released_keys.remove(key)
    def release(self, key):
        if key in self.allowed_keys:
            if key in self.pressed_keys:
                self.pressed_keys.remove(key)
                self.just_released.add(key)
            self.released_keys.add(key)
    def is_pressed(self, key):
        return key in self.pressed_keys
    def is_released(self, key):
        return key in self.released_keys
    def was_just_pressed(self, key):
        return key in self.just_pressed
    def was_just_released(self, key):
        return key in self.just_released
    def update(self):
        self.just_pressed.clear()
        self.just_released.clear()
    def get_pressed_keys(self):
        return self.pressed_keys.copy()

回放工具类:

python 复制代码
import json

import pygame

from remake1.component.ball import Ball
from remake1.component.paddle import Paddle
from remake1.config import Config


class Replay:
    def __init__(self, filepath):
        with open(filepath, 'r') as f:
            self.data = json.load(f)

        # 从保存的数据还原Config
        self.config = Config()
        self.config.__dict__ = self.data["config"]

        # 初始化游戏窗口
        self.width, self.height = pygame.image.load(self.config.background_image).get_size()
        self.screen = pygame.display.set_mode((self.width, self.height))
        self.fps = self.config.fps

        # 初始化游戏对象
        self.ball = Ball(self.config.ball_images, 0, 0, 0, 0)
        self.paddles = [
            Paddle(self.config.paddle_image, 0, 0, 0, self.height),
            Paddle(self.config.paddle_image, 0, 0, 0, self.height)
        ]

    def load_frame(self, frame_data):
        self.ball.load_state(frame_data["ball"])
        self.paddles[0].load_state(frame_data["paddle1"])
        self.paddles[1].load_state(frame_data["paddle2"])

    def play(self):
        clock = pygame.time.Clock()
        for frame in self.data["frames"]:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    return

            # 渲染背景
            self.screen.blit(pygame.image.load(self.config.background_image), (0, 0))
            self.load_frame(frame)

            # 渲染对象
            self.ball.render(frame["frame"], self.screen)
            self.paddles[0].render(self.screen)
            self.paddles[1].render(self.screen)

            pygame.display.flip()
            clock.tick(self.fps)

游戏主类:

python 复制代码
import json
import os
import sys

from datetime import datetime

import pygame.image

from remake1.component.ball import Ball
from remake1.component.paddle import Paddle
from remake1.constant.enums import Direction
from remake1.strategy.stragety import Strategy
from remake1.util.input_util import InputUtil

class Game:
    def __init__(self,config):
        self.config = config
        self.width,self.height = pygame.image.load(config.background_image).get_size()
        self.paddle_height,self.paddle_width = pygame.image.load(config.paddle_image).get_size()
        self.screen = pygame.display.set_mode((self.width, self.height))
        self.ball = Ball(config.ball_images,self.width//2,10,config.ball_speed,config.ball_speed)
        self.paddles = [
            Paddle(config.paddle_image,5,self.height//2,config.paddle_speed,self.height),
            Paddle(config.paddle_image,self.width-5,self.height//2,config.paddle_speed,self.height),
        ]
        self.game_history = []
        self.left_score = 0
        self.right_score = 0
    def save_state(self,direction1,direction2,frame):
        state = {
            'frame': frame,
            'ball': self.ball.get_state(),
            'paddle1': self.paddles[0].get_state(),
            'paddle2': self.paddles[1].get_state(),
            'actions': {
                'paddle1': direction1.name,
                'paddle2': direction2.name
            }
        }
        self.game_history.append(state)
    def update(self,direction1,direction2,frame):
        #更新物理位置
        self.ball.move() #更新球的位置
        self.paddles[0].move(direction1)
        self.paddles[1].move(direction2)
        #碰撞逻辑
        #挡板碰撞
        if self.ball.is_hit(self.paddles[0].rect):
            self.ball.rect.left = self.paddles[0].rect.right
            self.ball.speedx = -self.ball.speedx
        if self.ball.is_hit(self.paddles[1].rect):
            self.ball.rect.right = self.paddles[1].rect.left
            self.ball.speedx = -self.ball.speedx
        #上下边界碰撞
        if self.ball.rect.top < 0 or self.ball.rect.bottom > self.height:
            self.ball.speedy = -self.ball.speedy
        #左右边界计分
        if self.ball.rect.right < 0:
            self.ball.reset()
            self.right_score += 1
        if self.ball.rect.left > self.width:
            self.ball.reset()
            self.left_score += 1
        #渲染
        if self.config.render:
            self.ball.render(frame,self.screen)
            self.paddles[0].render(self.screen)
            self.paddles[1].render(self.screen)
    def winer(self):
        if self.right_score >= self.config.max_scores:
            return 1    #右边玩家win
        elif self.left_score >= self.config.max_scores:
            return -1   #左边玩家win
        else:
            return 0    #游戏继续
    def export_history(self):
        # 创建保存目录
        os.makedirs(self.config.save_dir, exist_ok=True)
        # 时间戳命名文件
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"game_{timestamp}.json"
        filepath = os.path.join(self.config.save_dir, filename)
        save_data = {
            "config": self.config.__dict__,
            "frames": self.game_history,
        }
        # 保存为json格式
        try:
            with open(filepath, 'w') as f:
                json.dump(save_data, f, indent=2)
            return True
        except Exception as e:
            print(f"Error saving game data: {e}")
            return False
    def start(self):
        pygame.init()
        clock = pygame.time.Clock()
        if self.config.mode == "PVE":
            input_listener = InputUtil([pygame.K_w, pygame.K_s])
        elif self.config.mode == "PVP":
            input_listener = InputUtil([pygame.K_w, pygame.K_s, pygame.K_UP, pygame.K_DOWN])
        else:
            input_listener = None
        frame = 0
        direction1 = Direction.IDLE
        direction2 = Direction.IDLE
        while True:
            if self.config.render:
                self.screen.blit(pygame.image.load(self.config.background_image),(0,0))
            for event in pygame.event.get():
                #按键监听
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    input_listener.press(event.key)
                if event.type == pygame.KEYUP:
                    input_listener.release(event.key)
            if self.config.mode == "PVP":
                #player1
                if input_listener.is_pressed(pygame.K_w):  # 多键输入优先向上
                    direction1 = Direction.UP
                elif input_listener.is_pressed(pygame.K_s):
                    direction1 = Direction.DOWN
                else:
                    direction1 = Direction.IDLE
                #player2
                if input_listener.is_pressed(pygame.K_UP):  # 多键输入优先向上
                    direction2 = Direction.UP
                elif input_listener.is_pressed(pygame.K_DOWN):
                    direction2 = Direction.DOWN
                else:
                    direction2 = Direction.IDLE
            elif self.config.mode == "PVE":
                # player1
                if input_listener.is_pressed(pygame.K_w):  # 多键输入优先向上
                    direction1 = Direction.UP
                elif input_listener.is_pressed(pygame.K_s):
                    direction1 = Direction.DOWN
                else:
                    direction1 = Direction.IDLE
                # ai
                if self.config.strategy_right == 1:
                    direction2 = Strategy.simple_ai(self.ball.get_state(), self.paddles[1].get_state())
                elif self.config.strategy_right == 2:
                    direction2 = Strategy.medium_ai(self.ball.get_state(), self.paddles[1].get_state(),self.paddle_height)
                elif self.config.strategy_right == 3:
                    direction2 = Strategy.advanced_ai(self.ball.get_state(), self.paddles[1].get_state(),self.width,self.paddle_height)
                elif self.config.strategy_right == 4:
                    direction2 = Strategy.expert_ai(self.ball.get_state(), self.paddles[1].get_state(),self.width,self.paddle_height)
                elif self.config.strategy_right == 5:
                    direction2 = Strategy.reactive_ai(self.ball.get_state(), self.paddles[1].get_state(),self.paddle_height)
            if self.config.save:
                self.save_state(direction1,direction2,frame)
            self.update(direction1,direction2,frame)
            if self.winer() != 0:
                if self.config.save:
                    self.export_history()
                break
            frame += 1
            pygame.display.update()
            clock.tick(self.config.fps)

人机策略类:

python 复制代码
import json

import pygame

from remake1.component.ball import Ball
from remake1.component.paddle import Paddle
from remake1.config import Config


class Replay:
    def __init__(self, filepath):
        with open(filepath, 'r') as f:
            self.data = json.load(f)

        # 从保存的数据还原Config
        self.config = Config()
        self.config.__dict__ = self.data["config"]

        # 初始化游戏窗口
        self.width, self.height = pygame.image.load(self.config.background_image).get_size()
        self.screen = pygame.display.set_mode((self.width, self.height))
        self.fps = self.config.fps

        # 初始化游戏对象
        self.ball = Ball(self.config.ball_images, 0, 0, 0, 0)
        self.paddles = [
            Paddle(self.config.paddle_image, 0, 0, 0, self.height),
            Paddle(self.config.paddle_image, 0, 0, 0, self.height)
        ]

    def load_frame(self, frame_data):
        self.ball.load_state(frame_data["ball"])
        self.paddles[0].load_state(frame_data["paddle1"])
        self.paddles[1].load_state(frame_data["paddle2"])

    def play(self):
        clock = pygame.time.Clock()
        for frame in self.data["frames"]:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    return

            # 渲染背景
            self.screen.blit(pygame.image.load(self.config.background_image), (0, 0))
            self.load_frame(frame)

            # 渲染对象
            self.ball.render(frame["frame"], self.screen)
            self.paddles[0].render(self.screen)
            self.paddles[1].render(self.screen)

            pygame.display.flip()
            clock.tick(self.fps)

最后:

尽管目前看来相比最初版代码变多了但功能反而少了,但实际上组件的拆分会大大加快开发的效率,但目前暂时不打算做丰富游戏功能的工作,毕竟游戏开发有那么多引擎何必纠结于pygame,只能作为一个了解代码逻辑的工具,以及pygame更多确实是用于强化学习方面的训练,后续会往这方面改,毕竟时代潮流在此,但参考目前代码框架想要二开应该难度不大,各位小伙伴可以自行尝试。

相关推荐
Evand J1 分钟前
【MATLAB例程】线性卡尔曼滤波的程序,三维状态量和观测量,较为简单,可用于理解多维KF,附代码下载链接
开发语言·matlab
苕皮蓝牙土豆15 分钟前
C++ map容器: 插入操作
开发语言·c++
Dxy123931021620 分钟前
Python 装饰器详解
开发语言·python
linab11227 分钟前
mybatis中的resultMap的association及collectio的使用
java·开发语言·mybatis
ganjiee000736 分钟前
新电脑软件配置二:安装python,git, pycharm
python
Ronin-Lotus36 分钟前
程序代码篇---python向http界面发送数据
python·http
NaclarbCSDN1 小时前
Java IO框架
开发语言·python
fanTuanye1 小时前
Java基础知识总结(超详细整理)
java·开发语言
Tom Boom1 小时前
19. 结合Selenium和YAML对页面实例化PO对象改造
python·测试开发·selenium·测试工具·自动化测试框架开发·po改造
顾子茵1 小时前
c++从入门到精通(六)--特殊工具与技术-完结篇
android·开发语言·c++