2.6
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# 创建界面的两种方式
|
||||
|
||||
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818ce3c31a9c0f360dc5d0" width="800" height="600" allow="fullscreen"/>
|
||||
|
||||
在我的世界中国版的客户端模组开发中,主要有两种创建界面的方式。使用这两种不同的方式创建的界面效果和用途略有区别。在实际开发过程中,应该选择合适的方式来创建界面。
|
||||
|
||||
> 在开服工具2.0的开发中,除了在客户端模组处创建界面,还可以在服务端预定义界面并发送到客户端。[链接](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/27-手机网络游戏/课程10:使用Spigot开服/30-Spigot服Demo详解/6-ServerFormDemo详解.html?catalog=1)
|
||||
>
|
||||
> 它可以使用Java代码来生成简单的一些界面,并通过SpigotMaster插件,将UI请求发送到客户端,不需要编写任何客户端代码。
|
||||
|
||||
## CreateUI
|
||||
|
||||
CreateUI方式创建的界面,是直接叠加在游戏界面之上的一种界面创建方式。
|
||||
|
||||
这种界面在游戏中有非常多的体现:
|
||||
|
||||
- 快捷栏
|
||||
- 血量条
|
||||
- 饥饿度条
|
||||
- ···
|
||||
|
||||
使用它创建的界面,可以设置是否属于Hud。使用Hud模式创建的界面,不会影响游戏的正常操作。反之,关闭Hud模式,界面会屏蔽游戏输入(方向、视角)。
|
||||
|
||||
上方所述的界面均是Hud界面。
|
||||
|
||||
## PushScreen
|
||||
|
||||
PushScreen方式使用堆栈来管理界面。即每次只能有一个处于栈顶的界面显示在游戏中。
|
||||
|
||||
这种界面在游戏中也有非常多的体现:
|
||||
|
||||
- 箱子界面
|
||||
- 熔炉界面
|
||||
- 铁砧界面
|
||||
- ···
|
||||
|
||||
这种方式创建的界面,不会和其他界面同时显示。也会默认屏蔽游戏输入,同时支持手柄的摇杆操作。
|
||||
|
||||
一般在制作玩法功能,不希望玩家在打开界面时进行移动或其他操作的情况下,推荐使用PushScreen来创建界面。
|
||||
87
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/1-界面编辑器的使用.md
Normal file
87
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/1-界面编辑器的使用.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 界面编辑器的使用
|
||||
|
||||
使用我的世界开发者工作台的界面编辑器功能,可以轻松地对界面进行设计。在本节课程中,将会教你如何制作一个简易的计分板界面。
|
||||
|
||||
要使用界面编辑器,首先应该创建一个空白附加包项目。切换到基岩版组件分类,点击新建,选择空白附加包。
|
||||
|
||||
作品名称随意填写,仅用于区分不同的项目,修改完成后启动编辑。
|
||||
|
||||

|
||||
|
||||
启动完成后,需要点击左上角的**界面**按钮,切换到界面编辑器。
|
||||
|
||||
随后点击左侧空间结构的新建,或者底部资源管理的新建按钮,创建一个界面文件。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
文件命名,没有特殊要求,推荐使用`团队名_界面名`,方便管理。
|
||||
|
||||
在这里,因为我们要尝试制作一个计分板模组,所以命名为`test_scoreboard`。
|
||||
|
||||
创建完成后,在默认窗口布局下:
|
||||
|
||||
- 左上角**控件结构**,即这个界面的树形展示区
|
||||
- 左下角**控件库**,在这里可以使用原生的控件或自定义控件,对界面进行设计
|
||||
- 中间为预览区域,可以实时展示正在编辑的界面
|
||||
- 中下为资源管理,可以在需要时查找,使用资源
|
||||
- 右侧为**属性**面板,在预览区域中选中某个控件,属性面板会显示其对应的属性并可以修改
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
原版计分板:
|
||||
|
||||

|
||||
|
||||
要制作一个计分板界面,首先我们可以对其组成进行分析,计分板可以由两个控件构成:
|
||||
|
||||
- 图片
|
||||
- 文本
|
||||
|
||||
|
||||
|
||||
首先新建一个图片控件,设置其父子锚点到右边,修改名称为bottom。修改尺寸X为`最大子控件尺寸X`,尺寸Y为`最大子控件尺寸Y`。
|
||||
|
||||

