找回密码
 立即注册
查看: 269|回复: 1

自己的 UI 设计与开发经验总结 (Unity)2023.2.18

[复制链接]
发表于 2023-3-1 17:44 | 显示全部楼层 |阅读模式
前言:

自己是一名游戏开发进修中的学生,被朋友提醒,决定总结一下自己做游戏开发中的 UI 设计和开发经验,供自己快速回顾,也给在苦恼相同问题的伙伴一些参考。自己没有从业经验,本篇文章请当做无专业性的文章看待。如果有前辈能够给予指点,不甚感激。
本篇聚焦于视觉交互设计,包含 GUI 及其动效的设计和开发。
〇、自己的 UI 设计流程

1. 在平面设计软件制作 UI 保真图


  • 在这个阶段与伙伴敲定一个可用的版本
  • 一些草图杂乱,很多部分都是在游戏中优化完成



草图



成品



草图



成品



草图



成品

2. 导出 UI 所需的图像素材

请将大小导出为 2^n(为了压缩),并最好使单位像素大小一致。
3. 在 Unity 中制作 UI

请见下文↓
一、在 Unity 中制作 UI(UGUI)

Unity 中 GUI 对象管理

一般自己针对一个使用场景会写一个 xxUIManager ,以单例模式存在,这里包含 GUI 的所有(有需的)GUI 对象引用、会触发事件(退出、重新开始等)。
xxUIManager 最终会挂在一个 Canvas 下,再将该 Canvas 存为预制体,修改只需修改预制体即可。
以下为例:

  • LevelUIManager (Canvas)

    • ButtonsPanel (Panel)

      • AButton
      • BButton

    • KeymapPanel
    • WinPanel

当某个面板的功能过多时,可能会将对一个面板的操作封装成一个脚本,挂在对应面板下。
xxUIManager 和面板的脚本下的物体引用通常会暴露给外部对象,但通常不会在外部对象中进行操作。外部对象对xxUIManager下子物体的操作尽可能封装成方法,供外部调用。
自定义 UI 组件 与 CustomEditor 与 预制体

编写自己的 UI 组件

很多时候 Unity 自带的 UI 组件完全不能满足自己的需求,此时会自己写 UI 脚本,继承 IPointerClickHandler, IPointerDownHandler, Up, Enter, Exit… 等常用的事件。
我还没有做过兼容手柄的菜单,所以此处肯定忽略了兼容手柄选择的情况。
对兼容手柄有需求的读者或许可以阅读 Selectable 类。
自定义 Inspector ( CustomEditor ) 方便地修改预制体参数

写好 UI 脚本后使用 CustomEditor 调整 UI 是十分方便的事。
不过使用预制体时使用 Editor 会出现无法将改变的参数保存的问题,自己找了一些地方找到了解决方案。 Editor 类:
private MyUIComponent componetSelf;
private void OnEnable()
{
        componetSelf = target as MyUIComponent;
}

public override void OnInspectorGUI()
{
        base.OnInspectorGUI();

        //objectSelf 指这个 editor 脚本所对应的组件(target)
        //objectSelf.TextComponent 是一个例子,指这个预制体内被修改的组件,这里是文字
        Undo.RecordObject(componetSelf.TextComponent, "descriptive name of this operation");

        //这里执行需要做的操作,例如修改文字的内容和字号
        componetSelf.TextComponent.text = EditorGUILayout.TextField("Name", componetSelf.TextComponent.text);
        componetSelf.TextComponent.fontSize = EditorGUILayout.FloatField("Font Size", componetSelf.TextComponent.fontSize);
   
        //标记文字组件被修改
        EditorUtility.SetDirty(componetSelf.TextComponent);
        PrefabUtility.RecordPrefabInstancePropertyModifications(componetSelf.TextComponent);
        //标记场景中我们的物体被修改
        EditorSceneManager.MarkSceneDirty(componetSelf.gameObject.scene);
}

资源文件管理

自己对 UI 的资源文件通常不会单开一个文件夹,而是放在对应种类的文件夹下方的 ~/UI/ 目录下。
自己通常会用 <文件类型[小写]>(_<细分类型[小写]>)_<文件名称[大写驼峰]>  的方式命名资源文件。对于 UI 的资源文件,例子是 spr_ui_HelloWorld ,其中 spr 代表 Sprite,ui 代表 UI,HelloWorld 是文件名。对于其它资源,命名方式同理,如预制体用 p_,状态机用 ant_,动画用 an_ 等,缩写的选择取决于自己和伙伴的认知。
精灵图的注意事项

对于用于 UI 的精灵图,建议将 Mipmap 关掉,(按需)将 Wrap Mode 改为 Clamp,这两者可能导致 UI 边缘出现错误的边线。
导入资源时设置资源属性可阅读:
二、动效

自己实现动效的方法可以不严谨地分为三类:
1. DoTween:功能完整、使用方便

有些学习成本、管理复杂
DOTween 的官网:
一个 DOTween 系列教程:
各种动画曲线一览:

2. 自己写个 Tweener:灵活可控,支持打断动画

数学基础是插值,推荐视频
在用 DOTween 时发现管理动画打断很不方便,会出现动画突然闪到终点的情况等等;另一方面,在场景切换或物体销毁时,物体的动画没有播放完,DOTween 也会给出安全模式警告。
于是就自己实现一个可以随时打断和返程的 Tweener(与 DOTween 中的 Tweener 无关)
写一个类:
    [Serializable] // 使可以在 Inspector 编辑
    public class SimpleTweener
    {
        [HideInInspector] public float time; // 动画的当前时间
        public float duration; //动画的持续时长
        public AnimationCurve curve; //动画曲线

        private bool isActive; //是否正在进行
        private bool isPositive; //是否进行正向计时

        public float ProgressRate => time / duration; //动画播放的进度

        public Action<float> doAnimation = (value) => { };

        public void Update()
        {
            if (isActive)
            {
                time = isPositive ?
                    Mathf.Min(time + Time.deltaTime, duration) :
                    Mathf.Max(time - Time.deltaTime, 0);
               
                if (time == 0 || time == duration) isActive = false;
               
                float result = curve.Evaluate(ProgressRate); //对曲线进行采样
                doAnimation.Invoke(result);
            }
        }

        public void Start()
        {
            isActive = true;
            isPositive = true;
        }

        public void Stop()
        {
            isActive = true;
            isPositive = false;
        }
    }
之后我们在自己的 MonoBehaviour 中使用:
public class AnimationTestBehaviour : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
    {
        public SimpleTweener tweener;
        public Image image;

        private void OnEnable()
        {
            tweener.doAnimation += DoTween;
        }
        private void OnDisable()
        {
            tweener.doAnimation -= DoTween;
        }
                               
        private void Update()
        {
            tweener.Update();
        }

        void DoTween(float value)
        {
            Color color = image.color;
            color.a = value;
            image.color = color;
            image.transform.localScale = (0.5f + value) * Vector3.one;
        }

        public void OnPointerEnter(PointerEventData eventData)
        {
            tweener.Start();
        }

        public void OnPointerExit(PointerEventData eventData)
        {
            tweener.Stop();
        }
    }



Inspector



效果

自己编写 Tweener 的可控性高却繁琐,单程动画自己一般会用 DoTween 实现。
我想 DOTween 也是有实现可打断动画的方法的,自己应该学习一下。

3. 无限趋近式:编写快捷、适应性高

动画形式单一、可控性差、无法达到目标值
在一次 Ludum Dare 的作品中我用了这个方法实现卡牌动画


无限趋近式的意思是,让希望改变的参数的实际值无限趋近这个目标值。
以缩放为例,我们设定一个物体缩放的目标值,然后让实际值无限趋近这个目标值:
float value = Mathf.Lerp(currentSize, targetSize, speed);
speed 是趋近的速度,在 0 ~ 1 之间。在 Update 中调用的话,记得将speed 乘上 Time.deltaTime .
public void Update(){
      float currentSize = transform.localScale.x;
      currentSize = Mathf.Lerp(currentSize, targetSize, speed * (Time.deltaTime * 50f));
      image.transform.localScale = currentSize * Vector3.one;
}


这种方法会导致物体永远无法达到目标值,动画的停用也需要用差值进行检测。但其管理和实现都很简单,2D 相机跟踪物体用这种方式实现十分方便。
后记:

回顾一下,发现自己做完的可玩的游戏都是 Game Jam 作品。自己在私下幻想和开发的游戏不便展示。所以本篇文章的 UI 美术都是 Game Jam 的快速生产内容,一定有许多不标准的地方。
做 UI 是自己喜欢的一件事,自己一有游戏想法,第一件事就是去做 UI,自己做了不少游戏的 UI,结果游戏却没怎么动工过w。若有机会,愿意结识更多的伙伴,去做些有趣好看的游戏。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-3-1 17:45 | 显示全部楼层
非常好文章,爱来自 Nova ( heartheartheart
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 14:55 , Processed in 0.093251 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表