查看不同分辨率

要么选不同Aspect,要么选Free aspect拖Game窗口,选一个非Free的Aspect拖窗口没有用。

UGUI

Canvas

Canvas没有适配屏幕时检查什么:

1.画布Render Mode和Render Camera;

2.画布子物体的Scale是不是1;

3.面板是4锚点,上下左右间距都是0;

Scroll Rect

  • Content指向的是能用鼠标拖动的物体。Viewport指向的是限定Content能移到的范围的物体。
  • Content指向的物体还必须是Scroll Rect所属物体的子物体才能拖动
  • 如果Viewport指向的物体完全包围了Content,则拖动后松开鼠标,Content会自动回到Viewport的中心。(设计者的意图是显示区域已经完全够显示Content了,所以无需拖动)而且此时下面说的Viewport对Content移动范围的限制会失效,只要不松鼠标,能一直拖直到鼠标到屏幕边界。

  • Content最往下能移到Content的上边缘和Viewport的上边缘对齐,其他边缘同理。如下图,Content是淡白色区域,Viewport是红色块,在勾选Horizontal和Vertical的情况下Content的移动范围如图。Content的移动范围其实和Scroll Rect所属物体的区域没有关系。鼠标在Scroll Rect所属物体的区域(淡黑色区域)或Content内按下都能拖动Content。

Mask

  • 需要同时挂载一个Image组件才有用,只显示Image区域的子物体。Mask不会影响鼠标按下能拖动的区域。在动态生成按钮的面板中作用是隐藏Scroll View之外的格子。

  • 以上两个组件中一共出现了3种范围,分别是Content可移到的范围,鼠标按下可拖动的范围,显示Content的范围。一般来说,3个范围应该一致,使用起来才不感觉奇怪。

Vertical Layout Group

垂直布局组 (Vertical Layout Group) - Unity 手册

控制子对象高度时各子对象的高度不同:

子对象高度和Text的字体大小有关。

看了看官方文档的高度规则,评价是写了个几把。

这里这个按钮的高度太小,就加一个Layout Element调一下最小和偏好高度。别的懒得研究。

Grid Layout Group

自动制表的组件。会令所有子物体的大小、位置不能调整,统一由这个组件调整。

Content Size Fitter组件

会改变所属物体的长宽,依据是它的子物体所占据的区域。和XXX Layout Group配合使用,Layout Group把控件排好,Content Size Fitter根据子控件占的大小设置Content的大小。Content大于Viewport,才能拖动。

Image组件

Color

Color是和Source Image的RGB相乘,所以白色*蓝色=蓝色,红色*蓝色=黑色

拖拽控件的注意点

脚本继承IDragHandler、IBeginDragHandler、IEndDragHandler处理拖拽开始、过程、结束的操作。

拖到另一个面板时被挡住

那么拖拽开始时就要把控件转移到hierarchy靠下的一个对象上。

拖拽松开时回到原位置

首先把父对象设回GridLayoutGroup,然后

LayoutRebuilder.ForceRebuildLayoutImmediate(gridLayoutGroup.transform as RectTransform);

RectTransform相关API

anchorMax和anchorMin

选择中间的9种单锚点模式时,anchorMax和anchorMin一样,是此锚点在父物体里的归一化坐标。选择6种双锚点模式和4锚点模式时,anchorMin是偏左下的锚点的归一化坐标,anchorMax是偏右上的锚点的归一化坐标。

如选中间的anchorMax和anchorMin都是(0.5,0.5),选左上都是(0,1)

sizeDelta

x是面板的宽度和锚点之间x距离的差值。对于单锚点,锚点之间x距离是0,sizeDelta.x就是面板宽度;对于左右双锚点,如果锚点在左右边缘,那么面板和屏幕同宽的时候,sizeDelta.x是0,屏幕宽1920,面板左边和屏幕左边对齐,右边在屏幕右边480,那么sizeDelta.x是480.y同理。

检查器的PosX、PosY对应的API

