經歷了長年的艱苦卓絕的披星戴月的慘絕人寰的跋山涉水,我們終於接近了AI之旅的尾聲(好吧,實際上我們這才是剛剛開始)。這一次真正展示一下這幾回辛勤工作的結果,最後的畫面會是這個樣子:
下面給出完整程式碼(注意需要gameobjects庫才可以執行,參考之前的向量篇):
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 |
SCREEN_SIZE = (640, 480) NEST_POSITION = (320, 240) ANT_COUNT = 20 NEST_SIZE = 100. import pygame from pygame.locals import * from random import randint, choice from gameobjects.vector2 import Vector2 class State(object): def __init__(self, name): self.name = name def do_actions(self): pass def check_conditions(self): pass def entry_actions(self): pass def exit_actions(self): pass class StateMachine(object): def __init__(self): self.states = {} self.active_state = None def add_state(self, state): self.states[state.name] = state def think(self): if self.active_state is None: return self.active_state.do_actions() new_state_name = self.active_state.check_conditions() if new_state_name is not None: self.set_state(new_state_name) def set_state(self, new_state_name): if self.active_state is not None: self.active_state.exit_actions() self.active_state = self.states[new_state_name] self.active_state.entry_actions() class World(object): def __init__(self): self.entities = {} self.entity_id = 0 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): 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.values(): entity.process(time_passed_seconds) def render(self, surface): surface.blit(self.background, (0, 0)) for entity in self.entities.itervalues(): entity.render(surface) def get_close_entity(self, name, location, range=100.): location = Vector2(*location) for entity in self.entities.itervalues(): if entity.name == name: distance = location.get_distance_to(entity.location) if distance < range: return entity return None 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 class Leaf(GameEntity): def __init__(self, world, image): GameEntity.__init__(self, world, "leaf", image) class Spider(GameEntity): def __init__(self, world, image): GameEntity.__init__(self, world, "spider", image) self.dead_image = pygame.transform.flip(image, 0, 1) self.health = 25 self.speed = 50. + randint(-20, 20) def bitten(self): self.health -= 1 if self.health <= 0: self.speed = 0. self.image = self.dead_image self.speed = 140. def render(self, surface): GameEntity.render(self, surface) x, y = self.location w, h = self.image.get_size() bar_x = x - 12 bar_y = y + h/2 surface.fill( (255, 0, 0), (bar_x, bar_y, 25, 4)) surface.fill( (0, 255, 0), (bar_x, bar_y, self.health, 4)) def process(self, time_passed): x, y = self.location if x > SCREEN_SIZE[0] + 2: self.world.remove_entity(self) return GameEntity.process(self, time_passed) class Ant(GameEntity): def __init__(self, world, image): GameEntity.__init__(self, world, "ant", image) exploring_state = AntStateExploring(self) seeking_state = AntStateSeeking(self) delivering_state = AntStateDelivering(self) hunting_state = AntStateHunting(self) self.brain.add_state(exploring_state) self.brain.add_state(seeking_state) self.brain.add_state(delivering_state) self.brain.add_state(hunting_state) self.carry_image = None def carry(self, image): self.carry_image = image def drop(self, surface): if self.carry_image: x, y = self.location w, h = self.carry_image.get_size() surface.blit(self.carry_image, (x-w, y-h/2)) self.carry_image = None def render(self, surface): GameEntity.render(self, surface) if self.carry_image: x, y = self.location w, h = self.carry_image.get_size() surface.blit(self.carry_image, (x-w, y-h/2)) class AntStateExploring(State): def __init__(self, ant): State.__init__(self, "exploring") self.ant = ant def random_destination(self): w, h = SCREEN_SIZE self.ant.destination = Vector2(randint(0, w), randint(0, h)) def do_actions(self): if randint(1, 20) == 1: self.random_destination() def check_conditions(self): leaf = self.ant.world.get_close_entity("leaf", self.ant.location) if leaf is not None: self.ant.leaf_id = leaf.id return "seeking" spider = self.ant.world.get_close_entity("spider", NEST_POSITION, NEST_SIZE) if spider is not None: if self.ant.location.get_distance_to(spider.location) < 100.: self.ant.spider_id = spider.id return "hunting" return None def entry_actions(self): self.ant.speed = 120. + randint(-30, 30) self.random_destination() class AntStateSeeking(State): def __init__(self, ant): State.__init__(self, "seeking") self.ant = ant self.leaf_id = None def check_conditions(self): leaf = self.ant.world.get(self.ant.leaf_id) if leaf is None: return "exploring" if self.ant.location.get_distance_to(leaf.location) < 5.0: self.ant.carry(leaf.image) self.ant.world.remove_entity(leaf) return "delivering" return None def entry_actions(self): leaf = self.ant.world.get(self.ant.leaf_id) if leaf is not None: self.ant.destination = leaf.location self.ant.speed = 160. + randint(-20, 20) class AntStateDelivering(State): def __init__(self, ant): State.__init__(self, "delivering") self.ant = ant def check_conditions(self): if Vector2(*NEST_POSITION).get_distance_to(self.ant.location) < NEST_SIZE: if (randint(1, 10) == 1): self.ant.drop(self.ant.world.background) return "exploring" return None def entry_actions(self): self.ant.speed = 60. random_offset = Vector2(randint(-20, 20), randint(-20, 20)) self.ant.destination = Vector2(*NEST_POSITION) + random_offset class AntStateHunting(State): def __init__(self, ant): State.__init__(self, "hunting") self.ant = ant self.got_kill = False def do_actions(self): spider = self.ant.world.get(self.ant.spider_id) if spider is None: return self.ant.destination = spider.location if self.ant.location.get_distance_to(spider.location) < 15.: if randint(1, 5) == 1: spider.bitten() if spider.health <= 0: self.ant.carry(spider.image) self.ant.world.remove_entity(spider) self.got_kill = True def check_conditions(self): if self.got_kill: return "delivering" spider = self.ant.world.get(self.ant.spider_id) if spider is None: return "exploring" if spider.location.get_distance_to(NEST_POSITION) > NEST_SIZE * 3: return "exploring" return None def entry_actions(self): self.speed = 160. + randint(0, 50) def exit_actions(self): self.got_kill = False def run(): pygame.init() screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32) world = World() w, h = SCREEN_SIZE clock = pygame.time.Clock() ant_image = pygame.image.load("ant.png").convert_alpha() leaf_image = pygame.image.load("leaf.png").convert_alpha() spider_image = pygame.image.load("spider.png").convert_alpha() for ant_no in xrange(ANT_COUNT): ant = Ant(world, ant_image) ant.location = Vector2(randint(0, w), randint(0, h)) ant.brain.set_state("exploring") world.add_entity(ant) while True: for event in pygame.event.get(): if event.type == QUIT: return time_passed = clock.tick(30) if randint(1, 10) == 1: leaf = Leaf(world, leaf_image) leaf.location = Vector2(randint(0, w), randint(0, h)) world.add_entity(leaf) if randint(1, 100) == 1: spider = Spider(world, spider_image) spider.location = Vector2(-50, randint(0, h)) spider.destination = Vector2(w+50, randint(0, h)) world.add_entity(spider) world.process(time_passed) world.render(screen) pygame.display.update() if __name__ == "__main__": run() |
這個程式的長度超過了以往任何一個,甚至可能比我們寫的加起來都要長一些。然而它可以展現給我們的也前所未有的驚喜。無數勤勞的小螞蟻在整個地圖上到處覓食,隨機出現的葉子一旦被螞蟻發現,就會搬回巢穴,而蜘蛛一旦出現在巢穴範圍之內,就會被螞蟻們群起而攻之,直到被驅逐出地圖範圍或者掛了,蜘蛛的屍體也會被帶入巢穴。
這個程式碼寫的不夠漂亮,沒有用太高階的語法,甚至都沒有註釋天哪……基本程式碼都在前面出現了,只是新引入了四個新的狀態,AntStateExploring、AntStateSeeking、AntStateDelivering和AntStateHunting,意義的話前面已經說明。比如說AntStateExploring,繼承了基本的Stat,這個狀態的動作平時就是讓螞蟻以一個隨機的速度走向螢幕隨機一個點,在此過程中,check_conditions會不斷檢查周圍的環境,發現了樹葉或蜘蛛都會採取相應的措施(進入另外一個狀態)。
遊戲設計藝術中,建立一個漂亮的AI是非常有挑戰性也非常有趣的事情。好的AI能讓玩家沉浸其中,而糟糕的AI則讓人感到非常乏味(有的時候AI中的一些bug被當作祕籍使用,也挺有意思的,不過如果到處是“祕籍”,可就慘了)。而且,AI是否足夠聰明有時候並不與程式碼量直接相關,看看我們這個演示,感覺上去螞蟻會合作攻擊蜘蛛,而實際上它們都是獨立行動的,不過就結果而言螞蟻們看起來都很聰明。
對AI而已,狀態機是個很有力的工具(當然狀態機不僅僅用在這裡),因為狀態機可以把複雜的系統分割成幾個容易實現的小段。而這每一小部分都是對一些簡單思考或動作的模擬,即便不是那麼容易轉化為程式碼,也很容易模擬。在遊戲中,我們只需要模擬就足夠了。
我們這幾次講述的東西相當有用,儘管不是那麼直觀,但對於遊戲設計至關重要,而此次的蟻巢演示,也給我們揭示了AI系統的種種,至少這個系統式可以運作的了,不錯不錯~ 參天大樹也是從小樹苗開始的。
本次使用的影象資源:
葉子:leaf.png
螞蟻:ant.png
蜘蛛:spider.png
下一次,我們開始更加激動人心的專案,3D影象!