-
Notifications
You must be signed in to change notification settings - Fork 4
Sprint 3 Main Character New Unlockable Attires and Functionality improvement
- Automatic Item PickUp Implementation
- Buff Debuffs Animation Integration
- Unlockable Attires
- Texture Atlas
- Testing
- UML Diagrams
- Relevant Files
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);
}
}
}
As the player Jason interacts progress in the game, it will experience buffs and debuffs from interacting with the obstacles and items.
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;
}
}
...
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.
A new screen for selecting unlocked attires was created and the Select Unlocked Attires Button was included in the game main menu.
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();
}
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 |
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();
}
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");
...
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);
}
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();
}
}
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.
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)
For OG Attire : mpcAnimation.atlas
For Gold_4 Attire : mpcAnimation_4.atlas
For Gold_6 Attire : mpcAnimation_6.atlas
The bellow image file shows the atlas image for Gold_6 attire.
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");
}
}
UnlockedAttiresDisplay
UnlockedAttiresScreen
PlayerAnimationController
AnimatorComponent
ItemComponent
MPCConfig
UnlockedAttiresScreen_createUI()
renderZeroUnlockedAttiresTable()
renderUnlockedAttires()
renderOriginalAttire()
UnlockedAttiresDisplay
confirmSelection()
MPCConfig_updateValues()
PlayerAnimationController.java
Camera Angle and The Player's Perspective
Achievements Trophies and Cards
πΎ Obstacle/Enemy
βMonster Manual
βObstacles/Enemies
ββ- Alien Plants
ββ- Variation thorns
ββ- Falling Meteorites
ββ- FaceHugger
ββ- AlienMonkey
βSpaceship & Map Entry
βParticle effect
[code for debuff animations](code for debuff animations)
Main Character Movement, Interactions and Animations - Code Guidelines
ItemBar & Recycle system
πΎ Obstacle/Enemy
βObstacle/Enemy
βMonster Manual
βSpaceship Boss
βParticle effects
βOther Related Code
βUML & Sequence diagram of enemies/obstacles
Scoring System Implementation Explanation
Buff and Debuff Implementation
Infinite generating terrains Implementation Explanation
Game Over Screen and functions explaination
Buffer timer before game start
Rocks and woods layout optimization
Magma and nails code implementation
Guide: Adding Background music for a particular screen
History Scoreboard - Score Details
Listening for important events in the Achievements ecosystem
Hunger and Thirst icon code guidelines
Hunger and Thirst User Testing
Buff and Debuff Manual User Testing
The New Button User Test in Setting Page
The Main Menu Buttons User Testing
Infinite loop game and Terrain Testing
https://github.com/UQdeco2800/2021-ext-studio-2.wiki.git
πΎ Obstacle/Enemy
βObstacle testing
ββ- Alien Plants & Variation Thorns
ββ- Falling Meteorites
βEnemy testing
ββ- Alien Monkeys & Facehugger
ββ- Spaceship Boss
βMonster Manual
βParticle-effect
βPlayer attack testing
ββ- Player Attack
Sprint 1
Sprint 2
Sprint 3
Sprint 4
Changeable background & Buffer time testing
Game over screen test sprint 4
New terrain textures on bonus map test sprint 4
Achievements System, Game Records and Unlockable Chapters
Musics Implementation Testing plan