概述

C#中的Struct是值类型数据结构,在使用时常发生值拷贝。

公用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public struct ChildStruct
{
public int Value;
}

public class ChildClass
{
public int Value;
}

public struct ParentStruct
{
public int Value;
public ChildStruct ChildStruct;
public ChildClass ChildClass;
}

作为参数进行值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;
Console.WriteLine(parentStruct1.Value);

CopyValue1(parentStruct1);

Console.WriteLine(parentStruct1.Value);
}

public static void CopyValue1(ParentStruct parentStruct)
{
parentStruct.Value = 2;
}

输出结果:

1
2
1
1

分析

函数CopyValue1中对传入参数parentStruct的修改,并不能影响函数外parentStruct1的值。所以认为Struct作为传入参数发生了值拷贝,函数内修改的与函数修改的Struct并非同一个引用。

改良

使用ref进行传参,这样不再传递Struct的值,而是传递Struct的引用来避免值拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;
Console.WriteLine(parentStruct1.Value);

CopyValue1(ref parentStruct1);

Console.WriteLine(parentStruct1.Value);
}

public static void CopyValue1(ref ParentStruct parentStruct)
{
parentStruct.Value = 2;
}
1
2
1
2

return进行值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;

ParentStruct parentStruct2 = CopyValue2(ref parentStruct1);
parentStruct2.Value = 3;

Console.WriteLine(parentStruct1.Value);

Console.WriteLine(parentStruct2.Value);
}

public static ParentStruct CopyValue2(ref ParentStruct parentStruct)
{
parentStruct.Value = 2;
return parentStruct;
}

输出结果

1
2
2
3

分析

从之前的示例可以得知ref传参传递的是引用,因此认为函数内的parentStruct变量的与函数外的parentStruct1变量是同一个引用。

变量parentStruct2接收CopyValue2的返回值,并对parentStruct2中的值进行修改,但输出的结果发现parentStruct1中的值并未发生修改。

可以得出结论:函数的返回值也是值传递,并非引用传递。

改良

使用ref将返回的类型为引用类型。接收的变量也需要使用ref修饰。来避免值拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;

ref ParentStruct parentStruct2 = ref CopyValue1(ref parentStruct1);
parentStruct2.Value = 3;

Console.WriteLine(parentStruct1.Value);

Console.WriteLine(parentStruct2.Value);
}

public static ref ParentStruct CopyValue1(ref ParentStruct parentStruct)
{
parentStruct.Value = 2;
return ref parentStruct;
}

1
2
3
3

Struct类型数组元素存为变量时进行值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
ParentStruct[] structs = new ParentStruct[1];


structs[0] = new ParentStruct();
structs[0].Value = 1;

Console.WriteLine(structs[0].Value);

ParentStruct parentStruct0 = structs[0];

parentStruct0.Value = 2;

Console.WriteLine(structs[0].Value);
Console.WriteLine(parentStruct0.Value);
}

输出结果

1
2
3
1
1
2

分析

由输出的结果可以发现对于将Struct数组的元素存为变量parentStruct0后,对于parentStruct0的修改并不会改变数组中元素内的值,所以拷贝的是值而不是引用。

改良

使用ref修饰变量,这样变量存储的是数组元素的引用来避免值拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
ParentStruct[] structs = new ParentStruct[1];


structs[0] = new ParentStruct();
structs[0].Value = 1;

Console.WriteLine(structs[0].Value);

ref ParentStruct parentStruct0 = ref structs[0];

parentStruct0.Value = 2;

Console.WriteLine(structs[0].Value);
Console.WriteLine(parentStruct0.Value);
}

函数内创建并返回的Struct避免值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
ParentStruct parentStruct1 = CreateParentStruct();
}

public static ParentStruct CreateParentStruct()
{
ParentStruct parentStruct = new ParentStruct();
return parentStruct;
}

分析

从之前的结论可以得知函数返回值是值拷贝,所以使用out关键词来避免值拷贝

改良

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
ParentStruct parentStruct1;
CreateParentStruct(out parentStruct1);
}

public static void CreateParentStruct(out ParentStruct parentStruct)
{
parentStruct = new ParentStruct();
}

Struct内的Struct类型成员与Class类型成员

ParentStruct是个Struct,内部有Struct类型成员变量ChildStructClass类型成员变量ChildClass。分别创建这两种类型的变量,初始化Value的值,并赋值给ParentStruct变量对应的成员。两种类型的变量的Value值,观察ParentStruct变量内部对应成员值的变化。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();

ChildStruct childStruct1 = new ChildStruct();
childStruct1.Value = 1;

parentStruct1.ChildStruct = childStruct1;

Console.WriteLine($"childStruct1.Value:{childStruct1.Value}");

childStruct1.Value = 2;
Console.WriteLine("-------childStruct1.Value的值修改后-------");

Console.WriteLine($"childStruct1.Value:{childStruct1.Value}");
Console.WriteLine($"parentStruct1.ChildStruct.Value:{parentStruct1.ChildStruct.Value}");

Console.WriteLine("\n");

ChildClass childClass1 = new ChildClass();
childClass1.Value = 3;

parentStruct1.ChildClass = childClass1;

Console.WriteLine($"childClass1.Value:{childClass1.Value}");

childClass1.Value = 4;
Console.WriteLine("-------childClass1.Value的值修改后-------");

Console.WriteLine($"childClass1.Value:{childClass1.Value}");
Console.WriteLine($"parentStruct1.ChildClass.Value:{parentStruct1.ChildClass.Value}");
}

输出结果

1
2
3
4
5
6
7
8
9
10
childStruct1.Value:1
-------childStruct1.Value的值修改后-------
childStruct1.Value:2
parentStruct1.ChildStruct.Value:1


childClass1.Value:3
-------childClass1.Value的值修改后-------
childClass1.Value:4
parentStruct1.ChildClass.Value:4

分析

Struct的Struct成员存储的是成员内部的值,而Struct的Class成员存储的是成员的引用。并且在C#11之前的版本没法使用ref成员。因此Struct类型的成员没法使用享元模式。因为每次都会拷贝。

疑问

对于C#的ref关键词,个人认为ref在修饰参数时和作用与类型上时是不一样的。 修饰参数时表示参数是通过引用的方式来传递的,但没法传递一个数组的元素的引用。只能传递数组元素的值拷贝,在内部对引用的变更并不会影响到外部。对此感到疑惑,因此需要慎重在方法内对于ref或out修饰的参数的引用进行修改。

