2.6
This commit is contained in:
3
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/0-教程示例下载.md
Normal file
3
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/0-教程示例下载.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 教程示例下载
|
||||
|
||||
本章所教学的惊变系统Demo可点击 [这里](https://g79.gdl.netease.com/ForgeLabsSystem.zip) 下载到本地。
|
||||
305
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/1-时间规则.md
Normal file
305
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/1-时间规则.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 时间规则
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你了解 MC 中的时间的问题,并从零开始带你搭建起一个基础的 UI 来帮助我们了解当前 MC 中的时间情况。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅MC 中的时间规则;
|
||||
- ✅简单 UI 搭建指南;
|
||||
|
||||
## MC 中的时间规则
|
||||
|
||||
> 这一部分可以查看 MC 的[官方介绍](https://zh.minecraft.wiki/w/%E6%98%BC%E5%A4%9C%E6%9B%B4%E6%9B%BF)。
|
||||
|
||||
## 时间换算
|
||||
|
||||
在我的世界中的时间正好是现实时间中**流逝速度**的 72 倍。这是因为现实世界中的 1 天有 24 * 60 = 1440 分钟,而在我的世界中,1 个完整的 Minecraft 天只有 20 分钟。1440 / 20 = 72,正好是 72 倍。
|
||||
|
||||
如果要进行时间单位的换算的话,那么可以得到下面两个表。
|
||||
|
||||
一个表是 Minecraft → 现实时间的换算表:
|
||||
|
||||
| Minecraft时间 | Minecraft 刻 | 现实时间 |
|
||||
| :--------------: | :----------: | :--------------------: |
|
||||
| 1秒 | 0.27 | 0.0138秒 |
|
||||
| 1分钟 | 16.6 | 0.83秒 |
|
||||
| 1小时 | 1,000 | 50秒 |
|
||||
| 1天 | 24,000 | 20分钟 |
|
||||
| 1周(7天) | 168,000 | 2.3小时 |
|
||||
| 1个月(30天) | 720,000 | 10小时 |
|
||||
| 1年(365.25 天) | 8,766,000 | 121.75小时(5.072916天 |
|
||||
|
||||
另一个表是现实时间 → Minecraft 时间的换算表:
|
||||
|
||||
| 现实时间 | Minecraft时间 |
|
||||
| :---------------: | :-------------------------------------: |
|
||||
| 1⁄20秒(1游戏刻) | 3.6秒 |
|
||||
| 1秒 | 1分钟12秒(72秒) |
|
||||
| 10秒 | 12分钟(720秒) |
|
||||
| 50秒 | 1小时(60分,3600秒) |
|
||||
| 1分钟 | 1小时12分钟 |
|
||||
| 1小时 | 3天 |
|
||||
| 1天 | 2.4个月 = 72天 |
|
||||
| 1周 | 约1.385年 ≈ 17个月 = 72周 = 504天 |
|
||||
| 1个月 | 6年 = 72个月 ≈ 308.5周 = 2,160天 |
|
||||
| 1年 | 72年 ≈ 876.5个月 ≈ 3,757周 ≈ 26,297.5天 |
|
||||
|
||||
## 游戏刻
|
||||
|
||||
你可以把游戏想象成一个巨大的机器,它需要不断地运转才能工作。MC 就是这样一个机器。就像时钟里的每个部件都要跟着钟摆的节奏一起动一样,游戏里的每个事情都要跟着游戏的节奏一起发生。我们把游戏的节奏叫做**游戏循环**,它就像是游戏的心跳。每次心跳,游戏就会更新一下自己的状态,比如玩家的位置,方块的变化,怪物的行动等等。我们把每次心跳的时间叫做一**刻(tick)**,它是游戏的最小时间单位。
|
||||
|
||||
游戏的一刻是指 Minecraft 的游戏循环运行一次所占用的时间。正常情况下,游戏固定以**每秒钟 20 刻**的速率运行,因此一刻的时间为 0.05 秒(50 毫秒,或一秒钟的二十分之一),使得游戏内的[一天](https://zh.minecraft.wiki/w/%E6%98%BC%E5%A4%9C%E6%9B%B4%E6%9B%BF)刚好持续 **24000 刻**,也就是 20 分钟。
|
||||
|
||||

|
||||
|
||||
## 游戏中的时间
|
||||
|
||||
有了上面我们对 MC 时间的了解,加上时间刻与现实时间的换算关系,我们就知道了游戏中的一天是如何度过的了。
|
||||
|
||||
### 白天
|
||||
|
||||

|
||||
|
||||
白天是一天周期中最长的一节,历时 10 分钟。
|
||||
|
||||
开始:0 刻(早上06:00:00.0)
|
||||
|
||||
中午:6000 刻(下午12:00:00.0)
|
||||
|
||||
结束:12000 刻(下午06:00:00.0)
|
||||
|
||||
### 日落
|
||||
|
||||

|
||||
|
||||
日落是介于白天和夜晚之间的时间段,持续 1 分半钟。
|
||||
|
||||
开始:12000 刻(下午06:00:00.0)
|
||||
|
||||
中点:12400 刻(下午06:54:00.0)
|
||||
|
||||
结束:13800 刻(下午07:48:00.0)
|
||||
|
||||
### 夜晚
|
||||
|
||||

|
||||
|
||||
夜晚持续 7 分钟。
|
||||
|
||||
开始:13800 刻(下午07:48:00.0)
|
||||
|
||||
午夜:18000 刻(早上12:00:00.0)
|
||||
|
||||
结束:22200 刻(早上04:12:00.0)
|
||||
|
||||
> 晴朗的夜晚时,玩家可以在 12542 刻(下午06:32:31.2)到 23460 刻(早上05:27:36.0)时睡觉。在雨天,玩家可以在 12010 刻(下午06:00:36.0)到 23992 刻(早上05:59:31.2)时睡觉。
|
||||
|
||||
### 日出/黎明
|
||||
|
||||

|
||||
|
||||
日出是介于夜晚和白天之间的时间段,持续 1 分半钟。
|
||||
|
||||
开始:22200 刻(早上04:12:00.0)
|
||||
|
||||
中点:23100 刻(早上05:06:00.0)
|
||||
|
||||
结束:24000(0)刻(早上06:00:00.0)
|
||||
|
||||
## 月相
|
||||
|
||||
游戏中每过一天,时间计数便会增加 24000 刻。虽然每天的交替是一样的,但[月亮](https://zh.minecraft.wiki/w/%E6%9C%88%E4%BA%AE)会经历 8 种月相。虽然没有命令直接更改月相,但`/time add 24000`命令可以快进至下一个月相。进一步而言,使用以下命令可以直接指定不同的月相:
|
||||
|
||||
| 命令 | 月相 |
|
||||
| :----------------- | :----- |
|
||||
| `/time set night` | 满月 |
|
||||
| `/time set 38000` | 亏凸月 |
|
||||
| `/time set 62000` | 下弦月 |
|
||||
| `/time set 86000` | 残月 |
|
||||
| `/time set 110000` | 新月 |
|
||||
| `/time set 134000` | 娥眉月 |
|
||||
| `/time set 158000` | 上弦月 |
|
||||
| `/time set 182000` | 盈凸月 |
|
||||
|
||||
游戏中月亮的原版贴图,自行对应:
|
||||
|
||||

|
||||
|
||||
## 昼夜更替
|
||||
|
||||
如果开启了命令。我们可以使用 `/gamerule doDaylightCycle [*true/false*]` 来控制是否开启昼夜更替。
|
||||
|
||||
当我们关闭昼夜更替之后,游戏中的时间刻虽然会继续运行,但是数值上不会有所变动了,而是**固定在某一刻**。
|
||||
|
||||
## 时间相关的 API
|
||||
|
||||
我们可以在[官方的文档](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/Api%E7%B4%A2%E5%BC%95%E8%A1%A8.html?catalog=1#%E6%97%B6%E9%97%B4)中查看到最新的、与时间相关的 API:
|
||||
|
||||

|
||||
|
||||
看着这么多,如果不算设置昼夜更替的 API 的话,其实总体就分为了两类:
|
||||
|
||||
- 获取时间类;
|
||||
- 设置时间类;
|
||||
|
||||
不管是维度的局部时间,还是其他任何时间,都符合上面介绍的时间规则。
|
||||
|
||||
## 实操:左上角时钟 UI 显示
|
||||
|
||||
接下来我们将带大家实操制作一个 UI,能够实时显示当前维度的时间,效果如下:
|
||||
|
||||

|
||||
|
||||
### Step1. 新增界面文件
|
||||
|
||||
首先,打开我们的 MC Studio,在界面一览选择新建一个「界面文件」:
|
||||
|
||||

|
||||
|
||||
点击下一步之后命名为「timeDisplayUI」就可以了:
|
||||
|
||||

|
||||
|
||||
我们的需求很简单,只需要在界面的左上角,使用「文本」控件显示出当前维度的时间就可以了,所以整个界面也十分简单,一个「文本」控件,设置在左上方即可:
|
||||
|
||||

|
||||
|
||||
把文本大小选择为大,并且把层级设置在 20 层以上(保证在原版的 UI 上方,不会被遮挡),这样方便我们查看。
|
||||
|
||||
OK,界面文件就此告成。
|
||||
|
||||
### Step2. 注册 UI
|
||||
|
||||
注册和创建 UI 需要监听 `UiInitFinished` 之后执行:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
self.mUINode = None
|
||||
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "UiInitFinished",
|
||||
self, self.OnUiInitFinished)
|
||||
|
||||
def UnListenEVent(self):
|
||||
self.UnListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), "UiInitFinished",
|
||||
self, self.OnUiInitFinished)
|
||||
|
||||
def Destroy(self):
|
||||
self.UnListenEVent()
|
||||
|
||||
def OnUiInitFinished(self, args=None):
|
||||
# 注册 UI
|
||||
uiClsPath = 'timeRuleScripts.uIScripts.UIScript'
|
||||
uiScreenDef = 'timeDisplayUI.main'
|
||||
clientApi.RegisterUI('timeRuleMod', 'timeDisplayUI', uiClsPath, uiScreenDef)
|
||||
# 创建 UI
|
||||
self.mUINode = clientApi.CreateUI('timeRuleMod', 'timeDisplayUI', {"isHud": 1})
|
||||
if self.mUINode:
|
||||
self.mUINode.Init() # 调用初始化函数
|
||||
```
|
||||
|
||||
### Step3. UI 代码
|
||||
|
||||
UI 代码也非常简单,也就两个功能:1)每秒更新 `label` 的文字;2)把游戏时间转换成与现实世界对应的时间。
|
||||
|
||||
完整代码如下:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
gameComp = CompFactory.CreateGame(clientApi.GetLevelId())
|
||||
|
||||
|
||||
class UIScript(ScreenNode):
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
self.mPlayerId = clientApi.GetLocalPlayerId()
|
||||
|
||||
# 组件注册地址
|
||||
self.mLabelPath = '/label'
|
||||
|
||||
# 界面需要使用的自定义属性
|
||||
self.mTimeCounter = 0
|
||||
|
||||
def Create(self):
|
||||
print("===== UI Create =====")
|
||||
|
||||
# 1 秒 30 帧
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
perSec = self.mTimeCounter % 30 == 0
|
||||
if perSec:
|
||||
self.UpdateLabelContent()
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def Init(self):
|
||||
print '=== UI 初始化 ==='
|
||||
self.UpdateLabelContent()
|
||||
|
||||
def UpdateLabelContent(self):
|
||||
timeComp = CompFactory.CreateTime(clientApi.GetLevelId())
|
||||
pressedTime = timeComp.GetTime()
|
||||
timeStr = self.GameTime2RealTime(pressedTime)
|
||||
self.GetLabel(self.mLabelPath).SetText(timeStr)
|
||||
|
||||
def GameTime2RealTime(self, gameTick):
|
||||
# 定义游戏中一天的刻数
|
||||
gameDayTicks = 24000
|
||||
# 定义游戏中一小时的刻数
|
||||
gameHourTicks = gameDayTicks / 24
|
||||
# 定义游戏中一分钟的刻数
|
||||
gameMinuteTicks = gameHourTicks / 60
|
||||
# 计算游戏中的天数
|
||||
gameDay = gameTick // gameDayTicks + 1
|
||||
# 计算游戏中的小时数
|
||||
gameHour = (gameTick % gameDayTicks) // gameHourTicks
|
||||
# 计算游戏中的分钟数
|
||||
gameMinute = (gameTick % gameHourTicks) // gameMinuteTicks
|
||||
# 把游戏中的小时数转换成现实中的小时数,加上6小时的偏移量
|
||||
realHour = (gameHour + 6) % 24
|
||||
# 把现实中的小时数、分钟数转换成字符串,补齐两位
|
||||
realHourStr = str(realHour).zfill(2)
|
||||
realMinuteStr = str(gameMinute).zfill(2)
|
||||
# 返回转换后的格式
|
||||
return "第{}天第{}时第{}分".format(gameDay, realHourStr, realMinuteStr)
|
||||
|
||||
def GetLabel(self, path):
|
||||
control = self.GetBaseUIControl(path)
|
||||
if control:
|
||||
return control.asLabel()
|
||||
return None
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
### Step4. 测试并验证
|
||||
|
||||
我们可以尝试使用 `/time set xxx` 命令来设置当前的时间,来验证 UI 代码的正确性。比如 `/time set 0` 界面会正确显示上面规则介绍的 `06:00:00` 这个时间:
|
||||
|
||||

|
||||
|
||||
至此,UI 就完成了。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 给模组左上角新增一个当前时间显示的 UI;
|
||||
- 熟悉并测试时间相关的 API;
|
||||
242
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/2-可成长怪物体系.md
Normal file
242
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/2-可成长怪物体系.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 可成长的怪物体系
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你了解怪物属性的 API,并结合之前学习的课程,带你搭建一个可跟随时间成长的超级僵尸。
|
||||
|
||||
## 成果演示
|
||||
|
||||
我们这节课将配合上一节课提到的时间规则,来实现一个可以跟随时间成长的超级僵尸,这个成长不仅仅是**属性上**的成长,还包括生物行为的丰富:
|
||||
|
||||
我们的需求很简单,先来简单梳理一下:
|
||||

|
||||
|
||||
- 最开始就是普通的僵尸;
|
||||
- 到游戏进行到第 10 天时,所有僵尸都拥有**破坏墙体**的能力;
|
||||
- 到游戏进行到 20 天以后,所有僵尸除了拥有破坏墙体的能力之外,还拥有**搭路**的能力;
|
||||
|
||||
## 实践
|
||||
|
||||
要满足需求,这就需要对应改造一下我们的系统和我们的自定义僵尸,我们一步一步来。
|
||||
|
||||
### Step1. 改造 zombie 行为文件
|
||||
|
||||
我们首先要定义三种强度的僵尸事件,用来支持僵尸行为的变更,不过在此之前我们还要考虑一件事。
|
||||
|
||||
我们之前搭路的行为是直接在 `query.has_target` 之后就开始生效了,现在我们需要把这个行为做一个开关。最容易想到的办法就是像之前课程播放破坏方块动画的那样,使用一个用于设置状态的组件,比如我们使用 `minecraft:mark_variant`:
|
||||
|
||||
```json
|
||||
// 省略其他无关内容
|
||||
"minecraft:entity": {
|
||||
"description": {
|
||||
"animations": {
|
||||
"put_block_sensor": "controller.animation.zombie.put_block"
|
||||
},
|
||||
"scripts": {
|
||||
"animate": [
|
||||
{
|
||||
// 只有在有目标的情况下,并且能够搭建方块时才执行控制器的逻辑
|
||||
"put_block_sensor": "query.has_target && query.mark_variant == 1"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"component_groups": {
|
||||
// 搭建方块的能力
|
||||
"can_not_put_block": {
|
||||
"minecraft:mark_variant": {
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"can_put_block": {
|
||||
"minecraft:mark_variant": {
|
||||
"value": 1
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
这样我们就可以使用组件组来标识该实体是否拥有该能力了。
|
||||
|
||||
然后定义好触发我们三种强度僵尸的事件:
|
||||
|
||||
```json
|
||||
"events": {
|
||||
// 自定义的强度1:就是普通的僵尸
|
||||
"tutorial:zombie_level_1": {
|
||||
"add": {
|
||||
"component_groups": ["can_not_put_block"]
|
||||
},
|
||||
"remove": {
|
||||
"component_groups": ["destroy_block", "destroy_block_attack", "can_put_block", "put_block"]
|
||||
}
|
||||
},
|
||||
// 自定义强度2:能够拆墙,但是不能够铺路的僵尸
|
||||
"tutorial:zombie_level_2": {
|
||||
"add": {
|
||||
"component_groups": ["destroy_block", "can_not_put_block"]
|
||||
},
|
||||
"remove": {
|
||||
"component_groups": ["can_put_block", "put_block"]
|
||||
}
|
||||
},
|
||||
// 自定义强度3:既能够拆墙,又能够搭路的僵尸
|
||||
"tutorial:zombie_level_3": {
|
||||
"add": {
|
||||
"component_groups": ["destroy_block", "can_put_block"]
|
||||
},
|
||||
"remove": {
|
||||
"component_groups": ["can_not_put_block"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
至此,基础的 `zombie.json` 就改造完成了。
|
||||
|
||||
### Step2. 代码检测新的一天
|
||||
|
||||
核心代码如下:
|
||||
|
||||
```python
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleServerSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
# 组件缓存,避免重复创建
|
||||
self.mTimeComp = CompFactory.CreateTime(serverApi.GetLevelId())
|
||||
#
|
||||
self.mTimeCounter = 0
|
||||
self.mLastDay = 0 # 上一次的天数,用来判断是否是新的一天
|
||||
self.mCurrentDay = 0 # 今天的天数
|
||||
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
# 每一秒都检查
|
||||
if self.mTimeCounter % 30 == 0:
|
||||
self._TriggerZombieEventIfNewDay()
|
||||
|
||||
def _TriggerZombieEventIfNewDay(self):
|
||||
# 从游戏开始经过的总帧数
|
||||
passedTime = self.mTimeComp.GetTime()
|
||||
# 从游戏开始经过的游戏天数
|
||||
day = passedTime / 24000
|
||||
if day != self.mCurrentDay:
|
||||
self.mCurrentDay = day
|
||||
isNewDay = self.mLastDay != self.mCurrentDay
|
||||
self.mLastDay = self.mCurrentDay
|
||||
|
||||
if isNewDay:
|
||||
self._ResetAllZombieLevel()
|
||||
```
|
||||
|
||||
为了配合玩家可能使用的 `/time set` 指令,所以我们这里每一秒都检测一下是否是新的一天。如果满足条件,那么重置当前世界中所有的僵尸等级。
|
||||
|
||||
### Step3. 重置僵尸等级和属性函数
|
||||
|
||||
有了检测是否进入新一天的代码了,那么我们可以着手开始考虑僵尸的属性和能力了。
|
||||
|
||||
首先需要定义好等级对应的属性值,以及游戏天数和僵尸等级的对应关系:
|
||||
|
||||
```python
|
||||
# 僵尸的标识符
|
||||
ZombieIdentifier = 'minecraft:zombie'
|
||||
# 僵尸的等级与天数的对应关系
|
||||
ZombieLevelDay = {
|
||||
1 : 1,
|
||||
10: 2,
|
||||
20: 3
|
||||
}
|
||||
# 僵尸等级与僵尸属性的对应关系
|
||||
ZombieLevelAttr = {
|
||||
1: {
|
||||
serverApi.GetMinecraftEnum().AttrType.HEALTH: 10,
|
||||
serverApi.GetMinecraftEnum().AttrType.SPEED : 0.25,
|
||||
serverApi.GetMinecraftEnum().AttrType.DAMAGE: 1
|
||||
},
|
||||
2: {
|
||||
serverApi.GetMinecraftEnum().AttrType.HEALTH: 20,
|
||||
serverApi.GetMinecraftEnum().AttrType.SPEED : 0.3,
|
||||
serverApi.GetMinecraftEnum().AttrType.DAMAGE: 2
|
||||
},
|
||||
3: {
|
||||
serverApi.GetMinecraftEnum().AttrType.HEALTH: 30,
|
||||
serverApi.GetMinecraftEnum().AttrType.SPEED : 0.4,
|
||||
serverApi.GetMinecraftEnum().AttrType.DAMAGE: 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
由于我们保存了当前的游戏天数,所以很容易的写出重置一个僵尸的函数:
|
||||
|
||||
```python
|
||||
def _SetEntityLevelAndAttrAccordCurrentDay(self, entityId):
|
||||
# 使用事件来触发对应僵尸的等级的行为
|
||||
zombieLevel = self._GetZombieLevelByDay(self.mCurrentDay)
|
||||
eventName = 'tutorial:zombie_level_' + str(zombieLevel)
|
||||
CompFactory.CreateEntityEvent(entityId).TriggerCustomEvent(entityId, eventName)
|
||||
# 根据僵尸等级来设置僵尸的属性值
|
||||
attrComp = CompFactory.CreateAttr(entityId)
|
||||
for _attrName, _attrValue in ZombieLevelAttr[zombieLevel].items():
|
||||
attrComp.SetAttrMaxValue(_attrName, _attrValue)
|
||||
attrComp.SetAttrValue(_attrName, _attrValue)
|
||||
|
||||
# 根据天数来获取当前僵尸等级
|
||||
def _GetZombieLevelByDay(self, day):
|
||||
for threshold, level in sorted(ZombieLevelDay.items(), reverse=True):
|
||||
if day >= threshold:
|
||||
return level
|
||||
return 1 # 默认等级为1
|
||||
```
|
||||
|
||||
重置所有僵尸,我们需要借助 [`serverApi.GetEngineActor()` API](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E4%B8%96%E7%95%8C/%E5%AE%9E%E4%BD%93%E7%AE%A1%E7%90%86.html#getengineactor) 来完成:
|
||||
|
||||
```python
|
||||
# 重置所有僵尸的等级
|
||||
def _ResetAllZombieLevel(self):
|
||||
print '=====重置所有僵尸等级===='
|
||||
for entityId, entityDict in serverApi.GetEngineActor().items():
|
||||
for _dimensionId, _identifier in entityDict.items():
|
||||
if _identifier == ZombieIdentifier:
|
||||
self._SetEntityLevelAndAttrAccordCurrentDay(entityId)
|
||||
```
|
||||
|
||||
### Step4. 考虑新增的僵尸
|
||||
|
||||
新增的僵尸也需要立刻适配当前世界天数对应的等级,这需要我们关注[实体新增的事件](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E4%BA%8B%E4%BB%B6/%E4%B8%96%E7%95%8C.html#addentityserverevent),我们需要在代码中进行监听,当新增时自动触发对应的事件:
|
||||
|
||||
```python
|
||||
def ListenEvent(self):
|
||||
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "AddEntityServerEvent", self,
|
||||
self.OnAddEntityServerEvent)
|
||||
|
||||
def UnListenEvent(self):
|
||||
self.UnListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), "AddEntityServerEvent", self,
|
||||
self.OnAddEntityServerEvent)
|
||||
|
||||
def Destroy(self):
|
||||
self.UnListenEvent()
|
||||
|
||||
def OnAddEntityServerEvent(self, args):
|
||||
entityId = args['id']
|
||||
engineTypeStr = args['engineTypeStr']
|
||||
if engineTypeStr == ZombieIdentifier:
|
||||
self._SetEntityLevelAndAttrAccordCurrentDay(entityId)
|
||||
```
|
||||
|
||||
### Step5. 测试并验证
|
||||
|
||||
首先,我们使用命令 `/time set 0`,可以看到僵尸对于在方块内的目标并没有任何办法,属性值和组件组也跟我们设定的一样:
|
||||
|
||||

|
||||
|
||||
然后,当使用命令 `/time set 240000` 把游戏进程过渡到第 10 天之后时,这时候僵尸属性值发生了变化,并且拥有了破坏墙体的能力:
|
||||
|
||||

|
||||
|
||||
当我们使用命令 `/time set 480000` 把游戏进程过渡到 20 天之后时,僵尸已经有能力击杀在高处的目标:
|
||||
|
||||

|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 改造原版的 `zombie.json` 并配合上节课的时间管理系统来进行管理生物行为和属性值,达到一个跟随时间不断变强的僵尸。
|
||||
248
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/3-怪物生成.md
Normal file
248
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/3-怪物生成.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 怪物生成
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你了解生物生成的规则。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅实体生成规则;
|
||||
- ✅如何使用代码生成自定义实体;
|
||||
|
||||
## 实体生成规则
|
||||
|
||||
在原版行为包下的 `spawn_rules` 目录下,定义了许多实体的生成规则,也就是如何生成实体在世界中。当你希望自定义实体自然生成时,你就需要使用生成规则。不同的组件允许可以定义实体不同的生成时间、位置和方式。
|
||||
|
||||
一般来说,我们都会选择与原版实体非常相似的方式来生成。比如,像牛一样生成在牛群中、像僵尸一样出生在晚上,或者像鱼一样只出生在水里。
|
||||
|
||||
### 基础结构
|
||||
|
||||
当你要使用原版的生成规则时,首先,需要在行为包的 `spawn_rules` 目录下新建一个 `identifier.json` (这里的 `identifier` 代指生物标识符)的新文件。该文件的内容应该如下所示:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"minecraft:spawn_rules": {
|
||||
"description": {
|
||||
"identifier": "tutiroal:identifier",
|
||||
"population_control": "animal"
|
||||
},
|
||||
"conditions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 `minecraft:spawn_rules` 内部,我们需要考虑两件事:**人口控制**和**条件**。
|
||||
|
||||
`description` description 定义文件的基本属性。 `identifier` identifier 应该匹配我们实体的标识符。 `population_control` population_control 定义游戏如何知道要生成多少生物,并且稍微复杂一些。
|
||||
|
||||
### 人口控制
|
||||
|
||||
我的世界有不同的实体池。当此处定义的池被认为已满时,游戏将不再生成该池中的生物。 共有以下几种不同的选择(一般我们使用前面三种即可):
|
||||
|
||||
- **"animal"**:被动生物,如牛和猪
|
||||
- **"water_animal"**:热带鱼、海豚等水生生物
|
||||
- **"monster"**:敌对生物,如骷髅和僵尸
|
||||
- **"villager"**:村民专属。
|
||||
- **"ambient"**:蝙蝠专属。
|
||||
- **"cat"**:猫专属。
|
||||
- **"pillager"**:掠夺者专属。
|
||||
|
||||
### 条件
|
||||
|
||||
`conditions` 是一系列允许生物在世界中生成的可能条件。 每个条件分别尝试在世界中生成生物。 每个条件都由一组组件组成,这些组件定义何时生产或不生成生物。
|
||||
|
||||
比如,我们可以查看**原版僵尸**的生成条件:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"minecraft:spawn_rules": {
|
||||
"description": {
|
||||
"identifier": "minecraft:zombie",
|
||||
"population_control": "monster"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"minecraft:spawns_on_surface": {},
|
||||
"minecraft:spawns_underground": {},
|
||||
"minecraft:brightness_filter": {
|
||||
"min": 0,
|
||||
"max": 7,
|
||||
"adjust_for_weather": true
|
||||
},
|
||||
"minecraft:difficulty_filter": {
|
||||
"min": "easy",
|
||||
"max": "hard"
|
||||
},
|
||||
"minecraft:weight": {
|
||||
"default": 100
|
||||
},
|
||||
"minecraft:herd": {
|
||||
"min_size": 2,
|
||||
"max_size": 4
|
||||
},
|
||||
"minecraft:permute_type": [
|
||||
{
|
||||
"weight": 95
|
||||
},
|
||||
{
|
||||
"weight": 5,
|
||||
"entity_type": "minecraft:zombie_villager"
|
||||
}
|
||||
],
|
||||
"minecraft:biome_filter": {
|
||||
"test": "has_biome_tag", "operator": "==", "value": "monster"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 组件名称 | 描述 |
|
||||
| :-------------------------------- | :----------------------------------------------------------- |
|
||||
| `minecraft:spawns_on_surface` | 生物在地表生成 |
|
||||
| `minecraft:spawns_on_underground` | 生物在地底生成 |
|
||||
| `minecraft:brightness_filter` | 仅以特定亮度生成实体。 接受三个选项,`min`、`max` 和 `adjust_for_weather`。 亮度级别范围从 0 到 15。 如果 `adjust_for_weather` 设置为 `true`,则将考虑由于下雨和雷暴导致的亮度降低。 |
|
||||
| `minecraft:difficulty_filter` | 难度选择器,`min`、`max` 定义了最低和最高难度。实体仅会在定义的难度内生成。 |
|
||||
| `minecraft:weight` | 实体生成时的权重。 数字越大,生物生成的频率越高。 |
|
||||
| `minecraft:herd` | 设置在同一生成规则上一起生成的实体数。也可以理解为群体生成。 |
|
||||
| `minecraft:permute_type` | 给生成的实体一定概率变异为其他实体。 |
|
||||
| `minecraft:biome_filter` | 群系选择器。仅会在通过条件的群系生成。 |
|
||||
|
||||
### 所有已知组件
|
||||
|
||||
下列是所有已知的用于控制实体生成的条件组件:
|
||||
|
||||
```text
|
||||
minecraft:weight
|
||||
minecraft:density_limit
|
||||
minecraft:spawns_on_block_filter
|
||||
minecraft:spawns_on_block_prevented_filter
|
||||
minecraft:spawns_above_block_filter
|
||||
minecraft:herd
|
||||
minecraft:permute_type
|
||||
minecraft:brightness_filter
|
||||
minecraft:height_filter
|
||||
minecraft:spawns_on_surface
|
||||
minecraft:spawns_underground
|
||||
minecraft:spawns_underwater
|
||||
minecraft:disallow_spawns_in_bubble
|
||||
minecraft:spawns_lava
|
||||
minecraft:biome_filter
|
||||
minecraft:difficulty_filter
|
||||
minecraft:distance_filter
|
||||
minecraft:is_experimental
|
||||
minecraft:world_age_filter
|
||||
minecraft:delay_filter
|
||||
minecraft:mob_event_filter
|
||||
minecraft:is_persistent
|
||||
minecraft:player_in_village_filter
|
||||
```
|
||||
|
||||
我们仅需要对上述组件有一个大概印象就可以,除了一些比较特殊的 `minecraft:is_persistent` 之类的,我们在原版中看不到例子之外,其他的都可以在原版行为包中搜索到相关的应用。我们只对下面两个比较特殊常用的来做一些说明。
|
||||
|
||||
#### spawns_above_block_filter
|
||||
|
||||
```JSON
|
||||
"minecraft:spawns_above_block_filter": {
|
||||
"blocks": "minecraft:stone",
|
||||
"distance": 10
|
||||
}
|
||||
```
|
||||
|
||||
这个组件会垂直检测设定距离内的方块,如果条件满足,则实体生成。生成在指定的方块上。
|
||||
|
||||
#### spawns_on_block_prevented_filter
|
||||
|
||||
```json
|
||||
"minecraft:spawns_on_block_prevented_filter": [
|
||||
"minecraft:nether_wart_block",
|
||||
"minecraft:shroomlight"
|
||||
]
|
||||
```
|
||||
|
||||
正好与上面一个组件相反,该组件的作用是让实体永远不会在列表内的方块上生成。
|
||||
|
||||
## 代码生成
|
||||
|
||||
虽然原版的生成规则已经能够满足大部分的需求,但有时我们也会想要自己生成一些怪物以应对特殊情况。比如,在惊变中,血月会导致怪物的生成速度和数量大幅增加:
|
||||
|
||||

|
||||
|
||||
这可能就需要我们代码来执行额外的生成逻辑了。
|
||||
|
||||
### 一切的基础
|
||||
|
||||
生成怪物,目前官方只有一个 [API](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E4%B8%96%E7%95%8C/%E5%AE%9E%E4%BD%93%E7%AE%A1%E7%90%86.html#createengineentitybytypestr):`CreateEngineEntityByTypeStr`,用法也很简单:
|
||||
|
||||
```python
|
||||
import mod.server.extraServerApi as serverApi
|
||||
ServerSystem = serverApi.GetServerSystemCls()
|
||||
class MyServerSystem(ServerSystem):
|
||||
def createMob(self):
|
||||
# 在主世界(0,5,0)的位置创建一个朝向为(0, 0)的尸壳
|
||||
entityId = self.CreateEngineEntityByTypeStr('minecraft:husk', (0, 5, 0), (0, 0), 0)
|
||||
```
|
||||
|
||||
### 示例:在玩家周围生成指定实体
|
||||
|
||||
API 很简单,最重要的就是对于坐标的选择了。我们想要实体生成在目标周围,但也要考虑一些特殊情况,比如当玩家在水中、空中、或者洞穴中能否支持生成。如果考虑得更加复杂一点,当前的控件是否支持实体的碰撞箱、光照强度等.....
|
||||
|
||||
这里提供一个简化之后的示例代码:
|
||||
|
||||
```python
|
||||
def SpawnEntityAround(self, targetId, spawnEntityIdentifier, num=1, minRadius=6, maxRadius=12):
|
||||
"""
|
||||
生成实体在目标周围
|
||||
:param targetId:
|
||||
:param spawnEntityIdentifier:
|
||||
:param num:
|
||||
:param minRadius:
|
||||
:param maxRadius:
|
||||
:return:
|
||||
"""
|
||||
entityList = []
|
||||
|
||||
targetX, targetY, targetZ = CompFactory.CreatePos(targetId).GetFootPos()
|
||||
dimensionId = CompFactory.CreateDimension(targetId).GetEntityDimensionId()
|
||||
|
||||
for _num in xrange(num):
|
||||
_randomPos = (
|
||||
targetX + random.randint(minRadius, maxRadius) * math.sin(random.uniform(0, 2 * math.pi)),
|
||||
targetY,
|
||||
targetZ + random.randint(minRadius, maxRadius) * math.cos(random.uniform(0, 2 * math.pi))
|
||||
)
|
||||
# 上面的 _randomPos 只是平面上的一个坐标,我们为了合理生成在地面上,需要 y 在 [-20, 20] 之间有一个合适的坐标
|
||||
_firstAirPos = self._RetFirstAirBlockPos(_randomPos, dimensionId, -20, 20)
|
||||
if _firstAirPos:
|
||||
entityId = self.CreateEngineEntityByTypeStr(spawnEntityIdentifier, _firstAirPos, (0, random.randint(-180, 180)),
|
||||
dimensionId)
|
||||
entityList.append(entityId)
|
||||
return entityList
|
||||
|
||||
# 范围内的第一个空气方块
|
||||
def _RetFirstAirBlockPos(self, basePos, dimensionId, step, maxStep):
|
||||
blockInfoComp = CompFactory.CreateBlockInfo(serverApi.GetLevelId())
|
||||
while step < maxStep:
|
||||
finalPos = (basePos[0], basePos[1] + step, basePos[2])
|
||||
blockDict = blockInfoComp.GetBlockNew(finalPos, dimensionId)
|
||||
step += 1
|
||||
if blockDict and blockDict['name'] == 'minecraft:air':
|
||||
return finalPos
|
||||
return None
|
||||
```
|
||||
|
||||
整体逻辑也很简单,画个图来解释一下:
|
||||
|
||||

|
||||
|
||||
先在平面上选择一个随机坐标,然后再从 y 方向上下手,寻找第一个空气方块,这个就是适合实体(简化)的坐标。
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 使用自定义的生成规则生成僵尸;
|
||||
- 使用自己的代码来自定义生成指定的生物;
|
||||
471
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/4-随机事件.md
Normal file
471
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/4-随机事件.md
Normal file
@@ -0,0 +1,471 @@
|
||||
# 随机事件
|
||||
|
||||
> 温馨提示:开始阅读这篇指南之前,我们希望你对《我的世界》基岩版附加包有一定了解,有能力撰写 JSON 数据格式,对 Python 进行模组开发有了解,并能够独立阅读《我的世界》开发者官网-开发指南或其他技术引用文档。
|
||||
|
||||
本文将带你在时间管理系统存在的前提(之前的课程)下,来触发我们的自定义事件,一个是血月,一个是大雾天。
|
||||
|
||||
在本教程中,您将学习以下内容。
|
||||
|
||||
- ✅血月的实现原理;
|
||||
- ✅雾天的实现原理;
|
||||
- ✅原版粒子的进阶使用知识;
|
||||
|
||||
## 随机事件的规则
|
||||
|
||||
我们这一次课程要给我们的世界加入两种随机事件,一个是在晚上触发的血月:
|
||||
|
||||

|
||||
|
||||
另一个就是在白天触发的大雾天气:
|
||||
|
||||

|
||||
|
||||
并且他们会跟随游戏天数的增加而增加出现的概率。
|
||||
|
||||
### 触发逻辑
|
||||
|
||||
我们先来搭建最基础的部分,就是先触发这两种事件,然后再去实现具体的效果。
|
||||
|
||||
这两个事件我们可以换个角度来理解,都是需要在白天切换黑夜,黑夜切换白天的节点来判断触发的:
|
||||
|
||||
```python
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
# 每一秒都检查
|
||||
if self.mTimeCounter % 30 == 0:
|
||||
self._TriggerRandomEventIfNewDayOrNight()
|
||||
|
||||
# 在新的白天或者夜晚触发随机事件
|
||||
def _TriggerRandomEventIfNewDayOrNight(self):
|
||||
# 从游戏开始经过的总帧数
|
||||
passedTime = self.mTimeComp.GetTime()
|
||||
previousTick = self.mCurrentTime
|
||||
self.mCurrentTime = passedTime
|
||||
|
||||
# 计算当前时间位置
|
||||
currentTime = self.mCurrentTime % self.mTicksPerDay
|
||||
previousTime = previousTick % self.mTicksPerDay
|
||||
|
||||
# 检查是否发生了昼夜交替
|
||||
if currentTime < (self.mTicksPerDay / 2) <= previousTime:
|
||||
self._HandleNightToDayTransition()
|
||||
elif currentTime >= (self.mTicksPerDay / 2) > previousTime:
|
||||
self._HandleDayToNightTransition()
|
||||
|
||||
# 更新白天/夜晚状态
|
||||
self.mCurrentTimeIsDayTime = currentTime < (self.mTicksPerDay / 2)
|
||||
```
|
||||
|
||||
## 血月的实现
|
||||
|
||||
首先,我们需要在「设置」→ 「视频」里面把「美丽的天空」选项打开:
|
||||
|
||||

|
||||
|
||||
这样才能在晚上看见月亮:
|
||||
|
||||

|
||||
|
||||
进一步的,我们想要把这个月亮给变成红色的呢,有以下几种思路。
|
||||
|
||||
### 修改原版资源
|
||||
|
||||
原版所有的资源文件,几乎都支持更改,包括不局限于:原版生物行为、动画、控制器、贴图文件等。月亮当然也不例外。
|
||||
|
||||
原版资源下的月亮位于目录:`\data\resource_packs\vanilla\textures\environment\moon_phases.png`,截图如下:
|
||||
|
||||

|
||||
|
||||
我们想要修改资源,直接复制一份原版的,然后修改保存到**现项目**资源文件下的 `textures/environment/moon_phases.png` 就行了。
|
||||
|
||||
不过直接修改有一个问题,假如我们把原版资源修改成了这样(别在意颜色...):
|
||||
|
||||

|
||||
|
||||
你会发现即使不是血月(特定时间)的晚上,也会是血月:
|
||||
|
||||

|
||||
|
||||
因为这个贴图不支持动态更改,意味着改了的话,在加载模组的情况下,就没有办法再进行动态的变更和改动了。
|
||||
|
||||
没办法,我们又只能曲线救国了。既然月亮的贴图是能变的,我们之前也介绍过**月相**的这个概念,我们的思路就有了。
|
||||
|
||||
我们只让满月的贴图变成血月:
|
||||
|
||||

|
||||
|
||||
其他的月相不变,然后让时间在某一个周期内循环,比如 24000 刻到 48000 刻(也就是第二天),只有血月的时候才在第一个周期内运行。这样就达到要求了。
|
||||
|
||||
但为此,我们需要对应改造我们的时间管理系统,也需要对天数进行对应的存档(因为存档在某一天内一直循环)。存档的天数也需要同步到客户端进行显示。
|
||||
|
||||
还可能会影响原版掠夺者的生成(原版掠夺者会在 5.5 个游戏日后距离任一玩家 24 到 48 格生成)。
|
||||
|
||||
所以我们这里只提一下思路。具体的实现,大家感兴趣的话,可以在自己的模组中,自行尝试一下。
|
||||
|
||||
### 使用特效遮住月亮
|
||||
|
||||
我们来介绍一个比较特别的方法,那就是用特效来遮住月亮:
|
||||
|
||||

|
||||
|
||||
这有助于提高我们对原版粒子的理解和认识。
|
||||
|
||||
能这么做有几个重要的前提:
|
||||
|
||||
- √ 玩家和月亮贴图之间存在相当的距离;
|
||||
- √ 我们可以获取月亮的角度;
|
||||
- √ 特效可以绑定在实体身上,跟随实体实时运动;
|
||||
- √ 我们可以通过自定义的变量来实时改变和控制特效的位置来遮挡住原版的月亮;
|
||||
|
||||
#### Step 1. 制作一个血月的贴图
|
||||
|
||||
首先,我们用软件制作一个血月的贴图:
|
||||
|
||||

|
||||
|
||||
并放置在资源文件下(`资源包/textures/particle/blood_moon.png`)备用。
|
||||
|
||||
#### Step 2. 在玩家身上绑定一个粒子动画
|
||||
|
||||
我们先来创建一个绑定在玩家身上播放的动画:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.8.0",
|
||||
"animations": {
|
||||
"animation.blood_moon.play": {
|
||||
"loop": true,
|
||||
// 设置一个足够短的时间,让它以非常快的速度刷新用来遮挡原本的月亮
|
||||
"animation_length": 0.01,
|
||||
"particle_effects": {
|
||||
"0.0": [
|
||||
{
|
||||
"effect": "blood_moon",
|
||||
// 我们这里绑定一个未知的 locator 这会默认绑定在玩家的 body 骨骼组上
|
||||
"locator": "unknown"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个动画文件设置成循环播放,这样我们可以持续播放这个带有粒子的动画;
|
||||
|
||||
我们同时也需要使用 API 在客户端在玩家的渲染器中增加上述的动画:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# ...省略了其他无关内容...
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, 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']
|
||||
if playerId == clientApi.GetLocalPlayerId():
|
||||
self.InitRender()
|
||||
|
||||
# 初始化绑定,给玩家绑定上血月的相关 query.mod.xx 变量和相关动画
|
||||
def InitRender(self):
|
||||
queryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
queryVariableComp.Register("query.mod.moon_rot", 0)
|
||||
queryVariableComp.Register("query.mod.play_blood_moon", 0)
|
||||
# 测试,默认播放血月效果
|
||||
queryVariableComp.Set("query.mod.play_blood_moon", 1)
|
||||
|
||||
actorRenderComp = CompFactory.CreateActorRender(clientApi.GetLocalPlayerId())
|
||||
actorRenderComp.AddPlayerParticleEffect("blood_moon", "tutorial:blood_moon")
|
||||
actorRenderComp.AddPlayerAnimation("play_blood_moon", "animation.blood_moon.play")
|
||||
actorRenderComp.AddPlayerScriptAnimate("play_blood_moon", "query.mod.play_blood_moon")
|
||||
res = actorRenderComp.RebuildPlayerRender()
|
||||
|
||||
```
|
||||
|
||||
#### Step 3. 特效文件
|
||||
|
||||
通过查阅官方 API,我们可以通过 `GetMoonRot` 获取到月亮的角度,那么就简单了。
|
||||
|
||||
我们通过观察可以发现,昼夜交替中的太阳月亮是只会在 x 轴上移动的,它们的移动轨迹近似于一个圆形:
|
||||
|
||||

|
||||
|
||||
那么已知了月亮的角度,那么只需要知道半径就可以计算出月亮当前的位置了:
|
||||
|
||||

|
||||
|
||||
假如月亮半径是 `variable.distance` 的话,那么很容易得出某一个具体的时间节点,月亮粒子的 x、y、z 位置应该如下:
|
||||
|
||||
```json
|
||||
"minecraft:emitter_shape_point": {
|
||||
"offset": ["math.sin(query.mod.moon_rot) * variable.distance", "math.cos(query.mod.moon_rot) * variable.distance", 0]
|
||||
}
|
||||
```
|
||||
|
||||
- x:`math.sin(query.mod.moon_rot) * variable.distance`,就是 `sin` 函数乘以半径;
|
||||
- y:`math.cos(query.mod.moon_rot) * variable.distance`,就是 `cos` 函数乘以半径;
|
||||
- z:由于没有偏移,所以始终是 0
|
||||
|
||||
所以完整的粒子文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"format_version": "1.10.0",
|
||||
"particle_effect": {
|
||||
"description": {
|
||||
"identifier": "tutorial:blood_moon",
|
||||
"basic_render_parameters": {
|
||||
"material": "particles_blend",
|
||||
"texture": "textures/particle/blood_moon"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"minecraft:emitter_local_space": {
|
||||
"position": true,
|
||||
"rotation": false
|
||||
},
|
||||
"minecraft:emitter_initialization": {
|
||||
"creation_expression": "variable.distance = 140;"
|
||||
},
|
||||
"minecraft:emitter_rate_steady": {
|
||||
// 用一个足够快的速度来产生粒子
|
||||
"spawn_rate": 200,
|
||||
// 限制最大的粒子数,避免造成卡顿
|
||||
"max_particles": 1
|
||||
},
|
||||
"minecraft:particle_lifetime_expression": {
|
||||
"max_lifetime": 0.01
|
||||
},
|
||||
"minecraft:emitter_lifetime_once": {
|
||||
"active_time": 0.01
|
||||
},
|
||||
"minecraft:emitter_shape_point": {
|
||||
"offset": ["math.sin(query.mod.moon_rot) * variable.distance", "math.cos(query.mod.moon_rot) * variable.distance", 0]
|
||||
},
|
||||
"minecraft:particle_appearance_billboard": {
|
||||
"size": [14, 14],
|
||||
"facing_camera_mode": "lookat_xyz",
|
||||
"uv": {
|
||||
"texture_width": 128,
|
||||
"texture_height": 128,
|
||||
"uv": [0, 0],
|
||||
"uv_size": [128, 128]
|
||||
}
|
||||
},
|
||||
"minecraft:particle_appearance_tinting": {
|
||||
"color": [1, 0, 0, 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里有一个**需要注意**的点,就是 `minecraft:emitter_local_space` 组件。它定义了是否使用实体的局部空间变量,`position` 和 `ratation` 分别是对应了「是否使用实体**局部坐标**」和「是否使用实体**局部旋转**」。
|
||||
|
||||
由于我们需要以玩家为中心来播放粒子,所以 `position` 为 `true`,而局部旋转则不需要,如果我们这里的 `rotation` 为 `true` 的话,就会出现粒子跟随玩家身体朝向来转动的效果:
|
||||
|
||||

|
||||
|
||||
#### Step 4. 把月亮角度传入 `query.mod.moon_rot`
|
||||
|
||||
为了缓解客户端的压力,我们每 3 帧更新一次 `query.mod.moon_rot` 的值:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# 省略了其他无关的内容...
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.mTimeCounter = 0
|
||||
#
|
||||
self.mSkyRenderComp = CompFactory.CreateSkyRender(clientApi.GetLevelId())
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
if self.mTimeCounter % 3 == 0:
|
||||
moonRot = self.mSkyRenderComp.GetMoonRot()[2]
|
||||
self.mQueryVariableComp.Set('query.mod.moon_rot', moonRot)
|
||||
```
|
||||
|
||||
#### Step 5. 测试效果
|
||||
|
||||
一切准备好之后,我们重新打开客户端进入游戏,输入指令 `/time set 11990` (11990 是一个足够接近月亮升起的时间,可以观察月亮升起的全过程)。
|
||||
|
||||
游戏效果如下:
|
||||
|
||||

|
||||
|
||||
## 雾天的实现
|
||||
|
||||
雾天的实现比较简单,基本上就依靠 `SetFogLength()` API 就可以了。比如在调试工具中运行下列代码:
|
||||
```python
|
||||
import mod.client.extraClientApi as clientApi
|
||||
levelId = clientApi.GetLevelId()
|
||||
comp = clientApi.GetEngineCompFactory().CreateFog(levelId)
|
||||
comp.SetFogLength(0.1, 32)
|
||||
```
|
||||
|
||||
就可以实现比较好的雾天的效果:
|
||||
|
||||

|
||||
|
||||
## 服务端客户端改造
|
||||
|
||||
至此,我所有客户端的效果都改造完成了,我们只需要有对应的事件来触发客户端的渲染就可以了。
|
||||
|
||||
客户端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# 省略了无关内容....
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
CompFactory = clientApi.GetEngineCompFactory()
|
||||
|
||||
|
||||
class TimeRuleClientSystem(clientApi.GetClientSystemCls()):
|
||||
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleClientSystem, self).__init__(namespace, name)
|
||||
self.ListenEvent()
|
||||
self.mQueryVariableComp = CompFactory.CreateQueryVariable(clientApi.GetLocalPlayerId())
|
||||
self.mFogComp = CompFactory.CreateFog(clientApi.GetLevelId())
|
||||
|
||||
def ListenEvent(self):
|
||||
# 自定义事件
|
||||
self.ListenForEvent("timeRuleMod", "timeRuleServerSystem", "BloodMoonEvent", self, self.OnBloodMoonEvent)
|
||||
self.ListenForEvent("timeRuleMod", "timeRuleServerSystem", "FoggyWeatherEvent", self, self.OnFoggyWeatherEvent)
|
||||
|
||||
def OnBloodMoonEvent(self, args):
|
||||
flag = args['flag']
|
||||
self.mQueryVariableComp.Set('query.mod.play_blood_moon', 1 if flag else 0)
|
||||
|
||||
def OnFoggyWeatherEvent(self, args):
|
||||
flag = args['flag']
|
||||
self._FoggyWeather(flag)
|
||||
|
||||
def _FoggyWeather(self, isOpen):
|
||||
if isOpen:
|
||||
self.mFogComp.SetFogLength(0.1, 32)
|
||||
else:
|
||||
self.mFogComp.ResetFogLength()
|
||||
|
||||
```
|
||||
|
||||
服务端代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
# 省略了无关内容...
|
||||
import mod.server.extraServerApi as serverApi
|
||||
import random
|
||||
|
||||
CompFactory = serverApi.GetEngineCompFactory()
|
||||
|
||||
class TimeRuleServerSystem(serverApi.GetServerSystemCls()):
|
||||
def __init__(self, namespace, name):
|
||||
super(TimeRuleServerSystem, self).__init__(namespace, name)
|
||||
#
|
||||
self.mIsBloodMoon = False # 是否开启了血月
|
||||
self.mIsFoggyWeather = False # 是否开启了大雾天气
|
||||
|
||||
# region 类函数
|
||||
# --------------------------------------------------------------------------------------------
|
||||
def _HandleDayToNightTransition(self):
|
||||
self._CloseDayEvent()
|
||||
self._TriggerNightEvent()
|
||||
|
||||
def _CloseDayEvent(self):
|
||||
if self.mIsFoggyWeather:
|
||||
# 让客户端关闭大雾天气渲染
|
||||
self.BroadcastToAllClient("FoggyWeatherEvent", {'flag': False})
|
||||
self.mIsFoggyWeather = False
|
||||
|
||||
def _TriggerNightEvent(self):
|
||||
if random.random() < self._GetEventProbability():
|
||||
# 通知全部玩家
|
||||
CompFactory.CreateCommand(serverApi.GetLevelId()).SetCommand("/title @a title §c血月降临!!")
|
||||
self.mIsBloodMoon = True
|
||||
# 让客户端渲染血月
|
||||
self.BroadcastToAllClient("BloodMoonEvent", {'flag': True})
|
||||
|
||||
def _HandleNightToDayTransition(self):
|
||||
self._CloseNightEvent()
|
||||
self._TriggerDayEvent()
|
||||
|
||||
def _CloseNightEvent(self):
|
||||
if self.mIsBloodMoon:
|
||||
# 让客户端关闭血月渲染
|
||||
self.BroadcastToAllClient("BloodMoonEvent", {'flag': False})
|
||||
self.mIsBloodMoon = False
|
||||
|
||||
def _TriggerDayEvent(self):
|
||||
if random.random() < self._GetEventProbability():
|
||||
CompFactory.CreateCommand(serverApi.GetLevelId()).SetCommand("/title @a title 大雾天气!!")
|
||||
# 通知客户端渲染大雾天气
|
||||
self.BroadcastToAllClient("FoggyWeatherEvent", {'flag': True})
|
||||
self.mIsFoggyWeather = True
|
||||
|
||||
# 触发事件的概率。随天数的增加,概率逐渐增高,最多为 0.9,最少为 0.1
|
||||
def _GetEventProbability(self):
|
||||
return min(0.9, (0.1 + (self.mCurrentDay / 100.0)))
|
||||
|
||||
# endregion
|
||||
```
|
||||
|
||||
对于血月更多实体的生成,我们复用上一节课的代码加上一点判断就行了:
|
||||
|
||||
```python
|
||||
# 省略掉其他无关内容
|
||||
# 僵尸的数量限制
|
||||
ZombieLimit = 100
|
||||
|
||||
class TimeRuleServerSystem(serverApi.GetServerSystemCls()):
|
||||
def Update(self):
|
||||
self.mTimeCounter += 1
|
||||
# 每一秒都检查
|
||||
if self.mTimeCounter % 30 == 0:
|
||||
# 血月的时候会检测生成更多的实体
|
||||
if not self.mCurrentTimeIsDayTime and self.mIsBloodMoon:
|
||||
self.SpawnZombieAroundPlayer()
|
||||
|
||||
def SpawnZombieAroundPlayer(self):
|
||||
# 需要先检查当前世界上的僵尸数量
|
||||
if self.CurrentZombieNum() <= ZombieLimit:
|
||||
randomPlayerId = random.choice(serverApi.GetPlayerList())
|
||||
self.SpawnEntityAround(randomPlayerId, ZombieIdentifier, 10) # 一次性生成 10 个,吓死玩家
|
||||
|
||||
def CurrentZombieNum(self):
|
||||
count = 0
|
||||
for entityId, entityDict in serverApi.GetEngineActor().items():
|
||||
for _dimensionId, _identifier in entityDict.items():
|
||||
if _identifier == ZombieIdentifier:
|
||||
count += 1
|
||||
return count
|
||||
```
|
||||
|
||||
## 课后作业
|
||||
|
||||
本次课后作业,内容如下:
|
||||
|
||||
- 在本地还原血月和大雾天气的效果;
|
||||
- 结合时间管理系统,在昼夜更替的时候概率触发;
|
||||
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/1_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/1_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_1.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_1.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_2.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_2.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_3.gif
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/4_3.gif
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/A_sunset.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/A_sunset.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Day_Night_Clock_24h.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Day_Night_Clock_24h.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Sunrise.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Sunrise.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Survival.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/Survival.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/TundraNight.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/TundraNight.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/blood_moon.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/blood_moon.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118203935199.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118203935199.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118211151267.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118211151267.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223613068.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223613068.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223659677.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223659677.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223906511.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231118223906511.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231119105709932.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231119105709932.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231120155538911.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231120155538911.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151314962.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151314962.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151421421.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151421421.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151719712.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121151719712.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190714556.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190714556.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190808650.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121190808650.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121191003030.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121191003030.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121193936537.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121193936537.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121194046552.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121194046552.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121205156004.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231121205156004.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122120529133.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122120529133.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122162736911.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122162736911.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122163632497.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122163632497.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122174043335.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122174043335.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122192922106.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122192922106.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122194716435.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image-20231122194716435.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image_1.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/SDK大师教程/1-惊变系统/assets/image_1.png
LFS
Normal file
Binary file not shown.
Reference in New Issue
Block a user