How to Build a JavaScript Shooter Game with Phaser 3: Complete Tutorial

Share me please

Creating browser-based games has become increasingly popular among developers looking to enhance their JavaScript skills while building engaging interactive experiences. This comprehensive tutorial will guide you through developing a complete top-down shooter game using Phaser 3, one of the most robust HTML5 game development frameworks available today.

What You’ll Learn and Build

Throughout this tutorial, you’ll create a fully functional 10-level space shooter where players control a ship, battle waves of UFOs, and progress through increasingly challenging levels. The game features continuous spread-fire mechanics, dynamic difficulty scaling, and smooth physics-based movement.

The final product will demonstrate key game development concepts including sprite management, collision detection, audio integration, and progressive difficulty systems. By the end of this tutorial, you’ll have a solid foundation for creating more complex browser games and a deeper understanding of Phaser 3’s capabilities.

Complete Game Code Overview

Before we dive into the detailed explanations, let’s look at the complete code structure that we’ll be building throughout this tutorial. This gives you a roadmap of what we’re creating and how all the pieces fit together:

html<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Phaser Shooter with Spread Fire</title>
    <script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.js"></script>
    <style>body { margin: 0; overflow: hidden; }</style>
</head>
<body>
    <script>
        const config = {
            type: Phaser.AUTO,
            width: 800,
            height: 600,
            physics: { default: 'arcade', arcade: { debug: false } },
            scene: { preload, create, update }
        };

        const TOTAL_LEVELS = 10;
        const ENEMIES_PER_LEVEL = 10;
        let player, cursors, bullets, enemies;
        let score = 0, level = 1;
        let scoreText, levelText, graphicsLine;
        let shootSound, explosionSound;
        let gameOver = false;

        new Phaser.Game(config);

        function preload() {
            this.load.audio('shoot', 'https://cdn.jsdelivr.net/gh/photonstorm/phaser3-examples/public/assets/audio/SoundEffects/p-ping.mp3');
            this.load.audio('explosion', 'https://cdn.jsdelivr.net/gh/photonstorm/phaser3-examples/public/assets/audio/SoundEffects/explosion.mp3');
            this.make.graphics({ add: false })
                .fillStyle(0x00ff00).fillRect(0, 0, 40, 20).generateTexture('player', 40, 20)
                .clear().fillStyle(0xffff00).fillRect(0, 0, 5, 15).generateTexture('bullet', 5, 15);
            this.load.image('ufo', 'ufo_no_watermark_100.png');
        }

        function create() {
            shootSound = this.sound.add('shoot');
            explosionSound = this.sound.add('explosion');

            player = this.physics.add.sprite(400, 550, 'player').setCollideWorldBounds(true);
            cursors = this.input.keyboard.createCursorKeys();

            bullets = this.physics.add.group();
            enemies = this.physics.add.group();

            graphicsLine = this.add.graphics();

            this.physics.add.overlap(bullets, enemies, hitEnemy, null, this);
            this.physics.add.overlap(player, enemies, hitPlayer, null, this);

            scoreText = this.add.text(10, 10, 'Score: 0', { fontSize: '20px', fill: '#fff' });
            levelText = this.add.text(10, 40, 'Level: 1/' + TOTAL_LEVELS, { fontSize: '20px', fill: '#fff' });

            spawnEnemies.call(this);

            // Continuous spread fire setup
            this.shootingEvent = null;
            this.shootBullet = () => {
                const range = 50;
                const step = 10;
                const speed = 300 + level * 30;
                for (let offset = -range; offset <= range; offset += step) {
                    const b = bullets.create(player.x + offset, player.y - 20, 'bullet');
                    b.body.velocity.y = -speed;
                }
                shootSound.play();
            };

            this.input.keyboard.on('keydown-SPACE', () => {
                if (!this.shootingEvent) {
                    this.shootBullet();
                    const fireRate = Math.max(300 - level * 20, 100);
                    this.shootingEvent = this.time.addEvent({
                        delay: fireRate,
                        callback: this.shootBullet,
                        callbackScope: this,
                        loop: true
                    });
                }
            });

            this.input.keyboard.on('keyup-SPACE', () => {
                if (this.shootingEvent) {
                    this.shootingEvent.remove(false);
                    this.shootingEvent = null;
                }
            });
        }

        function update() {
            if (gameOver) return;

            graphicsLine.clear().lineStyle(2, 0xffffff, 0.7)
                .beginPath().moveTo(player.x, player.y - 10).lineTo(player.x, 0).strokePath();

            if (level > TOTAL_LEVELS) return endGame(true);

            const baseSpeed = 300 + level * 30;
            const moveSpeed = level === 1 ? baseSpeed * 3 : baseSpeed;
            player.setVelocity(0);
            if (cursors.left.isDown) player.setVelocityX(-moveSpeed);
            else if (cursors.right.isDown) player.setVelocityX(moveSpeed);
            if (cursors.up.isDown) player.setVelocityY(-moveSpeed);
            else if (cursors.down.isDown) player.setVelocityY(moveSpeed);

            bullets.children.each(b => { if (b.y < 0) b.destroy(); });
            enemies.children.each(e => { if (e.y > 600) e.destroy(); });

            if (enemies.countActive() === 0) {
                level++;
                levelText.setText('Level: ' + level + '/' + TOTAL_LEVELS);
                if (level <= TOTAL_LEVELS) spawnEnemies.call(this);
            }
        }

        function spawnEnemies() {
            const speedBase = level === 1 ? 50 : 100 + level * 30;
            for (let i = 0; i < ENEMIES_PER_LEVEL; i++) {
                const x = Phaser.Math.Between(50, 750);
                const y = Phaser.Math.Between(-200, -20);
                const u = enemies.create(x, y, 'ufo');
                u.setDisplaySize(100, u.height * (100 / u.width));
                u.setOrigin(0.5, 0.5);
                u.body.velocity.y = Phaser.Math.Between(speedBase, speedBase + 50);
            }
        }

        function hitEnemy(bullet, enemy) {
            bullet.destroy(); enemy.destroy(); explosionSound.play();
            score += 10; scoreText.setText('Score: ' + score);
        }

        function hitPlayer() { endGame(false); }

        function endGame(won) {
            this.physics.pause(); gameOver = true; explosionSound.play();
            const msg = (won ? 'You Win!' : 'Game Over!') + ' Score: ' + score + '\nContinue?';
            if (confirm(msg)) window.location.reload();
        }
    </script>
</body>
</html>

This complete code structure shows the entire game implementation. Don’t worry if it seems overwhelming at first – we’ll break down each section in detail throughout the following sections, explaining how each part works and why it’s structured this way.

Setting Up Your Development Environment

Before diving into the code, you’ll need to prepare your development environment. The beauty of Phaser 3 lies in its simplicity – you can get started with just a few files and a basic understanding of JavaScript.

Your project structure will be minimal yet effective. Create a main HTML file that will serve as your game container, and prepare a UFO sprite image (approximately 100 pixels wide works best). The game loads Phaser 3 directly from a CDN, eliminating the need for complex build processes or package management.

However, modern browsers enforce CORS (Cross-Origin Resource Sharing) policies that prevent loading local files directly. To properly test your game with image assets, you’ll need to run a local web server. The easiest approach is using Python’s built-in HTTP server module.

Running a Local Development Server

Setting up a local server is straightforward and requires just a few commands. Open your terminal (CMD or PowerShell on Windows) in the folder containing your game files, then run:

bash# Python 3
python -m http.server 8000

Once the server is running, open your browser and navigate to http://localhost:8000/game.html. This setup allows your browser to properly load local image files without CORS restrictions.

With the local server running, your preload function can reference images using simple relative paths:

javascriptthis.load.image('ufo', 'ufo_no_watermark_100.png');

The game canvas will occupy an 800×600 pixel viewport, providing enough space for dynamic gameplay while maintaining compatibility across different screen sizes. This resolution strikes a good balance between visual clarity and performance optimization.

Creating the Game Foundation

The core architecture of your Phaser game revolves around three essential lifecycle methods that handle different aspects of game execution. The preload function manages asset loading, including audio files and sprite textures. The create function initializes game objects, sets up physics systems, and establishes user input handlers. Finally, the update function runs continuously, managing game state changes, collision detection, and rendering updates.

javascriptconst config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    physics: { default: 'arcade', arcade: { debug: false } },
    scene: { preload, create, update }
};

This configuration object tells Phaser to automatically choose the best rendering method for the user’s browser, sets up Arcade Physics for simple collision detection, and defines the three scene methods that will control your game’s behavior.

Asset Management and Texture Generation

Modern game development often requires efficient asset management, and Phaser 3 provides excellent tools for both loading external assets and generating textures programmatically. Your shooter game will use a hybrid approach, combining loaded audio files with dynamically generated graphics.

The player ship and bullets are created using Phaser’s built-in graphics generation capabilities. This approach reduces file dependencies while maintaining visual clarity. The graphics system allows you to create colored rectangles, circles, and other shapes that serve as game sprites without requiring external image files.

javascriptfunction preload() {
    this.load.audio('shoot', 'path/to/shoot-sound.mp3');
    this.load.audio('explosion', 'path/to/explosion-sound.mp3');
    
    this.make.graphics({ add: false })
        .fillStyle(0x00ff00).fillRect(0, 0, 40, 20).generateTexture('player', 40, 20)
        .clear().fillStyle(0xffff00).fillRect(0, 0, 5, 15).generateTexture('bullet', 5, 15);
        
    this.load.image('ufo', 'ufo_sprite.png');
}

Audio assets are loaded from external sources, providing immersive sound effects that enhance the gaming experience. The shooting sound gives immediate feedback for player actions, while explosion sounds create satisfying destruction effects.

Implementing Physics and Collision Systems

Phaser’s Arcade Physics system provides the foundation for realistic game object interactions without the complexity of full physics simulation. Your shooter game leverages this system for movement, boundary detection, and collision handling between different game objects.

The physics system automatically handles sprite positioning, velocity calculations, and collision detection between groups of objects. This allows you to focus on game logic rather than low-level physics calculations, significantly accelerating development time.

Player movement is handled through velocity-based physics, creating smooth and responsive controls. The ship can move in all four directions using arrow keys, with movement speed that scales based on the current level to maintain appropriate challenge progression.

javascriptfunction create() {
    player = this.physics.add.sprite(400, 550, 'player').setCollideWorldBounds(true);
    cursors = this.input.keyboard.createCursorKeys();
    
    bullets = this.physics.add.group();
    enemies = this.physics.add.group();
    
    this.physics.add.overlap(bullets, enemies, hitEnemy, null, this);
    this.physics.add.overlap(player, enemies, hitPlayer, null, this);
}

The collision system uses overlap detection to determine when bullets hit enemies or when enemies collide with the player. This approach provides precise collision detection while maintaining excellent performance even with multiple moving objects on screen.

Advanced Shooting Mechanics

The shooting system in your game goes beyond simple single-shot mechanics to provide an engaging spread-fire experience. Players can hold down the spacebar to continuously fire a fan of bullets, creating a more dynamic and visually appealing combat system.

The spread-fire mechanism creates multiple bullets simultaneously, each with slightly different horizontal positions but identical vertical velocities. This creates a shotgun-like effect that makes hitting targets easier while adding visual excitement to the gameplay.

javascriptthis.shootBullet = () => {
    const range = 50;
    const step = 10;
    const speed = 300 + level * 30;
    for (let offset = -range; offset <= range; offset += step) {
        const bullet = bullets.create(player.x + offset, player.y - 20, 'bullet');
        bullet.body.velocity.y = -speed;
    }
    shootSound.play();
};

The continuous firing system uses Phaser’s event system to handle spacebar press and release events. When the player presses the spacebar, the game immediately fires a spread of bullets and then sets up a repeating timer to continue firing until the key is released.

Fire rate increases with each level, creating a sense of progression and power advancement. This scaling system ensures that higher levels feel more intense while maintaining the core shooting mechanics that players have learned.

Enemy Management and Spawning Systems

Effective enemy management is crucial for creating engaging gameplay experiences. Your shooter implements a wave-based spawning system that creates increasingly challenging encounters as players progress through levels.

Each level spawns a predetermined number of UFO enemies from random positions above the visible screen area. The enemies descend at varying speeds, creating unpredictable movement patterns that require player skill to navigate and defeat.

javascriptfunction spawnEnemies() {
    const speedBase = level === 1 ? 50 : 100 + level * 30;
    
    for (let i = 0; i < ENEMIES_PER_LEVEL; i++) {
        const x = Phaser.Math.Between(50, 750);
        const y = Phaser.Math.Between(-200, -20);
        const enemy = enemies.create(x, y, 'ufo');
        enemy.setDisplaySize(100, enemy.height * (100 / enemy.width));
        enemy.body.velocity.y = Phaser.Math.Between(speedBase, speedBase + 50);
    }
}

The spawning system scales difficulty through multiple parameters. Enemy speed increases with each level, creating faster-moving targets that require improved player reflexes. The random positioning ensures that no two playthroughs feel identical, maintaining replay value.

Enemy sprites are automatically scaled to maintain consistent visual appearance regardless of the original image dimensions. This normalization ensures that collision detection remains accurate and that the game maintains visual consistency across different enemy types.

Level Progression and Difficulty Scaling

A well-designed progression system keeps players engaged by gradually increasing challenge while maintaining achievable goals. Your shooter implements multiple scaling mechanisms that work together to create a smooth difficulty curve.

Movement speed, bullet velocity, and enemy spawn rates all scale with level progression. The first level provides a gentle introduction with slower enemies and enhanced player movement speed, allowing new players to learn the basic mechanics without overwhelming challenge.

javascriptfunction update() {
    if (enemies.countActive() === 0) {
        level++;
        levelText.setText('Level: ' + level + '/' + TOTAL_LEVELS);
        if (level <= TOTAL_LEVELS) {
            spawnEnemies.call(this);
        } else {
            return endGame(true);
        }
    }
    
    const baseSpeed = 300 + level * 30;
    const moveSpeed = level === 1 ? baseSpeed * 3 : baseSpeed;
}

The progression system automatically advances players to the next level when all enemies are defeated. This creates clear objectives and immediate feedback for successful gameplay, encouraging continued engagement.

Visual feedback keeps players informed of their progress through level indicators and score displays. These UI elements provide context for the player’s current situation and goals, essential for maintaining engagement in longer gaming sessions.

Audio Integration and User Experience

Sound design plays a crucial role in creating immersive gaming experiences. Your shooter integrates audio feedback for key player actions, creating a more engaging and responsive gameplay experience.

The audio system provides immediate feedback for shooting actions and enemy destruction. Shooting sounds confirm successful input registration, while explosion sounds create satisfying feedback for successful hits. This audio feedback loop reinforces positive player actions and enhances the overall game feel.

javascriptfunction hitEnemy(bullet, enemy) {
    bullet.destroy();
    enemy.destroy();
    explosionSound.play();
    score += 10;
    scoreText.setText('Score: ' + score);
}

Performance considerations are important when implementing audio in web games. Phaser 3’s audio system automatically handles browser compatibility issues and provides efficient playback for repeated sound effects without memory leaks or performance degradation.

Game State Management and User Interface

Effective game state management ensures smooth transitions between different game phases and provides clear feedback to players about their progress and performance. Your shooter implements a comprehensive state system that handles gameplay, game over conditions, and victory scenarios.

The user interface provides essential information without cluttering the gameplay area. Score and level indicators occupy minimal screen space while remaining clearly visible during intense gameplay moments.

javascriptfunction endGame(won) {
    this.physics.pause();
    gameOver = true;
    explosionSound.play();
    const message = (won ? 'You Win!' : 'Game Over!') + ' Score: ' + score + '\nContinue?';
    if (confirm(message)) {
        window.location.reload();
    }
}

The game over system provides clear feedback about success or failure while offering immediate restart options. This streamlined approach reduces friction for players who want to attempt another playthrough, encouraging extended engagement with your game.

Performance Optimization and Best Practices

Web-based games require careful attention to performance optimization to ensure smooth gameplay across different devices and browsers. Your shooter implements several optimization strategies that maintain high frame rates while providing rich gameplay experiences.

Object pooling is handled automatically by Phaser’s group system, which efficiently manages sprite creation and destruction. This prevents memory leaks and reduces garbage collection overhead during intensive gameplay moments.

javascriptbullets.children.each(b => { if (b.y < 0) b.destroy(); });
enemies.children.each(e => { if (e.y > 600) e.destroy(); });

Cleanup routines remove off-screen objects that no longer contribute to gameplay, preventing memory accumulation that could degrade performance over time. This practice is essential for games with continuous object creation and destruction.

Extending Your Game Further

The foundation you’ve built provides numerous opportunities for expansion and enhancement. Consider adding power-up systems that temporarily modify player capabilities, such as increased fire rate, wider spread patterns, or temporary invincibility.

Background graphics and parallax scrolling effects can enhance visual appeal without significantly impacting performance. Particle effects for explosions and engine trails add visual polish that makes your game feel more professional and engaging.

Boss encounters at specific level intervals can break up the standard gameplay loop and provide memorable challenges that test player skills in new ways. These encounters can introduce new mechanics and attack patterns that keep experienced players engaged.

Conclusion

Building a complete game with Phaser 3 demonstrates the power and flexibility of modern web game development frameworks. The shooter you’ve created showcases essential game development concepts while providing a solid foundation for more complex projects.

The techniques you’ve learned – sprite management, physics integration, audio systems, and progressive difficulty scaling – are fundamental skills that apply to many different game genres. Whether you’re interested in platformers, puzzle games, or more complex action titles, these concepts will serve as valuable building blocks for your future projects.

Web-based game development continues to evolve, with new browser capabilities and framework improvements constantly expanding what’s possible. Your Phaser 3 shooter represents a solid step into this exciting field, providing both a complete game and a platform for continued learning and experimentation.

Leave a Reply