Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Visual Combat Feedback

Hello! Welcome back to our JRPG development course.

In our last session, we built the scaffolding for creating spectacular skill effects. By designing a data-driven AnimationPlayer and an ActionSequence interpreter, we created a system to choreograph character movements and sprite animations. However, a silent animation doesn't have the "punch" we're looking for. To make combat truly satisfying, players need clear, immediate feedback.

Today, we'll implement that feedback, directly addressing the learning outcome: Implement visual feedback for combat, such as damage pop-ups and sprite flashing. We'll add new commands to our action sequence system to make targets flash when they're hit and display scrolling numbers to show damage, healing, and other effects. These small details are what separate a functional battle system from a fun one.

Take a look at this combat clip. This is the quality of feedback we're aiming for.

JRPG Combat Visual Feedback: Damage Pop-up and Sprite Flashing
In this clip, notice two key things when the attack lands: the enemy sprite flashes white, and the number '18' pops up to show the damage dealt. These two simple effects provide instant, satisfying feedback.

1. The Hit Flash: Acknowledging Impact

The first piece of feedback we'll implement is the "hit flash." It's a classic JRPG trope for a reason: it's a simple, unambiguous signal that a character has taken damage.

Architecturally, this is a temporary state change on the battler object. For a brief moment, we want to override its normal drawing routine.

