# 自定义动画

1.2.0 版本添加了自定义动画功能,现在通过书写 JavaScript 脚本文件,即可实现自定义动画。

# 说明

  • 本说明适用于 1.2.0 版本及以上 Touhou Little Maid 模组;
  • 需要对 JavaScript 基本语法有简单的了解;
  • 需要高中及以上数学知识,尤其是对三角函数和极坐标的理解。
  • 文本编辑软件推荐 VSCode,相关文本文件均需要用 UTF-8 无 BOM 编码进行存储。

# 基本格式

动画脚本放置在文件夹任意位置均可,只需要在对应模型字段出申明动画文件位置即可。但出于规范性考虑,默认我们放置在 animation 文件夹下。

下面是它的一般通用模板:

var GlWrapper = Java.type("com.github.tartaricacid.touhoulittlemaid.client.animation.script.GlWrapper");

Java.asJSONCompatible({
    /**
     * @param entity 需要应用动画的实体对象
     * @param limbSwing 实体在行走过程中的速度(可以理解为汽车的速度表)
     * @param limbSwingAmount 实体行走的总里程数(可以理解为汽车的里程表)
     * @param ageInTicks 实体的 tick 时间,一个从 0 开始一直增加的数值
     * @param netHeadYaw 实体头部的偏航
     * @param headPitch 实体头部的俯仰
     * @param scale 实体缩放参数,默认为 0.0625
     * @param modelMap 为一个 map,存储了该模型所有的骨骼
     */
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
                          headPitch, scale, modelMap) {
        // 相关动画的书写
    }
})

这里我们举一个简单的例子,当前模型有一个带有名为 rotation 的骨骼,我们想要把让这个骨骼绕着 X 轴持续的做旋转运动,运动的速度大约为每 tick 1 度(也就是 18 秒转一圈),我们可以这样写动画。

var GlWrapper = Java.type("com.github.tartaricacid.touhoulittlemaid.client.animation.script.GlWrapper");

Java.asJSONCompatible({
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
                          headPitch, scale, modelMap) {
        // 先从 modelMap 中尝试获取名为 rotation 的骨骼
        rotation = modelMap.get("rotation");
        // 以防万一,我们做个简单的判定,确保此骨骼一定存在
        if (rotation != undefined) {
            // 通过骨骼的 setRotateAngleX 函数设置其 X 轴角度
            // ageInTicks 为实体的 tick 时间,一个从 0 开始一直增加的数值
            // 通过取余运算(也就是 % 符号)将这个数限定在 0~360 之间
            // 因为该方法只接收弧度值,所以需要乘以 0.017453292 转换成对应弧度
            // 这样我们就实现了每 tick 旋转 1 度的动画
            rotation.setRotateAngleX(ageInTicks % 360 * 0.017453292);
        }
    }
})

现在我们再进行一个更加复杂的运动,我们有一个名为 wing 的骨骼,我们想要其能够持续的来回摆动。

摆动围绕 Y 轴,摆动角度在 -20°~40° 之间,每 5 秒做一次完整的往复运动。

这一块恰好需要用到高中所学的三角函数知识,这一块选取正弦或者余弦均可,我们使用正弦函数。

var GlWrapper = Java.type("com.github.tartaricacid.touhoulittlemaid.client.animation.script.GlWrapper");

Java.asJSONCompatible({
    animation: function (entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw,
        headPitch, scale, modelMap) {
        // 先从 modelMap 中尝试获取名为 wing 的骨骼
        wing = modelMap.get("wing");
        // 以防万一,我们做个简单的判定,确保此骨骼一定存在
        if (wing != undefined) {
            // 每 5 秒完整的往复一次,也就是 100 tick
            // 通过乘法和求余来实现这个功能
            var time = (ageInTicks * 3.6) % 360;
            // 这一块调用了 JavaScript 的 Math 函数
            // 构建正弦函数,获得数值为 -20~40 的周期函数
            var func = 30 * Math.sin(time * 0.017453292) + 10;
            // 最后进行参数的应用
            wing.setRotateAngleY(func * 0.017453292);
        }
    }
})

其他复杂的运动均可通过相关函数来实现。

# 游戏内热重载功能

因为干巴巴的函数式并不能一下确定该动画是否表现正确,我们添加了游戏内的动画热重载功能,只需要选择你正在编辑的动画脚本文件,每次保存文件后游戏均会自动加载该脚本,从而达到快速调试的目的。

001

点击此按钮后会弹出选择文件的对话框(有概率会被游戏本身挡住,最小化游戏后即可看到文件选择对话框)。

选择文件后,即进入热重载模式。此时对脚本文件的任何修改,保存后均会自动加载。

注意

退出此界面后热重载会自动关闭。

# 函数文档

# entity 参数

依据附加动画的对象不同,entity 参数可用的函数也不相同。

# 女仆