AI绘画-Stable Diffusion环境搭建

  1. 下载git

    https://git-scm.com/download/win

  2. 下载python 3.10.6(其他版本的python可能会出现报错)

    https://www.python.org/ftp/python/3.10.6/python-3.10.6-amd64.exe

    安装时需要勾选Add Python 3.10 to PATH。

  3. 下载项目

    https://github.com/AUTOMATIC1111/stable-diffusion-webui.git

  4. 进入下载的目录

    打开stable-diffusion-webui\webui-user.bat文件。

    配置PYTHON值为python安装目录和VENV_DIR为取一个你想要保存模型的文件夹名称,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    @echo off

    set PYTHON=C:\Users\10246\AppData\Local\Programs\Python\Python310\python.exe
    set GIT=
    set VENV_DIR=venv
    set COMMANDLINE_ARGS=

    call webui.bat

  5. 可以使用dev-sidecar来加速。

    https://github.com/docmirror/dev-sidecar

  6. 运行python -m pip install --upgrade pip升级pip版本。

  7. 运行stable-diffusion-webui\webui-user.bat文件。

  8. 然后根据依赖信息(https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Dependencies)下载sd-v1-4.ckpt文件。

    1
    magnet:?xt=urn:btih:3a4a612d75ed088ea542acac52f9f45987488d1c&dn=sd-v1-4.ckpt&tr=udp%3a%2f%2ftracker.openbittorrent.com%3a6969%2fannounce&tr=udp%3a%2f%2ftracker.opentrackr.org%3a1337

  9. 将该文件放入stable-diffusion-webui\models\Stable-diffusion目录下,并重命名为model.ckpt

  10. 如果出现No module 'xformers'。或者显存不够CUDA out of memory,修改stable-diffusion-webui\webui-user.bat文件,COMMANDLINE_ARGS后加入--medvram --reinstall-xformers --xformers

    1
    2
    3
    4
    5
    6
    7
    8
    @echo off

    set PYTHON=C:\Users\10246\AppData\Local\Programs\Python\Python310\python.exe
    set GIT=
    set VENV_DIR=venv
    set COMMANDLINE_ARGS=--medvram --reinstall-xformers --xformers

    call webui.bat
  11. 没有报错,出现To create a public link, set share=True in launch().,代表安装成功。

  12. 在浏览器的地址栏输入http://127.0.0.1:7860,进入ui管理界面。

  13. 设置中文:

    1. 点击Extensions
    2. 点击Available
    3. localization的勾去掉。
    4. 往下翻,找到zh_CN Localization,点击Install进行安装。
    5. 安装完,点击Settings,再点击左侧菜单的User interface
    6. 从右侧内容区域找到Localization,点击右侧的刷新按钮,点击下拉框,选择zh_CN
    7. 点击顶部的Apply settings,再点击Reload UI, 就完成中文设置。

生成窗口

  1. Editor的目录下创建一个C#脚本。写入如下的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       public class FloorManagerWindow : EditorWindow
    {
    [MenuItem("数据管理/地板")]
    public static void ShowWindow()
    {
    var window = EditorWindow.GetWindow(typeof(FloorManagerWindow), false, "地板数据编辑器");
    window.minSize = new Vector2(1100, 600);
    window.Show();
    }
    }
  2. 创建一个编辑器窗口,需要该脚本继承UnityEditor.EditorWindows类。
  3. 需要编写一个任意方法名的静态方法,如:public static void ShowWindow() { }。并给该方法增加一个[MenuItem]特性,如:[MenuItem("数据管理/地板")],该特性中的参数代表Unity编辑器的菜单的路径。该特性可以按照传入的参数在Unity编辑器上创建对应的菜单。并在该菜单项的点击事件触发时调用被特性标记的方法。
  4. 调用EditorWindow.GetWindow(typeof(FloorManagerWindow))方法会根据类型创建一个窗口。
  5. Show()方法会显示窗口。PS:EditorWindow.GetWindow()方法本身就会去调用Show()方法,猜想可能是为了在EditorWindow.GetWindow()之后修改了某些值,因此需要再手动调用Show()方法,以更新修改的值。

ScriptObject

在Unity的论坛中对于获取IMGUI的窗口宽度的帖子的示例中使用Screen.width,但是在不同分辨率下会出现值不准确。建议使用EditorWindow.position中的width值。

const

修饰的变量只能在声明的时候赋值进行初始化。

const默认是static的,不能手动为const添加一个static修饰符。

本质上是在编译阶段将使用const的代码处用常量值进行替换,类似c语言中的#define

readonly

readonly变量是运行时变量,可以在声明或构造函数中初始化。

readonly在第一次赋值后不能修改它的值。

区别

  1. const是一个编译时常量,readonly是一个运行时常量。
  2. const只能修饰基元类型,枚举类型或字符串类型,readonly没有限制。

建议

  1. 当值绝对不变的时候使用const,例如一周的天数是7。
  2. 当想要对类的不同实例使用不同的常量,使用readonly
  3. 当在项目的配置文件中配置的项目中的可能会变化的常量,使用static readonly

Editor Window

创建自定义编辑器窗口可以方便制作游戏开发的工具。

步骤: 1. 创建派生自EditorWindow类的脚本。 2. 使用代码触发窗口以显示自身。 3. 编写工具的GUI代码。

派生EditorWindow

为了制作编辑器窗口,脚本必须存储在名为Editor的文件夹内。然后在OnGUI()函数中编写GUI控件。

1
2
3
4
5
6
7
8
9
10
using UnityEngine;
using UnityEditor;
using System.Collections;

public class Example : EditorWindow
{
void OnGUI () {
// The actual window code goes here
}
}

显示窗口

为了在屏幕上显示窗口,需要制作一个显示它的菜单项。这是通过一个MenuItem的特性(Attributes)触发函数来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;
using UnityEditor;
using System.Collections;

class MyWindow : EditorWindow {
[MenuItem ("Window/My Window")]
public static void ShowWindow () {
EditorWindow.GetWindow(typeof(MyWindow));
}

void OnGUI () {
// The actual window code goes here
}
}

如果需要自定义创建窗口的位置和大小,可以使用EditorWindow.GetWindowWithRect()函数。

编写GUI代码

窗口的实际内容是用过OnGUI()函数来实现的。可以使用GUIGUILayer类中的控件,还可以使用仅Editor中可以使用的EditorGUIEditorGUILayout中的控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using UnityEditor;
using UnityEngine;

