/******************************************************************************* * Copyright 2014 Rafael Garcia Moreno. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.bladecoder.engine.model; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Polygon; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Json; import com.badlogic.gdx.utils.Json.Serializable; import com.badlogic.gdx.utils.JsonValue; import com.bladecoder.engine.actions.SceneActorRef; import com.bladecoder.engine.assets.AssetConsumer; import com.bladecoder.engine.assets.EngineAssetManager; import com.bladecoder.engine.polygonalpathfinder.NavNodePolygonal; import com.bladecoder.engine.polygonalpathfinder.PolygonalNavGraph; import com.bladecoder.engine.util.EngineLogger; import com.bladecoder.engine.util.SerializationHelper; import com.bladecoder.engine.util.SerializationHelper.Mode; public class Scene implements Serializable, AssetConsumer { public static final Color ACTOR_BBOX_COLOR = new Color(0.2f, 0.2f, 0.8f, 1f); public static final Color WALKZONE_COLOR = Color.GREEN; public static final Color OBSTACLE_COLOR = Color.RED; public static final Color ANCHOR_COLOR = Color.RED; public static final float ANCHOR_RADIUS = 14f; public static final String VAR_PLAYER = "$PLAYER"; /** * All actors in the scene */ private HashMap<String, BaseActor> actors = new HashMap<String, BaseActor>(); /** * BaseActor layers */ private List<SceneLayer> layers = new ArrayList<SceneLayer>(); private SceneCamera camera = new SceneCamera(); private Array<AtlasRegion> background; private String backgroundAtlas; private String backgroundRegionId; /** For polygonal PathFinding */ private PolygonalNavGraph polygonalNavGraph; /** * depth vector. X: the actor 'y' position for a 0.0 scale, Y: the actor 'y' * position for a 1.0 scale. */ private Vector2 depthVector; private String player; /** The actor the camera will follow */ private SpriteActor followActor; /** * Music for the scene. */ private MusicDesc musicDesc; private Vector2 sceneSize; private String id; /** internal state. Can be used for actions to maintain a state machine */ private String state; private VerbManager verbs = new VerbManager(); public Scene() { } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getState() { return state; } public void setState(String s) { state = s; } public List<SceneLayer> getLayers() { return layers; } public SceneLayer getLayer(String name) { for (SceneLayer l : layers) { if (name.equals(l.getName())) return l; } return null; } public void addLayer(SceneLayer layer) { layers.add(layer); } public MusicDesc getMusicDesc() { return musicDesc; } public void setMusicDesc(MusicDesc musicDesc) { this.musicDesc = musicDesc; } public float getFakeDepthScale(float y) { if (depthVector == null) return 1.0f; float worldScale = EngineAssetManager.getInstance().getScale(); return Math.max(0, (y - depthVector.x * worldScale) / ((depthVector.y - depthVector.x) * worldScale)); } public VerbManager getVerbManager() { return verbs; } public Verb getVerb(String id) { return verbs.getVerb(id, state, null); } public void runVerb(String id) { verbs.runVerb(id, state, null); } public void update(float delta) { // We draw the elements in order: from top to bottom. // so we need to order the array list for (SceneLayer layer : layers) layer.update(); for (BaseActor a : actors.values()) { a.update(delta); } camera.update(delta); if (followActor != null) { camera.updatePos(followActor); } } public void draw(SpriteBatch batch) { if (background != null) { batch.disableBlending(); batch.setProjectionMatrix(camera.calculateParallaxMatrix(1, 1)); batch.begin(); float x = 0; for (AtlasRegion tile : background) { batch.draw(tile, x, 0f); x += tile.getRegionWidth(); } batch.end(); batch.enableBlending(); } // draw layers from bottom to top for (int i = layers.size() - 1; i >= 0; i--) { SceneLayer layer = layers.get(i); batch.setProjectionMatrix(camera.calculateParallaxMatrix(layer.getParallaxMultiplier(), 1)); batch.begin(); layer.draw(batch); batch.end(); } } public void drawBBoxLines(ShapeRenderer renderer) { // renderer.begin(ShapeType.Rectangle); renderer.begin(ShapeType.Line); for (BaseActor a : actors.values()) { Polygon p = a.getBBox(); if (p == null) { EngineLogger.error("ERROR DRAWING BBOX FOR: " + a.getId()); } if (a instanceof ObstacleActor) { renderer.setColor(OBSTACLE_COLOR); renderer.polygon(p.getTransformedVertices()); } else if (a instanceof AnchorActor) { renderer.setColor(Scene.ANCHOR_COLOR); renderer.line(p.getX() - Scene.ANCHOR_RADIUS, p.getY(), p.getX() + Scene.ANCHOR_RADIUS, p.getY()); renderer.line(p.getX(), p.getY() - Scene.ANCHOR_RADIUS, p.getX(), p.getY() + Scene.ANCHOR_RADIUS); } else { renderer.setColor(ACTOR_BBOX_COLOR); renderer.polygon(p.getTransformedVertices()); } // Rectangle r = a.getBBox().getBoundingRectangle(); // renderer.rect(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } if (polygonalNavGraph != null) { renderer.setColor(WALKZONE_COLOR); renderer.polygon(polygonalNavGraph.getWalkZone().getTransformedVertices()); // DRAW LINEs OF SIGHT renderer.setColor(Color.WHITE); ArrayList<NavNodePolygonal> nodes = polygonalNavGraph.getGraphNodes(); for (NavNodePolygonal n : nodes) { for (NavNodePolygonal n2 : n.neighbors) { renderer.line(n.x, n.y, n2.x, n2.y); } } } renderer.end(); } public BaseActor getActor(String id, boolean searchInventory) { if (VAR_PLAYER.equals(id)) return actors.get(player); BaseActor a = actors.get(id); if (a == null && searchInventory) { a = World.getInstance().getInventory().getItem(id); } return a; } public Map<String, BaseActor> getActors() { return actors; } public void addActor(BaseActor actor) { actors.put(actor.getId(), actor); actor.setScene(this); if (actor instanceof InteractiveActor) { InteractiveActor ia = (InteractiveActor) actor; SceneLayer layer = getLayer(ia.getLayer()); if (layer == null) { // fallback for compatibility layer = new SceneLayer(); layer.setName(ia.getLayer()); layers.add(layer); } layer.add(ia); } } public void setBackground(String bgAtlas, String bgId, String lightMapAtlas, String lightMapId) { this.backgroundAtlas = bgAtlas; this.backgroundRegionId = bgId; } /** * Returns the Interactive actor at the position. The actor must have the * interaction property enabled. */ public InteractiveActor getInteractiveActorAt(float x, float y) { for (SceneLayer layer : layers) { if (!layer.isVisible()) continue; // Obtain actors in reverse (close to camera) for (int i = layer.getActors().size() - 1; i >= 0; i--) { BaseActor a = layer.getActors().get(i); if (a instanceof InteractiveActor && ((InteractiveActor) a).canInteract() && a.hit(x, y)) { return (InteractiveActor) a; } } } return null; } private Rectangle tmpToleranceRect = new Rectangle(); /** * Obtains the actor at (x,y) with TOLERANCE. * * Creates a square with size = TOLERANCE and checks: * * 1. if some vertex from the TOLERANCE square is inside an actor bbox 2. if * some actor bbox vertex is inside the TOLERANCE square */ public InteractiveActor getInteractiveActorAt(float x, float y, float tolerance) { if (tolerance <= 0) { return getInteractiveActorAt(x, y); } List<SceneLayer> layers = getLayers(); tmpToleranceRect.x = x - tolerance / 2; tmpToleranceRect.y = y - tolerance / 2; tmpToleranceRect.width = tolerance; tmpToleranceRect.height = tolerance; for (SceneLayer layer : layers) { if (!layer.isVisible()) continue; // Obtain actors in reverse (close to camera) for (int l = layer.getActors().size() - 1; l >= 0; l--) { BaseActor a = layer.getActors().get(l); if (a instanceof InteractiveActor && ((InteractiveActor) a).canInteract()) { if (a.hit(x, y) || a.hit(tmpToleranceRect.x, tmpToleranceRect.y) || a.hit(tmpToleranceRect.x + tmpToleranceRect.width, tmpToleranceRect.y) || a.hit(tmpToleranceRect.x, tmpToleranceRect.y + tmpToleranceRect.height) || a.hit(tmpToleranceRect.x + tmpToleranceRect.width, tmpToleranceRect.y + tmpToleranceRect.height)) return (InteractiveActor) a; float[] verts = a.getBBox().getTransformedVertices(); for (int i = 0; i < verts.length; i += 2) { float vx = verts[i]; float vy = verts[i + 1]; if (tmpToleranceRect.contains(vx, vy)) return (InteractiveActor) a; } } } } return null; } /** * Returns the actor at the position. Include not interactive actors. */ public BaseActor getActorAt(float x, float y) { // 1. Search for ANCHOR Actors for (BaseActor a : actors.values()) { if (a instanceof AnchorActor) { float dst = Vector2.dst(x, y, a.getX(), a.getY()); if (dst < ANCHOR_RADIUS) return a; } } // 2. Search for INTERACTIVE Actors for (SceneLayer layer : layers) { if (!layer.isVisible()) continue; // Obtain actors in reverse (close to camera) for (int i = layer.getActors().size() - 1; i >= 0; i--) { BaseActor a = layer.getActors().get(i); if (a.hit(x, y)) { return a; } } } // 3. Search for OBSTACLE actors for (BaseActor a : actors.values()) { if (a instanceof ObstacleActor && a.hit(x, y)) { return a; } } return null; } public void setPlayer(CharacterActor a) { if (a != null) { player = a.getId(); a.setInteraction(false); } else { player = null; } } public CharacterActor getPlayer() { return (CharacterActor) actors.get(player); } public Vector2 getDepthVector() { return depthVector; } public void setDepthVector(Vector2 v) { depthVector = v; } public String getBackgroundAtlas() { return backgroundAtlas; } public void setBackgroundAtlas(String backgroundAtlas) { this.backgroundAtlas = backgroundAtlas; } public String getBackgroundRegionId() { return backgroundRegionId; } public void setBackgroundRegionId(String backgroundRegionId) { this.backgroundRegionId = backgroundRegionId; } public void removeActor(BaseActor a) { if (player != null && a.getId().equals(player)) { player = null; } BaseActor r = actors.remove(a.getId()); if (r == null) { EngineLogger.error("Removing actor from scene: Actor not found"); return; } if (a instanceof InteractiveActor) { InteractiveActor ia = (InteractiveActor) a; SceneLayer layer = getLayer(ia.getLayer()); layer.getActors().remove(ia); } if (a instanceof ObstacleActor && polygonalNavGraph != null) polygonalNavGraph.removeDinamicObstacle(a.getBBox()); a.setScene(null); } public Array<AtlasRegion> getBackground() { return background; } public SceneCamera getCamera() { return camera; } public void resetCamera(float worldWidth, float worldHeight) { camera.create(worldWidth, worldHeight); if (getPlayer() != null) setCameraFollowActor(getPlayer()); } public void setCameraFollowActor(SpriteActor a) { followActor = a; if (a != null) camera.updatePos(a); } public SpriteActor getCameraFollowActor() { return followActor; } @Override public void loadAssets() { if (backgroundAtlas != null && !backgroundAtlas.isEmpty()) { EngineAssetManager.getInstance().loadAtlas(backgroundAtlas); } for (BaseActor a : actors.values()) { if (a instanceof AssetConsumer) ((AssetConsumer) a).loadAssets(); } // CALC WALK GRAPH if (polygonalNavGraph != null) { polygonalNavGraph.createInitialGraph(actors.values()); } } @Override public void retrieveAssets() { // RETRIEVE BACKGROUND if (backgroundAtlas != null && !backgroundAtlas.isEmpty()) { background = EngineAssetManager.getInstance().getRegions(backgroundAtlas, backgroundRegionId); int width = 0; for (int i = 0; i < background.size; i++) { width += background.get(i).getRegionWidth(); } int height = background.get(0).getRegionHeight(); // Sets the scrolling dimensions. It must be done here because // the background must be loaded to calculate the bbox if (sceneSize == null) camera.setScrollingDimensions(width, height); // if(followActor != null) // camera.updatePos(followActor); } if (sceneSize != null) camera.setScrollingDimensions(sceneSize.x, sceneSize.y); // RETRIEVE ACTORS for (BaseActor a : actors.values()) { if (a instanceof AssetConsumer) ((AssetConsumer) a).retrieveAssets(); } } @Override public void dispose() { if (backgroundAtlas != null && !backgroundAtlas.isEmpty()) { EngineAssetManager.getInstance().disposeAtlas(backgroundAtlas); } // orderedActors.clear(); for (BaseActor a : actors.values()) { if (a instanceof AssetConsumer) ((AssetConsumer) a).dispose(); } } public Vector2 getSceneSize() { return sceneSize; } public void setSceneSize(Vector2 sceneSize) { this.sceneSize = sceneSize; } public void orderLayersByZIndex() { for (SceneLayer l : layers) { l.orderByZIndex(); } } public PolygonalNavGraph getPolygonalNavGraph() { return polygonalNavGraph; } public void setPolygonalNavGraph(PolygonalNavGraph polygonalNavGraph) { this.polygonalNavGraph = polygonalNavGraph; } @Override public void write(Json json) { if (SerializationHelper.getInstance().getMode() == Mode.MODEL) { json.writeValue("id", id); json.writeValue("layers", layers, layers.getClass(), SceneLayer.class); json.writeValue("actors", actors); if (backgroundAtlas != null) { json.writeValue("backgroundAtlas", backgroundAtlas); json.writeValue("backgroundRegionId", backgroundRegionId); } json.writeValue("musicDesc", musicDesc); if (depthVector != null) json.writeValue("depthVector", depthVector); if (polygonalNavGraph != null) json.writeValue("polygonalNavGraph", polygonalNavGraph); if (sceneSize != null) json.writeValue("sceneSize", sceneSize); } else { SceneActorRef actorRef; json.writeObjectStart("actors"); for (BaseActor a : actors.values()) { actorRef = new SceneActorRef(a.getInitScene(), a.getId()); json.writeValue(actorRef.toString(), a); } json.writeObjectEnd(); json.writeValue("camera", camera); if (followActor != null) json.writeValue("followActor", followActor.getId()); } verbs.write(json); if (state != null) json.writeValue("state", state); if (player != null) json.writeValue("player", player); } @SuppressWarnings("unchecked") @Override public void read(Json json, JsonValue jsonData) { if (SerializationHelper.getInstance().getMode() == Mode.MODEL) { id = json.readValue("id", String.class, jsonData); layers = json.readValue("layers", ArrayList.class, SceneLayer.class, jsonData); actors = json.readValue("actors", HashMap.class, BaseActor.class, jsonData); for (BaseActor actor : actors.values()) { actor.setScene(this); actor.setInitScene(id); if (actor instanceof InteractiveActor) { InteractiveActor ia = (InteractiveActor) actor; SceneLayer layer = getLayer(ia.getLayer()); layer.add(ia); } } orderLayersByZIndex(); backgroundAtlas = json.readValue("backgroundAtlas", String.class, jsonData); backgroundRegionId = json.readValue("backgroundRegionId", String.class, jsonData); musicDesc = json.readValue("musicDesc", MusicDesc.class, jsonData); depthVector = json.readValue("depthVector", Vector2.class, jsonData); polygonalNavGraph = json.readValue("polygonalNavGraph", PolygonalNavGraph.class, jsonData); sceneSize = json.readValue("sceneSize", Vector2.class, jsonData); } else { JsonValue jsonValueActors = jsonData.get("actors"); SceneActorRef actorRef; // GET ACTORS FROM HIS INIT SCENE AND MOVE IT TO THE LOADING SCENE. for (int i = 0; i < jsonValueActors.size; i++) { JsonValue jsonValueAct = jsonValueActors.get(i); actorRef = new SceneActorRef(jsonValueAct.name); Scene sourceScn = World.getInstance().getScene(actorRef.getSceneId()); if (sourceScn != this) { BaseActor actor = sourceScn.getActor(actorRef.getActorId(), false); sourceScn.removeActor(actor); addActor(actor); } } // READ ACTOR STATE. // The state must be retrieved after getting actors from his init // scene to restore verb cb properly. for (int i = 0; i < jsonValueActors.size; i++) { JsonValue jsonValueAct = jsonValueActors.get(i); actorRef = new SceneActorRef(jsonValueAct.name); BaseActor actor = getActor(actorRef.getActorId(), false); if (actor != null) actor.read(json, jsonValueAct); else EngineLogger.debug("Actor not found: " + actorRef); } orderLayersByZIndex(); camera = json.readValue("camera", SceneCamera.class, jsonData); String followActorId = json.readValue("followActor", String.class, jsonData); setCameraFollowActor((SpriteActor) actors.get(followActorId)); } verbs.read(json, jsonData); state = json.readValue("state", String.class, jsonData); player = json.readValue("player", String.class, jsonData); } }