|
||||
|
||||
这个控件会作为计分板底部的颜色较浅的区域,可以设置其使用贴图为原生图片`textures/ui/white_background.png`,勾选填充,并修改颜色和透明度。具体参数可以自行根据喜好调整。
|
||||
|
||||

|
||||
|
||||
接下来,我们需要向这个名为bottom的图片控件内添加一个文本,用来显示计分板的文本内容。
|
||||
|
||||
`尺寸X`和`尺寸Y`修改为适应,这样尺寸大小就会根据文字内容动态调整。
|
||||
|
||||

|
||||
|
||||
为了还原原版计分板,可以将`对齐`改为左,内容可以先随意填写几行文字,用来查看效果。
|
||||
|
||||

|
||||
|
||||
接下来开始制作计分板的标题,在`bottom`控件下,新增一个图片控件,命名为`title`。
|
||||
|
||||
父锚点设置为上,子锚点设置为下。这样这个图片控件就会显示在`bottom`控件的上方。
|
||||
|
||||
此外,标题的`尺寸X`应该和文本控件一样大,所以设置`尺寸X`为最大兄弟控件尺寸X,即文本控件的尺寸X。
|
||||
|
||||
`尺寸Y`设置为子控件尺寸Y的100%,根据标题高度动态调整大小。
|
||||
|
||||

|
||||
|
||||
使用贴图同样适用纯色贴图`textures/ui/white_background.png`,同时标题应该颜色稍深一点,透明度和颜色可以根据自己的感觉调整。
|
||||
|
||||

|
||||
|
||||
接下来,继续在`title`控件内添加一个文本控件,同样使用适应尺寸X,适应尺寸Y。
|
||||
|
||||
完成后,结构控件和效果如下图所示。
|
||||
|
||||

|
||||
|
||||
如果需要在界面上添加其它控件都是大同小异的。
|
||||
|
||||
**要更深入的了解界面编辑器的其他用法,可以参考[官方教程](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/1-%E7%95%8C%E9%9D%A2%E7%BC%96%E8%BE%91%E5%99%A8%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E.html?catalog=1)。**
|
||||
|
||||
360
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/2-界面逻辑的编写.md
Normal file
360
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/2-界面逻辑的编写.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 界面逻辑的编写
|
||||
|
||||
## 注册界面
|
||||
|
||||
在上一节中,我们使用界面编辑器简单制作了一个计分板界面。但是它的内容目前还是固定的,想要让它动起来,就需要编写界面逻辑的代码。
|
||||
|
||||
在[第一节](./0-创建界面的两种方式.md)中,已经介绍过,创建UI有两种方式,分别是CreateUI和PushScreen。根据计分板的这种界面的特性,我们应该使用CreateUI方式来创建Hud界面。
|
||||
|
||||
使用开发者工作台创建的模板插件默认包含一个客户端的界面注册管理类,我们可以先新建一个插件,命名为`testScoreboard`,并将其剪切到服务器模组目录,删除`developer_mods`文件。
|
||||
|
||||
然后找到上一节编辑的附加包项目,右键打开目录。
|
||||
|
||||
将`resource_pack`文件夹中的ui文件夹里的内容,复制到`testScoreboard\resource_packs\testScoreboardResource\ui`中。这里都是我们在附加包中编辑的界面文件的相关资源。
|
||||
|
||||
- _ui_defs.json:资源包中所使用的ui文件的定义,引用到的界面都需要在这个文件里定义
|
||||
- netease_editor_template_namespace.json: 网易编辑器模板文件,必须保留
|
||||
- test_scoreboard.json:上一节编辑的计分板界面文件
|
||||
|
||||

