Java tutorial
/******************************************************************************* * Copyright 2014 Maciej 'dagon' Szewczyk www.dagondev.com * * 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.dagondev.drop; import box2dLight.PointLight; import box2dLight.RayHandler; import com.badlogic.gdx.*; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.FPSLogger; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.*; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Timer; import com.gushikustudios.rube.RubeScene; import com.gushikustudios.rube.loader.RubeSceneLoader; import java.util.ArrayList; import java.util.Iterator; import java.util.Random; public class GameScreen implements Screen, InputProcessor, ContactListener { DropGame game; OrthographicCamera camera; World world; Box2DDebugRenderer debugRenderer; RayHandler rayHandler; FPSLogger fpsLogger; Array<Body> dynamicBoxes; ArrayList<PointLight> deathLights; Body player1Body; Body player2Body; Body sunBody; PointLight player1Light; PointLight player2Light; PointLight sunLight; Color currentSunColor; boolean player1Died = false; boolean player2Died = false; boolean drawSun = true; boolean endOfRound = false; boolean backToMenu = false; int baseWidth = 960; int baseHeight = 540; int checkLightCounter = 0; float slowMotionMultiplier = 1; float sundirection = -90f; Vector2 arenaStart; Vector2 arenaEnd; //not using those, but in every project relation of meters to pixels shoudn`t be 1:1, to make it work with debugrenderer look here: https://github.com/libgdx/libgdx/wiki/Viewports //static final float PIXELS_TO_METERS = 0.01f; //static final float METER_TO_PIXELS = 100f; static final float MOVE_SPEED = 19f; static final float ADD_TO_UP_MOVE_SPEED = 19f; static final float SLOW_MOTION_START = 3; static final float MIN_DEATH_LIGHT_RADIUS = 15; static final float MAX_DEATH_LIGHT_RADIUS = 120; static final float STARTING_Y_FOR_OBJECTS = 20; static final float MIN_FORCE_TO_RUN_COLLISION_TEST = 0.085f; static final float PLAYER_RADIUS = 1; static final int DEATH_HEIGHT = -32; static final int CHECK_LIGHTS_EVERY_TICKS = 15; static final int MAX_RAYS_IN_LIGHTS_LOW = 128; static final int RESPAWN_DYNAMIC_BOXES_EVERY_SECONDS = 25; static final String SCENE_NAME = "walker.json"; static final String ARENA_CAMERA_POSITION_NAME = "cameraPosition"; static final String DYNAMIC_BOXES_NAME = "dynamicCircle"; static final String SUN_NAME = "sunBody"; public static final Color COLOR_PLAYER1 = Color.RED; public static final Color COLOR_PLAYER2 = Color.BLUE; public static final Color COLOR_SUN = Color.ORANGE; static final Random random = new Random(); /** * Related to type of body from physics {@link com.badlogic.gdx.physics.box2d.Body} objects. * Creation: * {@code BODYTYPE bodyType = BODYTYPE.getBODYTYPE((Integer)body.getUserData());} */ enum BODYTYPE { DYNAMIC_BOX(0), PLAYER1(1), PLAYER2(2), SUN(3), DUNNO(4); private int value; private BODYTYPE(int value) { this.value = value; } public int getValue() { return value; }; public static boolean isPlayer(BODYTYPE bodytype) { return (PLAYER1.value == bodytype.value || PLAYER2.value == bodytype.value); } public static BODYTYPE getBODYTYPE(int value) { return value > 3 ? DUNNO : values()[value]; } } public GameScreen(DropGame game, int baseWidth, int baseHeight) { this.baseWidth = baseWidth; this.baseHeight = baseHeight; this.game = game; create(); } /** * Handles creation of all of game data (that can be changed in runtime) and loading/creating objects in world */ public void create() { Gdx.app.setLogLevel(Application.LOG_DEBUG); Gdx.input.setInputProcessor(this); debugRenderer = new Box2DDebugRenderer(true, false, false, true, false, false); fpsLogger = new FPSLogger(); createGameSettings(); createCamera(); createBodies(); createScheduledTasks(); } @Override public void render(float delta) { //even if game is locked at 60 fps everything should be using delta time because there can be case of too few fps TODO: google if there is way in libgdx that can manage that. preRenderUpdate(Gdx.graphics.getDeltaTime()); //clear Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); //Box2d World Renderer - to make it work with pixel to meter conversion, look here: https://github.com/libgdx/libgdx/wiki/Viewports debugRenderer.render(world, camera.combined); //lights rednerer //TODO: make rayHandler update at same speed as physics world with slowMotion - make use delta in some way rayHandler.setCombinedMatrix(camera.combined, camera.position.x, camera.position.y, camera.viewportWidth * camera.zoom, camera.viewportHeight * camera.zoom); rayHandler.updateAndRender(); //text on screen game.batch.begin(); game.font.draw(game.batch, String.valueOf(Gdx.graphics.getFramesPerSecond()), 25, 25); //font.draw(batch,"Slow motion: x"+slowMotionMultiplier, baseWidth /2,25); game.batch.end(); //if there player won, render text if (endOfRound) { String text; Color color; Color lastColor = game.font.getColor(); if (player1Died) { text = "Blue won!"; color = COLOR_PLAYER2; } else { text = "Red won!"; color = COLOR_PLAYER1; } game.batch.begin(); game.font.draw(game.batch, "Press Enter to restart match, or Escape to back to main screen.", baseWidth / 2, baseHeight - 25); game.font.setColor(color); game.font.draw(game.batch, text, baseWidth / 2, 25); game.batch.end(); game.font.setColor(lastColor); } postRenderUpdate(delta); } /** * Handles logic that should be handled before any rendering stuff happens * @param delta */ private void preRenderUpdate(float delta) { Vector2 pos1 = null; Vector2 pos2 = null; if (!player1Died) pos1 = player1Body.getPosition(); if (!player2Died) pos2 = player2Body.getPosition(); handleSlowMotion(pos1, pos2); world.step(delta / slowMotionMultiplier, 6, 2); handleDeath(pos1, pos2); handleInput(); handleCamera(); handleLights(delta); } /** * Handles logic that should be handled after any rendering stuff happens. * In this scenario that means after any other game logic. * @param delta */ private void postRenderUpdate(float delta) { //this must be on the end of game loop because dispose can only be called when nothing is in use. if (!backToMenu) return; game.setScreen(new MainMenuScreen(game, baseWidth, baseHeight)); dispose(); } @Override public void resize(int width, int height) { } @Override public void show() { } @Override public void hide() { } @Override public void pause() { } @Override public void resume() { } /** * Handles disposing all of game objects. Can be called only after all logic processed. */ private void disposeGameObjects() { rayHandler.dispose(); //this will handle lights too world.dispose(); //disposing the world is enough Timer.instance().clear(); } @Override public void dispose() { disposeGameObjects(); } /** * Call this for safe way to reset game. Only in {@link #postRenderUpdate(float)} because of {@link #disposeGameObjects()} */ private void resetGame() { disposeGameObjects(); create(); } /** * Resetting all game values to default */ private void createGameSettings() { //for reset mostly backToMenu = false; endOfRound = false; player1Died = false; player2Died = false; //drawSun = false; slowMotionMultiplier = 1; sundirection = -90f; currentSunColor = Color.ORANGE; checkLightCounter = 0; } private void createCamera() { camera = new OrthographicCamera(); camera.setToOrtho(false, baseWidth, baseHeight); } /** * Loads data from *.json scene file and get {@link #world} object from scene. * Create all bodies except player related. */ private void createBodies() { RubeSceneLoader loader = new RubeSceneLoader(); RubeScene scene = loader.addScene(Gdx.files.internal(SCENE_NAME)); world = scene.getWorld(); world.setContactListener(this); //get arena boundaries //TODO: find nice way to get height/width of rectangles without getting into vertices arenaStart = new Vector2(-35, 27); arenaEnd = new Vector2(36, -32); createLights(scene); dynamicBoxes = scene.getNamed(Body.class, DYNAMIC_BOXES_NAME); for (int i = 1; i < 3; i++) { //not sure why i use i=1 instead 0 but whatever createPlayer(i); } //get cameraPosition from body and remove Body cameraPosBody = scene.getNamed(Body.class, ARENA_CAMERA_POSITION_NAME).get(0); Vector2 cameraPos = cameraPosBody.getPosition(); camera.translate(cameraPos.x - baseWidth / 2, cameraPos.y - baseHeight / 2); camera.zoom = 0.125f; world.destroyBody(cameraPosBody); //when scene is not needed anymore scene.clear(); } /** * Handles creation of player object. That means physic body and lights. * Should be called only after existing one (if there is any) was destroyed with {@link #playerDie(int)} . * @param i Number of player, Can be 1 or 2. Not sure why didn`t choose 0-1 */ private void createPlayer(int i) { BodyDef bodyDef = new BodyDef(); bodyDef.type = BodyDef.BodyType.DynamicBody; bodyDef.position.set(new Vector2(getStartXInArena(), STARTING_Y_FOR_OBJECTS)); Body body = world.createBody(bodyDef); PolygonShape shape = new PolygonShape(); shape.setAsBox(PLAYER_RADIUS, PLAYER_RADIUS); FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = shape; fixtureDef.density = 1.0f; fixtureDef.friction = 0.3f; body.createFixture(fixtureDef); Color color = COLOR_PLAYER1; if (i > 1) { color = COLOR_PLAYER2; player2Body = body; } else player1Body = body; //TODO: don`t create new light where there is existing one - https://github.com/libgdx/box2dlights/wiki/Performance-Tuning - PointLight pointLight1 = new PointLight(rayHandler, (int) (MAX_RAYS_IN_LIGHTS_LOW / 1.5), color, 25, 0, 0); pointLight1.attachToBody(body, 0, 0); if (i > 1) player2Light = pointLight1; else player1Light = pointLight1; body.setUserData(i); } /** * Handles creation of lights. Except those related to player. Should be called in method that loads scene with {@link com.gushikustudios.rube.loader.RubeSceneLoader} * @param scene Loaded scene from .json file */ private void createLights(RubeScene scene) { deathLights = new ArrayList<PointLight>(); RayHandler.setGammaCorrection(true); //RayHandler.useDiffuseLight(true); //strange but didn`t see any performance boost with small FBO... rayHandler = new RayHandler(world, baseWidth / 3, baseHeight / 3); //rayHandler.setCulling(true); //indeed culling isn`t for free and takes good amount of fps. better to just track lights that are not in the camera rayHandler.setCombinedMatrix(camera.combined); //1023 is max for rays sunLight = new PointLight(rayHandler, MAX_RAYS_IN_LIGHTS_LOW, COLOR_SUN, 50, 200, 100); sunLight.setActive(drawSun); // sunLight.setXray(false); Body sun = scene.getNamed(Body.class, SUN_NAME).get(0); sunLight.attachToBody(sun, 0, 0); sun.setUserData(BODYTYPE.SUN.getValue()); sunBody = sun; rayHandler.setBlur(true); rayHandler.setBlurNum(2); rayHandler.setShadows(false); //not really lack of shadows, just better clarity for player } /** * Handles player death. Can be only called in {@link #postRenderUpdate(float)} because of destroying physical body * @param i which player. can be 1 or 2 */ private void playerDie(int i) { Vector2 pos; switch (i) { case 1: pos = player1Body.getPosition(); player1Light.remove(); player1Light = null; world.destroyBody(player1Body); player1Body = null; // player1Died=false; sunLight.setColor(COLOR_PLAYER2); break; case 2: pos = player2Body.getPosition(); player2Light.remove(); player2Light = null; world.destroyBody(player2Body); player2Body = null; // player2Died=false; sunLight.setColor(COLOR_PLAYER1); break; default: return; } endOfRound = true; PointLight pointLight = new PointLight(rayHandler, 60, Color.WHITE, MIN_DEATH_LIGHT_RADIUS, pos.x, pos.y); deathLights.add(pointLight); pointLight.setStaticLight(true); //createPlayer(i); } /** * Handles logic of setting {@link #slowMotionMultiplier} used by {@link #world} in update method. * Which makes world go in slow motion or not based on player positions. * @param pos1 Player 1 body position * @param pos2 Player 2 body position */ private void handleSlowMotion(Vector2 pos1, Vector2 pos2) { Vector2 middle = getMiddleOfPlayers(pos1, pos2); //slow motion if (endOfRound) slowMotionMultiplier = 10; else if (Math.abs(middle.x) <= SLOW_MOTION_START && Math.abs(middle.y) <= SLOW_MOTION_START) slowMotionMultiplier = 2; else slowMotionMultiplier = 1; } /** * Handles checking if player died by falling of arena. And calling {@link #playerDie(int)} if needed. * Because of that it should be called in {@link #postRenderUpdate(float)} * @param pos1 Player 1 body position * @param pos2 Player 2 body position */ private void handleDeath(Vector2 pos1, Vector2 pos2) { if (pos1 == null || pos2 == null) return; //check if someone was pushed if (pos1.y < DEATH_HEIGHT) player1Died = true; if (pos2.y < DEATH_HEIGHT) player2Died = true; //handle death if (player1Died) playerDie(1); if (player2Died) playerDie(2); } private void handleCamera() { camera.update(); } /** * Handles light created by player death. * Iterate * @param delta */ private void handleLights(float delta) { Iterator<PointLight> it = deathLights.iterator(); while (it.hasNext()) { PointLight pointLight = it.next(); float distance = pointLight.getDistance(); if (distance < MAX_DEATH_LIGHT_RADIUS) pointLight.setDistance(distance + 70 * delta); else { pointLight.remove(); it.remove(); } } } /** * * @param pos1 Player 1 body position * @param pos2 Player 2 body position * @return Delta between two player positions */ private Vector2 getMiddleOfPlayers(Vector2 pos1, Vector2 pos2) { if (pos1 == null || pos2 == null) return null; return pos2.cpy().sub(pos1); } /** * Handles most of the input, by polling specific keys and contains logic that is releated to those keys. * Should be called every tick. */ private void handleInput() { if (endOfRound) return; if (Gdx.input.isKeyPressed(Input.Keys.PAGE_DOWN)) { camera.zoom += 0.01; } if (Gdx.input.isKeyPressed(Input.Keys.PAGE_UP)) { camera.zoom -= 0.01; } if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) { player2Body.applyForceToCenter(new Vector2(-MOVE_SPEED, 0), true); } if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) { player2Body.applyForceToCenter(new Vector2(MOVE_SPEED, 0), true); } if (Gdx.input.isKeyPressed(Input.Keys.UP)) { player2Body.applyForceToCenter(new Vector2(0, MOVE_SPEED + ADD_TO_UP_MOVE_SPEED), true); } if (Gdx.input.isKeyPressed(Input.Keys.W)) { player1Body.applyForceToCenter(new Vector2(0, MOVE_SPEED + ADD_TO_UP_MOVE_SPEED), true); } if (Gdx.input.isKeyPressed(Input.Keys.A)) { player1Body.applyForceToCenter(new Vector2(-MOVE_SPEED, 0), true); } if (Gdx.input.isKeyPressed(Input.Keys.D)) { player1Body.applyForceToCenter(new Vector2(MOVE_SPEED, 0), true); } } @Override public boolean keyDown(int keycode) { return false; } /** * Handles event of releasing key. * @param keycode * @return */ @Override public boolean keyUp(int keycode) { if (keycode == Input.Keys.ESCAPE) backToMenu = true; else if (keycode == Input.Keys.ENTER) resetGame(); return false; } @Override public boolean keyTyped(char character) { return false; } @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { return false; } @Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { return false; } @Override public boolean touchDragged(int screenX, int screenY, int pointer) { return false; } @Override public boolean mouseMoved(int screenX, int screenY) { return false; } @Override public boolean scrolled(int amount) { return false; } /** * Event handler for physics that handles logic of rule `Hit the ring on top of the arena` * @param contact */ @Override public void beginContact(Contact contact) { if (endOfRound) return; //checking here if playerBox collide with Sun so i can handle mechanic of changing lights and adding point to score Body bodyA = contact.getFixtureA().getBody(); Body bodyB = contact.getFixtureB().getBody(); Object userDataA = bodyA.getUserData(); Object userDataB = bodyB.getUserData(); //only interested in collsion between one player and sun if (bodyA.getType() != BodyDef.BodyType.DynamicBody || bodyB.getType() != BodyDef.BodyType.DynamicBody || userDataA == null || userDataB == null) return; BODYTYPE bodytypeA = BODYTYPE.getBODYTYPE((Integer) userDataA); BODYTYPE bodytypeB = BODYTYPE.getBODYTYPE((Integer) userDataB); if (bodytypeA != BODYTYPE.SUN && bodytypeB != BODYTYPE.SUN) //if one of two bodies is sun that`s is enough return; Color newColor = COLOR_SUN; if (bodytypeA == BODYTYPE.PLAYER1 || bodytypeB == BODYTYPE.PLAYER1) { newColor = COLOR_PLAYER1; player2Died = true; } if (bodytypeA == BODYTYPE.PLAYER2 || bodytypeB == BODYTYPE.PLAYER2) { newColor = COLOR_PLAYER2; player1Died = true; } sunLight.setColor(newColor); } @Override public void endContact(Contact contact) { // Gdx.app.log("forcecollision","A="+contact.getChildIndexA()+"; B="+contact.getChildIndexB()); } @Override public void preSolve(Contact contact, Manifold oldManifold) { // Gdx.app.log("forcecollision","A="+contact.getChildIndexA()+"; B="+contact.getChildIndexB()); } /** * Event handler for physics that handles logic of rule `Drop an object on top of the enemy` * @param contact * @param impulse */ @Override public void postSolve(Contact contact, ContactImpulse impulse) { //death from above if (endOfRound) return; //checking here if playerBox was collided on top side so i can handle mechanic of getting killed by something from above float[] forces = impulse.getNormalImpulses(); Body bodyA = contact.getFixtureA().getBody(); Body bodyB = contact.getFixtureB().getBody(); WorldManifold worldManifold = contact.getWorldManifold(); Vector2 worldNormal = worldManifold.getNormal(); Object userDataA = bodyA.getUserData(); Object userDataB = bodyB.getUserData(); //only interested in collision between one player and dynamic box (can be another player) that happen above certain force if (bodyA.getType() != BodyDef.BodyType.DynamicBody || bodyB.getType() != BodyDef.BodyType.DynamicBody || (userDataA == null && userDataB == null)) return; if (forces[0] < MIN_FORCE_TO_RUN_COLLISION_TEST && forces[1] < MIN_FORCE_TO_RUN_COLLISION_TEST) return; BODYTYPE playerTypeA = BODYTYPE.DYNAMIC_BOX; BODYTYPE playerTypeB = BODYTYPE.DYNAMIC_BOX; if (userDataA != null) playerTypeA = BODYTYPE.getBODYTYPE((Integer) userDataA); if (userDataB != null) playerTypeB = BODYTYPE.getBODYTYPE((Integer) userDataB); //http://stackoverflow.com/questions/7459208/distinguish-between-collision-surface-orientations-in-box2d //where is bigger force that means that body was hitted BODYTYPE checkType = BODYTYPE.DUNNO; if (worldNormal.y < -0.74 && forces[1] > forces[0]) { //looking if bodyB was hitted on top side //Gdx.app.debug("Collision from above", "type=1"); checkType = playerTypeB; } else if (worldNormal.y > 0.74 && forces[0] > forces[1]) { //looking if bodAB was hitted on top side //Gdx.app.debug("Collision from above","type=2"); checkType = playerTypeA; } switch (checkType) { case PLAYER1: player1Died = true; break; case PLAYER2: player2Died = true; break; default: return; } } /** * Returns random x that is in bounds of arena and isn`t in position of ring * @return */ private float getStartXInArena() { float x; do { //making sure that nobody starts at sun position x = random.nextInt((int) (arenaEnd.x + -arenaStart.x - 2)) + random.nextFloat() + arenaStart.x; //making these sheningans because arenastart is at -35 if (x < arenaStart.x) x = arenaStart.x + 0.1f; } while (x >= -4 && x < 3); return x; } /** * Create all scheduled task that should be execute every X seconds. */ //http://stackoverflow.com/questions/21781161/how-can-i-do-something-every-second-libgdx private void createScheduledTasks() { Timer.schedule(new Timer.Task() { @Override public void run() { for (Body body : dynamicBoxes) { if (!body.isAwake() || body.getPosition().y < DEATH_HEIGHT) { body.setTransform(new Vector2(getStartXInArena(), arenaStart.y - 1), 0); body.setAwake(true); } } } }, 0, RESPAWN_DYNAMIC_BOXES_EVERY_SECONDS); } }