是anchoredPosition,不是localPosition。anchoredPosition是相对于锚点的位置,localPosition是相对于scene窗口的pivot的位置。

总结

凡是非单锚点模式(AnchorMin!=AnchorMax),Pivot就会失效,变成通过上下左右边和父级的上下左右边的距离确定位置大小。可以留意这4个词的变化。

实例化面板预制体时面板的一部分控件消失了

预制体:

实例化的:

原因:把那部分控件拖错了,拖到一个会被销毁的GameObject上了。这说明要尽量避免拖错,应该尽量使用每部分特有的组件。

试图用ToggleGroup做多选一列表遇到的坑

因为ToggleGroup自带的多选一特性,想用它做查看人物的人物列表或背包物品列表,遇到一些坑。要给Toggle加被激活时的回调,Toggle是根据数据文件动态创建的,结果创建的时候就调用了onValueChanged。要解决这个问题好像也不比几个按钮简单,就放弃了这个方案。所以动态创建的按钮即使多选一也不要用ToggleGroup,只有写死数量的多选一按钮可以用,比如人物的基本信息、武器、圣遗物、天赋按钮。如果一个面板预制体有一组Toggle,实例化预制体时不会执行任何Toggle的onValueChanged()。

人物查看界面右边多种可能的面板,改变查看的面板时不知道前一个查看的是什么面板,需要把所有面板都删一次

实例化的面板位置大小变了

如果实例化面板的位置、大小和预制体不一样了,可能是因为先实例化为了根对象,又设为画布的子对象。应该实例化时直接为画布的子对象:

page=Object.Instantiate(UIPrefab,canvas.transform);

多选一页签:ToggleGroup不如几个Button

理由如下:

  1. 多选一toggle按已经选中的toggle时还是会执行回调,白白重复了上次点击的工作。button可以代码判断跳过这个;
  2. 多选一toggle组已经不可能把toggle点灭,实际上回调onValueChanged里的bool参数已经没用了,还要多一步if判断。

只需要给按钮加一个选中后高亮的方法,维护一个已选中页签变量,就可以完成多选一页签的功能。多选一页签的功能包括对表格的更新和选中页签高亮的更新。高亮更新先把选中的页签高亮关闭,然后写入把新选中的页签写入,再打开选中的页签高亮。

public Tab tabGun,tabMag,tabGrenade...
Tab tabOn;
tabGun.button.onClick.AddListener(()=>{
                if(tabOn==tabGun){
                    return;
                }
                UpdateTabOn(tabGun);
                ClearCells();
                ShowGuns();
            });
void UpdateTabOn(TabShop tab){
            tabOn.imageSelected.enabled = false;
            tabOn=tab;
            tabOn.imageSelected.enabled=true;
        }

怎么让Image根据一个Text里文本占的大小自动调整大小???

如果让Image做Text的子对象,Image撑满Text,Text加Content size fitter,Text可以自动调整大小,但是Image会盖住文本。

主相机、UI相机叠加时显示血条

Vector3 screenPos = Camera.main.WorldToScreenPoint(barWorldPos);
        Vector2 pos;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            canvas.transform as RectTransform, screenPos,
            UICamera, out pos
        );
        transform.localPosition = pos;

先WorldToScreenPoint从世界坐标通过主相机到屏幕坐标,再用RectTransformUtility.ScreenPointToLocalPointInRectangle从屏幕坐标到血条的局部坐标。

UI框架设计

UI系统本质架构

游戏的数据处理包括

  1. 从数据持久文件读取数据到数据管理类(一般可以在使用数据管理脚本时通过构造方法顺便读取);
  2. 把读出的数据设置系统各组件(比如音量);
  3. 把数据从数据管理脚本显示到显示控件;
  4. 接收用户的操作并对数据管理脚本的数据做相应修改;
  5. 同时对相应组件做修改(比如音量)
  6. 然后重新显示数据;
  7. 在适当的时间把数据管理脚本存回数据持久文件(一般是关闭面板时);

UI的使命就是1.把用户的操作变成数据修改;2.如实反映数据;

