package com.arretadogames.pilot.render;
import com.arretadogames.pilot.R;
import com.arretadogames.pilot.config.GameSettings;
import com.arretadogames.pilot.entities.Entity;
import com.arretadogames.pilot.entities.LayerEntity.Layer;
import com.arretadogames.pilot.entities.Player;
import com.arretadogames.pilot.entities.PlayerNumber;
import com.arretadogames.pilot.entities.Steppable;
import com.arretadogames.pilot.entities.Toucan;
import com.arretadogames.pilot.entities.effects.Effect;
import com.arretadogames.pilot.entities.effects.EffectManager;
import com.arretadogames.pilot.physics.PhysicalWorld;
import com.arretadogames.pilot.render.opengl.GLCanvas;
import com.arretadogames.pilot.util.Profiler;
import com.arretadogames.pilot.util.Profiler.ProfileType;
import com.arretadogames.pilot.world.GameWorld;
import org.jbox2d.callbacks.QueryCallback;
import org.jbox2d.collision.AABB;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Fixture;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class GameCamera implements Renderable, Steppable {
private static final float CAMERA_MINIMUM_Y = 0.8f;
private static final float CAMERA_MOVE_UP_THRESHOLD = 2.4f;
private static GameWorld gameWorld = null;
private boolean calculateWidthFirst;
private int currentNumberOfPlayers;
private Vec2 currentLowerBound;
private Vec2 currentUpperBound;
private Vec2 currentTranslator;
private float currentPhysicsRatio;
private boolean transitioning;
private float transitionDuration; // Measured in milliseconds.
private long startTime;
private MovingBackground[] movingBackgrounds;
private enum TransitionTrigger {
NONE, PLAYER_NUM_CHANGED, VIEWPORT_SIDE_PRIORITY_CHANGED;
}
private TransitionTrigger transitionTrigger = TransitionTrigger.NONE;
private Vec2 targetLowerBound;
private Vec2 targetUpperBound;
private Vec2 targetTranslator;
private float targetPhysicsRatio;
private Vec2 lowerBound, upperBound, translator;
private float initialX = -1; // Initial X position from players
private float flagX = -1;
private Toucan toucan;
public GameCamera(GameWorld world) {
this(world, 1000f);// Default is 1000 milliseconds
}
public GameCamera(GameWorld world, float setTransitionDuration) {
gameWorld = world;
calculateWidthFirst = true;
currentNumberOfPlayers = -1;
transitioning = false;
transitionDuration = setTransitionDuration;
currentLowerBound = null;
currentUpperBound = null;
currentTranslator = null;
currentPhysicsRatio = 0;
targetLowerBound = null;
targetUpperBound = null;
targetTranslator = null;
targetPhysicsRatio = 0;
startTime = 0;
toucan = new Toucan();
movingBackgrounds = new MovingBackground[3];
movingBackgrounds[0] = new MovingBackground(R.drawable.mountains_repeatable, 150);
movingBackgrounds[1] = new MovingBackground(R.drawable.repeatable_back_trees, 100);
movingBackgrounds[2] = new MovingBackground(R.drawable.repeatable_trees, 70);
}
private int getNumberOfAlivePlayers(Collection<Player> players) {
int alive = 0;
for (Player p : players)
if (!p.isDead())
alive++;
return alive;
}
// Determine viewport: portion of World that will be visible. Obviously, it
// is measured in meters.
private void determineViewport(GLCanvas gameCanvas, float timeElapsed) {
Profiler.initTick(ProfileType.RENDER);
HashMap<PlayerNumber, Player> players = gameWorld.getPlayers();
int numberOfPlayers = getNumberOfAlivePlayers(players.values());
if (currentNumberOfPlayers == -1) { // First pass
currentNumberOfPlayers = numberOfPlayers;
}
if (numberOfPlayers != currentNumberOfPlayers) { // If someone died..
transitioning = true;
startTime = getCurrentTime();
targetLowerBound = null;
targetUpperBound = null;
targetTranslator = null;
transitionTrigger = TransitionTrigger.PLAYER_NUM_CHANGED;
currentNumberOfPlayers = numberOfPlayers;
}
float viewportWidth, viewportHeight, physicsRatio;
float maxXDistance = 0;
float maxYDistance = 0;
float minX = Float.MAX_VALUE;
Vec2 center = new Vec2();
Player furthestPlayer = null; // Player with the MOST lowest X
Player secondFurthestPlayer = null; // Player second with the lowest X
Iterator<PlayerNumber> iiterator = players.keySet().iterator();
while (iiterator.hasNext()) {
PlayerNumber i = iiterator.next();
if (players.get(i).isDead())
continue;
if (furthestPlayer == null) {
furthestPlayer = players.get(i);
} else {
if (furthestPlayer.getPosX() > players.get(i).getPosX()) {
secondFurthestPlayer = furthestPlayer;
furthestPlayer = players.get(i);
} else if (secondFurthestPlayer == null || secondFurthestPlayer.getPosX() > players.get(i).getPosX()) {
secondFurthestPlayer = players.get(i);
}
}
float x = players.get(i).getPosX();
float y = players.get(i).getPosY();
if (minX > x)
minX = x;
center.addLocal(x, y);
Iterator<PlayerNumber> jiterator = players.keySet().iterator();
while (jiterator.hasNext()) {
PlayerNumber j = jiterator.next();
if (i.equals(j) || players.get(j).isDead()) {
continue;
}
float x2 = players.get(j).getPosX();
float y2 = players.get(j).getPosY();
float currentXDistance = Math.abs(x - x2);
float currentYDistance = Math.abs(y - y2);
if (maxXDistance == 0) {
maxXDistance = currentXDistance;
} else if (maxXDistance < currentXDistance) {
maxXDistance = currentXDistance;
}
if (maxYDistance == 0) {
maxYDistance = currentYDistance;
} else if (maxYDistance < currentYDistance) {
maxYDistance = currentYDistance;
}
}
}
center.mulLocal(1f / numberOfPlayers);
// Makes the camera go a little further
center.addLocal(3, 0);
if (maxYDistance <= maxXDistance * 0.5f) { // Threshold indicating when
// it is good to start
// calculating height first.
// Measured in meters.
viewportWidth = maxXDistance + 10;// + 15;//+ 30;
physicsRatio = GameSettings.TARGET_WIDTH / viewportWidth; // This indicates the ZOOM
viewportHeight = GameSettings.TARGET_HEIGHT / physicsRatio;
if (!transitioning) {
if (!calculateWidthFirst) {
transitioning = true;
startTime = getCurrentTime();
transitionTrigger = TransitionTrigger.VIEWPORT_SIDE_PRIORITY_CHANGED;
}
calculateWidthFirst = true;
}
} else {
viewportHeight = maxYDistance + 6;// + 9;//+ 18;
physicsRatio = GameSettings.TARGET_HEIGHT / viewportHeight;
viewportWidth = GameSettings.TARGET_WIDTH / physicsRatio;
if (!transitioning) {
if (calculateWidthFirst) {
transitioning = true;
startTime = getCurrentTime();
transitionTrigger = TransitionTrigger.VIEWPORT_SIDE_PRIORITY_CHANGED;
}
calculateWidthFirst = false;
}
}
if (center.y < CAMERA_MOVE_UP_THRESHOLD) {
center.y = CAMERA_MINIMUM_Y;
} else {
center.y -= CAMERA_MOVE_UP_THRESHOLD - CAMERA_MINIMUM_Y;
}
lowerBound = new Vec2(center.x - viewportWidth / 2, center.y
- viewportHeight / 2);
upperBound = new Vec2(center.x + viewportWidth / 2, center.y
+ viewportHeight / 2);
translator = new Vec2(-physicsRatio * (center.x - viewportWidth / 2),
physicsRatio * (center.y - viewportHeight / 2));
if (!transitioning) {
currentLowerBound = lowerBound;
currentUpperBound = upperBound;
currentTranslator = translator;
currentPhysicsRatio = physicsRatio;
} else {
targetLowerBound = lowerBound;
targetUpperBound = upperBound;
targetTranslator = translator;
targetPhysicsRatio = physicsRatio;
}
if (currentLowerBound == null) {
currentLowerBound = targetLowerBound;
currentUpperBound = targetUpperBound;
currentTranslator = targetTranslator;
currentPhysicsRatio = targetPhysicsRatio;
} else if (targetLowerBound == null) {
targetLowerBound = currentLowerBound;
targetUpperBound = currentUpperBound;
targetTranslator = currentTranslator;
targetPhysicsRatio = currentPhysicsRatio;
}
if (transitioning) {
float currentTime = getCurrentTime();
float elapsedTime = currentTime - startTime;
float reachedPercentage = elapsedTime / transitionDuration;
if (reachedPercentage >= 1) {
transitionTrigger = TransitionTrigger.NONE;
transitioning = false;
currentLowerBound = new Vec2(targetLowerBound);
lowerBound = currentLowerBound;
currentUpperBound = new Vec2(targetUpperBound);
upperBound = currentUpperBound;
currentTranslator = new Vec2(targetTranslator);
translator = currentTranslator;
currentPhysicsRatio = targetPhysicsRatio;
physicsRatio = currentPhysicsRatio;
targetLowerBound = null;
targetUpperBound = null;
targetTranslator = null;
targetPhysicsRatio = 0;
} else {
// lowerBound = new Vec2(currentLowerBound);
// lowerBound.addLocal(targetLowerBound.sub(currentLowerBound)
// .mul(reachedPercentage));
//
// upperBound = new Vec2(currentUpperBound);
// upperBound.addLocal(targetUpperBound.sub(currentUpperBound)
// .mul(reachedPercentage));
//
// translator = new Vec2(currentTranslator);
// translator.addLocal(targetTranslator.sub(currentTranslator)
// .mul(reachedPercentage));
//
// physicsRatio = currentPhysicsRatio;
// physicsRatio += (targetPhysicsRatio - currentPhysicsRatio)
// * reachedPercentage;
}
}
// Start Toucan if needed
if (maxXDistance > 10 && !secondFurthestPlayer.hasFinished()) {
toucan.activate(lowerBound.x, upperBound.y, furthestPlayer, secondFurthestPlayer, flagX);
}
Profiler.profileFromLastTick(ProfileType.RENDER, "Calculate Viewport");
Profiler.initTick(ProfileType.RENDER);
gameCanvas.setPhysicsRatio(physicsRatio);
for (int i = 0 ; i < movingBackgrounds.length ; i++) {
movingBackgrounds[i].render(gameCanvas, 0, GLCanvas.physicsRatio, center.x,
center.y, initialX, flagX, translator);
}
Profiler.profileFromLastTick(ProfileType.RENDER, "Draw background");
Profiler.initTick(ProfileType.RENDER);
gameCanvas.saveState();
gameCanvas.translate(translator.x, translator.y);
List<Entity> entities = new ArrayList<Entity>();
entities.addAll(getPhysicalEntitiesToBeDrawn(lowerBound, upperBound));
List<Effect> effects = EffectManager.getInstance().getEffects();
// Sort based on layer
Collections.sort(entities, Layer.getComparator());
Collections.sort(effects, Layer.getComparator());
// "Merge" both lists using algorithm based on MergeSort
int i = 0, j = 0;
while (i < entities.size() || j < effects.size()) {
if (i == entities.size()) {
effects.get(j).render(gameCanvas, timeElapsed);
j++;
} else if (j == effects.size()) {
entities.get(i).render(gameCanvas, timeElapsed);
i++;
} else {
if (Layer.getComparator().compare(entities.get(i), effects.get(j)) > 0) {
effects.get(j).render(gameCanvas, timeElapsed);
j++;
} else {
entities.get(i).render(gameCanvas, timeElapsed);
i++;
}
}
}
toucan.render(gameCanvas, timeElapsed);
EffectManager.getInstance().removeInactiveEffects();
Profiler.profileFromLastTick(ProfileType.RENDER, "Draw entities");
Profiler.initTick(ProfileType.RENDER);
if (GameSettings.DRAW_PHYSICS)
PhysicalWorld.getInstance().render(gameCanvas, timeElapsed);
gameCanvas.restoreState();
}
private Set<Entity> getPhysicalEntitiesToBeDrawn(Vec2 lowerBound,
Vec2 upperBound) {
final Set<Entity> entities = new HashSet<Entity>();
PhysicalWorld.getInstance().getWorld().queryAABB(new QueryCallback() { // TODO:
// create
// QueryCallback
// just
// once
@Override
public boolean reportFixture(Fixture fixture) {
fixture.getBody().setAwake(true);
Object e = fixture.getBody().getUserData();
if (e != null) {
Entity entity = (Entity) e;
entities.add(entity);
}
return true;
}
}, new AABB(lowerBound, upperBound)); // TODO: create AABB just
// once
return entities;
}
public void render(final GLCanvas canvas, final float timeElapsed) {
if (flagX == -1 || initialX == -1) {
flagX = gameWorld.getFlagPos();
initialX = gameWorld.getPlayers().get(PlayerNumber.ONE).getPosX();
}
determineViewport(canvas, timeElapsed);
}
public void step(float timeElapsed) {
toucan.step(timeElapsed);
}
private long getCurrentTime() {
return System.nanoTime() / 1000000;
}
public Vec2 convertWorldToPixel(float posX, float posY) {
posX = (posX - lowerBound.x) * GLCanvas.physicsRatio;
posY = GameSettings.TARGET_HEIGHT - (posY - lowerBound.y) * GLCanvas.physicsRatio;
return new Vec2(posX, posY);
}
}