Files
netease-bedrock-wiki/mconline/60-我的世界创造营教程/SDK大师教程/2-武器和装备制作/4-消耗类武器.md
2025-08-25 18:36:29 +08:00

528 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 消耗类武器
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 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 道具,需要有一个简单可交互的界面、完整的第一、第三人称动画。