public class MyWindow : EditorWindow
{
string myString = "Hello World";
bool groupEnabled;
bool myBool = true;
float myFloat = 1.23f;

// Add menu item named "My Window" to the Window menu
[MenuItem("Window/My Window")]
public static void ShowWindow()
{
//Show existing window instance. If one doesn't exist, make one.
EditorWindow.GetWindow(typeof(MyWindow));
}

void OnGUI()
{
GUILayout.Label ("Base Settings", EditorStyles.boldLabel);
myString = EditorGUILayout.TextField ("Text Field", myString);

groupEnabled = EditorGUILayout.BeginToggleGroup ("Optional Settings", groupEnabled);
myBool = EditorGUILayout.Toggle ("Toggle", myBool);
myFloat = EditorGUILayout.Slider ("Slider", myFloat, -3, 3);
EditorGUILayout.EndToggleGroup ();
}
}

Property Drawers

Property Drawers可用于自定义Inspector中的某些控件的显示。在脚本中使用Attributes来控制显示的效果,或者通过序列化特殊的类来控制查看成员。

Property Drawers有两个用处:

  • 通过序列化类来自定义每个实例的GUI。
  • 在脚本成员使用自定义的Property Attributes来自定义GUI。

自定义可序列化类的GUI

这些不是编辑器脚本,而是游戏脚本的可序列化类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using UnityEngine;

enum IngredientUnit { Spoon, Cup, Bowl, Piece }

// Custom serializable class
[Serializable]
public class Ingredient
{
public string name;
public int amount = 1;
public IngredientUnit unit;
}

public class Recipe : MonoBehaviour
{
public Ingredient potionResult;
public Ingredient[] potionIngredients;
}

左侧为默认的不带自定义Property AttributesInspector,右侧为带有自定义Property AttributesInspector

使用CustomPropertyDrawer特性将IngredientDrawer类附加到可序列化类Ingredient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using UnityEditor;
using UnityEngine;

// IngredientDrawer
[CustomPropertyDrawer(typeof(Ingredient))]
public class IngredientDrawer : PropertyDrawer
{
// Draw the property inside the given rect
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Using BeginProperty / EndProperty on the parent property means that
// prefab override logic works on the entire property.
EditorGUI.BeginProperty(position, label, property);

// Draw label
position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);

// Don't make child fields be indented
var indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;

// Calculate rects
var amountRect = new Rect(position.x, position.y, 30, position.height);
var unitRect = new Rect(position.x + 35, position.y, 50, position.height);
var nameRect = new Rect(position.x + 90, position.y, position.width - 90, position.height);

// Draw fields - passs GUIContent.none to each so they are drawn without labels
EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("amount"), GUIContent.none);
EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("unit"), GUIContent.none);
EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("name"), GUIContent.none);

// Set indent back to what it was
EditorGUI.indentLevel = indent;

EditorGUI.EndProperty();
}
}

使用Property Attributes自定义脚本成员的GUI

使用Unity自带的RangeAttribute的内置PropertyAttribute来显示脚本成员的浮点或整数的取值范围。

1
2
[Range(0f, 10f)]
float myFloat = 0f;

也可以创建自己的PropertyAttribute类来实现该效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;

public class MyRangeAttribute : PropertyAttribute
{
readonly float min;
readonly float max;

void MyRangeAttribute(float min, float max)
{
this.min = min;
this.max = max;
}
}

现在已经有了PropertyAttribute,接下来需要绘制该特性在GUI上的效果。

绘制Property需要创建一个派生自PropertyDrawer的类。并且必须要具有CustomPropertyDrawer特性,并告知绘制的是那个Attribute类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEditor;
using UnityEngine;

// Tell the MyRangeDrawer that it is a drawer for properties with the MyRangeAttribute.
[CustomPropertyDrawer(typeof(MyRangeAttribute))]
public class RangeDrawer : PropertyDrawer
{
// Draw the property inside the given rect
void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// First get the attribute since it contains the range for the slider
MyRangeAttribute range = (MyRangeAttribute)attribute;

// Now draw the property as a Slider or an IntSlider based on whether it's a float or integer.
if (property.propertyType == SerializedPropertyType.Float)
EditorGUI.Slider(position, property, range.min, range.max, label);
else if (property.propertyType == SerializedPropertyType.Integer)
EditorGUI.IntSlider(position, property, (int) range.min, (int) range.max, label);
else
EditorGUI.LabelField(position, label.text, "Use MyRange with float or int.");
}
}

因为性能的原因EditorGUILayout的函数不能于PropertyDrawer一起使用

自定义编辑器

编辑模式下运行简单的脚本

ExecuteInEditMode特性可以使游戏运行时的脚本在编辑器模式下执行。

1
2
3
4
5
6
7
8
9
10
11
12
//C# Example (LookAtPoint.cs)
using UnityEngine;
[ExecuteInEditMode]
public class LookAtPoint : MonoBehaviour
{
public Vector3 lookAtPoint = Vector3.zero;

void Update()
{
transform.LookAt(lookAtPoint);
}
}

创建自定义编辑器

自定义编辑器时一个单独的脚本,它能将默认布局替换成任何你选择的编辑器控件。

创建自定义编辑器的步骤:

  1. 创建一个新的C#脚本并将其命名为“LookAtPointEditor”。
  2. 打开脚本并将内容替换为以下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
SerializedProperty lookAtPoint;

void OnEnable()
{
lookAtPoint = serializedObject.FindProperty("lookAtPoint");
}

public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(lookAtPoint);
serializedObject.ApplyModifiedProperties();
}
}

此类必须派生自EditorCustomEditor特性告诉Unity它作为编辑器该对那个组件生效。CanEditMultipleObjects特性告诉Unity,你可以选择多个有该编辑器所属组件的对象并能同时修改他们。

OnInspectorGUI()中执行的代码用于控制Inspector中显示的控件。可以在此处编写任何GUI的代码,它的工作原理与OnGUI()相同。可以访问目标游戏对象的属性。

以下代码扩展了编辑器脚本,用于显示一条信息,目标点高于还是低于游戏对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
SerializedProperty lookAtPoint;

void OnEnable()
{
lookAtPoint = serializedObject.FindProperty("lookAtPoint");
}

public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(lookAtPoint);

if (lookAtPoint.vector3Value.y > (target as LookAtPoint).transform.position.y)
{
EditorGUILayout.LabelField("(Above this object)");
}
if (lookAtPoint.vector3Value.y < (target as LookAtPoint).transform.position.y)
{
EditorGUILayout.LabelField("(Below this object)");
}

serializedObject.ApplyModifiedProperties();
}
}

Scene View 扩展

