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