当前位置: 首页 > news >正文

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也可加载不同的历史记录。

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

{"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.imageclass Ball:def __init__(self, images, x, y, speedx, speedy):self.origin_x = xself.origin_y = yself.origin_speedx = speedxself.origin_speedy = speedyself.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 = xself.rect.y = yself.speedx = speedxself.speedy = speedydef move(self):self.rect.x += self.speedxself.rect.y += self.speedydef 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 = xself.rect.y = ydef set_speed(self, speedx, speedy):self.speedx = speedxself.speedy = speedydef 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 pygamefrom remake1.constant.enums import Directionclass Paddle:def __init__(self, image, x, y, speed,height):self.surface = pygame.image.load(image)self.rect = self.surface.get_rect()self.rect.x = xself.rect.y = yself.speed = speedself.height = heightdef move(self, direction):if direction == Direction.UP:self.rect.y -= self.speedelif direction == Direction.DOWN:self.rect.y += self.speedif self.rect.top < 0:self.rect.top = 0if self.rect.bottom > self.height:self.rect.bottom = self.heightdef render(self, screen):screen.blit(self.surface, self.rect)def set_position(self, y):self.rect.y = ydef set_speed(self, speed):self.speed = speeddef 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_keysdef is_released(self, key):return key in self.released_keysdef was_just_pressed(self, key):return key in self.just_presseddef was_just_released(self, key):return key in self.just_releaseddef update(self):self.just_pressed.clear()self.just_released.clear()def get_pressed_keys(self):return self.pressed_keys.copy()

回放工具类:

import jsonimport pygamefrom remake1.component.ball import Ball
from remake1.component.paddle import Paddle
from remake1.config import Configclass Replay:def __init__(self, filepath):with open(filepath, 'r') as f:self.data = json.load(f)# 从保存的数据还原Configself.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 sysfrom datetime import datetimeimport pygame.imagefrom 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 InputUtilclass Game:def __init__(self,config):self.config = configself.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 = 0self.right_score = 0def 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.rightself.ball.speedx = -self.ball.speedxif self.ball.is_hit(self.paddles[1].rect):self.ball.rect.right = self.paddles[1].rect.leftself.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 += 1if 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    #右边玩家winelif self.left_score >= self.config.max_scores:return -1   #左边玩家winelse: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 Trueexcept Exception as e:print(f"Error saving game data: {e}")return Falsedef 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 = Noneframe = 0direction1 = Direction.IDLEdirection2 = Direction.IDLEwhile 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":#player1if input_listener.is_pressed(pygame.K_w):  # 多键输入优先向上direction1 = Direction.UPelif input_listener.is_pressed(pygame.K_s):direction1 = Direction.DOWNelse:direction1 = Direction.IDLE#player2if input_listener.is_pressed(pygame.K_UP):  # 多键输入优先向上direction2 = Direction.UPelif input_listener.is_pressed(pygame.K_DOWN):direction2 = Direction.DOWNelse:direction2 = Direction.IDLEelif self.config.mode == "PVE":# player1if input_listener.is_pressed(pygame.K_w):  # 多键输入优先向上direction1 = Direction.UPelif input_listener.is_pressed(pygame.K_s):direction1 = Direction.DOWNelse:direction1 = Direction.IDLE# aiif 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()breakframe += 1pygame.display.update()clock.tick(self.config.fps)

人机策略类:

import jsonimport pygamefrom remake1.component.ball import Ball
from remake1.component.paddle import Paddle
from remake1.config import Configclass Replay:def __init__(self, filepath):with open(filepath, 'r') as f:self.data = json.load(f)# 从保存的数据还原Configself.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更多确实是用于强化学习方面的训练,后续会往这方面改,毕竟时代潮流在此,但参考目前代码框架想要二开应该难度不大,各位小伙伴可以自行尝试。

相关文章:

  • SLAM定位与地图构建
  • Linux之Yum源与Nginx服务篇
  • FramePack - 开源 AI 视频生成工具
  • 差分振荡器:支持0.15ps超低抖动的高速时钟核心
  • 基于React的高德地图api教程004:线标记绘制、修改、删除功能实现
  • 低功耗实现方法思路总结
  • 什么是Agentic AI(代理型人工智能)?
  • ESP32简介及相关使用
  • ubuntu服务器版启动卡在start job is running for wait for...to be Configured
  • 浪潮云边协同:赋能云计算变革的强力引擎
  • 鸿道Intewell操作系统:人形机器人底层操作系统
  • SQLPub:一个提供AI助手的免费MySQL数据库服务
  • 十、HQL:排序、联合与 CTE 高级查询
  • Linux515 rsync定时备份
  • 多链互操作性标准解析:构建下一代区块链互联生态
  • 4.6/Q1,GBD数据库最新文章解读
  • Seata源码—3.全局事务注解扫描器的初始化二
  • C语言经典笔试题目分析(持续更新)
  • 单物理机上部署多个TaskManager与调优 Flink 集群
  • Cinema4D 26.014
  • 小米汽车机盖门陷谈判僵局,车主代表称小米表示“退订会造成崩塌”
  • 《大风杀》导演张琪:为了不算计观众,拍了部不讨好的警匪片
  • 特朗普促卡塔尔说服伊朗放弃核计划,伊朗总统:你来吓唬我们?
  • 牛市早报|4月新增社融1.16万亿,降准今日正式落地
  • “典孝急乐批麻蚌赢”:互联网“八字真言”与当代赛博赢学
  • 为惩戒“工贼”,美国编剧工会“痛下杀手”