Pixi.js 游戏开发日志02 前端解压缩压缩包引用资源

前言

利用开发出来的工具把角色素材拼合到了一起,但是放到画布中,我们得到确是一个合集的图片,我们需要做的是将这个纹理集拆开,变成纹理,然后被我们的精灵引用 image.png
如此图所显示的,右侧的是角色动作的集合是一个精灵,左侧的三个红圈(为了醒目)的角色也是单独的精灵。
但是在Pixi.js的内部,他们的引用最终解析的是一个名为BaseTexture的基础纹理,下一步就是Texture纹理,可以被拆开也可以作为单独的一张图片引用。
最后作为最关键的显示,是Sprite的精灵,可以看到左侧的三个红圈的角色是一模一样的,但是在Pixi.js的内部,是分为三个精灵的,三个精灵共同引用一个纹理,而这个纹理是从一个纹理集的一部分。
image.png
我们可以引用其他的纹理再给舞台添加三个精灵,放到对应的角色精灵上,充当装备的显示精灵
甚至也可以改变角色精灵的纹理
image.png
以上的效果均为PS实现,真实的情况是此刻我还没有敲下一行角色显示到Pixi.js的代码

上个教程的一些问题

Pixi.js的坐标系是从左上角开始计算的,同样放置在舞台的精灵,也是从左上角开始计算的。
上一个教程中仅仅是获取了帧数,方向数量,宽高。还需要额外增加序列帧播放速度,还有每个动作的锚点
image.png 锚点就是图中每个动作脚下的那个点,举个例子,把他们放到(500,500)的坐标,只会让图片的左上角开始激素按,而不是脚下刚好踩上那个(500,500)的点,而且切换动作的时候,有明显的异常晃动。 image.png image.gif
锚点如果没有设置好的话,就会和这张图片一样,切换后有个明显的错位

直接解压缩ZIP文件

解压缩我是使用的zip.js,不过因为Promise不熟练,所以勉强解决了回调的问题,直接上代码吧

import { BlobReader, BlobWriter, Entry, TextWriter, ZipReader } from "@zip.js/zip.js";
import { Rectangle, Texture } from "pixi.js";

/**
 * 角色文件配置
 */
interface SkinFileConifg {
    frameNumber: number,
    direction: number,
    width: number,
    height: number,
    pivotX: number,
    pivotY: number,
    speed: number,
}

/**
 * 皮肤配置
 */
interface SkinConifg {
    animation: Texture[][],
    frameNumber: number,
    pivotX: number,
    pivotY: number,
    speed: number,
}

/**
 * 回调列表对象
 */
interface CallbackListObject {
    skinName: string,
    caller: any,
    method: Function,
}

export class CharacterSkin {
    /**
     * 序列帧配置群组
     * 根据动作储存配置
     */
    private group: Record<number, SkinConifg>;

    /**
     * 角色皮肤数据
     */
    public constructor() { }

    /**
     * 获取配置
     * @param action 动作编号
     */
    public getConfig(action: number): SkinConifg {
        if (!this.group[action]) throw "当前动作配置没有";
        return this.group[action];
    }

    /**
     * 皮肤配置临时缓存
     */
    private static promiseCache: Record<string, Promise<void>> = Object.create(null);

    /**
     * 对应的皮肤配置
     */
    private static skinGroup: Record<string, CharacterSkin> = Object.create(null);

    /**
     * 回调列表对象
     */
    private static callbackList: CallbackListObject[] = [];

    /**
     * 加载皮肤
     * @param skinName 皮肤名称
     * @param method 执行方法
     * @param caller 作用域
     */
    public static loadSkin(skinName: string, method: Function, caller: any): void {
        // 添加一个皮肤回调
        this.callbackList.push({ skinName, method, caller });
        // 完成列表中存在则直接执行完成回调
        if (this.skinGroup[skinName]) return complete();
        // 否则创建一个Promise缓存 避免重复加载解压缩
        this.promiseCache[skinName] = this.promiseCache[skinName] || create(skinName);
        // 加载完成后的回调
        this.promiseCache[skinName].then(complete);
    }

    /**
     * 皮肤加载完成后执行回调
     * @param skinName 皮肤名称
     */
    public static runCallback(skinName: string): void {
        // 循环遍历列表
        for (let index = this.callbackList.length - 1; index >= 0; index--) {
            // 获取当前回调对象
            const callback: CallbackListObject = this.callbackList[index];
            // 判断当前皮肤是否为需要执行的
            if (callback.skinName != skinName) continue;
            // 执行回调
            callback.method && callback.method.call(callback.caller, callback.skinName);
            // 删除回调
            this.callbackList.splice(index, 1);
        }
    };

    /**
     * 执行所有加载完成的回调
     */
    public static runAllLoadCompleteCallback(): void {
        // 全部加载完成的皮肤
        const allCompleteSkin: string[] = Object.keys(this.skinGroup);
        // 遍历循环
        for (let index = 0; index < allCompleteSkin.length; index++) {
            // 当前的皮肤名称
            const skinName = allCompleteSkin[index];
            // 执行当前皮肤名称回调
            this.runCallback(skinName);
        }
    }

    /**
     * 删除回调 避免加载途中切换皮肤
     * @param skinName 皮肤名称
     * @param method 执行方法
     * @param caller 作用域
     */
    public static clearCallback(skinName: string, method: Function, caller: any): void {
        // 循环遍历列表
        for (let index = this.callbackList.length - 1; index >= 0; index--) {
            // 获取当前回调对象
            const callback: CallbackListObject = this.callbackList[index];
            // 判断当前皮肤是否为需要的
            if (callback.skinName != skinName) continue;
            // 判断作用域和方法是否一致
            if (callback.caller != caller || callback.method != method) continue;
            // 删除回调
            this.callbackList.splice(index, 1);
        }
    }

    /**
     * 创建一个唯一的角色配置 避免重复加载
     * @param skinName 皮肤名称
     * @param config 皮肤配置
     */
    public static create(skinName: string, config: Record<number, SkinConifg>): void {
        // 创建角色配置
        this.skinGroup[skinName] = this.skinGroup[skinName] || new CharacterSkin();
        // 设置角色配置
        this.skinGroup[skinName].group = config;
    }

    /**
     * 返回角色皮肤数据
     * @param skinName 皮肤名称 
     * @returns 角色皮肤数据    
     */
    public static get(skinName: string): CharacterSkin {
        return this.skinGroup[skinName];
    }
}

/**
 * 完成后的回调
 */
function complete(): void {
    CharacterSkin.runAllLoadCompleteCallback();
}

/**
 * 中转代替方法
 * @param skinName 皮肤名称
 */
async function create(skinName: string): Promise<void> {
    // 当前角色皮肤的路径
    const path: string = "./assets/skin/" + skinName + ".zip";
    // 创建一个角色配置类
    const config: Record<string, SkinConifg> = await unzip(path).then(zipListToSkinConfig);
    // 创建角色配置
    CharacterSkin.create(skinName, config);
}

/**
 * 加载并解压缩压缩包
 * @param path 压缩包路径
 */
async function unzip(path: string): Promise<Record<string, Texture | any>> {
    // 加载zip并获取他的Blob数据
    const blob: Blob = await fetch(path).then(response => response.blob());
    // 解析Blob数据并且获取文件列表
    const entries: Entry[] = await new ZipReader(new BlobReader(blob)).getEntries();
    // 创建一个空列表用来储存转换好的资源
    const list: Record<string, Texture | any> = Object.create(null);
    // 遍历文件列表并且处理文件
    for (let index = 0; index < entries.length; index++) {
        // 获取当前文件
        const file: Entry = entries[index],
            //获取文件的后缀名
            suffix: string = file.filename.split(".").pop();
        // 根据文件名处理对应的素材
        if (suffix == "json") list[file.filename] = await json(file);
        if (suffix == "png") list[file.filename] = await texture(file);
    }
    return list;
}

/**
 * 获取纹理
 * @param entry zip文件条目
 * @returns 纹理
 */
async function texture(entry: Entry): Promise<Texture> {
    // 获取图片标签
    const image: HTMLImageElement = await getImage(entry),
        // 设置纹理
        texture: Texture = Texture.from(image);
    // 返回纹理
    return texture;
}

/**
 * 解析JSON
 * @param entry zip文件条目
 * @returns json
 */
async function json(entry: Entry): Promise<any> {
    // 返回zip文件内容转换成JSON对象
    return JSON.parse(await entry.getData(new TextWriter("")));
}

/**
 * 获取图片
 * @param entry zip文件条目
 * @returns 图片标签
 */
async function getImage(entry: Entry): Promise<HTMLImageElement> {
    return new Promise<HTMLImageElement>(async function (resolve) {
        // 创建一个图片标签
        const image: HTMLImageElement = new Image(),
            // 获取zip文件的Blob对象
            blob: Blob = await entry.getData(new BlobWriter("image/png"));
        // 将图片标签的地址设置为Blob的路径
        image.src = URL.createObjectURL(blob);
        // 加载完成后则跳出返回这个图片标签
        image.onload = function () {
            resolve(image);
        }
    });
}

/**
 * 将压缩包文件裁剪并转换成需要的皮肤配置
 * @param zipList 压缩包列表
 */
