Skip to content

Instantly share code, notes, and snippets.

@RobinL
Created March 30, 2025 15:24
Show Gist options
  • Save RobinL/4f86a014af7d42b5dcade854463535a2 to your computer and use it in GitHub Desktop.
Save RobinL/4f86a014af7d42b5dcade854463535a2 to your computer and use it in GitHub Desktop.
Phaser instructions html form

Okay, here are specific and precise instructions, with code snippets, outlining the general pattern for integrating an HTML form with a Phaser 3 game, managing keyboard focus correctly.

This pattern ensures:

  1. Keyboard input intended for the HTML form (typing, arrow keys for navigation/value changes) works correctly when an input is focused.
  2. Phaser does not receive or react to keyboard input while the form is being edited.
  3. Submitting the form (via Enter key or clicking an update button) updates the Phaser game.
  4. Keyboard focus and control return to the Phaser game after submission.

General Pattern: HTML Form Interaction with Phaser 3

Scenario: You have an HTML <form> with input fields (e.g., <input type="number">, <input type="text">) and a <button type="submit">. You want users to be able to edit these inputs without triggering game actions (like player movement via arrow keys). When the form is submitted, the values should update properties in your active Phaser scene, and keyboard control should return to the game.

Ingredients:

  1. HTML: A <form> element containing your inputs and a submit button.
  2. Phaser Scene: Your game scene(s) that handle keyboard input and have public methods to accept updates from the form.
  3. JavaScript (main.js / glue code): Code to:
    • Initialize Phaser.
    • Get references to the HTML form and the active Phaser scene.
    • Implement focus listeners (focusin, focusout) on the form using event delegation.
    • Implement a submit listener (submit) on the form.
    • Manage a flag to track input focus state.
    • Toggle Phaser's keyboard enabled state and globalCapture based on focus.
    • Call scene methods upon form submission.
    • Blur the active input element upon submission to return control.

Step-by-Step Implementation:

Step 1: HTML Structure

Define your form in index.html. Use a <form> tag, give it an ID, include your inputs (with IDs), and a button with type="submit".

<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Phaser Form Interaction</title>
</head>
<body>
    <h1>Game Controls</h1>

    <!-- The Form -->
    <form id="game-controls-form">
        <div>
            <label for="setting1-input">Setting 1:</label>
            <input type="number" id="setting1-input" value="100" min="10" max="500">
        </div>
        <div>
            <label for="setting2-input">Setting 2:</label>
            <input type="text" id="setting2-input" value="Default Text">
        </div>
        <div>
            <!-- Button MUST be type="submit" to trigger form submission on Enter -->
            <button type="submit" id="update-game-button">Update Game</button>
        </div>
    </form>

    <!-- Phaser Game Container -->
    <div id="phaser-game-container"></div>

    <script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.min.js"></script>
    <script type="module" src="/src/main.js"></script>
</body>
</html>

Step 2: Phaser Scene Setup

In your Phaser scene (GameScene.js or similar), ensure you have:

  • Keyboard input listeners set up (e.g., createCursorKeys).
  • Public methods that can be called from your main JavaScript file to update game state.
// src/GameScene.js (Example Scene Structure)
import Phaser from 'phaser';

export class GameScene extends Phaser.Scene {
    constructor() {
        super({ key: 'GameScene' });
        this.player = null;
        this.cursors = null;
        this.gameSpeed = 100; // Example property to be updated
        this.playerName = 'Default Text'; // Example property
    }

    create() {
        // Example player sprite
        // this.player = this.add.rectangle(100, 100, 50, 50, 0xffffff);

        // Enable keyboard input FOR THE GAME
        this.cursors = this.input.keyboard.createCursorKeys();

        console.log("GameScene Created. Initial Speed:", this.gameSpeed);
    }

    update(time, delta) {
        // Game logic using keyboard input
        // IMPORTANT: This logic only runs when scene.input.keyboard.enabled is true
        if (this.cursors.left.isDown) {
            // Move player left, etc.
            // console.log("Left arrow pressed in game");
        }
        // ... other game input logic
    }

