前言
利用开发出来的工具把角色素材拼合到了一起,但是放到画布中,我们得到确是一个合集的图片,我们需要做的是将这个纹理集拆开,变成纹理,然后被我们的精灵引用
如此图所显示的,右侧的是角色动作的集合是一个精灵,左侧的三个红圈(为了醒目)的角色也是单独的精灵。
但是在Pixi.js的内部,他们的引用最终解析的是一个名为BaseTexture的基础纹理,下一步就是Texture纹理,可以被拆开也可以作为单独的一张图片引用。
最后作为最关键的显示,是Sprite的精灵,可以看到左侧的三个红圈的角色是一模一样的,但是在Pixi.js的内部,是分为三个精灵的,三个精灵共同引用一个纹理,而这个纹理是从一个纹理集的一部分。
我们可以引用其他的纹理再给舞台添加三个精灵,放到对应的角色精灵上,充当装备的显示精灵
甚至也可以改变角色精灵的纹理
以上的效果均为PS实现,真实的情况是此刻我还没有敲下一行角色显示到Pixi.js的代码
上个教程的一些问题
Pixi.js的坐标系是从左上角开始计算的,同样放置在舞台的精灵,也是从左上角开始计算的。
上一个教程中仅仅是获取了帧数,方向数量,宽高。还需要额外增加序列帧播放速度,还有每个动作的锚点
锚点就是图中每个动作脚下的那个点,举个例子,把他们放到(500,500)的坐标,只会让图片的左上角开始激素按,而不是脚下刚好踩上那个(500,500)的点,而且切换动作的时候,有明显的异常晃动。
锚点如果没有设置好的话,就会和这张图片一样,切换后有个明显的错位
直接解压缩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];
}
}
这个就是那个黑影动画的切换效果