V0.34.0.0 Release Note
注意事项
1. 遮罩按钮透明度可能发生变化
在之前的版本上,UI 遮罩按钮控件内部,如果底图和遮罩图使用的是半透明的 UI 贴图资源,底图和遮罩图的 alpha 值计算有问题,会导致其表现为比原始 UI 贴图偏透明;在 034 版本上修复了该 bug,显示为正确的透明度,这可能导致游戏中使用的遮罩按钮透明度发生变化。
2. 角色插槽下的挂件将依据可见性规则隐藏
在之前的版本中,隐藏角色时,角色插槽下的所有挂件都会一起被隐藏(不考虑可见性)
034之后,隐藏角色时,会先判断挂件的可见性,再根据可见性规则进行隐藏
3. 由于角色逻辑修复,可能导致项目逻辑产生变化
034上针对角色修复了部分的Bug。其中有10 个 BUG 的修复可能导致项目效果出现不同并且需要调整,若工程升级后在角色方面出现表现不一致,各位可以查看034版本角色变动汇总快速定位问题和解决问题。
新增功能
一、编辑器交互及发版策略
[新增]编辑器协作账号快捷切换
为了方便大家使用协作账号功能来进行资源的共享等操作,本次更新我们新增了一个快速切换协作账号的入口。
在编辑器 - 菜单 下新增快速切换账号功能:
编辑器工程菜单栏新增“账号”栏,可以查看当前登录的账号,并且当存在团队协作账号时,可以点击账号图标,进入协作账号切换界面
[优化]脚本组件化优化
- 脚本提供快捷编辑入口
- 在属性面板的脚本组件上新增了“编辑”按钮,点击按钮后将打开编程工具。
- 脚本组件内 Icon 优化
- 脚本组件内的数组类自定义属性,“添加”按钮移除了中文文本,“移除”样式改为和“+”号相对应的样式。
- 脚本组件支持复制和粘贴功能,可以右键脚本组件分栏-选择“复制”按钮后,再右键脚本组件分栏-选择“粘贴”按钮,进行复制粘贴,粘贴将创建一个新的脚本组件进行挂载。
注意
复制和粘贴的时候需要右键脚本组件分栏(上图的DefaultUI位置)
[优化]属性面板 UX&UI 优化
旧版本属性面板 | 新版本属性面板 |
- 优化了主编辑器和 UI 编辑器中属性面板的整体界面布局,将名称、网络状态、标签信息和搜索等信息提至属性面板最上方。
- 提供了一键展开与折叠的功能按钮。
- 将属性的占用空间从 2 行缩减至 1 行,大幅度缩减了行与行间不必要的间隔。
旧版本Transform栏 | 新版本Transform栏 |
- 空锚点-锚点偏移中“自动居中”和“顶点吸附”,因是功能不是属性,不符合属性面板统一性,所以从属性面板中移除,后续将加入进锚点工具优化中。
[优化]编辑器对象属性值增量写入
编辑器下属性面板属性值修改为增量写入,level 文件仅保持修改过的属性值。
[优化]KV 数据存储频率限制优化
原来的KV限制:
- 60s内,所有房间对单个Key的访问频率限制为60+房间玩家数量上限x10次,比如房间玩家数量上限为5,则每分钟单个Key的访问次数上限为60+5x10=110次,不区分来源,所有房间共用访问次数。
优化后的KV限制:
- 60s内,每个房间对单个Key的访问频率限制为60+房间玩家数量上限x10次,比如房间玩家数量上限为5,则每分钟每个房间对单个Key的访问次数上限为60+5x10=110次,每个房间单独计算访问次数。
注意事项:
访问次数为 获取(get)、改写(set)、删除(remove)的总次数
二、性能相关优化
[优化]服务端广播事件在客户端断线 1.5s 后不再发送
当服务器检测到某客户端断线 1.5s 时,事件广播Event.dispatchToAllClient()
以及Event.dispatchToAllClientUnreliable()
均不再向断线客户端发送消息,减轻网络通信压力。
三、UI 编辑器新增功能及 API 更新
[新增]UI 控件-列表视图和瓦片视图
- 列表视图是可以显示大量条目的虚拟列表,相比于简单的使用滚动框结合容器布局功能来容纳多行条目,列表视图内仅会创建可见的条目以提升性能。例如一个只有 5 个条目可见的列表视图中容纳了 50 个项目(Item),并不会真的创建 50 个项目的 UI,而只会创建当前实际可见的 5 个项目的 UI。推荐列表中要展示较多项目时使用列表视图控件,而不是使用滚动框和容器布局功能。
- 瓦片视图与列表视图相似,区别仅是条目以瓦片集排列。
- 列表视图(ListView)中每个条目固定占据一行/列,沿着滚动条方向填充
- 瓦片视图(TileView)会把每一行/列填充满之后才会沿着滚动条方向填充
- 详情请见产品手册:列表视图和瓦片试图 | 产品手册
[新增]文本框新增最大最小字体大小属性,用于限制开启自适应文本框时的字号范围
此前已上线的【自适应文本框】功能作用是根据文本内容和文本框尺寸计算出将文字尽量填满的合适字号,考虑到游戏项目在不同语言的本地化处理后,文本长度会有变化,这种情况可以开启【自适应文本框】功能让翻译后的文本不超框。但是实际使用中会出现同一批文本框中某些文本字数较少,从而算出的自适应字号过大的情况,例如:
- 新增功能:
- 文本框新增【最大字体大小】和【最小字体大小】两条属性;
- 开启【自适应文本框】时,如果计算出的自适应字号大于【最大字体大小】,则采用最大字体大小;如果计算出的自适应字号小于【最小字体大小】,则采用最小字体大小;如果计算出的自适应字号介于两者之间,就采用计算的自适应字号;
- 不开启【自适应文本框】时,文本字体由属性【字号大小】决定,与【最大字体大小】和【最小字体大小】的值无关;
- 【自适应文本框】自动计算出的字号已是将文本框填满的最大字号,如果游戏运行中动态设置文本导致应用到了【最小字体大小】,就意味着【最小字体大小】大于了自适应字号,文本会有较大概率超框,这时编辑器会给出报错提示;
注意
【最小字体大小】虽然可以自行设置,但是其作用主要是检查项目中的文本翻译为不同语言后是否存在字号过小的情况。如出现这种情况,建议精简翻译后的文案内容,或者调整文本框尺寸。
推荐用法:
开启自适应文本框后,同一批尺寸相同的文本框可以统一设定一个【最大字体大小】,让文本字数较少时,不再采用偏大的自适应字号。
设置最大字体大小前 | 设置最大字体大小后 |
也可以在脚本中使用 API 设置这两条属性:
属性名称 | 英文名称 | 类型 | 默认值 | 取值范围 | 说明 | 读写 | 编辑器 | 分组 |
---|---|---|---|---|---|---|---|---|
最大字号大小 | maxSize | number | 1000 | 0-1000 | 当开启自适应文本框时,如果计算出的自适应字号大于最大字体大小,则采用最大字体大小 | Read / Write | Visible | UI 编辑器-TextBlock-文本 |
最小字号大小 | minSize | number | 0 | 0-1000 | 当开启自适应文本框时,如果计算出的自适应字号小于最小字体大小,则采用最小字体大小 | Read / Write | Visible | UI 编辑器-TextBlock-文本 |
在034版本还修复了部分【自适应文本框】计算不准确导致文本没有填满框或超框的情况:
- 优化会导致相同情况下与033相比计算出的自适应字号大小不同,假设原本开启自适应后计算的字号偏小,不会填满文本框,如果手动调整文本框的大小超过了另一张底板图片的范围,采用新算法后,文本字号变大填满文本框,虽然这符合【自适应文本框】让文本填满文本框的目的,但是可能会导致033没有超框的文本在034超出底板范围。
此外,在034版本还新增了针对未开启【自适应文本框】的文本超框情况的警告提示:
- 在不开启自适应文本框时,每次新创建文本框,或修改到与之前不同文本内容时(包括使用本地化功能自动替换文本)都会检测文本内容是否超出了文本框范围,超框的话会给出警告提示。实际游戏中不会进行这一检测,只会在电脑端做这个检测并给出警告;并且只会检测在脚本中能获取到的文本框。
- 需要说明的是,出现警告并不意味着实际表现一定会超框,如果文本内容恰好填满了文本框,那么在一些分辨率下有超框的风险,所以这种情况还是会给出警告,便于检查。建议尽量避免这种恰好填满的情况,文本框留出一定空间。
- 适配多语言的自适应文本框的用法提示:
- 如果对于不同语种下文本显示效果要求较高,例如希望同一批尺寸相同的文本框内,字号大小能保持完全统一——则不应开启【自适应文本框】,而是需要自行选择一个合适的固定字号,并且逐一检查每一处文本各种语言的翻译后文案不超框。
- 如果不希望在不同语种下文本显示效果投入过多精力,能接受字号大小不统一——可以保持文本框的【自适应文本框】属性为打开状态,并且设定【最大字体大小】,防止文本较少时字体过大。
- 开启【自适应文本框】时,推荐容纳单行短文本的文本框尽可能使用高度较短,宽度较长的形状,这能保证大部分单行文本的字号一致,更加美观;即使翻译出超长文本也不会超框,不过还是要尽量避免这种情况,同一批文本框中的文本长度不应相差过多。
四、编辑器新增功能及 API 更新
[优化]多人跳转优化
传入的 userIds 对应的当前在线的玩家,在传送前可以分布在同一个游戏的不同房间内(可以是同一个游戏下的不同的场景)
TIP
锁房、不同房间指定 userid 跳转场景的功能只在移动端生效,电脑端暂不支持模拟多房间的情况
多人跳转时,若传入的 userid 数组中存在无法跳转的 id,仅无法跳转的 userid 不跳转,其余 userid 继续跳转房间。
- TeleportService.aysncTeleportToxxxx 接口的 TeleportOption 新增了参数
createNewPrivateRoom
:createNewPrivateRoom = true 时会将参数中的 userIds 对应的玩家传送到一个新创建的私有的房间中,该房间无法通过其他方式进入;createNewPrivateRoom = false 与之前的逻辑一致,将玩家传送到一个公开的房间
示例:
TypeScript
//异步传送到当前游戏的test场景,userIds是需要进行传送的玩家,createNewPrivateRoom创建新房间,并锁房。
TeleportService.asyncTeleportToScene("test", userIds, { createNewPrivateRoom: true })
//异步传送到当前游戏的test场景,userIds是需要进行传送的玩家,createNewPrivateRoom创建新房间,并锁房。
TeleportService.asyncTeleportToScene("test", userIds, { createNewPrivateRoom: true })
[新增]摄像机预览窗口
- 为了便于实时预览摄像机效果,选中对象管理器中的逻辑对象-摄像机时,会把该摄像机预览的画面展示在主视口右下方的摄像机预览窗口内;摄像机预览窗口可以同时显示多个,并且支持锁定;
- 由于未启动游戏时主视口内不会展示显示世界对象-摄像机(也就是角色身上的默认摄像机),所以世界对象-摄像机无法使用该预览功能。
[新增]工程内容新增数据目录
以前不能在游戏运行时读取本地静态数据,现在可以利用该功能作为配置文件使用。
数据文件可以在“工程内容”中的“数据文件”中新建:
API:
DataFile 类新增
功能说明 | 方法名 | 返回类型 | 输入说明 | 输出说明 | 使用域 |
---|---|---|---|---|---|
读取配置 | asyncLoad(fileName: string ): Promise string | string | fileName配置文件的文件名 | 配置文件的字符内容 | Client And Server |
判断配置是否存在 | exists(fileName: string): bool | boolean | fileName配置文件的文件名 | 配置文件是否存在 | Client And Server |
TypeScript
if(DataFile.exists("level2_3"))
{
let levelData = await DataFile.asyncLoad("level2_3");
}
if(DataFile.exists("level2_3"))
{
let levelData = await DataFile.asyncLoad("level2_3");
}
注意事项:
配置文件要求
编码格式:UTF-8
文件名称:"a-z"、"A-Z"、"0-9"、"_"
文件后缀:.data
目录容量大小限制
目录容量: ≤ 10MB
[新增]GameObject 支持在自身上扩展自定义属性
GameObject 支持在对象上扩展自定义属性,支持属性同步 & 回调监听。
功能 1:给对象新增/设置自定义属性
setCustomProperty()
给对象新增/设置自定义属性。下列示例展示在 S 端监听客户端消息,给一个立方体对象设置自定义属性。需要传入一个 string 类型的变量propertyName
作为自定义属性的唯一标识。此外还需要传入一个value
变量作为属性值,支持如下类型:
string | number | boolean | Rotation | Vector2 | Vector | Vector4 | LinearColor |
---|
如果当前不存在propertyName
对应的自定义属性,那么会新增自定义属性。
TypeScript
let cube = this.gameObject as Model;
Event.addClientListener("SetProperty", (player: Player, propertyName: string, val: any) => {
cube.setCustomProperty(propertyName, val);
cube.getCustomProperties().forEach((v) => {
console.log("custom property " + v);
});
});
let cube = this.gameObject as Model;
Event.addClientListener("SetProperty", (player: Player, propertyName: string, val: any) => {
cube.setCustomProperty(propertyName, val);
cube.getCustomProperties().forEach((v) => {
console.log("custom property " + v);
});
});
功能 2:获取对象(所有)自定义属性
getCustomProperty()
获取对象自定义属性值,需要传入一个 string 类型的变量propertyName
作为自定义属性的唯一标识,然后接口返回对应的属性值。如果不存在该属性返回 undefined。下列示例展示在设置自定义属性前后分别打印旧值和新值。
TypeScript
// 获取对象自定义属性值
console.error("oldVal " + go.getCustomProperty(propertyName));
go.setCustomProperty(propertyName, val);
console.error("val " + go.getCustomProperty(propertyName));
// 获取对象自定义属性值
console.error("oldVal " + go.getCustomProperty(propertyName));
go.setCustomProperty(propertyName, val);
console.error("val " + go.getCustomProperty(propertyName));
getCustomProperties()
获取对象所有自定义属性,接口返回 string 类型的数组-自定义属性名称列表。下列示例获取对象所有自定义属性后遍历打印:
TypeScript
Event.addClientListener("GetCustomProperties", (player: Player) => {
cube.getCustomProperties().forEach((v) => {
console.log("custom property " + v);
});
});
Event.addClientListener("GetCustomProperties", (player: Player) => {
cube.getCustomProperties().forEach((v) => {
console.log("custom property " + v);
});
});
功能 3:监听对象上所有自定义属性同步回调
onCustomPropertyChange
事件监听对象身上所有自定义属性的属性变化,事件仅客户端生效。事件传出三个参数供大家使用:
- path(当属性为基本类型的时候返回属性名)
- value(属性新值)
- oldValue(属性旧值)
下列示例展示给一个立方体对象的onCustomPropertyChange
事件绑定一个函数,当属性修改并同步至客户端时打印三个参数:
TypeScript
cube.onCustomPropertyChange.add((path, val, oldVal) => {
console.error("onCustomPropertyChange");
console.error("--path " + path + " --val " + val + " --oldVal " + oldVal);
});
cube.onCustomPropertyChange.add((path, val, oldVal) => {
console.error("onCustomPropertyChange");
console.error("--path " + path + " --val " + val + " --oldVal " + oldVal);
});
功能 4:获取某个自定义属性的同步回调委托
getCustomPropertyChangeDelegate
用来获取某个自定义属性的变化委托,需要传入一个 string 类型的变量propertyName
作为自定义属性的唯一标识,然后接口返回一个委托对象,可以往里面绑定自己的函数。下列示例展示获取立方体对象的某个自定义属性变化的委托并清空之前的函数,绑定一个新函数,其中打印自定义属性变化的路径,新值和旧值。
TypeScript
Event.addLocalListener("AddPropertyDelegate", (propertyName: string) => {
let d = cube.getCustomPropertyChangeDelegate(propertyName);
d.clear();
d.add((path, val, oldVal) => {
console.error("getCustomPropertyChangeDelegate");
console.error("--path " + path + " --val " + val + " --oldVal " + oldVal);
});
});
Event.addLocalListener("AddPropertyDelegate", (propertyName: string) => {
let d = cube.getCustomPropertyChangeDelegate(propertyName);
d.clear();
d.add((path, val, oldVal) => {
console.error("getCustomPropertyChangeDelegate");
console.error("--path " + path + " --val " + val + " --oldVal " + oldVal);
});
});
TIP
- 运行时无法移除自定义属性
- 自定义属性功能目前仅在脚本中可以设置,后续会加入在属性面板中调整的功能。
[新增]同步回调冒泡过程支持截断
当自定义类型的变量标记为同步属性后,修改类型变量里某个属性的值,如果该属性也标记同步,那么同步回调将以一个冒泡的顺序按照层级从下往上执行。此时在下级回调函数里调用return true
,则会中断冒泡逻辑。下列示例定义了一个自定义类型CustomData
,其中定义两个同步属性 v 和 s。同步回调函数:分别为onVChanged()
和onSChanged()
。在脚本里面定义一个同步属性data:CustomData
,同步回调函数onDataChanged()
。例如,修改data.v
并在onVChanged()
里面执行 return 逻辑,就可以实现执行onVchanged的同时不会执行onDataChanged,代码如下:
TypeScript
class CustomData {
@Property({replicated: true, onChanged: "onVChanged"})
v: number = 0;
onVChanged(path: string, value: unknown, oldVal: unknown) {
console.error("--path " + path + " --val " + value + " --oldVal " + oldVal);
// 截断冒泡
return true;
}
@Property({replicated: true, onChanged: "onSChanged"})
s: string = "s";
onSChanged(path: string, value: unknown, oldVal: unknown) {
console.error("--path " + path + " --val " + value + " --oldVal " + oldVal);
}
}
@Component
export default class NewScript extends Script {
@Property({replicated: true, onChanged: "onDataChanged"})
data: CustomData = new CustomData();
onDataChanged(path: string, value: unknown, oldVal: unknown) {
console.error("--path " + path + " --val " + value + " --oldVal " + oldVal);
}
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
if(SystemUtil.isClient()) {
this.flag = !this.flag;
InputUtil.onKeyDown(Keys.NumPadOne, () => {
this.serverChangeV();
})
}
}
@RemoteFunction(Server)
serverChangeV() {
this.data.v = MathUtil.randomInt(0, 1000);
}
class CustomData {
@Property({replicated: true, onChanged: "onVChanged"})
v: number = 0;
onVChanged(path: string, value: unknown, oldVal: unknown) {
console.error("--path " + path + " --val " + value + " --oldVal " + oldVal);
// 截断冒泡
return true;
}
@Property({replicated: true, onChanged: "onSChanged"})
s: string = "s";
onSChanged(path: string, value: unknown, oldVal: unknown) {
console.error("--path " + path + " --val " + value + " --oldVal " + oldVal);
}
}
@Component
export default class NewScript extends Script {
@Property({replicated: true, onChanged: "onDataChanged"})
data: CustomData = new CustomData();
onDataChanged(path: string, value: unknown, oldVal: unknown) {
console.error("--path " + path + " --val " + value + " --oldVal " + oldVal);
}
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
if(SystemUtil.isClient()) {
this.flag = !this.flag;
InputUtil.onKeyDown(Keys.NumPadOne, () => {
this.serverChangeV();
})
}
}
@RemoteFunction(Server)
serverChangeV() {
this.data.v = MathUtil.randomInt(0, 1000);
}
[新增]不可靠通信
新增不可靠通信手段适用于短暂事件,包括仅在短时间内生效的效果,或用于复制不断变化的数据。如果这些事件丢失,则不会重新发送。这可能会减少延迟和网络流量和压力,避免可靠栈溢出。
功能 1:UnReliableEvent 不可靠事件
不可靠事件通信包括:
- 从一个客户端指向服务器:
Event.dispatchToServerUnreliable()
- 从服务器指向特定客户端:
Event.dispatchToClientUnreliable()
- 从服务器指向所有客户端:
Event.dispatchToAllClientUnreliable()
下列示例代码展示在服务端分别使用可靠事件和不可靠事件向客户端广播一个打印命令。打印的值是在服务端累加计算。通过前端挂起操作对二者进行测试,其中不可靠事件前端打印的值有明显不连续,而可靠事件前端打印则为连续值。
TypeScript
@Component
export default class NewScript extends Script {
index: number = 0;
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
if(SystemUtil.isServer()) {
this.useUpdate = true;
}
if(SystemUtil.isClient()) {
Event.addServerListener("Print", (val) => {
console.log("val " + val);
});
}
}
/**
* 周期函数 每帧执行
* 此函数执行需要将this.useUpdate赋值为true
* @param dt 当前帧与上一帧的延迟 / 秒
*/
protected onUpdate(dt: number): void {
if(SystemUtil.isServer()) {
// 不可靠通信
// Event.dispatchToAllClientUnreliable("Print", this.index);
// 可靠通信
Event.dispatchToAllClient("Print", this.index);
this.index += 1;
}
}
}
@Component
export default class NewScript extends Script {
index: number = 0;
/** 当脚本被实例后,会在第一帧更新前调用此函数 */
protected onStart(): void {
if(SystemUtil.isServer()) {
this.useUpdate = true;
}
if(SystemUtil.isClient()) {
Event.addServerListener("Print", (val) => {
console.log("val " + val);
});
}
}
/**
* 周期函数 每帧执行
* 此函数执行需要将this.useUpdate赋值为true
* @param dt 当前帧与上一帧的延迟 / 秒
*/
protected onUpdate(dt: number): void {
if(SystemUtil.isServer()) {
// 不可靠通信
// Event.dispatchToAllClientUnreliable("Print", this.index);
// 可靠通信
Event.dispatchToAllClient("Print", this.index);
this.index += 1;
}
}
}
不可考通信 | 可靠通信 |
功能 2:UnreliableRemoteFunction 不可靠函数
同不可靠事件一样,MW 也存在不可靠函数。新增 Unreliable 标签,当 RemoteFunction 标签中存在 Unreliable 时该函数为不可靠函数。调用方法如下所示:
TypeScript
@RemoteFunction(Client, Multicast, Unreliable)
print(val: number) {
console.log("val " + val);
}
@RemoteFunction(Client, Multicast, Unreliable)
print(val: number) {
console.log("val " + val);
}
[优化]【角色不与其他角色碰撞】属性面板开关
暴露【角色不与其他角色碰撞】属性至角色属性面板,可以直接在角色的属性面板上设置该角色是否与其他角色碰撞
[优化] 角色进入游戏头顶 UI 自动加载玩家昵称
在移动端进入游戏时玩家角色头顶 UI 默认值为玩家昵称,电脑端上为 Player1/2/...
移动端 | 电脑端 |
TIP
电脑端上虽然会自动加载 Player1/2/... 玩家昵称,但是使用player.nickName是获取不到这个昵称的
[优化] 角色加载流程优化
角色移动组件初始化和角色换装逻辑解耦,在骨骼和主Mesh加载完成后,即可开始移动,移动组件工作更早
- 解决角色初始化时穿墙的 bug
- 进入游戏可以更早的操作角色
- 减少出现角色陷地和浮空等问题
[新增]动画对象混入混出模式
配合动画混入混出时间,动画对象新增混入混出模式Animation.blendInMode
以及Animation.blendOutMode
供大家选择。混合模式支持如下曲线:
Linear | Cubic | HermiteCubic | Sinusoidal | QuadraticInOut | CubicInOut | QuarticInOut |
---|---|---|---|---|---|---|
QuinticInOut | CircularIn | CircularOut | CircularInOut | ExpIn | ExpOut | ExpInOut |
示例代码展示在不同混入混出模式下动画表现的区别:
TypeScript
let ani = Player.localPlayer.character.loadAnimation("8355");
Event.addLocalListener("PLAY", (index1: number, index2: number) => {
ani.blendInMode = index1;
ani.blendOutMode = index2;
ani.play();
});
let ani = Player.localPlayer.character.loadAnimation("8355");
Event.addLocalListener("PLAY", (index1: number, index2: number) => {
ani.blendInMode = index1;
ani.blendOutMode = index2;
ani.play();
});
五、游戏功能对象新增功能及 API 更新
[新增]运动器预览功能
在运动器对象属性面板中增加预览按钮,可以在编辑场景中预览运动器的运动效果。