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

原文地址:https://www.cnblogs.com/hardcoreYutian/p/12926747.html