【本文参与征文活动】
前言
UI界面是游戏的重要组成部分,也是玩家在游戏内接触最多的东西。一个UI界面的感官体验直接决定着这个游戏的生命。那么一个好的UI界面,不仅要美术同学设计的好,还要前端/UI(一些公司会有专门拼UI的)还原的好。如果一个界面设计属于S,而放入游戏却只有C。那岂不是相当可惜。所以我这篇文章将介绍,如何在Creator中更方便快捷的使用设计图,提高UI还原度。
我有调研过一些公司如何去提高UI还原度问题。
大概分为一下几种:
第一类:就是美术标好坐标边距,然后前端根据设计图的标注拼界面;
缺点:美术费事费力,前端亦然,切还原度还不高
第二类:把设计图图放到界面后,对着图放控件,然后拼完移除;
缺点:美术还原度高,但是用完需要手动移除,有忘记移除的风险。
第三类:使用工具生成对应的界面,然后做微调。
缺点:对美术要求高,需要按照某个标准去制作,导出来的Prefab参差不齐。
总结以上问题,我采用第二种方案,然后在此基础上开发特定脚本来避免忘记移除的问题。(功能的灵感来自FairyGUI)
一、功能设计
1、需求分析
首先我们需要的基本功能大概就是,可以把设计图放到界面里,无额外的节点产生,并且不能影响界面中的操作。所以总结需求结合后期使用,总结出来设计图应该具备以下特征:
-
不影响界面控件的点击
-
设计图的节点在层级树里不能显示,但仍然可以设置位置和透明度等
-
可设置显示的层级,在界面最上端还是在最低端
-
不会被Creator计算引用,并打包到发布的项目中
-
仅在当前界面显示,复制或嵌套等不能显示
功能解释:
不影响界面控件的点击:
我们添加的设计图仅用来对位的,如果能够选中会影响界面布局等。
设计图的节点在层级树里不能显示,但仍然可以设置位置和透明度等
主要是设计图不是界面的一部分,只是一个辅助工具,所以不应该显示再层级树里,但是为了方便对位,又需要设置透明度和位置偏移等。
可设置显示的层级,在界面最上端还是在最低端
为了可以方便用户选中设计图在顶端还是底端,防止设计图影响界面效果显示。
不会被Creator计算引用,并打包到发布的项目中
这个是最重要的部分,也是不好实现的部分。如果界面中直接引用了某个图片,在发布项目的时候,Creator会自动计算依赖关系并打包到项目中,而设计图只是用来拼界面的。不需要,也不能被打包到项目中。
仅在当前界面显示,复制或嵌套等不能显示
如果当前prefab被其他界面引用,或者是嵌套使用,当前界面的预览图不能显示。因为当前界面的设计图往往是当前界面的样式,所以仅需要在当前界面显示。在其他界面显示反而会影响其他界面的开发。
2、实现方案
首先我们先把设计图能显示到界面中:
const {ccclass, property, executeInEditMode} = cc._decorator;
@ccclass
@executeInEditMode // 在编辑器中执行
export default class DesignView extends cc.Component {
@property
private _spriteFrame: cc.SpriteFrame = null;
@property({type: cc.SpriteFrame, visible: true, displayName: "设计图"})
public set spriteFrame(frame: cc.SpriteFrame) {
this._updateDesignView(frame);
}
public get spriteFrame(): cc.SpriteFrame {
return this._spriteFrame;
}
private _sprite: cc.Sprite = null;
private _updateDesignView(frame: cc.SpriteFrame, init: boolean = false) {
if (this._spriteFrame === frame && !init) {
return;
}
if (!this._sprite) {
let viewNode = new cc.Node("DesignView");
this._sprite = viewNode.addComponent(cc.Sprite);
viewNode.parent = this.node;
}
this._sprite.spriteFrame = frame;
this._spriteFrame = frame;
}
public onLoad() {
// 激活当前界面的时候显示预览图
this._updateDesignView(this._spriteFrame, true);
}
// 当前脚本激活时,显示预览图
public onEnable() {
if (this._sprite) {
this._sprite.enabled = true;
}
}
// 当前脚本取消激活时,隐藏预览图
public onDisable() {
if (this._sprite) {
this._sprite.enabled = false;
}
}
public onDestroy() {
// 当前脚本被手动移除时,删除预览图
if (this._sprite && !(this._sprite.node["_objFlags"] & cc.Object["Flags"].Destroying)) {
this._sprite.node.destroy();
}
}
}
简单的功能实现了,把设计图拖到后边的设计图属性上,界面出来了,但是这只是设计图,我们不想让他在左上角的层级树显示,因为并且也不能都点击到,这样才能方便我们布局界面。如果有看我写过嵌套的小伙伴可能就比较清楚了,这里我就不做过多的赘述了:
viewNode["_objFlags"] |= (cc.Object["Flags"].DontSave | cc.Object["Flags"].LockedInEditor | cc.Object["Flags"].HideInHierarchy);
完成上面的步骤之后,我们再来看看界面中①②③④,是不是功能已经实现了,把预览图拖到对应的位置,界面中显示,并且不会多出其他任何信息。
到这我们已经完成50%了,但是还有两个比较重要的问题还没显示,其一,就是发布项目的时候,设计图不能被打包到项目中,这才是我们要实现的精髓部分。这个功能实现不了,整个功能就是白费。
我通过各种查阅古籍和翻阅官方文档,终于知道了一个救命稻草editorOnly
,你们现在是不是也和我一样揣着无比喜悦的心情。最困难的问题终于要得到解决了。
@property({editorOnly: true})
private _spriteFrame: cc.SpriteFrame = null
修改好了以后,发布项目测试。
What?还在,说好的 在导出项目前剔除该属性 呢?好吧!没办法了只能另辟蹊径了。
这时候只能想看看本地是如何识别纹理的。打开对应的prefab,找到对应的位置格式就是"__uuid__":"xxx"
,所以这里我大胆的猜测官方就是识别的__uuid__
,而和后面的值一点关系都没有,所以如果我保存一个其他类型值他是不是就不知道是否引用了呢?
const {ccclass, property, executeInEditMode} = cc._decorator;
@ccclass
@executeInEditMode
export default class DesignView extends cc.Component {
// 删除@property
private _spriteFrame: cc.SpriteFrame = null;
// 添加保存纹理的属性
@property
private _designUrl: string = "";
....
private _updateDesignView(frame: cc.SpriteFrame, init: boolean = false) {
if (this._spriteFrame === frame && !init) {
return;
}
this._designUrl = frame ? frame["_uuid"] : "";
if (!this._sprite) {
let viewNode = new cc.Node("DesignView");
this._sprite = viewNode.addComponent(cc.Sprite);
viewNode.parent = this.node;
viewNode["_objFlags"] |= (cc.Object["Flags"].DontSave | cc.Object["Flags"].LockedInEditor | cc.Object["Flags"].HideInHierarchy);
}
this._sprite.spriteFrame = frame;
this._spriteFrame = frame;
}
...
现在再执行导出操作,确实项目中不会有设计图资源了。但是又出现了另一个问题,那就是我们重新打开当前界面的时候,设计图也不会被创建了。因为_spriteFrame
已经不是编辑器属性了,所以他不会被默认创建了,我们现在有的就是纹理的uuid
,所以我们现在能做的就是手动创建纹理出来。
public onLoad() {
if (this._designUrl) {
if (cc.assetManager && cc.assetManager.loadAny) {
cc.assetManager.loadAny({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> {
if (!err) {
this._updateDesignView(spriteFrame);
}
})
}
else {
// 兼容2.4.0之前版本
cc.loader.load({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> {
if (!err) {
this._updateDesignView(spriteFrame);
}
})
}
}
}
修改完以后,测试成功,喜大普奔。这里我就贴效果图了。到这里我们的主要功能就实现完了。下面就是如何实现被嵌套使用的时候不显示设计图问题了。(这里我就不过多解释原因了。)
如何实现设计图只在当前界面生效呢?
方案一:判断当前界面是不是在Scene层显示(我们所打开的所有界面,场景或者预制体的parent都是一个scene)。
方案二:找到一个唯一性标识的字段。
对于场景(Scene)来说,不会出现场景嵌套场景的情况,所以这里不做讨论。
通过代码和prefab文件的研究,发现有个fileId
属性,当prefab的parent不是scene时,这个id会变成,prefab的parent的fileId
。这里我们刚好可以利用这一点,在第一次挂在DesignView
脚本时,保存fileId
来记录默认的界面,一旦id发生改变,设计图就不显示。
private _isCurPrefab(): boolean {
let prefab = this.node["_prefab"];
return prefab && prefab.root && (!this._prefabFileId || this._prefabFileId === prefab.fileId);
}
到这里,我们的核心功能已经实现了。但是因为设计图的节点被我们隐藏了,所以我们要添加一些接口可以用来这是透明度、层级和位置偏移等。下面是实现的最终版代码。
const {ccclass, property, executeInEditMode} = cc._decorator;
@ccclass
@executeInEditMode
export default class DesignView extends cc.Component {
private _sprite: cc.Sprite = null;
private _spriteFrame: cc.SpriteFrame = null;
@property
private _prefabFileId: string = "";
@property
private _opacity: number = 70;
@property
private _offsetX: number = 0;
@property
private _offsetY: number = 0;
@property
private _showTop: boolean = true;
@property
private _designUrl: string = "";
@property({type: cc.SpriteFrame, displayName: "设计图"})
public set spriteFrame(frame: cc.SpriteFrame) {
this._updateDesignView(frame);
}
public get spriteFrame(): cc.SpriteFrame {
return this._spriteFrame;
}
@property({type: cc.Boolean, displayName: "置顶"})
public set showTop(top: boolean) {
this._showTop = top;
this._updateOrder();
}
public get showTop(): boolean {
return this._showTop;
}
@property({type: cc.Integer, displayName: "透明度", slide: true, min : 0, max: 255})
public set opacity(value: number) {
this._opacity = value;
this._updateOpatity();
}
public get opacity(): number {
return this._opacity;
}
@property({type: cc.Integer, displayName: "X偏移"})
public set offsetX(value: number) {
this._offsetX = value;
this._updateOffset();
}
public get offsetX(): number {
return this._offsetX;
}
@property({type: cc.Integer, displayName: "Y偏移"})
public set offsetY(value: number) {
this._offsetY = value;
this._updateOffset();
}
public get offsetY(): number {
return this._offsetY;
}
private _updateDesignView(frame: cc.SpriteFrame) {
if (this._spriteFrame === frame) {
return;
}
this._designUrl = frame ? frame["_uuid"] : "";
if (!this._sprite) {
let viewNode = new cc.Node("DesignView");
this._sprite = viewNode.addComponent(cc.Sprite);
viewNode.parent = this.node;
viewNode["_objFlags"] |= (cc.Object["Flags"].DontSave | cc.Object["Flags"].LockedInEditor | cc.Object["Flags"].HideInHierarchy);
this._updateOffset();
this._updateOrder();
this._updateOpatity();
}
this._sprite.spriteFrame = frame;
this._spriteFrame = frame;
}
private _updateOffset() {
if (!this._sprite) {
return;
}
let node = this._sprite.node;
node.x = this._offsetX;
node.y = this._offsetY;
}
private _updateOrder() {
if (!this._sprite) {
return;
}
this._sprite.node.zIndex = this._showTop ? 999 : -1;
}
private _updateOpatity() {
if (!this._sprite) {
return;
}
this._sprite.node.opacity = this._opacity;
}
public onLoad() {
if (!CC_EDITOR) {
return;
}
if (!this._prefabFileId && this.node["_prefab"]) {
this._prefabFileId = this.node["_prefab"].fileId;
}
if (this._isCurPrefab()) {
if (this._designUrl) {
if (cc.assetManager && cc.assetManager.loadAny) {
cc.assetManager.loadAny({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> {
if (!err) {
this._updateDesignView(spriteFrame);
}
})
}
else {
// 兼容2.4.0之前版本
cc.loader.load({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> {
if (!err) {
this._updateDesignView(spriteFrame);
}
})
}
}
if (this._sprite) {
this._updateOffset();
this._updateOrder();
this._updateOpatity();
}
}
else {
this.destroy();
}
}
private _isCurPrefab(): boolean {
let prefab = this.node["_prefab"];
return prefab && prefab.root && (!this._prefabFileId || this._prefabFileId === prefab.fileId);
}
public onEnable() {
if (this._sprite) {
this._sprite.enabled = true;
}
}
public onDisable() {
if (this._sprite) {
this._sprite.enabled = false;
}
}
public onDestroy() {
if (this._sprite && !(this._sprite.node["_objFlags"] & cc.Object["Flags"].Destroying)) {
this._sprite.node.destroy();
}
}
}
最终界面显示效果如下:
现在我们在开发过程中,只要把design-view
挂载到跟节点,然后把设计图拖到设计图
属性位置,就可以愉快的开发了,也不用担心设计图资源忘记移除,被打包到项目中了 (除非你把设计图放到resources目录下,你这样做了,我也帮不了你)。