20242214 实验四《Python程序设计》实验报告
20242214 2024-2025-2 《Python程序设计》实验四报告
课程:《Python程序设计》
班级: 2422
姓名: 陈冯
学号:20242214
实验教师:王志强
实验日期:2025年5月14日
必修/选修: 公选课
一、实验内容
制作一个游戏————草率版的《丝之歌》
众所不周知,丝之“鸽”预计将于今年出版。但是作为一个“幸运”许久的空洞玩家,还是难以相信。
长期处于“幸压抑”状态下的偶,终于决定手搓一个《丝之歌》出来,所以就以此作为Python的结课作业了。
二、实验过程及结果
1. 编写程序,完善操控系统
- 步骤:
- 在B站上搜寻是否有相关资源,找到了唯一一位用Python制作空洞类游戏up主的视频我用python制作了一个《空洞骑士》——其中的一次战斗
- 在这位up制作的也很草率,且复制的是空洞的第一个小Boss——龙牙哥,在参考了这位up的思路后,自行编写了一份代码。
- 由于代码问题过多,且确实不太严谨,如在角色操作和怪物AI的攻击上都要所欠缺,所以向AI提出疑问并修改。
- 源代码:
import pygame
import sys
import random
# 初始化pygame
pygame.init()
pygame.mixer.init()
# 屏幕设置
SCREEN_WIDTH = 1360
SCREEN_HEIGHT = 680
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("空洞骑士:丝之歌之战")
# 颜色定义
BLACK = (0, 0, 0)
# 加载图片资源函数
def load_image(img_name):
try:
image = pygame.image.load(f"resources/{img_name}")
print(f"成功加载图像: {img_name}")
return image
except pygame.error as e:
print(f"无法加载图像: {img_name}, 错误信息: {e}")
surf = pygame.Surface((100, 100))
surf.fill((255, 0, 255))
return surf
# 游戏主类
class Game:
def __init__(self):
self.state = 0 # 0: 标题界面, 1: 游戏界面, 2: 结果界面
self.title_image = load_image("title.png")
self.victory_image = load_image("victory.png")
self.defeat_image = load_image("defeat.png")
self.font = pygame.font.SysFont('SimHei', 36) # 使用中文字体
self.music_playing = False
self.current_music = None
# 初始化时播放标题音乐
self.background_music()
def update(self, knight_life, monster_life):
# 更新游戏状态
if self.state == 0:
self.title_screen()
elif self.state == 1:
# 游戏进行中
if knight_life <= 0 or monster_life <= 0:
self.state = 2
self.background_music()
elif self.state == 2:
self.result_screen(knight_life, monster_life)
def title_screen(self):
screen.blit(self.title_image, (0, 0))
# 添加开始游戏提示
start_text = self.font.render("按任意键开始游戏", True, (255, 255, 255))
screen.blit(start_text, (SCREEN_WIDTH//2 - start_text.get_width()//2, SCREEN_HEIGHT//2 + 150))
def result_screen(self, knight_life, monster_life):
if knight_life <= 0:
screen.blit(self.defeat_image, (0, 0))
result_text = self.font.render("战斗失败!再接再厉!", True, (255, 255, 255))
else:
screen.blit(self.victory_image, (0, 0))
result_text = self.font.render("恭喜胜利!成功击败灵魂大师!", True, (255, 255, 255))
screen.blit(result_text, (SCREEN_WIDTH//2 - result_text.get_width()//2, SCREEN_HEIGHT//2 + 150))
restart_text = self.font.render("按空格键返回主菜单", True, (255, 255, 255))
screen.blit(restart_text, (SCREEN_WIDTH//2 - restart_text.get_width()//2, SCREEN_HEIGHT//2 + 200))
def background_music(self):
# 根据游戏状态加载背景音乐
try:
if self.current_music != self.state:
# 停止当前音乐
pygame.mixer.music.stop()
# 加载并播放新音乐
if self.state == 0:
pygame.mixer.music.load("resources/title_music.mp3")
elif self.state == 1:
pygame.mixer.music.load("resources/battle_music.mp3")
elif self.state == 2:
pygame.mixer.music.load("resources/result_music.mp3")
pygame.mixer.music.play(-1) # -1表示循环播放
self.current_music = self.state
self.music_playing = True
print(f"播放状态 {self.state} 的音乐")
except Exception as e:
print(f"无法加载音乐: {e}")
# 空洞骑士类
class Knight:
def __init__(self):
# 加载骑士动画帧
self.stand_right = load_image("knight_r.png")
self.run_right = [load_image(f"knight_run_{i}.png") for i in range(1, 7)]
self.run_left = [pygame.transform.flip(img, True, False) for img in self.run_right]
self.attack_right = [load_image(f"knight_attack_r_{i}.png") for i in range(1, 4)]
self.attack_left = [pygame.transform.flip(img, True, False) for img in self.attack_right]
self.action = "stand" # 当前动作:stand, run, attack
self.direction = 1 # 1: 向右, -1: 向左
self.frame = 0 # 动画帧计数
# 物理属性
self.x = 200
self.y = 505
self.speed_x = 0
self.speed_y = 0
self.jump_speed = -15 # 跳跃速度
self.gravity = 0.5 # 重力加速度
self.is_jumping = False
# 状态属性
self.life = 4 # 生命值
self.attack_cooldown = 0 # 攻击冷却
self.invincible = 0 # 无敌时间(帧)
self.knockback = 0 # 击退时间
# 音效
self.jump_sound = self._load_sound("jump.wav")
self.attack_sound = self._load_sound("attack.wav")
self.hurt_sound = self._load_sound("hurt.wav")
def _load_sound(self, sound_name):
try:
return pygame.mixer.Sound(f"resources/{sound_name}")
except FileNotFoundError:
print(f"无法加载音效: {sound_name}")
return pygame.mixer.Sound()
def update(self, keys, monster_rect):
# 处理输入
self.handle_input(keys)
# 更新物理状态
self.apply_physics()
# 更新动画
self.update_animation()
# 检测碰撞
self.check_collision(monster_rect)
# 更新无敌状态和击退效果
if self.invincible > 0:
self.invincible -= 1
if self.knockback > 0:
self.knockback -= 1
self.speed_x = 5 * self.direction # 击退效果
# 更新攻击冷却
if self.attack_cooldown > 0:
self.attack_cooldown -= 1
def handle_input(self, keys):
# 水平移动
self.speed_x = 0
if keys[pygame.K_a]:
self.speed_x = -5
self.direction = -1
if not self.is_jumping:
self.action = "run"
if keys[pygame.K_d]:
self.speed_x = 5
self.direction = 1
if not self.is_jumping:
self.action = "run"
if self.speed_x == 0 and not self.is_jumping:
self.action = "stand"
# 跳跃
if (keys[pygame.K_w] or keys[pygame.K_SPACE]) and not self.is_jumping:
self.speed_y = self.jump_speed
self.is_jumping = True
self.jump_sound.play()
# 攻击
if keys[pygame.K_j] and self.attack_cooldown == 0:
self.attack_cooldown = 15 # 减少冷却时间
self.action = "attack"
self.frame = 0
self.attack_sound.play()
def apply_physics(self):
# 应用重力
if self.is_jumping:
self.speed_y += self.gravity
# 边界检查
if self.y >= 505:
self.y = 505
self.is_jumping = False
self.speed_y = 0
if self.x < 0:
self.x = 0
if self.x > SCREEN_WIDTH - 100: # 假设骑士宽度为100
self.x = SCREEN_WIDTH - 100
# 更新位置
self.x += self.speed_x
self.y += self.speed_y
def update_animation(self):
# 更新动画帧
if self.action == "stand":
if self.direction == 1:
image = self.stand_right
else:
image = pygame.transform.flip(self.stand_right, True, False)
elif self.action == "run":
self.frame = (self.frame + 1) % 6
if self.direction == 1:
image = self.run_right[self.frame]
else:
image = self.run_left[self.frame]
elif self.action == "attack":
if self.frame < len(self.attack_right):
self.frame += 1
if self.direction == 1:
image = self.attack_right[self.frame - 1]
else:
image = self.attack_left[self.frame - 1]
else:
self.action = "stand"
self.frame = 0
# 绘制角色,考虑无敌帧闪烁效果
if self.invincible == 0 or self.invincible % 3 == 0: # 每三帧闪烁一次
screen.blit(image, (self.x, self.y))
def check_collision(self, rect):
# 简单矩形碰撞检测
if self.invincible == 0: # 只有在非无敌状态时才会受到伤害
knight_rect = pygame.Rect(self.x, self.y, 100, 100)
if knight_rect.colliderect(rect):
self.hurt()
self.hurt_sound.play()
def hurt(self):
# 受伤处理
self.life -= 1
self.invincible = 60 # 无敌时间(帧)
self.knockback = 30 # 击退时间(帧)
def draw_attack_box(self):
# 绘制攻击判定框
if self.action == "attack" and self.frame >= 1 and self.frame <= 2:
width = 100 if self.direction == 1 else -100
rect = pygame.Rect(self.x + (100 if self.direction == 1 else -100),
self.y + 30, abs(width), 60)
# pygame.draw.rect(screen, (255, 0, 0, 128), rect, 2) # 可取消注释查看攻击判定框
return rect
return None
# 守护者类
class Monster:
def __init__(self):
# 加载怪物动画帧
self.stand_left = load_image("monster_stand_l.png")
self.move_left = [load_image(f"monster_move_l_{i}.png") for i in range(1, 4)]
self.attack_left = [load_image(f"monster_attack_l_{i}.png") for i in range(1, 7)]
self.stand_right = pygame.transform.flip(self.stand_left, True, False)
self.move_right = [pygame.transform.flip(img, True, False) for img in self.move_left]
self.attack_right = [pygame.transform.flip(img, True, False) for img in self.attack_left]
self.action = "stand"
self.direction = -1 # -1: 向左, 1: 向右
self.frame = 0
# 物理属性
self.x = SCREEN_WIDTH - 300 # 调整初始位置
self.y = 255
self.speed_x = 0
self.speed_y = 0
self.jump_speed = -12
self.gravity = 0.4
self.is_jumping = False
# 状态属性
self.life = 20
self.attack_cooldown = 0
self.attack_distance = 400 # 远程攻击距离
self.melee_distance = 200 # 近战攻击距离
# 音效
self.attack_sound = self._load_sound("monster_attack.wav")
self.hurt_sound = self._load_sound("monster_hurt.wav")
self.jump_sound = self._load_sound("monster_jump.wav")
def _load_sound(self, sound_name):
try:
return pygame.mixer.Sound(f"resources/{sound_name}")
except:
print(f"无法加载音效: {sound_name}")
return pygame.mixer.Sound()
def update(self, knight_rect):
# 更新怪物行为
self.behavior(knight_rect)
# 应用物理
self.apply_physics()
# 更新攻击冷却
if self.attack_cooldown > 0:
self.attack_cooldown -= 1
def behavior(self, knight_rect):
# 根据与玩家的距离选择行为
distance = knight_rect.x - self.x
# 根据距离决定方向
if distance > 0:
self.direction = 1
else:
self.direction = -1
if self.attack_cooldown == 0:
if abs(distance) > self.attack_distance:
# 远距离 - 移动接近
self.speed_x = 3 * (1 if distance > 0 else -1)
self.action = "move"
elif abs(distance) > self.melee_distance:
# 中距离 - 随机选择近战攻击
if random.random() > 0.5:
self.attack("melee")
else:
self.action = "move"
self.speed_x = 3 * (1 if distance > 0 else -1)
else:
# 近距离 - 强制远程攻击
self.attack("ranged")
else:
self.speed_x = 0
def apply_physics(self):
# 应用重力
if self.is_jumping:
self.speed_y += self.gravity
# 边界检查
if self.y >= 255:
self.y = 255
self.is_jumping = False
self.speed_y = 0
if self.x < 0:
self.x = 0
if self.x > SCREEN_WIDTH - 150: # 假设怪物宽度为150
self.x = SCREEN_WIDTH - 150
# 更新位置
self.x += self.speed_x
self.y += self.speed_y
def update_animation(self):
# 更新动画帧
if self.action == "stand":
if self.direction == 1:
image = self.stand_right
else:
image = self.stand_left
elif self.action == "move":
self.frame = (self.frame + 1) % 3
if self.direction == 1:
image = self.move_right[self.frame]
else:
image = self.move_left[self.frame]
elif self.action == "attack":
if self.frame < len(self.attack_right):
self.frame += 1
if self.direction == 1:
image = self.attack_right[self.frame - 1]
else:
image = self.attack_left[self.frame - 1]
else:
self.action = "stand"
self.frame = 0
self.attack_cooldown = 0
# 绘制怪物
screen.blit(image, (self.x, self.y))
def draw_attack_box(self):
# 绘制怪物攻击判定框
if self.action == "attack" and 3 <= self.frame <= 5:
# 在攻击动画的某些帧释放攻击判定
attack_rect = pygame.Rect(self.x + (200 if self.direction == 1 else -150),
self.y + 50, 150, 100)
# pygame.draw.rect(screen, (0, 0, 255, 128), attack_rect, 2) # 可取消注释查看攻击判定框
return attack_rect
return None
def attack(self, attack_type):
# 攻击逻辑
self.attack_cooldown = 60 # 攻击冷却时间(帧)
self.action = "attack"
self.frame = 0
self.attack_sound.play()
def take_damage(self, amount):
# 受伤处理
self.life -= amount
self.hurt_sound.play()
# 游戏主循环
def main():
# 创建游戏对象
game = Game()
knight = Knight()
monster = Monster()
# 获取游戏字体
font = pygame.font.SysFont('SimHei', 36)
# 游戏主循环
clock = pygame.time.Clock()
running = True
# 确保初始音乐播放
game.background_music()
while running:
clock.tick(60) # 60 FPS
# 处理事件 - 统一在主循环中处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
# 标题界面按任意键开始游戏
if game.state == 0:
game.state = 1
game.background_music()
# 结果界面按空格键返回主菜单
elif game.state == 2 and event.key == pygame.K_SPACE:
game.state = 0
knight = Knight()
monster = Monster()
game.background_music()
# 清空屏幕
screen.fill(BLACK)
# 更新游戏状态
if game.state == 0:
game.title_screen()
elif game.state == 1:
# 获取键盘状态
keys = pygame.key.get_pressed()
# 更新骑士
knight_rect = pygame.Rect(knight.x, knight.y, 100, 100)
knight_attack_rect = knight.draw_attack_box()
knight.update(keys, pygame.Rect(monster.x, monster.y, 150, 150))
# 更新怪物
monster.update(pygame.Rect(knight.x, knight.y, 100, 100))
# 绘制游戏背景
field = load_image("field.png")
if field:
screen.blit(field, (0, 0))
# 绘制角色和怪物
knight.update_animation()
monster.update_animation()
# 绘制生命值
knight_life_text = font.render(f"骑士生命: {knight.life}", True, (255, 255, 255))
monster_life_text = font.render(f"怪物生命: {monster.life}", True, (255, 255, 255))
screen.blit(knight_life_text, (20, 20))
screen.blit(monster_life_text, (SCREEN_WIDTH - monster_life_text.get_width() - 20, 20))
# 检测攻击碰撞
monster_attack_rect = monster.draw_attack_box()
# 骑士攻击怪物
if knight_attack_rect:
monster_rect = pygame.Rect(monster.x, monster.y, 150, 150)
if monster_rect.colliderect(knight_attack_rect):
monster.take_damage(1)
# 怪物攻击骑士
if monster_attack_rect and knight_rect.colliderect(monster_attack_rect) and knight.invincible == 0:
knight.hurt()
# 检查游戏结束条件
if knight.life <= 0 or monster.life <= 0:
game.state = 2
game.background_music()
elif game.state == 2:
game.result_screen(knight.life, monster.life)
# 更新屏幕
pygame.display.flip()
pygame.quit()
sys.exit()
if __name__ == "__main__":
main()
- 结果:


- 事实上这段代码最大的问题在于,在灵魂大师攻击后会直接闪退,但面前没有找到解决方案。
- 完整视频如下:
20242214 2024-2025-2 《Python程序设计》实验四报告
2. 补充必要文件,如图片、音频等
- 步骤:
- 在STEAM文件中搜索未果后,在网络上搜索相关资源。
黄蜂女图片
灵魂大师
空洞骑士2丝之歌 - 但是仅凭网络上的少数资源,仍无法满足游戏所需。在B站上搜索到了几位大佬编写的《丝之歌》,因此借助了他们的图片和音频数据。
【EasyX】C++复刻《丝之歌》Boss战 - 找到了官方的资源图片网址。
角色动画素材网站 - 结果:
- 子文件中添加文件,确保文件名与代码中一致。

3. 程序代码托管到码云
- 步骤:
- 在码云上创建新仓库,获取仓库URL。
- 在VScode中配置Git,初始化本地仓库,添加远程仓库。
- 提交代码到本地仓库,并推送到码云仓库。
- 命令栏输入
git add .``git commit -m "提交说明"``git push日常提交代码 - 结果:
- 在VScode中链接码云,编写好程序后可直接拉取,上转至码云。
- 成功将工程项目代码托管到码云上,实现了版本控制和代码分享。
[https://gitee.com/cf2006/python]
三、实验过程中遇到的问题和解决过程
问题1:在运行时出现文件无法运行,难以继续的情况。
问题1解决方案:
pygame下载有误,之前误下多个Python。- 删除无用的库后,重新运行下载。
问题2:在运行时出现音频无法读取的情况。
问题2解决方案:
- 音频打开无损坏,将
ogg改为更适配Python的wav仍有误,于是直接用mp3格式运行。
其他(感悟、思考等)
- 在自己编写了一个代码后,深感游戏制作不易,也对只有三人的《空洞骑士》制作团队深感敬意。但是我还是想知道,为什么《丝之歌》鸽了5年???!!!
全课报告
1. 全课总结
-
Python网络编程技术:
-
Socket技术
Socket即套接字,由IP地址和端口组成,是应用程序进行网络通讯的关键接口。分为低级别Socket(提供标准BSD Sockets API,可访问底层操作系统接口)和高级别Socket(如SocketServer模块,简化网络服务器开发)。
Socket原理类似打电话过程,服务器端先初始化Socket、绑定端口、监听并等待客户端连接;客户端则初始化Socket后连接服务器,双方建立连接后可发送和接收数据。
Socket模块涉及不同类型(如AF_UNIX、AF_INET等)和多种函数(服务端有bind、listen、accept,客户端有connect等,公共函数包括recv、send等),需注意TCP和UDP的区别(如TCP面向连接,UDP面向无连接)。 -
网络爬虫技术
网络爬虫是模拟人浏览网站行为获取信息的工具,Python因多种优势成为热门爬虫语言。简单Demo通过urllib.request实现网络请求并获取网页内容。
网络爬虫常用技术包括网络请求(有Urllib、Urllib3、Requests等模块)、请求headers处理(应对反爬虫策略)、网络超时异常处理、代理服务(解决访问受限问题)以及HTML解析(常用LXML、BeautifulSoup等模块)。
还有多种网络爬虫框架,如Scrapy(快速、高层次的抓取框架)、Crawley(支持多种数据库和数据导出格式)、PySpider(功能强大,有浏览器界面操作等优势)。 -
Python基础数据结构与相关操作:
-
序列
序列涵盖列表、元组、字典、集合等。列表是有序可变序列,元素可不同,支持多种操作(如创建、访问、增删改、排序、推导式等);元组与列表类似但元素不可变;字典是无序可变的键值对集合,通过键访问值,可进行创建、遍历、添加修改删除元素等操作;集合是无序可变的无重复元素集合,支持添加、删除、交并差集等操作。 -
字符串与正则表达式
字符串支持拼接、重复、截断、分割合并、检索(如count、find方法,in和not in运算符)、大小写转换、去除首尾字符以及格式化(%操作符和format方法)等多种操作。
正则表达式用于字符串模式匹配,包含普通字符和特殊字符(元字符),通过re模块实现查找(match、search、findall)、替换(sub)和分割(split)等操作,可处理如匹配特定格式(QQ号、身份证号等)的复杂情况。 -
Python进阶主题:
-
模块及异常处理
模块是.py文件,用于提高代码重用性和可维护性。导入模块有import和from...import两种方式,需注意函数名或模块名重名问题以及测试代码的编写(利用if name == 'main'语句)。
包是包含__init__.py文件的目录,用于解决模块重名问题,导入包有多种方式。异常是程序运行时发生的错误,异常处理可控制错误影响,如处理除0异常等。 -
文件及目录操作
文件操作包括打开、读写(不同模式如r、w、a等对应不同操作方式)和关闭,os模块和os.path模块提供了众多与目录相关的函数,os.walk用于遍历目录。
2. 课程感想体会
- 这次系统学习Python系列课程,对我来说是一次知识的深度拓展与技能的全方位提升。从最基础的序列、字符串操作,到较为复杂的网络编程、爬虫技术、数据库交互等内容,全面且连贯的课程体系让我对Python编程有了较为完整的认识。
- 在基础部分,对列表、元组、字典和集合特性的深入学习,让我明白了如何根据实际需求选择合适的数据结构高效处理数据。例如,在需要频繁修改元素的场景下优先选用列表,而若要保证元素唯一性则使用集合。这些基础知识扎实了我的编程功底,让我在编写代码时更加得心应手。
- 网络编程和爬虫技术的学习则打开了我获取网络数据的大门。理解Socket原理后,我知晓了网络通讯背后的机制,仿佛能洞察计算机之间如何“对话”。爬虫技术更是实用,从简单的urllib请求到运用各种技巧应对反爬虫策略,再到使用不同解析模块提取网页数据,让我具备了从网络收集公开信息的能力。这在如今数据驱动的时代,无疑为我提供了丰富的数据来源,能用于数据分析、信息监测等诸多领域。
- 模块及异常处理的知识,使我的代码更加规范、健壮。合理运用模块划分功能,让代码结构清晰,便于维护和复用;异常处理机制则减少了程序因意外错误而崩溃的情况,提升了程序的可靠性。文件及目录操作学习,让我能高效管理本地数据存储,无论是读取文件内容还是操作目录结构,都能轻松应对。
3. 意见和建议
-
课程内容方面:
-
希望后续课程能增加更多实际项目案例,将各个知识点融合在一个完整的项目中进行讲解。
-
对于一些较为抽象或复杂的概念(如Socket底层通信原理、正则表达式中一些高级匹配模式等),可以增加更多的图示、动画或类比解释。
-
课程资源方面:
-
除了现有的文档资料,可以补充一些视频讲解资源,尤其是对于代码演示部分,视频能更清晰地展示代码编写、运行和调试的全过程,可以更好地观察和学习代码的细节和技巧。
-
整理一份课程知识点速查手册,以简洁明了的形式罗列各个知识点及其核心要点、常用方法和注意事项等,方便在实际开发中快速查阅。
浙公网安备 33010602011771号