1 Commits
Author SHA1 Message Date
zhiyuan 3c84db5f3d 新增描述 2026-08-22 21:41:19 +08:00
11 changed files with 51 additions and 422 deletions

No files matched your search

+2 -36
View File
@@ -1,36 +1,2 @@
### 项目技术方案
- 游戏引擎:pygame
- 游戏设计:自行实现多场景
### 项目结构
```
project
start.py # 入口,包含初始化和主循环
data\ # 资源
src\ # 代码
config\ # 基础配置
game_config.py # 游戏配置
scenes.py # 场景类字典,用于多场景切换
scenes\ # 游戏场景
base_scene.py # 场景基类,被其他场景继承,含画布、计时器、下一场景、事件接收、每帧更新、切换场景、退出等参数方法
main_menu_scene.py # 主菜单场景,实现了按键组合、按键顺序、鼠标事件、文本显示、雨天特效等功能
game_scene.py # 游戏场景,实现了方向移动、碰撞判断等功能
empty_scene.py # 空场景,测试或复制用
```
### 运行
安装依赖
```
pip install -r requirements.txt
```
运行
```
python start.py
```
### 执行逻辑
1. start.py中执行Game() # 开始游戏
2. 类执行初始化__init__() # 读取game_config.py中的配置,初始化窗口画布等,初始化首个场景MainMenuScene并记录到self.scene
3. 初始化函数调用run()开始主循环 # 循环中获取事件并发送给场景的handle_event()方法用于接收玩家输入
4. 主循环中调用self.scene.update()方法 # 每帧更新场景中实现的游戏逻辑
5. 场景切换通过改变self.scene实现 # 主循环中读取场景类的next_scene参数,为None不切换,否则切换为src\config\scenes.py中类实例化对象
6. 退出游戏通过设置running为False实现 # 主循环中读取场景类的next_scene参数,为"exit"时退出
### 项目1文档
### 新增描述
+2 -2
View File
@@ -3,8 +3,8 @@ import os
class GameConfig:
game_name = "Test Game"
window_x = 1920
window_y = 1080
window_x = 1920/2
window_y = 1080/2
bg_color = (0, 0, 0)
def data_path():
-6
View File
@@ -1,14 +1,8 @@
from src.scenes.config_scene import ConfigScene
from src.scenes.empty_scene import EmptyScene
from src.scenes.main_menu_scene import MainMenuScene
from src.scenes.game_scene import GameScene
from src.scenes.space_scene import SpaceScene
scene_dict = {
"MainMenuScene": MainMenuScene,
"GameScene": GameScene,
"EmptyScene": EmptyScene,
"ConfigScene": ConfigScene,
"SpaceScene": SpaceScene,
}
-60
View File
@@ -1,60 +0,0 @@
import math
import pygame
from src.config.color import Color
class Player:
def __init__(self, screen):
self.screen = screen
screen_rect = self.screen.get_rect()
self.x = screen_rect.centerx
self.y = screen_rect.centery
self.v = 0.0
self.base_speed = 3.0
self.run_multiplier = 2.5
self.is_running = False
def move(self, dx, dy, dt):
"""
归一化移动,避免斜向移动过快
"""
magnitude = math.hypot(dx, dy)
if magnitude > 0:
dx /= magnitude
dy /= magnitude
speed = self.base_speed
if self.is_running:
speed *= self.run_multiplier
# 应用速度和时间差 (dt),实现帧率无关的移动
dt /= 1000.0 # 注意:这里 dt 是以秒为单位的浮点数
self.x += dx * speed * dt * 60 # 乘以 60 是为了兼容 60FPS 下的体感速度
self.y += dy * speed * dt * 60
def handle_input(self, keys, dt):
drx = 0
dry = 0
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
drx -= 1
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
drx += 1
if keys[pygame.K_UP] or keys[pygame.K_w]:
dry -= 1
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
dry += 1
# 按下shift冲刺
if keys[pygame.K_LSHIFT]:
self.is_running = True
else:
self.is_running = False
# 移动
self.move(drx, dry, dt)
def show(self):
pygame.draw.circle(self.screen, Color.white, (int(self.x), int(self.y)), 10)
def update(self, keys, dt):
self.handle_input(keys, dt)
self.show()
+4 -9
View File
@@ -1,14 +1,15 @@
import pygame
class BaseScene:
def __init__(self, screen: pygame.Surface):
def __init__(self, screen: pygame.Surface, clock: pygame.time.Clock):
"""
初始化场景,接收并保存画布、计时器
"""
self.screen = screen
self.clock = clock
self.next_scene = None
def update(self, dt=0):
def update(self):
"""
场景的每帧更新逻辑
"""
@@ -24,10 +25,4 @@ class BaseScene:
"""
切换场景
"""
self.next_scene = scene_name
def exit(self):
"""
退出游戏
"""
self.next_scene = "exit"
self.next_scene = scene_name
-23
View File
@@ -1,23 +0,0 @@
import pygame
from src.scenes.base_scene import BaseScene
class ConfigScene(BaseScene):
name = "ConfigScene" # 场景名称
def __init__(self, screen):
super().__init__(screen)
def update(self, dt=0):
"""
场景每帧更新逻辑
"""
def handle_event(self, event):
"""
接收主循环传来的事件并处理
"""
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.switch_to_scene("MainMenuScene")
-18
View File
@@ -1,18 +0,0 @@
from src.scenes.base_scene import BaseScene
class EmptyScene(BaseScene):
name = "EmptyScene" # 场景名称
def __init__(self, screen):
super().__init__(screen)
def update(self, dt=0):
"""
场景每帧更新逻辑
"""
def handle_event(self, event):
"""
接收主循环传来的事件并处理
"""
+3 -4
View File
@@ -8,8 +8,8 @@ from src.scenes.base_scene import BaseScene
class GameScene(BaseScene):
name = "GameScene"
def __init__(self, screen):
super().__init__(screen)
def __init__(self, screen, clock):
super().__init__(screen, clock)
# 初始化音乐
pygame.mixer.init()
# 播放并循环音乐
@@ -32,9 +32,8 @@ class GameScene(BaseScene):
self.RED = (255, 0, 0)
self.BLUE = (0, 0, 255)
def update(self, dt=0):
def update(self):
if self.back_status:
pygame.mixer_music.stop()
self.switch_to_scene("MainMenuScene")
if self.Left_status and self.dirty_x >= 0:
self.dirty_x -= 1
+32 -196
View File
@@ -12,148 +12,56 @@ class MainMenuScene(BaseScene):
"""
主菜单场景,显示提示信息,展示雨天特效
"""
def __init__(self, screen):
def __init__(self, screen, clock):
"""
初始化函数,初始化按钮按下状态、雨滴列表
"""
super().__init__(screen)
super().__init__(screen, clock)
self.button_status = False
self.mouse_status = None
self.mouse_last_status = None
self.debug_status = True
self.secret_keys = ["w","w","s","s","a","d","a","d"] # 上上下下左右左右
self.secret_key_compare_num = 0
self.select_id = 0
self.up_status = False
self.up_last_status = False
self.down_status = False
self.down_last_status = False
self.mouse_on_option = False
self.keyboard_ensure_status = False
self.debug_status = False
# 设置字体
font_file = os.path.join(data_path(),"Dymon-ShouXieTi.otf")
self.font = pygame.font.Font(font_file,32)
self.text_rect_list = []
self.__init_text_list()
self.font = pygame.font.Font(font_file,16)
# 文本
self.game_info_text = self.font.render('按任意键开始游戏,鼠标点击退出', True, Color.white)
# 雨滴列表
self.rain_max = 50
self.rain_list = []
# 雨滴生命周期
self.rain_live_max = 10
self.rain_live_max = 6
# 初始化雨滴
self.init_rain(self.rain_max)
# 鼠标位置
self.mouse_pos = (0, 0)
def update(self, dt=0):
def update(self):
# 按键按下则切换场景
if self.button_status:
self.switch_to_scene("GameScene")
if self.secret_key_compare_num == len(self.secret_keys):
print("我去,是通关秘籍!")
self.switch_to_scene("EmptyScene")
if self.__up():
self.select_id -= 1
if self.__down():
self.select_id += 1
self.select_id = self.select_id % len(self.text_rect_list)
# 显示选项文本
self.__show_ui()
# 显示提示文本
screen_rect = self.screen.get_rect() # 获取屏幕矩形
text_rect = self.game_info_text.get_rect() # 获取文本矩形
self.__show(text_rect, (0, 15))
text_rect.center = screen_rect.center # 将文本居中到屏幕
self.screen.blit(self.game_info_text, text_rect.topleft)
# 处理鼠标点击
self.handle_mouse_click()
# 处理空格或enter
self.handle_keyboard()
# 调试信息,debug模式下显示
self.__show(screen_rect)
self.__show(text_rect,(0,30))
self.__show(text_rect.center,(0,45))
# 雨滴坠落
self.rain_fall(dt)
# 更新鼠标位置
self.mouse_pos = pygame.mouse.get_pos()
# 更新当前帧鼠标状态
self.mouse_last_status = self.mouse_status
# 更新当前帧上下按键状态
self.up_last_status = self.up_status
self.down_last_status = self.down_status
# 调试文本
# self.__show(self.up_status)
# self.__show(self.up_last_status, (0, 20))
self.rain_fall()
def handle_event(self, event):
"""
接收主循环传来的事件并处理
"""
if event.type == pygame.KEYDOWN:
keys = pygame.key.get_pressed() # 获取所有按键状态,用于组合按键判断
if event.key == pygame.K_ESCAPE:
self.exit() # 退出游戏
if keys[pygame.K_z] and keys[pygame.K_x]: # 同时按下Z和X
self.button_status = True
# 处理空格和enter
if event.key == pygame.K_SPACE or event.key == pygame.K_RETURN:
self.keyboard_ensure_status = True
else:
self.keyboard_ensure_status = False
# 判断是否按顺序按下秘籍
if event.unicode.lower() == self.secret_keys[self.secret_key_compare_num]:
self.secret_key_compare_num += 1
else:
self.secret_key_compare_num = 0
# 处理上下键
if event.key == pygame.K_UP or event.key == pygame.K_w:
self.up_status = True
if event.key == pygame.K_DOWN or event.key == pygame.K_s:
self.down_status = True
if event.type == pygame.KEYUP:
# 处理上下键
if event.key == pygame.K_UP or event.key == pygame.K_w:
self.up_status = False
if event.key == pygame.K_DOWN or event.key == pygame.K_s:
self.down_status = False
# 鼠标事件
if event.type == pygame.MOUSEBUTTONDOWN:
self.mouse_status = "down"
elif event.type == pygame.MOUSEBUTTONUP:
self.mouse_status = "up"
elif event.type == pygame.MOUSEMOTION:
self.mouse_status = "move"
def __click(self):
"""
判断是否点击鼠标:鼠标抬起时,上一帧的状态如果是按下或者移动则判断为点击
"""
if self.mouse_status == "up":
if self.mouse_last_status in ["down", "move"]:
return True
return False
def __up(self):
"""
判断是否按下上键:按下时,上一帧的状态是未按下则判断为按下
"""
if self.up_status:
if not self.up_last_status:
return True
return False
def __down(self):
"""
判断是否按下下键:按下时,上一帧的状态是未按下则判断为按下
"""
if self.down_status:
if not self.down_last_status:
return True
return False
self.button_status = True
elif event.type == pygame.MOUSEBUTTONDOWN:
return "exit" # 主循环接收返回,为exit时设置running为False,退出游戏
def __show(self, info, dest=(0, 0)):
"""
@@ -166,19 +74,6 @@ class MainMenuScene(BaseScene):
info_text = self.font.render(info, True, Color.white)
self.screen.blit(info_text, dest)
def handle_mouse_click(self):
"""
处理鼠标点击
"""
if self.__click() and self.mouse_on_option:
# 触发选项处理
self.__handle_option()
def handle_keyboard(self):
if self.keyboard_ensure_status:
# 触发选项处理
self.__handle_option()
def __rain_list_append(self, status=0):
"""
向列表中添加一个雨滴,默认状态为0
@@ -192,63 +87,6 @@ class MainMenuScene(BaseScene):
}
self.rain_list.append(rain)
def __init_text_list(self):
# 文本
text_list = [
"开始游戏",
"测试场景",
"设置",
"退出游戏",
]
# 转换为文本框
for text_str in text_list:
self.text_rect_list.append(self.font.render(text_str, True, Color.white))
def __handle_option(self):
"""
处理选项,根据id执行
"""
if self.select_id is None:
return
elif self.select_id == 0:
self.switch_to_scene("GameScene")
elif self.select_id == 1:
self.switch_to_scene("SpaceScene")
elif self.select_id == 2:
self.switch_to_scene("ConfigScene")
elif self.select_id == len(self.text_rect_list) - 1:
self.exit()
def __show_ui(self):
"""
显示所有选项
"""
screen_rect = self.screen.get_rect() # 获取屏幕矩形
self.mouse_on_option = False # 未选中选项时重置状态
count = 0
for text in self.text_rect_list:
text_rect = text.get_rect() # 获取文本矩形
text_rect.centerx = screen_rect.centerx # 将文本居中到屏幕
text_rect.y = screen_rect.y + count * 2 * text_rect.height + screen_rect.centerx/2
self.screen.blit(text, text_rect.topleft)
# 是否高亮显示
if text_rect.y < self.mouse_pos[1] < text_rect.y + text_rect.height \
and text_rect.x < self.mouse_pos[0] < text_rect.x + text_rect.width:
pygame.draw.rect(self.screen, Color.white, text_rect, 1)
# 更新选中id
self.select_id = count
# 更新鼠标状态
self.mouse_on_option = True
count += 1
# 显示键盘选中
if self.select_id is not None:
text_rect = self.text_rect_list[self.select_id].get_rect()
text_rect.centerx = screen_rect.centerx # 将文本居中到屏幕
text_rect.y = screen_rect.y + self.select_id * 2 * text_rect.height + screen_rect.centerx/2
pygame.draw.rect(self.screen, Color.white, text_rect, 1)
def init_rain(self, max_num):
"""
初始化雨滴列表,保存雨滴落点的坐标
@@ -258,35 +96,33 @@ class MainMenuScene(BaseScene):
self.__rain_list_append(status)
def rain_fall(self, dt):
def rain_fall(self):
"""
展示雨滴画面
"""
dt = self.clock.tick()
for rain in self.rain_list:
# 当状态在区间1时绘制雨滴线条
if rain["status"] <= self.rain_live_max - 3:
# 设计高度
rain_high = 10 * self.rain_live_max * 2
# 线条长度
line_len = 10*(self.rain_live_max - rain["status"])
# 起点位置
# x坐标与最后落点x一致
x = rain["dest"][0]
# y坐标为落点位置向上移动设计高度后,加上生命周期落下的位置
y = (rain["dest"][1] - rain_high) + (rain["status"] * self.rain_live_max) * 2
y2 = y + 20
y = rain["dest"][1] - line_len
line_start_point = (x, y)
line_end_point = (x, y2)
pygame.draw.line(self.screen, Color.white, line_start_point, line_end_point, 1)
pygame.draw.line(self.screen, Color.white, line_start_point, rain["dest"], 1)
# 状态在区间2时绘制涟漪
elif self.rain_live_max - 6 < rain["status"]:
point_width = (rain["status"] - (self.rain_live_max - 3)) * 2
elif self.rain_live_max - 3 < rain["status"]:
point_width = rain["status"] - (self.rain_live_max - 3)
pygame.draw.circle(self.screen, Color.white, rain["dest"], point_width, 1)
# # 椭圆
# rain_point_rect = [rain["dest"][0], rain["dest"][1], rain["status"], rain["status"]]
# pygame.draw.ellipse(self.screen, Color.white, rain_point_rect, 1)
# 雨滴状态每d_status毫秒变化一次
d_status = 1000/30
d_status = 200
if rain["timer"] > d_status:
rain["status"] += 1
rain["timer"] -= d_status # 不设为0,避免累计时间误差
-50
View File
@@ -1,50 +0,0 @@
import os
import pygame
from src.config.color import Color
from src.config.game_config import data_path, GameConfig
from src.entity.player import Player
from src.scenes.base_scene import BaseScene
class SpaceScene(BaseScene):
name = "SpaceScene" # 场景名称
def __init__(self, screen):
super().__init__(screen)
# 实例化玩家
self.player = Player(screen)
# 设置字体
font_file = os.path.join(data_path(), "Dymon-ShouXieTi.otf")
self.font = pygame.font.Font(font_file, 32)
def update(self, dt=0):
"""
场景每帧更新逻辑
"""
# 玩家更新,接收键盘输入与每帧间隔
self.player.update(pygame.key.get_pressed(), dt)
self.__show("加速:"+str(self.player.is_running))
def handle_event(self, event):
"""
接收主循环传来的事件并处理
"""
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.switch_to_scene("MainMenuScene")
def __show(self, info, dest=(0, 0)):
"""
显示调试文本
"""
if not isinstance(info, str):
info = str(info)
info_text = self.font.render(info, True, Color.white)
self.screen.blit(info_text, dest)
+8 -18
View File
@@ -1,9 +1,8 @@
import os
import pygame
from src.config.game_config import GameConfig
from src.config.scenes import scene_dict
from src.scenes.main_menu_scene import MainMenuScene
class Game:
@@ -11,23 +10,17 @@ class Game:
# 初始化
pygame.init()
# 显示输入法候选框
os.environ["SDL_IME_SHOW_UI"] = "1"
# 设置窗口大小
size = (GameConfig.window_x, GameConfig.window_y)
self.screen = pygame.display.set_mode(size, pygame.NOFRAME)
# 避免每次启动需要切换输入法
pygame.key.stop_text_input()
# 设置计时器
self.clock = pygame.time.Clock()
# 场景字典
self.game_scenes = scene_dict
# 当前场景
self.scene = self.game_scenes["MainMenuScene"](self.screen)
self.scene = self.game_scenes["MainMenuScene"](self.screen, self.clock)
# 运行循环
self.running = True
@@ -35,31 +28,28 @@ class Game:
def run(self):
while self.running:
# 更新每帧时间
dt = self.clock.tick()
# 事件处理
for event in pygame.event.get(): # 这里已经获取了事件,事件队列被清空,在场景的update中再次获取将没有事件
if event.type == pygame.QUIT:
self.running = False
self.scene.handle_event(event) # 当前场景接收事件
result = self.scene.handle_event(event) # 当前场景接收事件
if result == "exit":
self.running = False
# 检查是否切换场景
if self.scene.next_scene is not None:
if self.scene.next_scene in self.game_scenes.keys():
scene_class = self.game_scenes[self.scene.next_scene]
self.scene = scene_class(self.screen)
self.scene = scene_class(self.screen, self.clock)
# 清屏
self.screen.fill(GameConfig.bg_color)
# 场景每帧更新逻辑运行
self.scene.update(dt)
# 当场景的下一场景为exit时退出游戏
if self.scene.next_scene == "exit":
self.running = False
self.scene.update()
# 更新显示
pygame.display.update()
if __name__ == "__main__":
Game()
Game()