用vue3+pixijs续写仙剑十里坡剑神传说

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

前言

本期将用vue3与pixijs作为主要技术来把当年仙剑一那个让无数仙剑玩家津津乐道的十里坡剑神的梗来写成的一个小游戏,这个梗大致就是某玩家在仙剑一中因为不知道可以坐船离开余杭镇,所以找不到出路,闲着无聊就在十里坡练级,不知不觉在新手村学到最后一个技能-剑神,这件事最后传出去,整个RPG界,把“十里坡剑神”都成了一个论人究竟有多无聊的传说。当然,我也是从小玩仙剑长大的,之前没有网络,仙剑一更是玩了9遍,所以,这次创作出一款仙剑同人小游戏来以此缅怀逝去的童年吧。

废话不多说,我们先来康康展示效果怎样吧:

VID_20220404_210604.gif

演示地址:jsmask.gitee.io/shilipojian…

介绍

本期在代码结构与上一期 用vue3+pixijs复刻童年记忆里的游戏-猎鸭季节 大同小异,所以不会讲到如资源加载,切换,发布订阅这些重复性的内容了,只会讲到场景对白,帧动画,碰撞检测,掉落道具等内容。

游戏说明

  1. 进入游戏点击妖怪即可降低其血量,当血量低于0则消灭。
  2. 妖怪消灭时,又概率掉落道具,道具很多种,有的增加战斗时间,有的增加攻击力。
  3. 在有限时间内,尽可能多的击杀妖怪,获得更多的得分。在结束时,会根据得分酒剑仙进行评判。

VID_20220404_211629.gif

主要技术

  1. vite:负责整个项目的模块构建打包任务。
  2. vue3:作为前端框架,方便完成一些界面的响应式、组件化等。
  3. scss:负责初始加载界面的css动画,与一些界面比例调整的样式工作。
  4. mitt.js:负责发布订阅的任务。
  5. pixi.js:游戏引擎,游戏中绝大部分任务都在这里完成。
  6. gsap.js:负责一些属性动画的操作。
  7. bump.js:负责做碰撞检测任务。

开始

场景对白

// talkData.js
export const talkData = [{
    name: "李逍遙",
    face: "hero_face_1",
    content: `村子和十裏坡逛個遍,怎麽還沒找到離開的辦法啊~`
}, {
    name: "酒劍仙",
    face: "npc_face_0",
    content: `小子,沒找到出路不如在十裏坡多練練武功,以後出去了也不至于吃虧嘛。`,
    position: "left"
}
  //....
]
复制代码

这里我们先写好要做的对话信息,比如名称,头像,内容,位置等。

export default class Talk {
    // ...
    show({ name, face, content, position }) {
        this.target.visible = false;
        return new Promise((resolve, reject) => {
            this.clearChildren();
            this.drawInfo({ name, face, content, position });
            this.target.visible = true;
            Bus.$on("talk_next", () => {
                Bus.$off("talk_next")
                resolve()
            })
        })
    }
    // ...
}
复制代码

还有写一个对话类,我们期望实例化它后,每次都用show方法去调用,它是一个异步的,当屏幕点击后会发出talk_next事件才会继续执行下一话。

这里我们可以想,它是如何不停的执行对话的呢,当然你可以用如下方法:

async stageTalk() {
    await this.talk.show.call(this.talk, talkData[0])
    await this.talk.show.call(this.talk, talkData[1])
    await this.talk.show.call(this.talk, talkData[2])
    // ...
}
复制代码

但是你会发现,这样相当不优雅而且额外增加大量的体力劳动。

可以通过如下方式进行改进:

// mainScene.js
import Scene from "./scene"
import { talkData, talkResult } from "./talkData"

export default class MainScene extends Scene {
    // ...
    async stageTalk() {
        let talkList = []
        for (let i = 0; i < talkData.length; i++) {
            talkList.push(this.talk.show.bind(this.talk, talkData[i]))
        }
        return new Promise(async (resove, reject) => {
            while (talkList.length > 0) {
                await talkList.shift()()
            }
            resove()
        })
	},
    // ...
}
复制代码

这是在做我们的初始对白场景,你可以发现这里把对话实例的show方法传入要对话信息然后存放到talkList数组里,这样再执行异步操作,通过while循环直到talkList数组的对话都说完的时候才会继续往下执行。

通过这样的方式来取代如下代码:

微信截图_20220404211709.png

