• 20181218 实验四《Python程序设计》实验报告


    20181218 2019-2020-2 《Python程序设计》实验四报告

    课程:《Python程序设计》
    班级: 1812
    姓名:
    学号:20181218
    实验教师:王志强
    实验日期:2020年6月13日
    必修/选修: 公选课

    1.实验内容

    使用pygame编程制作简单的塔防游戏。

    2. 实验过程及结果

    指导

    首先制作一个思维导图,主要包括涉及的类,以及类的属性和方法。这并不是代码最终实现的版本,在编程时有所修改。

    资产

    游戏中使用的图片,部分来自于https://craftpix.net/中的免费资源,部分是我自己画的(比如游戏背景)
    游戏中使用的背景音乐,来自于无版权音乐网站https://maoudamashii.jokersounds.com/

    编程

    游戏的代码实现学习自https://www.youtube.com/watch?v=iLHAKXQBOoA
    原作者在12小时的直播中完成的代码,我完整观看了12个小时的录播,跟随原作者实现代码,也有一点自己的修改
    代码的实现过程基本如下:
    实现敌人的移动
    实现攻击塔的攻击
    实现支援塔的支援
    实现敌人攻击、游戏失败
    实现塔的拖动放置、升级
    实现敌人的多轮攻势
    实现游戏暂停、音乐暂停
    实现游戏总菜单面板
    代码中较核心的功能的具体实现将在 “3.实验过程中遇到的问题和解决过程”中给出

    结果

    游戏功能如下,具体运行测试将在视频中给出
    游戏控制
    点击“开始”按钮,游戏即开始
    “音乐”按钮可以控制音乐的暂停和播放
    “继续”和“暂停”按钮可以控制游戏的进行,敌人的每一波攻势结束后,游戏会自动暂停,点击按钮即可迎接下一波攻势
    点击塔,移动光标至目标位置再次点击,即可放置塔
    注意,不可以将塔放在已放置的塔之上
    在游戏“暂停”时也可以放置塔
    点击已放置的塔,可以进行升级,升级后塔的攻击力提高,外形也会改变
    游戏资产
    “月亮”是购买和升级塔所需的货币,消灭敌人会获得“月亮”
    “心”是玩家的生命值,当敌人走到地图尽头,“心”的数量会减少,当“心”的数量减少至0时,游戏结束
    “尖石塔”和“巨石塔”是可以攻击敌人的塔
    “尖石塔”攻击范围较大,攻击力较小
    “巨石塔”攻击范围较小,攻击力较大
    “宝剑塔”和“波纹塔”是用于强化“尖石塔”和“巨石塔”的塔
    “宝剑塔”可以提升范围内“尖石塔”和“巨石塔”的攻击力
    “波纹塔”可以提升范围内“尖石塔”和“巨石塔”的攻击范围
    小结
    游戏难度梯度较不合理,但具备基本功能

    码云链接

    https://gitee.com/python_programming/sl_20181218/tree/master/TowerDefence

    目录树如下:

    ├─enemies
    │  └─__pycache__
    ├─game_assets
    │  ├─enemies
    │  │  ├─1
    │  │  ├─2
    │  │  └─4
    │  └─towers
    │      ├─stones
    │      │  ├─1
    │      │  ├─2
    │      │  └─3
    │      ├─stonetower
    │      │  ├─1
    │      │  ├─2
    │      │  └─3
    │      └─support_towers
    ├─main_menu
    │  └─__pycache__
    ├─menu
    │  └─__pycache__
    ├─towers
    │  └─__pycache__
    └─__pycache__
    
    

    game_assets中存放的是游戏资产,其余文件夹存放的都是代码文件。
    运行游戏需要运行 run.py

    3. 实验过程中遇到的问题和解决过程

    1.如何实现敌人的移动?

    首先得到一个含许多坐标点的列表,坐标基本如下图

    每一次移动,首先要得到两个点,即目前所在路径的起点(x1, y1)和终点(x2, y2)

    得到这条路径的长度sqrt((x2-x1)**2+(y2-y2)**2)
    然后确定一次移动的方向和距离,方向即(x2-x1, y2-y2)
    至于一次移动的距离,在x轴方向,可以由上面的方向变量的x坐标除以当前路径长度的x坐标,再乘上移动速度
    移动后,更新敌人实例的x和y坐标即可
    代码实现如下:

    编写测试鼠标点击坐标的测试代码,鼠标在地图的敌人移动路径的关键处点击,获得一个坐标列表。

      def run(self):
            run = True
            clock = pygame.time.Clock()
            while run:
                clock.tick(60)
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        run = False
    
    
                    pos = pygame.mouse.get_pos()
    
                    if event.type == pygame.MOUSEBUTTONDOWN:
                        self.clicks.append(pos)
                        print(pos)
    
                self.draw()
                
            pygame.quit()
        def draw(self):
            self.win.blit(self.bg,(0,0))
            for p in self.clicks:
                pygame.draw.circle(self.win,(255,0,0),(p[0],p[1]),5,0)
            pygame.display.update()
    

    然后给enemy类编写move方法:

        def __init__(self):
            self.width = 64
            self.height = 64
            self.imgs = []
            self.animation_count = 0
            self.health = 1
            self.vel = 3
            self.path = [(1, 178), (380, 173), (438, 241), (465, 316), (630, 314), (736, 181), (942, 179), (943, 481), (5,481),(-20,481),(-30,481)] # 鼠标测试得到的列表
            self.x = self.path[0][0]
            self.y = self.path[0][1]
            self.img = None
            self.dis = 0
            self.path_pos = 0
            self.move_dis = 0
            self.imgs = []
            self.flipped = False
            self.max_health = 0
            self.speed_increase = 1.5
    
        def move(self):
            """
            Move enemy
            :return:
            """
            self.animation_count += 1
            x1, y1 = self.path[self.path_pos] # 现在路径的起点
            if self.path_pos + 1 >= len(self.path):
                x2, y2 = (-10, 481) # 移动到地图外
            else:
                x2, y2 = self.path[self.path_pos + 1] # 现在路径的终点(下一个目标点)
    
            move_dis = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) # 现在路径的长度
    
            dirn = (x2 - x1, y2 - y1) # 方向
            length = math.sqrt((dirn[0])**2+(dirn[1])**2) # 现在路径的长度
            dirn = (dirn[0]/length * self.speed_increase, dirn[1]/length * self.speed_increase) # 一次移动的方向和距离
            if dirn[0]<0 and not(self.flipped): # 当敌人在x方向上转向时要将其动画水平翻转
                self.flipped = True
                for x,img in enumerate(self.imgs):
                    self.imgs[x] = pygame.transform.flip(img, True, False)
    
            move_x, move_y = (self.x + dirn[0], self.y + dirn[1]) 
    
            self.dis += math.sqrt((move_x - x1) ** 2 + (move_y - y1) ** 2) # 在现在路径上总共移动的距离
            self.x = move_x  # 重置敌人动画所在位置
            self.y = move_y
            # Go to next point
            # 确定移动方向
            if dirn[0] >= 0: # moving right
                if dirn[1] >=0: # moving down
                    if self.x >= x2 and self.y >= y2:
                        self.dis = 0
                        self.move_count = 0
                        self.path_pos += 1
                elif dirn[1] <0: # moving up
                    if self.x >= x2 and self.y <= y2:
                        self.dis = 0
                        self.move_count = 0
                        self.path_pos += 1
            else: # moving left
                if dirn[1] >=0: # moving down
                    if self.x <= x2 and self.y >= y2:
                        self.dis = 0
                        self.move_count = 0
                        self.path_pos += 1
                elif dirn[1] <0: # moving up
                    if self.x <= x2 and self.y <= y2:
                        self.dis = 0
                        self.move_count = 0
                        self.path_pos += 1
    
            if self.x == x2 and self.y == y2:
                self.dis = 0
                self.move_count = 0
                self.path_pos += 1
    

    2.类重写的错误

    在scorpion中设置的imgs不能覆盖其父类enemy的空imgs。
    解决方法:给scorpion写一个构造方法,把imgs放进去,写为self.imgs

    3.如何实现塔的攻击?

    编写Game类的run()方法

                    # loop through attack towers
                    for tw in self.attack_towers:
                        self.money += tw.attack(self.enemies)
    

    编写Enemy类的hit()方法

        def hit(self, damage):
            """
            Returns if an enemy has died and removes one health
            each call
            :param damage: int
            :return: Bool
            """
            self.health -= damage
            if self.health <= 0:
                return True
            return False
    

    编写StoneTowerOne类的attack()方法,传入的参数为敌人实例列表

        def attack(self, enemies):
            """
            attacks an enemy in the enemy list, modifies the list
            :param enemies: list of enemies
            :return: None
            """
            money = 0
            self.inRange = False
            enemy_closest = []
            for enemy in enemies:
                x, y = enemy.x, enemy.y
    
                dis = math.sqrt((self.x-x)**2 + (self.y-y)**2) # 得到敌人和塔的距离
                if dis < self.range: # 是否在塔的攻击范围
                    self.inRange = True
                    enemy_closest.append(enemy)
            
            # 对敌人被攻击的优先级排序,排序方式是敌人距离其最终移动目标的距离越近的优先级越高
            enemy_closest.sort(key=lambda x: x.path_pos)
            enemy_closest = enemy_closest[::-1]
            if len(enemy_closest)>0: # 塔的攻击范围内有敌人
                first_enemy = enemy_closest[0]
                if self.stone_count == 20: # 这是塔的动画播放时的计数器
                    if first_enemy.hit(self.damage) == True: # 敌人是否失去所有生命
                        money = first_enemy.money * 2 # 击杀敌人得到战利品
                        enemies.remove(first_enemy) # 将此敌人实例抹去
    
                # 根据敌人相对于塔的水平位置,水平翻转塔的动画
                if first_enemy.x < self.x and not (self.left): 
                    self.left = True
                    for x, img in enumerate(self.stone_imgs):
                        self.stone_imgs[x] = pygame.transform.flip(img, True, False)
                elif self.left and first_enemy.x >self.x:
                    self.left = False
                    for x, img in enumerate(self.stone_imgs):
                        self.stone_imgs[x] = pygame.transform.flip(img, True, False)
            return money
    

    4.如何实现将塔从侧边的菜单栏放置到地图上?

    实现了将塔放置,而且塔的位置不能重合,无法将塔放置在已放置的塔上。
    编写Game类的add_tower()方法:

        def add_tower(self, name):
            x,y = pygame.mouse.get_pos() # 得到鼠标位置
            name_list = ["buy_stone1", "buy_stone2", "buy_damage", "buy_range"]
            object_list = [StoneTowerOne(x,y), StoneTowerTwo(x,y), DamageTower(x,y), RangeTower(x,y)]
    
            try:
                obj = object_list[name_list.index(name)] # 根据索引确定要实例化的类
                self.moving_object = obj # 设定moving_object
                obj.moving = True # 表示正在移动一个塔
            except Exception as e:
                print(str(e) + "NOT VALID NAME")
    

    编写Button类的update()方法:

        def update(self):
            """
            updates button position
            :return: None
            """
            # 更新位置
            self.x = self.menu.x - 40
            self.y = self.menu.y - 95
    

    编写Menu类的update()方法:

        def update(self):
            """
            updata menu and button location
            :return: None
            """
            # 更新所有按钮的位置
            for btn in self.buttons:
                btn.update()
    

    编写Tower类的move()方法和collide()方法:

        def move(self, x, y):
            """
            moves tower to given x and y
            :param x: int
            :param y: int
            :return: None
            """
            # 将塔和menu的位置设置为传入的x和y的值
            self.x = x 
            self.y = y
            self.menu.x = x
            self.menu.y = y
            self.menu.update()
    
        def collide(self, otherTower):
            x2 = otherTower.x
            y2 = otherTower.y
    
            dis = math.sqrt((x2 - self.x)**2 + (y2 - self.y)**2) # 两座塔的距离
            if dis >= 90: # 允许放置
                return False
            else: # 发生碰撞,不允许放置
                return True
    

    编写Game类的run()方法:

      pos = pygame.mouse.get_pos() # 得到鼠标位置
    
                # check for moving object
                if self.moving_object: # 正在移动一个塔
                    self.moving_object.move(pos[0], pos[1]) # 传入鼠标位置
                    tower_list = self.attack_towers[:] + self.support_towers[:] # 攻击塔和支援塔(所有塔)
                    collide = False
                    for tower in tower_list:  # 检测塔的碰撞,用两种圆形的颜色表示可以放置和不可以放置
                        if tower.collide(self.moving_object): # 发生碰撞
                            collide = True
                            tower.place_color = (255, 128, 0, 100)
                            self.moving_object.place_color = (255, 128, 0, 100)
                        else: # 没有发生碰撞
                            tower.place_color = (0, 128, 255, 100)
                            if not collide:
                                self.moving_object.place_color = (0, 128, 255, 100)
    
    
                # main event loop
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        run = False
    
    
                    if event.type == pygame.MOUSEBUTTONDOWN:
                        # if you are moving an object and click
                        if self.moving_object:
                            not_allowed = False
                            tower_list = self.attack_towers[:] + self.support_towers[:]
                            for tower in tower_list:
                                if tower.collide(self.moving_object): # 发生碰撞
                                    not_allowed = True # 不允许放置
    
                            if not not_allowed:
                                if self.moving_object.name in attack_tower_names: # 放置的是攻击塔
                                    self.attack_towers.append(self.moving_object)
                                elif self.moving_object.name in support_tower_names: # 放置的是支援塔
                                    self.support_towers.append(self.moving_object)
                                self.moving_object.moving = False
                                self.moving_object = None
    

    5.如何实现游戏的暂停和继续?

    编写Game类的__init__()方法:

        def __init__(self, win):
            self.pause = True
            self.playPauseButton = PlayPauseButton(play_btn, pause_btn, 10, self.height-85)
            self.soundButton = PlayPauseButton(sound_btn, not_sound_btn, 70, self.height-85)
    

    编写Game类的gen_enemies()方法:

        def gen_enemies(self):
            """
            generate the next enemhy or enemies to show
            :return: enemy
            """
            if sum(self.current_wave) == 0 :
                if len(self.enemies) == 0: # 当前轮已经没有存活的敌人
                    self.wave += 1
                    self.current_wave = waves[self.wave]
                    self.pause = True # 暂停游戏
                    self.playPauseButton.paused = self.pause
            else: # 生成敌人
                wave_enemies = [Scorpion(), Club(), Ultraman()]
                for x in range(len(self.current_wave)):
                    if self.current_wave[x] != 0:
                        self.enemies.append(wave_enemies[x])
                        self.current_wave[x] = self.current_wave[x]-1
                        break
    

    编写Game类的run()方法

            while run:
                clock.tick(60)
    
                if self.pause == False: # 游戏没有暂停
                    # generate enemies
                    if time.time() - self.timer > random.randrange(1,5)/2:
                        self.timer = time.time() # 更新时间
                        self.gen_enemies() # 生成敌人
    
                         if event.type == pygame.MOUSEBUTTONDOWN:
                        # if you are moving an object and click
                        if self.moving_object:
                            not_allowed = False
                            tower_list = self.attack_towers[:] + self.support_towers[:]
                            for tower in tower_list:
                                if tower.collide(self.moving_object):
                                    not_allowed = True
    
                            if not not_allowed:
                                if self.moving_object.name in attack_tower_names:
                                    self.attack_towers.append(self.moving_object)
                                elif self.moving_object.name in support_tower_names:
                                    self.support_towers.append(self.moving_object)
                                self.moving_object.moving = False
                                self.moving_object = None
                        else:
                            # check for play or pause
                            if self.playPauseButton.click(pos[0], pos[1]): # 鼠标点击“继续/暂停”按钮
                                self.pause = not(self.pause) # 切换“继续”和“暂停”
                                self.playPauseButton.paused = self.pause
    
                # loop through enemies
                if not(self.pause): # 游戏没有“暂停”
                    to_del = []
                    for en in self.enemies:
                        en.move() # 移动敌人
                        if en.x <  -15: # 当敌人突破移动路线的最终目标时,抹去敌人
                            to_del.append(en)
    

    6.import本地文件的报错

    解决方法,在需要import的文件夹右击,选择 Mark Directory as Sources Root,即可import。

    其他(感悟、思考等)

    1.网上关于pygame编写游戏有许多现成的代码,也有微课视频,但我感觉学习效果不够好。这一次我选择跟随一个12小时的直播录像进行学习,可以跟随作者体会一个游戏如何从无到有,如何debug,如何解决难以实现的问题,非常有收获。
    2.经过这次实践,我对面向对象编程有了更深的体会和熟练,熟悉了pygame编写游戏的流程,学会了一些具体的实现方式

    参考

    https://craftpix.net/
    https://maoudamashii.jokersounds.com/
    https://github.com/techwithtim/Tower-Defense-Game
    https://www.youtube.com/watch?v=iLHAKXQBOoA

  • 相关阅读:
    分享一个动态生成RDLC报表的类
    第一次写这么长的js
    [SpringCloud]Gateway入门
    [SpringCloud]Hystrix
    [SpringCloud]Eureka+OpenFeign
    [Java]Socket API编写一个简单的私聊和群聊
    JVM内存分区和各分区溢出测试
    使用Python操作neo4j和画柱状图
    JDK1.8 HashMap为什么在链表长度为8的时候转红黑树,为啥不能是9是10?
    Redis过期策略
  • 原文地址:https://www.cnblogs.com/hardcoreYutian/p/12926747.html
Copyright © 2020-2023  润新知