function zipListToSkinConfig(zipList: Record<string, Texture | any>): Record<number, SkinConifg> {
    // 角色皮肤配置文件
    const skinFile = zipList["skin.json"];
    // 存放整个角色皮肤配置的数组
    const group: Record<number, SkinConifg> = Object.create(null);
    // 遍历角色配置并处理每一个动作的皮肤
    for (let index = 0; index < skinFile.length; index++) {
        // 当前动作的配置数据
        const skinFileConfig: SkinFileConifg = skinFile[index],
            // 获取当前动作的纹理总数
            total: number = skinFileConfig.direction * skinFileConfig.frameNumber,
            // 获取当前动作的纹理集
            textureSet: Texture = zipList[index + ".png"],
            // 存放裁剪完成后的纹理数组
            animation: Texture[][] = [];
        // 遍历纹理并裁剪
        for (let index = 0; index < total; index++) {
            // 获取当前动作的帧数
            const x: number = index % skinFileConfig.frameNumber,
                // 获取当前动作的方向
                y: number = (index - x) / skinFileConfig.frameNumber,
                // 序列帧的宽度
                width: number = skinFileConfig.width,
                // 序列帧的高度
                height: number = skinFileConfig.height,
                // 设置裁剪的区域
                rectangle: Rectangle = new Rectangle(x * width, y * height, width, height),
                // 裁剪的纹理
                texture: Texture = new Texture(textureSet.baseTexture, rectangle);
            // 获取当前方向的序列帧数组没有则创建
            animation[y] = animation[y] || [];
            // 将纹理放到数组内
            animation[y].push(texture);
        }
        // 设置对应配置群组
        group[index] = { animation, ...skinFileConfig };
    }
    // 返回角色配置群组
    return group;
}

// 加载默认皮肤
CharacterSkin.loadSkin("default", null, null);

我在代码中预加载了一个黑色的动画黑影,为了以后切换皮肤的时候,有个动画代替切换时候的空白

角色的显示

import { Sprite, Texture } from "pixi.js";
import { Timer } from "../utils/Timer";
import { CharacterSkin } from "./CharacterSkin";

export class CharacterView extends Sprite {
    /**
     * 角色配置文件
     */
    private config: CharacterSkin;

    /**
     * 序列帧列表
     */
    private animation: Texture[] = [];

    /**
     * 当前帧数
     */
    public currentFrame: number = 0;

    /**
     * 序列帧总帧数
     */
    public frameNumber: number = 0;

    /**
     * 播放速度
     */
    public speed: number = 0;

    /**
     * 皮肤名称
     */
    public skinName: string = "";

    /**
     * 动作编号
     */
    public action: number = 0;

    /**
     * 方向
     */
    public direction: number = 0;


    /**
     * 循环动作
     */
    public loop: boolean = true;

    /**
     * 角色显示对象
     * 目的是为了在Pixi.js内的显示
     */
    public constructor() {
        super();
        // 使用默认皮肤
        this.setSkin("default");
        // todo 动画播放事件
        Timer.get().loop(this.speed, this, this.updateAnimation);
    }

    /**
     * 皮肤加载完成后的回调
     */
    private onSkinLoadComplete(skinName: string): void {
        // 设置角色皮肤配置
        this.config = CharacterSkin.get(skinName);
        // 更新下新皮肤的动作设置
        this.changeAction();
    }

    /**
     * 改变动作
     */
    private changeDirection(): void {
        // 未加载完成皮肤则不执行
        if (!this.config) return;
        // 获取当前动作的序列帧
        const animation = this.config.getConfig(this.action).animation;
        // 改变当前序列帧的方向
        this.animation = animation[this.direction];
        // 序列帧重新归零播放
        this.currentFrame = 0;
        // 更新皮肤纹理
        this.updateAnimation();
    }

    /**
     * 改变角色动作
     */
    private changeAction(): void {
        // 未加载完成皮肤则不执行
        if (!this.config) return;
        // 获取当前动作的配置
        const config = this.config.getConfig(this.action);
        // 设置属性
        this.pivot.x = config.pivotX;
        this.pivot.y = config.pivotY;
        this.speed = config.speed;
        this.frameNumber = config.frameNumber;
        // 改变方向
        this.changeDirection();
        // 更新序列帧的速度
        Timer.get().clear(this, this.updateAnimation);
        Timer.get().loop(this.speed, this, this.updateAnimation);
    }

    /**
     * 设置皮肤
     * @param name 皮肤名称
     */
    public setSkin(name: string): void {
        // 避免相同皮肤重复执行
        if (this.skinName == name) return;
        // 删除上个皮肤未加载完成的回调 避免出现上个皮肤未加载完成 就切换皮肤的情况
        CharacterSkin.clearCallback(this.skinName, this.onSkinLoadComplete, this);
        // 使用默认角色皮肤
        CharacterSkin.loadSkin("default", this.onSkinLoadComplete, this);
        // 更新皮肤名称
        this.skinName = name;
        // 加载角色皮肤
        CharacterSkin.loadSkin(name, this.onSkinLoadComplete, this);
    }

    /**
     * 设置动作
     * @param id 动作编号
     */
    public setAction(id: number): void {
        // 如果设置项一致则结束
        if (this.action == id) return;
        // 设置动作编号
        this.action = id;
        // 改变动作的设置
        this.changeAction();
    }

    /**
     * 设置方向
     * @param id 方向编号 0-7
     */
    public setDirection(id: number): void {
        // 如果设置项一致则结束
        if (this.direction == id) return;
        // 设置方向
        this.direction = id;
        // 改变序列帧的方向
        this.changeDirection();
    }

    /**
     * 更新角色动画
     */
    public updateAnimation(): void {
        // 更新下一帧
        this.currentFrame = ++this.currentFrame % this.frameNumber;
        // 设置角色动作
        this.texture = this.animation[this.currentFrame];
    }
}

image.gif
这个就是那个黑影动画的切换效果