Skip to content

Menu Screens

adamFittlerUQ edited this page Oct 1, 2023 · 13 revisions

Menu Screens

Details the design and implementation of the 3 menu screens and 1 introduction screen in the game as well as the lose screen. For more information about the design of UI components, please see UI Skin.

Main Menu Screen

Design iterations

During the ideation process, our team reached a consensus that the main menu should showcase a celestial garden, with the user's perspective looking out into the cosmic expanse. Once this concept took root, we drew inspiration from a series of space-themed images to craft our initial sketches and bring our final product to life. The very first iteration of the UI for the home screen system was design. These mocks were made so that the team had a rough idea of the final design, and we knew what we were working towards.

Screenshot 2023-08-23 at 9 24 58 am Screenshot 2023-08-23 at 9 24 50 am

During the user testing and feedback process, the design committee expressed concerns about the blend of realistic and pixelated images and strongly advocated for a uniform pixelated aesthetic throughout the game. This input led us to explore the idea of incorporating pixelated asset images, including plants and characters, which were originally intended for in-game use. Thus, the second iteration of the background skin was developed:

galaxy_home_still

The new designs received a positive response from both the design committee and our own team. It was rewarding to see that the pixelated aesthetic resonated so well with everyone.

Implementation

In the development of our game, we have implemented a main menu using the MainMenuDisplay class. One of the key elements of this main menu is the background image. In this wiki, we will delve into the role and implementation of this background image. The background image in the main menu plays a crucial role in setting the tone and atmosphere of our game. It serves as the first visual impression for players and can significantly impact their initial perception of the game world. In our case, the background image represents the backdrop for our main menu, and it aligns with the theme and narrative of our game, creating a visually appealing and immersive experience. The approach of incorporating the background image through LibGDX provides flexibility for future enhancements. This level of customisation allows us to adapt the main menu's visual style as needed. The background image in the MainMenuDisplay class is a vital component that enhances the overall aesthetic appeal of our main menu. Its implementation, utilising the capabilities of LibGDX, enables us to create an engaging and immersive introduction to our game, "Gardens of the Galaxy."

The background image is added to the MainMenuDisplay class through the use of the LibGDX framework. Here's how it is implemented:

Image title = new Image( ServiceLocator.getResourceService() .getAsset("images/galaxy_home_still.png", Texture.class));

In the code snippet above, we create an Image object named title. We use ServiceLocator.getResourceService() to access game resources, and .getAsset() loads the background image from the specified path ("images/galaxy_home_still.png"). The image's dimensions are set to match the width and height of the game window, ensuring it covers the entire screen.

`title.setWidth(Gdx.graphics.getWidth());

title.setHeight(Gdx.graphics.getHeight());

title.setPosition(0, 0);`

These lines set the dimensions of the title image to match the width and height of the game window, making it fullscreen. The setPosition(0, 0) call ensures that the image is positioned at the centre of the screen.

table.add(title);

Finally, the title image is added to the table layout, which is part of the main menu's UI layout.

Controls Screen

The controls screen provides players of the game with a guide to the controls that are used to play the game. The layout is kept simple, with a table in the center of the screen, detailing keyboard keys and their associated actions within the game.

As the first sprint of the game was not focused on game play, the controls listed are minimal. The table however is designed to allow for the easy addition of future game controls.

Initial versions of this screen only contained the key-description table, but it was iteratively updated to be consistent in design with other menu pages, as the background and animations were developed.

Implementation

The Controls screen is powered by two classes - ControlsScreen and ControlsMenuDisplay.

ControlsScreen

This class is responsible for creating the background logic for the UI. It performs the following tasks -

  • Loading and unloading the image assets required for the ControlsMenuDisplay class (loadAssets() & unloadAssets)
  • Instantiating a renderer, which is used to draw the UI onto screen and also handle screen resizing events.
  • Register the services required for the game to function, including the InputService which is required to receive user interaction events.
  • Load and display the ControlsMenuDisplay class by adding it to the stage of the game's renderer.

ControlsMenuDisplay

This class is responsible for creating and laying out the UI for the Controls screen. It performs the following tasks -

  • Creating instances of the Label, Table, TextButton and Image classes that make up the UI.
  • Positioning the elements on the screen, by adding them to the screen's stage. This is done directly for the background image, and through a rootTable for other UI elements.

Adding Controls

The table of controls displayed on this screen is dynamically generated from a LinkedHashMap, named controls, in the makeControlsTable() method.

To add a new entry to the table, all you have to do is add a command after Ln 166 which meets the following format -

controls.put("KEY", "DESCRIPTION");

where KEY is the keyboard key that must be pressed and DESCRIPTION is the function of that key.

Introduction Screen

The introduction screen plays an animated, cut scene sequence before the main game begins. In early versions of the game, this is unskippable, but will be amended to only play the first time the user plays the game.

The animated sequence introduces the user to the background story of the game, and presents them with an objective "...to tame the planet we now call home". The text contained on this screen is dynamically generated and will be updated as the game's story and main objective evolve.

Implementation

The Controls screen is powered by two classes - IntroScreen and IntroDisplay.

IntroScreen

This class is responsible for creating the background logic for the UI. It performs the following tasks -

  • Loading and unloading the image assets required for the ControlsMenuDisplay class (loadAssets() & unloadAssets)
  • Instantiating a renderer, which is used to draw the UI onto screen and also handle screen resizing events.
  • Register the services required for the game to function, including the InputService which is required to receive user interaction events.
  • Load and display the ControlsMenuDisplay class by adding it to the stage of the game's renderer.

IntroDisplay

The display class for the introduction screen differs slightly from other screens in that two major UI components are laid out manually with position coordinates and that the major textual element uses a new class TypingLabel. This functionality is explained in the Animation section below. Other than this, it serves largely the same functions as the Display classes of other menu screens:

  • Creating instances of the Label, Table, TextButton and Image classes that make up the UI.
  • Positioning the elements on the screen, by adding them to the screen's stage. This is done directly for the background image, and through a rootTable for other UI elements.

Animation

The intro screen has two animated sequences - text animation and background animation.

Text

The text on the screen is animated to be revealed with a typewriter style effect. This functionality was achieved using the TypingLabel class which was included from the typing-label package which was added as a Gradle package. This class is a super-class of the Scene2d.ui.Label class, and as such behaves identically to it in terms of layout and positioning with the Table class. The text of the label can also thus be changed easily as the game's story evolves.

The main animation that the TypingLabel class provides is a typewriter effect with which the text for the label is typed out letter by letter. It also allows pauses to be interjected in between words, which were used in the introduction screen to create a sense of drama. One other feature that was used was to color the sentence outlining the game's main objective green, in order to highlight its importance to the user.

The TypingLabel class also provides a setTypingListener method, which allows for events such as the end of animation to be listened for. This was used in the cut scene to display a Continue button only after the entire animation was complete.

Background

The background animation was achieved by using two assets:

Starfield
  • intro_planet.png which is a stylised image of a planet that resembles the terrain seen in the main game. Sourced from Kenney Planets.
Planet

These two images form a moving animation in the background of the page, which is timed with the animation of the text to make the planet enter the screen when the relevant line in the textual story is displayed to the user. Together, the background animation simulates the view from a spaceship moving through space and finally reaching a destination planet.

The main logic to run this function is found in the update method of the IntroDisplay which is fired for every frame.

    @Override
    public void update() {
        // This movement logic is triggered on every frame, until the middle of the planet hits its target position
        // on screen
        if (planet.getY(Align.center) >= storyLabel.getY(Align.top) + planetToTextPadding) {
            planet.setY(planet.getY() - spaceSpeed); // Move the planet
            background.setY(background.getY() - spaceSpeed); // Move the background
        }
        stage.act(ServiceLocator.getTimeSource().getDeltaTime());
    }

Menu Screens Animation

Our approach to designing animations was grounded in a philosophy of maintaining a balance between engagement and user-friendliness. The background skins for our main menu, controls, and settings screens were already carefully crafted to be high-contrast and visually appealing. Therefore, we aimed to complement these designs with animations that were subtle yet captivating. For our animations, we decided to incorporate two prominent elements from our game world: a character and a plant. These elements are not only recognisable to players but also provide a connection to the in-game environment. Animating these elements allowed us to infuse life and personality into our user interface. The animations involved simple vertical motion—moving the character and plant up and down at the top of the screen. This motion was synchronised with the title of our game, creating a harmonious visual rhythm. The goal was to make the animations a seamless part of the user interface, evoking a sense of continuity and immersion. User testing and feedback were integral to the animation design process. We conducted usability tests with a diverse group of players to gather insights into their reactions and preferences. The feedback received was overwhelmingly positive, with players expressing appreciation for the subtle animations that added depth to our main menu and screens.

The first frame of the animation:

menu_animations0

Implementation

This subsection of the Wiki discusses the implementation of background animation in the MainMenuScreen and MainMenuDisplay classes of a game. The animation consists of a series of frames that create a dynamic background effect for the main menu. To understand the animation is to understand how these two classes work together.

In the MainMenuScreen class:

private static final int frameCount = 71; private static final String[] transitionTextures = new String[frameCount]; private static final String animationPrefix = "images/menu_animations/menu_animations";

frameCount: This variable represents the total number of frames in the animation. In this case, there are 71 frames. transitionTextures: An array of strings is initialised to store the file paths of each animation frame. animationPrefix: This string represents the common prefix for the file paths of the animation frames.

`private void loadFrames() { ResourceService resourceService = ServiceLocator.getResourceService();

for (int i = 0; i < frameCount; i++) {
    transitionTextures[i] = animationPrefix + i + ".png";
}

resourceService.loadTextures(transitionTextures);
ServiceLocator.getResourceService().loadAll();

}`

The loadFrames() method is responsible for dynamically generating the file paths for the animation frames. It constructs the file paths using the animationPrefix and a numeric index. Each frame's file path is added to the transitionTextures array. The loadTextures method from the game's ResourceService is used to load all the animation frames as textures.

In the MainMenuDisplay class:

private static final float Z_INDEX = 2f; public static int frame; private Image transitionFrames; private long lastFrameTime; private int fps = 15; private final long frameDuration = (long)(800 / fps);

frame: This variable keeps track of the current frame of the animation. transitionFrames: An Image instance used to display the animation frame. lastFrameTime: A timestamp representing the time when the last frame change occurred. fps and frameDuration: These variables control the animation speed by specifying frames per second (fps) and frame duration.

@Override public void create() { frame = 1; super.create(); addActors(); }

The create() method is overridden and initialises the frame variable to 1 (assuming that the animation starts from the first frame) and calls addActors() to set up the main menu display.

'private void addActors() { // ... (code for creating the main menu components)

if (frame < MainMenuScreen.frameCount) {
    // Load the current animation frame as an Image
    transitionFrames = new Image(ServiceLocator.getResourceService()
        .getAsset(MainMenuScreen.transitionTextures[frame], Texture.class));
    
    // Set the dimensions and position of the animation frame
    transitionFrames.setWidth(Gdx.graphics.getWidth());
    transitionFrames.setHeight(Gdx.graphics.getHeight() / 2);
    transitionFrames.setPosition(0, Gdx.graphics.getHeight() / 2 + 15);
    
    // Increment the frame counter
    frame++;
    
    // Add the animation frame to the stage
    stage.addActor(transitionFrames);
    
    // Record the current time
    lastFrameTime = System.currentTimeMillis();
} else {
    // Reset the frame counter to 1 if we've reached the end of the animation
    frame = 1;
}

// ... (code for adding other main menu components)

}'

Within the addActors() method, the current animation frame is loaded as an Image from the resources. The dimensions and position of the animation frame are set to cover the top half of the screen below the static menu. The frame counter is incremented, and the animation frame is added to the stage. A timestamp is recorded to control the frame rate. If the end of the animation is reached, the frame counter is reset to 1, allowing the animation to loop.

Lose Screen

The UI has been implemented in the same way as the menu screens so therefore it does not need to be covered

Lose Game Event

In the constructor for "MainGameScreen", an listener is registered under the MissionManager entity to call the lose game screen.

ServiceLocator.getMissionManager().getEvents().addListener("loseScreen", this::playLoseScreen);

When this event is triggered the function playLoseScreen() will be called. This function changes the screen to the LOSESCREEN

public void playLoseScreen() {
        game.setScreen(GdxGame.ScreenType.LOSESCREEN);
}

How to call the lose event

The lose screen can simply be activated by accessing the player handler and triggering the "loseScreen" event.

ServiceLocator.getMissionManager().getEvents().trigger("loseScreen");

Custom losing screen text

We are able to have custom losing messages by invoking the setLoseReason() function of the LoseScreenDisplay component. Below is an example showing its use for the default lose condition of loss of oxygen.

LoseScreenDisplay.setLoseReason("oxygen");

Win Screen

The UI has been implemented in the same way as the menu screens so therefore it does not need to be covered

Win Game Event

In the constructor for "MainGameScreen", an listener is registered under the MissionManager entity to call the win game screen.

ServiceLocator.getMissionManager().getEvents().addListener("winScreen", this::playWinScreen);

When this event is triggered the function playWinScreen() will be called. This will switch the current screen to the WINSCREEN

public void playWinScreen() {
        game.setScreen(GdxGame.ScreenType.WINSCREEN);
}

How to call the win event

The win screen can simply be activated by accessing the player handler and triggering the "winScreen" event.

ServiceLocator.getMissionManager.getEvents().trigger("winScreen")

End Credits Screen

The UI has been implemented in the same way as the menu screens so therefore it does not need to be covered

End Credits Event

In the constructor for "MainMenuActions", an listener is registered under the entity to call the onCredits function.

entity.getEvents().addListener("credits", this::onCredits);

When this event is triggered the function onCredits() will be called. This will switch the current screen to the ENDCREDITS

private void onCredits() {
    logger.info("Play Credits");
    game.setScreen(GdxGame.ScreenType.ENDCREDITS);
}

How to call the credits event

The win screen can simply be activated by accessing the player handler and triggering the "credits" event. Currently this is invoked using the end credits button in the "MainMenuDisplay" as shown below

creditsBtn.addListener(
                new ChangeListener() {
                    @Override
                    public void changed(ChangeEvent changeEvent, Actor actor) {

                        logger.debug("Credits button clicked");
                        entity.getEvents().trigger("credits");
                    }
});
Clone this wiki locally