用Python和Pygame写游戏-从入门到精通(14)

By | 2011/07/14

上一次稍微说了一下AI,为了更好的理解它,我们必须明白什么是状态机。有限状态机(英语:finite-state machine, FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。太抽象了,我们看看上一次的机器人的状态图,大概是长的这个样子:

状态机

状态定义了两个内容:

  • 当前正在做什么
  • 转化到下一件事时候的条件

状态同时还可能包含进入(entry)退出(exit)两种动作,进入时间是指进入某个状态时要做的一次性的事情,比如上面的怪,一旦进入攻击状态,就得开始计算与玩家的距离,或许还得大吼一声“我要杀了你”等等;而退出动作则是与之相反的,离开这个状态要做的事情。

我们来创建一个更为复杂的场景来阐述这个概念——一个蚁巢世界。我们常常使用昆虫来研究AI,因为昆虫的行为很简单容易建模。在我们这次的环境里,有三个实体(entity)登场:叶子、蜘蛛、蚂蚁。叶子会随机的出现在屏幕的任意地方,并由蚂蚁回收至蚁穴,而蜘蛛在屏幕上随便爬,平时蚂蚁不会在意它,而一旦进入蚁穴,就会遭到蚂蚁的极力驱赶,直至蜘蛛挂了或远离蚁穴。

尽管我们是对昆虫建模的,这段代码对很多场景都是合适的。把它们替换为巨大的机器人守卫(蜘蛛)、坦克(蚂蚁)、能源(叶子),这段代码依然能够很好的工作。

游戏实体类

这里出现了三个实体,我们试着写一个通用的实体基类,免得写三遍了,同时如果加入了其他实体,也能很方便的扩展出来。

一个实体需要存储它的名字,现在的位置,目标,速度,以及一个图形。有些实体可能只有一部分属性(比如叶子不应该在地图上瞎走,我们把它的速度设为0),同时我们还需要准备进入和退出的函数供调用。下面是一个完整的GameEntity类:

class GameEntity(object):
    def __init__(self, world, name, image):
        self.world = world
        self.name = name
        self.image = image
        self.location = Vector2(0, 0)
        self.destination = Vector2(0, 0)
        self.speed = 0.
        self.brain = StateMachine()
        self.id = 0
    def render(self, surface):
        x, y = self.location
        w, h = self.image.get_size()
        surface.blit(self.image, (x-w/2, y-h/2))
    def process(self, time_passed):
        self.brain.think()
        if self.speed > 0 and self.location != self.destination:
            vec_to_destination = self.destination - self.location
            distance_to_destination = vec_to_destination.get_length()
            heading = vec_to_destination.get_normalized()
            travel_distance = min(distance_to_destination, time_passed * self.speed)
            self.location += travel_distance * heading

观察这个类,会发现它还保存一个world,这是对外界描述的一个类的引用,否则实体无法知道外界的信息。这里类还有一个id,用来标示自己,甚至还有一个brain,就是我们后面会定义的一个状态机类。

render函数是用来绘制自己的。

process函数首先调用self.brain.think这个状态机的方法来做一些事情(比如转身等)。接下来的代码用来让实体走近目标。

世界类

我们写了一个GameObject的实体类,这里再有一个世界类World用来描述外界。这里的世界不需要多复杂,仅仅需要准备一个蚁穴,和存储若干的实体位置就足够了:

class World(object):
    def __init__(self):
        self.entities = {} # Store all the entities
        self.entity_id = 0 # Last entity id assigned
        # 画一个圈作为蚁穴
        self.background = pygame.surface.Surface(SCREEN_SIZE).convert()
        self.background.fill((255, 255, 255))
        pygame.draw.circle(self.background, (200, 255, 200), NEST_POSITION, int(NEST_SIZE))
    def add_entity(self, entity):
        # 增加一个新的实体
        self.entities[self.entity_id] = entity
        entity.id = self.entity_id
        self.entity_id += 1
    def remove_entity(self, entity):
        del self.entities[entity.id]
    def get(self, entity_id):
        # 通过id给出实体,没有的话返回None
        if entity_id in self.entities:
            return self.entities[entity_id]
        else:
            return None
    def process(self, time_passed):
        # 处理世界中的每一个实体
        time_passed_seconds = time_passed / 1000.0
        for entity in self.entities.itervalues():
            entity.process(time_passed_seconds)
    def render(self, surface):
        # 绘制背景和每一个实体
        surface.blit(self.background, (0, 0))
        for entity in self.entities.values():
            entity.render(surface)
    def get_close_entity(self, name, location, range=100.):
        # 通过一个范围寻找之内的所有实体
        location = Vector2(*location)
        for entity in self.entities.values():
            if entity.name == name:
                distance = location.get_distance_to(entity.location)
                if distance < range:
                    return entity
        return None

因为我们有着一系列的GameObject,使用一个列表来存储就是很自然的事情。不过如果实体增加,搜索列表就会变得缓慢,所以我们使用了字典来存储。我们就使用GameObject的id作为字典的key,实例作为内容来存放,实际的样子会是这样:

大多数的方法都用来管理实体,比如add_entity和remove_entity。process方法是用来调用所有试题的process,让它们更新自己的状态;而render则用来绘制这个世界;最后get_close_entity用来寻找某个范围内的实体,这个方法会在实际模拟中用到。

这两个类还不足以构筑我们的昆虫世界,但是却是整个模拟的基础,下一次我们就要讲述实际的蚂蚁类和大脑(状态机类)。

>> 用Python和Pygame写游戏-从入门到精通(15)

 

22 thoughts on “用Python和Pygame写游戏-从入门到精通(14)

  1. 机车男

    Pygame初学者,谢谢阁下的文章,获益匪浅!

    Reply
  2. 林先炎

    为什么get_close_entity函数最后renturn的只有一个entity?注释不是说找到所有的实体吗?

    Reply
  3. emanonchao

    能否解释一下GameEntity类里面的process方法中,travel_distance为何是取distance_to_destination和time_passed*self.speed 的较小值 ?

    Reply
    1. 鱼儿

      因为如果速度过快一帧就超过目的地点了. 是为防止超过目的地点.

      Reply
    2. sich

      time_passed * self.speed(简称t*s)表示 帧刷新时实体应该移动的距离
      distance_to_destination(简称dtd)表示 实体坐标到目标位置的距离
      当实体向目标移动过程中的最后一帧刷新时,实体按正常速度走的距离(t*s)往往会大于它到目标的距离(dtd),这样就会超过目标点。这时不能再按(t*s)刷新,应该按(dtd)刷新,使实体最终刚好落到目标点。

      Reply
  4. 高桥

    请问最后一个get_close_entity,
    按照楼主函数的写法,只能返回一个实体。
    如果self.entities是:{0:’Ants1′,1:’Ants2’…..}
    那寻找的时候会先从id为0的实体寻找,若是没有id=0的实体再找1的,否则立刻返回。
    是不是应该返回一个存有实体id的列表呢?

    Reply
    1. NiceHxf

      其实我觉得也有这个问题,我认为应该先遍历所有符合条件的实体,然后计算出一个最短距离的返回。但是这样复杂度提升了。如果,范围很小,实体很“稀疏”,那么能找到一个应该就非常近了,况且在运动状态下

      Reply
      1. circle orbit

        是的,,符合条件执行的动作可以改成list.append(entity.entity_id),循环结束后加个if list:ruturn list

        另外if entity.name==name这句,似乎类里面没有定义entity.name方法呢

        Reply
  5. 赚钱攻略

      此次的爆料竟来自言承旭同父异母的哥哥,爆料称言承旭的哥哥在 最新银行贷款利率表 喝酒后,脱口説出弟弟和林志玲和好了,不过并没透露覆合详情。 言承旭 林志玲  网友爆料  据台湾媒体报道,今年演艺圈喜事频传,本是“黄金剩女”的林心如、舒淇都嫁给了多年好友,收穫大批祝福,让大家开始猜测会不会哪天就换林志玲[微博]和言承旭[微博]宣布喜讯,而两人目前工作上共享同一位经纪人,可谓是“一家亲”,日前名嘴许圣梅也称他们从没分开过,近日网上则又传出言承旭同父异母的哥哥爆料两人已经和好,最新银行贷款利率表。  言承旭与林志玲感情纠缠14年,分合消息不断,许多粉丝都期待2人能修成正果,继先前许圣梅惊爆他们从没分手,近日“关爱八卦

    Reply
  6. 理财知识入门

      悬疑网络剧《法医秦明》正在热播中,开播后收视反响很好,这部剧把法医推送到观众面前。网友直呼这才是 粤国际微盘 最重口的国产”下饭”剧。 关注微信号:cweipan 手机也能炒白银原油,快来试试吧  作为博集天卷影业第一部作品,创作团队一直在朝着正经职业剧的道路上大步前进。该剧总制片人、总策划、总编剧郭琳媛在接受红网时刻新闻记者采访时表示:”我们始终带着 对这个行业的尊敬来做这部剧,我们希望做到不辱没这个行业,让观众自觉地认为看到了一个 良心之作。  郭琳媛透露,《法医秦明》第二季的剧本正在创作中,至于剧情会不会更酸爽?郭琳媛说:”明年冬天,你就知道了。”   相遇:一个”严肃活泼”的

    Reply
  7. Pingback: 如何用Pygame写游戏(十四)-识荒者

  8. Pingback: 用 Python 和 Pygame 写游戏 – 从入门到精通(目录) – ITPCB

发表评论

您的电子邮箱地址不会被公开。