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()

2. 补充必要文件,如图片、音频等

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实现网络请求并获取网页内容。
    网络爬虫常用技术包括网络请求(有UrllibUrllib3Requests等模块)、请求headers处理(应对反爬虫策略)、网络超时异常处理、代理服务(解决访问受限问题)以及HTML解析(常用LXMLBeautifulSoup等模块)。
    还有多种网络爬虫框架,如Scrapy(快速、高层次的抓取框架)、Crawley(支持多种数据库和数据导出格式)、PySpider(功能强大,有浏览器界面操作等优势)。

  • Python基础数据结构与相关操作:

  • 序列
    序列涵盖列表、元组、字典、集合等。列表是有序可变序列,元素可不同,支持多种操作(如创建、访问、增删改、排序、推导式等);元组与列表类似但元素不可变;字典是无序可变的键值对集合,通过键访问值,可进行创建、遍历、添加修改删除元素等操作;集合是无序可变的无重复元素集合,支持添加、删除、交并差集等操作。

  • 字符串与正则表达式
    字符串支持拼接、重复、截断、分割合并、检索(如countfind方法,innot in运算符)、大小写转换、去除首尾字符以及格式化(%操作符和format方法)等多种操作。
    正则表达式用于字符串模式匹配,包含普通字符和特殊字符(元字符),通过re模块实现查找(match、search、findall)、替换(sub)和分割(split)等操作,可处理如匹配特定格式(QQ号、身份证号等)的复杂情况。

  • Python进阶主题:

  • 模块及异常处理
    模块是.py文件,用于提高代码重用性和可维护性。导入模块有importfrom...import两种方式,需注意函数名或模块名重名问题以及测试代码的编写(利用if name == 'main'语句)。
    包是包含__init__.py文件的目录,用于解决模块重名问题,导入包有多种方式。异常是程序运行时发生的错误,异常处理可控制错误影响,如处理除0异常等。

  • 文件及目录操作
    文件操作包括打开、读写(不同模式如r、w、a等对应不同操作方式)和关闭,os模块和os.path模块提供了众多与目录相关的函数,os.walk用于遍历目录。

2. 课程感想体会

  • 这次系统学习Python系列课程,对我来说是一次知识的深度拓展与技能的全方位提升。从最基础的序列、字符串操作,到较为复杂的网络编程、爬虫技术、数据库交互等内容,全面且连贯的课程体系让我对Python编程有了较为完整的认识。
  • 在基础部分,对列表、元组、字典和集合特性的深入学习,让我明白了如何根据实际需求选择合适的数据结构高效处理数据。例如,在需要频繁修改元素的场景下优先选用列表,而若要保证元素唯一性则使用集合。这些基础知识扎实了我的编程功底,让我在编写代码时更加得心应手。
  • 网络编程和爬虫技术的学习则打开了我获取网络数据的大门。理解Socket原理后,我知晓了网络通讯背后的机制,仿佛能洞察计算机之间如何“对话”。爬虫技术更是实用,从简单的urllib请求到运用各种技巧应对反爬虫策略,再到使用不同解析模块提取网页数据,让我具备了从网络收集公开信息的能力。这在如今数据驱动的时代,无疑为我提供了丰富的数据来源,能用于数据分析、信息监测等诸多领域。
  • 模块及异常处理的知识,使我的代码更加规范、健壮。合理运用模块划分功能,让代码结构清晰,便于维护和复用;异常处理机制则减少了程序因意外错误而崩溃的情况,提升了程序的可靠性。文件及目录操作学习,让我能高效管理本地数据存储,无论是读取文件内容还是操作目录结构,都能轻松应对。

3. 意见和建议

  • 课程内容方面:

  • 希望后续课程能增加更多实际项目案例,将各个知识点融合在一个完整的项目中进行讲解。

  • 对于一些较为抽象或复杂的概念(如Socket底层通信原理、正则表达式中一些高级匹配模式等),可以增加更多的图示、动画或类比解释。

  • 课程资源方面:

  • 除了现有的文档资料,可以补充一些视频讲解资源,尤其是对于代码演示部分,视频能更清晰地展示代码编写、运行和调试的全过程,可以更好地观察和学习代码的细节和技巧。

  • 整理一份课程知识点速查手册,以简洁明了的形式罗列各个知识点及其核心要点、常用方法和注意事项等,方便在实际开发中快速查阅。

posted @ 2025-06-10 11:47  chfg  阅读(53)  评论(1)    收藏  举报