我们终于把pygame的方方面面都说了一遍,也经过了两个小游戏的洗礼,如果您真的好好学习了每一部分并自动动手演练过,那就是一个很不错的Python游戏开发人员啦!
但是,不得不说我们到现在为止,写的东西都不够酷!pygame就这点能耐么?第一篇介绍pygame的时候,我就说了pygame很适合做一个植物大战僵尸之类的游戏,OK,那么,这次开始我们就来用pygame山寨一个吧哈哈。
游戏介绍就省略了,不过说实话这游戏我也没玩几关,感受了一下就关了。所以现在仿制的与真品相比肯定差了很多,这里只是稍微把我们学习的东西温习应用一下而已,如果各位看到什么不对劲儿的东西,只要不是很致命,也就一笑而过吧。
游戏是否卖座,创意是极为重要的。所有其他技术性的东西,比如画面音乐都是有可替代性的,也就是说,可以通过学习获得的,而创意则不同,植物大战僵尸之前,有无数的塔防游戏,为什么没有这么火?首先制作精良,上手容易,然后两者的结合给人耳目一新,闯关的模式更是易于上手难于精通,非常具有可挖掘性。当然原因肯定不止这么多,不管是游戏还是电影,成功永远有规律可循,然而却永远无法复制。可以学习一下它的好的地方,但光学习是不可能突破的。
这个虽然极为重要,但不是我们的重点,我们这里基本就是抄袭——哦不,是参考,所以就不多讲了(也没有人能讲出什么东西来)。
好的游戏往往需要极为漂亮的画面,这里也还是发挥拿来主义吧~ 各种背景角色啊音乐都解包复制过来就是了。怎么解包?网上搜一下。
不过如果分析一下资源包里的文件,会发现它分的特别细,比如说一个太阳花,周围一圈花瓣,居然是一个一个分开来存放的!这个引擎还挺有意思。我们的pygame不太适合用这样的图片资源,还是把每一帧完整的分别存放吧。当然可以一帧一个文件,有些麻烦,还增加读盘时间。回想下Surface里学习的东西,我们可以把这么多帧拼成一个图片,然后取一部分就可以了。比如说太阳花,处理以后会是下面的样子:
每个帧都并列排开,太阳花的话一共有18个画面,很长的一张图。每个画面都有共同的高度和宽度,所以一旦我们读进这个文件并转成Surface以后,就拆成18个Subsurface,在渲染的时候,分别画出来,就有动态的效果了。
很显然我们在分割的时候需要知道宽度(高度的话,如果只有一行,就无所谓了),可以在代码里写好,我这里就是这么做的。不过也许自己在写游戏的时候,美工和程序不在一起做,那么就需要良好沟通,或者这种小制作的话,就把图片文件名规定一下,叫【sunflower_82x77_18.png】好了,意思是【名字_宽x高_数.后缀】,这样代码可以根据读入的文件名自动转换。只是举个例子,总之,游戏资源繁多,好好的管理是很重要的。
因为只是做一个demo,太复杂的构架也就免了,这次我就随便写写了。而且也没有什么绝对优秀的构架可以放到哪里都好用,根据要求自己选择判断才是重要的。
我们之前的各种动画,变化的只有位置,在一款优秀的游戏中,这个是不允许存在的,飞机什么的可能还好说一些,要是有人物走动的时候,脚也不动一下岂不见鬼了?不过,难道每画一帧就换一个图片贴上去么?好像很麻烦啊……
放心,一点都不比移动困难,温习一下之前的Sprite篇(其实AI部分结束那个时候我就开始穿插写实战了,发现缺少Sprite的知识实在不方便,就加了一个Sprite的介绍,实战就挪到了最后),我们既然把画图全部交给Sprite来做,那么图片的切换也可以交给它来做。
MAIN_DIR = os.path.split(os.path.abspath(__file__))[0]
def load_image(file, width=None, number=None):
file = os.path.join(MAIN_DIR, 'data/image', file)
try:
surface = pygame.image.load(file).convert_alpha()
except pygame.error:
raise SystemExit('Could not load image "%s" %s'%(file, pygame.get_error()))
if width == None:
return surface
height = surface.get_height()
return [surface.subsurface(
Rect((i * width, 0), (width, height))
) for i in xrange(number)]
class SunFlower(pygame.sprite.Sprite):
_width = 82
_height = 77
_number = 18
images = []
def __init__(self):
self.order = 0
pygame.sprite.Sprite.__init__(self)
if len(self.images) == 0:
self.images = load_image("sunflower.png", self._width, self._number)
self.image = self.images[self.order]
self.rect = Rect(0, 0, self._width, self._height)
def update(self):
if self.order >= self._number - 1:
self.order = -1
self.order += 1
self.image = self.images[self.order]
这里有两个内容,一个是load_image函数,它接受三个参数,图片文件名、每一帧的宽度和总帧数,它所做的事情就是把图片读入之后分割Subsurface,把一个Subsurface的列表返回。这个函数被SunFlower类的初始化函数调用,返回值存放在静态变量images里。
为什么把Subsurface列表存为静态变量,也是有含义的。这样一来这个图片只会第一次新建SunFlower类的实例的时候加载一次,下一次在新建一个太阳花的时候,就不会再读取sunflower.png文件了,节约内存和时间。
然后看update方法,这里做的事情便是是SunFlower当前的image指向下一个Subsurface(到头的话重新开始),这样每次blit的时候,就会和上一次有些差别,动态效果就出来了。
不过对于“基于时间的移动”那部分学习的比较好的话,很肯会考虑到一个问题。我们这里是一帧切换一个画面,万一游戏变得很复杂或者运行的机器很糟糕,每一帧都花了大量时间的话,岂不是就动的很慢了?牛机器上的话,又要摆的像螺旋桨一样看着就要飞起来似地……怎么办?一样,使用基于时间的帧切换!
class SunFlower(pygame.sprite.Sprite):
_rate = 100
_width = 82
_height = 77
_number = 18
images = []
def __init__(self):
self.order = 0
pygame.sprite.Sprite.__init__(self)
if len(self.images) == 0:
self.images = load_image("sunflower.png", self._width, self._number)
self.image = self.images[self.order]
self.rect = Rect(0, 0, self._width, self._height)
self.life = self._life
self.passed_time = 0
def update(self, passed_time):
self.passed_time += passed_time
self.order = ( self.passed_time / self._rate ) % self._number
if self.order == 0 and self.passed_time > self._rate:
self.passed_time = 0
self.image = self.images[self.order]
与上一个SunFlower类相比,我们多了一个_rate静态变量,然后初始化的时候也多了一个passed_time静态变量,update也多了一个参数。尽管看起来有变得复杂了一些,这一切都是值得的!_rate意味着一帧画面要保持多少毫秒,passed_time则记录着游戏经过了多少毫秒(为了避免passed_time无限增大,我在合适的时候把它归零了),我们通过这两个参数计算出现在应该显示第几帧,尽管形式不同,这和基于时间的移动是一个道理。希望大家能看明白(不明白的话多看几遍……再看不明白的去复习第8部分,再不明白的,面壁去……哦,我是说咱俩都得去面壁)。
这里不就先不给出完整的程序了,相信有了前面的经验很容易就可以写一个框子运行起来,自己试一下?如果你能看到类似左边的画面,你就赢了!准备迎接下一波惊喜吧。
如果没能成功的运行起来(虽然似乎有点不应该),也没关系,这个系列完成以后会放出完整代码的。
本次使用的太阳花资源图片: sunflower.png