Sprint 3 Main Character New Unlockable Attires and Functionality improvement - UQdeco2800/2021-ext-studio-2 GitHub Wiki

Contents

Automatic Item PickUp Implementation

When the main player - Jason collides with an item, Jason will automatically pick up the item. With this feature, the user has no need to press a key to initiate item pickup.

PlayerFactory.java


    switch (attire) {

            ...
            case "OG":
            default:
                mpcAnimator = createAnimationComponent("images/mpc/finalAtlas/OG/mpcAnimation.atlas");
                mpcTexture = new TextureRenderComponent("images/mpc/finalAtlas/OG/mpc_right.png");
                break;
        }
    ...
    mpcAnimator.addAnimation("main_player_pickup",0.125f,Animation.PlayMode.LOOP);

Automatic Item Pick Up animation is triggered from ItemComponent.java

After 1s the animation is stopped.


    private void onCollisionStart(Fixture me, Fixture other){

        if (PhysicsLayer.contains(PhysicsLayer.PLAYER, other.getFilterData().categoryBits)) {
           // checking if the collision is done with the player
           callback.accept(target);
           target.getEvents().trigger("itemPickUp");
           entity.getEvents().trigger("itemPickedUp");
           ...
           try {
               ...
               // after 1s stop the item pickUp animation
               Timer timer=new Timer();
               timer.scheduleTask(new Timer.Task() {
                   @Override
                   public void run() {
                       target.getEvents().trigger("stopPickUp");
                       timer.stop();
                   }
               },1);
           }
           catch (Exception e){
               System.out.print(e);
           }
        }
    }

Buff Debuffs Animation Integration

As the player Jason interacts progress in the game, it will experience buffs and debuffs from interacting with the obstacles and items.

Buffs And Debuffs Map

Buff/ Debuff Type
Hungry Debuff
Poisoned Debuff
Dizzy Debuff
Health Down Debuff
Health Limit UP Debuff
Speed Down Debuff
Thirsty Debuff
Recovered Buff
Health Up Buff

PlayerFactory.java adds the animation for those buffs and debuffs

These buffs/Debuffs are added for each movement animation.


           ...
            mpcAnimator.addAnimation("main_player_run_dizzy", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_health-down", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_health-limit-up", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_health-up", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_hungry", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_poisoned", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_recovered", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_speed-down", 0.1f, Animation.PlayMode.LOOP);
            mpcAnimator.addAnimation("main_player_run_thirsty", 0.1f, Animation.PlayMode.LOOP);
           ...

The listeners implemented here lookout for trigger events and run the corresponding functions in PlayerAnimationController.java. These functions are discussed in detail in the following sections of the wiki.


     ...
     public void create() {
        super.create();
        animator = this.entity.getComponent(AnimationRenderComponent.class);
        ...
        entity.getEvents().addListener("hungry", this::animateHungry);
        entity.getEvents().addListener("poisoned", this::animatePoison);
        entity.getEvents().addListener("healthDown", this::animateHealthDown);
        entity.getEvents().addListener("dizzy", this::animateDizzy);
        entity.getEvents().addListener("health_limit_up", this::animateHealthLimit);
        entity.getEvents().addListener("healthUp", this::animateHealthUP);
        entity.getEvents().addListener("recovered", this::animateRecovered);
        entity.getEvents().addListener("speedDown", this::animateSpeedDown);
        entity.getEvents().addListener("thirsty", this::animateThirsty);
        entity.getEvents().addListener("stopBuffDebuff", this::animateWalk);
    }
    ...

Each time an animation event is triggered, a function is used to start the animation for the buff/debuff while taking into account the previous animation that was running and runs an animation for buff/debuff corresponding to that animation.

    
   ...
   
   /**
     *  Activate the hungry animation debuff
     */
    private void animateHungry() {
        animationName = animator.getCurrentAnimation();
        preAnimationCleanUp();
        switch (animationName) {
            case "main_player_run":
                animator.startAnimation("main_player_run_hungry");
                break;
            case "main_player_pickup":
                animator.startAnimation("main_player_pickup_hungry");
                break;
            case "main_player_jump":
                animator.startAnimation("main_player_jump_hungry");
                break;
            case "main_player_attack":
                animator.startAnimation("main_player_attack_hungry");
                break;
            case "main_player_crouch":
                animator.startAnimation("main_player_crouch_hungry");
                break;
            case "main_player_right":
                animator.startAnimation("main_player_right_hungry");
                break;
            case "main_player_walk":
            default:
                animator.startAnimation("main_player_walk_hungry");
                break;
        }
    }
  ...

Unlockable Attires

NOTE: For demo and testing purposes, all the attires are unlocked by default for now. This was done by hard coding the number of unlocked gold achievements to >6.

Select Attires Screen

A new screen for selecting unlocked attires was created and the Select Unlocked Attires Button was included in the game main menu.

States of Attires Screen

Zero Gold Achievements

One Gold Achievement

Two Gold Achievements

Three Gold Achievements

Four Gold Achievements

Five Gold Achievements

Six Gold Achievements or higher

Getting and Storing number of gold achievements

The number of gold achievements unlocked are retrieved and stored as an int.

 int goldAchievements = GameRecords.getGoldAchievementsCount();

public UnlockedAttiresDisplay(GdxGame game,int goldAchievements) {
        this.goldAchievements = goldAchievements;
        this.game = game;
        this.stats = FileLoader.readClass(PlayerConfig.class, "configs/player.json");
        this.attire = stats.attire;
        ServiceLocator.registerResourceService(new ResourceService());
        loadAssets();

    }

Achievements needed to unlock attires

The table below lists the number of gold achievements needed to unlock each new attire

Number of Gold Achievements Attire Unlocked
0 Original
2 gold_2
4 gold_4
6 gold_6

Attires Unlocking Mechanism

Once the required number of achievements are unlocked by the user, the following snippets of code handle the access and unlocking of new attires on the attires screen.

public UnlockedAttiresDisplay(GdxGame game,int goldAchievements) {
        this.goldAchievements = goldAchievements;
        this.game = game;
        this.stats = FileLoader.readClass(PlayerConfig.class, "configs/player.json");
        this.attire = stats.attire;
        ServiceLocator.registerResourceService(new ResourceService());
        loadAssets();

    }
 if (goldAchievements == 0) {
            renderZeroUnlockedAttiresTable();
        } else {
            renderUnlockedAttiresTable();
        }
/**
     * Renders all unlocked attires
     */

 private void renderUnlockedAttiresTable() {
        renderUnlockedAttires(goldAchievements, 1);
    }

The following method renders the screen when the user has zero gold achievements.

/**
     * Renders screens to show zero unlocked attires
     */
    private void renderZeroUnlockedAttiresTable() {
        Label message1 = new Label("YOU HAVEN'T UNLOCKED ANY NEW ATTIRES YET!",
                new Label.LabelStyle(new BitmapFont(), Color.RED));
        message1.setFontScale(3f);
        table.add(message1).padTop(20f).center();
        table.row();
        Label message2 = new Label("UNLOCK MORE GOLD ACHIEVEMENTS TO ACCESS NEW ATTIRES!",
                new Label.LabelStyle(new BitmapFont(), Color.YELLOW));
        message2.setFontScale(2f);
        table.add(message2).padTop(20f).center();
        table.row();

        Image gold_2 = new Image(ServiceLocator.getResourceService()
                .getAsset("images/mpc/attires/gold_2.png", Texture.class));
        table.add(gold_2).padLeft(10f).padRight(10f).padTop(20f).size(220, 150);
        table.row();
        Image gold_4 = new Image(ServiceLocator.getResourceService()
                .getAsset("images/mpc/attires/gold_4.png", Texture.class));
        table.add(gold_4).padLeft(10f).padRight(10f).padTop(20f).size(220, 150);
        table.row();
        Image gold_6 = new Image(ServiceLocator.getResourceService()
                .getAsset("images/mpc/attires/gold_6.png", Texture.class));
        table.add(gold_6).padLeft(10f).padRight(10f).padTop(20f).size(220, 150);
        table.row();
    }

The following method calls the corresponding method to the number of gold achievements in order to render the attires screen.

/**
     * Utility function to render the given list of achievements and corresponding unlocked attires
     * @param goldAchievements number of gold achievements
     * @param alpha the opacity of each image (low for the ones which are locked)
     */
    private void renderUnlockedAttires(int goldAchievements, float alpha) {
        Label label2 = new Label("SELECT AN ATTIRE", new Label.LabelStyle(new BitmapFont(), Color.YELLOW));
        label2.setFontScale(2);
        table.center();
        table.add(label2).padTop(10f).padBottom(10f);
        table.row();

        if(goldAchievements < 2) {
            table.removeActor(label2);
            Label message1 = new Label("UNLOCK 1 MORE GOLD ACHIEVEMENT TO ACCESS NEW ATTIRES!",
                    new Label.LabelStyle(new BitmapFont(), Color.RED));
            message1.setFontScale(1.5f);
            table.add(message1).padTop(20f).center();
            table.row();

        }
        // Unlock 1 new attire
        if(goldAchievements == 2 || goldAchievements == 3) {
            lessThanFour(alpha);
        }
        // Unlock 2 new attires
        if(goldAchievements == 4 || goldAchievements == 5) {
            lessThanFour(alpha);
            lessThanSix(alpha);
        }
        // Unlock 3 new attires
        if(goldAchievements >= 6) {
            moreThanSix(alpha);

        }

    }

Method to render the original attire

 private void renderOriginalAttire() {

        ImageButton attireImg = getImageButton("images/mpc/attires/original.png");
        attireImg.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, Actor actor) {
                attireType = "OG";
                MPCConfig.updateAttire(attireType);
                confirmSelection("ORIGINAL", "original");

            }
        });
        unlockedAttiresTable.add(attireImg).left().padLeft(10f).padRight(10f).size(220, 150);
        unlockedAttiresTable.center();
        unlockedAttiresTable.row();
        
    }

Trigger to update and store user's selected attire in mpc.json

The snippet below is used by the methods that follow it, to trigger on button click and update the mpc.json file with the attire selected by the user.

attireImg.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, Actor actor) {
                attireType = "gold_2";
                MPCConfig.updateAttire(attireType);
                confirmSelection("GOLD_2", "gold_2");

            }
        });

Method to render the attires screen when the user has less than four gold achievements.

private void lessThanFour(float alpha) {
        renderOriginalAttire();
        Image achievementImg = new Image(ServiceLocator.getResourceService()
                .getAsset("images/mpc/attires/trophies_2x.png", Texture.class));
        achievementImg.setScaling(Scaling.fit);
        achievementImg.setColor(255, 255, 255, alpha);
       ...
        unlockedAttiresTable.add(attireImg).left().padTop(10f).padLeft(10f).padRight(10f).size(220, 150);
        unlockedAttiresTable.add(achievementImg).right().padTop(10f).padLeft(10f).padRight(10f).size(220, 150);

        if(goldAchievements == 3) {
            Label message1 = new Label("UNLOCK 1 MORE GOLD ACHIEVEMENT TO ACCESS A NEW ATTIRE!",
                    new Label.LabelStyle(new BitmapFont(), Color.RED));
            message1.setFontScale(1.5f);
            unlockedAttiresTable.row();
            unlockedAttiresTable.add(message1).padLeft(10f).padRight(10f).size(120, 50);
            unlockedAttiresTable.row();
        }
    }

Method to render the attires screen when the user has less than six gold achievements.

    private void lessThanSix(float alpha) {
        Image achievementImg = new Image(ServiceLocator.getResourceService()
                .getAsset("images/mpc/attires/trophies_4x.png", Texture.class));
        achievementImg.setScaling(Scaling.fit);
        achievementImg.setColor(255, 255, 255, alpha);
...
        ImageButton attireImg = getImageButton("images/mpc/attires/gold_4.png");

Method to render the attires screen when the user has more than six gold achievements.

    private void moreThanSix(float alpha) {
        Label message1 = new Label("YOU HAVE UNLOCKED ALL ATTIRES FOR NOW!",
                new Label.LabelStyle(new BitmapFont(), Color.YELLOW));
        message1.setFontScale(2f);
        unlockedAttiresTable.add(message1).padTop(20f).padBottom(20f).center();
        unlockedAttiresTable.row();
        unlockedAttiresTable.center();
        lessThanFour(alpha);
        lessThanSix(alpha);

        Image achievementImg = new Image(ServiceLocator.getResourceService()
                .getAsset("images/mpc/attires/trophies_6x.png", Texture.class));
        achievementImg.setScaling(Scaling.fit);
        achievementImg.setColor(255, 255, 255, alpha);

        ImageButton attireImg = getImageButton("images/mpc/attires/gold_6.png");
...       

User selection of attires

Once the user selects an attire on the attires screen, a pop up confirmation is presented to confirm the selection.

private void confirmSelection (String attireType, String attirePath) {

        dialog = new Dialog("YOU HAVE SELECTED THE " + attireType + " ATTIRE!", skin);
        dialog.setModal(true);
        dialog.setMovable(false);
        dialog.setResizable(true);

        dialog.pad(50).padTop(120);
        Image attire = new Image(new Texture("images/mpc/attires/" + attirePath + ".png"));
        Label heading = new Label("ATTIRE SELECTED!", new Label.LabelStyle(new BitmapFont(), Color.BLACK));
        heading.setFontScale(2f);
        dialog.getContentTable().add(heading).expandX().row();
        dialog.getContentTable().add(attire);
        dialog.getButtonTable().add(renderCloseButton()).size(50, 50).row();

        dialog.show(stage);
    }

Persisting user attire selection data in JSON file

The user's selected attire is stored in the mpc.json file locally in the home directory of the user.

{
attire: gold_6
}

The format of the JSON file and the properties are defined in the PlayerConfig.java file

/**
 * Defines the properties stored in player config files to be loaded by the Player Factory.
 */
public class PlayerConfig extends BaseEntityConfig  {
  public int gold = 1;
  public String favouriteColour = "none";
  public String attire = "OG";
}

The class below is used to read and write to the JSON file, and is called every time the file needs to be updated wit ha new value.

public class MPCConfig {

    private static final String ROOT_DIR = "DECO2800Game";
    private static final String CONFIG_FILE = "mpc.json";
    private static final String path = ROOT_DIR + File.separator + CONFIG_FILE;

    /**
     * Stores the current values into a JSON file
     */
    public void readValues() {
        PlayerConfig values = getValues();
    }

    public static void updateAttire(String attireType) {
        PlayerConfig values = getValues();
        values.attire = attireType;

        updateValues(values);
    }
    public static void updateValues(PlayerConfig values) {

        FileLoader.writeClass(values, path, EXTERNAL);
    }
    public static void updateValues() {
        PlayerConfig values = getValues();
        if(values.attire == null){
            values.attire = "OG";
        }
        updateValues(values);

    }
    public static PlayerConfig getValues() {
        PlayerConfig values =  FileLoader.readClass(PlayerConfig.class, path, EXTERNAL);
        return values != null ? values : new PlayerConfig();

    }
}

Dynamically Loading New Attire Atlases

Once the user finishes selecting an attire and starts the game, the code below dynamically retrieves the attire property and loads the required textures and animation atlases for the selected attire. This form of dynamic loading reduces overhead during the start of the game.

private static final PlayerConfig stats =
            FileLoader.readClass(PlayerConfig.class, "configs/player.json");
String attire = updateAttireConfig();
AnimationRenderComponent mpcAnimator;
        TextureRenderComponent mpcTexture;
        System.out.println("Loading attire: "+ attire);
        switch (attire) {

            case "gold_2":
                mpcAnimator = createAnimationComponent("images/mpc/finalAtlas/gold_2/mpcAnimation_2.atlas");
                mpcTexture = new TextureRenderComponent("images/mpc/finalAtlas/gold_2/mpc_right.png");
                break;
            case "gold_4":
                mpcAnimator = createAnimationComponent("images/mpc/finalAtlas/gold_4_buff_to_be_test/mpcAnimation_4.atlas");
                mpcTexture = new TextureRenderComponent("images/mpc/finalAtlas/gold_4_buff_to_be_test/mpc_right.png");
                break;
            case "gold_6":
                mpcAnimator = createAnimationComponent("images/mpc/finalAtlas/gold_6_buff_to_be_tested/mpcAnimation_6.atlas");
                mpcTexture = new TextureRenderComponent("images/mpc/finalAtlas/gold_6_buff_to_be_tested/mpc_right.png");
                break;
            case "OG":
            default:
                mpcAnimator = createAnimationComponent("images/mpc/finalAtlas/OG_buff_to_be_tested/mpcAnimation.atlas");
                mpcTexture = new TextureRenderComponent("images/mpc/finalAtlas/OG_buff_to_be_tested/mpc_right.png");
                break;
        }

Each corresponding animation in every attire's animation atlas have the same name and need to be statically added to the player entity only once, irrespective of the attire chosen.

Texture Atlas

Buff/Debuff Atlas

For the different attires, we have the same atlas in terms of creating buffs/ debuffs animation and was generated using a texture packer. The atlas has player buff representations for all the movements (explained in Sprint 2 Documentation )in the following states:

  1. Dizzy (facing right, animated)
  2. Hungry (facing right, animated)
  3. Poisoned (facing right, animated)
  4. Health Down (facing right, animated)
  5. Speed Down (facing right, animated)
  6. Thirsty (facing right, animated)
  7. Recovered (facing right, animated)
  8. Health UP (facing right, animated)
  9. Health Limit Up (facing right, animated)

Atlas Files

For OG Attire : mpcAnimation.atlas

For Gold_4 Attire : mpcAnimation_4.atlas

For Gold_6 Attire : mpcAnimation_6.atlas

Generated Texture pack

The bellow image file shows the atlas image for Gold_6 attire.

mpcAnimation.png mpcAnimation.png

Testing

Considering it is hard to test whether an animation plays because of its visual nature, unit tests were written to verify and validate that when an animation event is triggered the corresponding movement animation is triggered. The tests check that all the expected animations actually are rendered when their respective events are triggered.

PlayerAnimationRenderTest.java

@ExtendWith(GameExtension.class)
class PlayerAnimationRenderTest {

    private Entity player;
    private AnimationRenderComponent animator;
    @BeforeEach
    void beforeEach() {
        ServiceLocator.registerPhysicsService(new PhysicsService());
        player = new Entity()
                .addComponent(new PhysicsComponent())
                .addComponent(new PlayerAnimationController());

        animator = mock(AnimationRenderComponent.class);
        player.addComponent(animator);

        PlayerAnimationController animationController =
                player.getComponent(PlayerAnimationController.class);
        animationController.setTexturePresent(false);
        player.create();
    }


    @Test
    void shouldTriggerRightMovement() {
        player.getEvents().trigger("walkRight");
        verify(animator).startAnimation("main_player_run");
    }

    @Test
    void shouldTriggerWalkMovement() {
        player.getEvents().trigger("startMPCAnimation");
        verify(animator).startAnimation("main_player_walk");
    }

    @Test
    void shouldTriggerJumpMovement() {
        player.getEvents().trigger("jump");
        verify(animator).startAnimation("main_player_jump");
    }

    @Test
    void shouldTriggerCrouchMovement() {
        player.getEvents().trigger("crouch");
        verify(animator).startAnimation("main_player_crouch");
    }

    @Test
    void shouldTriggerItemPickUpMovement() {
        player.getEvents().trigger("itemPickUp");
        verify(animator).startAnimation("main_player_pickup");
    }

    @Test
    void shouldTriggerAttackMovement() {
        player.getEvents().trigger("attack");
        verify(animator).startAnimation("main_player_attack");
    }
}

UML Diagrams

Class Diagram

UnlockedAttiresDisplay

UnlockedAttiresScreen

PlayerAnimationController

AnimatorComponent

ItemComponent

MPCConfig

Sequence Diagrams

UnlockedAttiresScreen_createUI()

renderZeroUnlockedAttiresTable()

renderUnlockedAttires()

renderOriginalAttire()

UnlockedAttiresDisplay

confirmSelection()

MPCConfig_updateValues()

Relevant Files

UnlockedAttiresScreen.java

UnlockedAttiresDisplay.java

PlayerConfig.java

MPCConfig.java

PlayerFactory.java

PlayerAnimationController.java

PlayerAnimationRenderTest.java

ItemComponent.java

mpcAnimation.atlas

mpcAnimation_4.atlas

mpcAnimation_6.atlas

mpcAnimation.png

mpcAnimation_4.png

mpcAnimation_6.png