前言:
本章实现游戏组件的复用解耦,以及使用配置文件替代原有硬编码形式,进而只需要改动配置文件即可实现整个游戏的难度和地图变化,同时增加历史记录功能,在配置文件开启后即可保存每一局的记录为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也可加载不同的历史记录。
配置文件如下:(可选择组件图片,游戏模式,组件速度,分数上限,是否保存,保存路径,人机策略等)
{
"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:
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:
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"]
输入工具类:
规范允许输入按键,对长按和短按键进行区分。
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()
回放工具类:
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)
游戏主类:
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)
人机策略类:
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更多确实是用于强化学习方面的训练,后续会往这方面改,毕竟时代潮流在此,但参考目前代码框架想要二开应该难度不大,各位小伙伴可以自行尝试。