你可以向Scene View添加额外的代码。通过在你的自定义编辑器脚本中实现OnSceneGUI()方法。

OnsceneGUI()OnInspectorGUI()类似,除了是运行在Scene view中。

以下所有代码被设计在3D Scene view中工作,不适用2D Scene。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
SerializedProperty lookAtPoint;

void OnEnable()
{
lookAtPoint = serializedObject.FindProperty("lookAtPoint");
}

public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(lookAtPoint);

if (lookAtPoint.vector3Value.y > (target as LookAtPoint).transform.position.y)
{
EditorGUILayout.LabelField("(Above this object)");
}
if (lookAtPoint.vector3Value.y < (target as LookAtPoint).transform.position.y)
{
EditorGUILayout.LabelField("(Below this object)");
}

serializedObject.ApplyModifiedProperties();
}

public void OnSceneGUI()
{
var t = (target as LookAtPoint);

EditorGUI.BeginChangeCheck();
Vector3 pos = Handles.PositionHandle(t.lookAtPoint, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Move point");
t.lookAtPoint = pos;
t.Update();
}
}
}

如果你想要添加2D GUI 对象,你需要将GUI代码包裹在Handles.BeginGUI()Handles.EndGUI()之间来调用。

树状图

重要的类和方法

TreeView类之外最重要的类是TreeViewItemTreeViewState

TreeViewState包含与编辑器中与TreeView字段交互时更改的状态信息。例如:选择状态,展开状态和滚动状态。TreeViewState是唯一可序列化的状态。TreeView本身是不可序列化的,它是在构造或重新加载时进行的数据重新构建。在你的EditorWindow派生类中添加一个TreeViewState字段确保当重新加载脚本或进入Play模式的时候用户修改的状态不会丢失。

TreeViewItem包含关于TreeView单个节点的数据,并用于在编辑器中构建tree结构的代理。每一个TreeViewItem必须用在TreeView中所有节点中唯一的整数Id来构造。Id用于在tree中查找单个节点的选择状态,展开状态和导航。如果tree表示的时Unity objects,使用每个对象的GetInstanceID作为TreeViewItem的Id。当重新加载脚本或在编辑器中进入播放模式时,在TreeViewState中使用Id来持久保存用户更改的状态。所有的TreeViewItem都有一个深度属性,它代表视觉的缩进。

BuildRootTreeView类的一个抽象方法,必须实现它才能创建TreeView。使用此方法处理创建treeroot节点.任何时候tree被调用时都会调用Reload方法。对于使用少量数据集的简单tree,可以在BuildRoot中的root节点下创建全部的ItemViewItem。对于非常大的tree,每次重新加载的时候创建全部的节点不是最佳的方案。对于这种情况,创建root,然后重写BuildRows方法,只创建当前行的节点。

BuildRows是一个虚方法,默认实现会根据BuildRoot中创建的完整tree处理构建行列表。如果只在BuildRoot中创建了root,则应该重写此方法以处理展开的行。

此图是TreeView的生命周期

初始化TreeView

当从TreeView对象调用Reload方法时,TreeView被初始化。

有两种方式来设置TreeView:

  1. 创建完整的tree:为tree模型数据中的所有节点创建TreeViewItem。这是默认的设置,需要的代码更少。当从TreeView对象调用BuildRoot时,将构建完整的tree。
  2. 只创建展开的节点:这种方法要求重写BuildRows以手动控制所显示的行,并且BuildRoot仅用于创建root的TreeViewItem。这种方法最适合大型数据数据集或经常变化的数据。

有三种方法可以设置TreeViewItem: * 创建具有从一开始就初始化的子级,父级和深度的TreeViewItem。 * 创建带有父级和子级的TreeViewItem,然后使用SetupDepthsFromParentsAndChildren来设置深度。 * 只使用深度信息创建TreeViewItem,然后使用SetupDepthsFromParentsAndChildren来设置父引用和子引用。

例子1:简单示例

一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class SimpleTreeView : TreeView
{
public SimpleTreeView(TreeViewState treeViewState)
: base(treeViewState)
{
Reload();
}

protected override TreeViewItem BuildRoot ()
{
// BuildRoot is called every time Reload is called to ensure that TreeViewItems
// are created from data. Here we create a fixed set of items. In a real world example,
// a data model should be passed into the TreeView and the items created from the model.

// This section illustrates that IDs should be unique. The root item is required to
// have a depth of -1, and the rest of the items increment from that.
var root = new TreeViewItem {id = 0, depth = -1, displayName = "Root"};
var allItems = new List<TreeViewItem>
{
new TreeViewItem {id = 1, depth = 0, displayName = "Animals"},
new TreeViewItem {id = 2, depth = 1, displayName = "Mammals"},
new TreeViewItem {id = 3, depth = 2, displayName = "Tiger"},
new TreeViewItem {id = 4, depth = 2, displayName = "Elephant"},
new TreeViewItem {id = 5, depth = 2, displayName = "Okapi"},
new TreeViewItem {id = 6, depth = 2, displayName = "Armadillo"},
new TreeViewItem {id = 7, depth = 1, displayName = "Reptiles"},
new TreeViewItem {id = 8, depth = 2, displayName = "Crocodile"},
new TreeViewItem {id = 9, depth = 2, displayName = "Lizard"},
};

// Utility method that initializes the TreeViewItem.children and .parent for all items.
SetupParentsAndChildrenFromDepths (root, allItems);

// Return root of the tree
return root;
}
}

也可以先创建节点,再设置父节点和子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected override TreeViewItem BuildRoot()
{
var root = new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
var animals = new TreeViewItem { id = 1, displayName = "Animals" };
var mammals = new TreeViewItem { id = 2, displayName = "Mammals" };
var tiger = new TreeViewItem { id = 3, displayName = "Tiger" };
var elephant = new TreeViewItem { id = 4, displayName = "Elephant" };
var okapi = new TreeViewItem { id = 5, displayName = "Okapi" };
var armadillo = new TreeViewItem { id = 6, displayName = "Armadillo" };
var reptiles = new TreeViewItem { id = 7, displayName = "Reptiles" };
var croco = new TreeViewItem { id = 8, displayName = "Crocodile" };
var lizard = new TreeViewItem { id = 9, displayName = "Lizard" };

root.AddChild(animals);
animals.AddChild(mammals);
animals.AddChild(reptiles);
mammals.AddChild(tiger);
mammals.AddChild(elephant);
mammals.AddChild(okapi);
mammals.AddChild(armadillo);
reptiles.AddChild(croco);
reptiles.AddChild(lizard);

SetupDepthsFromParentsAndChildren(root);

return root;
}

在EditorWindow派生类中处理编写的SimpleTreeView类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.IMGUI.Controls;

class SimpleTreeViewWindow : EditorWindow
{
// SerializeField is used to ensure the view state is written to the window
// layout file. This means that the state survives restarting Unity as long as the window
// is not closed. If the attribute is omitted then the state is still serialized/deserialized.
[SerializeField] TreeViewState m_TreeViewState;

//The TreeView is not serializable, so it should be reconstructed from the tree data.
SimpleTreeView m_SimpleTreeView;

void OnEnable ()
{
// Check whether there is already a serialized view state (state
// that survived assembly reloading)
if (m_TreeViewState == null)
m_TreeViewState = new TreeViewState ();

m_SimpleTreeView = new SimpleTreeView(m_TreeViewState);
}

void OnGUI ()
{
m_SimpleTreeView.OnGUI(new Rect(0, 0, position.width, position.height));
}

// Add menu named "My Window" to the Window menu
[MenuItem ("TreeView Examples/Simple Tree Window")]
static void ShowWindow ()
{
// Get existing open window or if none, make a new one:
var window = GetWindow<SimpleTreeViewWindow> ();
window.titleContent = new GUIContent ("My Window");
window.Show ();
}
}

例子2:多列树状图

将编写一个名为MultiColumnHeader的树状图类。支持重命名,多选,使用普通IMGUI控件,按列排序,搜索功能。

序列化数据的数据模型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Serializable]
//The TreeElement data class is extended to hold extra data, which you can show and edit in the front-end TreeView.
internal class MyTreeElement : TreeElement
{
public float floatValue1, floatValue2, floatValue3;
public Material material;
public string text = "";
public bool enabled = true;

public MyTreeElement (string name, int depth, int id) : base (name, depth, id)
{
floatValue1 = Random.value;
floatValue2 = Random.value;
floatValue3 = Random.value;
}
}

ScriptableObject类可以确保在序列化tree时数据保留在资产中。

1
2
3
4
5
6
7
8
9
10
11
[CreateAssetMenu (fileName = "TreeDataAsset", menuName = "Tree Asset", order = 1)]
public class MyTreeAsset : ScriptableObject
{
[SerializeField] List<MyTreeElement> m_TreeElements = new List<MyTreeElement> ();

internal List<MyTreeElement> treeElements
{
get { return m_TreeElements; }
set { m_TreeElements = value; }
}
}

简介

IMGUI是一个代码驱动的GUI系统。由对实现它的任何脚本上的OnGUI函数的调用进行驱动。

IMGUI通常用于:

  • 创建游戏内调试显示和工具。
  • 创建脚本组件自定义的Inspectors。
  • 创建editor窗口和工具来扩展Unity本身。

基础

OnGUI()代码每帧都会被调用,因此无需显式创建或销毁GUI控件。

声明GUI控件的三个要素为:控件类型,位置和内容。

控件类型 Type( 位置 Rect, 内容 String/GUIContent );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
void OnGUI ()
{
if (Time.time % 2 < 1)
{
if (GUI.Button (new Rect (10,10,200,20), "Meet the flashing button"))
{
print ("You clicked me!");
}
}
}
}

控件类型

控件类型通过调用Unity的GUI类或GUILayout类中的函数来声明。

GUI.Label

标签控件GUI.Label是非交互式的。它只用于显示无法单击或拖动。最适合只显示信息。

1
2
3
4
5
6
7
8
9
10
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
void OnGUI ()
{
GUI.Label (new Rect (25, 25, 100, 30), "Label");
}
}

GUI.Button

按钮控件GUI.Button是典型的交互式按钮。鼠标按下时,无论鼠标保持按下多久都只会响应一次。释放鼠标按钮后,将立即发生响应。

鼠标单击时返回true,否则为false

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
void OnGUI ()
{
if (GUI.Button (new Rect (25, 25, 100, 30), "Button"))
{
// This code is executed when the Button is clicked
}
}
}

GUI.RepeatButton

重复按钮控件GUI.RepeatButton是常规按钮的变体。GUI.RepeatButton将响应鼠标按钮保持按下的每一帧。这允许你创建单击并按住功能。

鼠标按住时每一帧都返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
void OnGUI ()
{
if (GUI.RepeatButton (new Rect (25, 25, 100, 30), "RepeatButton"))
{
// This code is executed every frame that the RepeatButton remains clicked
}
}
}

GUI.TextField

文本输入框控件GUI.TextField是一个可编辑单行文本字段。

文本字段将始终显示一个字符串。必须提供要在文本字段中显示的字符串。对字符串进行编辑时,TextField函数将返回编辑后的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private string textFieldString = "text field";

void OnGUI ()
{
textFieldString = GUI.TextField (new Rect (25, 25, 100, 30), textFieldString);
}
}

GUI.TextArea

文本输入域控件GUI.TextArea是一个可编辑多行文本区域。

文本字段将始终显示一个字符串。必须提供要在文本字段中显示的字符串。对字符串进行编辑时,TextField函数将返回编辑后的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private string textAreaString = "text area";

void OnGUI ()
{
textAreaString = GUI.TextArea (new Rect (25, 25, 100, 30), textAreaString);
}
}

GUI.Toggle

选择框控件GUI.Toggle会创建一个保持开/关的选项框。用户可以通过单击来改变状态。

true为选中。

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private bool toggleBool = true;

void OnGUI ()
{
toggleBool = GUI.Toggle (new Rect (25, 25, 100, 30), toggleBool, "Toggle");
}
}

GUI.Toolbar

工具条控件GUI.Toolbar本质上是一组只能有单个被选中的按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private int toolbarInt = 0;
private string[] toolbarStrings = {"Toolbar1", "Toolbar2", "Toolbar3"};

void OnGUI ()
{
toolbarInt = GUI.Toolbar (new Rect (25, 25, 250, 30), toolbarInt, toolbarStrings);
}
}

GUI.SelectionGrid

选择网格控件GUI.SelectionGrid是一个多行的GUI.Toolbar。可以确定网格中的列和行。同一组中只能有个一个按钮被选中。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private int selectionGridInt = 0;
private string[] selectionStrings = {"Grid 1", "Grid 2", "Grid 3", "Grid 4"};

void OnGUI ()
{
selectionGridInt = GUI.SelectionGrid (new Rect (25, 25, 300, 60), selectionGridInt, selectionStrings, 2);
}
}

GUI.HorizontalSlider

水平滑块控件GUI.HorizontalSlider是一个典型的水平滑动按钮,可以拖动它在预定的最小值和最大值之间更改值。

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private float hSliderValue = 0.0f;
void OnGUI ()
{
hSliderValue = GUI.HorizontalSlider (new Rect (25, 25, 100, 30), hSliderValue, 0.0f, 10.0f);
}
}

GUI.VerticalSlider

垂直滑块控件GUI.VerticalSlider是一个典型的垂直滑动按钮,可以拖动它在预定的最小值和最大值之间更改值。

1
2
3
4
5
6
7
8
public class GUITest : MonoBehaviour 
{
private float vSliderValue = 0.0f;
void OnGUI ()
{
vSliderValue = GUI.VerticalSlider (new Rect (25, 25, 100, 30), vSliderValue, 10.0f, 0.0f);
}
}

GUI.HorizontalScrollbar

水平滚动条控件GUI.HorizontalScrollbar类似于滑块控件。被用于导航GUI.BeginScrollView/GUI.EndScrollView滚动视图控件。

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private float hScrollbarValue;
void OnGUI ()
{
hScrollbarValue = GUI.HorizontalScrollbar (new Rect (25, 25, 100, 30), hScrollbarValue, 1.0f, 0.0f, 10.0f);
}
}

GUI.VerticalScrollbar

垂直滚动条控件GUI.VerticalScrollbar类似于滑块控件。被用于导航GUI.BeginScrollView/GUI.EndScrollView滚动视图控件。

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private float vScrollbarValue;
void OnGUI ()
{
vScrollbarValue = GUI. VerticalScrollbar (new Rect (25, 25, 100, 30), vScrollbarValue, 1.0f, 10.0f, 0.0f);
}
}

GUI.BeginScrollView/GUI.EndScrollView

滚动视图GUI.BeginScrollView/GUI.EndScrollView是将范围内的控件加入到一个带有滚动条的视图。

ScrollViews需要两个Rects作为参数。第一个Rect定义屏幕上可以查看的滚动视图的位置和大小。第二个Rect定义可视区域内包含的空间大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private Vector2 scrollViewVector = Vector2.zero;
private string innerText = "I am inside the ScrollView";

void OnGUI ()
{
// Begin the ScrollView
scrollViewVector = GUI.BeginScrollView (new Rect (25, 25, 100, 100), scrollViewVector, new Rect (0, 0, 400, 400));

// Put something inside the ScrollView
innerText = GUI.TextArea (new Rect (0, 0, 400, 400), innerText);

// End the ScrollView
GUI.EndScrollView();
}
}

GUI.Window

窗口控件Window是可拖拽的容器。单击时,他们会接收和失去焦点。每个窗口控件都有一个ID,其内容在窗口控件具有焦点时调用的单独函数中声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
private Rect windowRect = new Rect (20, 20, 120, 50);

void OnGUI ()
{
windowRect = GUI.Window (0, windowRect, WindowFunction, "My Window");
}

void WindowFunction (int windowID)
{
// Draw any Controls inside the window here
}
}

GUI.changed

要检测用户是否在GUI中执行了任何操作(例如:单击按钮,拖动滑块),请从脚本中读取GUI.changed的值。执行了操作,将返回true

一种常见的方案是工具栏,希望根据单击工具栏中的按钮来改变特定的值。但不想每次调用OnGUI()时分配值,而是在单击其中一个按钮时才分配值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{

private int selectedToolbar = 0;
private string[] toolbarStrings = {"One", "Two"};

void OnGUI ()
{
// Determine which button is active, whether it was clicked this frame or not
selectedToolbar = GUI.Toolbar (new Rect (50, 10, Screen.width - 100, 30), selectedToolbar, toolbarStrings);

// If the user clicked a new Toolbar button this frame, we'll process their input
if (GUI.changed)
{
Debug.Log("The toolbar was clicked");

if (0 == selectedToolbar)
{
Debug.Log("First button was clicked");
}
else
{
Debug.Log("Second button was clicked");
}
}
}
}

控件位置

Position是任何GUI控制函数的第一个参数。该参数是一个Rect类型。

可以通过获取屏幕宽度Screen.width和高度Screen.height来控制控件的相对位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{

void OnGUI()
{
GUI.Box (new Rect (0,0,100,50), "Top-left");
GUI.Box (new Rect (Screen.width - 100,0,100,50), "Top-right");
GUI.Box (new Rect (0,Screen.height - 50,100,50), "Bottom-left");
GUI.Box (new Rect (Screen.width - 100,Screen.height - 50,100,50), "Bottom-right");
}

}

控件内容

GUI控件的第二个参数用于控制控件显示的内容。可以显示文本或图像。

如果需要同时显示图像和文本。可以使用GUIContent类型作为参数,并定义GUIContent的文字和图像。

还可以通过GUIContent定义tooltip,并在鼠标悬停时显示提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
public Texture2D controlTexture;

public Texture2D icon;

void OnGUI ()
{
//显示文本
GUI.Label(new Rect(10, 0, 100, 50), "This is the text string for a Label Control");

//显示图片
GUI.Label(new Rect(10, 70, 100, 50), controlTexture);

//按钮显示文字
if(GUI.Button(new Rect(10, 130, 100, 20), "This is text"))
{
print("you clicked the text button");
}

//按钮显示图片
if(GUI.Button(new Rect(10, 160, 100, 50), icon))
{
print("you clicked the icon");
}

//方形区域通过GUIContent显示文字和图片
GUI.Box(new Rect(10, 220, 100, 50), new GUIContent("This is text", icon));

//按钮通过GUIContent显示文字和悬浮提示
GUI.Button(new Rect(10, 280, 100, 20), new GUIContent("Click me1", "This is the tooltip1"));

//定义悬浮提示的位置和大小
GUI.Label(new Rect(10, 310, 200, 20), GUI.tooltip);

//按钮通过GUIContent显示文字,图片和悬浮提示
GUI.Button(new Rect(10, 340, 100, 20), new GUIContent("Click me2", icon, "This is the tooltip2"));

//定义悬浮提示的位置和大小
GUI.Label(new Rect(10, 380, 200, 20), GUI.tooltip);
}
}

自定义控件

自定义外观

在未自定义GUIStyles的情况下,会使用Unity默认的GUIStyles

GUIStyles

