Utilisez vue3+pixijs pour reproduire le jeu dans la saison de chasse au canard de mémoire d'enfance

Je participe au concours individuel du Nuggets Community Game Creativity Contest, pour plus de détails, veuillez consulter : Game Creativity Contest

avant-propos

Dans ce numéro, j'utiliserai vue3 et pixijs pour recréer le jeu auquel je jouais à Xiaobawang pendant la saison de chasse aux canards. J'avais besoin d'un pistolet léger pour y jouer au début, et il était très difficile de viser. Chaque fois que je touchais un canard, j'étais très excité. Il y aura des moqueries de chiens, en pensant à quel point il était simple d'être heureux à cette époque. Maintenant, je veux juste faire une modification, pour qu'il puisse le frapper avec une souris sans pistolet léger, et réaliser un rêve d'enfant.

Sans plus tarder, venons d'abord à Kangkang pour montrer l'effet :

VID_20220325_103042.gif

Adresse de démonstration : jsmask.gitee.io/duck-hunt/

présenter

Parce que la majeure partie de l'interface de ce jeu est dessinée avec pixi.js, la quantité de code elle-même est relativement volumineuse et lourde, et il est impossible de tout terminer en même temps, donc ce problème explique principalement comment le charger et y dessiner. Interface, comment faire de l'animation de jeu, comment déterminer les coups et comment s'adapter à la taille de l'écran, etc. Faisons d'abord un travail préparatoire.

Regles du jeu

  1. Il y a cinq tours dans chaque tour du jeu, et deux canards et trois balles apparaîtront à chaque tour.
  2. 500 points seront attribués chaque fois que vous toucherez tous les canards, et il y aura des récompenses spéciales si vous touchez tous les canards à chaque tour.
  3. Si les balles s'épuisent ou si le temps expire, les canards s'envoleront, veuillez saisir chaque balle et chérir le temps.
  4. Si vous frappez plus de 6 canards à chaque tour, vous pouvez passer au tour suivant, jusqu'à trois tours.

Déroulement du jeu

  1. Cliquez sur l'écran initial pour démarrer l'interface du jeu.
  2. La cinématique Hound commence à chaque tour du jeu.
  3. Après la cinématique Hound, le jeu est prêt à tourner.
  4. Chaque fois que vous cliquez sur la souris, cela signifie qu'une balle est tirée et que le canard est ajouté au point. Lorsque vous cliquez sur le canard, le canard accélère et s'envole s'il expire ou si la balle s'épuise.
  5. Si le jeu dépasse trois tours ou si les conditions de promotion ne sont pas remplies, vous serez obligé de revenir à l'écran initial et d'enregistrer le score, et vous pourrez recommencer.

Technique principale

  1. vite : responsable des tâches de construction de modules et d'emballage de l'ensemble du projet.
  2. Vue3 : En tant que framework front-end, il est pratique de compléter la réactivité et la composantisation de certaines interfaces.
  3. scss : responsable de l'animation CSS de l'interface de chargement initiale et travail avec certains styles de mise à l'échelle de l'interface.
  4. mitt.js : responsable des tâches de publication et d'abonnement.
  5. pixi.js : moteur de jeu, où la plupart des tâches du jeu sont effectuées.
  6. gsap.js : responsable de certaines opérations d'animation.

matériel de jeu

Capture d'écran WeChat_20220327112836.png

这里为了缩小游戏本体尺寸也是为了更接近原作用Press Start 2P用了像素字体,声音统一做了压缩处理从wav转成mp3格式。而图片原本是一张大图这里看到的碎图是因为用了ShoeBox软件去完成了拆分,但是我没有做进一步处理图片(将它们尺寸统一等再进行TexturePackerGUI拼合),后面会讲明我用了另一种方法来处理这些动画。

开始

发布订阅

import mitt from "mitt";

const bus = {};

const emitter = mitt();

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

export default bus;
复制代码

因为vue3里面没有了 O n on, off,所以我们使用mitt来去代替发布订阅这个任务,里面很多的状态改变的通知都借助于它来完成的。

文件结构

<template>
  <div>
    <Loading v-if="progress<100" :progress="progress" />
    <DuckGame />
  </div>
</template>
复制代码

加载动画组件我之前写的一篇文章,就是拿它来完成加载动画的: WEB加载动画之像素字动画

DuckGame组件就是我们游戏的主容器了。

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

我们这里要把游戏容器传到Game中,生成一个实例,当然在你可以看到在onProgress中,发给加载动画组件通知当前的进度状况。

Capture d'écran WeChat_20220327120321.png

游戏场景

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;
    }
}
复制代码

游戏里所有的场景都继承了Scene,因为逻辑比较简单只涉及了开始界面和游戏界面两个场景,所有比较的简单,目前只有显示隐藏更新这些基础方法。

我们每当创建新界面就会有继承它,如开始界面:

import Scene from "./scene"

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

export default StartScene
复制代码

加载素材

因为我们用了vue3所以就借鸡下蛋,用了URL的方式去获取对应的素材。

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
}
复制代码

通过pixi.js中的Loader去完成加载任务,同时通知vue3加载动画组件当前的加载进度。同时还要将他们变成纹理图存储起来以方便后面pixi.js绘图使用。

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(){
      // 渲染界面  
    },
    // ...
}
复制代码

Capture d'écran WeChat_20220327121512.png

绘制界面

本作大部分的界面都是pixi.js中的绘图API来完成,主要是体力劳动,可以参考pixi.js官网的API来学习。这里制作简单的介绍,如下面的背景黑块绘制,和总积分绘制。

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
复制代码

Capture d'écran WeChat_20220327122115.png

游戏动画

Étant donné que pixi.js n'est pas un moteur de jeu visuel, nous utilisons à la place gsap.js pour rendre les animations de jeu plus pratiques. Certaines animations scintillantes qui apparaissent dans le jeu, telles que le scintillement du bouton de texte cliquez pour démarrer le jeu dans l'interface de démarrage, utilisez SteppedEase pour ralentir, ce qui semble être conforme au goût de cette époque.

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);
复制代码

VID_20220327_150632.gif

Bien sûr, il y a plus d'animations de trame impliquées, telles que la chasse au chien, les moqueries, le vol du canard, etc. sont toutes complétées par une animation de trame. pixi.js a également un schéma d'exécution d'animation d'image, mais je n'ai pas de traitement supplémentaire du matériel ici, j'ai donc pris une astuce et utilisé le SteppedEase de gsap.js pour faciliter l'image de simulation. L'avantage de ceci est que chaque image peut ont une méthode pour ajuster la position de l'image afin de compenser le problème de déplacement causé par différentes tailles d'image.

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()
复制代码

VID_20220327_151452.gif

frapper le jugement

Il existe deux façons de déterminer, la première est la détection de la boîte englobante, pour déterminer si le point cliqué par la souris coïncide avec le canard, si c'est le cas, cela signifie un coup. Le second est l'événement pointerdown qui existe dans pixi.js. Voici un peu de paresse, et l'événement pointerdown est utilisé pour empêcher l'événement de faire d'une pierre deux coups. Lorsque nous cliquons sur le canard, nous modifions l'état actuel du canard pour indiquer un coup. Dans le même temps, notre système émettra également un événement de balle. Si le statut isHit du canard devient vrai et isDie est faux, cela signifie que le canard n'est pas mort, alors il exécutera le score d'affichage, supprimera l'animation de la mort, et enfin le détruire.

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();
        
        // 点中改变状态
        this.target.on("pointerdown", () => {
            if (!this.isHit) this.isHit = true;
        })
        
        // 接收子弹事件
        Bus.$on("sendBullet", ({ e, callback }) => {
            if (this.isHit && !this.isDie) {
                this.isDie = true;
                this.hit();
                this.duck_sound.play()
                callback && callback(this)
            }
        })
        // 接收飞走事件
        Bus.$on("flyaway", () => {
            this.isFlyaway = true;
        })
        return this;
    }
    move(delta) {
        // 移动
    }
    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()
    }
    die() {
        // 死亡
    }
    fly() {
        // 飞行
    }
    destroy() {
        // 销毁
        if (this.target.parent) {
            this.target.parent.removeChild(this.target)
        }
    }
}
复制代码

Capture d'écran WeChat_20220327152946.png

S'adapter à l'écran

Afin de rendre l'interface affichée au maximum sans être déformée, j'ai utilisé un schéma délicat, en utilisant la méthode transform:scale+v-bind de css, pour que vue calcule l'échelle maximale, puis la lie à 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>
复制代码

Capture d'écran WeChat_20220327154002.png

Épilogue

Dans l'ensemble, pixi.js est toujours très puissant, et il est parfait pour ce type de jeu. S'il y a beaucoup d'interfaces de scène et d'animations, il est recommandé d'utiliser le créateur de cocos, ce qui peut économiser beaucoup de travail.

Le jeu dans ce numéro est également fait par moi en prêtant autant que possible attention à certains détails du jeu. Ma mémoire d'enfance s'est progressivement éclaircie du flou. J'espère que mes amis d'enfance qui veulent jouer à des jeux quand je serai grand n'oublieront pas mon intention originale et avoir le temps de les mettre en pratique.Créer vos jeux préférés dans votre enfance à votre manière n'est pas un moyen d'exercer des compétences techniques et de vous rappeler la vie insouciante de votre enfance.

Je suppose que tu aimes

Origine juejin.im/post/7079944397575421965
conseillé
Classement