-
Notifications
You must be signed in to change notification settings - Fork 0
Transition to Boss
The transition from regular game to boss functionality covers the event from triggering the spawn of the Final Boss (Kanga), the visual/audio effects during the boss chases, up to the point where both the player and enemy hitboxes collide triggering the boss cutscene which then go into combat with the boss.
In class ForestGameArea
, add a listener to detect the event triggering the spawn of Kanga Boss
// Add listener for event which will trigger the spawn of Kanga Boss
player.getEvents().addListener("spawnKangaBoss", this::spawnKangarooBoss);
// Initialize the flag when creating the area
kangarooBossSpawned = false;
which then calls the method to spawn the boss, with a flag to check whether it has spawned already to prevent multiple spawn of the boss
private void spawnKangarooBoss() {
if (!kangarooBossSpawned) {
// Create entity
Entity kangarooBoss = NPCFactory.createKangaBossEntity(player);
// Create in the world
spawnEntityOnMap(kangarooBoss);
// Set flag to true after Kanga Boss is spawned
kangarooBossSpawned = true;
}
}
Adding an attribute to specify if NPC entity in class ChaseTask
and WanderTask
(used in AITaskComponent
when creating an NPC)
private final boolean isBoss;
public ChaseTask(Entity target, int priority, float viewDistance, float maxChaseDistance, boolean isBoss) {
...
this.isBoss = isBoss;
}
The method to create a Boss NPC entity, where WanderTask
and ChaseTask
are attached with isBoss
as true
public static Entity createBossNPC(Entity target) {
AITaskComponent aiComponent =
new AITaskComponent()
.addTask(new WanderTask(new Vector2(2f, 2f), 2f, true))
.addTask(new ChaseTask(target, 10, 3f, 4f, true));
Entity npc =
new Entity()
.addComponent(new PhysicsComponent())
.addComponent(new PhysicsMovementComponent())
.addComponent(new ColliderComponent())
.addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC))
.addComponent(new TouchAttackComponent(PhysicsLayer.PLAYER, 1.5f))
.addComponent(aiComponent);
PhysicsUtils.setScaledCollider(npc, 0.9f, 0.4f);
return npc;
}
In method start()
and stop()
in ChaseTask
, add conditions to start/stop the effects once the chase started/stopped
public void start() {
...
if (this.isBoss) {
playTensionSound();
this.target.getEvents().trigger("startHealthBarBeating");
}
}
public void stop() {
...
if (this.isBoss) {
stopTensionSound();
this.target.getEvents().trigger("stopHealthBarBeating");
}
}
With these two methods to play/stop the tension (heart beating) sound
void playTensionSound() {
if (heartbeatSound == null && ServiceLocator.getResourceService() != null) {
heartbeatSound = ServiceLocator.getResourceService().getAsset(heartbeat, Music.class);
heartbeatSound.setLooping(true);
heartbeatSound.setVolume(1.0f);
}
if (heartbeatSound != null) {
ForestGameArea.pauseMusic();
heartbeatSound.play();
}
}
void stopTensionSound() {
if (heartbeatSound != null) {
ForestGameArea.playMusic();
heartbeatSound.stop();
}
}
In the PlayerStatsDisplay
class, add listeners to detect the triggers mentioned
// Add listener for kanga chase start/stop to trigger beating effect
entity.getEvents().addListener("startHealthBarBeating", this::startHealthBarBeating);
entity.getEvents().addListener("stopHealthBarBeating", this::stopHealthBarBeating);
Then calls the said methods
public void startHealthBarBeating() {
// Stop any existing beating actions
heartImage.clearActions();
vignetteImage.clearActions();
vignetteImage.setVisible(true);
heartImage.addAction(Actions.forever(
Actions.sequence(
Actions.scaleTo(1.0f, 1.05f, 0.3f), // Slightly enlarge
Actions.scaleTo(1.0f, 0.95f, 0.3f) // Return to normal size
)
));
vignetteImage.addAction(Actions.forever(
Actions.sequence(
Actions.fadeIn(0.3f), // Fade in for vignette effect
Actions.fadeOut(0.3f) // Fade out for vignette effect
)
));
}
public void stopHealthBarBeating() {
heartImage.clearActions();
vignetteImage.clearActions();
vignetteImage.setVisible(false); // Hide vignette when not beating
heartImage.setScale(1.0f); // Reset to normal scale
}
In PlayerActions
class, add a listener to listen to the trigger to start combat
entity.getEvents().addListener("startCombat", this::startCombat);
which calls this method which will determine whether to go into a cutscene first before the actual combat if that enemy is a boss
public void startCombat(Entity enemy){
AITaskComponent aiTaskComponent = enemy.getComponent(AITaskComponent.class);
PriorityTask currentTask = aiTaskComponent.getCurrentTask();
if ((currentTask instanceof WanderTask && ((WanderTask) currentTask).isBoss() ||
(currentTask instanceof ChaseTask && ((ChaseTask) currentTask).isBoss()))) {
currentTask.stop();
game.addBossCutsceneScreen(enemy);
} else {
game.addCombatScreen(enemy);
}
}
if addBossCutsceneScreen(enemy)
is called, the screen will go transitioned into the screen from BossCutsceneScreen
class, which after 3 seconds will transition into to the combat screen.
public class BossCutsceneScreen extends ScreenAdapter {
private static final float CUTSCENE_DURATION = 3.0f; // Cutscene lasts for 3 seconds
private float timeElapsed = 0;
private boolean transition;
...
public void render(float delta) {
if (!isPaused) {
physicsEngine.update();
ServiceLocator.getEntityService().update();
renderer.render();
timeElapsed += delta;
if (timeElapsed >= CUTSCENE_DURATION && !transition) {
transition = true;
logger.info("Cutscene finished, transitioning to combat screen");
// dispose();
game.setScreen(new CombatScreen(game, oldScreen, oldScreenServices, enemy));
}
}
}
private void createUI() {
// animate the cutscene
}
...
}
The cutscene shows an animation of the Boss NPC sprite, with its name sliding from the sides of the screen.
Testing ensures that the functionalities being developed work as expected, which roughly includes:
- Triggering the boss spawn correctly
- Starting/Stopping audio/visual effects at the right time
- Transitioning into cutscene and combat works correctly
The approaches for testing are by using JUnit tests and also by inspection, which is done casually to manually review and debug the code.
Video testing: https://www.youtube.com/watch?v=9uUHNTqCTns