所有GUI控件的函数的最后一个可选参数:用于选择控件的GUIStyles。省略此项,使用Unity默认GUIStyles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour
{
void OnGUI ()
{
// Make a label that uses the "box" GUIStyle.
GUI.Label (new Rect (0,0,200,100), "Hi - I'm a label looking like a box", "box");

// Make a button that uses the "toggle" GUIStyle
GUI.Button (new Rect (10,140,180,20), "This is a button", "toggle");
}
}

声明公共变量的GUIStyle

声明公共变量的GUIStyle时,可以在Inspector中查看和配置GUIStyle

1
2
3
4
5
6
7
8
9
10
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
public GUIStyle customButton;
void OnGUI () {
// Make a button. We pass in the GUIStyle defined above as the style to use
GUI.Button (new Rect (10,10,150,20), "I am a Custom Button", customButton);
}
}

动态更改GUIStyle属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class Fontsize : MonoBehaviour
{
void OnGUI ()
{
//Set the GUIStyle style to be label
GUIStyle style = GUI.skin.GetStyle ("label");

//Set the style font size to increase and decrease over time
style.fontSize = (int)(20.0f + 10.0f * Mathf.Sin (Time.time));

//Create a label and display with the current settings
GUI.Label (new Rect (10, 10, 200, 80), "Hello World!");
}
}

GUISkins

GUISkinsGUIStyles的集合。GUIStyles定义GUI控件的外观。如果使用GUIStyles,则不必使用GUISkins

当有大量不同的GUIStyles需要使用时,可以在单个GUISkins中定义他们。

创建GUISkins

Assets->Create->GUI Skin

应用GUISkins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {

public GUISkin mySkin;
void OnGUI () {
// Assign the skin to be the one currently used.
GUI.skin = mySkin;

// Make a button. This will get the default "button" style from the skin assigned to mySkin.
GUI.Button (new Rect (10,10,150,20), "Skinned Button");
}
}

布局模式

分为Fixed布局和Automatic布局。

OnGUI()中可以同时使用两种布局模式。

界面的元素数量和位置确定的时候使用Fixed,反之使用Automatic模式。

使用Automatic模式的主要区别:

  • 使用GUILayout代替GUI
  • 使用Automatic布局控件不需要Rect()函数。
1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
void OnGUI () {
// Fixed Layout
GUI.Button (new Rect (25,25,100,30), "I am a Fixed Layout Button");

// Automatic Layout
GUILayout.Button ("I am an Automatic Layout Button");
}
}

排列控件

分为: Fixed布局-GroupsAutomatic布局-AreasAutomatic布局-HorizontalAutomatic布局-Vertical

获取当前窗口大小

当前窗口宽度:Screen.width

当前窗口高度:Screen.height

Fixed布局-Groups

使用GUI.BeginGroup()和GUI.EndGroup()函数。组内控件是根据组的左上角,而不是屏幕的左上角进行定位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
void OnGUI () {
// Make a group on the center of the screen
GUI.BeginGroup (new Rect (Screen.width / 2 - 50, Screen.height / 2 - 50, 100, 100));
// All rectangles are now adjusted to the group. (0,0) is the topleft corner of the group.

// We'll make a box so you can see where the group is on-screen.
GUI.Box (new Rect (0,0,100,100), "Group is here");
GUI.Button (new Rect (10,40,80,30), "Click me");

// End the group we started above. This is very important to remember!
GUI.EndGroup ();
}
}

可以使用多个组互相嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
// background image that is 256 x 32
public Texture2D bgImage;
// foreground image that is 256 x 32
public Texture2D fgImage;
// a float between 0.0 and 1.0
public float playerEnergy = 1.0f;

void OnGUI () {
// Create one Group to contain both images
// Adjust the first 2 coordinates to place it somewhere else on-screen
GUI.BeginGroup (new Rect (0,0,256,32));

// Draw the background image
GUI.Box (new Rect (0,0,256,32), bgImage);

// Create a second Group which will be clipped
// We want to clip the image and not scale it, which is why we need the second Group
GUI.BeginGroup (new Rect (0,0,playerEnergy * 256, 32));

// Draw the foreground image
GUI.Box (new Rect (0,0,256,32), fgImage);

// End both Groups
GUI.EndGroup ();

GUI.EndGroup ();
}
}

Automatic布局-Areas

Automatic的默认模式。

默认是类似于FixedGroups布局。

以左上角为锚点。

区域内具有可见元素的控件宽度会拉整个区域的宽度。

使用GUILayout.BeginArea()GUILayout.EndArea()来创建区域。

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
void OnGUI () {
GUILayout.Button ("I am not inside an Area");
GUILayout.BeginArea (new Rect (Screen.width/2, Screen.height/2, 300, 300));
GUILayout.Button ("I am completely inside an Area");
GUILayout.EndArea ();
}
}

Automatic布局-Horizontal和Vertical

通过使用GUILayout.BeginHorizontal()GUILayout.EndHorizontal()GUILayout.BeginVertical()GUILayout.EndVertical()函数,决定区域内的组件的排列放松。可以嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
private float sliderValue = 1.0f;
private float maxSliderValue = 10.0f;

void OnGUI()
{
// Wrap everything in the designated GUI Area
GUILayout.BeginArea (new Rect (0,0,200,60));

// Begin the singular Horizontal Group
GUILayout.BeginHorizontal();

// Place a Button normally
if (GUILayout.RepeatButton ("Increase max\nSlider Value"))
{
maxSliderValue += 3.0f * Time.deltaTime;
}

// Arrange two more Controls vertically beside the Button
GUILayout.BeginVertical();
GUILayout.Box("Slider Value: " + Mathf.Round(sliderValue));
sliderValue = GUILayout.HorizontalSlider (sliderValue, 0.0f, maxSliderValue);

// End the Groups and Area
GUILayout.EndVertical();
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
}

GUILayoutOptions定义控件

可以使用GUILayoutOptions覆盖某些自动布局参数。

1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
void OnGUI () {
GUILayout.BeginArea (new Rect (100, 50, Screen.width-200, Screen.height-100));
GUILayout.Button ("I am a regular Automatic Layout Button");
GUILayout.Button ("My width has been overridden", GUILayout.Width (95));
GUILayout.EndArea ();
}
}

扩展IMGUI

复合控件

在GUI中可能出现两种类型的控件同时出现的情况。就可以创建同时包含多种控件的复合控件。

例如:该控件同时包含LabelHorizontalSlider

在此示例中,LabelSlider()会返回正确的值,以使其具有交互性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
private float mySlider = 1.0f;
void OnGUI () {
mySlider = LabelSlider (new Rect (10, 100, 100, 20), mySlider, 5.0f, "Label text here");
}

