This commit is contained in:
boybook
2025-12-01 20:59:16 +08:00
parent 12738a142c
commit 760c2dd9ad
5535 changed files with 21070 additions and 2021 deletions

View File

@@ -0,0 +1,3 @@
# 教程示例下载
本章所教学的武器和装备制作Demo可点击 [这里](https://g79.gdl.netease.com/tutorial_1.zip) 下载到本地。

View File

@@ -0,0 +1,702 @@
# 制作3D武器
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将帮助你从零开始创建一把属于自己的 3D 武器,包含第一、第三人称动画。
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程如果对此感兴趣的同学可以自行学习和了解。
在本教程中,您将学习以下内容。
- ✅3D 武器的两种实现方式和原理;
- ✅3D 武器第一人称和第三人称动画的制作;
## 成果展示
通过本节课的学习,我们会逐步实现下列的一把自定义武器,拥有第一人称和第三人称的完整动画:
![](./assets/1_1.gif)
## 3D 武器的两种实现方式
我们先在 MC Studio 官方的内容库中“偷"一个 3D 武器的模型来使用,我这里选择的是下图所示的包:
![](./assets/image-20231124000353350.png)
我们随便选择其中一个模型吧,比如下面这个:
![](./assets/image-20231124001122747.png)
### 方法1作为玩家的额外骨骼
第一种方法就是作为玩家的**额外骨骼**,挂接到玩家的渲染控制器上去。这种方法非常适合用于**仅涉及单一类型的实体**(比如玩家)模型,并且仅涉及一个装备位置的情况。这种方法也非常适合和方便在 Blockbench 中查看效果。
#### Step 1.对应玩家骨骼建模并对齐
首先我们需要对武器进行建模操作,当然我们这里直接使用了内容库中的内容,就不展开了。
然后需要**对应玩家的骨骼**进行**对齐**操作,如果我们不对默认的玩家骨骼进行修改,那么原版的玩家骨骼位置位于本地客户端的如下目录:`\data\skin_packs\vanilla\geometry.json`。我们如果使用 Blockbench 打开会发现该文件下有三个模型:
![](./assets/image-20231118113431838.png)
我们打开任一玩家模型就可以了,一般来说我们选择**纤细模型**,因为手臂模型更细,更需要对齐处理避免穿模。
接下来,我们只需要把我们的武器模型放在 `rightItem` 骨骼组下并对齐,这样就能完美的继承玩家的骨骼:
![](./assets/image-20231124001254351.png)
> 注意:我们操作的时候最好把原版骨骼复制一份,再在复制文件上进行操作。这样能避免误操作,对原版文件造成不必要的影响。
为了方便我们后续制作动画,我们需要把这个对齐好的模型先保存在一个临时目录下。
导出骨骼文件只需要把武器无关的骨骼全部删除掉就行了(删掉人物骨骼):
![](./assets/image-20231124102419837.png)
#### Step 2. 创建动画
如果我们需要自定义的攻击动作,那么我们就需要创建两个动画:一个是用于第三人称播放的人物动画,一个是用于第一人称播放的武器动画。
##### 第三人称动画
第三人称的动画很简单,就利用上面对齐好骨骼并且带有完整人物模型的直接 k 就行了,这里我们简单 k 一个只带手臂动作的简单动画:
![k的第三人称动画](./assets/1_2.gif)
这个动画只对 `rightArm` 和武器的根骨骼组 `sword` 进行了处理。
可以看到,由于我们这里有完整的骨骼和骨骼组,所以在 k 第三人称动画的时候非常方便。可以直接看到完整的效果。
##### 第一人称动画
但是第一人称就有点麻烦了。首先,我们隐藏掉所有的玩家骨骼(因为第一人称这些骨骼都是隐藏掉的)。
然后需要在 BlockBench **模拟游戏中第一人称的视角**
第一步,在场景中右键,选择「保存相机角度」的选项:
![](./assets/image-20231124100839104.png)
填入下图中的数据保存:
![](./assets/image-20231124100919188.png)
然后我们再「角度」中选择刚才保存好的相机视角:
![](./assets/image-20231124101035754.png)
这是在模拟第一人称下的相机选项,但你会发现什么东西都看不见了,因为原版的游戏在第一人称时,手臂还会附加一个特殊的动画。
所以第二步,我们添加一个自定义的动画来模拟第一人称下手臂的位置:
![](./assets/image-20231124101220659.png)
该动画文件如下:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.first_person_guide.right_arm.method_one": {
"loop": true,
"bones": {
"rightArm": {
"rotation": [95, -45, 115],
"position": [13.5, -10, 12]
},
"rightItem": {
"position": [0, 0, -1]
}
}
}
}
}
```
至此,我们就模拟好了第一人称下的武器位置。我们需要创建两个动画,第一个用于修正第一人称下的握持位置,第二个则是模拟第一人称下的攻击路径。
修正动画效果:
![](./assets/image-20231124101753131.png)
> 提醒:只需要点击动画右边的圆圈就可以同时播放多个动画了哦。
我们在 k 攻击动画的时候,需要同时把上述的「第一人称手臂模拟动画」和「第一人称握持修正动画」打开,再开始 k这是麻烦的地方
![模拟第一人称武器动画](./assets/1_3.gif)
![](./assets/image-20231124102156415.png)
至此,我们的动画就制作完成了。导出我们的动画到资源包 `animations` 目录下即可。
#### Step 3. 准备动画控制器和渲染控制器
我们的武器相当于是一个额外的骨骼,所以需要一个额外的渲染控制器来控制是否进行渲染:
```python
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.tutorial_custom_sword": {
"geometry": "Geometry.custom_sword",
"materials": [{"*": "Material.default"}],
"textures": ["Texture.custom_sword"]
}
}
}
```
动画控制器也非常简单,不同视角的动画控制器播放不同的动画就可以了。
第三人称动画控制器:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.custom_sword.third_attack": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"third_person_attack1": "query.mod.custom_sword_attack == 1.0"
}
]
},
"third_person_attack1": {
"animations": [
// 对应玩家动画
"third_person_attack1"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_sword_attack == 0.0"
}
]
}
}
}
}
}
```
第一人称动画控制器:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.custom_sword_by_addon_bones": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"first_person_attack1": "query.mod.custom_sword_attack == 1.0"
}
]
},
"first_person_attack1": {
"animations": [
// 对应代码里面配置的动画名称
"custom_sword_first_attack"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_sword_attack == 0.0"
}
]
}
}
}
}
}
```
#### Step 4. 使用代码注册所有的资源
为了更少的侵入性,所以我们这里使用代码来把这些资源都挂接在玩家的渲染控制器下:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import time
import config
CompFactory = clientApi.GetEngineCompFactory()
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
self, self.OnAddPlayerCreatedClientEvent)
def OnAddPlayerCreatedClientEvent(self, args):
playerId = args['playerId']
self.InitRender(playerId) # 包括其他玩家也需要被初始化
# 初始化绑定
def InitRender(self, playerId):
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
# 定义攻击的自定义变量,这里就是 query.mod.custom_sword_attack
queryVariableComp.Register(config.AttackVarName, 0)
# 第一种方法:作为玩家的附加骨骼添加进渲染控制器中进行控制
self._InitMethodOne(playerId)
# 两种方式都通用的第三人称动画和控制器
actorRenderComp = CompFactory.CreateActorRender(playerId)
actorRenderComp.AddPlayerAnimation("third_person_attack1", "animation.tutorial_custom_sword_by_attachable.player.attack1")
actorRenderComp.AddPlayerAnimationController("custom_sword_third_attack", "controller.animation.custom_sword.third_attack")
actorRenderComp.AddPlayerScriptAnimate("custom_sword_third_attack", "!variable.is_first_person")
actorRenderComp.RebuildPlayerRender()
# 为第一个方法进行初始化渲染
def _InitMethodOne(self, playerId):
actorRenderComp = CompFactory.CreateActorRender(playerId)
# 3D武器额外骨骼 —— 所需的就是把骨骼作为玩家的一部分添加上
actorRenderComp.AddPlayerGeometry("custom_sword", "geometry.tutorial_custom_sword_by_addon_bones")
actorRenderComp.AddPlayerTexture("custom_sword", "textures/models/tutorial_custom_sword")
actorRenderComp.AddPlayerRenderController("controller.render.tutorial_custom_sword",
"query.get_equipped_item_name('main_hand') == 'custom_sword_by_addon_bones'")
# 作为额外骨骼,还需要添加第一人称的动画文件和相关的控制器
actorRenderComp.AddPlayerAnimation("custom_sword_first_hold", "animation.tutorial_custom_sword_by_addon_bones.hold_first_person")
actorRenderComp.AddPlayerAnimation("custom_sword_first_attack", "animation.tutorial_custom_sword_by_addon_bones.first_attack1")
actorRenderComp.AddPlayerAnimationController("custom_sword_first_attack_controller",
"controller.animation.custom_sword_by_addon_bones")
actorRenderComp.AddPlayerScriptAnimate(
"custom_sword_first_hold",
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_sword_by_addon_bones'"
)
actorRenderComp.AddPlayerScriptAnimate("custom_sword_first_attack_controller", "variable.is_first_person")
```
这里做几点说明:
- `query.get_equipped_item_name('main_hand') == 'custom_sword_by_addon_bones'` 这里对应的条件是当玩家主手有对应物品时生效,并且后面的标识符是不带前缀的,至少我自己测试时如果写了的是 `tutorial:custom_sword_by_addon_bones` 会失效;
- 这里判断第一人称要使用 `variable.is_first_person`
#### Step 5. 处理攻击事件
首先客户端需要检测左键按下的事件,然后还需要把本地玩家的攻击状态同步给其他客户端:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import time
import config
CompFactory = clientApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
#
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
self.mItemComp = CompFactory.CreateItem(clientApi.GetLocalPlayerId())
#
self.mCarriedItem = self.mItemComp.GetCarriedItem() # 手持物品
self.mAttackStep = 0 # 攻击的阶段
self.mLastAttackTime = 0 # 上一次攻击的时间
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncAttackStateEvent', self, self.OnSyncAttackStateEvent)
# 处理攻击相关的事件,监听左键按下事件
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "TapBeforeClientEvent",
self, self.OnLeftClick)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "LeftClickBeforeClientEvent",
self, self.OnLeftClick)
# 接受到其他客户端传来的同步事件
def OnSyncAttackStateEvent(self, args):
playerId = args['playerId']
value = float(args['value'])
CompFactory.CreateQueryVariable(playerId).Set(config.AttackVarName, value)
def OnLeftClick(self, args=None):
# 手持自定义武器并且上一次的攻击快要结束时,才会响应攻击
if self._IsCarriedCustomWeapon():
args['cancel'] = True # 不响应原版的点击事件
if self._LastAttackWillFinished():
self._HandleAttack()
def _IsCarriedCustomWeapon(self):
self.mCarriedItem = self.mItemComp.GetCarriedItem()
return self.mCarriedItem and self.mCarriedItem['itemName'] in config.CustomWeaponList
def _LastAttackWillFinished(self):
currentTime = time.time()
return currentTime - config.AttackInterval > self.mLastAttackTime
def _HandleAttack(self):
self.mLastAttackTime = time.time()
gameComp.AddTimer(0, self._SetAttackStateAndSyncToOtherClients, 'start')
gameComp.AddTimer(0.2, self._SetAttackStateAndSyncToOtherClients, 'will_hit')
gameComp.AddTimer(0.5, self._SetAttackStateAndSyncToOtherClients, 'end')
def _SetAttackStateAndSyncToOtherClients(self, state):
# 设置本地的自定义变量
self.mQueryVariableComp.Set(config.AttackVarName, 1.0 if state in ['start', 'will_hit'] else 0)
# 通知其他客户端更新数据
self.NotifyToServer("SyncAttackStateEvent", {'playerId': clientApi.GetLocalPlayerId(), 'state': state})
```
服务端就是响应攻击以及同步玩家的攻击状态就可以了:
```python
# -*- coding: utf-8 -*-
#
import math
import mod.server.extraServerApi as serverApi
from mod.common.utils.mcmath import Vector3
CompFactory = serverApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(serverApi.GetLevelId())
class TutorialServerSystem(serverApi.GetServerSystemCls()):
def __init__(self, namespace, name):
super(TutorialServerSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncAttackStateEvent", self, self.OnSyncAttackStateEvent)
# region 监听事件
# --------------------------------------------------------------------------------------------
def OnSyncAttackStateEvent(self, args):
playerId = args['playerId']
state = args['state']
if state == 'will_hit':
weaponDamage = self._GetCarriedWeaponDamage(playerId)
self._HurtFrontArea(playerId, 3, weaponDamage)
else:
relevantPlayers = CompFactory.CreatePlayer(playerId).GetRelevantPlayer([playerId])
self.NotifyToMultiClients(relevantPlayers, 'SyncAttackStateEvent', {
'playerId': playerId,
'value' : 1.0 if state == 'start' else 0.0
})
# endregion
# region 类函数
# --------------------------------------------------------------------------------------------
def _GetCarriedWeaponDamage(self, playerId):
itemComp = CompFactory.CreateItem(playerId)
carriedItem = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
basicInfo = itemComp.GetItemBasicInfo(carriedItem['itemName'])
return basicInfo['weaponDamage']
# 攻击前方的区域
def _HurtFrontArea(self, attacker, radius, damage, betweenAngle=75, knocked=True):
attackerPos = CompFactory.CreatePos(attacker).GetFootPos()
attackerRot = CompFactory.CreateRot(attacker).GetRot()
# 计算攻击者朝向向量
forwardVector = serverApi.GetDirFromRot(attackerRot)
entityList = gameComp.GetEntitiesAround(attacker, 6, {
'any_of': {
'test' : 'is_family',
'subject' : 'other',
'operator': 'not',
'value' : 'instabuild'
}
})
for _entityId in entityList:
entityPos = CompFactory.CreatePos(_entityId).GetFootPos()
delta = Vector3(entityPos) - Vector3(attackerPos)
# 计算角度
angle = math.degrees(math.acos(Vector3.Dot(delta.Normalized(), Vector3(forwardVector).Normalized())))
# 判断是否在扇形攻击范围内
if betweenAngle == 0.0 or (angle < betweenAngle and delta.Length() < radius):
CompFactory.CreateHurt(_entityId).Hurt(
damage, serverApi.GetMinecraftEnum().ActorDamageCause.EntityAttack, attacker, knocked=knocked
)
# endregion
```
#### Step 6. 进入游戏检查效果
进入游戏,就可以看到看到效果了:
![方法1游戏测试](./assets/1_1.gif)
### 方法2绑定骨骼
第二种方法,就是把我们制作好的模型绑定在任何可附加的生物骨骼上。**原版**的三叉戟、望远镜、弓和盾牌都是采用的这种方法。
虽然这种方法能够应用在更多的生物上,但模型绑定也有一些特殊的地方。我们下面会进行说明。
#### Step 1. 改造模型文件
首先,我们需要导出我们的模型文件,然后确认模型文件的版本是 `1.16.0`,然后在根骨骼组下加入下列一行话:
```text
"binding": "query.item_slot_to_bone_name(context.item_slot)"
```
整个文件就会看起来像是这样:
```json
{
"format_version": "1.16.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.tutorial_custom_sword_by_attachable",
"texture_width": 64,
"texture_height": 64,
"visible_bounds_width": 4,
"visible_bounds_height": 2.5,
"visible_bounds_offset": [0, 0.75, 0]
},
"bones": [
{
"name": "sword",
"binding": "query.item_slot_to_bone_name(context.item_slot)",
"pivot": [-6, 14, 0]
},
// ...............
```
这时候的模型文件就不需要对齐玩家的骨骼组了,而是默认会根据物品的槽位来对骨骼进行绑定:
- 如果是主手则绑定 `rightItem` 骨骼;
- 如果是副手则绑定 `leftItem` 骨骼;
这里我们也不对模型进行位置的修改了,只是把骨骼组的根改名为了 `sword`
![](./assets/image-20231124112144873.png)
我们自己在创建模型的时候也要注意骨骼组名称不要跟原版的骨骼重名了。
#### Step 2. 创建动画
这时候我们使用 BlockBench 的动画模式,会发现菜单栏会出现几个选项供我们选择视角:
![](./assets/image-20231124112318044.png)
##### 第三人称修正动画
在选择第三人称视角的情况下BlockBench 会自动出现一个玩家模型,这可以让我们来查看实际的情况情况:
![](./assets/image-20231124112507125.png)
所以我们要做的就是创建一个动画来修正位置就可以了:
![](./assets/image-20231124112547968.png)
##### 第三人称攻击动画
如果我们想要自定义的第三人称攻击动画,我们仍然需要把骨骼拖入原版的玩家骨骼之中,然后完成第三人称动画:
![](./assets/image-20231124001254351.png)
![k的第三人称动画](./assets/1_2.gif)
这时候吊诡的地方就出现了,我们先完成动画,之后再进行说明。
##### 第一人称动画
跟第一种方法一样,我们需要一个手持修正的动画和一个攻击动画,这里就演示一下成果:
![第一人称动画演示](./assets/1_4.gif)
#### Step 3. 准备动画控制器
第三人称的玩家动画控制器已经准备好了,我们只需要准备用于 attachable 里面的动画控制器就可以:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.custom_sword.first_attack": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"first_person_attack1": "query.mod.custom_sword_attack == 1.0"
}
]
},
"first_person_attack1": {
"animations": [
// 对应 attachable 里面配置的动画名称
"first_person_attack1"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_sword_attack == 0.0"
}
]
}
}
}
}
}
```
#### Step 4. attable 文件
我们需要在资源包下的 `attable` 文件夹下创建一个用于匹配我们自定义物品的附加物品定义文件:
```json
{
"format_version": "1.10",
"minecraft:attachable": {
"description": {
"identifier": "tutorial:custom_sword_by_attachable",
"materials": {
"default": "entity_alphatest",
"enchanted": "entity_alphatest_glint"
},
"textures": {
"default": "textures/models/tutorial_custom_sword",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.tutorial_custom_sword_by_attachable"
},
"animations": {
// 第一人称手持动画
"hold_first_person": "animation.tutorial_custom_sword_by_attachable.hold_first_person",
// 第三人称手持动画
"hold_third_person": "animation.tutorial_custom_sword_by_attachable.hold_third_person",
// 第一人称下的攻击动画
"first_person_attack1": "animation.tutorial_custom_sword_by_attachable.first_attack1",
"first_person_attack2": "animation.tutorial_custom_sword_by_attachable.first_attack2",
// 第一人称下的攻击动画控制器
"first_person_attack_controller": "controller.animation.custom_sword.first_attack",
// 第三人称下的攻击动画(共用玩家的动画就可以)
"third_person_attack1": "animation.tutorial_custom_sword_by_attachable.player.attack1",
// 第三人称动画控制器,共用玩家的就可以
"third_person_attack_controller": "controller.animation.custom_sword.third_attack"
},
"scripts": {
"animate": [
{
"first_person_attack_controller": "c.is_first_person"
},
{
"third_person_attack_controller": "!c.is_first_person"
},
{
"hold_first_person": "c.is_first_person"
},
{
"hold_third_person": "!c.is_first_person"
}
]
},
"render_controllers": [
"controller.render.item_default"
]
}
}
}
```
除了正确匹配资源文件之外,吊诡和麻烦的地方就来了。
仔细观察,我们可以发现,这个文件的结构跟 `r\entity` 下的文件很像,本质上也是一种渲染控制器。而**控制器只能控制自身范围内的资源**。
我们在创建第三人称动画时k 的动画同时涉及了玩家和物品。而玩家和物品可以说是独立的两个个体,所以说他们的控制器互相不影响,包括动画。
我们在第三人称的情况下,播放玩家动画时,由于玩家本身不存在 `sword` 骨骼组,所以玩家的动画对物品的骨骼没有一丝影响。
不在物品设置第三人称动画的情况下动画演示:
![第三人称动画演示1](./assets/1_5.gif)
对比我们自己 k 的动画,会发现,物品相关的骨骼完全没有移动:
![k的第三人称动画](./assets/1_2.gif)
反过来如果我们仅仅是在物品下attachable 文件中)设置了第三人称动画,玩家的骨骼也不会受到影响:
![第三人称动画演示2](./assets/1_6.gif)
这里就是方法二麻烦的地方:你需要**同时处理**玩家和物品的第三人称动画。
#### Step 5. 代码注入资源
其他就跟方法一如出一辙了,主要是注册自定义的攻击变量,以及注入第三人称动画和控制器:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import time
import config
CompFactory = clientApi.GetEngineCompFactory()
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
self, self.OnAddPlayerCreatedClientEvent)
def OnAddPlayerCreatedClientEvent(self, args):
playerId = args['playerId']
self.InitRender(playerId) # 包括其他玩家也需要被初始化
# 初始化绑定
def InitRender(self, playerId):
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
# 定义攻击的自定义变量
queryVariableComp.Register(config.AttackVarName, 0)
# 第一种方法:作为玩家的附加骨骼添加进渲染控制器中进行控制
self._InitMethodOne(playerId)
# 另外一种形式的 3D 武器:附加骨骼
# 则是通过 query.item_slot_to_bone_name 把模型绑定上了固定的位置,再用动画进行修正
# 两种方式都通用的第三人称动画和控制器
actorRenderComp = CompFactory.CreateActorRender(playerId)
actorRenderComp.AddPlayerAnimation("third_person_attack1", "animation.tutorial_custom_sword_by_attachable.player.attack1")
actorRenderComp.AddPlayerAnimationController("custom_sword_third_attack", "controller.animation.custom_sword.third_attack")
actorRenderComp.AddPlayerScriptAnimate("custom_sword_third_attack", "!variable.is_first_person")
actorRenderComp.RebuildPlayerRender()
```
#### Step 6. 处理攻击事件
这里跟第一种方法一样,所以就不再重复了。
## 小结
两种方法各有优劣吧。不过第一种方法来说,可以不局限在某一个具体的武器上,也可以是其他任何一个附加的骨骼(包括不限于翅膀、盔甲、盾牌等)。不过缺点就是只能局限在某一个具体的实体上(毕竟模型骨骼是需要严格对齐的)。
方法二就是一个标准的原版的方法了,对于一般的武器装备,这也是比较推荐的方法。这样原版的僵尸一类的生物也可以像玩家一样使用武器。
## 课后作业
本次课后作业,内容如下:
- 分别使用两种方法制作 3D 武器,感受两种方法的不同之处。

View File

@@ -0,0 +1,251 @@
# 制作3D盔甲
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将帮助你添加一套拥有独立骨骼的 3D 盔甲,并且帮助你修改原版盔甲。(强烈建议阅读之前先阅读第一节课的内容,因为思路一样)
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程如果对此感兴趣的同学可以自行学习和了解。
在本教程中,您将学习以下内容。
- ✅3D 盔甲的制作原理;
- ✅如何修改原版盔甲;
## 成果展示
这节课我们将添加一套自定义骨骼的 3D 盔甲:
![](./assets/image-20231124151500984.png)
以及我们在尝试修改原版的盔甲之后,所有的原版盔甲都改变了造型(以钻石甲为例):
![](./assets/image-20231126141604269.png)
可以看到头盔上多了一个问号,肚子也鼓起来了,然后鞋子上多了一个骨骼。(别喷.. 我也觉得丑..
## 3D 盔甲的制作方法
跟我们的 3D 武器是一个思路,还是存在两种方法。一种是作为额外的骨骼,用渲染控制器来条件渲染,这需要代码配合。由于盔甲一般比较简单,所以我们这里不介绍了。
重点还是介绍第二种 attachable 的方法。
### 3D 盔甲模型原理
我们这里先在内容库「偷」一套 3D 盔甲的模型:
![](./assets/image-20231126181256641.png)
这里先随便选择一套盔甲模型,打开:
![](./assets/image-20231126181413356.png)
我们在第一节课中介绍过,这里 3D 盔甲的关键就是**对齐原版的玩家骨骼**,如果你不熟悉的话,可以打开原版的玩家骨骼进行检查(下列是玩家模型骨骼树状图,锚点即模型格式内的 pivot
```text
-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
```
只要骨骼在对应的骨骼组下方,且锚点对应的,那么 3D 盔甲就可以完美继承玩家的骨骼。所以我们在一番检查之后发现这个骨骼存在以下问题:
- 首先是少了根骨骼组 `root`,我们给加上;
- 其次是左右腿的继承关系错了,原版是继承在 `waist` 下,而这个模型是继承在了 `body` 下面;
- 再然后就是左右腿的 `pivot` 反了,原版左腿(`leftLeg`)是 `1.9`,而该模型左腿是 `-1.9`,右腿也是同理。
一番检查之后,我们就可以按照一般装备的划分,把骨骼拆成对应的四个部分就行了:
![](./assets/image-20231126182318172.png)
注意上图中的骨骼对应关系。然后导出模型到资源包的 `modles\entity\` 目录下即可。
### attachable 文件
接下来新增 `attachable` 文件让物品与骨骼对应就可以,下面列举一下四个文件。除了骨骼和 `scripts` 中的内容不一样外,其余都是相同的。
头部 helmet
```json
{
"format_version": "1.10",
"minecraft:attachable": {
"description": {
"identifier": "tutorial:custom_armor_helmet",
"materials": {
"default": "armor",
"enchanted": "armor_enchanted"
},
"textures": {
"default": "textures/models/tutorial_custom_armor",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.custom_armor_helmet"
},
"scripts": {
"parent_setup": "variable.helmet_layer_visible = 0.0;"
},
"render_controllers": [
"controller.render.armor"
]
}
}
}
```
盔甲 chestplate
```json
{
"format_version": "1.10",
"minecraft:attachable": {
"description": {
"identifier": "tutorial:custom_armor_chestplate",
"materials": {
"default": "armor",
"enchanted": "armor_enchanted"
},
"textures": {
"default": "textures/models/tutorial_custom_armor",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.custom_armor_chestplate"
},
"scripts": {
"parent_setup": "variable.helmet_layer_visible = 0.0;"
},
"render_controllers": [
"controller.render.armor"
]
}
}
}
```
绑腿 leggings
```json
{
"format_version": "1.10",
"minecraft:attachable": {
"description": {
"identifier": "tutorial:custom_armor_leggings",
"materials": {
"default": "armor",
"enchanted": "armor_enchanted"
},
"textures": {
"default": "textures/models/tutorial_custom_armor",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.custom_armor_leggings"
},
"scripts": {
"parent_setup": "variable.helmet_layer_visible = 0.0;"
},
"render_controllers": [
"controller.render.armor"
]
}
}
}
```
鞋子 boots
```json
{
"format_version": "1.10",
"minecraft:attachable": {
"description": {
"identifier": "tutorial:custom_armor_boots",
"materials": {
"default": "armor",
"enchanted": "armor_enchanted"
},
"textures": {
"default": "textures/models/tutorial_custom_armor",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.custom_armor_boots"
},
"scripts": {
"parent_setup": "variable.helmet_layer_visible = 0.0;"
},
"render_controllers": [
"controller.render.armor"
]
}
}
}
```
### 进入游戏测试
不出意外的话,进入游戏穿戴好装备,就可以看到实际的效果了:
![](./assets/image-20231126182907228.png)
并且因为骨骼组跟玩家完美适配的情况下,玩家原版的动画也完美适配(比如游泳):
![游泳动画演示](./assets/2_1.gif)
## 原版盔甲的修改方法
其实原版的盔甲模型,我们也是可以修改的。原版玩家的盔甲模型位于原版资源包 `models\entity\player_armor.json`
![](./assets/image-20231126183125264.png)
我们复制一份到我们的项目对应目录中,然后使用 BlockBench 打开,发现这文件中有很多骨骼:
![](./assets/image-20231126183332640.png)
我们只需要处理第一个就可以了,打开:
![](./assets/image-20231126183640077.png)
这个就是原版盔甲的骨骼了,由于需要适配原版的贴图,所以我们先来导入一下原版的纹理,下面是目录:
![](./assets/image-20231126183729470.png)
这里需要导入两个,`_1` 结尾的是不包含绑腿的贴图,`_2` 结尾的是包含绑腿的贴图:
![](./assets/image-20231126183821545.png)
然后我们就可以在这个框架内任意的增加骨骼了,只需要注意适配原版的贴图就可以,比如我们就胡乱改成了这样:
![](./assets/image-20231126183912027.png)
进入游戏查看效果:
![](./assets/image-20231126141604269.png)
## 小结
不管是自定义的 3D 盔甲模型还是修改原版的模型,我们发现都是需要在原版的骨骼架构内完成。
所以只需要注意这一点就行了。
修改原版骨骼这一点,是给大家留了一点可以想象的空间,可以自由发挥一下。
## 课后作业
本次课后作业,内容如下:
- 制作一套属于自己的 3D 盔甲;
- 修改原版的盔甲模型文件,在游戏中查看效果;

View File

@@ -0,0 +1,918 @@
# 枪械制作
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将帮助你添加一个自定义的枪械,包含了界面的相关文件。
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程如果对此感兴趣的同学可以自行学习和了解。
在本教程中,您将学习以下内容。
- ✅枪械的制作;
- ✅第一人称显示手臂的两种方式;
- ✅枪械 UI 的搭建;
## 成果展示
我们在本节课中将要制作一个带有开火特效,第一、第三人称动画完整,并且附带人物手臂显示和界面的自定义枪械:
![枪械展示](./assets/3_1.gif)
## 制作枪械
换个角度来看,枪械就是更复杂一些的 3D 武器罢了。这里比较**推荐**使用方法一(请查看「制作 3D 武器章节」)来制作枪械,因为这会方便我们制作动画,以及节省我们编写和调试 attachable 文件的时间,还有一些额外的好处。
以下是一个枪械所需要的基本文件目录结构(不包含 ui 和贴图文件)和相应的说明:
```text
├─behavior_packs
│ └─tutorial_b
│ ├─entities
│ │ tutorial_custom_gun_bullet_projectile.json → 子弹抛射物
│ │
│ ├─netease_items_beh
│ │ tutorial_custom_gun.json → 自定义枪械物品
│ │ tutorial_custom_gun_bullet.json → 子弹物品定义
│ │
│ └─tutorialScripts → 代码
└─resource_packs
└─tutorial_r
├─animations
│ tutorial_custom_gun.animation.json → 自定义枪械动画
│ tutorial_custom_gun_bullet_project.animation.json → 子弹抛射物的动画
├─animation_controllers
│ tutorial_custom_gun.animation_controllers.json → 枪械动画控制器
├─entity
│ tutorial_custom_gun_bullet_projectile.entity.json → 子弹抛射物的渲染器定义
├─models
│ └─entity
│ tutorial_custom_gun.geo.json → 自定义枪械骨骼
│ tutorial_custom_gun_bullet_projectile.geo.json → 子弹抛射物骨骼
├─netease_items_res
│ tutorial_custom_gun.json
│ tutorial_custom_gun_bullet.json
├─particles
│ gun_fire.particle.json → 开火特效
└─render_controllers
player.render_controllers.json → 原版玩家渲染控制器,主要用来修改第一人称下的右臂显示
tutorial_custom_gun.render_controllers.json → 自定义枪械控制器
```
让我们开始吧。
### 显示手臂的两种方法
为什么先来介绍这个呢,因为这涉及到我们对于模型和动画的处理。总体思路有两种:
[第一种](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/20-%E7%8E%A9%E6%B3%95%E5%BC%80%E5%8F%91/15-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B8%B8%E6%88%8F%E5%86%85%E5%AE%B9/1-%E8%87%AA%E5%AE%9A%E4%B9%89%E7%89%A9%E5%93%81/9-%E8%87%AA%E5%AE%9A%E4%B9%893D%E7%89%A9%E5%93%81/3-%E8%87%AA%E5%AE%9A%E4%B9%893D%E6%AD%A6%E5%99%A8%E6%94%BB%E5%87%BB%E6%95%88%E6%9E%9C%EF%BC%88%E4%B8%8A%EF%BC%89.html?catalog=1):为第一人称单独建一个带玩家手臂的模型,然后让手臂继承原版的 uv 继承,让手臂使用原版玩家的贴图。原理大概如下:
![](./assets/attack0_5.0abe0a89.png)
第二种:用动画控制原版的手臂并让其在第一人称下显示。
我们这里也是使用的第二种方法,毕竟第一种需要对美术有要求。
我们先来说一下第二种方法的原理。我们可以先了解一下原版第一人称**不显示手臂的原因**,观察原版的 `player.render_controller.json` 文件:
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.player.first_person": {
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
"textures": [ "Texture.default" ],
"part_visibility": [
{ "*": false },
{ "rightArm": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map'" },
{ "rightSleeve": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map'" },
{ "leftArm": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" },
{ "leftSleeve": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" }
]
},
```
可以看到,只有在不拿物品或者拿着指定物品时,左右手臂才会显示。这意味着我们可以通过修改原版的控制器,再配合动画,就可以在第一人称显示手臂了。
### 添加基础文件
首先基础物品的定义都很简单,一个子弹物品,记得设置 `max_stack_size` 为 64另一个自定义枪械记住添加以下组件就 OK
```json
"netease:show_in_hand": {
"value": false
},
```
我们直接进入到模型的制作。对齐原版的骨骼制作好一个基础的模型文件(当然位置也要对齐):
![](./assets/image-20231125143841803.png)
由于我们需要挂接开火的特效,所以模型上还需要额外添加上两个定位器:
![](./assets/image-20231126111446817.png)
然后准备一个简单的激光子弹模型:
![](./assets/image-20231126111216700.png)
### 开火特效
我们先使用原版的粒子贴图来制作一个开火特效,起因是我们发现原版的贴图中有一排很适合用来模拟开火特效的粒子贴图:
![](./assets/image-20231126115936850.png)
简单弄一下效果:
![特效效果展示](./assets/3_2.gif)
关于原版特效的创建与导入可参考[原版特效创建与导入](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/16-美术/9-特效/20-原版特效创建与导入.html?catalog=1)
特效文件如下:
```json
{
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "tutorial:gun_fire",
"basic_render_parameters": {
"material": "particles_alpha",
"texture": "textures/particle/particles"
}
},
"components": {
"minecraft:emitter_local_space": {
"position": true,
"rotation": true
},
"minecraft:emitter_rate_instant": {
"num_particles": 2
},
"minecraft:emitter_lifetime_expression": {
"activation_expression": 0.1
},
"minecraft:emitter_shape_point": {},
"minecraft:particle_lifetime_expression": {
"max_lifetime": 0.1
},
"minecraft:particle_initial_spin": {
"rotation": "Math.random(-200, 200)",
"rotation_rate": "Math.random(-200, 200)"
},
"minecraft:particle_initial_speed": 0,
"minecraft:particle_motion_dynamic": {},
"minecraft:particle_appearance_billboard": {
"size": ["variable.particle_random_1 * 0.3 + variable.particle_age", "variable.particle_random_1 * 0.3 + variable.particle_age"],
"facing_camera_mode": "lookat_xyz",
"uv": {
"texture_width": 128,
"texture_height": 128,
"flipbook": {
"base_UV": [0, 72],
"size_UV": [8, 8],
"step_UV": [8, 0],
"frames_per_second": 4,
"max_frame": 8,
"stretch_to_lifetime": true
}
}
},
"minecraft:particle_appearance_tinting": {
"color": {
"interpolant": "variable.particle_age / variable.particle_lifetime",
"gradient": {
"0.0": "#FFE2D51E",
"0.21": "#FFF7F5EC",
"0.41": "#FFC12807",
"1.0": "#FFB94C02"
}
}
}
}
}
}
```
### 动画文件
我们先来准备一个用于制作动画的模型,复制一份原版的玩家模型,然后导入我们的自定义枪械模型,删除贴图:
![](./assets/image-20231126115406787.png)
#### 第三人称动画
由于骨骼完美继承玩家,所以第三人称就不需要有对齐的动画了,只需要一个攻击动画。
我们来简单弄一个抬手开火的动画:
![第三人称动画展示](./assets/3_3.gif)
这里的特效需要用到制作模型时加入的定位器:
![](./assets/image-20231126120825562.png)
可以直接在 BlockBench 中看到效果,还是非常方便的。
另外由于我们手持物品时,原版默认有一个小小的抬起,所以动画这里要设置成覆盖:
```json
// 因为原版手持物品时,手臂有一个向上抬起的偏移量,这里需要覆盖掉
"override_previous_animation": true,
```
#### 第一人称动画
第一人称需要两个动画:手持动画和攻击动画。
正如我们第一节课中说的那样,需要先模拟游戏中第一人称的视角,这需要一个动画:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.first_person_guide.right_arm.method_one": {
"loop": true,
"bones": {
"rightArm": {
"rotation": [95, -45, 115],
"position": [13.5, -10, 12]
},
"rightItem": {
"position": [0, 0, -1]
}
}
}
}
}
```
和一个固定的相机视角:
![](./assets/image-20231126113433017.png)
然后在播放上述动画的情况下,把除了 `rightArm` 之外的骨骼全部隐藏掉k 我们自己的第一人称手持动画就可以了:
![](./assets/image-20231126115603941.png)
攻击动画建议直接先**复制**第三人称攻击动画的节点,主要是对齐动画发生的时间节点,然后再在播放「手持动画」和「第一人称模拟动画」之后对着第三人称动画 k 就行了:
![第一人称攻击动画展示](./assets/3_4.gif)
### 动画控制器
准备两个动画控制器,分别在第一人称和第三人称的情况下控制播放不同的动画,没什么好说的,除了播放的动画名称不一样,其余都相同,完整文件如下:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.custom_gun_first_person": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"first_person_attack1": "query.mod.custom_gun_attack == 1.0"
}
]
},
"first_person_attack1": {
"animations": [
"custom_gun_attack_first_person"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_gun_attack == 0.0"
}
]
}
}
},
"controller.animation.custom_gun_third_person": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"third_person_attack1": "query.mod.custom_gun_attack == 1.0"
}
]
},
"third_person_attack1": {
"animations": [
"custom_gun_attack_third_person"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_gun_attack == 0.0"
}
]
}
}
}
}
}
```
这里的 `query.mod.custom_gun_attack` 是需要后续在代码中注册和设置的自定义变量。
### 渲染器
这里我们需要先把原版的 `player.render_controllers.json` 复制到我们的项目中(`_r\render__controllers\` 目录下),然后进行改造:
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.player.first_person": {
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
"textures": [ "Texture.default" ],
"part_visibility": [
{ "*": false },
// 修改原版渲染器,让它支持在手持自定义枪械时,显示右手臂
{ "rightArm": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun'" },
{ "rightSleeve": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun'" },
{ "leftArm": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" },
{ "leftSleeve": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" }
]
},
```
主要的变动就是在 `rightArm``rightSleeve` 的条件中,加入了手持自定义枪械的 Molang。
然后就是新增一个自定义枪械的渲染器(`tutorial_custom_gun.render_controllers.json`
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.tutorial_custom_gun": {
"geometry": "Geometry.custom_gun",
"materials": [{"*": "Material.default"}],
"textures": ["Texture.custom_gun"]
}
}
}
```
### 子弹抛射物
行为包完整文件如下:
```json
{
"format_version": "1.13.0",
"minecraft:entity": {
"description": {
"is_experimental": false,
"identifier": "tutorial:custom_gun_bullet_projectile",
"is_spawnable": false,
"is_summonable": false
},
"component_groups": {
},
"components": {
"minecraft:despawn": {
"despawn_from_distance": {}
},
"minecraft:physics": {},
"minecraft:projectile": {
"on_hit": {
"remove_on_hit": {},
"impact_damage": {
"catch_fire": false,
"knockback": true,
"damage": 4,
"destroy_on_hit": true
}
},
"gravity": 0.0,
"power": 1.0,
"offset": [
0,
0,
0
]
},
"minecraft:collision_box": {
"width": 0.31,
"height": 0.31
},
"netease:custom_entity_type": {
"value": "projectile_entity"
},
"minecraft:pushable": {
"is_pushable_by_piston": true,
"is_pushable": true
}
},
"events": {
}
}
}
```
重点还是添加上网易的组件,标识上这个实体是一个抛射物:
```json
"netease:custom_entity_type": {
"value": "projectile_entity"
},
```
资源包的 `.entity.json` 文件定义也很简单:
```json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial:custom_gun_bullet_projectile",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/entity/tutorial_custom_gun_bullet_projectile"
},
"geometry": {
"default": "geometry.tutorial_custom_gun_bullet_projectile"
},
"animations": {
"move": "animation.tutorial_custom_gun_bullet_projectile.move"
},
"scripts": {
"animate": [
"move"
]
},
"render_controllers": [
"controller.render.default"
]
}
}
}
```
这里的 `move` 动画,是一个固定的动画,让子弹抛射物的根骨骼(这里是 `body`)执行下列文件就行:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.tutorial_custom_gun_bullet_projectile.move": {
"loop": true,
"bones": {
"body": {
"rotation": [
"-query.target_x_rotation",
"-query.target_y_rotation",
0.0
]
}
}
}
}
}
```
这个动画会让抛射物始终朝向速度方向。
### 添加相关资源
我们用代码把刚才的相关资源全部注入到玩家的渲染器中:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import time
import config
CompFactory = clientApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
#
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
self.mItemComp = CompFactory.CreateItem(clientApi.GetLocalPlayerId())
#
self.mCarriedItem = self.mItemComp.GetCarriedItem() # 手持物品
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncCustomGunStateEvent', self, self.OnSyncCustomGunStateEvent)
# 系统事件
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
self, self.OnAddPlayerCreatedClientEvent)
def OnSyncCustomGunStateEvent(self, args):
playerId = args['playerId']
value = float(args['value'])
CompFactory.CreateQueryVariable(playerId).Set(config.CustomGunAttackVarName, value)
def OnAddPlayerCreatedClientEvent(self, args):
playerId = args['playerId']
self.InitRender(playerId) # 包括其他玩家也需要被初始化
# 初始化绑定
def InitRender(self, playerId):
# 自定义枪械
self._InitToCustomGun(playerId)
# 自定义枪械初始化渲染
def _InitToCustomGun(self, playerId):
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
queryVariableComp.Register(config.CustomGunAttackVarName, 0)
queryVariableComp.Set(config.CustomGunAttackVarName, 0)
actorRenderComp = CompFactory.CreateActorRender(playerId)
actorRenderComp.AddPlayerGeometry('custom_gun', 'geometry.tutorial_custom_gun')
actorRenderComp.AddPlayerTexture('custom_gun', 'textures/models/tutorial_custom_gun')
actorRenderComp.AddPlayerRenderController("controller.render.tutorial_custom_gun",
"query.get_equipped_item_name('main_hand') == 'custom_gun'")
# 定义动画和控制器名称
animations = ['hold_first_person', 'attack_first_person', 'attack_third_person']
controllers = ['custom_gun_first_person', 'custom_gun_third_person']
for anim in animations:
animationKey = 'custom_gun_' + anim
animationName = 'animation.tutorial_custom_gun.' + anim
actorRenderComp.AddPlayerAnimation(animationKey, animationName)
for controller in controllers:
controllerKey = controller + "_controller"
controllerName = 'controller.animation.' + controller
actorRenderComp.AddPlayerAnimationController(controllerKey, controllerName)
# 添加动画的触发条件
actorRenderComp.AddPlayerScriptAnimate(
'custom_gun_hold_first_person',
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_gun'"
)
actorRenderComp.AddPlayerScriptAnimate(
'custom_gun_first_person_controller',
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_gun'"
)
actorRenderComp.AddPlayerScriptAnimate(
'custom_gun_third_person_controller',
"!variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_gun'"
)
# 添加特效
actorRenderComp.AddPlayerParticleEffect('gun_fire', 'tutorial:gun_fire')
# 有兴趣的同学也可以尝试加一下音效
```
### 射速的实现
要实现射速,除了代码上的配合外,还需要动画也跟着加速,正好原版支持这一特性。我们只需要在动画文件中添加上神秘代码就可以:
```json
"animation.tutorial_custom_gun.attack_third_person": {
// 实现射速
"anim_time_update": "query.anim_time + query.delta_time * query.mod.custom_gun_attack_speed",
```
其中,前面的 `query.anim_time + query.delta_time` 是必须的,而后面的参数是可以自定义的,比如 `query.anim_time + query.delta_time * 2` 就是 2 倍速播放,`query.anim_time + query.delta_time * 1` 就是原速播放。
所以我们这里又使用了另外一个自定义变量,我们也需要在初始化的时候声明好:
```python
# 自定义枪械初始化渲染
def _InitToCustomGun(self, playerId):
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
# 射速相关的定义
queryVariableComp.Register(config.CustomGunAttackSpeedVarName, config.CustomGunDefaultAttackSpeed)
queryVariableComp.Set(config.CustomGunAttackSpeedVarName, config.CustomGunDefaultAttackSpeed)
```
3 倍射速演示:
![三倍射速演示](./assets/3_5.gif)
0.5 倍速演示:
![0.5倍速演示](./assets/3_6.gif)
### UI 实现
UI 很简单,只需要在右下角显示当前背包中的子弹数量。然后在右下角固定一个射击按钮就可以了:
![](./assets/image-20231126130431189.png)
在客户端监听 `UiInitFinished` 事件之后进行注册:
```python
def OnUiInitFinished(self, args=None):
# 注册 UI
clientApi.RegisterUI('tutorialMod', 'tutorialCustomGunUI', 'tutorialScripts.uiScripts.UIScripts', 'tutorialGunUI.main')
self.mCustomGunUINode = clientApi.CreateUI('tutorialMod', 'tutorialCustomGunUI', {'isHud': 1})
```
### UI 代码
首先,我们需要在手持自定义枪械的时候显示 UI完整代码如下
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import config
ViewBinder = clientApi.GetViewBinderCls()
ViewRequest = clientApi.GetViewViewRequestCls()
ScreenNode = clientApi.GetScreenNodeCls()
Namespace = clientApi.GetEngineNamespace()
SystemName = clientApi.GetEngineSystemName()
CompFactory = clientApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
#ui布局绑定
class UIScripts(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mPlayerId = clientApi.GetLocalPlayerId()
self.mClientSystem = clientApi.GetSystem('tutorialMod', 'tutorialClientSystem')
self.mItemComp = CompFactory.CreateItem(self.mPlayerId)
self.mQueryVariableComp = CompFactory.CreateQueryVariable(self.mPlayerId)
# 组件地址
self.mShotBtnPath = "/shotBtn"
self.mBulletLabelPath = "/bulletNumLabel"
# 界面需要使用的自定义属性
self.mFrameCnt = 0
self.mCarriedItem = self.mItemComp.GetCarriedItem() # 手持物品
self.mIsBtnDown = False # 按钮是否被按下
self.mBtnDownFrame = 0 # 按钮按下帧数
self.mShotSpeed = 0 # 攻击速度
self.mAttackFrame = 0 # 攻击所需要的帧数
self.mBulletNum = 0 # 子弹数量
def Create(self):
print("===== Tutorial Custom Gun UI Create Finished =====")
# 注册按钮的事件
control = self.GetBaseUIControl(self.mShotBtnPath).asButton()
control.AddTouchEventParams({"isSwallow": True})
control.SetButtonTouchDownCallback(self.OnShotBtnDown)
control.SetButtonTouchUpCallback(self.OnShotBtnUp)
control.SetButtonTouchMoveOutCallback(self.OnShotBtnUp)
# 关注事件
namespace, systemName = clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName()
self.mClientSystem.ListenForEvent(namespace, systemName, "OnCarriedNewItemChangedClientEvent", self, self.OnCarriedNewItem)
# 刚创建时也自动触发一次
self.OnCarriedNewItem({'itemDict': self.mItemComp.GetCarriedItem()})
def Update(self):
self.mFrameCnt += 1
if self.mBtnDownFrame > 0:
if self.mBtnDownFrame + self.mAttackFrame <= self.mFrameCnt:
if self.mIsBtnDown:
# 还处于按下的状态,那么继续攻击
self.mBtnDownFrame = self.mFrameCnt + self.mAttackFrame
self._HandleAttack()
else:
# 还原 query 变量
self.mQueryVariableComp.Set(config.CustomGunAttackVarName, 0.0)
# region 按钮事件
# --------------------------------------------------------------------------------------------
def OnShotBtnDown(self, args):
self._FreshBagBullet()
if self.mBulletNum == 0:
# 没有子弹不响应
return
self.mIsBtnDown = True
if self.mFrameCnt >= self.mBtnDownFrame + self.mAttackFrame:
self.mBtnDownFrame = self.mFrameCnt
self._HandleAttack()
def OnShotBtnUp(self, args):
self.mIsBtnDown = False
# endregion
# region 事件监听
# --------------------------------------------------------------------------------------------
def OnCarriedNewItem(self, args):
self.mCarriedItem = args['itemDict']
if self._IsCarriedCustomGun():
self._SetUIVisible(True)
self._FreshUIData()
else:
self._SetUIVisible(False)
# endregion
# region 类函数
# --------------------------------------------------------------------------------------------
def _IsCarriedCustomGun(self):
if self.mCarriedItem and self.mCarriedItem['itemName'] == 'tutorial:custom_gun':
return True
return False
def _FreshUIData(self):
# 读取攻击速度
self.mShotSpeed = self.mQueryVariableComp.Get(config.CustomGunAttackSpeedVarName)
self.mAttackFrame = self._GetAttackFrame()
# 读取背包的信息,查看子弹信息
self._FreshBagBullet()
def _FreshBagBullet(self):
bulletNum = 0
allItems = self.mItemComp.GetPlayerAllItems(clientApi.GetMinecraftEnum().ItemPosType.INVENTORY)
for _itemDict in allItems:
if not _itemDict:
continue
if _itemDict and _itemDict['itemName'] == 'tutorial:custom_gun_bullet':
bulletNum += _itemDict['count']
self.mBulletNum = bulletNum
self.GetBaseUIControl(self.mBulletLabelPath).asLabel().SetText("背包剩余子弹:" + str(self.mBulletNum))
def _SetUIVisible(self, flag):
self.SetScreenVisible(flag)
def _GetAttackFrame(self):
framePerSecond = 30
return int(config.CustomGunAttackAnimDuration * (1 / float(self.mShotSpeed)) * framePerSecond)
def _HandleAttack(self):
animTotalTime = config.CustomGunAttackAnimDuration * (1 / float(self.mShotSpeed))
# 原版的的等式转换一下而已
# fireTime / float(animTotalTime) = config.CustomGunAttackAnimAttackFrame / float(config.CustomGunAttackAnimDuration)
fireTime = config.CustomGunAttackAnimAttackFrame / float(config.CustomGunAttackAnimDuration) * float(animTotalTime)
gameComp.AddTimer(0, self._SetAttackStateAndSyncToOtherClients, 'start')
gameComp.AddTimer(fireTime, self._SetAttackStateAndSyncToOtherClients, 'fire')
gameComp.AddTimer(animTotalTime, self._SetAttackStateAndSyncToOtherClients, 'end')
# 刷新背包的子弹数
gameComp.AddTimer(animTotalTime, self._FreshBagBullet)
def _SetAttackStateAndSyncToOtherClients(self, state):
# 设置本地自定义变量
self.mQueryVariableComp.Set(config.CustomGunAttackVarName, 1.0 if state in ['start', 'fire'] else 0.0)
# 通知其他客户端
self.mClientSystem.NotifyToServer('SyncCustomGunAttackStateEvent', {'state': state, 'playerId': self.mPlayerId})
# endregion
```
对代码稍微做一些解释:
- 这里面我们监听了 `OnCarriedNewItemChangedClientEvent` 而用来控制整个界面的显示与否;
- 还有一个 `_GetAttackFrame` 函数用来转换射速和游戏帧数,因为界面文件中的 Tick 函数中,一秒是 30 帧,所以这里要配合射速进行转换;
- 我们攻击状态除了设置本地之外,还需要使用事件来同步到其他客户端;
- 连发的实现思路是1按下之后记录按下的帧数2当按下之后立马处理攻击然后等待到下一次检测帧也就是按下帧数+攻击动画所需的帧数3如果此时还处于按下的状态再次处理攻击并重置按下的帧数4以此循环
### 处理攻击
服务端代码:
```python
# -*- coding: utf-8 -*-
import math
import mod.server.extraServerApi as serverApi
from mod.common.utils.mcmath import Vector3
CompFactory = serverApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(serverApi.GetLevelId())
class TutorialServerSystem(serverApi.GetServerSystemCls()):
def __init__(self, namespace, name):
super(TutorialServerSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncCustomGunAttackStateEvent", self,
self.OnSyncCustomGunAttackStateEvent)
def OnSyncCustomGunAttackStateEvent(self, args):
playerId = args['playerId']
state = args['state']
if state == 'fire':
self._ShotBullet(playerId)
else:
relevantPlayers = CompFactory.CreatePlayer(playerId).GetRelevantPlayer([playerId])
self.NotifyToMultiClients(relevantPlayers, 'SyncCustomGunStateEvent', {
'playerId': playerId,
'value' : 1.0 if state == 'start' else 0.0
})
# 发射子弹
def _ShotBullet(self, playerId):
rot = CompFactory.CreateRot(playerId).GetRot()
footPos = CompFactory.CreatePos(playerId).GetFootPos()
shotPos = (footPos[0], footPos[1] + 1.25, footPos[2])
shotPos = self._MoveForward(shotPos, rot, 1.3)
param = {
'power' : 1.2,
'gravity' : 0,
'position' : shotPos,
'direction': serverApi.GetDirFromRot(rot)
}
projectileComp = CompFactory.CreateProjectile(playerId)
projectileComp.CreateProjectileEntity(playerId, 'tutorial:custom_gun_bullet_projectile', param)
self._ReduceItem(playerId, 'tutorial:custom_gun_bullet', 1)
# 向前移动坐标
def _MoveForward(self, pos, rot, distance):
_rot = (0, rot[1]) # 第一个参数是上下角度,第二个是左右角度
rx, ry, rz = serverApi.GetDirFromRot(_rot)
return (pos[0] + rx * distance, pos[1] + ry * distance, pos[2] + rz * distance)
def _ReduceItem(self, playerId, itemName, count):
itemComp = CompFactory.CreateItem(playerId)
allItems = itemComp.GetPlayerAllItems(serverApi.GetMinecraftEnum().ItemPosType.INVENTORY)
remainingCount = count # 剩余要减少的数量
for slotPos, itemDict in enumerate(allItems):
if itemDict and itemDict['itemName'] == itemName:
item_count = itemDict['count']
if item_count >= remainingCount:
itemComp.SetInvItemNum(slotPos, item_count - remainingCount)
return True # 减少成功
else:
itemComp.SetInvItemNum(slotPos, 0)
remainingCount -= item_count
return False # 减少失败,物品数量不足
```
由于我们在本地的界面中已经判断了物品数量是否足够,所以服务端不做特殊的处理。
唯一需要处理的就是子弹发出的初始位置,是玩家的 `footPos``y` 方向上 `+1.25` 之后,再向朝向的方向向前移动 `1.3` 的距离。
客户端唯一需要做的就是监听事件,并设置自定义变量:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import config
CompFactory = clientApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncCustomGunStateEvent', self, self.OnSyncCustomGunStateEvent)
def OnSyncCustomGunStateEvent(self, args):
playerId = args['playerId']
value = float(args['value'])
CompFactory.CreateQueryVariable(playerId).Set(config.CustomGunAttackVarName, value)
```
### 进入游戏测试
至此,自定义枪械就基本完成了,就可以进入游戏进行测试了。主要是测试一下联机时候的表现如何:
![第三视角](./assets/3_7.gif)
可以看到完全没问题。
## 小结
这篇文章带大家制作了一个简单的自定义枪械,主要的精力都花在了如何「开火」和「手臂显示」这两个部分。
但其实枪械还有很多地方可以优化的地方,比如:奔跑动画、换弹动画等。感兴趣的小伙伴可以自行尝试。思路完全是一样的。
另外比较重要的是学习了「**射速**」的实现,这一点特性大家可以发挥想象,其实可以运用在很多地方。
## 课后作业
本次课后作业,内容如下:
- 制作一个第一人称、第三人称动画完整,有自定义界面的自定义枪械。
- 并且支持配置「射速」这一个特性;

View File

@@ -0,0 +1,527 @@
# 消耗类武器
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将帮助你添加一个可以投掷出去的燃烧瓶 3D 武器。(强烈建议阅读之前先阅读第一节课的内容,因为思路一样)
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程如果对此感兴趣的同学可以自行学习和了解。
在本教程中,您将学习以下内容。
- ✅制作一个燃烧瓶,可投掷并造成伤害。
## 成果展示
还是一个动画完整,附带一个简单交互界面的燃烧瓶,可以扔出去造成伤害:
![燃烧瓶演示](./assets/4_1.gif)
## 燃烧瓶制作
所有的 3D 武器都是有两种制作思路 ,由于燃烧瓶几乎是只提供给玩家使用的道具,所以我们这里直接采取第一种额外骨骼的方式。
### 模型制作
先来一个骨骼对应好的模型:
![](./assets/image-20231127024732769.png)
除此之外,我们还需要一个用于投掷出去的抛射物模型:
![](./assets/image-20231127024818392.png)
制作好之后导出到响应文件下就可以。
### 动画制作
我们先来准备一个用于制作动画的模型,复制一份原版的玩家模型,然后导入我们的自定义燃烧瓶模型,删除贴图:
![](./assets/image-20231127025052105.png)
#### 第三人称动画
由于骨骼是与玩家严格对齐,所以第三人称手持动画就不需要制作了。直接制作一个抬手仍出去的攻击动画:
![燃烧瓶第三人称攻击动画](./assets/4_3.gif)
由于第三人称的情况下手臂默认会有一个抬起的动画,所以第三人称的动画需要勾选上动画的「覆盖」模式。
#### 第一人称动画
还是按照之前的方法,先模拟出游戏中的第一人称视角。加入我们的模拟动画:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.first_person_guide.right_arm.method_one": {
"loop": true,
"bones": {
"rightArm": {
"rotation": [95, -45, 115],
"position": [13.5, -10, 12]
},
"rightItem": {
"position": [0, 0, -1]
}
}
}
}
}
```
再来一个模拟第一人称相机的视角:
![](./assets/image-20231126113433017.png)
再把除了右手之外的其他骨骼给隐藏掉,然后修正第一人称视角下的位置:
![](./assets/image-20231127025645424.png)
然后我们可以直接考虑复制第三人称攻击动画的帧,加入到第一人称攻击动画中,这样做主要是为了对齐关键动作的时间,然后再做一些修改,就可以得到我们第一人称的攻击动画了
![燃烧瓶第一人称攻击动画](./assets/4_4.gif)
#### 抛射物的飞行动画
为了稍微「精致」一点儿,我们也要为投掷出去的抛射物制作一个旋转动画:
![燃烧瓶的旋转动画](./assets/4_5.gif)
### 动画控制器
动画控制器很简单,一个第一人称使用,一个第三人称使用,除了播放的动画不一样之外,没有区别:
```json
{
"format_version": "1.10.0",
"animation_controllers": {
"controller.animation.custom_fire_bottle_first_person": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"first_person_attack1": "query.mod.custom_fire_bottle_attack == 1.0"
}
]
},
"first_person_attack1": {
"animations": [
"custom_fire_bottle_attack_first_person"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_fire_bottle_attack == 0.0"
}
]
}
}
},
"controller.animation.custom_fire_bottle_third_person": {
"initial_state": "default",
"states": {
"default": {
"transitions": [
{
"third_person_attack1": "query.mod.custom_fire_bottle_attack == 1.0"
}
]
},
"third_person_attack1": {
"animations": [
"custom_fire_bottle_attack_third_person"
],
"transitions": [
{
"default": "query.any_animation_finished && query.mod.custom_fire_bottle_attack == 0.0"
}
]
}
}
}
}
}
```
### 渲染器
为了在第一人称下显示手臂,我们也需要对应修改原版的 `player.render_controllers.json` 文件:
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.player.first_person": {
"geometry": "Geometry.default",
"materials": [ { "*": "Material.default" } ],
"textures": [ "Texture.default" ],
"part_visibility": [
{ "*": false },
// 修改原版渲染器,让它支持在手持自定义枪械和燃烧瓶时,显示右手臂
{ "rightArm": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun' || query.get_equipped_item_name(0, 1) == 'custom_fire_bottle'" },
{ "rightSleeve": "query.get_equipped_item_name(0, 1) == '' || query.get_equipped_item_name(0, 1) == 'map' || query.get_equipped_item_name(0, 1) == 'custom_gun' || query.get_equipped_item_name(0, 1) == 'custom_fire_bottle'" },
{ "leftArm": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" },
{ "leftSleeve": "(query.get_equipped_item_name(0, 1) == 'map' && query.get_equipped_item_name('off_hand') != 'shield') || (query.get_equipped_item_name('off_hand') == 'map' && !query.item_is_charged) || (!query.item_is_charged && (variable.item_use_normalized > 0 && variable.item_use_normalized < 1.0))" }
]
},
```
然后还有燃烧瓶的渲染控制器(`tutorial_custom_fire_bottle.render_controllers.json`
```json
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.tutorial_custom_fire_bottle": {
"geometry": "Geometry.custom_fire_bottle",
"materials": [{"*": "Material.default"}],
"textures": ["Texture.custom_fire_bottle"]
}
}
}
```
### 抛射物的定义
我们还需要新建一个实体用来当做燃烧瓶的抛射物:
```json
{
"format_version": "1.13.0",
"minecraft:entity": {
"description": {
"is_experimental": false,
"identifier": "tutorial:custom_fire_bottle_projectile",
"is_spawnable": false,
"is_summonable": false
},
"component_groups": {
},
"components": {
"minecraft:despawn": {
"despawn_from_distance": {}
},
"minecraft:physics": {},
"minecraft:projectile": {
"on_hit": {
"remove_on_hit": {},
"impact_damage": {
"catch_fire": true,
"knockback": false,
"damage": 1,
"destroy_on_hit": true
}
},
"gravity": 0.0,
"power": 1.0,
"offset": [
0,
0,
0
]
},
"minecraft:collision_box": {
"width": 0.31,
"height": 0.31
},
"netease:custom_entity_type": {
"value": "projectile_entity"
},
"minecraft:pushable": {
"is_pushable_by_piston": true,
"is_pushable": true
}
},
"events": {
}
}
}
```
资源包下的实体定义:
```json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial:custom_fire_bottle_projectile",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/models/tutorial_custom_fire_bottle"
},
"geometry": {
"default": "geometry.tutorial_custom_fire_bottle_projectile"
},
"animations": {
"move": "animation.tutorial_custom_fire_bottle_projectile.move"
},
"scripts": {
"animate": [
"move"
]
},
"render_controllers": [
"controller.render.default"
]
}
}
}
```
### 注入相关资源
把我们上面制作好的资源通过代码注入到玩家的渲染器下,并且监听了服务端传回来的状态同步事件:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import time
import config
CompFactory = clientApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialServerSystem', 'SyncCustomFireBottleStateEvent', self,
self.OnSyncCustomFireBottleStateEvent)
def OnSyncCustomFireBottleStateEvent(self, args):
playerId = args['playerId']
value = float(args['value'])
CompFactory.CreateQueryVariable(playerId).Set(config.FireBottleAttackVarName, value)
def OnAddPlayerCreatedClientEvent(self, args):
playerId = args['playerId']
self.InitRender(playerId) # 包括其他玩家也需要被初始化
# 初始化绑定
def InitRender(self, playerId):
# 燃烧瓶
self._InitToFireBottle(playerId)
actorRenderComp = CompFactory.CreateActorRender(playerId)
actorRenderComp.RebuildPlayerRender()
# 燃烧瓶渲染器
def _InitToFireBottle(self, playerId):
queryVariableComp = CompFactory.CreateQueryVariable(playerId)
queryVariableComp.Register(config.FireBottleAttackVarName, 0)
queryVariableComp.Set(config.FireBottleAttackVarName, 0)
actorRenderComp = CompFactory.CreateActorRender(playerId)
# 控制器
actorRenderComp.AddPlayerGeometry('custom_fire_bottle', 'geometry.tutorial_custom_fire_bottle')
actorRenderComp.AddPlayerTexture('custom_fire_bottle', 'textures/models/tutorial_custom_fire_bottle')
actorRenderComp.AddPlayerRenderController("controller.render.tutorial_custom_fire_bottle",
"query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'")
# 定义动画和控制器名称
animations = ['hold_first_person', 'attack_first_person', 'attack_third_person']
controllers = ['custom_fire_bottle_first_person', 'custom_fire_bottle_third_person']
for anim in animations:
animationKey = 'custom_fire_bottle_' + anim
animationName = 'animation.tutorial_custom_fire_bottle.' + anim
actorRenderComp.AddPlayerAnimation(animationKey, animationName)
for controller in controllers:
controllerKey = controller + "_controller"
controllerName = 'controller.animation.' + controller
actorRenderComp.AddPlayerAnimationController(controllerKey, controllerName)
# 添加动画的触发条件
actorRenderComp.AddPlayerScriptAnimate(
'custom_fire_bottle_hold_first_person',
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'"
)
actorRenderComp.AddPlayerScriptAnimate(
'custom_fire_bottle_first_person_controller',
"variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'"
)
actorRenderComp.AddPlayerScriptAnimate(
'custom_fire_bottle_third_person_controller',
"!variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_fire_bottle'"
)
```
### 编写 UI 文件
我们只需要一个拥有绝对定位的按钮的简单 UI
![](./assets/image-20231127030703484.png)
界面也很简单:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
import config
ViewBinder = clientApi.GetViewBinderCls()
ViewRequest = clientApi.GetViewViewRequestCls()
ScreenNode = clientApi.GetScreenNodeCls()
Namespace = clientApi.GetEngineNamespace()
SystemName = clientApi.GetEngineSystemName()
CompFactory = clientApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
#ui布局绑定
class FireBottleUIScripts(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.mPlayerId = clientApi.GetLocalPlayerId()
self.mClientSystem = clientApi.GetSystem('tutorialMod', 'tutorialClientSystem')
self.mItemComp = CompFactory.CreateItem(self.mPlayerId)
self.mQueryVariableComp = CompFactory.CreateQueryVariable(self.mPlayerId)
# 组件地址
self.mBtnPath = "/button"
# 界面所需的变量
self.mCarriedItem = None
def Create(self):
print("===== Tutorial Custom Gun UI Create Finished =====")
# 注册按钮的事件
control = self.GetBaseUIControl(self.mBtnPath).asButton()
control.AddTouchEventParams({"isSwallow": True})
control.SetButtonTouchUpCallback(self.OnButtonUp)
# 关注事件
namespace, systemName = clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName()
self.mClientSystem.ListenForEvent(namespace, systemName, "OnCarriedNewItemChangedClientEvent", self, self.OnCarriedNewItem)
# 刚创建时也自动触发一次
self.OnCarriedNewItem({'itemDict': self.mItemComp.GetCarriedItem()})
# region 按钮事件
# --------------------------------------------------------------------------------------------
def OnButtonUp(self, args):
self._HandleThrow()
# 按了一次之后就直接隐藏界面,避免误操作
self._SetUIVisible(False)
# endregion
# region 事件监听
# --------------------------------------------------------------------------------------------
def OnCarriedNewItem(self, args):
self.mCarriedItem = args['itemDict']
if self._IsCarriedCustomGun():
self._SetUIVisible(True)
else:
self._SetUIVisible(False)
# endregion
# region 类函数
# --------------------------------------------------------------------------------------------
def _IsCarriedCustomGun(self):
if self.mCarriedItem and self.mCarriedItem['itemName'] == 'tutorial:custom_fire_bottle':
return True
return False
def _SetUIVisible(self, flag):
self.SetScreenVisible(flag)
def _HandleThrow(self):
throwTime = 0.25
animTotalTime = 0.5
gameComp.AddTimer(0, self._SetAttackStateAndSyncToOtherClients, 'start')
gameComp.AddTimer(throwTime, self._SetAttackStateAndSyncToOtherClients, 'throw')
gameComp.AddTimer(animTotalTime, self._SetAttackStateAndSyncToOtherClients, 'end')
def _SetAttackStateAndSyncToOtherClients(self, state):
# 设置本地自定义变量
self.mQueryVariableComp.Set(config.FireBottleAttackVarName, 1.0 if state in ['start', 'throw'] else 0.0)
# 通知其他客户端
self.mClientSystem.NotifyToServer('SyncCustomFireBottleStateEvent', {'state': state, 'playerId': self.mPlayerId})
# endregion
```
- 我们监听了 `OnCarriedNewItemChangedClientEvent` 事件,会在切换到该物品时显示按钮,也会在切换到其他物品时隐藏按钮;
- 另外我们在响应点击之后,立马就隐藏了界面,防止多次点击;
### 处理投掷事件
我们已经在界面文件中,在响应点击之后发送了事件给服务端,所以服务端只需要监听事件做出响应就可以:
```python
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
CompFactory = serverApi.GetEngineCompFactory()
gameComp = CompFactory.CreateGame(serverApi.GetLevelId())
class TutorialServerSystem(serverApi.GetServerSystemCls()):
def __init__(self, namespace, name):
super(TutorialServerSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncCustomFireBottleStateEvent", self,
self.OnSyncCustomFireBottleStateEvent)
def OnSyncCustomFireBottleStateEvent(self, args):
playerId = args['playerId']
state = args['state']
if state == 'throw':
self._ThrowFireBottle(playerId)
else:
relevantPlayers = CompFactory.CreatePlayer(playerId).GetRelevantPlayer([playerId])
self.NotifyToMultiClients(relevantPlayers, 'SyncCustomFireBottleStateEvent', {
'playerId': playerId,
'value' : 1.0 if state == 'start' else 0.0
})
# 向前投掷燃烧瓶
def _ThrowFireBottle(self, playerId):
rot = CompFactory.CreateRot(playerId).GetRot()
# 默认是向上抬一点
rot = (rot[0] - 30, rot[1])
param = {
'power' : 1.2,
'gravity' : 0.125,
'direction': serverApi.GetDirFromRot(rot)
}
projectileComp = CompFactory.CreateProjectile(playerId)
projectileComp.CreateProjectileEntity(playerId, 'tutorial:custom_fire_bottle_projectile', param)
self._ReduceCarriedItemNum(playerId, 1)
def _ReduceCarriedItemNum(self, playerId, reduceNum):
itemComp = CompFactory.CreateItem(playerId)
selectSlotId = itemComp.GetSelectSlotId()
itemDict = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.INVENTORY, selectSlotId)
return itemComp.SetInvItemNum(selectSlotId, itemDict['count'] - reduceNum)
```
这里默认的投掷方向是玩家当前朝向向上偏移 30° 作为初始的燃烧瓶的速度方向。
### 进入游戏测试
完成上面的步骤,我们就可以进入游戏中愉快的测试了。
## 课后作业
本次课后作业,内容如下:
- 制作一个可投掷出去的 3D 道具,需要有一个简单可交互的界面、完整的第一、第三人称动画。

View File

@@ -0,0 +1,458 @@
# 投掷类武器
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
本文将帮助你添加一个类似于原版三叉戟的可投掷武器。(强烈建议阅读之前先阅读前面一节课的内容,因为思路一样)
本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程如果对此感兴趣的同学可以自行学习和了解。
在本教程中,您将学习以下内容。
- ✅类似于原版三叉戟的投掷类武器。
## 成果展示
类似于原版的三叉戟的自定义投掷类武器:
![投掷武器演示](./assets/5_1.gif)
## 投掷类武器制作
还是一样的思路,先去官方的内容库中「偷」一个模型文件:
![image-20231126203148298](./assets/image-20231126203148298.png)
![](./assets/image-20231126203237062.png)
### 原版三叉戟
我们没有制作投掷类武器的经验,所以我们直接去原版查看三叉戟的 `attachable` 文件(位于 `definitionsattachables\trident.entity.json`
```json
{
"format_version": "1.10",
"minecraft:attachable": {
"description": {
"identifier": "minecraft:trident",
"materials": {
"default": "entity_alphatest",
"enchanted": "entity_alphatest_glint"
},
"textures": {
"default": "textures/entity/trident",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.trident"
},
"animations": {
"wield": "controller.animation.trident.wield",
"wield_first_person": "animation.trident.wield_first_person",
"wield_first_person_raise": "animation.trident.wield_first_person_raise",
"wield_first_person_raise_shake": "animation.trident.wield_first_person_raise_shake",
"wield_first_person_riptide": "animation.trident.wield_first_person_riptide",
"wield_third_person": "animation.trident.wield_third_person",
"wield_third_person_raise": "animation.trident.wield_third_person_raise"
},
"scripts": {
"pre_animation": [
"variable.charge_amount = math.clamp((query.main_hand_item_max_duration - (query.main_hand_item_use_duration - query.frame_alpha + 1.0)) / 10.0, 0.0, 1.0f);"
],
"animate": [
"wield"
]
},
"render_controllers": [ "controller.render.item_default" ]
}
}
}
```
### 模型文件
那思路就很简单了,直接仿制一个三叉戟的模型文件,其余的东西都是可以通用的。所以我们把模型稍微改一下:
![](./assets/image-20231126203544339.png)
这里原版三叉戟的 `pivot` 影响了动画的对齐和命中效果,我们要保持**绝对一致**(也就是说这个 24 不能改):
![](./assets/image-20231126203731758.png)
我们稍微观察一下三叉戟就大概明白了这个 `pivot` 是在什么位置:
![](./assets/image-20231126205321013.png)
如果打开「调试」中的「能见度边界框」的话,也能够发现这个锚点实际上就是命中的位置:
![](./assets/image-20231126205445572.png)
所以我们稍微更改一下我们的模型:
![](./assets/image-20231126203946097.png)
上面是用于手持的物品模型,对于处理用于**投掷出去的抛射物模型**,我们这里有两种方式处理:
- 不添加额外的模型,通过动画来修正投掷出去的动画。好处是不需要增加额外的动画,但。
- 添加额外的抛射物模型。好处是动画简单,而且能够复用,坏处就是要多处理一遍模型。
如果我们想要采用第一种方式的话,就需要把模型往下移,把锚点移动到矛的尖上:
![](./assets/image-20231126205828881.png)
但此时游戏中的握持方式就会很奇怪,因为我们自己的模型长度跟原版的三叉戟不一致:
![](./assets/image-20231126205928159.png)
所以我们这里不采用第一种方式,而是额外增加一个单独的用于投掷物实体的模型。
不过这里还是先放一下采用第一种方式时的动画,提供给需要的同学:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.tutorial_thrown_custom_throw_weapon.move": {
"loop": true,
"bones": {
"pole": {
// 下列是采用第一种方式时采用的动画
"rotation": [
"-query.target_x_rotation + 90", // 这里需要旋转 90 正对 N 方向
"-query.target_y_rotation",
0.0
],
"position": [
// 这里的 -24 对应了 pivot 偏移的 24
0, -24, 0
]
}
}
}
}
}
```
OK那我们采用第二种方式只需要新增一个锚点在原点并且朝向 N 方向的模型就可以:
![](./assets/image-20231126210304161.png)
此时的动画文件,可以通用,就是让实体朝向运动方向:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.tutorial_thrown_custom_throw_weapon.move": {
"loop": true,
"bones": {
"pole": {
// 下列是教程中采用的第二种方法的动画
"rotation": [
"-query.target_x_rotation",
"-query.target_y_rotation",
0.0
]
}
}
}
}
}
```
### 基础物品定义
行为包下的物品定义文件:
```json
{
"format_version": "1.10",
"minecraft:item": {
"description": {
"category": "Equipment",
"identifier": "tutorial:custom_throw_weapon",
"custom_item_type": "ranged_weapon"
},
"components": {
"netease:show_in_hand": {
"value": false
},
"minecraft:max_damage": 10,
// 保证使用足够长,否则动画和视角会重新开始
"minecraft:use_duration": 99999
}
}
}
```
当我们把 `custom_item_type` 定义为 `ranged_weapon` 时,并且组件中拥有 `minecraft:use_duration` 时,我们在手持物品的情况下右键(手机是长按),就自动会有镜头缩放的效果:
![缩放效果](./assets/5_2.gif)
### 抛射物实体文件
我们还需要额外创建一个抛射物实体,直接复制粘贴原版的三叉戟就好,只不过需要额外添加两个组件:
```json
{
"format_version": "1.12.0",
"minecraft:entity": {
"description": {
"identifier": "tutorial:thrown_custom_throw_weapon",
"is_spawnable": false,
"is_summonable": false,
"is_experimental": false
},
"components": {
"minecraft:collision_box": {
"width": 0.25,
"height": 0.35
},
"minecraft:projectile": {
"on_hit": {
"impact_damage": {
"damage": 8,
"knockback": true,
"semi_random_diff_damage": false,
"destroy_on_hit": false
},
"stick_in_ground": {
"shake_time": 0
}
},
"liquid_inertia": 0.99,
"hit_sound": "item.trident.hit",
"hit_ground_sound": "item.trident.hit_ground",
"power": 4,
"gravity": 0.10,
"uncertainty_base": 1,
"uncertainty_multiplier": 0,
"stop_on_hurt": true,
"anchor": 1,
"should_bounce": true,
"multiple_targets": false,
"offset": [0, -0.1, 0]
},
"minecraft:physics": {
},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": true
},
"netease:custom_entity_type": {
"value": "projectile_entity"
},
"netease:pick_up": {
"item_name": "tutorial:custom_throw_weapon"
}
}
}
}
```
我们需要额外添加 `netease:custom_entity_type` 来标识这个实体是抛射物,以及 `netease:pick_up` 组件,用来在玩家接触时拾取变成物品。
行为包下 `\entity` 文件:
```json
{
"format_version": "1.10.0",
"minecraft:client_entity": {
"description": {
"identifier": "tutorial:thrown_custom_throw_weapon",
"materials": {
"default": "entity_alphatest"
},
"textures": {
"default": "textures/models/tutorial_custom_throw_weapon"
},
"geometry": {
"default": "geometry.tutorial_thrown_custom_throw_weapon"
},
"animations": {
"move": "animation.tutorial_thrown_custom_throw_weapon.move"
},
"scripts": {
"animate": [
"move"
]
},
"render_controllers": [
"controller.render.default"
]
}
}
}
```
### 代码注入第三人称动画
当我们把这些文件都准备好之后,你会发现第三人称并不会把手抬起来:
![第三人称并不会举手](./assets/5_3.gif)
这就是我们在「自定义枪械」那一节课中说的attachable 中的动画,只会影响武器,而不会反作用于玩家。
所以我们还需要在玩家手持投掷武器时,播放原版的投掷动画。
问题是我们并不知道原版的投掷动画是哪一个,我们要么去原版的文件中找(还是挺好找的),要么,就按 F3 直到出现下列的界面:
![](./assets/image-20231126211648095.png)
然后打开动画编辑器:
![](./assets/image-20231126211714774.png)
然后我们就可以在手持三叉戟的情况下,通过不断右键触发动画,来找到到底播放的是哪一个动画:
![如何找到播放的哪一个动画](./assets/5_4.gif)
很快,我们就找到了播放的 `brandish_spear` 动画,一番搜索,就定位到了动画名称:
```json
animation.humanoid.brandish_spear
```
我们使用代码注入到玩家的控制器中:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
CompFactory = clientApi.GetEngineCompFactory()
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
def ListenEvent(self):
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "AddPlayerCreatedClientEvent",
self, self.OnAddPlayerCreatedClientEvent)
def OnAddPlayerCreatedClientEvent(self, args):
playerId = args['playerId']
self.InitRender(playerId) # 包括其他玩家也需要被初始化
# 初始化绑定
def InitRender(self, playerId):
# 投掷武器
self._InitToThrowWeapon(playerId)
actorRenderComp = CompFactory.CreateActorRender(playerId)
actorRenderComp.RebuildPlayerRender()
# 投掷武器的渲染
def _InitToThrowWeapon(self, playerId):
actorRenderComp = CompFactory.CreateActorRender(playerId)
# 使用原版的动画
actorRenderComp.AddPlayerAnimation('custom_throw_weapon_third_person_raise', 'animation.humanoid.brandish_spear')
actorRenderComp.AddPlayerScriptAnimate(
'custom_throw_weapon_third_person_raise',
"!variable.is_first_person && query.get_equipped_item_name('main_hand') == 'custom_throw_weapon' && query.main_hand_item_use_duration > 0"
)
```
这样,我们在第三人称的情况下,就可以播放原版的抬手动作了:
![抬手动作演示](./assets/5_5.gif)
### 处理投掷事件
客户端代码:
```python
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
CompFactory = clientApi.GetEngineCompFactory()
class TutorialClientSystem(clientApi.GetClientSystemCls()):
def __init__(self, namespace, name):
super(TutorialClientSystem, self).__init__(namespace, name)
self.ListenEvent()
self.mTimeCounter = 0
#
self.mIsUsingItem = False # 是否正在使用投掷类武器
self.mStartUsingFrame = 0 # 开始使用物品的时间
def ListenEvent(self):
# 投掷武器相关的事件
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ClientItemTryUseEvent", self,
self.OnClientItemTryUseEvent)
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "ItemReleaseUsingClientEvent", self,
self.OnItemReleaseUsingClientEvent)
def Update(self):
self.mTimeCounter += 1
def OnClientItemTryUseEvent(self, args):
if args['itemName'] == 'tutorial:custom_throw_weapon':
self.mIsUsingItem = True
self.mStartUsingFrame = self.mTimeCounter
def OnItemReleaseUsingClientEvent(self, args):
itemDict = args['itemDict']
if itemDict and itemDict['itemName'] == 'tutorial:custom_throw_weapon':
# 最大蓄力 2s也就是说威力最多为 2 倍
power = min(2.0, (self.mTimeCounter - self.mStartUsingFrame) / 30.0)
self.NotifyToServer('ThrowCustomWeapon', {'playerId': args['playerId'], 'power': power}
```
服务端代码:
```python
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
CompFactory = serverApi.GetEngineCompFactory()
class TutorialServerSystem(serverApi.GetServerSystemCls()):
def __init__(self, namespace, name):
super(TutorialServerSystem, self).__init__(namespace, name)
def ListenEvent(self):
# 自定义事件
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "SyncCustomGunAttackStateEvent", self,
self.OnSyncCustomGunAttackStateEvent)
self.ListenForEvent('tutorialMod', 'tutorialClientSystem', "ThrowCustomWeapon", self, self.OnThrowCustomWeapon)
def OnThrowCustomWeapon(self, args):
playerId = args['playerId']
power = args['power']
param = {
'power' : 4 * power,
'damage' : 3 + power * 3, # 最高伤害为 3+2*3=9 点伤害
'direction': serverApi.GetDirFromRot(CompFactory.CreateRot(playerId).GetRot())
}
projectileComp = CompFactory.CreateProjectile(playerId)
projectileEntityId = projectileComp.CreateProjectileEntity(playerId, 'tutorial:thrown_custom_throw_weapon', param)
if projectileEntityId != '-1':
self._ReduceCarriedItemNum(playerId, 1)
def _ReduceCarriedItemNum(self, playerId, reduceNum):
itemComp = CompFactory.CreateItem(playerId)
selectSlotId = itemComp.GetSelectSlotId()
itemDict = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.INVENTORY, selectSlotId)
return itemComp.SetInvItemNum(selectSlotId, itemDict['count'] - reduceNum)
```
### 进入游戏测试
一切准备好之后,就可以进入游戏测试了。然后你就得到了一个自定义的投掷类 3D 武器。
## 小结
我们这一节课是高度利用了原版的三叉戟物品的动画,如果你想要完全自定义的投掷动画,也可以参照自定义枪械那样,完全定制化动画。
## 课后作业
本次课后作业,内容如下:
- 实现自己的类似于原版三叉戟的投掷类 3D 武器;