Implementation Strategy

  1. Add State to the Battler: In your Battler class (or whatever you've named the class representing a character/enemy in combat), add properties to manage the flash state:

    class Battler {
        constructor(/*...*/) {
            // ... existing properties
            this.isFlashing = false;
            this.flashDuration = 150; // ms
            this.flashTimer = 0;
        }
        // ...
    }
    
  2. Trigger the Flash: Your ActionSequence interpreter executes an APPLY_EFFECT command to deal damage. This is the perfect place to trigger the flash. The command should find the target Battler and activate its flash state.

    // Inside your action sequence interpreter, when processing APPLY_EFFECT...
    const target = findTarget(/*...*/);
    target.applyDamage(damageAmount);
    target.startFlash(); // A new method on the Battler class
    
    // In Battler.js
    startFlash() {
        this.isFlashing = true;
        this.flashTimer = this.flashDuration;
    }
    
  3. Update the Timer: In your Battler's update(deltaTime) method, you need to count down the timer.

    update(deltaTime) {
        // ... other update logic
        if (this.isFlashing) {
            this.flashTimer -= deltaTime;
            if (this.flashTimer <= 0) {
                this.isFlashing = false;
            }
        }
    }
    
  4. Modify the Draw Call: Finally, in your Battler's draw(context) method, check the isFlashing flag. If it's true, render the flash effect. A common and simple approach is to overlay a solid white color.

    draw(context) {
        // ... calculate x, y position
        if (this.isFlashing) {
            // Option 1: Simple white rectangle (a bit crude)
            // context.fillStyle = 'white';
            // context.fillRect(this.x, this.y, this.width, this.height);
    
            // Option 2: Tint the sprite white (better)
            // First, draw the sprite normally
            this.sprite.draw(context, this.x, this.y);
            
            // Then, use a composite operation to overlay a color
            context.globalCompositeOperation = 'source-atop';
            context.fillStyle = 'rgba(255, 255, 255, 0.75)'; // Semi-transparent white
            context.fillRect(this.x, this.y, this.width, this.height);
            
            // IMPORTANT: Reset the composite operation
            context.globalCompositeOperation = 'source-over';
        } else {
            this.sprite.draw(context, this.x, this.y);
        }
    }
    

This same technique can be used for other color flashes, like green for healing or red for taking fire damage. As you can see in RPG Maker tools, this is often a dedicated command.

RPG Maker MV Tutorial - Free Action Sequence - Healstrike Skill - Deal damage and heal the party

Recall the 'Healstrike' skill from our last lesson. Notice how the action sequence has a specific command to make the target flash.

Watch the clip from 08:15 to 08:25. The creator points out the command enemy effect: Target, whiten. This demonstrates how flashing is treated as a distinct event within the overall skill sequence.

2. Damage Pop-ups: The Numbers Game

While flashing confirms a hit, damage pop-ups provide crucial quantitative information. They tell the player if their new sword is effective, if they hit a weakness, or if they landed a critical hit.

Like our AnimationPlayer, these pop-ups are temporary, animated entities. This calls for a similar architectural pattern: a DamagePopup class to define the object and a manager within the BattleScene to update and draw active instances.

The following video, while using Unity, provides a superb conceptual walkthrough for building a polished pop-up system from scratch. The principles are directly applicable to our JavaScript/Canvas engine.

POLISH your Game with Damage Popups! (Unity Tutorial)

Code Monkey's 'POLISH your Game with Damage Popups!' is an excellent guide. We'll focus on the core mechanics: creating a text object, animating its movement and fade, and styling it for different situations.

Watch these three segments to understand the lifecycle of a damage pop-up: Instantiation and Setup (01:38 - 04:26): Focus on the concept of a setup function that takes the damage value and applies it to a text object. In our case, this will be a call to context.fillText(). Movement and Fading (07:46 - 11:54): This is the core logic. Pay attention to how update is used to change the pop-up's position and alpha over time until it disappears and is destroyed. Critical Hit Visuals (11:54 - 14:05): See how passing a simple boolean (isCriticalHit) into the creation function allows for different styling (bigger font, different color). This makes the feedback much more dynamic.

Implementation Sketch in JavaScript

Let's translate the concepts from the video into a JavaScript structure for our engine.

The DamagePopup Class:
This object is a short-lived particle. It manages its own position, appearance, and lifetime.

const POPUP_LIFETIME = 1200; // 1.2 seconds

class DamagePopup {
    constructor(text, x, y, options = {}) {
        this.text = text;
        this.x = x;
        this.y = y;

        // Options with defaults
        this.color = options.color || 'white';
        this.fontSize = options.isCritical ? 32 : 24;
        
        // Animation properties
        this.lifetime = POPUP_LIFETIME;
        this.yVelocity = -40; // Pixels per second
        this.alpha = 1.0;
    }

    update(deltaTime) {
        this.lifetime -= deltaTime;
        
        // Move upwards
        this.y += this.yVelocity * (deltaTime / 1000);
        
        // Fade out in the last part of its life
        if (this.lifetime < POPUP_LIFETIME / 2) {
            this.alpha = (this.lifetime / (POPUP_LIFETIME / 2));
        }
    }

    draw(context) {
        context.save(); // Save the current context state
        
        context.globalAlpha = this.alpha;
        context.fillStyle = this.color;
        context.font = `bold ${this.fontSize}px 'Arial'`; // Choose a fitting font
        context.textAlign = 'center';
        context.fillText(this.text, this.x, this.y);
        
        context.restore(); // Restore context state
    }
    
    isFinished() {
        return this.lifetime <= 0;
    }
}

The BattleScene Manager:
Your battle scene will manage a list of these pop-ups, just like it manages active skill animations.

class BattleScene {
    constructor() {
        // ...
        this.damagePopups = [];
    }
    
    // Called by the Action Sequence interpreter
    createDamagePopup(text, x, y, options) {
        this.damagePopups.push(new DamagePopup(text, x, y, options));
    }
    
    update(deltaTime) {
        // ...
        // Update all popups
        this.damagePopups.forEach(popup => popup.update(deltaTime));
        
        // Remove finished popups
        this.damagePopups = this.damagePopups.filter(popup => !popup.isFinished());
    }

    draw() {
        // ...
        // Draw all popups
        this.damagePopups.forEach(popup => popup.draw(this.context));
    }
}

By adding a call to battleScene.createDamagePopup() within your APPLY_EFFECT command, you'll have numbers flying with every hit! You can create different colors for HP damage (white), HP recovery (green), MP damage (purple), and critical hits (yellow or red).

Test your understanding!

You want to create a "Miss" pop-up for attacks that fail to hit. How would you implement this using the DamagePopup system? Where in the game logic would you trigger its creation?

Show answer
  1. Implementation: You would call battleScene.createDamagePopup("Miss", target.x, target.y, { color: 'lightblue' }). You could create a specific style for misses (e.g., a different font or color).
  2. Trigger Location: The check for whether an attack hits or misses should happen before damage is calculated, likely in the initial part of your APPLY_EFFECT command or even before it. If the hit check fails, you would create the "Miss" pop-up and terminate the action sequence for that attack early, skipping the damage calculation and hit flash.

3. Polish & Accessibility: The Extra Mile

Your front-end development background has surely taught you the importance of user settings and accessibility. Intense visual effects like flashing can be problematic for some players. While we won't build a full options menu today, it's important to architect our systems with this in mind.

A great practice is to make your effects conditional based on a global configuration.

// A global config object, potentially loaded from localStorage
const GameConfig = {
    effectIntensity: 'high' // could be 'low' or 'off'
};

// In your Battler's draw method
if (this.isFlashing && GameConfig.effectIntensity === 'high') {
    // draw the flash
} else {
    // draw normally
}

This simple check allows you to tone down or disable effects based on user preference. The article below discusses how a developer added a "Reduced Effect Intensity" option to their RPG Maker game, showing the practical importance of this concept.

[How-to] Adding a 'Reduced Effect Intensity' option to an ...

This devlog from Tanuki-Sama Studios, '[How-to] Adding a 'Reduced Effect Intensity' option...', details their process for making their game more accessible. It's a great real-world example of why we should think about these features.

Read the sections 'Screen Flashes & Shakes' and 'Bright VFX in battles'. Note how they use a global switch (the equivalent of our GameConfig flag) to conditionally change or disable intense effects like screen flashes and bright animations.

Conclusion

Congratulations! You've now implemented the core visual feedback that makes JRPG combat so satisfying. By layering these effects into our action sequence system, we're getting closer to a polished and professional-feeling battle experience.

Key Takeaways:

  • Visual feedback like sprite flashing and damage pop-ups are crucial for game "feel" and communicating information to the player.
  • Sprite flashing is best implemented as a temporary state on the Battler object, managed within its update and draw cycles.
  • Damage pop-ups are ephemeral, animated entities. They require a dedicated DamagePopup class and a manager in the main scene to control their lifecycle, similar to how we handle skill animations.
  • Good architecture considers configurability. Building in checks for user preferences, like an effect intensity setting, makes your game more accessible and robust.

Preview of the next lesson:
This lesson concludes our deep dive into the "juice" of the combat system. With actions, AI, animations, and feedback in place, we're ready to zoom out. In our next lesson, we'll kick off a new module, Character Progression and Inventory. We'll tackle the very heart of the RPG loop by designing and implementing an experience and leveling system. After all, the reward for winning these flashy battles is getting stronger for the next one

Can't find a good explanation? Sign up and we'll make it for you

Sign up