float LabelSlider (Rect screenRect, float sliderValue, float sliderMaxValue, string labelText) {
GUI.Label (screenRect, labelText);

// <- Push the Slider to the end of the Label
screenRect.x += screenRect.width;

sliderValue = GUI.HorizontalSlider (screenRect, sliderValue, 0.0f, sliderMaxValue);
return sliderValue;
}
}

静态复合控件

通过使用静态函数,可以创建自己的复合控件集合。这样能使控件复用。

这样其他脚本需要使用LabelSlider就只需要调用CompoundControls.LabelSlider(),传入正确的参数和处理返回值就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;
using System.Collections;

public class CompoundControls : MonoBehaviour {
public static float LabelSlider (Rect screenRect, float sliderValue, float sliderMaxValue, string labelText) {
GUI.Label (screenRect, labelText);

// <- Push the Slider to the end of the Label
screenRect.x += screenRect.width;

sliderValue = GUI.HorizontalSlider (screenRect, sliderValue, 0.0f, sliderMaxValue);
return sliderValue;
}
}

可复用的RGB滑块示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
public Color myColor;
void OnGUI () {
myColor = RGBSlider (new Rect (10,10,200,10), myColor);
}

Color RGBSlider (Rect screenRect, Color rgb) {
rgb.r = GUI.HorizontalSlider (screenRect, rgb.r, 0.0f, 1.0f);

// <- Move the next control down a bit to avoid overlapping
screenRect.y += 20;
rgb.g = GUI.HorizontalSlider (screenRect, rgb.g, 0.0f, 1.0f);

// <- Move the next control down a bit to avoid overlapping
screenRect.y += 20;

rgb.b = GUI.HorizontalSlider (screenRect, rgb.b, 0.0f, 1.0f);
return rgb;
}
}

现在在其他复合组件的基础上来构建复合组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using System.Collections;

public class GUITest : MonoBehaviour {
public Color myColor;
void OnGUI () {
myColor = RGBSlider (new Rect (10,10,200,30), myColor);
}

Color RGBSlider (Rect screenRect, Color rgb) {
rgb.r = CompoundControls.LabelSlider (screenRect, rgb.r, 1.0f, "Red");

// <- Move the next control down a bit to avoid overlapping
screenRect.y += 20;
rgb.g = CompoundControls.LabelSlider (screenRect, rgb.g, 1.0f, "Green");

// <- Move the next control down a bit to avoid overlapping
screenRect.y += 20;

rgb.b = CompoundControls.LabelSlider (screenRect, rgb.b, 1.0f, "Blue");

return rgb;
}
}

分类

Unity提供给用户的UI系统分为:

  • UI Toolkit
  • uGUI(The Unity UI package)
  • IMGUI

UI Toolkit

UI Toolkit是Unity最新的UI系统。基于标准Web技术。但是缺少uGUI和IMGUI的一些功能。

uGUI

是一个较旧的,基于游戏对象的UI系统,可以用于开发游戏和应用程序Runtime的UI。在uGUI中,可以使用组件和游戏视图来排列,定位用户界面并设置其样式。支持高级渲染和文本功能。

IMGUI

IMGUI(Immediate Mode Graphical User Interface)即时模式图形用户界面,是一个代码驱动的UI工具包。通过实现脚本的OnGUI方法来绘制和管理UI。可以使用IMGUI创建自定义的Inspectors,Unity Editor扩展和游戏内的debugging显示。不建议用于Runtime的UI。IMGUI自由度比UI Toolkit更高。

建议

Runtime的UI开发建议使用uGUI,可以考虑使用UI Toolkit作为替代。 Editor的UI开发建议使用IMGUI,可以考虑使用UI Toolkit作为替代。

步骤

  1. 打开Unity,点击菜单“Edit->Preferences...”
  2. 在打开的窗口左侧选择“External Tools”
  3. 根据需要显示的代码类型勾选“Generate .csproj file for:”的选项(默认只有“Embedded packages”和“Local packages”被勾选)
  1. 点击“Regenerate project files”按钮,生成项目文件

TBD Flow

Trunk based Development,又叫 主干开发 ,是一套代码分支管理策略,开发人员之间通过约定向被指定为 主干 的分支提交代码,以此抵抗因为长期存在的多分支导致的开发压力。此举可 避免分支合并的困扰,保证随时拥有可发布的版本 。“主干”这个词隐喻了树木生长的场景,树木最粗最长的部位是主干,分支从主干分离出来但是长度有限。

使用主干开发后,我们的代码库原则上就只能有一个 Trunk 分支即 master 分支了,所有新功能的提交也都提交到 master 分支上,保证每次提交后 master 分支都是可随时发布的状态。没有了分支的代码隔离,测试和解决冲突都变得简单,持续集成也变得稳定了许多,但也有如下几个问题:

  • 如何避免发布引入未完成 Feature,答案是使用 Feature Toggle 。在代码库里加一个特性开关来随时打开和关闭新特性是最容易想到的也是最容易被质疑的解决方案。Feature Toggle 是有成本的,不管是在加 Toggle 时的代码设计,还是在移除 Toggle 时的人力成本和风险,都是需要和它带来的价值进行衡量的。
  • 如何进行线上 Bug Fix,答案是在发布时打上 Release Tag,一旦发现这个版本有问题,如果此时 master 分支还没有其他提交,那可以直接在 master 分支上 Hot Fix 然后合并至 release 分支;如果 master 分支已经有了提交就需要做以下三件事:
    • 从 Release Tag 创建发布分支。
    • 在 master 上做 Fix Bug 提交。
    • 将 Fix Bug 提交 Cherry Pick 到 release 分支。
    • 为 release 分支打上新的 Tag 并做一次发布。

优点

  • 分支少,与现有工作流程差别不大,易于上手
  • 没有长期分离的其他开发分支,合并冲突少
  • 利于持续部署和持续交付的支持
  • 在不满足现有需求后方便扩展到其他管理策略,如:TBD++ Flow,GitLab Flow

版本号管理

版本规则

发布release的命名

1
release-主版本号.次版本号

如:

1
release-0.1

Release版本需要一个或多个Bugfix,在交付给测试的时候需要将修订号+1

1
主版本号.次版本号.修订号

如:

1
2
0.1.0
0.1.1

注意事项

  • release的bug修复代码,需要与功能迭代的代码分开提交,并及时pick到release上。
  • 提交代码需要先拉取再提交
  • 确保提交的代码项目能够编译通过与正常运行。

扩展

如不满足现有的需求可以灵活扩展成TBD++ Flow

0%