com.dagondev.drop.GameScreen.java Source code

Java tutorial

Introduction

Here is the source code for com.dagondev.drop.GameScreen.java

Source

/*******************************************************************************
 * 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);

    }
}