函数名 返回值 备注
hasHelmet() boolean 女仆穿戴头盔后,返回 true
hasChestPlate() boolean 女仆穿戴胸甲后,返回 true
hasBoots() boolean 女仆穿戴靴子后,返回 true
isBegging() boolean 女仆当前是否处于祈求状态
isSwingingArms() boolean 当女仆使用手臂时,此函数会返回 true
isRiding() boolean 女仆是否处于骑乘状态
isSitting() boolean 女仆是否处于待命状态
isHoldTrolley() boolean 女仆是否持有拉杆箱等实体
isRidingMarisaBroom() boolean 女仆是否持有骑乘扫帚
isRidingPlayer() boolean 女仆是否骑乘玩家
isHoldVehicle() boolean 女仆是否使用载具
isSwingLeftHand() boolean 女仆是否使用的是左臂还是右臂,如果是右臂,返回 false
getLeftHandRotation() float[3] 获取载具的左手旋转数据
getRightHandRotation() float[3] 获取载具的右手旋转数据
getDim() int 获取女仆所处的维度

# 坐垫

函数名 返回值 备注
isRidingPlayer() boolean 该坐垫是否被玩家骑乘

# limbSwing 和 limbSwingAmount 参数

均为浮点数,limbSwing 实体在行走过程中的速度(可以理解为汽车的速度表),limbSwingAmount 实体行走的总里程数(可以理解为汽车的里程表)。

这两处数据主要用于腿部和手臂的摆动,原版有一个基础的数学公式对其运动进行描述,并用到了这两个数据。

Math.cos(limbSwing * 0.6662) * limbSwingAmount(左手)

-Math.cos(limbSwing * 0.6662) * limbSwingAmount(右手)

Math.cos(limbSwing * 0.6662) * limbSwingAmount * 1.4(左腿)

-Math.cos(limbSwing * 0.6662) * limbSwingAmount * 1.4(右腿)

更改 0.6662 这个数据可以控制摆动的频率,为整个公式乘上系数(比如腿部运动就乘上了 1.4 这个系数)可以更改摆动的幅度。

使用原版的手臂、腿部摆动公式可以做出更加自然的摆动动画。

# ageInTicks 参数

浮点数,一个从 0 开始每 tick 自增的变量,用于绝大部分动画函数中自变量参数。

# netHeadYaw 和 headPitch 参数

均为浮点数,且均为角度值(原版就是这么设计的)。头部运动时传入的参数。

一般来说此参数可直接使用作为旋转角度,只需要将其转换为弧度值即可。

head.setRotateAngleX(headPitch * 0.017453292);
head.setRotateAngleY(netHeadYaw * 0.017453292);

注意

这块的系数如果设置的大于 0.017453292,可能会出现头部偏转错误的问题。

# scale 参数

浮点数,固定为 0.0625。

意味不明的数值。

# modelMap 参数

一个存储了骨骼的 Map,以字符串为键。

直接通过 modelMap.get("xxx") 可获取对应骨骼对象。如果对应名称的骨骼不存在,返回 undefined

比如我们想要获取一个名为 head 的骨骼对象:

head = modelMap.get("head");

然后就可以为这个 head 参数设置各种角度来进行动画的制作了。

当然,为了稳妥起见,我们最好还是对这个骨骼做个判定,确保它是存在的。

head = modelMap.get("head");
if (head != undefined) {
    // 进行各种动画的制作
}

# 骨骼对象

我们通过 modelMap.get("xxx") 获取到的对象,即为骨骼对象。

函数名 返回值 备注
setRotateAngleX(float rotateAngleX) 设置该骨骼 X 角度
setRotateAngleY(float rotateAngleY) 设置该骨骼 Y 角度
setRotateAngleZ(float rotateAngleZ) 设置该骨骼 Z 角度
setOffsetX(float offsetX) 设置该骨骼 X 坐标偏移
setOffsetY(float offsetY) 设置该骨骼 Y 坐标偏移
setOffsetZ(float offsetZ) 设置该骨骼 Z 坐标偏移
setHidden(boolean hidden) 设置该骨骼是否隐藏
getRotateAngleX(float rotateAngleX) float 获取该骨骼 X 角度
getRotateAngleY(float rotateAngleY) float 获取该骨骼 Y 角度
getRotateAngleZ(float rotateAngleZ) float 获取该骨骼 Z 角度
getOffsetX(float offsetX) float 获取该骨骼 X 坐标偏移
getOffsetY(float offsetY) float 获取该骨骼 Y 坐标偏移
getOffsetZ(float offsetZ) float 获取该骨骼 Z 坐标偏移
isHidden(boolean hidden) boolean 该骨骼是否隐藏

# GlWrapper

在脚本的最上方我们引入了一个工具 GlWrapper,能够进行一些整体的旋转、平移、缩放等操作。

函数名 返回值 备注
translate(float x, float y, float z) 将实体平移到 x y z 处
rotate(float angle, float x, float y, float z) 以直线(0, 0, 0) (x, y, z) 为轴,旋转 angle 度,其中 angle 为角度。
scale(float x, float y, float z) 实体的三个轴向缩放 x y z 倍