帧动画

import { AnimatedSprite } from "pixi.js";
class Hero {
    // ...
    attack() {
        if (this.state === HERO_STATE.attack) return;
        this.state = HERO_STATE.attack;
        playAttack();
        this.clearChildren();
        let attack_list = []
        for (let i = 0; i < 3; i++) {
            attack_list.push(createSprite({ name: "hero_attack_" + i, anchor: 0 }).texture);
        }
        this.attackAnimatedSprite = new AnimatedSprite(attack_list);
        this.attackAnimatedSprite.loop = false;
        this.attackAnimatedSprite.animationSpeed = .32;
        this.attackAnimatedSprite.gotoAndPlay(0);
        this.attackAnimatedSprite.onComplete = () => {
            this.normal()
        }
        this.target.addChild(this.attackAnimatedSprite)
    }
    // ...
}
复制代码

我们所有的帧动画都用了pixi.js自带的AnimatedSprite,使用起来非常简单,先将要变成动画的精灵纹理按顺序存到数组里,再实例化AnimatedSprite把这个数组传入进去。这是可以通过gotoAndPlay等方法进行执行操作,当然可以控制它是否循环,播放速度,完成后事件,别忘了还要把存入目标节点中来显示出来,处理一些简单帧动画动画用这个方式去完成是非常方便的。

微信截图_20220405183723.png

碰撞检测

// tools.js
import * as PIXI from "pixi.js"
import Bump from "bump.js"
export const bump = new Bump(PIXI)
复制代码

bump.js库来帮助我们去判断鼠标在游戏中是否击中妖怪的检测,在PIXI中使用它,必须通过把PIXI传入生成一个实例去使用。

// enemy.js
import { bump } from "../tools"
class Enemy {
    // ...
    hunt(e) {
        if (this.hp <= 0 || this.state !== ENEMY_STATE.normal) return;
        if (!bump.hit(e.pos, this.target, true, true)) return;
        this.hp -= e.power;
        this.health.cut(this.hp)
        playHit();
        this.hp <= 0 && this.die();
    }
    // ...
}
复制代码

bump.hit方法进行点与当前敌人的碰撞检测,如果为真则证明点中了,进行相应的掉血等逻辑,而且这样好处是可以穿透,达到了一次打击多个敌人的效果需求,当然它还可以做包围盒碰撞等等检测判断,使用起来十分的方便。

微信截图_20220404211723.png

掉落道具

const createEnemyData = () => {
    return {
        yong: {
            hp: 100,
            speed: .78,
            animationSpeed: .1,
            score: 7,
            itemType: 1,
            chance: 30
        },
        // ...
    }
}
复制代码

这里是生成敌人-蛹的基础数据,其中chance字段代表了掉落道具的概率,itemType代表掉落道具的种类。

// mainScene.js
import Scene from "./scene"
import Item from "./item"

export default class MainScene extends Scene {
    // ...
    addCount(obj) {
        // ...
        // 随机掉落道具实例
        if (random(0, 100) < obj.chance) {
            new Item({
                x: obj.diePos.x,
                y: obj.diePos.y,
                stage: this.stageContainer,
                hero: this.hero,
                type: obj.itemType
            })
        }
        
        // 增加得分和击杀数
        this.score += obj.score;
        this.count += 1;
	}
    // ...
}
复制代码

这里是当妖怪被击杀后,会发出 addCount事件,我们执行的时候,会通过当前妖怪掉落道具的概率取一次随机数,如果达成掉落条件,则会生成一个道具实例、当然这个道具实例出现后会自动执行一个到达角色身旁的动画,当完成后会发出addBuff事件,来给角色增加相应的能力。

微信截图_20220404211759.png

结语

pixi.js处理一些场景布局,碰撞检测也不是非常的直观,如果想做的更大的h5游戏,还是比较推荐cocos creator虽然他比较重。因为这次部署到线上用了免费的Gitee Pages ,打开可能并不是太快,所以尽可能的将代码和资源变得更轻些。

这次素材也是比较有限的,但也是尽可能的按照新版仙剑一的风格还原出来了,包括文字都用了繁体,致敬一下当年的台湾大宇。小时候一周只有两小时的玩游戏时间,虽然很短但非常的怀念那段时间,以后有机会,我会实现更多的童年的经典游戏,希望得到大家的鼓励支持~

猜你喜欢

转载自juejin.im/post/7083283393844084750