1
\$\begingroup\$

I've asked this question before but i am still unclear of how to do this after hours of frustration I have messed up yet another decent project I've been working on. I had a fully functioning Snake game which I was very happy with but I had the idea that I wanted to put the entire game id created onto the screen inside of an image of something similar to an old GameBoy. (in case im not describing this clearly I have made a quick diagram): https://i.stack.imgur.com/3J1Ol.jpg I've read so much on this matter before but I just can't make it work in my own projects.

No matter what I have done (ie. where i set ScreenX position for my inner viewport) the inner viewport always positions at the very left edge of screen and spans the width of screen - even though I have set the "World Width", I want the inner screen inset inside the controllerViewport so that it looks like you are playing it on that device. (I tried using things other than FitViewport, but i need the inner screen to be relative to the VIRTUAL WIDTH, HEIGHT I created.

I will paste the code now (sorry i have it all in one class on this project as it started from a tutorial i completed - i dont usually write my code like this):

public class GameScreen extends ScreenAdapter {

    private static final float WORLD_WIDTH = 640;
    private static final float WORLD_HEIGHT = 480;

    private static final float TOTAL_HEIGHT = 1920 / 2;
    private static final float TOTAL_WIDTH = 1080 / 2;

    private static final float MOVE_TIME = 0.15f;
    private static final int SNAKE_MOVEMENT = 32;
    private static final int GRID_CELL = 32;

    private static final int RIGHT = 0;
    private static final int LEFT = 1;
    private static final int UP = 2;
    private static final int DOWN = 3;

    private static final String GAME_OVER_TEXT = "Game Over... Punk!!";

    private static final int POINTS_PER_APPLE = 20;

    private SnakeGame game;

    private SpriteBatch batch;
    private Texture snakeHead;
    private Texture snakeBody;
    private Texture apple;

    private boolean appleAvailable = false;
    private int appleX, appleY;

    private float timer = MOVE_TIME;
    private float snakeX = 0, snakeY = 0;
    private float snakeXBeforeUpdate = 0, snakeYBeforeUpdate = 0;

    private int snakeDirection = RIGHT;
    private Array<BodyPart> bodyParts = new Array<BodyPart>();



    private ShapeRenderer shapeRenderer;

    private boolean directionSet = false;
    private boolean hasHit = false;

    private enum STATE { PLAYING, GAME_OVER }

    private STATE state = STATE.PLAYING;

    private BitmapFont bitmapFont;
    private GlyphLayout layout = new GlyphLayout();

    private int score = 0;

    private Viewport viewport1;
    private Camera camera;
    private Viewport controllerViewport;

    private Dpad dpad;
    float dPadWidth;
    private Texture dPadTexture;
    Vector3 touchPos;

    Texture background;
    Texture gameboyTexture;


    public GameScreen(SnakeGame game){
        this.game = game;
        batch = new SpriteBatch();
        snakeHead = new Texture(Gdx.files.internal("snakehead.png"));
        snakeBody = new Texture(Gdx.files.internal("snakebody.png"));
        apple = new Texture(Gdx.files.internal("apple.png"));
        background = new Texture(Gdx.files.internal("backgroundTile.png"));
        dPadTexture = new Texture(Gdx.files.internal("dpad.png"));
        gameboyTexture = new Texture("gameboy.png");
        shapeRenderer = new ShapeRenderer();
        bitmapFont = new BitmapFont();
        bitmapFont.getData().setScale(2f,2f); // set font size here
        camera = new OrthographicCamera();
        camera.position.set(TOTAL_WIDTH / 2, TOTAL_HEIGHT / 2, 0);
        camera.update();
        controllerViewport = new FillViewport(TOTAL_WIDTH, TOTAL_HEIGHT, camera);
        viewport1 = new FitViewport(WORLD_WIDTH, WORLD_HEIGHT);
        viewport1.setScreenBounds((int)(controllerViewport.getWorldWidth() * 0.25f), (int)(controllerViewport.getWorldHeight() - (controllerViewport.getWorldHeight() * 0.1f)),
                (int)(controllerViewport.getWorldWidth() * 0.75f) , (int)(controllerViewport.getWorldHeight() * 0.33f));

        switch (game.dpadSize){
            case 1:
                dPadWidth = viewport1.getWorldWidth() / 4 * 0.75f;
                break;
            case 2:
                dPadWidth = viewport1.getWorldWidth() / 4 * 1.00f;
                break;
            case 3:
                dPadWidth = viewport1.getWorldWidth() / 4 * 1.25f;
                break;
            case 4:
                dPadWidth = viewport1.getWorldWidth() / 4 * 1.50f;
                break;
        }

        float dPadHeight = dPadWidth;
        dpad = new Dpad(dPadTexture, viewport1.getWorldWidth() - dPadWidth, viewport1.getScreenY(), dPadWidth, dPadHeight);
        touchPos = new Vector3();



    }

    @Override
    public void show() {


    }

    @Override
    public void render(float delta) {
        switch (state) {
            case PLAYING:
                updateSnake(delta);
                queryInput();
                checkAppleCollision();
                checkAndPlaceApple();
                break;
            case GAME_OVER:
                checkForRestart();
                break;
        }

        clearScreen();
//        drawGrid();
        draw();
    }

    @Override
    public void resize(int width, int height) {
        viewport1.update(width, height);
        controllerViewport.update(width, height);
    }

    private void checkForRestart(){
        if (Gdx.input.isKeyPressed(Input.Keys.SPACE)){
            doRestart();
        }
        if (state == STATE.GAME_OVER && Gdx.input.isTouched()){
            doRestart();
        }
    }

    private void doRestart() {
        state = STATE.PLAYING;
        bodyParts.clear();
        snakeDirection = RIGHT;
        directionSet = false;
        timer = MOVE_TIME;
        snakeX = 0;
        snakeY = 0;
        snakeXBeforeUpdate = 0;
        snakeYBeforeUpdate = 0;
        score = 0;
        appleAvailable = false;
    }

    private void updateSnake(float delta) {
        if (!hasHit) {
            timer -= delta;
            if (timer <= 0) {
                timer = MOVE_TIME;
                moveSnake();
                checkForOutOfBounds();
                updateBodyPartsPosition();
                checkSnakeBodyCollision();
                directionSet = false;
            }
        }
        // add transparency to snake tail:
        for (int i = 0; i < bodyParts.size; i++){
            bodyParts.get(i).setAlphaValue(0.02f * (i + 6));
            if (bodyParts.get(i).alphaValue <= 0.15f){
                bodyParts.get(i).alphaValue = 0.15f;
            }
            if (bodyParts.get(i).alphaValue >= 0.95f){
                bodyParts.get(i).alphaValue = 0.95f;
            }
        }
    }

    private void updateIfNotOppositeDirection(int newSnakeDirection, int oppositeDirection){
        if (snakeDirection != oppositeDirection || bodyParts.size == 0){
            snakeDirection = newSnakeDirection;
        }
    }

    private void updateDirection(int newSnakeDirection){
        if (!directionSet && snakeDirection != newSnakeDirection){
            directionSet = true;
            game.step2Sound.play();
            switch (newSnakeDirection){
                case LEFT:
                    updateIfNotOppositeDirection(newSnakeDirection, RIGHT);
                    break;
                case RIGHT:
                    updateIfNotOppositeDirection(newSnakeDirection, LEFT);
                    break;
                case UP:
                    updateIfNotOppositeDirection(newSnakeDirection, DOWN);
                    break;
                case DOWN:
                    updateIfNotOppositeDirection(newSnakeDirection, UP);
                    break;
            }
        }
    }

    private void queryInput() {
        boolean lPressed = Gdx.input.isKeyPressed(Input.Keys.LEFT);
        boolean rPressed = Gdx.input.isKeyPressed(Input.Keys.RIGHT);
        boolean uPressed = Gdx.input.isKeyPressed(Input.Keys.UP);
        boolean dPressed = Gdx.input.isKeyPressed(Input.Keys.DOWN);

        boolean leftDpadPressed = false;
        boolean rightDpadPressed = false;
        boolean upDpadPressed = false;
        boolean downDpadPressed = false;

        touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
        camera.unproject(touchPos, viewport1.getScreenX(), viewport1.getScreenY(), viewport1.getScreenWidth(), viewport1.getScreenHeight());


        if (touchPos.x >= dpad.upRect.x && touchPos.x <= dpad.upRect.x + dpad.upRect.width
                && touchPos.y >= dpad.upRect.y && touchPos.y <= dpad.upRect.y + dpad.upRect.height)
            upDpadPressed = true;
        if (touchPos.x >= dpad.downRect.x && touchPos.x <= dpad.downRect.x + dpad.downRect.width
                && touchPos.y >= dpad.downRect.y && touchPos.y <= dpad.downRect.y + dpad.downRect.height)
            downDpadPressed = true;
        if (touchPos.x >= dpad.leftRect.x && touchPos.x <= dpad.leftRect.x + dpad.leftRect.width
                && touchPos.y >= dpad.leftRect.y && touchPos.y <= dpad.leftRect.y + dpad.leftRect.height)
            leftDpadPressed = true;
        if (touchPos.x >= dpad.rightRect.x && touchPos.x <= dpad.rightRect.x + dpad.rightRect.width
                && touchPos.y >= dpad.rightRect.y && touchPos.y <= dpad.rightRect.y + dpad.rightRect.height)
            rightDpadPressed = true;


        if (lPressed || leftDpadPressed) updateDirection(LEFT);
        if (rPressed || rightDpadPressed) updateDirection(RIGHT);
        if (uPressed || upDpadPressed) updateDirection(UP);
        if (dPressed || downDpadPressed) updateDirection(DOWN);
    }

    private void moveSnake() {
        snakeXBeforeUpdate = snakeX;
        snakeYBeforeUpdate = snakeY;
        if (!directionSet) {
            game.step1Sound.play(0.1f);
        }
        switch (snakeDirection) {
            case RIGHT: {
                snakeX += SNAKE_MOVEMENT;
                return;
            }
            case LEFT: {
                snakeX -= SNAKE_MOVEMENT;
                return;
            }
            case UP: {
                snakeY += SNAKE_MOVEMENT;
                return;
            }
            case DOWN: {
                snakeY -= SNAKE_MOVEMENT;
                return;
            }
        }
    }

    private void checkForOutOfBounds() {
        if (snakeX >= viewport1.getWorldWidth()) {
            snakeX = 0;
        }
        if (snakeX < 0) {
            snakeX = viewport1.getWorldWidth() - SNAKE_MOVEMENT;
        }
        if (snakeY >= viewport1.getScreenY() + viewport1.getWorldHeight()) {
            snakeY = viewport1.getScreenY();
        }
        if (snakeY < viewport1.getScreenY()) {
            snakeY = viewport1.getScreenY() + viewport1.getWorldHeight() - SNAKE_MOVEMENT;
        }
    }

    private void updateBodyPartsPosition() {
        if (bodyParts.size > 0) {
            BodyPart bodyPart = bodyParts.removeIndex(0);
            bodyPart.updateBodyPosition(snakeXBeforeUpdate, snakeYBeforeUpdate);
            bodyParts.add(bodyPart);
        }
    }

    private void checkAndPlaceApple() {
        if (!appleAvailable) {
            if (!(appleX == snakeX && appleY == snakeY)) {
                if (bodyParts.size == 0) {
                    appleX = (MathUtils.random((int) (viewport1.getWorldWidth() / SNAKE_MOVEMENT) - 1) * SNAKE_MOVEMENT) + viewport1.getScreenX();
                    appleY = (MathUtils.random((int) viewport1.getWorldHeight() / SNAKE_MOVEMENT - 1) * SNAKE_MOVEMENT) + viewport1.getScreenY();
                    appleAvailable = true;
                }
                for (BodyPart bodyPart : bodyParts) {
                    if (!(appleX == bodyPart.x && appleY == bodyPart.y)) {
                        appleX = (MathUtils.random((int) (viewport1.getWorldWidth() / SNAKE_MOVEMENT) - 1) * SNAKE_MOVEMENT) + viewport1.getScreenX();
                        appleY = (MathUtils.random((int) viewport1.getWorldHeight() / SNAKE_MOVEMENT - 1) * SNAKE_MOVEMENT) + viewport1.getScreenY();
                        appleAvailable = true;
                    }
                }
            }
        }
    }

    private void checkAppleCollision() {
        if (appleAvailable && appleX == snakeX && appleY == snakeY) {
            BodyPart bodyPart = new BodyPart(snakeBody);
            bodyPart.updateBodyPosition(snakeX, snakeY);
            bodyParts.insert(0,bodyPart);
            score += POINTS_PER_APPLE * bodyParts.size;
            game.pickupSound.play(0.7f);
            appleAvailable = false;
        }
    }

    private void checkSnakeBodyCollision(){
        for (BodyPart bodyPart : bodyParts){
            if (bodyPart.x == snakeX && bodyPart.y == snakeY){
                state = STATE.GAME_OVER;
                game.loseSound.play();
            }
        }
    }

    private void clearScreen() {
        Gdx.gl.glClearColor(Color.BLACK.r, Color.BLACK.g, Color.BLACK.b, Color.BLACK.a);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    }

    private void draw() {
        batch.setProjectionMatrix(camera.combined);
        batch.begin();
        batch.draw(gameboyTexture, 0, 0, controllerViewport.getWorldWidth(), controllerViewport.getWorldHeight());
        batch.draw(background, viewport1.getScreenX(), viewport1.getScreenY(), viewport1.getWorldWidth(), viewport1.getWorldHeight());
        batch.draw(snakeHead, snakeX, snakeY);


        for (int i = 0; i < bodyParts.size; i++){
            bodyParts.get(i).draw(batch);
        }
        if (appleAvailable) {
            batch.draw(apple, appleX, appleY);
        }
        if (state == STATE.GAME_OVER) {
            layout.setText(bitmapFont, GAME_OVER_TEXT);
            bitmapFont.draw(batch, GAME_OVER_TEXT, (viewport1.getWorldWidth() - layout.width) / 2, (viewport1.getWorldHeight() - layout.height) / 2);
        }
        dpad.setColor(1,1,1,0.3f);
        dpad.draw(batch);
        batch.setColor(1,1,1,1);
        drawScore();
        batch.end();
    }

    private void drawScore(){
        if (state == STATE.PLAYING){
            String scoreAsString = Integer.toString(score);
            layout.setText(bitmapFont, scoreAsString);
            bitmapFont.draw(batch, scoreAsString, (viewport1.getWorldWidth() - layout.width) / 2, viewport1.getWorldHeight() - layout.height * 2);
        }
    }

    private void drawGrid(){
        shapeRenderer.setProjectionMatrix(camera.combined);
        shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
        for (int x = 0; x < viewport1.getWorldWidth(); x += GRID_CELL){
            for (int y = 0; y < viewport1.getWorldHeight(); y += GRID_CELL){
                shapeRenderer.rect(x, y, GRID_CELL, GRID_CELL);
            }
        }
        shapeRenderer.end();
    }

    private class BodyPart {

        private float x, y;
        private Texture texture;
        private float alphaValue;

        public BodyPart(Texture texture) {
            this.texture = texture;
        }

        public void updateBodyPosition(float x, float y) {
            this.x = x;
            this.y = y;
        }

        public void setAlphaValue(float alpha){
            alphaValue = alpha;
        }

        public void draw(Batch batch) {
            if (!(x == snakeX && y == snakeY)){
                batch.setColor(1,1,1,alphaValue);
                batch.draw(texture, x, y);
                batch.setColor(1,1,1,1);
            }
        }
    }
}

If anyone can help me finally figure this out I would be eternally grateful

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Rather than focusing on the idea of multiple viewports, you should approach this problem in the same way that you approach any game with the game world rendered on one layer, and the HUD rendered on a layer above it.

Simply create a stage, camera, and batch for the world and another one for the HUD. You could use a ScreenViewport for the world and zoom the camera out so that it is small enough to fit inside whatever gameboy image you want to layer on top of it.

You could then use any kind of Viewport for the HUD, whether it be Stretch or Screen, whatever works best. Add the image of the gameboy to the hud stage, and then add the buttons on top of that.

You can then play around with the camera of the world stage in order to zoom and position the stage exactly where it needs to be to fit inside the gameboy hud.

I should mention that if you do not want to use a stage for drawing the world, you can still use the batch and camera and the necessary draw methods inside of the render method. The key difference between your code and what I am suggesting is that you would have a separate camera and batch for the HUD rather than trying to use the same one for both.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .