# 自定义动画
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);
}
}
})
其他复杂的运动均可通过相关函数来实现。
# 游戏内热重载功能
因为干巴巴的函数式并不能一下确定该动画是否表现正确,我们添加了游戏内的动画热重载功能,只需要选择你正在编辑的动画脚本文件,每次保存文件后游戏均会自动加载该脚本,从而达到快速调试的目的。
点击此按钮后会弹出选择文件的对话框(有概率会被游戏本身挡住,最小化游戏后即可看到文件选择对话框)。
选择文件后,即进入热重载模式。此时对脚本文件的任何修改,保存后均会自动加载。
注意
退出此界面后热重载会自动关闭。
# 函数文档
# 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 倍 |