Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
80 lines (66 sloc) 6.33 KB
title tags categories date
一个面向过程变为面向对象的例子
Python
作品
笔记
面向对象
计算机
2016-01-29 05:42:47 -0800

最近忽然想完整地写一个事件驱动的炸弹人游戏玩玩,算是把之前学到的Tkinter、稳定帧率、人工智能等等的技术综合起来练练手吧。以前写程序时,我一般会提前确定整体是面向过程的还是面向对象的(基本上貌似只要较为复杂就会试图从建立接口开始工作),但这样的弊端是前期开发时常常会陷入可怕的定义接口和划分责任的泥沼中,对于一个人独立快速开发一些好玩的东西来说,这种不必要的困难根本就是我自找的。因此,这一次我告诫自己,尽量先用最简单和最快的方式来书写,看看到什么时候我才会觉得不得不搬出类型和对象的那一套来。于是,现在时候到了,我发现了一个不错的“面向过程和面向对象的区别”的例子。

这个游戏的架构是在单一线程中监听按键事件和计时器事件。当按键事件发生时只是简单记录下来。当计时器事件发生时,它处理上次计时器事件以来发生的按键事件,判定用户的指令会对玩家角色产生什么效果,并询问非玩家角色采取什么动作,有效的动作会产生相应的动画。接下来,所有正在执行的动画被前进到新的一帧,最后绘制出这一帧的图像。

解释一下所谓的“动画”:以经典游戏《红色警戒》为例,在某一时刻,用户点击地图,这个操作的效果是取决于很多因素的,例如当前选中的部队类型,被点击的物体等等。如果这是一个有效的“为部队设定目的地”操作,那么接下来即使玩家什么也不做,游戏也会在很长的一段时间内执行“部队行进”动画——动画不是指简单的视频,而是指一个需要一定时间才能完成的、自动逐渐进行的事情。在这里,系统每隔一个很短的时间就重新计算这个部队的线路,并且使部队前进一点点。这个动画会在部队到达目的地后结束,或者因为部队受到攻击而改变为“反击敌方有威胁的单位”的动画,当然了,部队被全歼也会导致动画结束。

现在我先做出一个能行走和放炸弹的最简单的炸弹人。角色的坐标必须是整数,如果要行走,就在一段时间内慢慢移动到相邻的位置,直到移动完成,角色的坐标才变成新值,行走过程中按方向键是无效的。任何时候按空格立刻在当前坐标放置一枚炸弹,炸弹经过一段时间后爆炸消失。炸弹不能重叠。最初我所写出的计时器事件程序大致是这个样子的:

def onTimer(animes=[]):
    if 用户按下方向键 and 目标位置是空的 and 角色当前没有在移动:
        animes.append(['move', 角色, 起始坐标x, 起始坐标y, deltax, deltay, 0])
    if 用户按下空格键 and 角色脚下是空的:
        animes.append(['bomb', 绘制圆形(x, y, r), 炸弹威力, 坐标x, 坐标y, 0])
    for anime in animes:
        if anime[0] == 'move':
            anime[6] += 0.1
            if anime[6] > 0.99:
                anime[1].x = anime[2] + anime[4]
                anime[1].y = anime[3] + anime[5]
                anime[0] = None
                在x=anime[2]+anime[4]*anime[6], y=anime[3]+anime[5]*anime[6]处绘制角色
        elif anime[0] == 'bomb':
            anime[5] += 1
            if anime[5] > 5:
                爆炸(假设爆炸不绘制任何图形,也没有后续动画,那么这个动画就结束了)
                清除图形(anime[1])
                anime[0] = None
    animes = filter(lambda a: a[0], animes)

我的思路就是用列表中的第一项来表示动画的类型,后面若干项记录了这个动画的一些参数,具体含义视类型而不同。最后一般都有一项用来记录目前播放的进度。然而这一段代码充斥着“特殊情况”——anime[3]的含义各处不同,而且这个写法本身就是晦涩难懂的。要是我利用Python的dict类型而不是list,可读性会好一些,但假如我在写C程序,就没有什么dict可以用了。最最致命的是,这段代码把各种各样(以后可能增加到几十种)不同的动画的处理全堆积在一系列if...elif中,这是没有可扩展性的。至少也要把那一堆if...elif换成一句exec('anime_'+anime[0]+'(anime)')才好(要是想在C中实现类似于这样的东西,就得给所有动画编号,然后把处理它们的函数们组成函数指针数组)。

这正是面向对象应该大显身手的地方。所有的动画之间是有共性的——它们都要耗费一定时间才能完成,需要一个变量来存储当前的进度,每次计时器触发后都要做点事情。但是它们又各不相同——每类动画有自己需要的一些参数,有自己要做的工作。上面那个for循环就应该改成这样才好:

for anime in animes:
    anime.step(nowtime)

我们可以定义一个通用的Anime类,做每一个anime都要做的事情:

class Anime:
    stoped = False
    def __init__(self, starttime):
        self.starttime = starttime
    def step(self, nowtime):
        self.progress = nowtime - self.starttime
    def stop(self):
        self.stoped = True

而子类,比如AnimeMove这样实现:

class AnimeMove(Anime):
    def __init__(self, starttime, player, x, y, dx, dy, totaltime):
        Anime.__init__(self, starttime)
        self.player = player
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
        self.totaltime = totaltime
    def step(self, nowtime):
        Anime.step(self, nowtime)
        if self.progress >= self.totaltime:
            self.player.x = self.x + self.dx
            self.player.y = self.y + self.dy
            self.stop()
        在x=..., y=...处绘制角色

这样一来,处理不同动画的程序就在不同的位置各自独立存在,维护起来容易多了。而且,如果未来要加入和已有的动作相似的新动作,也可以通过继承而来,增强了代码的可复用性。

You can’t perform that action at this time.