上一次稍微說了一下AI,為了更好的理解它,我們必須明白什麼是狀態機。有限狀態機(英語:finite-state machine, FSM),又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。太抽象了,我們看看上一次的機器人的狀態圖,大概是長的這個樣子:
狀態定義了兩個內容:
- 當前正在做什麼
- 轉化到下一件事時候的條件
狀態同時還可能包含進入(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,例項作為內容來存放,實際的樣子會是這樣:
大多數的方法都用來管理實體,比如add_entity和remove_entity。process方法是用來呼叫所有試題的process,讓它們更新自己的狀態;而render則用來繪製這個世界;最後get_close_entity用來尋找某個範圍內的實體,這個方法會在實際模擬中用到。
這兩個類還不足以構築我們的昆蟲世界,但是卻是整個模擬的基礎,下一次我們就要講述實際的螞蟻類和大腦(狀態機類)。