package com.arretadogames.pilot.entities; import android.graphics.Color; import com.arretadogames.pilot.R; import com.arretadogames.pilot.config.GameSettings; import com.arretadogames.pilot.entities.effects.EffectDescriptor; import com.arretadogames.pilot.entities.effects.EffectManager; import com.arretadogames.pilot.items.ItemType; import com.arretadogames.pilot.render.AnimationManager; import com.arretadogames.pilot.render.AnimationSwitcher; import com.arretadogames.pilot.render.PhysicsRect; import com.arretadogames.pilot.render.Renderable; import com.arretadogames.pilot.render.opengl.GLCanvas; import com.arretadogames.pilot.util.Util; import org.jbox2d.collision.Manifold; import org.jbox2d.collision.shapes.PolygonShape; import org.jbox2d.collision.shapes.Shape; import org.jbox2d.collision.shapes.ShapeType; import org.jbox2d.common.Vec2; import org.jbox2d.dynamics.BodyType; import org.jbox2d.dynamics.Fixture; import org.jbox2d.dynamics.contacts.Contact; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class Water extends Entity implements Steppable { private static final int WATER_LAYER_POS = -1; private static final PhysicsRect SEAWEED_SIZE = new PhysicsRect(0.6f, 0.8f); private static final PhysicsRect BORDER_SIZE = new PhysicsRect(0.6f, 0.6f); private static EffectDescriptor splashEffect; public static EffectDescriptor getSplashEffect() { if (splashEffect == null) { splashEffect = new EffectDescriptor(); splashEffect.type = "Splash"; splashEffect.repeat = false; splashEffect.layerPosition = WATER_LAYER_POS + 1; } return splashEffect; } private class Seaweed implements Renderable { public AnimationSwitcher animation; public Vec2 position; @Override public void render(GLCanvas canvas, float timeElapsed) { canvas.saveState(); canvas.translatePhysics(position.x, position.y); animation.render(canvas, SEAWEED_SIZE, timeElapsed); canvas.restoreState(); } } private class Spring { public float springHeight; public float springRelativeX; public float speed; private float deltaFromNaturalheight; public void Update() { deltaFromNaturalheight = springHeight - waterHeight; speed += -SPRING_SWIFTNESS * deltaFromNaturalheight - SPRING_DAMPENING * speed; if (Math.abs(speed) > SPRING_MAXIMUM_SPEED) speed = SPRING_MAXIMUM_SPEED * (speed / Math.abs(speed)); springHeight += speed; } } // Rendering Properties // This is the factor that makes the collided wave goes further down or not private static final float ENTITY_VELOCITY_REDUCTION_FACTOR = 30; // This makes the springs more dense or not private static final float SPRING_SWIFTNESS = 0.01f; // This makes the waves spread more or less.. private static final float WAVES_SPREADNESS = 0.006f; // Maximum distance between springs, smaller value causes more springs to be created private static final float WATER_SPRINGS_MAX_DISTANCE = 0.6f; // Meters // Factor which makes the springs stop (higher value makes them stop faster) private static final float SPRING_DAMPENING = 0.0005f; //0.025f; // Maximum speed that a spring can go.. This avoids then to go a lot higher or lower private static final float SPRING_MAXIMUM_SPEED = 0.035f; // 0.05 - 0.04 // Padding top for the water drawing private static final float WATER_TOP_PADDING = 0.25f; // Meters // Colors private static final int COLOR_ALPHA = 200; private static final int WATER_SURFACE_COLOR = Color.argb(COLOR_ALPHA, 92, 133, 255); private static final int WATER_BOTTOM_COLOR = Color.argb(COLOR_ALPHA, 0, 56, 224); private static final float WATER_SURFACE_LINE_WIDTH = 3; private Spring[] springs; float[] leftDeltas; float[] rightDeltas; // Physics Properties private PolygonShape shapeA; Collection<Entity> entitiesContact; private float waterHeight; private float waterWidth; private float density; private Fixture fixture; private List<Seaweed> seaweeds; public Water(float x, float y, float width, float height, float density) { super(x, y - 0.5f); // TODO: Remove this on newer maps this.waterWidth = width; this.waterHeight = height; this.density = density; shapeA = new PolygonShape(); shapeA.setAsBox(width/2,height/2); fixture = body.createFixture(shapeA,0.0f); body.setType(BodyType.STATIC); entitiesContact = new ArrayList<Entity>(); fixture.setSensor(false); initializeWaterSprings(); initializeSeaweeds(); } private void initializeSeaweeds() { seaweeds = new ArrayList<Seaweed>(); // Algorithm: // We add a random amount to X, and then add a seaweed there // Until we can't add anymore seaweeds (because it is outside the width) float x = SEAWEED_SIZE.width() * Util.random(1f, 2f); float waterStartX = getPosX() - this.waterWidth / 2; while (x < waterWidth - SEAWEED_SIZE.width()) { Seaweed sw = new Seaweed(); sw.animation = AnimationManager.getInstance().getSprite("seaweed"); sw.position = new Vec2(waterStartX + x, getPosY() - waterHeight + SEAWEED_SIZE.height() / 2 + 0.4f); seaweeds.add(sw); x += SEAWEED_SIZE.width() * Util.random(1f, 4f); } } @Override public void preSolve(Entity e, Contact contact, Manifold oldManifold) { contact.setEnabled(false); // Walk above water if (e.getType() == EntityType.PLAYER) { Player p = (Player) e; if (p.getItem() != null && p.getItem().getType() == ItemType.WaterWalk && p.getItem().isActive()) { contact.setEnabled(true); } } } private void initializeWaterSprings() { // Check this stuff int waterDivisions = (int) Math.floor(waterWidth / WATER_SPRINGS_MAX_DISTANCE); float divisionlength = waterWidth / waterDivisions; springs = new Spring[waterDivisions + 1]; float currentX = 0; for (int i = 0 ; i < waterDivisions + 1 ; i++) { springs[i] = new Spring(); springs[i].springRelativeX = currentX; currentX += divisionlength; springs[i].springHeight = this.waterHeight; } leftDeltas = new float[springs.length]; rightDeltas = new float[springs.length]; } /** * Causes the water to create waves and a splash effect in the given x position * @param e */ public void splash(Entity e, Contact contact) { float x = e.getPosX(); float yVel = e.body.m_linearVelocity.y / ENTITY_VELOCITY_REDUCTION_FACTOR; if (x <= getPosX() - waterWidth / 2 || x >= getPosX() + waterWidth / 2) { return; // Out of range } if (yVel < 0) { // Splash Threshold (usually -0.1 or less, closer to 0 ) EffectDescriptor splashEffect = getSplashEffect(); splashEffect.pRect = e.physRect; splashEffect.position = new Vec2(e.getPosX() + 0.2f, // Splash a little in front of the player getPosY() + waterHeight / 2 + e.physRect.height() * 0.5f); // There is a little offset up based on the image (so the base of the water fits the top of the water) EffectManager.getInstance().addEffect(splashEffect); } float waterRelativePosition = x - (getPosX() - waterWidth / 2); int selectedDivision = (int) Math.floor(waterRelativePosition / WATER_SPRINGS_MAX_DISTANCE); if (selectedDivision >= springs.length) selectedDivision = springs.length - 1; springs[selectedDivision].speed = yVel; // speed it up! } @Override public int getLayerPosition() { return WATER_LAYER_POS; } public static List<Vec2> transformToVec2(List<List<Float>> l){ List<Vec2> result = new ArrayList<Vec2>(); for(List<Float> pair : l){ result.add(new Vec2(pair.get(0), pair.get(1))); } return result; } public static Vec2 getCentroid(List<List<Float>> l){ Vec2 centroid = new Vec2(0,0); PolygonShape a = new PolygonShape(); List<Vec2> ele = transformToVec2(l); Vec2 arr[] = new Vec2[ele.size()]; for(int i = 0 ; i < ele.size(); i++){ arr[i] = ele.get(i); } a.computeCentroidToOut(arr, ele.size(), centroid); return centroid; } public static float calcArea(List<List<Float>> l){ if(l.size() < 3) return 0f; float area = 0f; List<Vec2> ele = transformToVec2(l); for(int i = 2; i < ele.size(); i++){ area += Vec2.cross(ele.get(i).sub(ele.get(0)), ele.get(i-1).sub(ele.get(0))); } area = area/2; return Math.abs(area); } public static List<List<Float>> findIntersectionOfPolygon(List<List<Float>> subjectPolygon, List<List<Float>> clipPolygon){ if(subjectPolygon.size() < 3 || clipPolygon.size() < 3) return new ArrayList<List<Float>>(); List<List<Float>> output = new ArrayList<List<Float>>(); for(List<Float> a : subjectPolygon){ output.add(a); } List<Float > cp1 = clipPolygon.get(clipPolygon.size() - 1); for(List<Float> clipVertex : clipPolygon){ List<Float> cp2 = clipVertex; List<List<Float>> inputList = new ArrayList<List<Float>>(); for(List<Float> a : output){ inputList.add(a); } output.clear(); List<Float> s; if( inputList.size() > 0 ){ s = inputList.get(inputList.size() - 1); for(List<Float> subjectVertex : inputList){ List<Float> e = subjectVertex; if(inside(cp1,cp2,e)){ if(! inside(cp1,cp2,s)){ output.add(computeIntersection(cp1,cp2,e,s)); } output.add(e); } else if (inside(cp1,cp2,s)){ output.add(computeIntersection(cp1,cp2,e,s)); } s = e; } } cp1 = cp2; } return output; } private static List<Float> computeIntersection(List<Float> cp1, List<Float> cp2, List<Float> e, List<Float> s) { List<Float> dc = new ArrayList<Float>(); dc.add(cp1.get(0) - cp2.get(0)); dc.add(cp1.get(1) - cp2.get(1)); List<Float> dp = new ArrayList<Float>(); dp.add(s.get(0) - e.get(0)); dp.add(s.get(1) - e.get(1)); Float n1 = Float.valueOf(cp1.get(0) * cp2.get(1) - cp1.get(1) * cp2.get(0)); Float n2 = Float.valueOf(s.get(0) * e.get(1) - s.get(1) * e.get(0)); Float n3 = Float.valueOf(1.0f / (dc.get(0) * dp.get(1) - dc.get(1) * dp.get(0))); List<Float> result = new ArrayList<Float>(); result.add((n1*dp.get(0) - n2*dc.get(0)) * n3); result.add((n1*dp.get(1) - n2*dc.get(1)) * n3); return result; } private static boolean inside(List<Float> cp1, List<Float>cp2, List<Float> p) { return(cp2.get(0)-cp1.get(0))*(p.get(1)-cp1.get(1)) > (cp2.get(1)-cp1.get(1))*(p.get(0)-cp1.get(0)); } @Override public void render(GLCanvas canvas, float timeElapsed) { float bottomY = getPosY() - waterHeight / 2; float initialX = getPosX() - waterWidth / 2; // Draw Seaweeds... for (Seaweed seaweed : seaweeds) { seaweed.render(canvas, timeElapsed); } for (int i = 1 ; i < springs.length ; i++) { canvas.drawRectFromPhysics( initialX + springs[i-1].springRelativeX, bottomY + springs[i-1].springHeight + WATER_TOP_PADDING, initialX + springs[i-1].springRelativeX, bottomY, initialX + springs[i].springRelativeX, bottomY, initialX + springs[i].springRelativeX, bottomY + springs[i].springHeight + WATER_TOP_PADDING, WATER_SURFACE_COLOR, WATER_BOTTOM_COLOR, WATER_BOTTOM_COLOR, WATER_SURFACE_COLOR); } // Draw Surface Lines for (int i = 1 ; i < springs.length ; i++) { canvas.drawLine( GLCanvas.physicsRatio * (initialX + springs[i-1].springRelativeX), GameSettings.TARGET_HEIGHT - GLCanvas.physicsRatio * (bottomY + springs[i-1].springHeight + WATER_TOP_PADDING), GLCanvas.physicsRatio * (initialX + springs[i].springRelativeX), GameSettings.TARGET_HEIGHT - GLCanvas.physicsRatio * (bottomY + springs[i].springHeight + WATER_TOP_PADDING), WATER_SURFACE_LINE_WIDTH, WATER_BOTTOM_COLOR); } canvas.saveState(); canvas.translatePhysics( getPosX() - waterWidth / 2 + BORDER_SIZE.width() / 2 - 0.1f, getPosY() - waterHeight / 2 + BORDER_SIZE.height() / 2 - 0.1f); canvas.drawBitmap(R.drawable.water_border_left, BORDER_SIZE); canvas.restoreState(); canvas.saveState(); canvas.translatePhysics(getPosX() + waterWidth / 2 - BORDER_SIZE.width() / 2 + 0.1f, getPosY() - waterHeight / 2 + BORDER_SIZE.height() / 2 - 0.1f); canvas.drawBitmap(R.drawable.water_border_right, BORDER_SIZE); canvas.restoreState(); } @Override public void step(float timeElapsed) { for( Entity e : entitiesContact){ applyBuoyancy(e); } // Update WaterSprings updateWaterSprings(); } private void updateWaterSprings() { for (Spring s : springs) s.Update(); // do some passes where springs pull on their neighbours for (int j = 0; j < 8; j++) { for (int i = 0; i < springs.length; i++) { if (i > 0) { leftDeltas[i] = WAVES_SPREADNESS * (springs[i].springHeight - springs [i - 1].springHeight); springs[i - 1].speed += leftDeltas[i]; } if (i < springs.length - 1) { rightDeltas[i] = WAVES_SPREADNESS * (springs[i].springHeight - springs [i + 1].springHeight); springs[i + 1].speed += rightDeltas[i]; } } for (int i = 0; i < springs.length; i++) { if (i > 0) springs[i - 1].springHeight += leftDeltas[i]; if (i < springs.length - 1) springs[i + 1].springHeight += rightDeltas[i]; } } } private void applyBuoyancy(Entity caixa) { Shape shape = caixa.getWaterContactShape(); if (shape.getType() == ShapeType.POLYGON) { PolygonShape polygon = (PolygonShape) shape; final Vec2 v[] = polygon.getVertices(); int cont = polygon.getVertexCount(); final Vec2 v2[] = shapeA.getVertices(); int cont2 = shapeA.getVertexCount(); List<List<Float>> a = new ArrayList<List<Float>>(); List<List<Float>> b = new ArrayList<List<Float>>(); for( int i = 0; i < cont; i++){ Vec2 p = caixa.body.getWorldPoint(v[i]); List<Float> ui = new ArrayList<Float>(); ui.add(p.x); ui.add(p.y); a.add(ui); } for( int i = 0; i < cont2; i++){ Vec2 p = body.getWorldPoint(v2[i]); List<Float> ui = new ArrayList<Float>(); ui.add(p.x); ui.add(p.y); b.add(ui); } List<List<Float>> in = findIntersectionOfPolygon(a, b); if( in.size() > 2){ float area = calcArea(in); Vec2 centroid = getCentroid(in); float displacedMass = density * area; Vec2 force = world.getGravity().mul(-displacedMass); caixa.body.applyForce(force, centroid); } List<Vec2> intersectionPoints = transformToVec2(in); for (int i = 0; i < intersectionPoints.size(); i++) { //the end points and mid-point of this edge Vec2 v0 = intersectionPoints.get(i); Vec2 v1 = i+1 < intersectionPoints.size() ? intersectionPoints.get(i+1) : intersectionPoints.get(0); Vec2 midPoint = (v0.add(v1)).mul(0.5f); //find relative velocity between object and fluid at edge midpoint Vec2 velDir = caixa.body.getLinearVelocityFromWorldPoint( midPoint ).sub( body.getLinearVelocityFromWorldPoint( midPoint )); float vel = velDir.normalize(); Vec2 edge = v1.sub(v0); float edgelength = edge.normalize(); Vec2 normal = Vec2.cross(-1,edge); //gets perpendicular vector float dragDot = Vec2.dot(normal, velDir); if ( dragDot < 0 ) continue; //normal points backwards - this is not a leading edge float dragMag = (float) (dragDot * edgelength * density * vel * vel); Vec2 dragForce = velDir.mul(- dragMag); caixa.body.applyForce( dragForce, midPoint ); } } else if (shape.getType() == ShapeType.CIRCLE) { Vec2 midPoint = caixa.body.getWorldCenter(); //find relative velocity between object and fluid at edge midpoint Vec2 velDir = caixa.body.getLinearVelocityFromWorldPoint( midPoint ).sub( body.getLinearVelocityFromWorldPoint( midPoint )).clone(); float vel = velDir.normalize(); float dragMag = (float) (density * vel * vel); Vec2 dragForce = velDir.mul(- dragMag); caixa.body.applyForce( dragForce, midPoint ); } } @Override public EntityType getType() { return EntityType.FLUID; } @Override public void setSprite(AnimationSwitcher sprite) { // No Sprite } @Override public void beginContact(Entity e, Contact contact) { super.beginContact(e, contact); entitiesContact.add(e); e.setOnWater(true); splash(e, contact); } @Override public void endContact(Entity e, Contact contact) { super.endContact(e, contact); e.setOnWater(false); entitiesContact.remove(e); } public float getWidth() { return waterWidth; } public float getHeight() { return waterHeight; } }