注意:不能偷懒在用户操作输入控件时直接修改显示控件(比如从背包销毁一个物品时直接把这个格子删掉)!这样不能保证显示控件如实显示数据!显示控件永远只能根据数据管理类的数据显示!

上面和图里的操作可以分为两类:输出和输入。在面板初始化时只用执行输出代码,在输入控件响应用户输入时先修改数据,再刷新显示,刷新显示一般需要把一些已有的控件、物品销毁,再生成新的。

UI面板的储存和加载

UI面板要做预制体。我们知道加载资源的方式有:

  1. 直接引用;
  2. Resources;
  3. AB包;

直接引用,因为任何面板可能在任何时候需要加载,需要一个全局存在、能通过拖入引用对象的对象,也就是ScriptableObject。就需要一个作为UI面板总表的ScriptableObject,然后这玩意还需要Resources或AB包加载。新加一个面板需要的工作量是总表加一个字段、拖一下。

Resources加载需要有路径,面板们的路径记在哪?一个总表还是各个面板类里?又是一个选择。记载总表里,面板预制体路径和面板类名是割裂的,记在面板类里,则可以

Resources.Load(PanelXXX.path);
PanelXXX.Instance.XXX();

预制体路径和面板类是联系起来的。 

场景里面板互相调用的方法

A面板调用B面板时要找到B面板有多种方法,可以

1.transform.Find(B的名字);

2.A脚本里记录一个B脚本变量,在编辑器拖;(如果面板多了,各面板互相调用,会变成蜘蛛网)

3.使用FindObjectOfType<>()在场景里找;(好像也不错)

4.每个面板一个单例,定义一个泛型单例面板基类;

面板的显示和消失方式

有两种方案:

1.预制体实例化和销毁;如果每个面板一个单例,调用一个未显示面板的单例时就要实例化预制体。在哪里记录预制体位置呢?可以1.在全局数据脚本里使用路径(这种方法除非人工保证面板的类名和路径常量名保持一致,否则面板类名和预制体路径常量名之间没有明显关系);2.在各面板脚本里用一个public const string path记录,实例化时执行:

Instantiate(Resources.Load(XXXPanel.path));

这样实例化和下面调用时都出现了面板类名,类名和路径的关系明显;3.如果喜欢拖,放一个继承Monobehaviour的单例脚本,在检查器把面板预制体拖进去,这个物体需要DontDestroyOnLoad

这个方案的问题:1.如果按钮有声音,且声源在这个面板预制体内,按退出一个面板的按钮时,面板会瞬间被销毁,声音无法播放。如果声源放在一个全局物体上,就要用脚本找到这个声源,不可能在预制体编辑界面拖。

2.激活和失活。如果每个面板一个单例,需要一开始所有面板写入单例后把自己失活。这样的问题是编辑模式所有面板都要打开,Scene窗口很乱。

打开一个面板时前一个面板怎么处理

1.不消失,新面板使用一个带碰撞体的全屏透明贴图挡住前一个面板的输入;

2.前一个面板消失;

动态生成按钮面板的类型

动态生成按钮的面板可能是UI里最复杂的一类面板。这类面板也分几种类型,复杂程度不同。

无详细信息、点击按钮直接操作型 

动态生成按钮一般都包括这几步:

  1. 实例化按钮预制体(可能还要先得到预制体),把它加入按钮列表;
  2. 得到这个数据的信息,一般需要同时从配表和用户数据取信息,填入按钮的图标、文本等元素;
  3. 按钮添加对应数据的地址,可能是一个List里的index;
  4. 添加按钮回调(存在闭包的问题);

动态按钮的回调一般有参数,不同按钮的参数不同。动态生成的控件按钮的回调怎么赋值?用代码还是拖?函数定义在面板脚本还是按钮脚本?

如果想用拖,函数必须写在按钮脚本里,这个函数如果有参数,参数必须在预制体里就确定,所以这个函数不能有参数。不过可以用一个无参函数把真正要调用的,可能是其他类的有参数方法封装起来(动画事件也可以用类似套路)。

1.页面脚本生成按钮时添加回调,需要把函数写入lambda表达式,参数临时声明一个变量记录,这里出现了闭包。

int serverId=serverInfos[i].id;
serverCell.buttonServer.onClick.AddListener(
    ()=>{serverCell.ServerCellClick(serverId);});

选中后显示详细信息、共用操作按钮型

按下按钮直接执行操作往往不能让玩家掌握详细信息,需要进一步优化成这种框架。

这比动态生成按钮列表更复杂一点,还需要:

  1. 确定一种显示选中项的方法,可能是加框、文本变色、图标替换等;
  2. 按下按钮后,更新选中项index,把所有按钮设为未选中,把index的按钮设为选中;
  3. 所有按钮共用的哪些选项按钮要触发的回调随着选中的index相应变化。

下面这个例子:选中项的图标会向右。选中显示的详细信息包括销毁枪械、实例化新枪械(包括可能的动作变化)、刷新右侧的所有信息。右下角按钮回调的内容随选中项的index自然变化。

写了几次发现这种面板都是套路,或许可以提炼一个模板:

  • 系统有一个面板脚本和一个格子按钮脚本;
  • 面板脚本有一个按钮脚本的列表,一个int选中项的索引,一个对按钮预制体的引用;
  • 按钮脚本有对自己身上的Button、Image、Text的引用,有一个int index记录自己是按钮列表的第几个元素;
  • 一个批量显示按钮的方法,用for循环,先把按钮预制体实例化,同时设置父级为自动布局组件的transform,再得到按钮上的按钮脚本,从用户数据和配置数据取得需要的数据,把文本、图标写入给按钮的控件,把循环变量i写给;

鼠标进入显示悬浮面板型

浮动面板使用IPointerEnterHandler、IPointerExitHandler实例化和销毁。浮动面板有几个坑:

被其他格子挡住

如上图。因为浮动面板的父级设为了所属的格子,hierarchy里一般格子下面还有格子。要在hierarchy所有格子的下方建一个锚点对象做浮动面板的父级,设置位置可以先实例化为格子父级,设置局部位置0,再改成锚点的子级。或者设置世界位置和锚点一样。

鼠标在格子和浮动面板重叠的区域时浮动面板一直闪

因为面板出现时挡住鼠标射线被判定移出格子,销毁面板后又判定进入格子。解决方法:浮动面板的所有Image和Text不勾选Raycast Target。

动态按钮列表记录选中的按钮是用int index还是按钮类?

用int index好处是得到上一个、下一个按钮很方便,用按钮类则需要list.IndexOf()先得到索引。而

数据管理脚本读取数据持久文件的方式

1.在数据管理脚本构造方法里读取

2.我想在数据属性的get方法里读取,结果发现这样set方法也应该立即写入文件,读取写入文件的频率会很高。不知道开销会不会很大。

