前言:
本章实现游戏组件的复用解耦,以及使用配置文件替代原有硬编码形式,进而只需要改动配置文件即可实现整个游戏的难度和地图变化 ,同时增加历史记录功能 ,在配置文件开启后即可保存每一局的记录为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更多确实是用于强化学习方面的训练,后续会往这方面改,毕竟时代潮流在此,但参考目前代码框架想要二开应该难度不大,各位小伙伴可以自行尝试。