|
||||
|
||||
接下来使用IDE打开这个模组文件夹,观察代码。在客户端初始化的时候,实例化了uiMgr.UIMgr。
|
||||
|
||||
```python
|
||||
def __init__(self, namespace, systemName):
|
||||
ClientSystem.__init__(self, namespace, systemName)
|
||||
self.mUIMgr = uiMgr.UIMgr()
|
||||
```
|
||||
|
||||
观察UIMgr的代码。可以发现,只要我们调用Init方法,并传入ClientSystem,就会自动帮我们注册uiDef中的所有界面。
|
||||
|
||||
```python
|
||||
class UIMgr(object):
|
||||
def __init__(self):
|
||||
super(UIMgr, self).__init__()
|
||||
self.mUIDict = {}
|
||||
self.mClientSystem = None
|
||||
|
||||
def Destroy(self):
|
||||
pass
|
||||
|
||||
def Init(self, system):
|
||||
self.mClientSystem = system
|
||||
for uiKey, config in uiDef.UIData.iteritems():
|
||||
self.InitSingleUI(uiKey, config)
|
||||
|
||||
def InitSingleUI(self, uiKey, config):
|
||||
cls, screen = config["cls"], config["screen"]
|
||||
extraClientApi.RegisterUI(ModName, uiKey, cls, screen)
|
||||
extraParam = {}
|
||||
if config.has_key("isHud"):
|
||||
extraParam["isHud"] = config["isHud"]
|
||||
ui = extraClientApi.CreateUI(ModName, uiKey, extraParam)
|
||||
if not ui:
|
||||
print "InitSingleUI %s fail" % uiKey
|
||||
return
|
||||
if config.has_key("layer"):
|
||||
ui.GetBaseUIControl("").SetLayer(config["layer"])
|
||||
self.mUIDict[uiKey] = ui
|
||||
|
||||
def GetUI(self, uiKey):
|
||||
return self.mUIDict.get(uiKey, None)
|
||||
|
||||
def RemoveUI(self, uiKey):
|
||||
ui = self.mUIDict.get(uiKey, None)
|
||||
if ui:
|
||||
del self.mUIDict[uiKey]
|
||||
ui.SetRemove()
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
注册界面的流程是:
|
||||
|
||||
1. `extraClientApi.RegisterUI(ModName, uiKey, cls, screen)`
|
||||
2. `extraClientApi.CreateUI(ModName, uiKey, extraParam)`
|
||||
|
||||
即注册了界面之后,直接调用了CreateUI方法,使用CreateUI的方式创建了界面。
|
||||
|
||||
如果我们后期需要使用PushScreen界面,也可以对这里的代码进行简单的修改,让它RegisterUI后直接返回。后续我们需要创建界面的时候,直接调用PushScreen方法。
|
||||
|
||||
因此我们可以提前编辑好uiDef,并且在UI初始化完成后,调用Init方法,即可完成对界面的注册。
|
||||
|
||||
UIDef中主要定义了几个参数
|
||||
|
||||
- cls:逻辑代码类的路径
|
||||
- screen:界面json的命名空间和画布,画布默认为main
|
||||
- layer:注册ui后,ui所在的显示层级
|
||||
- isHud:是否为Hud
|
||||
|
||||
我们这里可以根据需要修改,修改screen的定义,删除layer:
|
||||
|
||||
```python
|
||||
UIDef.ScoreboardScreen : {
|
||||
"cls":"testScoreboardScript.ui.test_scoreboard_screen.ScoreboardScreen",
|
||||
"screen":"test_scoreboard.main",
|
||||
"isHud":1
|
||||
}
|
||||
```
|
||||
|
||||
接下来在ClientSystem中,UI初始化完毕的事件中,调用Init方法,注册界面。
|
||||
|
||||
```python
|
||||
def OnUiInitFinished(self, args):
|
||||
logger.info("%s OnUiInitFinished", ScoreboardConst.ClientSystemName)
|
||||
self.mUIMgr.Init(self)
|
||||
```
|
||||
|
||||
UI注册到此完毕,如果这时进入游戏,将可以看到静态的UI会被显示在游戏中。
|
||||
|
||||
想要让它根据需要动起来,就需要编写界面逻辑,也就是之前所提到的uiDef中cls对应的类。接下来将介绍界面逻辑的两种方式:
|
||||
|
||||
- 使用模组SDK
|
||||
- 使用数据绑定
|
||||
|
||||
## 使用模组SDK
|
||||
|
||||
找到`test_scoreboard_screen.py`文件,可以看到,它是一个继承了ScreenNode的类。
|
||||
|
||||
所有UI逻辑相关的代码都需要继承ScreenNode,并且基于界面的操作的很多方法,都在ScreenNode的成员方法中。[文档链接](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html#screennode)
|
||||
|
||||
其中最为常用的方法便是[GetBaseUIControl](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html#getbaseuicontrol),它可以用来根据路径获取BaseUIControl示例,从而对界面控件进行修改。
|
||||
|
||||
对UI中的内容进行操作的一般步骤如下:
|
||||
|
||||
- 定义各个控件的路径
|
||||
- 通过GetBaseUIControl来获取各个控件的实例
|
||||
- 根据控件的不同类型,调用[asXXX](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html#getbaseuicontrol),转换为具体的实例
|
||||
- 根据要求设置数据,例如修改文本、图片等
|
||||
|
||||
拿我们要制作的计分板UI来说,我们主要需要对两个地方进行设置:标题、内容。
|
||||
|
||||
因此我们可以先定义好两个控件的路径。
|
||||
|
||||
```python
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
print '==== %s ====' % 'init ScoreboardScreen'
|
||||
self.mBottom = "/bottom"
|
||||
self.mBottomLabel = self.mBottom + "/label"
|
||||
self.mTitle = self.mBottom + "/title"
|
||||
self.mTitleLabel = self.mTitle + "/label"
|
||||
```
|
||||
|
||||
接下来可以编写一个方法,用来设置计分板的标题和内容。
|
||||
|
||||
要设置文本内容,首先应该先通过GetBaseUIControl获取实例,再asLabel,获取文本的控件实例。
|
||||
|
||||
```python
|
||||
self.GetBaseUIControl(self.mBottomLabel)
|
||||
```
|
||||
|
||||
接下来查阅[文档](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E6%8E%A7%E4%BB%B6.html?catalog=1#labeluicontrol),找到LabelUIControl的方法,可以看到使用SetText来设置文本内容。
|
||||
|
||||
完整的方法如下:
|
||||
|
||||
```python
|
||||
def SetText(self, title, content):
|
||||
self.GetBaseUIControl(self.mBottomLabel).asLabel().SetText(content)
|
||||
self.GetBaseUIControl(self.mTitleLabel).asLabel().SetText(title)
|
||||
```
|
||||
|
||||
这样就完成了文本的设置,后续如果需要在服务器上同步计分板信息给玩家,只需要客户端监听来自服务端的事件,并且每次收到事件时调用这个方法即可。
|
||||
|
||||
|
||||
|
||||
我们也可以在Create方法中,调用这个函数,来测试修改是否生效。
|
||||
|
||||
> Create方法是ScreenNode的生命周期之一,它会在UI界面被创建完成的时候被调用,这时可以获取控件并修改控件属性了。
|
||||
>
|
||||
> 其他生命周期可以参考[文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/30-UI%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.html?docindex=1&type=0#%E7%95%8C%E9%9D%A2%E5%88%9B%E5%BB%BA%E6%B5%81%E7%A8%8B%E5%8F%8A%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F)
|
||||
|
||||
参考代码如下:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
|
||||
|
||||
class ScoreboardScreen(ScreenNode):
|
||||
"""
|
||||
Scoreboard
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
print '==== %s ====' % 'init ScoreboardScreen'
|
||||
self.mBottom = "/bottom"
|
||||
self.mBottomLabel = self.mBottom + "/label"
|
||||
self.mTitle = self.mBottom + "/title"
|
||||
self.mTitleLabel = self.mTitle + "/label"
|
||||
|
||||
def SetText(self, title, content):
|
||||
self.GetBaseUIControl(self.mBottomLabel).asLabel().SetText(content)
|
||||
self.GetBaseUIControl(self.mTitleLabel).asLabel().SetText(title)
|
||||
|
||||
# Create函数是继承自ScreenNode,会在UI创建完成后被调用
|
||||
def Create(self):
|
||||
print '==== %s ====' % 'ScoreboardScreen Create'
|
||||
self.SetText("这是一个标题", "第一行\n第二行\n第三行\n第四行\n\n上面有一个空行\n某某某服务器")
|
||||
```
|
||||
|
||||
## 使用数据绑定
|
||||
|
||||
除了使用模组SDK在数据更变时主动调用接口来设置控件数据之外,还可以使用数据绑定的方式,将Python代码和界面某个属性进行绑定。
|
||||
|
||||
数据绑定[说明文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/70-UI%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A.html?catalog=1)
|
||||
|
||||
我们可以先将之前编写的使用模组SDK方式来修改界面的`test_scoreboard_screen.py`文件复制一份保留,然后再在原文件上做修改。
|
||||
|
||||
### Json文件修改
|
||||
|
||||
首先我们需要明确,需要绑定修改数据的控件,分别是`/bottom/label`和`/bottom/title/label`。
|
||||
|
||||
打开ui文件`resource_packs/ui/test_scoreboard.json`,找到两个文本控件。
|
||||
|
||||
例如下方截图的是bottom控件下的label控件,即路径为`/buttom/label`的控件。
|
||||
|
||||

