Game Development 14 min read

Recreating the Classic Duck Hunt Game with Vue 3 and PixiJS

This article walks through building a browser‑based Duck Hunt‑style shooting game using Vue 3 for the UI framework, PixiJS as the rendering engine, and supporting tools such as Vite, SCSS, mitt, and GSAP to handle asset loading, scene management, animations, hit detection, and responsive scaling.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Recreating the Classic Duck Hunt Game with Vue 3 and PixiJS

Preface

The author revisits the childhood game "Duck Hunt" and recreates it with Vue 3 and PixiJS, replacing the original light‑gun control with mouse clicks to make the experience accessible on modern browsers.

Introduction

The tutorial explains how the game loads assets, draws the interface, creates animations, performs hit detection, and adapts to different screen sizes. Because most of the UI is drawn with PixiJS, the codebase is fairly large and detailed.

Game Rules

Each round consists of five turns, each turn spawning two ducks and providing three bullets.

Hitting a duck awards 500 points; hitting all ducks in a round grants a special bonus.

If bullets run out or time expires, the ducks fly away.

Scoring more than six ducks per round qualifies the player for the next round, up to three rounds total.

Game Flow

Click the start screen to enter the game.

A hunting‑dog cut‑scene plays at the beginning of each round.

After the cut‑scene, the player can shoot.

Each mouse click fires a bullet; hitting a duck adds points, while missed shots or depleted ammo cause the ducks to accelerate away.

After three rounds or failing to meet the advancement criteria, the game returns to the start screen and records the score.

Main Technologies

Vite – module bundling and build tasks.

Vue 3 – reactive component framework.

SCSS – styling and CSS animations for the loading screen.

mitt – lightweight publish/subscribe implementation.

PixiJS – core 2D rendering engine.

GSAP – animation handling.

Game Assets

The project uses the "Press Start 2P" pixel font, compressed audio (WAV → MP3), and sprite sheets split with ShoeBox. The author later describes an alternative method for handling these textures.

Start

Publish/Subscribe

import mitt from "mitt";

const bus = {};
const emitter = mitt();

bus.$on = emitter.on;
bus.$off = emitter.off;
bus.$emit = emitter.emit;

export default bus;

Vue 3 no longer provides an off method, so mitt is used for event handling.

File Structure

<template>
  <div>
    <Loading v-if="progress<100" :progress="progress" />
    <DuckGame />
  </div>
</template>

The DuckGame component is the main container for the game.

<template>
  <div class="game" ref="canvas"></div>
</template>
new Game({
    width,
    height,
    el: canvas.value,
    resolution: 1,
    onProgress: n => {
        Bus.$emit("changeProgress", n);
    }
}).init();

The game instance is created and the loading progress is emitted to a Vue component.

Scene Base Class

import { Container } from "pixi.js";
export default class Scene {
    constructor(game) {
        this.game = game;
        this.stage = new Container();
        this.stage.interactive = true;
        this.stage.buttonMode = true;
        this.stage.sortableChildren = true;
        this.stage.zIndex = 1;
        return this;
    }
    onStart() {}
    init() {}
    show() { this.stage.visible = true; }
    hide() { this.stage.visible = false; }
    update(delta) { if (!this.stage.visible) return; }
}

Start Scene

import Scene from "./scene";

class StartScene extends Scene {
    constructor(game) {
        super(game);
        this.topScore = null;
        return this;
    }
}
export default StartScene;

Load Assets

export function getImageUrl(name, ext = "png") {
    return new URL(`/src/assets/${name}.${ext}`, import.meta.url).href;
}
const audioList = {
    fire: getImageUrl("fire", "mp3"),
    // ...more
};

const stage = getImageUrl("stage");
// ...more

export default {
    stage,
    ...audioList,
    // more
};

Assets are loaded with PixiJS's Loader , and progress is reported back to Vue.

export default class Game {
    // ...
    init() {
        this.loaderTextures().then(res => {
            Object.entries(res).forEach(([key, value]) => setTextures(key, value.texture));
            this.render();
        });
    }
    loaderTextures() {
        const { loader, onProgress } = this;
        return new Promise((resolve, reject) => {
            Object.entries(assets).forEach(([key, value]) => loader.add(key, value, () => {
                onProgress(loader.progress);
            }));
            loader.load((loader, resources) => {
                onProgress(loader.progress);
                resolve(resources);
            });
        });
    }
    // ...
}

Render UI

import { Text, Graphics, Container } from "pixi.js";

class StartScene extends Scene {
    drawBg() {
        const { width, height } = this.game;
        const graphics = new Graphics();
        graphics.beginFill(0x000000, 1);
        graphics.drawRect(0, 0, width, height);
        graphics.endFill();
        this.stage.addChild(graphics);
    }
    drawTopScore(score = 0) {
        const { width, height } = this.game;
        this.topScore = new Text("top score = " + score, {
            fontFamily: 'Press Start 2P',
            fontSize: 24,
            leading: 20,
            fill: 0x66DB33,
            align: 'center',
            letterSpacing: 4
        });
        this.topScore.anchor.set(0.5, 0.5);
        this.topScore.position.set(width / 2, height - 60);
        this.stage.addChild(this.topScore);
    }
}
export default StartScene;

Game Animation

import { TimelineMax } from "gsap";

let btnAni = new TimelineMax().fromTo(this.btn, { alpha: 0 }, { alpha: 1, duration: .45, immediateRender: true, ease: "SteppedEase(1)" });
btnAni.repeat(-1);
btnAni.yoyo(true);

Button flashing uses a stepped ease to mimic retro arcade style.

let dogSearchAni = new TimelineMax();
 dogSearchAni
    .from(dog, 0.16, { texture: getTextures("dog0"), ease: "SteppedEase(1)" })
    .to(dog, 0.16, { texture: getTextures("dog1"), ease: "SteppedEase(1)" })
    .to(dog, 0.16, { texture: getTextures("dog2"), ease: "SteppedEase(1)" })
    .to(dog, 0.16, { texture: getTextures("dog3"), ease: "SteppedEase(1)" })
    .to(dog, 0.2, { texture: getTextures("dog4"), ease: "SteppedEase(1)" });
 dogSearchAni.repeat(-1);
 dogSearchAni.play();

Hit Detection

export default class Duck {
    constructor({ dIndex = 0, x = 0, y = 0, speed = 3, direction = 1, stage, rect = [0, 0, 1200, 759] }) {
        // ...
        this.target = new Container();
        // click handling
        this.target.on("pointerdown", () => {
            if (!this.isHit) this.isHit = true;
        });
        // bullet event
        Bus.$on("sendBullet", ({ e, callback }) => {
            if (this.isHit && !this.isDie) {
                this.isDie = true;
                this.hit();
                this.duck_sound.play();
                callback && callback(this);
            }
        });
        // fly away event
        Bus.$on("flyaway", () => {
            this.isFlyaway = true;
        });
        return this;
    }
    // ...
    async hit() {
        const { sprite, score, target } = this;
        this.normalAni.kill();
        sprite.texture = getTextures("duck_9");
        sprite.width = getTextures("duck_9").width;
        sprite.height = getTextures("duck_9").height;
        showScore({ parent: this.stage, score, x: target.x - (this.vx < 0 ? +sprite.width : 0), y: target.y });
        await wait(.35);
        this.die();
    }
    // ...
}

Screen Adaptation

<script setup>
let width = 1200;
let height = 769;
const scale = `scale(${ window.innerHeight < window.innerWidth ? window.innerHeight / height : window.innerWidth / width })`;
</script>

<template>
  <div class="game" ref="canvas"></div>
</template>

<style lang="scss" scoped>
.game {
  transform: v-bind(scale);
  cursor: none;
}
</style>

Conclusion

PixiJS proves to be a powerful engine for this type of 2D game, though for more complex projects the author recommends Cocos Creator to reduce workload. The tutorial demonstrates how to recreate a nostalgic game while practicing modern frontend and graphics techniques.

animationFrontend DevelopmentGame developmentVue3GSAPPIXIJS
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.