Achievement Screen - UQdeco2800/2022-studio-1 GitHub Wiki
Jump to a section or return to Achievement Summary here!
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.
Summary achievement display:
Game achievement display:
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.
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
.
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);
}
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.
The ImageButton
s 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.
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 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.
Non-stat achievements use the basic achievement card with no extra table for milestones.
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;
}
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);
}
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.