Login
芋圆社区 > 编程 > 【项目】桌宠之旅 > 13-实现桌宠词板功能

13-实现桌宠词板功能

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

  • • 词板也就是播放语音的时候出现的文本框,首先先解包公主连结的游戏(具体可以参考一些解包方法),拿到需要的词板图片:
  • • 放在/src/public/images文件夹下,取名取成card_颜色.png就行了:
  • • 接下去要写这个词板的样式了,先去/src/index.html写个div来装这个词板:
  • <div class="taro-pet-dialog" id="taro-pet-dialog"></div>
  • • 再去写CSS,/src/public/css/index.css:
  • - 词板初始是隐藏的,所以opacity是0
  • - user-select是用户选取为none
  • - 位置是写死的,用绝对定位,再加上flex布局,宽度写死250px
  • - 高度最小50px,让它自己拓展,最后加上transition过度不让词板出现特别突兀
  • - p标签就是一些文本的大小和字体等
  • .taro-pet-dialog {
        opacity: 0;
        user-select: none;
        position: absolute;
        left: 68px;
        bottom: 245px;
        display: flex;
        gap: 4px;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 250px;
        min-height: 50px;
        padding: 10px 21px 10px 35px;
        transition: all 0.5s ease-in-out;
    }
    
    .taro-pet-dialog p {
        font-size: 15px;
        font-family: "楷体";
        font-weight: 800;
        opacity: 0.9;
    }
  • • 最后到kkr.js配置(/src/modules/kkr/kkr.js):
  • - dialog:就是词板的样式,可可萝偏绿色选择了绿色的词板,
  • - color:就是文字的颜色,#33812a就是绿色的一种
  • - voice:音频的文件
  • - element:文本,就是音频的中文翻译
  • export const voice_config = [
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "1.mp3",
            element: `
                        <p>就让身为引导者的我</p>
                        <p>全力支持主人吧 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "2.mp3",
            element: `
                        <p>主人,肚子饿不饿?</p>
                        <p>任何事都要先</p>
                        <p>填饱肚子再说 ♪ ♪ ♪</p>
                    `,
        }
    
        ...其他配置
    ]
  • • 最后回到渲染进程/src/_renderer/renderer.js添加变量:
  • // 音频的配置
    let voice_config = null;
    // 词板的dom
    let dialog_dom = null;
  • • 同理,获取kkr.js配置的时候要获取音频配置:
  • // 获取配置
    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;
  • • 赋值词板的dom元素:
  • // 赋值音频dom,词板dom
    audio_dom = document.getElementById("taro-pet-audio");
    dialog_dom = document.getElementById("taro-pet-dialog");
  • • 修改鼠标点击的方法:
  • - 随机获取一个音频的索引index,音频的配置对象就是voice_config[index]
  • - 接着设置URL,设置词板的文本颜色
  • - 这里设置background,其实就是把词板的图片放到刚才HTML里创建的div里,然后不重复 居中 100%显示
  • - 接着修改透明度opacity为1,这样词板就显示出来了
  • - 在监听结束的方法里,再把词板的透明度改为0就隐藏了
  • // 左键点击播放语音
    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();
        }
    });
  • • 最后还是在终端pnpm start,点击可可萝的本体,成功显示词板并播放语音!
  • • /src/index.html完整代码:
  • <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>TaroPet</title>
        <link rel="stylesheet" href="./public/css/index.css" />
    </head>
    <body>
        <div id="taro-pet" class="taro-pet">
            <canvas id="taro-canvas"></canvas>
            <canvas id="shadow-canvas"></canvas>
        </div>
    
        <div class="taro-pet-dialog" id="taro-pet-dialog"></div>
    
        <audio id="taro-pet-audio" src="" controls="" preload=""></audio>
    
        <script src="./public/js/pixi.min.js"></script>
        <script src="./_renderer/renderer.js" type="module"></script>
    </body>
    </html>
  • • /src/public/css/index.css完整代码:
  • * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
    }
    
    html,
    body {
        width: 100vw;
        height: 100vh;
        overflow: hidden;
    }
    
    .taro-pet {
        width: 100vw;
        height: 100vh;
        transform: scaleX(-1);
    }
    
    .taro-pet-dialog {
        opacity: 0;
        user-select: none;
        position: absolute;
        left: 68px;
        bottom: 245px;
        display: flex;
        gap: 4px;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 250px;
        min-height: 50px;
        padding: 10px 21px 10px 35px;
        transition: all 0.5s ease-in-out;
    }
    
    .taro-pet-dialog p {
        font-size: 15px;
        font-family: "楷体";
        font-weight: 800;
        opacity: 0.9;
    }
    
    #taro-pet-audio {
        display: none;
    }
    
    #shadow-canvas {
        position: absolute;
        z-index: -1;
        opacity: 0;
    }
    
  • • /src/modules/kkr/kkr.js完整代码:
  • export const init_config = [
        {
            index: 0,
            name: "normal",
            frames: 60,
            texture: [],
            object: {
                loop: true,
            },
        },
        {
            index: 1,
            name: "drap",
            frames: 54,
            texture: [],
            object: {
                loop: true,
            },
        },
    ];
    
    export const action_config = [
        {
            index: 0,
            name: "dear_jump",
            frames: 66,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 1,
            name: "dear_smile",
            frames: 64,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 2,
            name: "high_touch",
            frames: 51,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 3,
            name: "joy_long",
            frames: 108,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 4,
            name: "joy_short",
            frames: 54,
            texture: [],
            object: {
                loop: false,
            },
        },
    
        {
            index: 5,
            name: "salute_smile",
            frames: 94,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 6,
            name: "stand_by",
            frames: 54,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 7,
            name: "weapon_joy",
            frames: 107,
            texture: [],
            object: {
                loop: false,
                endTime: 800,
            },
        },
        {
            index: 8,
            name: "weapon_skill",
            frames: 80,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 9,
            name: "weapon_ub",
            frames: 247,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 10,
            name: "cheer_joy",
            frames: 54,
            texture: [],
            object: {
                loop: false,
            },
        },
        {
            index: 11,
            name: "mana_jump",
            frames: 31,
            texture: [],
            object: {
                loop: false,
            },
        },
    ];
    
    export const voice_config = [
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "1.mp3",
            element: `
                        <p>就让身为引导者的我</p>
                        <p>全力支持主人吧 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "2.mp3",
            element: `
                        <p>主人,肚子饿不饿?</p>
                        <p>任何事都要先</p>
                        <p>填饱肚子再说 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "3.mp3",
            element: `
                        <p>看来是累了呢</p>
                        <p>管理健康也是我的职责</p>
                        <p>要是有什么吩咐</p>
                        <p>请尽管说 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "4.mp3",
            element: `
                        <p>能够服侍主人这样</p>
                        <p>温柔的人</p>
                        <p>我真是个幸福的人 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "5.mp3",
            element: `
                        <p>请让我像主人的夫人一样</p>
                        <p>来服侍主人吧 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "6.mp3",
            element: `
                        <p>为了与主人相遇做准备</p>
                        <p>我从小时候起</p>
                        <p>就和精灵交谈</p>
                        <p>请尽管使唤我吧 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "7.mp3",
            element: `
                        <p>被主人注视着</p>
                        <p>精灵们好像也很高兴 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "8.mp3",
            element: `
                        <p>我会与大自然一同</p>
                        <p>助主人一臂之力 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "9.mp3",
            element: `
                        <p>我从年幼时起</p>
                        <p>就一直期待着</p>
                        <p>能够服侍主人 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "10.mp3",
            element: `
                        <p>有什么事就请呼唤我</p>
                        <p>我会全心全意地</p>
                        <p>回应主人的要求 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "11.mp3",
            element: `
                        <p>是与主人的情谊</p>
                        <p>催生出了这般的力量吧</p>
                        <p>主人……从今往后</p>
                        <p>也请一直让我陪伴在您身边</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "12.mp3",
            element: `
                        <p>我的感情</p>
                        <p>得到了精灵们的回应……</p>
                        <p>有了这股力量</p>
                        <p>今后就能更好地服侍主人了</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "13.mp3",
            element: `
                        <p>多么轻柔的微风…</p>
                        <p>呵呵……简直就像是精灵们</p>
                        <p>在为我和主人</p>
                        <p>送来祝福呢 ♪ ♪ ♪</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "14.mp3",
            element: `
                        <p>花瓣宛如新娘的捧花</p>
                        <p>我和主人也……哈!</p>
                        <p>我、我竟然,</p>
                        <p>会去想象这么逾矩的事情</p>
                    `,
        },
        {
            dialog: "card_green.png",
            color: "#33812a",
            voice: "15.mp3",
            element: `
                        <p>有了这股力量</p>
                        <p>就能更加周到地照顾主人了</p>
                        <p>自早至晚</p>
                        <p>全都交给我吧 ♪ ♪ ♪</p>
                    `,
        },
    ];
    
  • • 渲染进程/src/_renderer/renderer.js完整代码:
  • // 导入方法
    import { get_pixel_color_func, body_area_func } from "../utils/anim.js";
    // 当前使用的模型
    let current_module = "kkr";
    // 初始配置和动作配置
    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 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);
    };

上一篇:12-实现桌宠播放语音

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

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!

签到失败!今日已签到!

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

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