Skip to content

Achievement Screen

Ella West edited this page Oct 18, 2022 · 23 revisions

Page Navigation

Jump to a section or return to Achievement Summary here!

Summary

This screen was made obsolete in Sprint 4 and was subsequently removed. This section has been left for achival purposes. For the updated achievement interface please visit this page.

The achievement screen can be accessed from the main game screen and displays badges for each achievement in the game and their completion status. It is made up of three components - AchievementScreen, AchievementDisplay and AchievementActions - which hold all functionality for the UI.

Examples of achievement screen after sprint 3

Summary achievement display: Summary achievement display

Game achievement display: Game Achievement display

Design

The main challenge of implementing the achievement screen was in getting the display to match the designs initially set out for it. This still requires some work to make the formatting look good on more screen sizes, but significant progress was made by upscaling the size of images that we were using since they were being downsized for screens smaller than our background images; however, for larger screens the backgrounds were not even reaching halfway across the screen.

More information of the design process for this screen can be found in the Achievement UI section of the wiki.

Technical

The implementation of the achievement screen uses three main classes: AchievementScreen, AchievementDisplay and AchievementActions.

The achievement screen can be set to the current screen using the setScreen() method in AtlantisSinks by making the following call:

game.setScreen(AtlantisSinks.ScreenType.ACHIEVEMENT);

Where game is an instance of AtlantisSinks.

Achievement Screen

AchievementScreen is the base class that the UI operates from. It initialises the service locator with the needed services:

ServiceLocator.registerTimeSource(new GameTime());

ServiceLocator.registerInputService(new InputService());
ServiceLocator.registerResourceService(new ResourceService());
ServiceLocator.registerEntityService(new EntityService());
ServiceLocator.registerRenderService(new RenderService());
ServiceLocator.registerAchievementHandler(new AchievementHandler());

this.renderer = RenderFactory.createRenderer();

The screen then helps load the assets the rest of the display will use by calling loadAssets() before it creates the UI display using createUI(), which creates a new instance of AchievementDisplay:

private void createUI() {
    logger.debug("Creating achievement UI");
    Stage stage = ServiceLocator.getRenderService().getStage();

    Entity ui = new Entity();
    ui.addComponent(new AchievementDisplay())
            .addComponent(new InputDecorator(stage, 10))
            .addComponent(new AchievementActions(this.game));
    ServiceLocator.getEntityService().registerNamed("AchievementUI", ui);
}

Achievement Display

AchievementDisplay creates the UI elements of the screen using a series of tables that are added to the render component of the game. These are constructed in the addActors() method following the below diagram.

Achievement Display Layout Diagram

The ImageButtons in the navigation table from the diagram are created using the createButton() method:

private ImageButton createButton(String image) {
    Texture buttonTexture = new Texture(Gdx.files.internal(image));
    TextureRegionDrawable up = new TextureRegionDrawable(buttonTexture);
    TextureRegionDrawable down = new TextureRegionDrawable(buttonTexture);

    return new ImageButton(up, down);
}

These buttons then have their events mapped to them using the addButtonEvent() method:

private void addButtonEvent(ImageButton button, String name) {
    button.addListener(
            new ChangeListener() {
                @Override
                public void changed(ChangeEvent event, Actor actor) {
                    logger.debug("{} button clicked", name);
                    entity.getEvents().trigger(events.get(name), displayTable);
                }
            });
}

This is done to reduce the amount of repeated code that goes into creating the navigation buttons as they all use the same structure.

The displayed content is rotated using the changeDisplay() method, which is called both in the addActors() method and in the methods of AchievementActions. This makes it so that only one screen class is needed to change what achievements are being displayed.

public static void changeDisplay(Table displayTable, AchievementType type) {
    displayTable.clear();
    displayTable.add(new Label(type.getTitle(), skin)).colspan(6).expandX();
    displayTable.row();

    int achievementsAdded = 0;
    ArrayList<Achievement> achievements = new ArrayList<>(ServiceLocator.getAchievementHandler().getAchievements());

    if (type == AchievementType.SUMMARY) {
        for (AchievementType achievementType : AchievementType.values()) {
            if (achievementsAdded != 0 && achievementsAdded % 2 == 0) {
                displayTable.row();
            } else if (achievementType == AchievementType.SUMMARY) {
                continue;
            }

            displayTable.add(buildAchievementSummaryCard(achievementType)).colspan(3).fillX();

            achievementsAdded++;
        }

        return;
    }

    for (Achievement achievement : achievements) {
        if (achievement.getAchievementType() == type) {
            if (achievementsAdded != 0 && achievementsAdded % 2 == 0) {
                displayTable.row();
            }

            displayTable.add(buildAchievementCard(achievement)).colspan(3).fillX();

            achievementsAdded++;
        }
    }
}

This method calls buildAchievementCard() to populate the display with achievement cards from the achievement handler's achievement list.

Achievement Cards

The display table holds all of the achievement cards, made using tables following the diagrams below depending on if the achievement is a stat or non-stat achievement.

Stat Achievement

Stat achievements have an extra table added to their design (seen the diagram below) that is used to hold the milestone indicator images. These images can be hovered over to show the different stages of the stat achievement that have been completed.

Stat Achievement Card Layout Diagram

Non-Stat Achievement

Non-stat achievements use the basic achievement card with no extra table for milestones.

Achievement Card Layout Diagram

Achievement Card Creation

All achievement cards are built using the buildAchievementCard() method, which constructs a card using tables following the above diagrams.

public static Table buildAchievementCard(Achievement achievement) {
    Table achievementCard = new Table();
    achievementCard.pad(30);
    Texture backgroundTexture = new Texture(Gdx.files.internal(achievement.isCompleted() ? "images/achievements/achievement_card_completed.png" : "images/achievements/achievement_card_locked_n.png"));
    Image backgroundImg = new Image(backgroundTexture);
    achievementCard.setBackground(backgroundImg.getDrawable());

    Table achievementCardHeader = new Table();
    Texture achievementTypeTexture = new Texture(Gdx.files.internal(achievement.getAchievementType().getPopupImage()));
    Image achievementTypeImage = new Image(achievementTypeTexture);
    achievementTypeImage.setAlign(Align.left);
    achievementCardHeader.add(achievementTypeImage);
    achievementCardHeader.add(new Label(achievement.getName(), skin, "small"));
    achievementCard.add(achievementCardHeader).expand();
    achievementCard.row();

    ArrayList<String> achievementDescription = splitDescription(achievement.getDescription());

    var descriptionLabel = new Label(achievementDescription.get(0), skin, "small");
    achievementCard.add(descriptionLabel).colspan(3).expand();
    achievementCard.row();

    if (achievement.isStat()) {
        descriptionLabel.setText(achievement.getDescription().formatted(achievement.getTotalAchieved()));
        achievementCard.add(buildAchievementMilestoneButtons(achievement, descriptionLabel)).padBottom(20);
    } else {
        for (String s : achievementDescription) {
            if (achievementDescription.indexOf(s) == 0) {
                continue;
            }

            achievementCard.add(new Label(s, skin, "small")).colspan(3).expand();
            achievementCard.row();
        }

        if (achievementDescription.size() == 1) {
            achievementCard.add(new Label("", skin, "small"));
        }
    }

    achievementCard.pack();

    return achievementCard;
}

Stat achievements have an extra call to buildAchievementMilestoneButtons() to create the milestone indicators with the on hover event for the indicators.

public static Table buildAchievementMilestoneButtons(Achievement achievement, Label descriptionLabel) {
    var achievementService = ServiceLocator.getAchievementHandler();
    Table milestoneButtons = new Table();
    milestoneButtons.add();
    milestoneButtons.add(getMilestoneImageButtonByNumber(1,
            achievementService.isMilestoneAchieved(achievement, 1), achievement, descriptionLabel));
    milestoneButtons.add(getMilestoneImageButtonByNumber(2,
            achievementService.isMilestoneAchieved(achievement, 2),achievement,descriptionLabel));
    milestoneButtons.add(getMilestoneImageButtonByNumber(3,
            achievementService.isMilestoneAchieved(achievement, 3),achievement,descriptionLabel));
    milestoneButtons.add(getMilestoneImageButtonByNumber(4,
            achievementService.isMilestoneAchieved(achievement, 4),achievement,descriptionLabel));
    milestoneButtons.add();
    return milestoneButtons;
}

The hover event is implemented during the call to getMilestoneImageButtonByNumber(), which itself calls createMilestoneImageButtonWithHoverEvent() to add the event listener to the milestone indicator image:

private static Image createMilestoneImageButtonWithHoverEvent(boolean isComplete, Label descriptionLabel, Achievement achievement, int milestoneNumber) {
    AchievementHandler achievementService = ServiceLocator.getAchievementHandler();

    Texture backgroundTexture = new Texture(Gdx.files.internal(
            isComplete ? "images/achievements/milestone_%d_completed.png".formatted(milestoneNumber) :
                    "images/achievements/milestone_%d_incomplete.png".formatted(milestoneNumber) ));
    var image = new Image(backgroundTexture);
    if (isComplete) {
        image.addListener(new ClickListener() {
            @Override
            public void enter(InputEvent event, float x, float y, int pointer, @Null Actor fromActor) {
                descriptionLabel.setText(achievement.getDescription().formatted(achievementService.getMilestoneTotal(achievement, milestoneNumber)));
            }

            @Override
            public void exit(InputEvent event, float x, float y, int pointer, @Null Actor toActor) {
                descriptionLabel.setText(achievement.getDescription().formatted(achievement.getTotalAchieved()));
            }
        });
    }
    return image;
}

Achievement Actions

AchievementActions listens to all the events created by the achievement display on creation:

public void create() {
    entity.getEvents().addListener(AchievementDisplay.EVENT_SUMMARY_BUTTON_CLICKED, this::onSummary);
    entity.getEvents().addListener(AchievementDisplay.EVENT_BUILDING_BUTTON_CLICKED, this::onBuilding);
    entity.getEvents().addListener(AchievementDisplay.EVENT_GAME_BUTTON_CLICKED, this::onGame);
    entity.getEvents().addListener(AchievementDisplay.EVENT_KILL_BUTTON_CLICKED, this::onKill);
    entity.getEvents().addListener(AchievementDisplay.EVENT_RESOURCE_BUTTON_CLICKED, this::onResource);
    entity.getEvents().addListener(AchievementDisplay.EVENT_UPGRADE_BUTTON_CLICKED, this::onUpgrade);
    entity.getEvents().addListener(AchievementDisplay.EVENT_MISC_BUTTON_CLICKED, this::onMisc);
    entity.getEvents().addListener(AchievementDisplay.EVENT_EXIT_BUTTON_CLICKED, this::onExit);
}

If an event is triggered, the class calls the relative method to change the displayed achievements to the requested ones. For example if the EVENT_KILL_BUTTON_CLICKED event is triggered, the onKill() method is called:

private void onKill(Table displayTable) {
    logger.info("Kill achievement screen");
    AchievementDisplay.changeDisplay(displayTable, AchievementType.KILLS);
}

This calls the changeDisplay() method from the AchievementDisplay class with the KILLS achievement type specified. This is method is itentical for all event listeners other than EVENT_EXIT_BUTTON_CLICKED, which returns to the main game screen using the same call to game.setScreen() as is used to create the achievement screen:

private void onExit(Table displayTable) {
    logger.info("Exiting achievement screens");
    displayTable.clear();
    game.setScreen(AtlantisSinks.ScreenType.MAIN_GAME);
}

Testing

The video below demonstrates the working achievement screen, showing navigation to the achievement screen, clicking through of achievement tabs, and hover functionality for stat achievements.

Achievement Screen Demonstration


Back to Achievement Summary

Table of Contents

Home

How to Play

Introduction

Game Features

Main Character

Enemies
The Final Boss

Landscape Objects

Shop
Inventory
Achievements
Camera

Crystal

Infrastructure

Audio

User Interfaces Across All Pages
Juicy UI
User Interfaces Buildings
Guidebook
[Resource Management](Resource-Management)
Map
Day and Night Cycle
Unified Grid System (UGS)
Polishing

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Game Screens and Areas

Terrain

Concurrency & Threading

Settings

Troubleshooting

MacOS Setup Guide

Clone this wiki locally