feat:上传mcguide-开发指南部份

This commit is contained in:
Othniel su
2024-12-23 10:57:59 +08:00
parent 7292166c88
commit 0dc59fa4f0
3297 changed files with 63375 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# Json配置自定义3D物品
在本节教程中,我们将围绕**如何自定义一个3D物品**的话题教给大家如何仅使用JSON配置和附加包开发知识来快速创建你的第一个3D物品。
> 温馨提示开始读起这篇指南前我们希望你对《我的世界》基岩版附加包有一定了解有能力撰写JSON数据格式并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
**通过这节教程,你将学会:**
•如何将一个现有的微软基岩版格式物品模型变为自定义物品的3D模型。
•校对模型在模型空间的相对位置,以匹配并固定在玩家第三人称的手持位置。
## 背景介绍
模组API 2.0 (基岩版版本 1.17.2开始在资源包定义物品的附着物Attachable文件可以绑定微软原版模型至物品上。
## 制作模型规范
让人型生物(如 溺尸、尸壳、僵尸 或 玩家手持3D物品模型时我们推荐使模型骨骼遵守玩家模型骨骼布局。以便模型能优先保证在玩家手持的情况下能够跟随肢体动画进行正常的幅度摆动。
原版玩家模型使用一条root根骨骼作为基骨骼子骨骼内使用waist骨骼控制上半身rightLeg骨骼和leftLeg骨骼控制左腿和右腿。
基岩版标识符推荐以小写数字母搭配下划线因为在游戏中使用rightLeg或rightleg效果是一致的。以下是玩家模型的骨骼树状图锚点即模型格式内的pivot。
```
-root锚点[0, 0, 0]
--waist锚点[0, 12, 0]
---jacket用于persona
---cape披风
---body锚点[0, 24, 0]
----leftArm锚点[5, 22, 0]
-----leftSleeve用于persona
-----leftItem锚点[6, 15, 1]
----rightArm锚点[-5, 22, 0]
-----rightSleeve用于persona
-----rightItem锚点[-6, 15, 1]
--leftLeg锚点[-1.9, 12, 0]
---leftPants用于persona
--rightLeg锚点[1.9, 12, 0]
---rightPants用于persona
```
## 校正模型在空间下对应第三人称手持的位置
先在模型工程内点击 **【文件】** - **【保存项目】** 。
![img](./images/1_0.png)
新建一个人型模型工程,并根据骨骼树状图拉出人型模型格式。
![img](./images/2_0.png)
选择 **【文件】** - **【导入】** 导入前面保存的3D模型项目。
![img](./images/3_0.png)
此时可以看到物品模型的贴图完全丢失。但不用担心它默认援引了人型模型的材质只要它的UV在前面的工程内已经分好这边就无需多管。
![img](./images/4_0.png)
将物品模型基骨骼放入 **【root】** - **【waist】** - **【body】** - **【rightArm】** - **【rightItem】** 骨骼下。并删除除右手方块以外的其他模型方块体。
![img](./images/5_0.png)
继续调整3D物品模型直至调整到一个满意的手持角度。此时删去右手模型方块体。这样一个有效的基础第三人称右手手持模型就做好了。
![img](./images/6_0.png)
## 将3D模型实装
在附加包的自定义资源包内新建attachables文件夹新建一个JSON文件。
![img](./images/7_0.png)
- 渲染控制器:默认可填 **"controller.render.item_default"** ,这是原版提供的一个物品渲染控制器。
- 材质:填写 **"controller.render.item_default"** 后,必须新增 **"default"** 和 **"enchanted"** 键,其中前者对应物品模型默认的材质,后者对应附魔时物品携带的材质。 **"default"** 无特殊情况默认填写 **"entity_alphatest"** "enchanted"默认填写 **"entity_alphatest_glint"** 。
- 贴图纹理:填写 **"controller.render.item_default"** 后,必须 **"default"** 和 **"enchanted"** 键, 其中前者对应自定义物品模型贴图,后者对应物品附魔时的附魔效果贴图(一般默认填写 **""textures/misc/enchanted_item_glint"** )。
- 动画附着物与实体一样可以添加动画和动画控制器也可以添加粒子但目前粒子默认会以实体脚底坐标发射粒子暂时不能在粒子挂点locator上发射。
- 几何体附着物的模型以自定义3D模型的geometry标识符为准。
- 物品标识符:与行为包的自定义物品标识符一致即可。在这里预先填写一个自定义物品标识符 **"design:custom_golden_sword"** ,稍后将用一个自定义物品行为与之呼应。
```json
{
"format_version":"1.10.0",
"minecraft:attachable":{
"description":{
"identifier":"design:custom_golden_sword",
"materials":{
"default":"entity_alphatest",
"enchanted":"entity_alphatest_glint"
},
"textures":{
"default":"textures/entity/custom_golden_sword",
"enchanted":"textures/misc/enchanted_item_glint"
},
"geometry":{
"default":"geometry.custom_golden_sword"
},
"render_controllers":[
"controller.render.item_default"
]
}
}
}
```
接着在附加包的自定义行为包里创建netease_items_beh文件夹并创建子文件custom_golden_sword.json。有关更多自定义物品行为请查看此链接[自定义武器及工具](../2-自定义武器及工具.md)
```json
{
"format_version":"1.10",
"minecraft:item":{
"description":{
"identifier":"design:custom_golden_sword",
"register_to_create_menu":true,
"custom_item_type":"weapon",
"category":"Equipment"
},
"components":{
"minecraft:max_damage":700,
"minecraft:max_stack_size":1,
"netease:weapon":{
"type":"sword",
"level":0,
"attack_damage":7,
"enchantment":10
}
}
}
}
```
我们还可以继续在资源包内的netease_items_res文件夹为自定义物品添上2D的物品图标。有关更多自定义物品的基础内容请查看此链接[自定义基础物品](../1-自定义基础物品.md)
```json
{
"format_version":"1.10",
"minecraft:item":{
"description":{
"identifier":"design:custom_golden_sword",
"category":"Equipment"
},
"components":{
"minecraft:icon":"design:custom_golden_sword",
"minecraft:render_offsets":"tools"
}
}
}
```
现在你就可以在游戏加载附加包完成后使用这个自定义3D物品了
![img](./images/8_0.png)
可以使用快速格式化Json等在线工具定位到可能在撰写过程中出现的错误语法或结构。

View File

@@ -0,0 +1,194 @@
# 自定义3D物品第一人称动画
在上节教程中我们学会了如何使用微软的Attachable功能实现自定义3D物品并使用社区工具Blockbench校正模型在第三人称的位置最后在游戏内实装上一把自定义3D金剑。
**本节课程Demo[戳这里下载](https://g79.gdl.netease.com/CustomWeapon3D.zip)**
**目前我们已经掌握:**
- 如何将一个现有的微软基岩版格式物品模型变为自定义物品的3D模型。第一节内容
- 校对模型在模型空间的相对位置,以匹配并固定在玩家第三人称的手持位置。(第一节内容)✔
- 借助《我的世界》基岩版电脑开发版,使用开发者工具制作模型的第一人称动画。(本节内容)❌
所以接下来将带领大家借助《我的世界》基岩版电脑开发版,使用开发者工具制作模型的第一人称动画。
![img](./images/0_0.png)
我的世界基岩版中,第一人称和第三人称的摄像机存在空间角度区别。因此需要单独为物品模型设计第一人称的手持动画。(下图为第三人称手持正确,但在第一人称显示意外的图例)
<img src="./images/0_1.png" alt="image-20220608133313000" style="zoom:150%;" />
启动Blockbench选择之前预制好的自定义3D物品模型选择【动画模式】-【添加动画创建一个用于控制第一人称手持角度位置的动画一个用于控制第一人称手持3D物品缩放的动画。我的世界基岩版动画可以叠加叠加后会对同一时刻下对位移、缩放、旋转的值进行加总
![image-20220608133346326](./images/0_2.png)
需要将动画设置为循环播放并将动画时长长度设置为0。
在对应的第一人称手持缩放动画(以下简称"first_scale"插入一帧缩放关键帧任意填入关键帧的缩放参数这里我们填写XYZ为1.1并保存。
![image-20220608133438507](./images/0_3.png)
在对应的第一人称手持位置角度动画以下简称“first_hold”插入一帧关键帧同时调整位置和旋转值这里我们任意填写XYZ的位置和旋转值为3并保存。
![image-20220608133501905](./images/0_4.png)
接着回到附着物的定义文件,将前面的"first_hold"和"first_scale"动画进行挂载并加入进scripts/animate根动画中。
```json
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
"identifier": "design:custom_golden_sword",
"materials": {
"default": "entity_alphatest",
"enchanted": "entity_alphatest_glint"
},
"textures": {
"default": "textures/entity/custom_golden_sword",
"enchanted": "textures/misc/enchanted_item_glint"
},
"animations": {
"first_hold": "animation.custom_golden_sword.first_hold", //挂载自定义动画“第一人称手持”
"first_scale": "animation.custom_golden_sword.first_scale" //挂载自定义动画“第一人称剑缩放”
},
"geometry": {
"default": "geometry.custom_golden_sword"
},
"render_controllers": [ "controller.render.item_default" ]
}
}
}
```
再次回到《我的世界》电脑版基岩开发版在游戏界面中按F3按键3次或14次呼出开发者控制台。接着按下F11键呼出鼠标。
![image-20220608133527259](./images/0_11.png)
打开Animation动画列表点击Open Editor打开编辑器
![image-20220608133539709](./images/0_10.png)
默认情况下,编辑器窗口会很小。此时点击右下角的伸缩区域进行拉伸。
![image-20220608133552800](./images/0_9.png)
拉到足够看清楚的大小后,直接往下拉到最底,这里展示着附着物挂载的动画状态。
![image-20220608133606005](./images/0_8.png)
打开“first_hold”的Details动画细节再点击Bone Animation骨骼动画就可以看到前面用Blockbench制作的动画的各项值并且支持直接在编辑器内动态修改最后直观的体现在游戏内。
<img src="./images/0_7.png" alt="image-20220608133619036" style="zoom:150%;" />
这里我们对"first_hold"的position设置值为X为-8Y为5Z为-6。再对rotation设置X为-45Y为15Z为-135。对"first_scale"的scale设置为0.35。
![image-20220608133639677](./images/0_6.png)
再按F3按键直到界面消失就可以在游戏内看到基本合格的第一人称自定义3D物品手持效果了。
![image-20220608133650381](./images/0_5.png)
请格外注意在这里调整的动画并不会最终保存进动画Json文件里因此我们还需要将值背下来并写入动画文件中。
```json
{
"format_version": "1.8.0",
"animations": {
"animation.custom_golden_sword.first_hold": {
"loop": true,
"bones": {
"sword": {
"rotation": [-45, 15, 135],
"position": [-8, 5, -6]
}
}
},
"animation.custom_golden_sword.first_scale": {
"loop": true,
"bones": {
"sword": {
"scale": 0.35
}
}
}
}
}
```
最后,对"first_hold"和"first_scale"的加载模式添加条件,使用"c.is_first_person"来判定动画是否运行在第一人称模式下,是则运行。
```json
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
"identifier": "design:custom_golden_sword",
"materials": {
"default": "entity_alphatest",
"enchanted": "entity_alphatest_glint"
},
"textures": {
"default": "textures/entity/custom_golden_sword",
"enchanted": "textures/misc/enchanted_item_glint"
},
"animations": {
"first_hold": "animation.custom_golden_sword.first_hold", //挂载自定义动画“第一人称手持”
"first_scale": "animation.custom_golden_sword.first_scale" //挂载自定义动画“第一人称剑缩放”
},
"scripts": {
"animate": [ // 挂载动画至根动画中
{
"first_hold": "c.is_first_person" // 保证“第一人称手持”动画只在第一人称下显示
},
{
"first_scale": "c.is_first_person"// 保证“第一人称剑缩放”动画只在第一人称下显示
}
]
},
"geometry": {
"default": "geometry.custom_golden_sword"
},
"render_controllers": [ "controller.render.item_default" ]
}
}
}
```
c是context的缩写就像可以使用q来代表query一样query是查询某个活动对象特定状态的查询函数。c.is_first_person将返回附着物是否处于第一人称视角下在前面加上!运算符即可代表是否处于非第一人称视角则可能是第三人称前视角或第三人称后视角下。其他的已知context函数还有
- **context.owning_entity**返回附着物穿戴的实体可以使用Molang的->运算符获取该实体身上的属性假设已知自定义3D物品由玩家手持则可以使用"c.owning_entity -> q.is_sneaking"返回玩家是否此时手持该附着物并下蹲。
- **context.item_slot**附着物手持的位置,返回"main_hand"(主手)或者"off_hand"(副手)。

View File

@@ -0,0 +1,551 @@
# 自定义3D武器第一人称攻击效果
在前两节自定义3D物品教程结束后我们学会了如何使用微软的Attachable功能完全实现自定义3D物品功能。
许多武器动作模组在《我的世界》模组体验上占有非常高的地位自定义3D物品可以在武器方面着重凸显组件的特点。本节课将会带领开发者学习如何使用自定义金剑制作独特的攻击形式配合模组SDK让作品特色更上一层楼。
**假设你看过前两节自定义3D物品教程你应该已经学会**
- 如何将一个现有的微软基岩版格式物品模型变为自定义物品的3D模型。✔
- 校对模型在模型空间的相对位置,以匹配并固定在玩家第三人称的手持位置。✔
- 借助《我的世界》基岩版电脑开发版使用开发者工具制作3D模型的第一人称动画。✔
**在本节课程你将学会:**
- 使用Blockbench为附着物设计手臂模型并重新调整金剑建模。
- 使用Blockbench和基岩版动画叠加效果制作第一人称手持金剑攻击动画。
- 用额外的渲染控制器挂载手臂模型至附着物。
- 借助模组SDK和自定义Query节点完成第一人称手持金剑攻击效果。
**本节课程的[Demo请戳这里下载](https://g79.gdl.netease.com/CustomWeaponAttackFP.zip)**
![动画](./images/attack0_0.gif)
## 设计手臂模型并重新调整金剑建模
打开前一节[自定义3D物品的Demo](https://g79.gdl.netease.com/CustomWeapon3D.zip),目前金剑的骨骼会按照下列排序:
```
-root玩家根骨骼
--waist玩家上半身骨骼
---body玩家身体骨骼
----rightArm玩家右手骨骼
-----rightItem玩家右手手持物品骨骼
------sword金剑骨骼
```
![image-20220612014504234](./images/attack0_0.png)
**我们设法在玩家手持金剑时,会在第一人称下显示两个手臂紧握着金剑。则此时两只手臂是跟随着金剑模型,而不牵扯到玩家模型本身。这样的设计意图给予了一些新的启发与可预料到的挑战。**
![任务计划甘特图 (2)](./images/attack0_1.png)
首先金剑的模型直接挂载在右手手持物品骨骼下。为了在之后的动画可以让金剑跟随着挥舞的手臂需要用arm骨骼取代sword骨骼用来存放握住金剑把手的手臂并将原先sword骨骼的体块纳入一个新的骨骼组item并将它作为arm骨骼的子骨骼。 **为之后做第一人称动画提供便利** 直接调整arm骨骼的动画参数即可金剑会跟随arm旋转、偏移或缩放。
![任务计划甘特图 (2)](./images/attack0_2.png)
除此之外我们并不希望让arm骨骼会出现在第三人称视角。 **这是因为当玩家手持物品时,玩家模型的手臂会自动消失。但切换回第三人称将重新出现。玩家模型本身的手臂会与金剑的新双臂同时出现,影响观感。**
![任务计划甘特图 (2)](./images/attack0_3.png)
**最终得出的初步方案里我们需要使用一个新的渲染控制器挂接arm骨骼至附着物上。一个附着物是可以满足挂载多个渲染控制器同时在骨骼层级不变的情况下可以在默认渲染控制器下挂载剑模型在arm骨骼渲染控制器下挂载arm模型。接着设置渲染控制器只会在第一人称下显示。**
**最终剑模型的骨骼层级如下Tips右键对应骨骼点击Resolve Group可在不移动子骨骼的前提下展开父骨骼**
```
-root玩家根骨骼
--waist玩家上半身骨骼
---body玩家身体骨骼
----rightArm玩家右手骨骼
-----rightItem玩家右手手持物品骨骼
------arm金剑双臂骨骼-不放置体块)
------item金剑骨骼
```
![image-20220612025314595](./images/attack0_4.png)
**最终剑手臂模型的骨骼层级如下Tips将原版资源包/models/mobs.json内的humanoid模型的两只手臂直接复制到这里可以继承原版steve模型的UV展开并快速对手臂进行建模**
```
-root玩家根骨骼
--waist玩家上半身骨骼
---body玩家身体骨骼
----rightArm玩家右手骨骼
-----rightItem玩家右手手持物品骨骼
------arm金剑双臂骨骼
------item金剑骨骼-不放置体块)
```
![image-20220612025523344](./images/attack0_5.png)
## 制作第一人称手持金剑攻击动画
切换至动画模式新建用以表现第一人称攻击动画sword_attack。
![image-20220612094740384](./images/attack0_6.png)
选中sword_attack动画同时勾选first_hold动画在手持第一人称动画生效的同时以此视角基础设计挥舞动画。
![image-20220612095303937](./images/attack0_7.png)
这里设计了一个0.75秒的攻击动画,在游戏里将实际从右往左挥舞金剑。其他形式可自行创作。
![动画1](./images/attack0_1.gif)
## 挂载手臂模型至附着物
在资源包/render_controllers文件夹下新建一个自定义的剑渲染控制器用以显示手臂模型。
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.sword_controller": {
"geometry": "Geometry.arm",
"materials": [
{
"*": "Material.default"
}
],
"textures": [
"Texture.arm"
]
}
}
}
```
接着将手臂模型、手臂贴图、渲染控制器和第一人称动画挂载于附着物上。
```json
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
"identifier": "design:custom_golden_sword",
"materials": {
"default": "entity_alphatest",
"enchanted": "entity_alphatest_glint"
},
"textures": {
"default": "textures/entity/custom_golden_sword",
"enchanted": "textures/misc/enchanted_item_glint",
"arm": "textures/entity/steve" // arm键对应渲染控制器内的Texture.arm贴图路径则是原版内置的steve粗手臂贴图
},
"animations": {
"first_hold": "animation.custom_golden_sword.first_hold",
"first_scale": "animation.custom_golden_sword.first_scale",
"attack_rotation_sword": "animation.custom_golden_sword.sword_attack" // 自定义第一人称剑攻击动画
},
"scripts": {
"animate": [
{
"first_hold": "c.is_first_person"
},
{
"first_scale": "c.is_first_person"
}
]
},
"geometry": {
"default": "geometry.custom_golden_sword",
"arm": "geometry.custom_golden_sword_arm" // arm键对应渲染控制器内的Geometry.arm
},
"render_controllers": [ "controller.render.item_default",
{
"controller.render.sword_controller": "c.is_first_person" // 只有在第一人称下显示手臂模型和手臂贴图
}
]
}
}
}
```
进入游戏后,可以看到拿出金剑时,显示了两支手臂握住剑柄的效果。
![image-20220612102522220](./images/attack0_9.png)
## 完成第一人称手持金剑攻击效果
使用自定义Query节点接口可以最大限度地减少组件冲突的影响。自定义Query接口可以在客户端线程上自定义新的Query查询函数可以内置在动画或动画控制器中作为条件改变动画和动画控制器的效果。
![任务计划甘特图 (2)](./images/attack0_10.png)
在行为包内创建Python脚本目录取名CustomSwordScripts并创建服务端系统和客户端系统。有关模组SDK目录创建和系统注册的模块可以查看[此官网教程链接](https://g.126.fm/00FchC4)。
```
-CustomSwordScripts文件夹
--client.py客户端Python文件
---AnimationClient客户端类
--modMain.pymod入口
--server.py服务端Python文件
---AnimationServer服务端类
```
```python
# -*- coding: UTF-8 -*-
from mod.common.mod import Mod
import mod.server.extraServerApi as serverApi
import mod.client.extraClientApi as clientApi
@Mod.Binding(name="CustomSwordMod", version="0.1")
class CustomSwordMod(object):
def __init__(self):
pass
@Mod.InitClient()
def init_client(self):
clientApi.RegisterSystem("CustomSwordMod", "AnimationClient",
"CustomSwordScripts.client.AnimationClient")
@Mod.InitServer()
def init_server(self):
serverApi.RegisterSystem("CustomSwordMod", "AnimationServer",
"CustomSwordScripts.server.AnimationServer")
@Mod.DestroyClient()
def destroy_client(self):
pass
@Mod.DestroyServer()
def destroy_server(self):
pass
```
在客户端类的\__init__函数内监听OnLocalPlayerStopLoading事件当玩家客户端加载完毕后调用Query节点接口并注册query.mod.sword_attack。
```python
# -*- coding: UTF-8 -*-
import mod.client.extraClientApi as clientApi
import time
compFactory = clientApi.GetEngineCompFactory()
ClientSystem = clientApi.GetClientSystemCls()
class AnimationClient(ClientSystem):
def __init__(self, namespace, system_name):
ClientSystem.__init__(self, namespace, system_name)
namespace = clientApi.GetEngineNamespace()
system_name = clientApi.GetEngineSystemName()
self.ListenForEvent(namespace, system_name,
'OnLocalPlayerStopLoading', self, self.client_init) # 监听事件
def client_init(self, event):
player_id = clientApi.GetLocalPlayerId()
query_comp = compFactory.CreateQueryVariable(clientApi.GetLevelId()) # 创建基于世界的query组件
query_comp.Register('query.mod.sword_attack_time', 0.0) # 注册自定义Query节点必须以query.mod开头
query_comp = compFactory.CreateQueryVariable(player_id)
query_comp.Set('query.mod.sword_attack_time', 0.0) # 对本地玩家再次设置此节点的默认值
```
新增第一人称剑攻击动画控制器controller.animation.sword.first_attack。
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.sword.first_attack": {
"initial_state" : "default",
"states" : {
"default": {
"transitions" : [
{
"first_person" : "c.is_first_person"
}
]
},
"first_person": {
"transitions" : [
{
"default" : "!c.is_first_person" // 若为非第一人称视角,则返回至默认状态
},
{
"first_person_attack": "query.mod.sword_attack_time" // 当query.mod.sword_attack_time为真时切换至播放攻击动画的状态
}
]
},
"first_person_attack": {
"animations": [
"attack_rotation_sword" // 攻击动画
],
"transitions" : [
{
"default" : "!c.is_first_person" // 若为非第一人称视角,则返回至默认状态
},
{
"first_person": "!query.mod.sword_attack_time"// 当query.mod.sword_attack_time为假时切换至默认状态
}
]
}
}
}
}
}
```
将第一人称剑攻击动画控制器置入附着物定义文件内。
```json
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
//...
"animations": {
"first_hold": "animation.custom_golden_sword.first_hold",
"first_scale": "animation.custom_golden_sword.first_scale",
"attack_rotation_sword": "animation.custom_golden_sword.sword_attack",
"controller.attack": "controller.animation.sword.first_attack"
},
"scripts": {
"animate": [
"controller.attack",
{
"first_hold": "c.is_first_person"
},
{
"first_scale": "c.is_first_person"
}
]
}
// ...
}
}
}
```
紧接着监听LeftClickBeforeClientEvent与TapBeforeClientEvent分别对应在电脑启动器上使用左键与在手机上点击屏幕的事件事件回调可指向同一个函数地址代码解析如下
1. 在客户端系统上贮存点击的时间戳。
2. 在事件响应时计算当前时间扣去过去时间戳是否大于攻击动画的时间我们的自定义攻击动画时间为0.75秒因此判断结果需要大于0.75。
3. 满足条件2时继续判断是否满足是金剑是则重新计算时间戳。并执行函数send_attacked_packet。
4. send_attacked_packet函数内的业务逻辑主要是创建定时器任务用来发送数据至服务端再依次通过服务端向其他玩家客户端通知本地玩家播放挥舞动画。并在本地客户端设置query.mod.sword_attack的值帮助动画控制器播放动画效果。
```python
# -*- coding: UTF-8 -*-
import mod.client.extraClientApi as clientApi
import time
compFactory = clientApi.GetEngineCompFactory()
ClientSystem = clientApi.GetClientSystemCls()
class AnimationClient(ClientSystem):
def __init__(self, namespace, system_name):
ClientSystem.__init__(self, namespace, system_name)
namespace = clientApi.GetEngineNamespace()
system_name = clientApi.GetEngineSystemName()
self.ListenForEvent(namespace, system_name,
'OnLocalPlayerStopLoading', self, self.client_init)
self.ListenForEvent(namespace, system_name,
'LeftClickBeforeClientEvent', self, self.attack_click)
self.ListenForEvent(namespace, system_name,
'TapBeforeClientEvent', self, self.attack_click)
self.click_cooldown = time.time()
def client_init(self, event):
# ...
pass
def attack_click(self, event):
current_time = time.time()
carried_item = compFactory.CreateItem(clientApi.GetLocalPlayerId()).GetCarriedItem()
if current_time - self.click_cooldown > 0.75 and carried_item and carried_item['newItemName'] == 'design:custom_golden_sword':
self.click_cooldown = time.time()
self.send_attacked_packet(0.0, {# 动画开始的时间点
'playerId': clientApi.GetLocalPlayerId(),
'type': 'start'
})
self.send_attacked_packet(0.16, {# 动画打击出伤害的时间点,具体以自定义动画的设计形式为准
'playerId': clientApi.GetLocalPlayerId(),
'type': 'will_hit'
})
self.send_attacked_packet(0.75, { # 动画结束时间点
'playerId': clientApi.GetLocalPlayerId(),
'type': 'end'
})
else:
# 正在播放动画时,若重复点击,则取消点击
if carried_item and carried_item['newItemName'] == 'design:custom_golden_sword':
event['cancel'] = True
def send_attacked_packet(self, _time, data):
game_comp = compFactory.CreateGame(clientApi.GetLevelId())
game_comp.AddTimer(
_time,
self.NotifyToServer, # 发送数据至服务端,用来告知其他客户端当前玩家的播放动画状态,以便在其他客户端上也能看到玩家播放动画的效果
'AttackedPacket',
data
)
game_comp.AddTimer(
_time,
compFactory.CreateQueryVariable(clientApi.GetLocalPlayerId()).Set, # 设置本地玩家客户端播放动画所需的query节点的值
'query.mod.sword_attack_time',
1.0 if data['type'] == 'start' or data['type'] == 'will_hit' else 0.0
)
```
在服务端系统代码上监听来自客户端的事件AttackedPacket。代码解析如下
1. 若玩家点击左键播放动画的自定义客户端事件判断状态为will_hit动画打击出伤害获取周围的实体执行扇形攻击函数sector_attack。
2. sector_attack函数内计算了某个实体是否在另一个实体的扇形视角范围内。通过计算攻击者和受害者的坐标距离与攻击者和受害者的视角差度判断视角差度在一个固定的扇形角度时则创建伤害组件对受害者创造伤害。
3. 使用GetRelevantPlayer接口获取周围的玩家ID并使用NotifyToMultiClients接口向其他玩家传送事件AttackSync告知此名玩家开始播放动画其他玩家需在自己的本地客户端同样设置该名玩家的query.mod.sword_attack为真以实现动画效果同步。
```python
# -*- coding: UTF-8 -*-
import mod.server.extraServerApi as serverApi
import math
from mod.common.utils.mcmath import Vector3
compFactory = serverApi.GetEngineCompFactory()
ServerSystem = serverApi.GetServerSystemCls()
class AnimationServer(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
namespace = serverApi.GetEngineNamespace()
system_name = serverApi.GetEngineSystemName()
self.ListenForEvent('CustomSwordMod', 'AnimationClient',
'AttackedPacket', self, self.attacked)
def attacked(self, event):
_type = event['type']
player_id = event['playerId']
if _type == 'will_hit':
entities = compFactory.CreateGame(player_id).GetEntitiesAround(player_id, 6,
{
'any_of': {
'test': 'is_family',
'subject': 'other',
'operator': 'not',
'value': 'instabuild'
}
})
for entity in entities:
self.sector_attack(player_id, entity, 65.0, 6.0, 7, attacker_id=player_id)
elif _type == 'start':
players = compFactory.CreatePlayer(player_id).GetRelevantPlayer([player_id])
self.NotifyToMultiClients(players, 'AttackSync', {
'playerId': player_id,
'value': 1.0
})
else:
players = compFactory.CreatePlayer(player_id).GetRelevantPlayer([player_id])
self.NotifyToMultiClients(players, 'AttackSync', {
'playerId': player_id,
'value': 0.0
})
def sector_attack(self, attacker, victim, between_angle=0.0, radius=0.0, damage=0, cause=serverApi.GetMinecraftEnum().ActorDamageCause.EntityAttack, attacker_id=None, child_attacker_id=None, knock=True):
attacker_foot_pos = compFactory.CreatePos(attacker).GetFootPos()
victim_foot_pos = compFactory.CreatePos(victim).GetFootPos()
delta = Vector3(victim_foot_pos) - Vector3(attacker_foot_pos)
forward_vector = serverApi.GetDirFromRot(compFactory.CreateRot(attacker).GetRot())
angle = math.degrees(math.acos(Vector3.Dot(delta.Normalized(), Vector3(forward_vector).Normalized())))
if angle < between_angle and delta.Length() < radius:
compFactory.CreateHurt(victim).Hurt(damage, cause, attacker_id, child_attacker_id, knock)
```
最后在客户端上监听来自自定义服务端的AttackSync事件。在本地客户端设置其他玩家播放动画的进度。
```python
# -*- coding: UTF-8 -*-
import mod.client.extraClientApi as clientApi
import time
compFactory = clientApi.GetEngineCompFactory()
ClientSystem = clientApi.GetClientSystemCls()
class AnimationClient(ClientSystem):
def __init__(self, namespace, system_name):
ClientSystem.__init__(self, namespace, system_name)
namespace = clientApi.GetEngineNamespace()
system_name = clientApi.GetEngineSystemName()
# ....
self.ListenForEvent('CustomSwordMod', 'AnimationServer',
'AttackedSync', self, self.attacked_sync)
# ....
def client_init(self, event):
# ....
pass
def attack_click(self, event):
# ....
pass
def attacked_sync(self, event):
player_id = event['playerId']
value = event['value']
compFactory.CreateQueryVariable(player_id).Set(
'query.mod.sword_attack_time',
value
)
def send_attacked_packet(self, _time, data):
# ....
pass
```
可以看到最后实际效果如预期所示,对羊群造成了范围伤害并播放自定义攻击动画。有条件联机的开发者,也可以验证多人联机下攻击动画的播放效果。
![动画2](./images/attack0_2.gif)

View File

@@ -0,0 +1,250 @@
# 自定义3D武器第三人称攻击效果
在上节中我们初步掌握如何自定义3D武器在第一人称视角下的攻击效果。由于玩家存在第一人称和第三人称适配第三人称是必不可少的。
第三人称下的自定义3D武器攻击效果需要依赖Python代码这里使用传统的注册自定义系统的模组开发方式为大家讲解。
**在本章你将学会:**
- 使用Blockbench和基岩版动画叠加效果制作第三人称手持金剑动画与对应的攻击动画。
- 使用模组SDK挂载新的动画资源并重构第三人称动画控制器。
本节课程所使用到的[Demo请戳这里](https://g79.gdl.netease.com/customweaponnew.zip),以下是最终效果呈现:**
![444](./images/attack21_0.gif)
## 创建第三人称所需的动画资源
启动Blockbench打开custom_golden_sword_arm.geo.json并切换至动画模式新建表现第三人称手持动画的third_hold。并将手臂向X轴朝上方旋转。
![image-20220702104920739](./images/attack21_0.png)
新建表现第三人称挥舞动画的third_arm_attack选择third_arm_attack动画并勾选third_hold动画自定义一段0.75秒的攻击动画。并将关键帧设置为平滑,让动画效果更丝滑。
![0_0](./images/attack21_1.gif)
由于手臂和剑分成两个模型手臂的动画通道无法影响武器为了让手臂挥舞时剑能与之轨迹同步我们需要在剑模型上再多做一个动画。打开custom_golden_sword.geo.json切换至动画模式新增third_sword_attack动画在选中后勾选third_hold动画也自定义一段和手挥舞时长一样的0.75秒攻击动画这里可以将third_arm_attack的arm骨骼关键帧拷贝一份过来并对third_sword_attack的item骨骼打上动画关键帧。最后选择third_sword_attack动画并勾选third_hold和third_arm_attack动画可以看到完整的第三人称攻击动画效果。
![0_1](./images/attack21_2.gif)
## 挂载新的动画资源并创建新的动画控制器
新建controller.animation.player.third_person_attack_fixed动画控制器下设default和third_person_attack两个状态当满足query.mod.sword_attack_time不等于0时切换至播放第三人称攻击动画的状态动画播放结束时且对应的query.mod.sword_attack_time为0时切换回default状态以下是代码示例。
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.player.third_person_attack_fixed" : {
"states" : {
"default": {
"transitions": [
{
"third_person_attack": "query.mod.sword_attack_time"
}
]
},
"third_person_attack": {
"animations": [
"third_arm_attack"
],
"transitions": [
{
"default": "!query.mod.sword_attack_time"
}
]
}
}
}
}
}
```
接着在CustomSwordScripts/client.py内的client_init函数下添加ActorRender组件在游戏运行时动态添加美术资源至玩家资源定义内。由于第三人称的手臂动画涉及到玩家的资源这是不得不采取的措施为了尽量减少冲突的可能性我们不选择直接修改玩家资源定义内容而是使用Python代码导入。
```python
class AnimationClient(ClientSystem):
# ...
def client_init(self, event):
# ...
actor_comp.AddPlayerAnimationController( # 第三人称攻击动画控制器
'controller.third_person_attack_fixed',
'controller.animation.player.third_person_attack_fixed'
)
actor_comp.AddPlayerAnimation(
'sword_third_hold',
'animation.custom_golden_sword.third_hold' # 第三人称手持动画
)
actor_comp.AddPlayerAnimation(
'third_arm_attack',
'animation.custom_golden_sword.third_arm_attack' # 第三人称手臂动画
)
# 添加第三人称手持动画进玩家root动画控制器下的third_person状态里这里存放的都是玩家在第三人称时会使用到的动画
# 动画需满足主手手持必须是自定义金剑才会生效
actor_comp.AddPlayerAnimationIntoState(
'root', 'third_person', 'sword_third_hold', "query.get_equipped_item_full_name('main_hand') == 'design:custom_golden_sword'"
)
# 添加第三人称手持动画控制器进玩家root动画控制器下的third_person状态里动画控制器同样可以被嵌套在其他动画控制器内
# 动画控制器需满足主手手持必须是自定义金剑才会生效
actor_comp.AddPlayerAnimationIntoState(
'root', 'third_person', 'controller.third_person_attack_fixed', "query.get_equipped_item_full_name('main_hand') == 'design:custom_golden_sword'"
)
actor_comp.RebuildPlayerRender() #导入资源后必须重新构建玩家渲染调用RebuildPlayerRender方法
```
在剑的attachable定义文件内挂接third_sword_attack动画。这是因为剑挥舞的效果可以转移给剑本身因此third_sword_attack应该挂在剑的资源定义文件内。
```json
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
"identifier": "design:custom_golden_sword",
//materials
//textures
"animations": {
//...
"attack_rotation_sword": "animation.custom_golden_sword.sword_attack",
"third_attack_rotation_sword": "animation.custom_golden_sword.third_sword_attack",
"controller.attack": "controller.animation.sword.attack"
},
"scripts": {
"animate": [
"controller.attack",
{
"first_hold": "c.is_first_person"
},
{
"first_scale": "c.is_first_person"
}
]
}
//geometry
//render_controlls
}
}
}
```
重写controller.attack动画控制器新增third_person和third_person_attack两个状态。要求在第三人称下状态切换为third_person攻击开始时状态切换至third_person_attack并播放第三人称的剑挥舞动画在query.mod.sword_attack_time为0时切换回third_person状态。在有关第三人称的所有状态下提供切换至default状态的条件即满足c.is_first_person手持实体处于第一人称时。
```json
{
"format_version": "1.10.0",
"animation_controllers": {
//...
"controller.animation.sword.attack": {
"initial_state" : "default",
"states" : {
"default": {
"transitions" : [
// first_person
{
"third_person": "!c.is_first_person"
}
]
},
// first_person
"third_person": {
"transitions" : [
{
"default" : "c.is_first_person"
},
{
"third_person_attack": "query.mod.sword_attack_time"
}
]
},
"third_person_attack": {
"animations": [
"third_attack_rotation_sword"
],
"transitions" : [
{
"default" : "c.is_first_person"
},
{
"third_person": "!query.mod.sword_attack_time"
}
]
}
// first_person_attack
}
}
}
}
```
最后进入游戏,可以看到第三人称效果已经生效。
![444](./images/attack21_0.gif)
## 一些其他优化
由于我们并没有屏蔽玩家在挥舞时点触方块的逻辑造成挥舞的区域若离方块较近会叠加出较为难看的挖掘动画。为了避免这个问题可以在client.py和server.py文件分别监听StartDestroyBlockClientEvent和StartDestroyBlockServerEvent根据实际情况判断结果对挖掘进行取消。
以下是代码示例:
```python
# client.py
class AnimationClient(ClientSystem):
def __init__(self, namespace, system_name):
#....
namespace = clientApi.GetEngineNamespace()
system_name = clientApi.GetEngineSystemName()
self.ListenForEvent(namespace, system_name,
'StartDestroyBlockClientEvent', self, self.attack_click_block)
#....
def attack_click_block(self, event):
player_id = event['playerId']
if player_id != clientApi.GetLocalPlayerId():
return
current_time = time.time()
if current_time - self.click_cooldown < 0.75:
event['cancel'] = True
```
```python
# server.py
class AnimationServer(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
#....
namespace = serverApi.GetEngineNamespace()
system_name = serverApi.GetEngineSystemName()
self.ListenForEvent(namespace, system_name,
'StartDestroyBlockServerEvent', self, self.attack_click_block)
self.player_attacked_cache = {}
self.attack_type = ['start', 'will_hit', 'end']
#...
def attack_click_block(self, event):
player_id = event['playerId']
if self.player_attacked_cache.get(player_id, '') != 'end':
event['cancel'] = True
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB