Sprint 3 Main Character New Unlockable Attires and Functionality improvement - UQdeco2800/2021-ext-studio-2 GitHub Wiki
Contents
- Automatic Item PickUp Implementation
- Buff Debuffs Animation Integration
- Unlockable Attires
- Texture Atlas
- Testing
- UML Diagrams
- Relevant Files
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.
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:
- Dizzy (facing right, animated)
- Hungry (facing right, animated)
- Poisoned (facing right, animated)
- Health Down (facing right, animated)
- Speed Down (facing right, animated)
- Thirsty (facing right, animated)
- Recovered (facing right, animated)
- Health UP (facing right, animated)
- 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.
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
PlayerAnimationController.java