|
||||
|
||||
修改字段:
|
||||
|
||||
```json
|
||||
"bindings" : [
|
||||
{
|
||||
"binding_condition" : "always_when_visible",
|
||||
"binding_name" : "#testScoreboard.content",
|
||||
"binding_name_override" : "#testScoreboard.content"
|
||||
}
|
||||
],
|
||||
"text" : "#testScoreboard.content"
|
||||
```
|
||||
|
||||
根据[UI说明文档](https://mc.163.com/dev/mcmanual/mc-dev/mcguide/18-%E7%95%8C%E9%9D%A2%E4%B8%8E%E4%BA%A4%E4%BA%92/30-UI%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.html?catalog=1#label),可以查阅到text字段的值对应了label控件显示的文本,因此我们可以给它创建一个绑定,名为`#testScoreboard.content`,并把这个值赋值给text字段。
|
||||
|
||||
同样的,继续修改`/bottom/title/label`控件,也给它添加一个绑定,名为`#testScoreboard.title`。最终修改后的JSON文件如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"main" : {
|
||||
"controls" : [
|
||||
{
|
||||
"bottom" : {
|
||||
"alpha" : 0.80,
|
||||
"anchor_from" : "right_middle",
|
||||
"anchor_to" : "right_middle",
|
||||
"clip_direction" : "left",
|
||||
"clip_ratio" : 0.0,
|
||||
"color" : [ 0.1686274509803922, 0.1686274509803922, 0.1686274509803922 ],
|
||||
"controls" : [
|
||||
{
|
||||
"label" : {
|
||||
"font_type" : "default",
|
||||
"layer" : 1,
|
||||
"text" : "#testScoreboard.content",
|
||||
"type" : "label",
|
||||
"bindings" : [
|
||||
{
|
||||
"binding_condition" : "always_when_visible",
|
||||
"binding_name" : "#testScoreboard.content",
|
||||
"binding_name_override" : "#testScoreboard.content"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title" : {
|
||||
"alpha" : 0.90,
|
||||
"anchor_from" : "top_middle",
|
||||
"anchor_to" : "bottom_middle",
|
||||
"clip_direction" : "left",
|
||||
"clip_ratio" : 0.0,
|
||||
"color" : [ 0.1372549019607843, 0.1372549019607843, 0.1372549019607843 ],
|
||||
"controls" : [
|
||||
{
|
||||
"label" : {
|
||||
"font_type" : "default",
|
||||
"layer" : 1,
|
||||
"text" : "#testScoreboard.title",
|
||||
"text_alignment" : "center",
|
||||
"type" : "label",
|
||||
"bindings" : [
|
||||
{
|
||||
"binding_condition" : "always_when_visible",
|
||||
"binding_name" : "#testScoreboard.title",
|
||||
"binding_name_override" : "#testScoreboard.title"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"fill" : true,
|
||||
"layer" : 2,
|
||||
"size" : [ "100.0%sm+0.0px", "100.0%c+0.0px" ],
|
||||
"texture" : "textures/ui/white_background",
|
||||
"type" : "image"
|
||||
}
|
||||
}
|
||||
],
|
||||
"fill" : true,
|
||||
"layer" : 1,
|
||||
"size" : [ "100.0%cm+0.0px", "100.0%cm+0.0px" ],
|
||||
"texture" : "textures/ui/white_background",
|
||||
"type" : "image"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type" : "screen"
|
||||
},
|
||||
"namespace" : "test_scoreboard"
|
||||
}
|
||||
```
|
||||
|
||||
### Python文件修改
|
||||
|
||||
我们可以直接在ScreenNode中定义两个变量,分别将其和`#testScoreboard.title`、`#testScoreboard.content`进行绑定。
|
||||
|
||||
具体代码如下:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
ViewBinder = clientApi.GetViewBinderCls()
|
||||
|
||||
|
||||
class ScoreboardScreen(ScreenNode):
|
||||
"""
|
||||
Scoreboard
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
print '==== %s ====' % 'init ScoreboardScreen'
|
||||
self.mContent = "第一行\n第二行\n第三行\n第四行\n\n上面有一个空行\n某某某服务器"
|
||||
self.mTitle = "这是一个标题"
|
||||
|
||||
@ViewBinder.binding(ViewBinder.BF_BindString, "#testScoreboard.title")
|
||||
def ReturnTitleText(self):
|
||||
return self.mTitle
|
||||
|
||||
@ViewBinder.binding(ViewBinder.BF_BindString, "#testScoreboard.content")
|
||||
def ReturnContentText(self):
|
||||
return self.mContent
|
||||
```
|
||||
|
||||
通过给方法添加`@ViewBinder.binding`注解,可以给函数设置数据绑定。其中第一个参数为数据类型,第二个参数为绑定名。要更新数据时,直接修改变量的值即可。
|
||||
|
||||
除此之外还可以对其他类型的数据进行绑定,可以参考文档深入了解。
|
||||
|
||||
## 测试
|
||||
|
||||
部署到服务器进行测试:
|
||||
|
||||

|
||||
|
||||
可以看到实现了原版计分板的效果。
|
||||
|
||||
229
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/3-作业.md
Normal file
229
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/3-作业.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# 作业
|
||||
|
||||
<iframe src="https://cc.163.com/act/m/daily/iframeplayer/?id=64818d47c31a9c0f360dc5da" width="800" height="600" allow="fullscreen"/>
|
||||
|
||||
要求:依赖客户端Mod制作一个简易的菜单插件。
|
||||
|
||||
- 进入游戏自动生成一个在Hud界面上的菜单
|
||||
- 服务器可以配置每个菜单按钮:
|
||||
- 索引位置
|
||||
- 图片、文本
|
||||
- 点击后以玩家身份执行指令
|
||||
|
||||
示例图:
|
||||
|
||||

|
||||
|
||||
## Spigot插件
|
||||
|
||||
Spigot插件的逻辑较为简单,在玩家UI加载完成之后,向玩家客户端发送提前加载好的菜单配置文件即可。
|
||||
|
||||
配置文件格式:
|
||||
|
||||
```yaml
|
||||
button1:
|
||||
idx: 1
|
||||
# 该按钮在菜单中的位置
|
||||
texture: 'textures/items/apple'
|
||||
# 该按钮显示的贴图
|
||||
text: '按钮1'
|
||||
# 该按钮下方的提示文本
|
||||
commands:
|
||||
- 'say 点击了按钮1'
|
||||
# 点击后执行的玩家指令
|
||||
button2:
|
||||
idx: 2
|
||||
texture: 'textures/items/diamond_sword'
|
||||
text: '按钮2'
|
||||
commands:
|
||||
- 'say 点击了按钮2'
|
||||
```
|
||||
|
||||
读取配置文件的代码不过多介绍,需要参考可以下载源码进行查看。
|
||||
|
||||
主类中包含了与客户端通讯的关键代码,供参考:
|
||||
|
||||
```java
|
||||
package me.zhanshi123.tutorialmenu;
|
||||
|
||||
import com.neteasemc.spigotmaster.SpigotMaster;
|
||||
import me.zhanshi123.tutorialmenu.config.ConfigManager;
|
||||
import me.zhanshi123.tutorialmenu.config.MenuButtonConfig;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class TutorialMenu extends JavaPlugin {
|
||||
private static TutorialMenu instance;
|
||||
private SpigotMaster spigotMaster;
|
||||
|
||||
private final String NAMESPACE = "testMenu";
|
||||
private final String SERVER_SYSTEM_NAME = "testMenuDev";
|
||||
private final String CLIENT_SYSTEM_NAME = "testMenuBeh";
|
||||
private final String CLIENT_UI_LOADED_EVENT = "ClientUiLoadedEvent";
|
||||
private final String SERVER_MENU_EVENT = "ServerMenuEvent";
|
||||
private final String CLIENT_MENU_CLICKED_EVENT = "ClientMenuClickedEvent";
|
||||
|
||||
|
||||
public static TutorialMenu getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
ConfigManager.getInstance().loadConfig();
|
||||
spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
|
||||
spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_UI_LOADED_EVENT, (player, map) ->
|
||||
spigotMaster.notifyToClient(player, NAMESPACE, SERVER_SYSTEM_NAME, SERVER_MENU_EVENT, ConfigManager.getInstance().getClientData()));
|
||||
spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_MENU_CLICKED_EVENT, (player, map) -> {
|
||||
int index = (int) map.get("index");
|
||||
MenuButtonConfig menuButtonConfig = ConfigManager.getInstance().getMenuConfigs().get(index);
|
||||
if (menuButtonConfig == null) {
|
||||
getLogger().warning("玩家 " + player.getName() + " 发送了一个不正确的菜单数据");
|
||||
return;
|
||||
}
|
||||
menuButtonConfig.dispatchCommand(player);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 客户端模组
|
||||
|
||||
创建一个命名空间为testMenu的插件,删除`developer_mods`文件夹之后,就可以先创建一个空白附加包,开始编辑菜单界面。
|
||||
|
||||
### 界面编辑
|
||||
|
||||
对于这种有序排列的界面,我们可以直接使用布局面板来自动给按钮排版。
|
||||
|
||||
1. 新建一个布局面板,将其锚点都设置在左上方,并设置`尺寸X`为适应,修改排列方式为水平排布。
|
||||
|
||||
2. 新建一个面板,命名为`btn_tpl`,作为按钮的模板。自行调整其尺寸,教程中设置为60x60。
|
||||
|
||||
3. 在`btn_tpl`下新建一个按钮,这就是按钮本体。自行调整其尺寸,教程中设置为40x40。
|
||||
|
||||
4. 在空间结构中展开界面`button`,找到`button_label`,它被用来显示按钮上的文本,将它的父锚点设置到下,子锚点设到上。这样它就会整个显示在按钮图片的下方。
|
||||
|
||||

|
||||
|
||||
5. 最后将`btn_tpl`设置为隐藏。后面我们会把它作为模板来克隆别的按钮,添加到`stack_panel`中。
|
||||
|
||||

|
||||
|
||||
> 除了使用布局面板+克隆模板的方式来制作这种多按钮的界面。
|
||||
>
|
||||
> 还可以使用网格来实现。需要注意的是,网格的内容在Create生命周期的时候,有可能还没有被生成。
|
||||
>
|
||||
> 需要监听[GridComponentSizeChangedClientEvent](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E4%BA%8B%E4%BB%B6/UI.html#gridcomponentsizechangedclientevent),在其数量由0变为其他的时候,才能对网格下的控件进行操作。
|
||||
|
||||
### 界面逻辑
|
||||
|
||||
将刚刚编辑好的文件,复制到对应的客户端资源`testMenu/resource_packs/testMenuResource/ui`处。
|
||||
|
||||
接下来编辑UiDef,修改screen的值为刚刚编辑的json文件。
|
||||
|
||||
```python
|
||||
UIData = {
|
||||
UIDef.MenuScreen: {
|
||||
"cls": "testMenuScript.ui.test_menu_screen.MenuScreen",
|
||||
"screen": "test_menu.main",
|
||||
"isHud": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
客户端界面初始化完成后,加载`UiMgr`,并向服务器通知。
|
||||
|
||||
```python
|
||||
def OnUiInitFinished(self, args):
|
||||
logger.info("%s OnUiInitFinished", MenuConst.ClientSystemName)
|
||||
self.mUIMgr.Init(self)
|
||||
self.NotifyToServer("ClientUiLoadedEvent", {})
|
||||
```
|
||||
|
||||
监听来自服务器的`ServerMenuEvent`事件,并通过`mUIMgr`获取`ScreenNode`实例,调用`SetData`方法来设置来自服务端的数据。
|
||||
|
||||
```python
|
||||
def __init__(self, namespace, systemName):
|
||||
ClientSystem.__init__(self, namespace, systemName)
|
||||
self.mUIMgr = uiMgr.UIMgr()
|
||||
|
||||
self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), MenuConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
|
||||
self.ListenForEvent(MenuConst.ModName, MenuConst.ServerSystemName, "ServerMenuEvent", self, self.OnServerMenu)
|
||||
|
||||
def OnServerMenu(self, args):
|
||||
print "OnServerMenu", args
|
||||
if self.mUIMgr:
|
||||
self.mUIMgr.GetUI(UIDef.MenuScreen).SetData(args["data"])
|
||||
else:
|
||||
print "UIMgr还没有加载!"
|
||||
```
|
||||
|
||||
编写MenuScreen的`SetData`方法,通过传入的数据来克隆按钮。并给按钮添加回调。
|
||||
|
||||
[Clone](https://mc.163.com/dev/mcmanual/mc-dev/mcdocs/1-ModAPI/%E6%8E%A5%E5%8F%A3/%E8%87%AA%E5%AE%9A%E4%B9%89UI/UI%E7%95%8C%E9%9D%A2.html?key=Clone&docindex=2&type=0#clone)具体参数见文档。
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
import mod.client.extraClientApi as clientApi
|
||||
|
||||
from testMenuScript.menuConst import ModName, ClientSystemName
|
||||
|
||||
ScreenNode = clientApi.GetScreenNodeCls()
|
||||
|
||||
|
||||
class MenuScreen(ScreenNode):
|
||||
"""
|
||||
Menu
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name, param):
|
||||
ScreenNode.__init__(self, namespace, name, param)
|
||||
self.mStackPanel = "/stack_panel"
|
||||
self.mTemplate = "/btn_tpl"
|
||||
print '==== %s ====' % 'init MenuScreen'
|
||||
|
||||
# Create函数是继承自ScreenNode,会在UI创建完成后被调用
|
||||
def Create(self):
|
||||
print '==== %s ====' % 'MenuScreen Create'
|
||||
|
||||
def OnBtnClick(self, args):
|
||||
print args["ButtonPath"]
|
||||
path = args["ButtonPath"] # 路径为 /stack_panel/btn_x/button
|
||||
buttonId = int(path[path.rindex("_") + 1:path.index("/button")]) # 截取路径中的x,它就是按钮的索引
|
||||
clientApi.GetSystem(ModName, ClientSystemName).NotifyToServer("ClientMenuClickedEvent", {"index": buttonId}) # 发送给服务器
|
||||
|
||||
def SetData(self, data):
|
||||
data = sorted(data, key=lambda x: x["index"])
|
||||
# 对按钮顺序进行排序
|
||||
for btn in data:
|
||||
newName = "btn_{}".format(btn["index"]) # 把按钮对应的index存在路径中,后面按钮点击时,根据路径判断是哪个按钮
|
||||
self.Clone(self.mTemplate, self.mStackPanel, newName)
|
||||
newPath = self.mStackPanel + "/" + newName
|
||||
self.GetBaseUIControl(newPath).SetVisible(True) # 设置可见
|
||||
newPath += "/button"
|
||||
btnControl = self.GetBaseUIControl(newPath).asButton()
|
||||
btnControl.AddTouchEventParams({"isSwallow": True}) # 先开启按钮回调功能
|
||||
btnControl.SetButtonTouchUpCallback(self.OnBtnClick) # 再设置按钮回调函数
|
||||
# 按钮的贴图有3个,分别对应默认、按下、悬浮。这里三个都设置。
|
||||
self.GetBaseUIControl(newPath + "/default").asImage().SetSprite(btn["texture"])
|
||||
self.GetBaseUIControl(newPath + "/pressed").asImage().SetSprite(btn["texture"])
|
||||
self.GetBaseUIControl(newPath + "/hover").asImage().SetSprite(btn["texture"])
|
||||
self.GetBaseUIControl(newPath + "/button_label").asLabel().SetText(btn["text"]) # 设置按钮下的文本
|
||||
|
||||
```
|
||||
|
||||
## 效果展示
|
||||
|
||||

|
||||
|
||||
## 代码下载
|
||||
|
||||
Spigot插件:[点我](https://g79.gdl.netease.com/TutorialMenu-Spigot.zip)
|
||||
|
||||
客户端模组:[点我](https://g79.gdl.netease.com/testMenu-Python.zip)
|
||||
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/01.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/01.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/02.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/02.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/03.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/03.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/04.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/04.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/05.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/05.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/06.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/06.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/07.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/07.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/08.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/08.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/09.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/09.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/10.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/10.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/11.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/11.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/12.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/12.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/13.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/13.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/14.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/14.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/15.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/15.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/16.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/16.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/17.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/17.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/18.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/18.png
LFS
Normal file
Binary file not shown.
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/19.png
LFS
Normal file
BIN
docs/mconline/60-我的世界创造营教程/网络游戏开服教程/2-客户端界面的制作/images/19.png
LFS
Normal file
Binary file not shown.
Reference in New Issue
Block a user