def test_lru(self): lru = LRUCache(2) lru.add("k1", "v1") self.assertEqual(lru.debug_peek('k1'), 'v1') self.assertEqual(lru.debug_peek('k2'), None) lru.add("k2", "v2") self.assertEqual(lru.debug_peek('k2'), 'v2') lru.add("k3", "v3") self.assertEqual(lru.debug_peek('k1'), None) self.assertEqual(lru.debug_peek('k2'), 'v2') self.assertEqual(lru.get("k2"), "v2") lru.add("k4", "v4") self.assertEqual(lru.debug_peek('k3'), 'v3') lru.add("k5", "v5") self.assertEqual(lru.debug_peek('k3'), None) self.assertEqual(lru.get('k10086'), None)
def test_lru(): cache_store = LRUCache(5) cache_store.add('1', 1) assert cache_store.get('1') == 1 assert cache_store.first.key == '1' assert cache_store.last.key == '1' cache_store.add('2', 2) assert cache_store.get('2') == 2 assert cache_store.first.key == '1' assert cache_store.last.key == '2' cache_store.add('3', 3) assert cache_store.get('3') == 3 assert cache_store.first.key == '1' assert cache_store.last.key == '3' cache_store.add('4', 4) assert cache_store.get('4') == 4 assert cache_store.first.key == '1' assert cache_store.last.key == '4' cache_store.add('5', 5) assert cache_store.get('5') == 5 assert cache_store.first.key == '1' assert cache_store.last.key == '5' cache_store.add('5', 6) assert cache_store.get('5') == 6 assert cache_store.first.key == '1' assert cache_store.last.key == '5' cache_store.add('6', 6) assert cache_store.get('6') == 6 assert cache_store.get_cache_space() == 5 assert cache_store.get('1') == None assert cache_store.first.key == '2' assert cache_store.last.key == '6' cache_store.remove('2') assert cache_store.get('2') == None assert cache_store.get_cache_space() == 4 assert cache_store.first.key == '3' assert cache_store.first.previous_item == None assert cache_store.last.key == '6' cache_store.add('3', 4) assert cache_store.get('3') == 4 assert cache_store.first.key == '4' assert cache_store.first.previous_item == None assert cache_store.last.key == '3' cache_store.add('4', 6) assert cache_store.get('4') == 6 assert cache_store.first.key == '5' assert cache_store.last.key == '4' cache_store.add('4', 1) assert cache_store.get('4') == 1 assert cache_store.last.key == '4' cache_store.add('7', 7) assert cache_store.get('7') == 7 assert cache_store.get_cache_space() == 5 assert cache_store.last.key == '7' cache_store.add('8', 8) assert cache_store.get('8') == 8 assert cache_store.last.key == '8' assert cache_store.get('5') == None cache_store.remove('3') assert cache_store.get('3') == None cache_store.remove('4') assert cache_store.get('4') == None cache_store.remove('6') assert cache_store.get('6') == None cache_store.remove('7') assert cache_store.get('7') == None cache_store.remove('8') assert cache_store.get('8') == None assert cache_store.get_cache_space() == 0
class ChunkManager(object): def __init__(self, texture_config, spawner, map_generator, storage_mgr, chunk_title_count=16, chunk_tile_size=2., chunk_count=9, ): """ :param chunk_title_count: chunk中的tile的数量 :param chunk_tile_size: tile在世界中的尺寸 :return: """ self._tile_not_found_exception = Exception('tile not found') self._chunk_count = chunk_count self._storage_mgr = storage_mgr self._chunk_tile_count = chunk_title_count self._chunk_tile_size = chunk_tile_size self._chunks = {} self._chunk_size = self._chunk_tile_count * self._chunk_tile_size # chunk在世界中的尺寸 self._center_chunk_id = (0, 0) self._generator = map_generator self._spawner = spawner self._cache = LRUCache(32) # TODO 根据机器内存大小动态设置一个合理的值。 self._async_loader = async_loader.AsyncLoader() self._async_loader.start() self._yielders = [] self._processing_chunk_keys = [] # 当前正在加载的ChunkIDs(加载分为很多帧进行的) self._ground_geom_util = GroundGeomUtil(self._chunk_tile_size, self._chunk_tile_count, map_generator, texture_config) from collections import Counter self._unload_counter = Counter() self._counter_threshold = 0 if G.debug else 1 def __str__(self): items = [] for chunk_id, chunk_data in self._chunks.iteritems(): items.append(str(chunk_id)) return "ChunkManager[%s]" % ('\t'.join(items)) def get_chunk_ids(self): return self._chunks.keys() def xy2rc(self, x, y): c = int(math.floor(x / self._chunk_size)) r = int(math.floor(y / self._chunk_size)) return r, c def transfer_frozen_object(self, src_chunk, frozen_object): """ 尝试将物体从src_chunk移动到正确的chunk中. :param src_chunk: :param frozen_object: :return: """ pos = frozen_object.get_pos() key = self.xy2rc(pos[0], pos[1]) target_chunk = self._chunks.get(key) # assert target_chunk != src_chunk, frozen_object.get_pos() if target_chunk: target_chunk.add_object(frozen_object) return True else: return False def rc2xy(self, r, c): return c * self._chunk_size, r * self._chunk_size def _iter_chunk_keys(self, x, y): """ 按照某种规则,返回点(x,y)附近需要载入的方块。 :param x: :param y: :return: """ center_r, center_c = self.xy2rc(x, y) result = [] for dr in range(-3, 4): for dc in range(-3, 4): r, c = center_r + dr, center_c + dc tx, ty = self.rc2xy(r, c) tx += self._chunk_size * .5 ty += self._chunk_size * .5 dist_sq = (tx - x) ** 2 + (ty - y) ** 2 result.append((dist_sq, (r, c))) # 排序并返回 result.sort(key=lambda v: v[0]) for v in result[:self._chunk_count]: yield v[1] def xy2world_rc(self, x, y): c = int(math.floor(x / self._chunk_tile_size)) r = int(math.floor(y / self._chunk_tile_size)) return r, c def world_rc2chunk_rc(self, r, c): return r / self._chunk_tile_count, c / self._chunk_tile_count def world_rc2inner_rc(self, r, c): return r / self._chunk_tile_count, c / self._chunk_tile_count def get_around_tiles(self, x, y, radius): """ Warning 该函数性能较差, 不适合经常调用 :param x: :param y: :param radius: :return: """ world_r, world_c = self.xy2world_rc(x, y) tiles = [] for delta_r in range(-radius, radius + 1): for delta_c in range(-radius, radius + 1): key = self.world_rc2chunk_rc(world_r + delta_r, world_c + delta_c) chk = self._chunks.get(key) if not chk: continue inner_r, inner_c = world_r + delta_r - key[0] * self._chunk_tile_count, world_c + delta_c - key[1] * self._chunk_tile_count tile = chk.get_tile_at(inner_r, inner_c) assert tile tiles.append(tile) return tiles def on_save(self): """ 保存当前所有信息到storage_mgr :return: """ for key, value in self._chunks.iteritems(): self._storage_mgr.set(str(key), value.on_save()) def destroy(self): """ 离开当前地图时调用,销毁所有chunk。 Warning:赢得先调用on_save保存当前地图。 :return: """ for chk in self._chunks.itervalues(): chk.destroy() # TODO 考虑正在载入的部分 def get_around_objects(self, cx, cy, size=1): objects = [] for dr in range(-size, size + 1): for dc in range(-size, size + 1): x = cx + dr * self._chunk_tile_size y = cy + dc * self._chunk_tile_size r, c = self.xy2rc(x, y) chunk = self._chunks.get((r, c)) if chunk: objects.extend(chunk.get_objects_at_pos(x, y)) return objects def get_closest_object(self, cx, cy, size): objects = self.get_around_objects(cx, cy, size) min_dist = 999 min_obj = None for obj in objects: pos = obj.get_pos() dist = (cx - pos.get_x()) ** 2 + (cy - pos.get_y()) ** 2 if dist < min_dist: min_dist = dist min_obj = obj return min_obj def get_closest_objects(self, cx, cy, size): objects = self.get_around_objects(cx, cy, size) sorted_objects = [] for obj in objects: pos = obj.get_pos() dist = (cx - pos.get_x()) ** 2 + (cy - pos.get_y()) ** 2 sorted_objects.append((dist, obj)) sorted_objects.sort(key=lambda v: v[0]) return sorted_objects def on_load(self): """ 不需要on_load函数,载入on_update时动态载入。 :return: """ assert False def remove_entity(self, entity): pos = entity.get_pos() r, c = self.xy2rc(pos.get_x(), pos.get_y()) chunk = self._chunks.get((r, c)) # assert chunk, (r, c, chunk, entity, entity.get_name()) if chunk: chunk.remove_entity(entity) self._update_chunk_static_models(chunk) def _update_chunk_static_models(self, chunk): chunk.get_flatten_fn()() def spawn_to_exist_chunk(self, x, y, config): """ Spawn an entity at position (x,y) with config. :param x: :param y: :param config: :return: """ key = self.xy2rc(x, y) chk = self._chunks.get(key) assert chk, 'pos (%s,%s) not in any chunk' % (x, y) obj = self._spawner.spawn(x, y, config) assert sys.getrefcount(obj) == 2 chk.add_object(obj) return obj def add_ground_item(self, ground_item): pos = ground_item.get_pos() r, c = self.xy2rc(pos.get_x(), pos.get_y()) chunk = self._chunks.get((r, c)) assert chunk, (r, c, ground_item) chunk.add_ground_item(ground_item) def _load_chunk(self, r, c): chunk_self = self chunk_key = (r, c) assert chunk_key not in self._processing_chunk_keys self._processing_chunk_keys.append(chunk_key) def wrapper(): chunk = None try: chunk = self._chunks.get(chunk_key) if chunk: return chunk = self._load_chunk_real(r, c) assert not self._chunks.get(chunk_key), '防止chunk._load_chunk_real()中不小心赋值' time.sleep(0.001) if chunk and not chunk.is_geom_flattened(): fn = chunk.get_flatten_fn() if fn: fn() finally: # 确保 _processing_chunk_keys 里面的值始终正确 chunk_self._processing_chunk_keys.remove(chunk_key) if chunk: self._chunks[chunk_key] = chunk self._async_loader.add_job(wrapper) def _create_block_bodies(self, r, c): """ 生成地图不可到达部分的collider :return: """ blocked_tiles = set() for tile_r in range(-1, self._chunk_tile_count + 1): for tile_c in range(-1, self._chunk_tile_count + 1): info = self._generator.get(r * self._chunk_tile_count + tile_r, c * self._chunk_tile_count + tile_c) if not info: blocked_tiles.add((tile_r, tile_c)) if not blocked_tiles: return None body = BulletRigidBodyNode('chunk_collider') body.setMass(0) body.set_static(True) body.setIntoCollideMask(gconf.BIT_MASK_BLOCKED) bx, by = self.rc2xy(r, c) half_tile_size = self._chunk_tile_size * .5 half_size = Vec3(half_tile_size) rc_list = ((-1, 0), (1, 0), (0, 1), (0, -1)) for tile_r in range(0, self._chunk_tile_count): for tile_c in range(0, self._chunk_tile_count): if (tile_r, tile_c) not in blocked_tiles: continue any_walkable = False for r, c in rc_list: if (tile_r + r, tile_c + c) not in blocked_tiles: any_walkable = True break if not any_walkable: continue shape = BulletBoxShape(half_size) pos = Point3(bx + tile_c * self._chunk_tile_size + half_tile_size, by + tile_r * self._chunk_tile_size + half_tile_size, half_tile_size) body.addShape(shape, TransformState.makePos(pos)) return body def spawn_with_data(self, x, y, data): new_obj = self._spawner.spawn_from_storage(data) assert new_obj new_obj.set_pos(Vec3(x, y, 0)) r, c = self.xy2rc(x, y) chunk = self._chunks[(r, c)] assert chunk, 'cannot spawn outside of the view area (%s,%s)' % (x, y) chunk.add_object(new_obj) self._update_chunk_static_models(chunk) def spawn_object(self, name, x, y): new_obj = self._spawner.spawn_default(name, x, y) r, c = self.xy2rc(x, y) chunk = self._chunks[(r, c)] assert chunk, 'cannot spawn outside of the view area (%s,%s)' % (x, y) chunk.add_object(new_obj) def _load_chunk_real(self, r, c): """ :param r: :param c: :return: """ chunk_key = (r, c) bx, by = self.rc2xy(r, c) # 从缓存中载入 cache_value = self._cache.get(chunk_key) if cache_value: cache_value.set_enabled(True) return cache_value # 从存档种载入 new_chunk = chunk.Chunk(self, bx, by, self._chunk_tile_count, self._chunk_tile_size) if self._storage_mgr: storage_data = self._storage_mgr.get(str((r, c))) if storage_data: new_chunk.on_load(self._spawner, storage_data) ground_data = new_chunk.get_ground_data() assert ground_data ground_np = self._ground_geom_util.create_ground_from_data(r, c, ground_data) time.sleep(0.001) block_body = self._create_block_bodies(r, c) new_chunk.set_ground_geom(ground_np, ground_data, block_body) return new_chunk # 生成tile物体和地形 time.sleep(0.001) block_body = self._create_block_bodies(r, c) plane_np, tiles_data = self._ground_geom_util.new_ground_geom(r, c) new_chunk.set_ground_geom(plane_np, tiles_data, block_body) # 遍历所有tile生成物体 # TODO 优化点,map_generator的get可以只调用一次吗? br = r * self._chunk_tile_count bc = c * self._chunk_tile_count for ir in range(br, br + self._chunk_tile_count): time.sleep(0.001) for ic in range(bc, bc + self._chunk_tile_count): ginfo = self._generator.get(ir, ic) if not ginfo: continue obj_info = ginfo.get('object') if obj_info: x = (ic + .5) * self._chunk_tile_size y = (ir + .5) * self._chunk_tile_size new_obj = self._spawner.spawn(x, y, obj_info) pos = new_obj.get_pos() assert abs(pos.get_x() - x) < 0.01, '%s,%s => %s' % (x, y, pos) assert new_obj # 确保spawner可以返回正确的值 ref_count = sys.getrefcount(new_obj) assert ref_count == 2, ref_count # 确保spawner自己不会占用引用. new_obj占一个引用,参数占一个引用 new_chunk.add_object(new_obj) assert sys.getrefcount(new_obj) == 3 # 确保被正确添加到了chunk中 return new_chunk def _unload_chunk(self, chunk_key): self._unload_counter[chunk_key] += 1 if self._unload_counter[chunk_key] < self._counter_threshold or random.random() < .5: return del self._unload_counter[chunk_key] chunk_self = self if chunk_key in self._processing_chunk_keys: return self._processing_chunk_keys.append(chunk_key) def wrapper(): try: self._unload_chunk_real(chunk_key) finally: chunk_self._processing_chunk_keys.remove(chunk_key) self._async_loader.add_job(wrapper) def _unload_chunk_real(self, chunk_id): # 这里有个小坑,不能用 dict[key] = None 这种方式来删除key(在Lua中是可以的)。 target_chunk = self._chunks[chunk_id] target_chunk.set_enabled(False) cache_key, cache_value = self._cache.add(chunk_id, target_chunk) del self._chunks[chunk_id] del chunk_id # 防止误用 time.sleep(0.001) # 删除过期的cache if cache_key: data = cache_value.on_save() time.sleep(0.001) cache_value.destroy() if self._storage_mgr: self._storage_mgr.set(str(cache_key), data) def on_update(self, x, y, dt): """ 根据位置进行更新,始终保持只有主角附近的chunk在内存中。 :param x: 世界坐标x :param y: 世界坐标y :param dt: 单位秒 :return: None """ # 更新并创建不存在的chunk all_keys = set() for (r, c) in self._iter_chunk_keys(x, y): key = (r, c) del self._unload_counter[key] all_keys.add(key) existing_chunk = self._chunks.get(key) if not existing_chunk: if (r, c) not in self._processing_chunk_keys: self._load_chunk(r, c) continue existing_chunk.on_update(dt) # 删除不在附近的chunk for chunk_id in self._chunks.keys(): if chunk_id not in all_keys: self._unload_chunk(chunk_id) # 执行yielders. 纯粹是性能优化,可暂时忽略。 remained_yielders = [] for yielder in self._yielders: try: yielder.next() remained_yielders.append(yielder) except StopIteration: pass self._yielders = remained_yielders