Login
芋圆社区 > 编程 > 【项目】桌宠之旅 > 15-实现桌宠模组切换

15-实现桌宠模组切换

701
0
2023-07-08
2023-07-18
Hey、小怪兽

  • • 之前就注意到我这里modules创了三个文件夹分别代表可可萝kkr,凯露kru,佩可pko,之前的教程都是完成可可萝的,其他两个也一样,配置好全部的文件后就可以进行模组切换了:
  • • 首先需要electron-store进行本地化的存储,在终端输入安装:
  • pnpm add electron-store
  • • 安装完后package.json多了一行:
  • "dependencies": {
      "electron": "24.3.1",
      "electron-store": "^8.1.0",
      "nodemon": "^2.0.22"
    }
  • • 在主进程(/src/_main/main.js)引入:
  • // electron-store本地化存储
    const Store = require("electron-store");
  • • 接着配置这个存储:
  • - name就是文件的名称,没配就是config
  • - encryptionKey是加密的方式,现在先不用
  • let option = {
        name: "taro_pet_config", //文件名称,默认 config
        // encryptionKey: "aes-256-cbc", //对配置文件进行加密
    };
  • • new一个存储,初始化,顺便打印一下存储的路径,方便看存储的内容:
  • const store = new Store(option);
    console.log(store.path);
  • • 可以理解为这个存储是存在用户的电脑里,如果用户之前使用的是佩可或是凯露的桌宠,那么就会存pko或kru,如果第一次使用就默认可可萝kkr的桌宠
  • // 用来获取上一次使用的模组
    const setModuleName = () => {
        if (store.has("last_module") === false) {
            // 默认可可萝模组
            getModuleData("kkr");
        } else {
            getModuleData(store.get("last_module"));
        }
    };
  • • getModuleData也就是模组的方法:
  • - 上面说了如果第一次使用的话,那么就会传入可可萝kkr,那么这个last_module就会存储kkr
  • - 接着移除监听,再监听预加载脚本的get-module-data,如果监听到了的话就传name,也就是kkr
  • // 获取模组数据并监听
    const getModuleData = (name) => {
        // 存储当前模组
        store.set("last_module", name);
        // 先移除
        ipcMain.removeHandler("get-module-data");
        // 主进程监听渲染进程发送的请求,返回相应数据
        ipcMain.handle("get-module-data", () => {
            return {
                name: name,
            };
        });
    };
  • • 在创建窗口前执行就行了,还得修改为async(async搭配await使用,等待setModuleName方法执行完才会执行下面的):
  • app.whenReady().then(async () => {
        await setModuleName();
        createMainWindow();
        createTray();
    });
  • • 接着前往预加载脚本/src/_preload/preload.js添加上,主要用来给渲染进程传主进程的存储数据,也就是传kkr
  • // 在 contextBridge 中暴露方法,仅允许渲染进程访问需要的接口
    contextBridge.exposeInMainWorld("configAPI", {
        // 获取主进程中的数据
        getModuleData: async () => {
            return await ipcRenderer.invoke("get-module-data");
        },
    });
  • • 最后在渲染进程/src/_renderer/renderer.js,先把之前的当前模组的变量改为null,这样就知道代码写的对不对:
  • // 当前使用的模型
    // 之前的let current_module = 'kkr';
    let current_module = null;
  • • 在加载方法里:
  • // 在渲染进程中获取数据
    const config_data = await window.configAPI.getModuleData();
    current_module = config_data.name;
  • • 最后实现切换就行了,之前不是搞了系统托盘,先准备好三个图片(可以去B站Wiki拿,或者解包里):
  • • 在主进程(/src/_main/main.js)系统托盘里增加:
  • { type: "separator" },
    {
        label: "可可萝",
        type: "normal",
        icon: nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/kkr.png")),
        click: () => {
            getModuleData("kkr");
            mainWindow.reload();
        },
    },
    {
        label: "凯露",
        type: "normal",
        icon: nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/kru.png")),
        click: () => {
            getModuleData("kru");
            mainWindow.reload();
        },
    },
    {
        label: "佩可莉姆",
        type: "normal",
        icon: nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/pko.png")),
        click: () => {
            getModuleData("pko");
            mainWindow.reload();
        },
    },
  • • 最后在终端pnpm start,可以看到之前打印的console.log(store.path)方法出来了,打印出了我们存储数据的本地位置:
  • • 可以直接Ctrl+鼠标左键点击这个地址,或者自己去找,当前的模组就是可可萝kkr,
  • • 切换的话,右键点击这个系统盘:
  • • 如下图GIF所示:
  • • 完美!已经完成了桌宠模组的切换了!
  • • 主进程/src/_main/main.js完整代码;
  • const { app, screen, ipcMain, Menu, Tray, BrowserWindow, nativeImage } = require("electron");
    const path = require("path");
    // 主窗口
    let mainWindow = null;
    // 拖拽的初始位置
    let drapPosition = { x: 0, y: 0 };
    // 拖拽定时器
    let drapTimer = null;
    
    // electron-store本地化存储
    const Store = require("electron-store");
    let option = {
        name: "taro_pet_config", //文件名称,默认 config
        // encryptionKey: "aes-256-cbc", //对配置文件进行加密
    };
    const store = new Store(option);
    console.log(store.path);
    
    // 用来获取上一次使用的模组
    const setModuleName = () => {
        if (store.has("last_module") === false) {
            // 默认可可萝模组
            getModuleData("kkr");
        } else {
            getModuleData(store.get("last_module"));
        }
    };
    
    // 获取模组数据并监听
    const getModuleData = (name) => {
        // 存储当前模组
        store.set("last_module", name);
        // 先移除
        ipcMain.removeHandler("get-module-data");
        // 主进程监听渲染进程发送的请求,返回相应数据
        ipcMain.handle("get-module-data", () => {
            return {
                name: name,
            };
        });
    };
    
    const createMainWindow = () => {
        // 获取当前桌面的宽度和高度
        const size = screen.getPrimaryDisplay().workAreaSize;
        const { width, height } = size;
    
        mainWindow = new BrowserWindow({
            width: 390,
            height: 390,
            // 起始位置是屏幕宽度减去窗口宽度,再减去10个像素
            x: width - 390 - 10,
            y: height - 390 - 10,
            // 隐藏菜单栏
            autoHideMenuBar: true,
            // 设置为透明窗口
            transparent: true,
            // 隐藏窗口边框
            frame: false,
            // 窗口置顶
            alwaysOnTop: true,
            // 隐藏任务栏图标
            skipTaskbar: true,
            // 禁止改变窗口大小
            resizable: false,
            // 先隐藏窗口
            show: false,
            // Preload 脚本
            webPreferences: {
                preload: path.resolve(__dirname, "../_preload/preload.js"),
            },
        });
    
        // 允许鼠标穿透
        mainWindow.setIgnoreMouseEvents(true, { forward: true });
    
        // 开启调试工具
        mainWindow.webContents.openDevTools();
    
        mainWindow.loadFile(path.resolve(__dirname, "../index.html"));
    
        mainWindow.on("ready-to-show", () => {
            mainWindow.show();
        });
    };
    
    // 系统托盘方法
    const createTray = () => {
        const icon = nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/icon@16.png"));
        tray = new Tray(icon);
        const contextMenu = Menu.buildFromTemplate([
            { label: "TaroPet v1.0", type: "normal", enabled: false, icon: icon },
            { type: "separator" },
            {
                label: "可可萝",
                type: "normal",
                icon: nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/kkr.png")),
                click: () => {
                    getModuleData("kkr");
                    mainWindow.reload();
                },
            },
            {
                label: "凯露",
                type: "normal",
                icon: nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/kru.png")),
                click: () => {
                    getModuleData("kru");
                    mainWindow.reload();
                },
            },
            {
                label: "佩可莉姆",
                type: "normal",
                icon: nativeImage.createFromPath(path.resolve(__dirname, "../public/icon/pko.png")),
                click: () => {
                    getModuleData("pko");
                    mainWindow.reload();
                },
            },
            { type: "separator" },
            {
                label: "重新加载",
                type: "normal",
                click: () => {
                    mainWindow.reload();
                },
            },
            {
                label: "退出",
                type: "normal",
                click: () => {
                    tray.destroy(); // 销毁托盘
                    app.quit(); // 退出应用程序
                },
            },
        ]);
    
        tray.setToolTip("TaroPet v1.0");
        tray.setContextMenu(contextMenu);
    }
    
    app.whenReady().then(async () => {
        await setModuleName();
        createMainWindow();
        createTray();
    });
    
    // 鼠标移动监听,用于判断是否需要穿透
    ipcMain.on("mouse-move", (event, obj) => {
        if (obj.ignore) {
            mainWindow.setIgnoreMouseEvents(true, { forward: true });
        } else {
            mainWindow.setIgnoreMouseEvents(false);
        }
    });
    
    // 桌宠拖动开始,记录点击位置,让窗口粘着鼠标
    ipcMain.on("mouse-drap-start", (event, obj) => {
        drapPosition = {
            x: obj.x,
            y: obj.y,
        };
        drapTimer = setInterval(() => {
            const { x, y } = screen.getCursorScreenPoint();
            mainWindow.setPosition(x - drapPosition.x, y - drapPosition.y);
        }, 10);
    });
    
    // 桌宠拖动结束,也就是再按一下右键,定时器清空
    ipcMain.on("mouse-drap-end", (event, obj) => {
        clearInterval(drapTimer);
    });
    
  • • 预加载脚本/src/_preload/preload.js完整代码:
  • const { contextBridge, ipcRenderer } = require("electron");
    
    contextBridge.exposeInMainWorld("mouseAPI", {
        mouseMove: (obj) => ipcRenderer.send("mouse-move", obj),
        mouseDrapStart: (obj) => ipcRenderer.send("mouse-drap-start", obj),
        mouseDrapEnd: (obj) => ipcRenderer.send("mouse-drap-end", obj),
    });
    
    // 在 contextBridge 中暴露方法,仅允许渲染进程访问需要的接口
    contextBridge.exposeInMainWorld("configAPI", {
        // 获取主进程中的数据
        getModuleData: async () => {
            return await ipcRenderer.invoke("get-module-data");
        },
    });
  • • 渲染进程/src/_renderer/renderer.js完整代码:
  • // 导入方法
    import { get_pixel_color_func, body_area_func } from "../utils/anim.js";
    // 当前使用的模型
    let current_module = null;
    // 初始配置和动作配置
    let init_config = null;
    let action_config = null;
    let voice_config = null;
    // 当前的动画
    let anim_current = null;
    // 初始动画
    let anim_normal = null;
    // 拖拽动画
    let anim_drap = null;
    // 存储的随机动作索引,用来给定时器随机
    let anim_action_random_index = [];
    let anim_action_random_cache = [];
    // 动画的定时器
    let timer = null;
    // 拖拽状态
    let drag_static = false;
    // 语音状态
    let talk_static = false;
    // 动画的舞台
    let app = null;
    // 音频的dom
    let audio_dom = null;
    // 词板的dom
    let dialog_dom = null;
    // 动画的canvas
    let anim_canvas = null;
    // 虚拟的canvas
    let shadow_canvas = null;
    // 获取窗口的大小
    const bower_width = window.innerWidth;
    const bower_height = window.innerHeight;
    
    // 页面加载完成执行
    window.addEventListener("load", async (event) => {
        // 在渲染进程中获取数据
        const config_data = await window.configAPI.getModuleData();
        current_module = config_data.name;
    
        // 获取配置
        const module = await import(`../modules/${current_module}/${current_module}.js`);
        init_config = module.init_config;
        action_config = module.action_config;
        voice_config = module.voice_config;
    
        // 赋值canvas
        anim_canvas = document.getElementById("taro-canvas");
        shadow_canvas = document.getElementById("shadow-canvas");
        shadow_canvas.width = bower_width;
        shadow_canvas.height = bower_height;
    
        // 赋值音频dom,词板dom
        audio_dom = document.getElementById("taro-pet-audio");
        dialog_dom = document.getElementById("taro-pet-dialog");
    
        // 动画舞台配置
        app = new PIXI.Application({
            view: anim_canvas,
            width: bower_width,
            height: bower_height,
            backgroundAlpha: 0,
            resolution: 1,
        });
        // 添加给div-taropet元素
        document.getElementById("taro-pet").appendChild(app.view);
    
    
        // 先把初始的动画加载完成
        anim_normal = await create_anim_func(init_config[0], 0);
        anim_normal.play();
        app.stage.addChild(anim_normal);
        // 赋值给当前动画
        anim_current = anim_normal;
    
        // 开始初始化其他的动画
        anim_drap = await create_anim_func(init_config.find((obj) => obj.name === "drap"), 0);
    
        // 将动作的配置转换成随机索引赋值 [0,1,2,3,4]
        anim_action_random_index = Array.from(
            action_config.map((obj, index) => {
                obj.index = index;
                return obj;
            }),
            ({ index }) => index
        );
    
        // 开启定时器
        setIntervalTimer();
    });
    
    // 创建动画的方法 obj-配置对象, type-是否初始化0/1
    const create_anim_func = async (obj, type) => {
        // 存放文件前缀, 文件格式(png,jpg)
        const file_prefix = "./modules";
        const file_format = ".png";
        const { name, frames, object } = obj;
        const texture_array = [];
        // 通过帧数循环获取贴图
        for (let i = 0; i < frames; i++) {
            const num = `000${i}`.slice(-3);
            // texture_name ./modules/kkr/normal/001.png
            const texture_name = type === 0 ? `${file_prefix}/${current_module}/${name}/${num}${file_format}` : `${file_prefix}/${current_module}/action/${name}/${num}${file_format}`;
            const texture = await PIXI.Texture.from(texture_name);
            texture_array.push(texture);
        }
        // 生成动画,配置动画属性
        const anim = new PIXI.AnimatedSprite(texture_array);
        anim.name = name;
        anim.animationSpeed = 0.5;
        anim.loop = object.loop;
    
        // 设置交互模式
        anim.eventMode = "dynamic";
    
        // 鼠标移动事件
        anim.on("mousemove", (event) => {
            const global_position = event.data.global;
            const local_position = anim.toLocal(global_position);
            // 当前这一帧的动画贴图
            const anim_img = anim.texture.baseTexture.resource.source;
    
            if (drag_static) {
                // 这个时候在拖拽,什么都不做
            } else {
                body_area_func(get_pixel_color_func(local_position.x, local_position.y, anim_img), local_position)
            }
        });
    
        // 左键点击播放语音
        anim.on("click", (event) => {
            if (drag_static === false && talk_static === false) {
                talk_static = true;
                // 随机获取音频的index
                const index = Math.floor(Math.random() * voice_config.length);
                const voice_data = voice_config[index];
                // 设置音频文件的 URL
                audio_dom.src = `${file_prefix}/${current_module}/voice/${voice_data.voice}`;
                // 设置词板
                dialog_dom.style.color = voice_data.color;
                // 这里必须要一同设置repeat和size等,不然背景图会变形
                dialog_dom.style.background = `url("./public/images/${voice_data.dialog}") no-repeat center / 100% 100%`;
                dialog_dom.style.opacity = 1;
                dialog_dom.innerHTML = voice_data.element;
                const onAudioEnded = () => {
                    talk_static = false;
                    dialog_dom.style.opacity = 0;
                    // 移除事件监听器
                    audio_dom.removeEventListener("ended", onAudioEnded);
                };
                audio_dom.addEventListener("ended", onAudioEnded);
                audio_dom.play();
            }
        });
    
        // 鼠标点击右键拖拽
        anim.on("rightclick", (event) => {
            const global_position = event.data.global;
            const local_position = anim.toLocal(global_position);
            if (drag_static === false) {
                // 如果没在拖拽状态,右键后进入推拽状态,传给主进程点击的位置
                window.mouseAPI.mouseDrapStart({
                    x: local_position.x,
                    y: local_position.y,
                    drap: true,
                });
                // 开启拖拽状态进入拖拽动画
                drag_static = true;
                change_anim_func(anim_current, anim_drap, 0);
            } else {
                // 再次点击脱离拖拽状态
                window.mouseAPI.mouseDrapEnd({
                    drap: false,
                });
                // 取消拖拽状态进入普通动画
                drag_static = false;
                change_anim_func(anim_drap, anim_normal, 0);
            }
        });
    
        if (object.loop === false) {
            anim.onComplete = () => {
                // 完成动作后500毫秒后进入普通动画
                change_anim_func(anim, anim_normal, object?.endTime ?? 100);
            };
        }
    
        if (type === 1) {
            // 缓存随机动作,这样下次不需要再次生成
            anim_action_random_cache.push(anim);
            // 给生成动画时间
            setTimeout(() => {
                // 生成动画后1秒后进入动作动画
                if (drag_static === false) {
                    change_anim_func(anim_normal, anim, 0);
                }
            }, 1000);
        } else {
            // 如果是初始动画的话就返回动画
            return anim;
        }
    };
    
    // 进入动画,可以用来切换动画(切换回normal或进入drap)
    const change_anim_func = (from_anim, to_anim, time) => {
        from_anim.stop();
        setTimeout(() => {
            app.stage.removeChild(from_anim);
            to_anim.gotoAndPlay(0);
            app.stage.addChild(to_anim);
            // 替换当前动画
            anim_current = to_anim;
        }, time);
    };
    
    // 设置定时器,用来一定时间播放一次随机动作
    const setIntervalTimer = () => {
        timer = setInterval(() => {
            if (drag_static === false && talk_static === false) {
                // 随机获取动作的index
                const index = Math.floor(Math.random() * anim_action_random_index.length);
                // 通过index获取动作的配置
                const action = action_config[anim_action_random_index[index]];
                // 如果有缓存的动作就不需要生成了
                const cacheAction = anim_action_random_cache.find((obj) => obj.name === action.name);
    
                if (cacheAction) {
                    change_anim_func(anim_normal, cacheAction, 0);
                } else {
                    create_anim_func(action, 1);
                }
            } else {
                // 说明是在拖拽,什么都不做
            }
        }, 1000 * 60 * 5);
    };

上一篇:14-设置系统托盘

下一篇:16-桌宠数据和状态拓展

Message Board
回复
回复内容不允许为空
留言字数要大于2,小于200!
提交成功,5s后刷新页面!
编程导航

0-桌宠开发前言

1-搭建环境和创建简单的应用程序

2-修改项目结构

3-创建一个透明的窗口

4-nodemon实现热加载

5-获取桌宠每一帧图片

6-Pixi.js加载桌宠初始动画

7-添加鼠标事件

8-获取区域颜色和判断桌宠本体

9-设置进程通信和鼠标穿透

10-实现桌宠拖拽

11-实现桌宠随机动作

12-实现桌宠播放语音

13-实现桌宠词板功能

14-设置系统托盘

15-实现桌宠模组切换

16-桌宠数据和状态拓展

17-生成ICO图标和准备打包

18-完成开发并打包桌宠应用

Copyright © 2020 芋圆社区

Powered by 浙ICP备2020039309号-1

此页面不支持夜间模式!

已进入夜间模式!

已进入普通模式!

搜索框不允许为空

签到成功!经验+5!芋圆币+2!

签到失败!今日已签到!

需要登录社区账号才可以进入!

复制成功
寄,页面未加载完成或页面无锚点