package net.alcuria.umbracraft.engine.entities; import net.alcuria.umbracraft.Config; import net.alcuria.umbracraft.Db; import net.alcuria.umbracraft.Game; import net.alcuria.umbracraft.definitions.area.AreaDefinition; import net.alcuria.umbracraft.definitions.component.ComponentDefinition; import net.alcuria.umbracraft.definitions.config.ConfigDefinition; import net.alcuria.umbracraft.definitions.entity.EntityDefinition; import net.alcuria.umbracraft.definitions.map.EntityReferenceDefinition; import net.alcuria.umbracraft.definitions.map.MapDefinition; import net.alcuria.umbracraft.engine.components.DirectedInputComponent; import net.alcuria.umbracraft.engine.events.CameraTargetEvent; import net.alcuria.umbracraft.engine.scripts.ChangeDirectionScriptCommand; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.ObjectMap; /** The EntityManager maintains all game entities and updates/renders them * accordingly. Entities are divided into three different {@link EntityScope} * types when added: <ol><li>MAP - the entity persists until the player changes * maps</li><li>AREA - the entity persists until the player changes * areas</li><li>GLOBAL - the entity sticks around FOREVER</li></ol> * @author Andrew Keturi */ public class EntityManager { /** The scope of entities found in the game. Area-specific, Map-specific, or * global. * @author Andrew Keturi */ public static enum EntityScope { AREA, GLOBAL, MAP } private static final int ENTITY_TILE_PAD = 4; private final ObjectMap<EntityScope, Array<Entity>> entities = new ObjectMap<EntityScope, Array<Entity>>(); private int mapHeight; private final Array<Entity> visibleEntities = new Array<Entity>(); { entities.put(EntityScope.AREA, new Array<Entity>()); entities.put(EntityScope.GLOBAL, new Array<Entity>()); entities.put(EntityScope.MAP, new Array<Entity>()); } /** Adds an entity to the manager of some particular scope. * @param scope the {@link EntityScope}. for instance * {@link EntityScope#MAP} entities get cleared on map change. * @param entity the {@link Entity} to add to the manager. */ public void add(EntityScope scope, Entity entity) { if (scope == null) { throw new NullPointerException("Scope cannot be null for entity insertion"); } else if (entity == null) { throw new NullPointerException("Entity cannot be null for entity insertion"); } entities.get(scope).add(entity); } private void addComponents(Entity entity, String name) { EntityDefinition entityDef = Game.db().entity(name); if (entityDef != null) { for (ComponentDefinition componentDef : entityDef.components) { entity.addComponent(componentDef); } entity.speedModifier = entityDef.speedModifier; if (entityDef.renderOffset != 0) { entity.setRenderOffset(entityDef.renderOffset); } // TODO: for ControlledInputComponents, I think we need to set the input processor here... if (entity.getName().equals(Entity.PLAYER)) { //FIXME: ugleh Game.publisher().publish(new CameraTargetEvent(entity)); } } } /** Takes as input the name of a map in the {@link Db} and creates all * entities and their components for that map. Clears any existing entities. * @param scope the scope of entities to create * @param name the map id {@link String} */ public void create(EntityScope scope, final String name) { if (scope == null) { throw new NullPointerException("scope cannot be null"); } visibleEntities.clear(); entities.get(scope).clear(); if (scope == EntityScope.GLOBAL) { ConfigDefinition config = Game.db().config(); if (config.globalEntities != null) { for (String entityName : config.globalEntities) { Entity entity = new Entity(); entity.setName(entityName); entity.position.x = config.startingX * Config.tileWidth; entity.position.y = config.startingY * Config.tileWidth; addComponents(entity, entityName); entities.get(EntityScope.GLOBAL).add(entity); } } } else if (scope == EntityScope.AREA) { AreaDefinition area = Game.db().area(name); for (String entityName : area.entities) { Entity entity = new Entity(entityName); addComponents(entity, entity.getName()); entities.get(EntityScope.AREA).add(entity); } } else if (scope == EntityScope.MAP) { // create entities MapDefinition mapDef = Game.db().map(name); mapHeight = mapDef.getHeight(); if (mapDef != null && mapDef.entities != null) { for (EntityReferenceDefinition reference : mapDef.entities) { Entity entity = new Entity(); entity.setFromReference(reference, name); addComponents(entity, reference.name); if (reference.facing != null) { ChangeDirectionScriptCommand.setDirection(reference.facing, entity); } entities.get(EntityScope.MAP).add(entity); } } else { Game.error("Map not found: " + name); } } } /** Dispose all entities, freeing up any resources. */ public void dispose(EntityScope scope) { if (scope == null) { throw new NullPointerException("Scope cannot be null"); } if (entities == null) { return; } for (int i = 0; i < entities.get(scope).size; i++) { entities.get(scope).get(i).dispose(); } } /** Finds an entity with the given name. Entities may have the same name, but * the first one added to the {@link EntityManager} will be returned. If no * entity is found with the given name, <code>null</code> is returned. * @param name the name {@link String} * @return the {@link Entity} with the given name, or <code>null</code> if * none is found */ public Entity find(String name) { for (EntityScope scope : EntityScope.values()) { for (int i = 0; i < entities.get(scope).size; i++) { if (entities.get(scope).get(i).getName() != null && entities.get(scope).get(i).getName().equals(name)) { return entities.get(scope).get(i); } } } return null; } /** Gets the column an entity is at for rendering */ private int getCol(Entity entity) { return (int) ((entity.position.x) / Config.tileWidth); } /** @param scope the {@link EntityScope} for which we want to fetch all * entities * @return all entities in the {@link EntityManager}. This is not a copy so * be careful with it. */ public Array<Entity> getEntities(EntityScope scope) { if (scope == null) { throw new NullPointerException("Entity scope cannot be null"); } return entities.get(scope); } /** Gets the row an entity is at for rendering */ private int getRow(Entity entity) { return (int) ((entity.position.y + entity.position.z) / Config.tileWidth); } /** Renders all objects in view. */ public void render() { if (entities == null) { return; } // get the visible tiles on the screen int x = (int) (Game.view().getCamera().position.x - Config.viewWidth / 2) / Config.tileWidth - ENTITY_TILE_PAD; int y = (int) (Game.view().getCamera().position.y - Config.viewHeight / 2) / Config.tileWidth - ENTITY_TILE_PAD; int width = Config.viewWidth / Config.tileWidth + ENTITY_TILE_PAD * 2; int height = (Config.viewHeight / Config.tileWidth) + ENTITY_TILE_PAD * 2; // add visible entitities onscreen int row = y + height; // start at the top for (EntityScope scope : EntityScope.values()) { for (int i = 0; i < entities.get(scope).size; i++) { final int entityRow = getRow(entities.get(scope).get(i)); final int entityCol = getCol(entities.get(scope).get(i)); if (entityCol > x && entityCol < x + width && entityRow < row && entityRow >= row - height && entityRow >= 0 && entityRow <= mapHeight) { visibleEntities.add(entities.get(scope).get(i)); } } } visibleEntities.sort(); // render each row in view, starting from the top int idx = 0; while (row > y - Game.map().getMaxAltitude() * 2) { Game.map().render(row, x + ENTITY_TILE_PAD); while (idx < visibleEntities.size && (visibleEntities.get(idx).position.y - visibleEntities.get(idx).getRenderOffset()) / Config.tileWidth >= row) { visibleEntities.get(idx).render(); idx++; } row--; } // render our overlays last Game.map().renderOverlays(x + ENTITY_TILE_PAD, y - ENTITY_TILE_PAD); // FIXME: required for the overlays since they're drawn 4 tiles up? visibleEntities.clear(); } /** Attempts to render the path of any Entity with a * {@link DirectedInputComponent} */ public void renderPaths() { for (final Entity e : entities.get(EntityScope.MAP)) { final DirectedInputComponent component = e.getComponent(DirectedInputComponent.class); if (component != null) { component.renderPaths(); } } } /** Sets the height of the map, for rendering. This should be in pixels. * @param height height of the map */ public void setRenderHeight(int height) { mapHeight = height; } /** Updates the state of all objects. */ public void update(float delta) { if (entities == null) { return; } Game.map().update(delta); for (EntityScope scope : EntityScope.values()) { for (int i = 0; i < entities.get(scope).size; i++) { entities.get(scope).get(i).update(); } } } }