用Pygame写游戏-从入门到精通14


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

用Pygame写游戏-从入门到精通14-LMLPHP

状态定义了两个内容:

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

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

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

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

游戏实体类

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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用来描述外界。这里的世界不需要多复杂,仅仅需要准备一个蚁穴,和存储若干的实体位置就足够了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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,实例作为内容来存放,实际的样子会是这样:

用Pygame写游戏-从入门到精通14-LMLPHP

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

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

09-15 07:32