# 投掷类武器

温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。

本文将帮助你添加一个类似于原版三叉戟的可投掷武器。(强烈建议阅读之前先阅读前面一节课的内容,因为思路一样)

本文假定你熟悉 Molang、渲染控制器、动画和实体定义有基本的了解。本文不涉及美术资源的相关教程,如果对此感兴趣的同学可以自行学习和了解。

在本教程中,您将学习以下内容。

  • ✅类似于原版三叉戟的投掷类武器。

# 成果展示

类似于原版的三叉戟的自定义投掷类武器:

投掷武器演示

# 投掷类武器制作

还是一样的思路,先去官方的内容库中「偷」一个模型文件:

image-20231126203148298

# 原版三叉戟

我们没有制作投掷类武器的经验,所以我们直接去原版查看三叉戟的 attachable 文件(位于 definitionsattachables\trident.entity.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" ]
    }
  }
}

# 模型文件

那思路就很简单了,直接仿制一个三叉戟的模型文件,其余的东西都是可以通用的。所以我们把模型稍微改一下:

这里原版三叉戟的 pivot 影响了动画的对齐和命中效果,我们要保持绝对一致(也就是说这个 24 不能改):

我们稍微观察一下三叉戟就大概明白了这个 pivot 是在什么位置:

如果打开「调试」中的「能见度边界框」的话,也能够发现这个锚点实际上就是命中的位置:

所以我们稍微更改一下我们的模型:

上面是用于手持的物品模型,对于处理用于投掷出去的抛射物模型,我们这里有两种方式处理:

  • 不添加额外的模型,通过动画来修正投掷出去的动画。好处是不需要增加额外的动画,但。
  • 添加额外的抛射物模型。好处是动画简单,而且能够复用,坏处就是要多处理一遍模型。

如果我们想要采用第一种方式的话,就需要把模型往下移,把锚点移动到矛的尖上:

但此时游戏中的握持方式就会很奇怪,因为我们自己的模型长度跟原版的三叉戟不一致:

所以我们这里不采用第一种方式,而是额外增加一个单独的用于投掷物实体的模型。

不过这里还是先放一下采用第一种方式时的动画,提供给需要的同学:

{
    "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 方向的模型就可以:

此时的动画文件,可以通用,就是让实体朝向运动方向:

{
    "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
                    ]
                }
            }
        }
    }
}

# 基础物品定义

行为包下的物品定义文件:

{
    "format_version": "1.10",
    "minecraft:item": {
        "description": {
            "category": "Equipment",
            "identifier": "tutorial:custom_throw_weapon",
            "custom_item_type": "ranged_weapon",
            "register_to_create_menu": true
        },
        "components": {
            "netease:show_in_hand": {
                "value": false
            },
            "minecraft:max_damage": 10,
            // 保证使用足够长,否则动画和视角会重新开始
            "minecraft:use_duration": 99999
        }
    }
}

当我们把 custom_item_type 定义为 ranged_weapon 时,并且组件中拥有 minecraft:use_duration 时,我们在手持物品的情况下右键(手机是长按),就自动会有镜头缩放的效果:

缩放效果

# 抛射物实体文件

我们还需要额外创建一个抛射物实体,直接复制粘贴原版的三叉戟就好,只不过需要额外添加两个组件:

{
    "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 文件:

{
    "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"
            ]
        }
    }
}

# 代码注入第三人称动画

当我们把这些文件都准备好之后,你会发现第三人称并不会把手抬起来:

第三人称并不会举手

这就是我们在「自定义枪械」那一节课中说的,attachable 中的动画,只会影响武器,而不会反作用于玩家。

所以我们还需要在玩家手持投掷武器时,播放原版的投掷动画。

问题是我们并不知道原版的投掷动画是哪一个,我们要么去原版的文件中找(还是挺好找的),要么,就按 F3 直到出现下列的界面:

然后打开动画编辑器:

然后我们就可以在手持三叉戟的情况下,通过不断右键触发动画,来找到到底播放的是哪一个动画:

如何找到播放的哪一个动画

很快,我们就找到了播放的 brandish_spear 动画,一番搜索,就定位到了动画名称:

animation.humanoid.brandish_spear

我们使用代码注入到玩家的控制器中:

# -*- 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"
        )

这样,我们在第三人称的情况下,就可以播放原版的抬手动作了:

抬手动作演示

# 处理投掷事件

客户端代码:

# -*- 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}

服务端代码:

# -*- 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 武器;

成果展示

投掷类武器制作

原版三叉戟

模型文件

基础物品定义

抛射物实体文件

代码注入第三人称动画

处理投掷事件

进入游戏测试

小结

课后作业