    // --- PUBLIC METHODS for HTML Interaction ---
    // These methods are called FROM main.js

    setGameSpeed(newSpeed) {
        // Add validation/clamping as needed
        this.gameSpeed = Phaser.Math.Clamp(newSpeed, 10, 500);
        console.log("GameScene: Speed updated to", this.gameSpeed);
        // Potentially apply the speed to game objects here
    }

    setPlayerName(newName) {
        this.playerName = newName || 'Default Text'; // Basic validation
        console.log("GameScene: Player name updated to", this.playerName);
        // Update any text objects displaying the name, etc.
    }
}

Step 3: JavaScript Glue Code (main.js)

This is where the core logic for focus management and communication resides.

// src/main.js
import Phaser from 'phaser';
import { GameScene } from './GameScene.js'; // Import your scene

// --- Phaser Game Configuration & Initialization ---
const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-game-container',
    scene: [GameScene], // Add your scene(s) here
    // Optional: Make canvas background transparent or set a color
    // backgroundColor: '#1a1a1a',
};

// Initialize the game
const game = new Phaser.Game(config);

// --- Helper to Get Active Scene ---
// Ensures we always target the correct, running scene instance
function getActiveGameScene() {
    // Replace 'GameScene' with the actual key you used in the scene constructor
    const scene = game.scene.getScene('GameScene');
    if (scene && scene.scene.isActive()) {
        return scene;
    }
    console.warn("Could not get active GameScene.");
    return null;
}

// --- Global Flag & Selector ---
let isAnyInputFocused = false;
// Selector for input elements that should pause the game's keyboard input
const formInputSelector = 'input[type="text"], input[type="number"], textarea, [contenteditable="true"]';

// --- Get HTML Form Reference ---
const gameForm = document.getElementById('game-controls-form');

if (gameForm) {

    // --- 1. FOCUS MANAGEMENT (Event Delegation on the Form) ---

    gameForm.addEventListener('focusin', (event) => {
        // Check if the focused element is one of our designated inputs
        if (event.target.matches(formInputSelector)) {
            // console.log('Focus IN:', event.target.id);
            isAnyInputFocused = true; // Mark that an input is active
            const currentScene = getActiveGameScene();
            if (currentScene?.input?.keyboard) {
                // Disable Phaser's keyboard processing for the scene
                currentScene.input.keyboard.enabled = false;
                // IMPORTANT: Stop Phaser from preventing default browser actions
                // for keys (like arrows moving cursor in input)
                currentScene.input.keyboard.disableGlobalCapture();
                // console.log('Phaser keyboard DISABLED');
            }
        }
    });

    gameForm.addEventListener('focusout', (event) => {
        if (event.target.matches(formInputSelector)) {
            // console.log('Focus OUT:', event.target.id);
            isAnyInputFocused = false; // Mark that this input lost focus

            // Use a tiny delay (setTimeout 0) to allow focus to shift
            // to another input (e.g., via Tab key) before re-enabling.
            setTimeout(() => {
                // Only re-enable if NO OTHER designated input has gained focus
                if (!isAnyInputFocused) {
                    const currentScene = getActiveGameScene();
                    if (currentScene?.input?.keyboard) {
                        // Restore Phaser's ability to capture keys (prevent default)
                        currentScene.input.keyboard.enableGlobalCapture();
                        // Re-enable Phaser's keyboard processing
                        currentScene.input.keyboard.enabled = true;
                        // console.log('Phaser keyboard ENABLED');
                    }
                } else {
                    // console.log('Skipping re-enable, another input focused.');
                }
            }, 0); // 0ms timeout defers execution slightly
        }
    });

    // --- 2. FORM SUBMISSION HANDLING ---

    gameForm.addEventListener('submit', (event) => {
        event.preventDefault(); // VERY IMPORTANT: Prevent page reload!
        // console.log('Form submitted');

        const currentScene = getActiveGameScene();
        if (!currentScene) {
            console.error("Cannot update game, scene not found or inactive.");
            return;
        }

        // --- Read values from inputs ---
        const setting1Input = document.getElementById('setting1-input');
        const setting2Input = document.getElementById('setting2-input');

        // Read and potentially validate/sanitize values
        const setting1Value = parseInt(setting1Input.value, 10) || 0; // Example fallback
        const setting2Value = setting2Input.value || '';

        // --- Call public methods on the Phaser scene ---
        currentScene.setGameSpeed(setting1Value); // Use the actual method name
        currentScene.setPlayerName(setting2Value); // Use the actual method name

        // --- Return focus/control to the game ---
        // Blur the element that triggered the submit (could be input via Enter, or the button)
        // This implicitly returns focus to the document body, allowing Phaser to capture keys again.
        if (document.activeElement && document.activeElement.matches(formInputSelector + ', button[type="submit"]')) {
             // console.log('Blurring active element:', document.activeElement.id);
             document.activeElement.blur();
        }

        // Blurring should trigger the 'focusout' handler, which re-enables Phaser keyboard.
    });

} else {
    console.error("Game controls form not found!");
}

// --- Optional: Click on Game Canvas to Return Focus ---
const gameContainer = document.getElementById('phaser-game-container');
if (gameContainer) {
    gameContainer.addEventListener('click', (event) => {
        // Check if click is on the container or the canvas itself
        if (event.target === gameContainer || event.target.tagName === 'CANVAS') {
             // If an input managed by our form is currently focused, blur it.
             if (document.activeElement && document.activeElement.matches(formInputSelector)) {
                 // console.log('Canvas clicked, blurring input:', document.activeElement.id);
                 document.activeElement.blur();
             }
        }
    });
}

Explanation of Key Parts:

  1. focusin Listener: When any element inside the form receives focus, this listener checks if it matches our formInputSelector. If yes, it immediately disables Phaser's keyboard processing (enabled = false) and, crucially, tells Phaser to stop intercepting keys (disableGlobalCapture()). This allows arrow keys, letters, etc., to behave normally within the focused input field.
  2. focusout Listener: When an element loses focus, this listener also checks if it was one of our managed inputs. It sets isAnyInputFocused to false. The setTimeout(..., 0) is a small trick: it defers the re-enabling check slightly. This handles the case where the user Tabs directly from one input to another. Without the delay, the keyboard might re-enable momentarily between the focusout of the first input and the focusin of the second. Inside the timeout, it checks isAnyInputFocused again. If it's still false (meaning focus didn't immediately go to another managed input), it re-enables Phaser's keyboard (enableGlobalCapture(), enabled = true).
  3. submit Listener: Attached to the form itself, this catches submissions triggered by pressing Enter in an input or clicking the type="submit" button.
    • event.preventDefault(): Stops the browser's default form submission behavior (which is usually a page reload).
    • Gets the active scene.
    • Reads values from the relevant input elements. Remember to add validation/sanitization.
    • Calls the public methods you defined on your Phaser scene (currentScene.setGameSpeed(...), etc.) to pass the data.
    • document.activeElement.blur(): This is the key to returning control. By removing focus from the form element, focus typically returns to the <body> or the game canvas, allowing the now re-enabled Phaser keyboard listeners to take over.
  4. Public Scene Methods: Defining clear methods like setGameSpeed on your scene creates a clean interface for the HTML/JavaScript side to interact with the game logic without needing direct access to internal scene variables.
  5. isAnyInputFocused Flag: This simple boolean tracks whether any of the designated inputs currently hold focus, ensuring the keyboard is only re-enabled when all relevant inputs are blurred.
  6. formInputSelector: Centralizes the definition of which elements should trigger the focus management logic. Easily extendable.

This pattern provides a robust and maintainable way to handle interactions between standard HTML forms and your Phaser 3 game, correctly managing keyboard focus throughout the process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment