I am participating in the nuggets Community Game Creativity Submission Contest. For details, please see: Game Creativity Submission Contest.

preface

In this issue, vue3 and PixiJS will be used to reproduce the game I played in my childhood — Duck hunting season. At the beginning, it needed a light gun to play it, which was very difficult to aim at. Whenever I hit a duck, I would be very excited, and if I missed it, I would be laughed at by the dog. Now, I want to do a transformation, so that he can hit it without a light gun with the mouse, round a childhood dream.

Without further ado, let’s first show kangkang how the effect is:

Address: jsmask.gize. IO /duck-hunt/

introduce

Because most of the game interface is done with pixi. Js mapped, amount of code in itself is also more and more complicated, can’t all speak at once, so this period mainly explain, it is how to load, how to draw the interface, how to do animation game, how to hit the judgement, as well as how to fit the screen size, and so on. Let’s do some preparatory work at the beginning.

The rules of the game

  1. Enter the game every five rounds, each round will appear two ducks, and three bullets.
  2. A bonus of 500 points is awarded for each duck hit, with a special bonus for all ducks hit in each round.
  3. If you run out of bullets or run out of time, the ducks will fly away. Grasp each bullet and cherish the time.
  4. If you hit more than six ducks in a round, you qualify for the next round, up to three rounds.

The game process

  1. Click the initial screen to enter the game interface.
  2. Each round begins with a cutscene of the hound.
  3. After the hound cutscene, the game can be used for shooting.
  4. Click the mouse every time, then said to launch bullets, points in the duck is extra points, points in the duck, timeout or bullets used up the duck speed away and other logic.
  5. More than three rounds of the game or not meet the promotion conditions, forced back to the initial screen and record the score, you can start again.

The main technical

  1. Vite: Responsible for the module building and packaging of the whole project.
  2. Vue3: as a front-end framework, it is convenient to complete the responsiveness and componentization of some interfaces.
  3. SCSS: Responsible for the initial loading of the CSS animations of the interface, working with some interface scaling styles.
  4. Mitt. Js: Responsible for publishing and subscribing.
  5. Pixi.js: The game engine where most of the tasks in the game are done.
  6. Gsap.js: Responsible for some animation operations.

The game is material

In order to reduce the size of the game itself and to be closer to its original purpose, Press Start 2P uses pixel fonts and the sound is compressed from WAV to MP3. The image was originally a large image and the broken image I see here was broken up using ShoeBox, but I didn’t do any more processing on the image (unifying the size of the image and then TexturePackerGUI), as I’ll explain later, I used a different way to process the animation.

start

Release subscription

import mitt from "mitt";

const bus = {};

const emitter = mitt();

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

export default bus;
Copy the code

Because VUe3 does not have on, on, on, off, we use Mitt to replace the task of publish and subscribe, and many state change notifications are completed by means of it.

File structure

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

The loading animation component is used for loading animation in one of my previous articles: WEB loading animation for Pixel Word animation

The DuckGame component is the main container for our game.

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

What we’re going to do here is upload the Game container to the Game, generate an instance of it, and of course you can see in onProgress, send the loading animation component to inform you of the current progress.

The game scene

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; }}Copy the code

All scenes in the game inherit from Scene, because the logic is relatively simple, only involving the start interface and game interface two scenes, all relatively simple, currently only show hidden update such as basic methods.

We inherit it whenever we create a new interface, such as the start interface:

import Scene from "./scene"

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

Load the material

Because we used VUe3, we borrowed the chicken to lay eggs and used the WAY of URL to get the corresponding material.

export function getImageUrl(name, ext = "png") {
    return new URL(`/src/assets/${name}.${ext}`.import.meta.url).href
}
Copy the code

Then configure:

const audioList = {
    fire: getImageUrl("fire"."mp3"),
    / /... more
}

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

export default{ stage, ... audioList,// more
}
Copy the code

The Loader in pixi.js is used to complete the loading task, and vue3 is informed of the current loading progress of the loading animation component. They are also stored as texture maps for later pixi.js drawings.

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)
          })
        })
    },
    reader(){
      // Render the interface
    },
    // ...
}
Copy the code

Draw the interface

Most of the interface is done by drawing API in pixi.js, mainly manual labor, can refer to the API on the official website of pixi.js to learn. Here is a simple introduction to making background black block drawing as follows, and total integral drawing.

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 = ".toUpperCase() + 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
Copy the code

Animation game

Since Pixi.js is not a visual game engine, we used Gsap.js instead to make it easier to animate games. Some flashing animations will appear in the game, such as the flash of the text button click to Start the game in the start interface, which is slowed by SteppedEase, which seems to be in line with the taste of that era.

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);
Copy the code

Of course, there is more involved in the inside of the frame animation, such as hunting dogs, laughing, duck flight and so on are all frame animation to complete. Pixi.js also has a frame animation execution scheme, but I did not further process the material here, so I chose a coincidence, or use the SteppedEase of GSAP. js to slowly simulate the frame, which has the advantage that each frame can have a method to adjust the position of the picture to compensate for the displacement caused by the different size of the picture.

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()
Copy the code

Hit judgement

There are two ways to determine, the first is the enveloping box detection, determine whether the mouse click point and the duck overlap, if the overlap means hit. The second is a Pointerdown event that exists in pixi.js. The pointerdown event is used to avoid killing two birds with one stone. When we click on the duck, we change the current state of the duck to indicate hit. At the same time, our system will issue a bullet event, and if the duck’s isHit status changes to true and isDie is false, it will display the score, drop the death animation, and finally destroy it.

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();
        
        // Change the state in point
        this.target.on("pointerdown".() = > {
            if (!this.isHit) this.isHit = true;
        })
        
        // Receive bullet event
        Bus.$on("sendBullet".({ e, callback }) = > {
            if (this.isHit && !this.isDie) {
                this.isDie = true;
                this.hit();
                this.duck_sound.play()
                callback && callback(this)}})// Receive the fly away event
        Bus.$on("flyaway".() = > {
            this.isFlyaway = true;
        })
        return this;
    }
    move(delta) {
        / / move
    }
    async hit() {
        / / 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()
    }
    die() {
        / / death
    }
    fly() {
        / / flight
    }
    destroy() {
        / / destroy
        if (this.target.parent) {
            this.target.parent.removeChild(this.target)
        }
    }
}
Copy the code

Fit the screen

In order to make the interface display without deformation, I used a trick scheme, using CSS transform:scale+ V-bind method, let Vue calculate the maximum proportion, and then bind to CSS.

<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>
Copy the code

conclusion

In general, Pixi.js is very powerful and is perfect for this type of game. If there are a lot of scene interfaces and animations, it is still recommended to use Cocos Creator, which can save a lot of work.

The game in this issue is also made by me as much as possible. My childhood memory has gradually become clearer from obscurity. I hope that you who said that I would make games when I grow up will not forget your original intention and have time to create the games you loved in childhood in your own way. It’s also a way to hone your skills and reminisce about your carefree childhood.