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

今天遇到个之前觉得很常见的代码写法, 在成员方法中直接调用成员变量, 代码A 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test
{
public static Random Random { get; set; }

public void TestRandom()
{
this.Random = new Random();

if(this.Random == null)
return;

var num = this.Random.Next();

var num1 = this.Random.Next();
}
}

存在的问题

问题一, 在多线程下可能存在问题

在多线程下可能存在执行this.Random == null之后, 被其他线程将成员变量Random将其改为null, 这样将会导致空指针

因此使用局部变量接收成员变量的引用, 代码B 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test
{
public Random Random { get; set; }

public void TestRandom()
{
this.Random = new Random();

var random = this.Random;

if(random == null)
return;

var num = random.Next();

var num1 = random.Next();
}
}

问题二, 性能上访问成员变量不如访问局部变量快

直接调用成员属性生成的汇编

var num = this.Random.Next();

1
2
3
4
5
6
7
8
9
10
11
00007FF7E7A85D6D  mov         rcx,qword ptr [rbp+80h]  
00007FF7E7A85D74 call 方法存根对象: TestVariable.Test.get_Random() (07FF7E7A859B0h)
00007FF7E7A85D79 mov qword ptr [rbp+38h],rax
00007FF7E7A85D7D mov rcx,qword ptr [rbp+38h]
00007FF7E7A85D81 mov rax,qword ptr [rbp+38h]
00007FF7E7A85D85 mov rax,qword ptr [rax]
00007FF7E7A85D88 mov rax,qword ptr [rax+40h]
00007FF7E7A85D8C call qword ptr [rax+28h]
00007FF7E7A85D8F mov dword ptr [rbp+34h],eax
00007FF7E7A85D92 mov ecx,dword ptr [rbp+34h]
00007FF7E7A85D95 mov dword ptr [rbp+5Ch],ecx

调用局部变量生成的汇编

var num = random.Next();

1
2
3
4
5
6
7
8
00007FF7E7A88D27  mov         rcx,qword ptr [rbp+48h]  
00007FF7E7A88D2B mov rax,qword ptr [rbp+48h]
00007FF7E7A88D2F mov rax,qword ptr [rax]
00007FF7E7A88D32 mov rax,qword ptr [rax+40h]
00007FF7E7A88D36 call qword ptr [rax+28h]
00007FF7E7A88D39 mov dword ptr [rbp+24h],eax
00007FF7E7A88D3C mov ecx,dword ptr [rbp+24h]
00007FF7E7A88D3F mov dword ptr [rbp+44h],ecx

直接调用成员属性每次都需要去找到对应的成员的地址再调用, 而局部变量则不需要

结论

如果在一个方法内需要多次调用一个成员变量/成员属性/静态成员变量, 建议使用一个局部变量存储引用, 通过局部变量去调用。

Git开发模式分类

开发模式从本质上可以分为两类: 特性分支开发模式(Feature Branch Development)主干开发模式(Trunk Based Development).

特性分支开发模式

介绍

特性分支开发模式是指为一个或多个特定的需求 / 缺陷 / 任务创建代码分支(branch),在其上完成相应的开发(一般经过增量测试)后,把它合并(merge)到主干 / 集成分支的开发模式。

通常这种分支生命期会持续一段时间,从几天到几周不等,极少数情况甚至以月算。

以Git-Flow为例:

优点

特性开发周期宽松:因为生命期可以较长,较大的需求特性可以在宽松的时间内完成再合入主干;

分支测试的时间宽松:因为生命期可以较长,可以有较多时间对分支进行测试,甚至手工测试;

缺点

分支管理复杂:原因在于大量采用代码分支,且来源分支和合入目标分支各异,操作复杂 —— 以上图为例,可以从 master(Tag 1.0.0) 拉出 hotfix 1.0.2 分支,然后合入到 develop 分支,开发阶段结束后合入到 release branches,发布后合入 master,非常复杂,很容易出错;

合并冲突多、解决难:分支生命期越长,意味着与主干的代码差异越大,冲突概率越高,冲突的解决难度越大(甚至成为不可能);

迭代速度慢:特性分支生命期长(数天至数周)意味着特性上线速度慢,相应的迭代速度也慢;

需要较多测试环境:每个特性分支都需要分配至少 1 个测试环境,且长期占用(有状态);

适用环境

对版本迭代速度要求不高

测试自动化程度低,或说主要靠人工测试的

常用模式

Git-Flow, Github-Flow, Gitlab-Flow

主干开发模式

介绍

主干开发,是指开发人员直接向主干(习惯上主干分支通常为:trunk 或 master)提交 / 推送代码。通常,开发团队的成员 1 天至少 1 次地将代码提交到主干分支。在到达发布条件时,从主干拉出发布分支(通常为 release),用于发布。若发现缺陷,直接在主干上修复,并根据需要 cherry pick 到对应版本的发布分支。

以TBD Flow为例:

优点

分支模型简单高效,开发人员易于掌握不容易出现错误操作

避免了分支合并、冲突解决的困扰

随时拥有可发布的版本

有利于持续集成和持续交付

缺点

基础架构要求高:合入到主干的代码若质量不过关将直接阻塞整个团队的开发工作,因此需要高效的持续集成平台进行把关;

自动化测试要求高:需有完备单元测试代码,确保在代码合入主干前能在获得快速和可靠的质量反馈;

最好有代码评审:若代码质量要求高,需要配套代码评审(CR)机制,在代码提交到主干时,触发 CR,通过 Peer Review 后才能正式合入;

最好有特性开关:主干开发频发合入主干的情况下,特性拆分得很小,可能是半成品特性,需要配套特性开关(Feature Toggle),只有当特性整体开发完才通过灰度发布等手段逐步打开;

适用环境

对迭代速度要求高,希望需求快速交付上线

基础架构强,持续集成工具高效;

团队成员习惯 TDD(测试驱动开发),代码自动化测试覆盖率高(至少增量代码的自动化测试覆盖率高);

常用模式

TBD Flow, TBD++ Flow

在学react的代码的入门例子的时候遇到了以下代码

1
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

当时想当然的认为并不需要括号,如以下代码

1
const status = 'Next player: ' + this.state.xIsNext ? 'X' : 'O';

但显示结果会为X

但是该代码实际上在类型严格的语言如C#会报错, 因为代码在没有加括号会从左到右来执行, 这样会将'Next player: ' + this.state.xIsNext先计算, 因此得到一个string类型, 但强类型语言的string没法转换成bool类型, 因此会编译不通过.

但是js是个弱类型的语言, 因此string类型也能进行true和false判断, string不为null则为true, 反之为false, 因此'Next player: ' + this.state.xIsNext先计算, 进行真假判断, 得到结果为true, 所以界面会显示X

将多个对象的所有枚举属性拷贝到目标对象上

1
2
3
4
5
6
7
8
var target={a:1,b:2};
var source1={b:3,c:4};
var source2={c:5,d:6};

var newTarget=Object.assign(target,source1,source2);

console.log('target '+target);
console.log('newTarget '+newTarget);

输出:

1
2
target {a:1,b:3,c:5,d:6}
newTarget {a:1,b:3,c:5,d:6}

拷贝出新对象, 不修改原对象

1
2
3
4
5
var target={a:1,b:2};
var newTarget=Object.assign({},target,{c:3});

console.log('target '+ target);
console.log('newTarget '+ newTarget);

输出:

1
2
3
target {a:1,b:2}

newTarget {a:1,b:2,c:3}

github创建Pages的网页静态页面仓库

github仓库名为 github用户名.github.io

github创建blog的仓库

名字随便

hexo 安装

安装nodejs

安装git

修改npm镜像源为淘宝镜像源

1
npm config set registry https://registry.npm.taobao.org

安装hexo

1
npm install -g hexo-cli

hexo 初始化

1
2
3
4
hexo init blog
cd blog
npm install
hexo server

安装hexo通过git发布的插件

1
npm install hexo-deployer-git --save

修改_config.yml文件

1
2
3
4
deploy:
type: git
repository: https://github.com/github用户名/github用户名.github.io.git
branch: main

安装 NexT 模板

1
2
npm install hexo-theme-next
git clone https://github.com/next-theme/hexo-theme-next themes/next

修改_config.yml文件 theme: next

hexo 配置

修改_config.yml文件

同步生成文件夹 post_asset_folder: true

修正路径(渲染器设置)

1
2
3
marked:
prependRoot: true
postAsset: false

安装pandoc

  1. 下载并安装pandoc:下载地址

  2. 选择安装到全部用户

  3. 检查_config.yml中是否有以下配置

    1
    2
    pandoc:
    pandoc_path: C:/Program Files/Pandoc/pandoc.exe

更换渲染器

1
2
npm un hexo-renderer-marked --save
npm i hexo-renderer-pandoc --save

配置数学公式

1
2
npm install hexo-filter-mathjax
hexo clean

如果需要使用 MathJax 来加载公式,对应文档的的文件头加入一个选项:mathjax: true,可以根据个人需求是否加入位于 scaffolds/post.md 文件模板中。

生成静态网页和发布

1
2
hexo g
hexo d

参考资料

https://hexo.io/docs/

http://home.ustc.edu.cn/~liujunyan/blog/hexo-next-theme-config/

0%