闲来无聊,想来玩玩以前的魔塔60层小游戏并看看其实现机制,发现网上这种资源挺少的,于是有了自己开发的想法,用的主要是python的pygame模块。
本次开发暂时只有魔塔第一层的内容,后面的部分有时间再继续完成 (地图太多了不想手算坐标)
1.首先设计好游戏界面
我这里设计的游戏界面是大小19*13的,其中游戏区域为13*13(数数看是11格加上一层padding),左侧3*13放个人信息,右侧3*13放宝物信息。(注意,这里将一个像素块定义为50*50大小的,主要是方便计算后面的坐标)
#初始化及设置
pygame.init()
pygame.mixer.init()#初始化混音器模块
size = width , height = 950 , 650
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Tower")
2.加载一些素材
wall = pygame.transform.scale(pygame.image.load('w1.PNG').convert_alpha(),(50,50))
#背景音乐
pygame.mixer.music.load("鸡你太美.mp3")
pygame.mixer.music.set_volume(0.02)#设置音量
#pygame.mixer.music.play(-1)#循环播放(第一个参数为播放次数【-1表示循环】,第二个为从第n秒开始播放)
#设定变量用于判断开关音乐
musicPlaying = False
#音效sound:打怪,开门,通关等
sound0 = pygame.mixer.Sound("0.wav")
#等待1秒让mixer完成初始化
pygame.time.delay(1000)
#伞兵一号卢本伟准备就绪
sound0.set_volume(0.5)
sound0.play()
#加载字体
my_fontCH = pygame.font.Font("简体.ttf", 40)
my_fontCHs = pygame.font.Font("简体.ttf", 30)
my_fontEN = pygame.font.Font("times.ttf", 40)
#最上面一格 "
text_surface0 = my_fontCH.render("守护全世界最好的坤坤", True, (0,0,0), (191,191,191))
#加载属性框(侧边栏)
#矩形的四个参数left,top,width,height
att_rect1 = pygame.Rect(0,50,200,600)
att_rect2 = pygame.Rect(750,50,200,600)
att_rect3 = pygame.Rect(200,600,550,50)
att_rect4 = pygame.Rect(0,0,950,50)
#获取钥匙图片所在位置的矩形对象
ykey_rect = ykey.get_rect()
ykey_rect.left,ykey_rect.top = 0,400
bkey_rect = bkey.get_rect()
bkey_rect.left,bkey_rect.top = 0,450
rkey_rect = rkey.get_rect()
rkey_rect.left,rkey_rect.top = 0,500
#电梯位置的矩形对象
up_lift_rect = pygame.Rect(770,100,40,20)
down_lift_rect = pygame.Rect(770,120,40,20)
大部分加载素材的代码都略过了,大同小异
值得注意的是,这里把边框栏设置完成了,不过现在只有他们的图片已经对应的矩形对象。(后面可以通过判定鼠标与矩形对象的碰撞来判定事件的发生)
3.定义人物类
class character:
def __init__(self,tuple):
#位置信息
self.floor = 1
#self.direction = "UP"
#self.location = (7,12)
#属性信息
self.attack = tuple[0]
self.defend = tuple[1]
self.life = tuple[2]
self.gold = 0
#物品栏
self.property = {"Yellow_key":0,"Blue_key":0,"Red_key":0}
#图像
self.image = pygame.transform.scale(pygame.image.load('character.PNG').convert_alpha(),(50,50))
self.bg = pygame.transform.scale(pygame.image.load('w2.PNG').convert_alpha(),(50,50))
self.image_rect = self.image.get_rect()
self.image_rect.left = 450
self.image_rect.top = 550
#def save(self):
#f=open("Archive.txt","wb")
#pickle.dump(f,{"floor":self.floor,"attack":self.attack,"defend":self.defend,"life":self.life,"Yellow_key":self.property["Yellow_key"],"Blue_key":self.property["Blue_key"],"Red_key":self.property["Red_key"]})
#def load(self):
#pass
#墙壁以及门在没有对应钥匙时的不可通过机制写在移动函数的判定里了
def move(self,str):
if str == "UP":
FORBID = 0
rect_new = self.image_rect.copy()
rect_new.move_ip(0,-50)
#是否撞墙
for wall_ in wallL[:]:
if rect_new.colliderect(wall_):
FORBID = 1
#是否在没有钥匙的时候通过门(暂时只有黄门)
for y_door in ydoorL[:]:
if rect_new.colliderect(y_door) and self.property["Yellow_key"] == 0:
FORBID = 1
#打不过怪时不可通过
for monster_ in list_morect[:]:
if rect_new.colliderect(monster_):
x = list_morect.index(monster_)
if(fight(CAIXUKUN,eval(list_moname[x] + "_"),1)) == [0]:
sound5.play()
FORBID = 1
if FORBID == 1:
pass
else :
#判断是否出界
if self.image_rect.top < 100:
pass
else :
self.image_rect.move_ip(0,-50)
if str == "DOWN":
FORBID = 0
rect_new = self.image_rect.copy()
rect_new.move_ip(0,50)
#是否撞墙
for wall_ in wallL[:]:
if rect_new.colliderect(wall_):
FORBID = 1
#是否在没有钥匙的时候通过门(暂时只有黄门)
for y_door in ydoorL[:]:
if rect_new.colliderect(y_door) and self.property["Yellow_key"] == 0:
FORBID = 1
#打不过怪时不可通过
for monster_ in list_morect[:]:
if rect_new.colliderect(monster_):
x = list_morect.index(monster_)
if(fight(CAIXUKUN,eval(list_moname[x] + "_"),1)) == [0]:
sound5.play()
FORBID = 1
if FORBID == 1:
pass
else :
#判断是否出界
if self.image_rect.bottom > 550:
pass
else :
self.image_rect.move_ip(0,50)
if str == "LEFT":
FORBID = 0
rect_new = self.image_rect.copy()
rect_new.move_ip(-50,0)
#是否撞墙
for wall_ in wallL[:]:
if rect_new.colliderect(wall_):
FORBID = 1
#是否在没有钥匙的时候通过门(暂时只有黄门)
for y_door in ydoorL[:]:
if rect_new.colliderect(y_door) and self.property["Yellow_key"] == 0:
FORBID = 1
#打不过怪时不可通过
for monster_ in list_morect[:]:
if rect_new.colliderect(monster_):
x = list_morect.index(monster_)
if(fight(CAIXUKUN,eval(list_moname[x] + "_"),1)) == [0]:
sound5.play()
FORBID = 1
if FORBID == 1:
pass
else :
#判断是否出界
if self.image_rect.left < 250:
pass
else :
self.image_rect.move_ip(-50,0)
if str == "RIGHT":
FORBID = 0
rect_new = self.image_rect.copy()
rect_new.move_ip(50,0)
#是否撞墙
for wall_ in wallL[:]:
if rect_new.colliderect(wall_):
FORBID = 1
#是否在没有钥匙的时候通过门(暂时只有黄门)
for y_door in ydoorL[:]:
if rect_new.colliderect(y_door) and self.property["Yellow_key"] == 0:
FORBID = 1
#打不过怪时不可通过
for monster_ in list_morect[:]:
if rect_new.colliderect(monster_):
x = list_morect.index(monster_)
if(fight(CAIXUKUN,eval(list_moname[x] + "_"),1)) == [0]:
sound5.play()
FORBID = 1
if FORBID == 1:
pass
else :
#判断是否出界
if self.image_rect.right > 700:
pass
else :
self.image_rect.move_ip(50,0)
这里我本想设置人物的形象随方向的改变而变化,实现转向的效果,无奈坤坤的照片我只有这样一张,后来觉得麻烦删了。
这里代码比较繁多的原因是我把人物移动的判定机制写进去了:
4.定义怪物类
怪物类就简单很多了,因为他们不涉及移动物品等等(没错我就是馋它金币)...
#怪物类:
class monster:
def __init__(self,tuple):
#属性信息
self.attack = tuple[0]
self.defend = tuple[1]
self.life = tuple[2]
#获取金币数
self.gold = tuple[3]
这样我们初步定义一下人物和一些怪物
CAIXUKUN = character((10,10,100))
gSlime_ = monster((11,5,10,1))
rSlime_ = monster((12,5,10,2))
sBat_ = monster((13,5,10,3))
bWizard_ = monster((15,5,10,5))
Skeleton_ = monster((20,11,10,8))
scSkeleton_ = monster((25,11,10,10))
5.设计地图
这是整个开发过程中碰到最烦的事情了,而且不小心就会出现很多bug
###设计第一层的地图
#先铺地板:范围从(200,50)到(700,550)
ground_L = []
for i in range(4,14+1) :#加1是因为range不包含最后一个数
for j in range(1,11+1):
ground_L.append((50*i,50*j))
#墙壁的位置(矩形的左上角(left,top))
wall_L = [(200,100),(250,100),(300,100),(350,100),(400,100),(450,100),(500,100),(550,100),(600,100),(650,100),(450,150),(650,150),(350,200),(450,200),(650,200),(200,250),(300,250),(350,250),(450,250),(500,250),(550,250),(650,250),(350,300),(650,300),(350,350),(450,350),(500,350),(550,350),(600,350),(650,350),(200,400),(300,400),(350,400),(350,450),(400,450),(500,450),(550,450),(600,450),(700,450),(350,500),(550,500),(350,550),(550,550)]
#黄门的位置
dict_door = {(350,150):"yellow",(250,250):"yellow",(600,250):"yellow",(450,300):"yellow",(250,400):"yellow",(450,450):"yellow",(650,450):"yellow"}
#宝物的位置
dict_trea = {(550,150):"ykey",(200,350):"ykey",(300,500):"ykey",(400,500):"ykey",(300,550):"ykey",(200,150):"rbottle",(200,500):"rbottle",(200,550):"rbottle",(550,200):"rbottle",(650,550):"bbottle",(500,150):"rgem",(500,200):"bgem"}
#楼梯的位置
dict_stair = {(200,50):"up"}
#怪物的位置
dict_monster = {(300,50):"gSlime",(400,50):"gSlime",(600,550):"gSlime",(700,550):"gSlime",(350,50):"rSlime",(500,300):"sBat",(600,300):"sBat",(650,500):"sBat",(550,300):"bWizard",(250,300):"Skeleton",(250,450):"scSkeleton"}
#神秘位置
Guide = [(500,500)]
Lift = [(250,550)]
#设置变量判断是否拿到电梯,初始化为0
get_lift = 0
##转化为专一的list
#门
List_temp1 = [k for k,v in dict_door.items() if v=="yellow"]
#楼梯
List_temp2 = [k for k,v in dict_stair.items() if v=="up"]
#宝物
List_temp3 = [k for k,v in dict_trea.items() if v=="rbottle"]
List_temp4 = [k for k,v in dict_trea.items() if v=="bbottle"]
List_temp5 = [k for k,v in dict_trea.items() if v=="rgem"]
List_temp6 = [k for k,v in dict_trea.items() if v=="bgem"]
List_temp7 = [k for k,v in dict_trea.items() if v=="ykey"]
#怪物
#见下方的list_molocation
##创建矩形对象的列表,用于判定遇到事件
#记住tuple下标也是从零开始计算
def build_List(LIST1,LIST2):
for i in range(1,len(LIST2)+1):
LIST1.append( pygame.Rect(LIST2[i-1][0],LIST2[i-1][1],50,50) )
wallL = []
build_List(wallL,wall_L)
ydoorL = []
build_List(ydoorL,List_temp1)
stairupL = []
build_List(stairupL,List_temp2)
rbottleL = []
build_List(rbottleL,List_temp3)
bbottleL = []
build_List(bbottleL,List_temp4)
rgemL = []
build_List(rgemL,List_temp5)
bgemL = []
build_List(bgemL,List_temp6)
ykeyL = []
build_List(ykeyL,List_temp7)
#怪物的不想一个个写了,直接写成字典(矩形对象:怪物名)
##上面的构象失败了,因为矩形对象无法哈希化,即不能放入字典,那么只能用两个列表代替了
list_molocation =list(dict_monster)
list_moname = list(dict_monster.values())
list_morect = []
build_List(list_morect ,list_molocation)
#特殊位置和宝物的全部是单独的列表
guideL = []
guideL.append( pygame.Rect(Guide[0][0],Guide[0][1],50,50) )
liftL = []
liftL.append( pygame.Rect(Lift[0][0],Lift[0][1],50,50) )
这里总感觉自己写的不太好,等到写第二层的时候再优化优化
6.pygame事件循环
算是最重要的地方了
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_w:
#向上移动一格
CAIXUKUN.move("UP")
if event.key == pygame.K_s:
#向下移动一格
CAIXUKUN.move("DOWN")
if event.key == pygame.K_a:
#向左移动一格
CAIXUKUN.move("LEFT")
if event.key == pygame.K_d:
#向右移动一格
CAIXUKUN.move("RIGHT")
if event.key == pygame.K_p:
if musicPlaying:
pygame.mixer.music.stop()
else:
pygame.mixer.music.play(-1)
#更换变量状态
musicPlaying = not musicPlaying
if event.key == pygame.K_1:
sound0.play()
#设置一个简单的外挂,鼠标左键点击钥匙图标可以+1钥匙,右键-1,中键归0
elif event.type == pygame.MOUSEBUTTONDOWN:
#检测鼠标的单击位置是否在钥匙的rect内
if ykey_rect.collidepoint(event.pos):
if event.button == 1:
CAIXUKUN.property["Yellow_key"] += 1
sound4.play()
elif event.button == 2:
CAIXUKUN.property["Yellow_key"] = 0
elif event.button == 3:
CAIXUKUN.property["Yellow_key"] -= 1
elif bkey_rect.collidepoint(event.pos):
if event.button == 1:
CAIXUKUN.property["Blue_key"] += 1
sound4.play()
elif event.button == 2:
CAIXUKUN.property["Blue_key"] = 0
elif event.button == 3:
CAIXUKUN.property["Blue_key"] -= 1
elif rkey_rect.collidepoint(event.pos):
if event.button == 1:
CAIXUKUN.property["Red_key"] += 1
sound4.play()
elif event.button == 2:
CAIXUKUN.property["Red_key"] = 0
elif event.button == 3:
CAIXUKUN.property["Red_key"] -= 1
#电梯功能
elif up_lift_rect.collidepoint(event.pos):
if event.button == 1 and get_lift and CAIXUKUN.floor < 66 and CAIXUKUN.floor != 42:#其实还要增加设定,不可去到没到过的楼层
CAIXUKUN.floor += 1
elif down_lift_rect.collidepoint(event.pos):
if event.button == 1 and get_lift and CAIXUKUN.floor > 1 and CAIXUKUN.floor != 44:
CAIXUKUN.floor -= 1
##开始画地图
#地板
for i in ground_L:
screen.blit(ground,i)
#墙
for j in wall_L:
screen.blit(wall,j)
#门
for k in List_temp1:
screen.blit(yellow_door,k)
#楼梯
for k in List_temp2:
screen.blit(upstair,k)
#宝物
for k in List_temp3:
screen.blit(rbottle,k)
for k in List_temp4:
screen.blit(bbottle,k)
for k in List_temp5:
screen.blit(rgem,k)
for k in List_temp6:
screen.blit(bgem,k)
for k in List_temp7:
screen.blit(ykey,k)
#怪物
for k in list_molocation:
n = list_molocation.index(k)
k_name = list_moname[n]
screen.blit(eval(k_name),k)
screen.blit(lift,Lift[0])
screen.blit(guide,Guide[0])
##判定遇到事件
'''
遇到红血瓶+50生命,遇见蓝血瓶+100生命,遇到钥匙+1,遇到红宝石攻击+2,遇到蓝宝石防御+2,遇到门对应钥匙-1
遇到怪物启用战斗系统,遇到特殊事件(宝物或人)发生特定事件
'''
#对于列表中的元素,碰撞以后:
#黄门
for y_door in ydoorL[:]:
if CAIXUKUN.image_rect.colliderect(y_door):
if CAIXUKUN.property["Yellow_key"] > 0:
i = ydoorL.index(y_door)
ydoorL.remove(y_door)
CAIXUKUN.property["Yellow_key"] -= 1
del List_temp1[i]
else :
#实际上这里应该是不能通过,当作墙壁,写在类的机制里了
pass
#楼梯的判定事件较为复杂,暂时不写
#for stairup in stairupL[:]:
#self.floor += 1
##后期增加功能,根据self.floor判断楼层数并绘制地图
#红血瓶
for r_bottle in rbottleL[:]:
if CAIXUKUN.image_rect.colliderect(r_bottle):
sound1.play()
i = rbottleL.index(r_bottle)
rbottleL.remove(r_bottle)
CAIXUKUN.life += 50
del List_temp3[i]
#蓝血瓶
for b_bottle in bbottleL[:]:
if CAIXUKUN.image_rect.colliderect(b_bottle):
sound1.play()
i = bbottleL.index(b_bottle)
bbottleL.remove(b_bottle)
CAIXUKUN.life += 100
del List_temp4[i]
for r_gem in rgemL[:]:
if CAIXUKUN.image_rect.colliderect(r_gem):
sound1.play()
i = rgemL.index(r_gem)
rgemL.remove(r_gem)
CAIXUKUN.attack += 2
del List_temp5[i]
for b_gem in bgemL[:]:
if CAIXUKUN.image_rect.colliderect(b_gem):
sound1.play()
i = bgemL.index(b_gem)
bgemL.remove(b_gem)
CAIXUKUN.defend += 2
del List_temp6[i]
for y_key in ykeyL[:]:
if CAIXUKUN.image_rect.colliderect(y_key):
i = ykeyL.index(y_key)
ykeyL.remove(y_key)
CAIXUKUN.property["Yellow_key"] += 1
del List_temp7[i]
##战斗事件
for monster_ in list_morect[:]:
if CAIXUKUN.image_rect.colliderect(monster_):
x = list_morect.index(monster_)
oops = eval(list_moname[x] + "_")
if (fight(CAIXUKUN,oops)!=[0]):
sound2.play()
list_morect[x] = pygame.Rect(-100,-100,50,50)
#字符串转化为变量(这里的变量是怪物类的实例)
list_molocation[x] = (-100,-100)
else :
pass#这里和没有钥匙开门同理,打不过不可通过的机制放在移动机制中
for lift_ in liftL[:]:
if CAIXUKUN.image_rect.colliderect(lift_):
sound6.play()
liftL.remove(lift_)
get_lift = 1
Lift[0] = (-100, -100)
#侧边栏背景
screen.fill((191,191,191), rect=att_rect1, special_flags=0)
screen.fill((191,191,191), rect=att_rect2, special_flags=0)
screen.fill((191,191,191), rect=att_rect3, special_flags=0)
screen.fill((191,191,191), rect=att_rect4, special_flags=0)
#物品栏
screen.blit(ykey, (0,400))
screen.blit(bkey, (0,450))
screen.blit(rkey, (0,500))
#属性栏 "
text_surface1 = my_fontCH.render("第 %s 层"%str(CAIXUKUN.floor), True, (0,0,0), (191,191,191))
text_surface2 = my_fontCH.render("攻击力: %s"%str(CAIXUKUN.attack), True, (0,0,0), (191,191,191))
text_surface3 = my_fontCH.render("防御力: %s"%str(CAIXUKUN.defend), True, (0,0,0), (191,191,191))
text_surface4 = my_fontCH.render("生命值: %s"%str(CAIXUKUN.life), True, (0,0,0), (191,191,191))
text_surface5 = my_fontCH.render("金币 : %s"%str(CAIXUKUN.gold), True, (0,0,0), (191,191,191))
##钥匙数目
Yellow_key = CAIXUKUN.property["Yellow_key"]
Blue_key = CAIXUKUN.property["Blue_key"]
Red_key = CAIXUKUN.property["Red_key"]
#物品栏
text_surface6 = my_fontCH.render("%s 把"%str(Yellow_key), True, (0,0,0), (191,191,191))
text_surface7 = my_fontCH.render("%s 把"%str(Blue_key), True, (0,0,0), (191,191,191))
text_surface8 = my_fontCH.render("%s 把"%str(Red_key), True, (0,0,0), (191,191,191))
text_surface9 = my_fontCHs.render("永久使用宝物", True, (0,0,0), (191,191,191))
text_surface10 = my_fontCHs.render("一次使用宝物", True, (0,0,0), (191,191,191))
text_surface11 = my_fontCHs.render("自动使用宝物", True, (0,0,0), (191,191,191))
#如果获得电梯,则在右侧边栏加上该宝物
if get_lift:
screen.blit(pygame.transform.scale(pygame.image.load('lift.PNG').convert_alpha(),(40,40)),(770,100))
screen.blit(CAIXUKUN.image,CAIXUKUN.image_rect)
#属性栏
screen.blit(text_surface0, (280,0))
screen.blit(text_surface1, (0,100))
screen.blit(text_surface2, (0,150))
screen.blit(text_surface3, (0,200))
screen.blit(text_surface4, (0,250))
screen.blit(text_surface5, (0,300))
screen.blit(text_surface6, (100,400))
screen.blit(text_surface7, (100,450))
screen.blit(text_surface8, (100,500))
screen.blit(text_surface9, (758,50))
screen.blit(text_surface10, (758,280))
screen.blit(text_surface11, (758,510))
pygame.display.update()
这里注释挺详细的,刚开始写的时候踩了不少坑,大部分源自于地图问题,我在后面的讨论中再详细说下。
7.战斗机制
采用的是日韩游戏中常用的回合型战斗机制(你拍一啊我拍一,说起来我对疯狂加血版魔塔真的映像深刻)
def fight(A,B,cancel=0):
a = A.attack - B.defend
b = B.attack - A.defend
#保存怪物的初始生命值
alife = A.life
blife = B.life
if a <= 0: #失败,未能破防
return [0]
if a > 0 and b <= 0: #成功,怪物无法给主角造成伤害,则无需战斗结算
if(cancel == 0):
A.gold += B.gold
if(cancel != 0):
A.life = alife
B.life = blife
return 0
else: #进行战斗结算
for i in range(1,100):
B.life = B.life - a
if B.life < 0 : #成功
if(cancel == 0):
#重置怪物的生命值
B.life = blife
A.gold += B.gold
if(cancel != 0):
A.life = alife
B.life = blife
return alife - A.life
A.life = A.life - b
if A.life < 0 :#失败,重置双方生命值
A.life = alife
B.life = blife
return [0]
讨论
介绍一下设计过程中遇到的一些问题
Q:移动图片后图片在原始位置上的痕迹还在(主角移动后在原位置留下残影)
A:每次移动的时候,用背景色覆盖之前的位置(这里用的是地板覆盖)
Q:游戏全屏显示出现问题
A:因为元素是按像素大小放上去的,改变屏幕大小会出问题,不要改游戏设置使游戏可以全屏或者设置可放缩屏幕
Q:解决楼层时的问题,可以考虑采用双缓冲,这样不必每次都重新慢慢画,DOUBLEBUF,使用pygame.display.flip()刷新屏幕
A:很好的想法,暂时没打算采用
Q:钥匙上升到10把后,“把”字会后移,而掉回10把以内后会出现重影
A:把背景设置拖到循环里来即可
Q:音效无法播放
A:sound.play()的括号一定要加上(是我傻了)
Q:将怪物合并为一个列表之后,每次遇到一个并删除之后,列表前移一个,对应位置的图像发生变化了
A:将删除改为投影到屏幕外即可,如设定坐标(-100,-100)
Q:怪物的顺序有问题
A:list_morect也不可以remove,道理同上,改删除为换位
Q:升到第二层后地图设置没有改变
A:实现的想法是,设计两个函数Analysis_map()和Draw_map(),第一个用于根据楼层解析地图,第二个用于画地图,Draw_map()是一直放入事件循环中的,Analysis_map()做一次初始化后,根据楼层改变的扳机触发。这样的想法带来的后果是,所有特殊的道具必须提前做好放进列表里,还有触发事件也必须提前写好并编号,比如观音1号观音2号...(对于我这样才看了几层地图的懒人来说实在有点困难)