public SettingsData settingsData{
        get{
            if(File.Exists(settingsDataPath)){
                string jsonString=File.ReadAllText(settingsDataPath);
                return JsonUtility.FromJson<SettingsData>(jsonString);
            }
            else{
                return new SettingsData(){
                    musicOn=true,
                    musicVolume=1,
                    soundOn=true,
                    soundVolume=1,
                };
        }}
    }

    UI材质的stencil(模板测试)

    这是默认的设置。

    缓冲区存有一个Stencil ID,当前物体存有一个Stencil ID(参考值),二者按Stencil Comparison比较,得到是否通过,通过则渲染当前物体,并且Stencil Operation决定后续对缓冲区的操作。

    Stencil Comparison

    名称 作用
    0 Disabled 禁用模板测试(或深度测试不可用时)
    1 Never 测试永远不通过,像素始终被丢弃‌
    2 Less 当 (refValue & readMask) < (bufferValue & readMask) 时通过测试‌
    3 Equal 当 (refValue & readMask) == (bufferValue & readMask) 时通过测试‌
    4 LessEqual 当 (refValue & readMask) <= (bufferValue & readMask) 时通过测试‌
    5 Greater 当 (refValue & readMask) > (bufferValue & readMask) 时通过测试‌
    6 NotEqual 当 (refValue & readMask) != (bufferValue & readMask) 时通过测试‌
    7 GreaterEqual 当 (refValue & readMask) >= (bufferValue & readMask) 时通过测试‌
    8 Always 测试永远通过(默认值)

    默认永远通过,就是一定渲染当前物体。

    Stencil Operation

    0 Keep 保持模板缓冲区的当前值不变(默认操作)
    1 Zero 将模板缓冲区的值重置为0
    2 Replace 用参考值(Ref)替换模板缓冲区的值(UGUI Mask组件的默认操作)
    3 IncrSat 增加模板缓冲区的值(上限255,超过则保持255)
    4 DecrSat 减少模板缓冲区的值(下限0,低于则保持0)
    5 Invert 按位取反模板缓冲区的值(如1变为254)
    6 IncrWrap 增加模板缓冲区的值(溢出时从0重新开始,255→0)
    7 DecrWrap 减少模板缓冲区的值(溢出时从255重新开始,0→255)

    默认是渲染后(如果通过)保持缓冲区不变。比如当前缓冲区Stencil ID=2,当前物体Stencil ID=0,渲染当前物体,并且缓冲区保持Stencil ID=2。

    Mask的设置:

    一定通过测试,渲染,渲染后把自己的Stencil ID=1写入缓冲区,替代之前默认材质的0.

    Mask的子对象设置:

    ID相等才渲染,它的ID和Mask相同,和默认材质不同,所以只在Mask区域渲染。它不改变缓冲区,可以预测再来一个子对象会覆盖它。

    再来看看【unity小技巧】实现FPS武器的瞄准放大效果(UGUI实现反向遮罩,全屏遮挡,局部镂空效果)_unity 开镜-CSDN博客

    里面瞄准镜的两个材质:

    镂空材质:用来在屏幕中心掏一个圆形区域。它使用一个中心是圆形,其他是透明的图片。

    它会通过,把缓冲区写成1.

    背景材质:生成大部分的黑色。背景放在镂空后面,比它后渲染。

    它和缓冲区相同时通过,读取遮罩是1,就是只比较第1位,它的ID是2,二进制是00000010,和默认材质00000000比较第1位,能通过,覆盖默认材质,和镂空00000001比较第1位,不通过,露出镂空材质。所以它在镂空对象下面也不覆盖它。

    这里背景的Read Mask写255,ID写0也能让它覆盖默认材质,露出镂空材质。

    Stencil Operation是测试是否通过都执行吗?

    资料说还有Pass、Fail参数指定通过和失败时的操作,但是材质面板只有一个Stencil Operation,失败时是否执行?可以做个实验

    红4强制通过把缓冲区写成4,青3强制通过把缓冲区写成3,橙4和缓冲区比较,Stencil Operation是把缓冲区写成4,蓝4也是和缓冲区比较,Stencil Operation是把缓冲区写成4。那么橙4在青3的部分未通过,如果执行了把缓冲区写4,蓝4就应该通过。但蓝4没有通过。证明Stencil Operation只在通过时执行。虽然对于不修改缓冲区的和强制通过的(包括了大部分物体),这个结论没有意义,只有非强制通过,修改缓冲区的才有影响。

    Color Mask

    限定渲染的颜色,为0时不渲染。但会对缓冲区ID做自己的处理。

    Use Alpha Clip

    是否对图片的透明区域缓冲区做自己的处理,不勾选则处理区域一定是长方形。和渲染的图片是否透明无关。

    得到当前鼠标是否在某个UI元素上

    UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject()

    这个函数返回的好像是上一次点击光标在不在UI上。

    这一段是正确的:

    public bool IsMouseOverUI()
    {
        PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current);
        eventDataCurrentPosition.position = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
        List<RaycastResult> results = new List<RaycastResult>();
        EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
        return results.Count > 0;
    }

    Logo

    这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

    更多推荐