【Unity书籍】《Unity3d游戏开发》第二版笔记
聪头 游戏开发萌新

《Unity3d游戏开发》笔记

时间:2021年11月21日11:01:20

作者:聪头

Ch2.编辑器的结构

P10 版本管理

一定不要上传到版本管理的文件夹:

  • Library:根据Assets目录下的游戏资源所生成的中间文件
  • Temp:Library生成过程中产生的临时文件

P12 Project视图

概述:游戏资源的集合视图,里面放的都是引擎所用到的游戏资源

资源分成两类:

  • 外部资源:非使用Unity引擎创建的,而是由外部工具做的模型以及贴图,或者业内达成共识的通用格式资源。*
    • 如图片、模型、动画、视频和声音资源等。
  • 内部资源:必须使用Unity引擎创建,仅Unity中才能识别的资源。
    • 如脚本、Shader、场景、预制体、材质、精灵、动画控制器、角色遮罩、时间线等

搜索资源:在搜索栏输入,可快速搜索资源

  • t: 类型搜索,如 t:Scene 表示在Project搜索场景资源
  • xxx: 标识搜索,如 a t:Scene 表示在场景中搜索名字中包含“a”的场景
  • l: 标签搜索,如 par l:Effect 表示搜索资源名包含par且标签为Effect的资源

自定义标签:

image

P16 Hierarchy视图

概述:Project视图中的游戏资源,如果需要出现在正式游戏中,就需要Hierarchy视图了。通常只有游戏对象才能放进

游戏对象分为:预先编辑和运行时代码动态生成

搜索对象:

  • 按名称搜索
  • 按类型搜索

P17 Inspector视图

概述:Inspector视图承载着所有游戏对象以及游戏资源组件参数的编辑工作

原理:就是键入一些数据并将其序列化在这个对象身上。在Hierarchy选中,数据就是保存在这个对象所在场景上;在Project视图中选中,数据就是保存在这个资源本身上

P19 Scene视图

概述:Scene视图就是游戏最终画面的自由视角

Scene 导航栏

image

操作原点:

  • Pivot:表示父对象的操作原点就是自身的坐标点
  • Center:表示父对象的操作原点是所有子对象共同的中心点
image

Scene 标题栏

image

开关音频:

image

P22 Game视图

概述:Game视图就是最终展示给玩家的一面,把游戏主摄像机看到的内容显示到了这里

Game 标题栏

  • 主要功能就是控制Game视图的显示,这里的设置并不能影响最终发布游戏的结果,但是可以让开发更方便一些
image

开关音频:

image

P23 导航栏视图

概述:所有视图通用的一些功能以及设置信息

播放器及使用技巧:

image

技巧1:点击暂停再运行,游戏会被暂停到第一帧

技巧2:可以利用 Ctrl + Shift + P 组合键暂停/恢复游戏

P25 其他功能

概述:隐藏的小功能

1.小锁头,锁住Inspector面板

2.窗口菜单

3.保存组件参数

  • 每个组件的参数都可以保存,好东西!
image

Ch3.拓展编辑器

P28 拓展Project视图

1.拓展右键菜单

概述:实际就是使用MenuItem特性,以 Assets 开头的菜单项

说明:编辑模式下的代码,需要放在Editor文件夹下;Editor文件夹的位置比较灵活,它还可以作为多个目录的子文件夹存在

MenuItem 属性用于向主菜单和检视面板上下文菜单添加菜单项

Selection 类

访问编辑器中的选择

脚本:拓展 Project 右键菜单

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
public class MyScript3_1 
{
//MenuItem特性解析:
//①itemName:菜单路径
//②isValidateFunction:如果 isValidateFunction 为 true,它将表示一个验证函数,并在系统调用具有相同 itemName 的菜单函数之前进行调用。
//③priority:排序优先级,越小越靠前
[MenuItem("Assets/My Tools/Tools 1", false, 1)]
static void MyTools1()
{
//打印资源名称
Debug.Log(Selection.activeObject.name);
}

//新的一组
[MenuItem("Assets/My Tools/Tools 2", false, 11)]
static void MyTools2()
{
Debug.Log(Selection.activeObject.name);
//创建一个立方体
GameObject.CreatePrimitive(PrimitiveType.Cube);
}

//Tools 1的验证函数,只有返回true才可以调用Tools 1
[MenuItem("Assets/My Tools/Tools 1", true)]
static bool MyTools1_Validation()
{
if (Selection.activeObject.name == "MyScript3_1") return true;
else return false;
}
}

2.拓展布局和生命周期

概述:当鼠标选中一个资源后,右边将出现拓展后的click按钮,点击这个按钮,程序会自动在Console窗口中打印选中的资源名称

GUI类 和 GUILayout类

GUI 类是 Unity GUI 的接口,并且具有手动定位功能

GUILayout 类是 Unity GUI 的接口,并且具有自动布局功能

两者区别:https://www.ituring.com.cn/article/2668

  • GUI是固定布局:使用GUI绘制控件的时候,需要设置控件的Rect()方法,也就是说需要设定控件的整体显示区域。相当于写死位置和大小,如果计算不准确会发生重叠
  • GUILayout是自动布局:GUILayout无须设定显示区域,系统会自动帮我们计算控件的显示区域,并且保证它们不会重叠

Initialize** 特性

  • CSDN:https://blog.csdn.net/qq_35130510/article/details/80905961

  • InitializeOnLoad:允许在 Unity 加载时和重新编译脚本时初始化 Editor

    • 该特性只能修饰类,被修饰的类在每次编译新内容后(可以是修改其他脚本的代码时)就会重新执行一遍静态构造方法
  • InitializeOnLoadMethod:允许在 Unity 加载时初始化编辑器类方法

    • 该特性只能修饰静态方法,每次编译新内容后会重新执行方法内内容
  • RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad):运行时在Awake前调用

  • RuntimeInitializeOnLoadMethod:运行Awake后,Start前

  • RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad):Awake后,Start前

修饰的方法均为静态方法,重点掌握前两个特性

EditorApplication 类

主应用程序类

脚本:拓展 Project 布局

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
public class MyScript3_3
{
//此方法会在C#代码每次编译完成后首先调用
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
//监听该委托,即可用GUI方法来绘制自定义的UI元素
EditorApplication.projectWindowItemOnGUI = delegate (string guid, Rect selectionRect)
{
//在Project视图中选择一个资源
if (Selection.activeObject &&
guid == AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(Selection.activeObject)))
{
//拓展按钮区域
float width = 50f;
selectionRect.x += (selectionRect.width - width);
selectionRect.y += 2f;
selectionRect.width = width;
GUI.color = Color.red;
//点击事件
if (GUI.Button(selectionRect, "click"))
{
Debug.Log($"click: { Selection.activeObject.name }");
}
GUI.color = Color.white; //还原白色,否则之后的资源项全成为红色
}
};
}
}

脚本:Project 生命周期函数

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
46
47
48
49
50
51
52
53
54
55
56
57
public class MyScript3_4 : UnityEditor.AssetModificationProcessor 
{
//允许在 Unity 加载时初始化编辑器类方法,无需用户操作
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
//全局监听 Project 视图下的资源是否发生变化(添加、删除和移动)
//EditorApplication.projectWindowChanged = () => Debug.Log("change"); //弃用
EditorApplication.projectChanged += () => Debug.Log("change");
}

//监听“双击鼠标左键,打开资源”事件
//官方:当 Unity 检查资源以确定是否应禁用编辑器时,则会调用此方法。
//不实用,一碰资源文件打印一堆meta的路径,返回false都没用,怀疑理解有误
//public static bool IsOpenForEdit(string assetPath, out string message)
//{
// message = null;
// Debug.Log($"assetPath: {assetPath}");
// //true表示该资源可以打开,false表示不允许在Unity中打开该资源
// if (assetPath.EndsWith(".meta")) return false;
// return true;
//}

//监听“资源即将被创建”事件
//官方:Unity 在即将创建未导入的资源(例如,.meta 文件)时调用此方法。
public static void OnWillCreateAsset(string path)
{
Debug.Log("OnWillCreateAsset Path: " + path);
}

//监听“资源即将被保存”事件
//官方:Unity 即将向磁盘写入序列化资源或场景文件时会调用此方法。
public static string[] OnWillSaveAssets(string[] paths)
{
if (paths != null)
Debug.Log("OnWillSaveAssets Path: " + string.Join(",", paths));
return paths;
}

//监听“资源即将被移动”事件
//官方:Unity 即将在磁盘上移动资源时会调用此方法。
public static AssetMoveResult OnWillMoveAsset(string oldPath, string newPath)
{
Debug.Log("OnWillMoveAsset from: " + oldPath + "to: " + newPath);
//AssetMoveResult.DidMove 表示该资源可以移动
return AssetMoveResult.DidMove;
}

//监听“资源即将被删除”事件
//官方:当 Unity 即将从磁盘中删除资源时,则会调用此方法。
public static AssetDeleteResult OnWillDeleteAsset(string assetPath, RemoveAssetOptions option)
{
Debug.Log("delete: " + assetPath);
//AssetDeleteResult.DidDelete 表示该资源可以被删除
return AssetDeleteResult.DidNotDelete;
}
}

P33 拓展 Hierarchy 视图

1.拓展菜单

概述:实际就是使用MenuItem特性,以 GameObject 开头的菜单项

Undo 类

让您可以针对要执行更改的特定对象注册撤销操作

脚本:Undo教程

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

#region Undo简单操作
public class UndoExtension
{
//1.把选中物体位置归0,并沿各轴旋转45度
//记录执行 RecordObject 函数之后对对象所做的任何更改。
//使用这一函数可以记录几乎所有属性更改。无法使用这一函数记录变换组件的父项、AddComponent 和对象销毁,应该使用专用函数来记录。
[MenuItem("Extension/Undo/RecordObject")]
static void RecordObject()
{
Transform transform = Selection.activeTransform;
Undo.RecordObject(transform, "Pos");
transform.position = new Vector3(0, 0, 0);
transform.Rotate(new Vector3(45, 45, 45));
}

//2.选中物体添加刚体组件
//向游戏对象添加组件并针对这一操作注册撤销操作。
//执行撤销操作后,新添加的组件会被销毁。
[MenuItem("Extension/Undo/AddComponent")]
static void AddComponent()
{
GameObject go = Selection.activeGameObject;
//添加组件,执行撤销后被删除
Rigidbody body = Undo.AddComponent<Rigidbody>(go);
}

//3.删除物体
//销毁对象并记录撤销操作,以便能够重新创建该对象。
[MenuItem("Extension/Undo/DestroyObjectImmediate")]
static void DestroyObjectImmediate()
{
GameObject go = Selection.activeGameObject;
//注册撤销删除对象
Undo.DestroyObjectImmediate(go);
}

//4.撤销父子关系
//将变换组件的父项设置为新的父项,并记录撤销操作。
//这相当于调用 transform.parent = newParent,但还会记录撤销操作。
[MenuItem("Extension/Undo/SetTransform")]
static void SetTransform()
{
Transform root = Camera.main.transform;
Transform transform = Selection.activeTransform;
//将选中物体的父类设为root对应的transform,可撤销
Undo.SetTransformParent(transform, root, "Main Camera");
}

//5.创建物体,如果物体ID含有0则取消创建
[MenuItem("Extension/Undo/RevertAllInCurrentGroup")]
static void RevertAllInCurrentGroup()
{
GameObject ticket = new GameObject("ticket0");
//针对新创建的对象注册撤销操作
Undo.RegisterCreatedObjectUndo(ticket, "UndoCreate");
int number = ticket.GetInstanceID();
Debug.Log("InstanceID: " + number);
if (number.ToString().Contains("0"))
//执行最后一次撤销操作,但不记录重做操作。
Undo.RevertAllInCurrentGroup();
}


}
#endregion

#region 将Undo操作分组,撤销到指定位置
//Undo的每一步操作都有它的ID,纪录ID就可以实现特殊的Undo操作
public class ExampleWindow : EditorWindow
{
[MenuItem("Window/ExampleWindow")]
static void Open()
{
GetWindow<ExampleWindow>();
}

GameObject go;
int group1 = 0;
int group2 = 0;
int group3 = 0;

private void OnEnable()
{
go = Camera.main.gameObject;
}

private void OnGUI()
{
if (GUILayout.Button("给摄像机添加组件"))
{
group1 = Undo.GetCurrentGroup();

Undo.AddComponent<Rigidbody>(go);

Undo.IncrementCurrentGroup();

group2 = Undo.GetCurrentGroup();

Undo.AddComponent<BoxCollider>(go);

Undo.IncrementCurrentGroup();

group3 = Undo.GetCurrentGroup();

Undo.AddComponent<ConstantForce>(go);

//打印撤销操作编号
Debug.Log(group1 + "--" + group2 + "--" + group3);

}

if (GUILayout.Button("回到group2状态"))
{
//回到group2之前的状态
Undo.RevertAllDownToGroup(group2);
EditorGUIUtility.ExitGUI();
}
}
}
#endregion

脚本:拓展 Hierarchy 视图

1
2
3
4
5
[MenuItem("GameObject/My Create/Cube", false, 0)]
static void CreateCube()
{
GameObject.CreatePrimitive(PrimitiveType.Cube); //创建立方体
}

2.拓展布局

脚本:拓展 Hierarchy 布局

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
public class MyScript3_6
{
//要在每个 OnGUI 事件上为 Hierarchy 窗口中的每个可见列表项调用的委托
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
EditorApplication.hierarchyWindowItemOnGUI = delegate (int instanceID, Rect selectionRect)
{
//在Hierarchy视图中选择一个资源
if (Selection.activeObject && instanceID == Selection.activeObject.GetInstanceID())
{
//设置拓展按钮区域
float width = 50f;
//float height = 18f;
selectionRect.x += (selectionRect.width - width);
selectionRect.width = width;
//selectionRect.height = height;
//点击事件
if (GUI.Button(selectionRect, AssetDatabase.LoadAssetAtPath<Texture>("Assets/Chapter3/unity.png")))
{
Debug.Log($"click: { Selection.activeObject.name }");
}
}
};
}
}

image

3.重写菜单

Event 类

UnityGUI 事件。事件与用户输入(按键、鼠标操作)相对应,或者是 UnityGUI 布局或渲染事件

脚本:重写 Hierarchy 菜单

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
public class MyScript3_7 : MonoBehaviour 
{
[MenuItem("Window/Test/yusong")]
static void Test()
{

}

[MenuItem("Window/Test/momo")]
static void Test1()
{

}

[MenuItem("Window/Test/聪头/MOMO")]
static void Test3()
{

}

[InitializeOnLoadMethod]
static void StartInitializeOnLoadMethod()
{
//要在每个 OnGUI 事件上为 Hierarchy 窗口中的每个可见列表项调用的委托。
EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
}

private static void OnHierarchyGUI(int instanceID, Rect selectionRect)
{
//事件不为空,且该对象与鼠标相交,且操作的是右键,且为按下或抬起事件
if (Event.current != null && selectionRect.Contains(Event.current.mousePosition)
&& Event.current.button == 1 && Event.current.type <= EventType.MouseUp)
{
//通过InstanceID获取GameObject
GameObject selectedGameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
//这里可以判断 selectedGameObject 条件
if (selectedGameObject)
{
Vector2 mousePosition = Event.current.mousePosition;
EditorUtility.DisplayPopupMenu(new Rect(mousePosition.x, mousePosition.y, 0, 0), "Window/Test", null);
Event.current.Use();
}
}
}
}

脚本:重写 Image 菜单项

  • 在生成Image生成时取消勾选Raycast属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//重写Image菜单项
public class MyScript3_8
{
[MenuItem("GameObject/UI/Image")]
static void CreateImage()
{
if (Selection.activeTransform)
{
if (Selection.activeTransform.GetComponentInParent<Canvas>())
{
Image image = new GameObject("image").AddComponent<Image>();
image.raycastTarget = false;
image.transform.SetParent(Selection.activeTransform, false);
//设置选中状态
Selection.activeTransform = image.transform;
}
}
}
}

P37 扩展Inspector视图

1.拓展源生组件

局限性:拓展组件只能加在源生组件的最上面或最下面

脚本:拓展 Inspector 源生组件

1
2
3
4
5
6
7
8
9
10
11
12
13
[CustomEditor(typeof(Camera))] //自定义哪个组件
public class MyScript3_9 : Editor
{
//重写OnInspectorGUI,进行重新绘制
public override void OnInspectorGUI()
{
if (GUILayout.Button("拓展按钮"))
{
Debug.Log(target.name);
}
base.OnInspectorGUI();
}
}

2.拓展继承组件

概述:有些系统组件可能在Unity内部已经重写了绘制方法,但是外部访问不了内部代码,修改起来比较麻烦。例如Transform组件,如果直接使用上节方式进行扩展,会出问题(如下图)

image

分析:Unity将大量Editor绘制方法封装在内部的DLL文件里,开发者无法调用它的方法。如果要解决这个问题,就要利用反射

Editor 类

从此基类派生,以便为自定义对象创建自定义检视面板和编辑器

EditorUtility 类

Editor 实用程序函数

脚本:拓展 Inspector 系统组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[CustomEditor(typeof(Transform))]
public class MyScript3_10 : Editor
{
private Editor m_Editor;
private void OnEnable()
{
//为 targetObject 或 targetObjects 创建自定义编辑器。
m_Editor = Editor.CreateEditor(target, Assembly.GetAssembly(typeof(Editor))
.GetType("UnityEditor.TransformInspector", true));
}

public override void OnInspectorGUI()
{
if (GUILayout.Button("拓展按钮"))
{
Debug.Log(target.name);
}
//调用系统绘制方法
m_Editor.OnInspectorGUI();
//base.OnInspectorGUI();
}
}

3.组件不可编辑

脚本:锁 Transform 组件内容

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
[CustomEditor(typeof(Transform))]
public class MyScript3_11 : Editor
{
private Editor m_Editor;
private void OnEnable()
{
m_Editor = Editor.CreateEditor(target,
Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector", true));
}

public override void OnInspectorGUI()
{
if (GUILayout.Button("拓展按钮上"))
{

}
//开始禁止
GUI.enabled = false;
m_Editor.OnInspectorGUI();
//结束禁止
GUI.enabled = true;
if (GUILayout.Button("拓展按钮下"))
{

}
}
}

image

HideFlags 枚举

HideFlags 可以使用按位或 ( | ) 同时保持多个属性

  • None:清除状态
  • DontSave:设置对象不会被保存(仅编辑模式下使用,运行时剔除掉)
  • DontSaveInBuild:设置对象构建后不会被保存设
  • DontSaveInEditor:设置对象编辑模式下不会被保存
  • DontUnloadUnusedAsset:设置对象不会被Resources.UnloadUnusedAssets()卸载无用资源时卸掉
  • HideAndDontSave:设置对象隐藏,并且不会被保存
  • HideInHierarchy:设置对象在层次视图中隐藏
  • HideInInspector:设置对象在控制面板视图中隐藏
  • HideFlags.NotEditable:设置对象不可被编辑

脚本:锁具体对象所有组件的内容

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
public class MyScript3_12 
{
[MenuItem("GameObject/3D Object/Lock/Lock", false, 0)]
static void Lock()
{
if (Selection.gameObjects != null)
{
foreach(var gameObject in Selection.gameObjects)
{
gameObject.hideFlags = HideFlags.NotEditable;
}
}
}

[MenuItem("GameObject/3D Object/Lock/UnLock", false, 1)]
static void UnLock()
{
if (Selection.gameObjects != null)
{
foreach (var gameObject in Selection.gameObjects)
{
gameObject.hideFlags = HideFlags.None;
}
}
}
}

4.拓展Context菜单

概述:点击组件中设置(鼠标右键),可以弹出Context菜单。以 CONTEXT/组件名/ 开头

用于提取 MenuItem 的上下文

MenuCommand 对象传递到使用 MenuItem 属性定义的自定义菜单项函数

官方示例

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

public class Something : EditorWindow
{
// Add menu item
[MenuItem("CONTEXT/Rigidbody/Do Something")]
static void DoSomething(MenuCommand command)
{
//context:上下文是作为菜单命令目标的对象
Rigidbody body = (Rigidbody)command.context;
body.mass = 5;
Debug.Log("Changed Rigidbody's Mass to " + body.mass + " from Context Menu...");
}
}

脚本:拓展 Context 菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyScript3_13
{
[MenuItem("CONTEXT/Transform/New Context 1")]
public static void NewContext1 (MenuCommand command)
{
//获取对象名
Debug.Log(command.context.name);
}
[MenuItem("CONTEXT/Transform/New Context 2")]
public static void NewContext2 (MenuCommand command)
{
Debug.Log(command.context.name);
}
}

其中如果想给所有组件都添加菜单栏,这里改成Component即可

以上设置也可以应用在自己写的脚本中,在代码中可以通过 MenuCommand 来获取脚本对象,从而访问脚本中的变量

ContextMenu 特性

ContextMenu 属性用于向上下文菜单添加命令。

在该附加脚本的 Inspector 中,当用户选择该上下文菜单时, 将执行此函数。这对于从该脚本自动设置场景数据非常有用。

此函数必须是非静态的。

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/ContextMenu.html

官方示例

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

public class ContextTesting : MonoBehaviour
{
/// Add a context menu named "Do Something" in the inspector
/// of the attached script.
[ContextMenu("Do Something")]
void DoSomething()
{
Debug.Log("Perform operation");
}
}

脚本:拓展脚本 Context 菜单

两种为脚本添加自定义菜单的方式,使用MenuItem或ContextMenu

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;
#if UNITY_EDITOR
using UnityEditor; //仅在Editor模式下执行,发布后剔除
#endif


public class MyScript3_14 : MonoBehaviour
{
public string contextName;
#if UNITY_EDITOR
[MenuItem("CONTEXT/MyScript3_14/New Context 1")]
public static void NewContext2 (MenuCommand command)
{
MyScript3_14 script = command.context as MyScript3_14;
script.contextName = "hello world!";
}

[ContextMenu("Remove Component")]
void RemoveComponent()
{
Debug.Log("RemoveComponent");
//等一帧再删除自己
UnityEditor.EditorApplication.delayCall = delegate ()
{
DestroyImmediate(this);
};
}
#endif
}

P44 拓展 Scene 视图

1.辅助元素

Gizmo的绘制原理就是在脚本中添加OnDrawGizmosSelected(),此方法仅在编辑模式下生效。使用Gizmos.cs工具类,我们可以绘制出任意辅助元素

Gizmos 类

概述:辅助图标用于协助在 Scene 视图中进行视觉调试或设置

所有辅助图标绘图都必须在此脚本的 OnDrawGizmosOnDrawGizmosSelected 函数中进行。\ 每一帧都调用 OnDrawGizmos。在 OnDrawGizmos 中渲染的所有辅助图标均可选择。 仅在选择了附加此脚本的对象时才调用 OnDrawGizmosSelected

脚本:绘制辅助元素

1
2
3
4
5
6
7
8
9
10
11
public class MyScript3_15 : MonoBehaviour 
{
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
//画线
Gizmos.DrawLine(transform.position, Vector3.one);
//立方体
Gizmos.DrawCube(Vector3.one, Vector3.one);
}
}

2.辅助UI

概述:添加EditorGUI,这样可以方便地在视图中处理一些操作事件

Handles 类

场景视图中的自定义 3D GUI 控件和绘制操作

我的理解就是这是个3D GUI的接口,而之前的GUI和GUILayout是2D接口

EventType 枚举

UnityGUI 输入和处理事件的类型。

使用它来辨别在 GUI 中发生了哪种类型的事件。Events 类型包括鼠标点击、鼠标拖动、按下按钮、鼠标进入或退出窗口、滚轮以及以下提到的其他类型。

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/EventType.html

脚本:辅助UI

继承Editor,重写OnSceneGUI,在Handles.BeginGUI()Handles.EndGUI()中间绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[CustomEditor(typeof(Camera))]
public class MyScript3_16 : Editor
{
private void OnSceneGUI()
{
Camera camera = target as Camera;
if (camera != null)
{
Handles.color = Color.red;
Handles.Label(camera.transform.position, camera.transform.position.ToString());
//Handles.CubeHandleCap(0, camera.transform.position, camera.transform.rotation, 1f, EventType.Repaint);
Handles.BeginGUI();
GUI.backgroundColor = Color.red;
if (GUILayout.Button("click", GUILayout.Width(200f)))
{
Debug.Log($"click = { camera.name }");
}
GUILayout.Label("Label");
Handles.EndGUI();
}
}
}
image

脚本:常驻辅助UI

原理:重写SceneView.onSceneGUIDelegate,依然需要在Handles.BeginGUI()Handles.EndGUI()中间完成绘制

1
2
3
4
5
6
7
8
9
10
11
12
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
//SceneView.onSceneGUIDelegate //弃用
SceneView.duringSceneGui += delegate (SceneView sceneView)
{
Handles.BeginGUI();
GUI.Label(new Rect(0, 0, 50f, 50f), "标题");
GUI.Button(new Rect(0, 20f, 50f, 50f), AssetDatabase.LoadAssetAtPath<Texture>("Assets/Chapter3/unity.png"));
Handles.EndGUI();
};
}

P48 扩展 Game 视图

分类:运行模式下以及非运行模式下

原理:在脚本类名上方声明[ExecuteInEditMode],表示此脚本可以在编辑模式中生效。此类脚本通常只是编辑器有效,可以加UNITY_EDITOR条件编译

1
2
3
4
5
6
7
8
9
10
11
12
13
#if UNITY_EDITOR
[ExecuteInEditMode]
public class MyScript3_19 : MonoBehaviour
{
void OnGUI()
{
if (GUILayout.BUtton("Click"))
{
Debug.Log("click!!");
}
GUILayout.Label("Hello World!");
}
}

P49 MenuItem菜单

1.拓展全局自定义快捷键

热键:

  • %: Ctrl键
  • #: Shift键
  • &: Alt键
  • LEFT/RIGHT/UP/DOWN: 表示左、右、上、下4个方向键
  • F1…F12: 表示 F1 至 F12 菜单键
  • HOME、END、PGUP和PGDN键

脚本:自定义快捷键

1
2
3
4
5
6
7
8
public class MyScript3_22
{
[MenuItem("Assets/HotKey %#d", false, -1)]
private static void HotKey()
{
Debug.Log("Ctrl Shift + D");
}
}

P53 面板拓展

脚本挂载在游戏对象上时,右侧会出现它的详细信息面板,这些信息是根据脚本中声明的public可序列化变量而来的。此外,也可以通过EditorGUIEditorGUILayout来对它进行绘制,让面板具有可操作性

1.Inspector 面板

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
46
public enum MyEnum
{
One = 1,
Two
}
public class MyScript3_23 : MonoBehaviour
{
public Vector3 scrollPos;
public int myId;
public string myName;
public GameObject prefab;
public MyEnum myEnum = MyEnum.One;
public bool toggle1;
public bool toggle2;
}

#if UNITY_EDITOR
[CustomEditor(typeof(MyScript3_23))]
public class MyScriptEditor3_23 : Editor
{
private bool m_EnableToogle;

public override void OnInspectorGUI()
{
//获取脚本对象
MyScript3_23 script = target as MyScript3_23;
//绘制滚动条
script.scrollPos = EditorGUILayout.BeginScrollView(script.scrollPos, false, true);
script.myName = EditorGUILayout.TextField("text", script.myName);
script.myId = EditorGUILayout.IntField("int", script.myId);
script.prefab = EditorGUILayout.ObjectField("GameObject", script.prefab, typeof(GameObject), true) as GameObject;
//绘制按钮
EditorGUILayout.BeginHorizontal();
GUILayout.Button("1");
GUILayout.Button("2");
script.myEnum = (MyEnum)EditorGUILayout.EnumPopup("MyEnum:", script.myEnum);
EditorGUILayout.EndHorizontal();
//Toggle组件
m_EnableToogle = EditorGUILayout.BeginToggleGroup("EnableToggle", m_EnableToogle);
script.toggle1 = EditorGUILayout.Toggle("toggle1", script.toggle1);
script.toggle2 = EditorGUILayout.Toggle("toggle2", script.toggle2);
EditorGUILayout.EndToggleGroup();
EditorGUILayout.EndScrollView();
}
}
#endif
image

2.EditorWindows 窗口

概述:使用 EditorWindow.GetWindow() 方法即可打开自定义窗口,在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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class MyScript3_24 : EditorWindow 
{
[MenuItem("Window/Open My Window")]
static void Init()
{
MyScript3_24 window = (MyScript3_24)EditorWindow.GetWindow(typeof(MyScript3_24));
window.Show();
}

private Texture m_MyTextuer = null;
private float m_MyFloat = 0.5f;

private void Awake()
{
Debug.Log("窗口初始化时调用");
m_MyTextuer = AssetDatabase.LoadAssetAtPath<Texture>("Assets/Chapter3/unity.png");
}

private void OnGUI()
{
GUILayout.Label("Hello World!!", EditorStyles.boldLabel);
m_MyFloat = EditorGUILayout.Slider("Slider", m_MyFloat, -5, 5);
GUI.DrawTexture(new Rect(0, 30, 100, 100), m_MyTextuer);
}

private void OnDestroy()
{
Debug.Log("窗口销毁时调用");
}

private void OnFocus()
{
Debug.Log("窗口拥有焦点时调用");
}

private void OnHierarchyChange()
{
Debug.Log("Hierarchy 视图发生改变时调用");
}

private void OnInspectorUpdate()
{
//Debug.Log("Inspector 每帧更新");
}

private void OnLostFocus()
{
Debug.Log("失去焦点");
}

private void OnProjectChange()
{
Debug.Log("Project 视图发生改变时调用");
}

private void OnSelectionChange()
{
Debug.Log("在 Hierarchy 或者 Project 视图中选择一个对象时调用");
}

private void Update()
{
//Debug.Log("每帧更新");
}
}
image

3.EditorWindows 下拉菜单

概述:在EditorWindows编辑窗口的右上角,有个下拉菜单,我们也可以对该菜单中的选项进行扩展,这里需要实现IHasCustomMenu接口

GenericMenu类

GenericMenu 允许您创建自定义上下文菜单和下拉菜单

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
public class MyScript3_25 : EditorWindow, IHasCustomMenu
{
public void AddItemsToMenu(GenericMenu menu)
{
menu.AddDisabledItem(new GUIContent("Disabe"));
//①:将添加为菜单项的 GUIContent
//②:指定是否显示菜单项当前已激活(即菜单项旁边的勾选标记)。
//③:选中菜单项时要调用的函数。
menu.AddItem(new GUIContent("Test1"), true, () =>
{
Debug.Log("Test1");
});
menu.AddItem(new GUIContent("Test2"), true, () =>
{
Debug.Log("Test2");
});
menu.AddSeparator("Test/");
menu.AddItem(new GUIContent("Test/Tes3"), true, () =>
{
Debug.Log("Tes3");
});
}

[MenuItem("Window/Open My Window")]
static void Init()
{
MyScript3_25 window = (MyScript3_25)EditorWindow.GetWindow(typeof(MyScript3_25));
window.Show();
}
}
image

4.预览窗口

概述:可以为Hierarchy的物体设置Preview窗口

原理:继承ObjectPreview并重写OnPreviewGUI()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
[CustomPreview(typeof(GameObject))]
public class MyScript3_26 : ObjectPreview
{
public override bool HasPreviewGUI()
{
return true;
}
public override void OnPreviewGUI(Rect r, GUIStyle background)
{
GUI.DrawTexture(r, AssetDatabase.LoadAssetAtPath<Texture>("Assets/Chapter3/unity.png"));
GUILayout.Label("Hello World");
}
}
image

5.获取预览信息

没跑通,没懂

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
public class MyScript3_27 : EditorWindow 
{
private GameObject m_MyGo;
private Editor m_MyEditor;

[MenuItem("Window/Open My Window")]
static void Init()
{
MyScript3_27 window = (MyScript3_27)EditorWindow.GetWindow(typeof(MyScript3_27));
window.Show();
}
private void OnGUI()
{
//设置一个对象
m_MyGo = (GameObject)EditorGUILayout.ObjectField(m_MyGo, typeof(GameObject), true);
if (m_MyGo != null)
{
if (m_MyEditor == null)
{
//创建 Editor 实例
m_MyEditor = Editor.CreateEditor(m_MyGo);
}
//预览它
m_MyEditor.OnPreviewGUI(GUILayoutUtility.GetRect(500, 500), EditorStyles.whiteLabel);
}
}
}

6.查找静态引用

概述:在运行时查找所有静态引用

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class Script_03_32 :Editor
{
[MenuItem("Tools/Report/脚本Static引用")]
static void StaticRef ()
{
//静态引用
LoadAssembly ("Assembly-CSharp-firstpass");
LoadAssembly ("Assembly-CSharp");

}

static void LoadAssembly(string name)
{
Assembly assembly = null;
try {
assembly = Assembly.Load(name);
}
catch (Exception ex) {
Debug.LogWarning (ex.Message);
}
finally{
if (assembly != null) {
foreach (Type type in assembly.GetTypes()) {
try {
HashSet<string> assetPaths = new HashSet<string>();
FieldInfo[] listFieldInfo = type.GetFields (BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
foreach (FieldInfo fieldInfo in listFieldInfo) {
if (!fieldInfo.FieldType.IsValueType) {
SearchProperties (fieldInfo.GetValue (null),assetPaths);
}
}
if (assetPaths.Count > 0) {
StringBuilder sb = new StringBuilder ();
sb.AppendFormat ("{0}.cs\n", type.ToString ());
foreach (string path in assetPaths) {
sb.AppendFormat ("\t{0}\n", path);
}
Debug.Log (sb.ToString ());
}

} catch(Exception ex){
Debug.LogWarning (ex.Message);
}
}
}
}
}

static HashSet<string> SearchProperties(object obj,HashSet<string> assetPaths)
{
if (obj != null) {
if (obj is UnityEngine.Object) {
UnityEngine.Object[]depen = EditorUtility.CollectDependencies (new UnityEngine.Object[]{ obj as UnityEngine.Object });
foreach (var item in depen) {
string assetPath = AssetDatabase.GetAssetPath (item);
if (!string.IsNullOrEmpty (assetPath)) {
if (!assetPaths.Contains (assetPath)) {
assetPaths.Add (assetPath);
}
}
}
} else if (obj is IEnumerable) {
foreach (object child in (obj as IEnumerable)) {
SearchProperties (child,assetPaths);
}
}else if (obj is System.Object) {
if (!obj.GetType ().IsValueType) {
FieldInfo[] fieldInfos = obj.GetType ().GetFields ();
foreach (FieldInfo fieldInfo in fieldInfos) {
object o = fieldInfo.GetValue (obj);
if (o != obj) {
SearchProperties (fieldInfo.GetValue (obj),assetPaths);
}
}
}
}
}
return assetPaths;
}
}

7.自定义资源导入类型

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
//监听的后缀名
[ScriptedImporter(1, "yusongmomo")]
public class Script_03_33 : ScriptedImporter
{
//监听自定义资源导入
public override void OnImportAsset(AssetImportContext ctx)
{
//创建立方体对象
var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
//将参数提取出来
var position = JsonUtility.FromJson<Vector3>(File.ReadAllText(ctx.assetPath));

cube.transform.position = position;
cube.transform.localScale = Vector3.one;
//将立方体绑定到对象身上
ctx.AddObjectToAsset("obj", cube);
ctx.SetMainObject(cube);

//添加材质
var material = new Material(Shader.Find("Standard"));
material.color = Color.red;
ctx.AddObjectToAsset("material", material);

var tempMesh = new Mesh();
DestroyImmediate(tempMesh);
}
}

xx.yusongmomo文件

image

最终效果

image

Ch4.游戏脚本

P78 脚本的生命周期

1.Reset

概述:Reset() 方法仅在非运行模式下才会生效,当把脚本挂在某个游戏对象上时,或者右击已经挂上脚本的对象,从弹出菜单中选择Reset菜单项时,它就会执行

1
2
3
4
5
6
7
8
9
public class MyScript4_2 : MonoBehaviour 
{
#if UNITY_EDITOR
private void Reset()
{
Debug.Log($"GameObject: { gameObject.name } 绑定MyScript4_2.cs 脚本");
}
#endif
}

2.脚本初始化和销毁

概述:脚本挂在游戏对象上,运行时就会立即执行初始化方法Awake(),它是一个同步方法,而Start()方法会在下一帧执行。如果游戏对象或其脚本被删除,就会执行OnDestroy()销毁方法

  • 初始化和销毁在脚本的生命周期中只会执行一次

游戏对象还有禁用和激活状态,OnEnable()OnDisable()可以多次调用

P86 脚本序列化

1.私有序列化数据

两个Serializedxxx

书本解释:如果变量设为private数据,外部无法访问。如果使用拓展编辑器来编辑这些数据的话,可以使用这两个类

官方解释:SerializedPropertySerializedObject 这两个类能够以完全通用的方式编辑对象上的属性(可自动处理撤销),同时还能调整预制件的 UI 样式

官方文档:

官方案例

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

public class MyObject : ScriptableObject
{
public int myInt = 42;
}

public class SerializedPropertyTest : MonoBehaviour
{
void Start()
{
MyObject obj = ScriptableObject.CreateInstance<MyObject>();
SerializedObject serializedObject = new UnityEditor.SerializedObject(obj);

SerializedProperty serializedPropertyMyInt = serializedObject.FindProperty("myInt");

Debug.Log("myInt " + serializedPropertyMyInt.intValue);
}
}

2.ScriptableObject

这里只讲动态创建和保存

1
2
3
4
5
6
7
//创建 ScriptableObject
Script4_12 script = ScriptableObject.CreateInstance<Script4_12>();
//赋值...
//将资源保存到本地
AssetDatabase.CreateAsset(script, "Assets/Resources/Create Script4_12.asset");
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

3.脚本的Attributes特性

概述:本例中自定义了一个RangeInt特性,用于限制Int值范围

PropertyAttribute

用于派生自定义属性特性的基类。这可用于为脚本变量创建特性

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/PropertyAttribute.html

PropertyDrawer

用于从中派生自定义属性绘制器的基类。使用此基类可为您自己的 Serializable 类或者具有自定义 PropertyAttribute 的脚本变量创建自定义绘制器

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/PropertyDrawer.html

1
2
3
4
5
6
7
public class MyScript4_13 : MonoBehaviour 
{
[RangeInt(0, 100)]
public int rangeInt;

public string name;
}
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
public sealed class RangeIntAttribute : PropertyAttribute 
{
public readonly int min;

public readonly int max;

public RangeIntAttribute(int min, int max)
{
this.min = min;
this.max = max;
}
}

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(RangeIntAttribute))]
public sealed class RangeIntDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return 100; //设置面板高度
}
//当数据在面板中发生修改时,就会回调到OnGUI()方法中
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
RangeIntAttribute attribute = this.attribute as RangeIntAttribute;
property.intValue = Mathf.Clamp(property.intValue, attribute.min, attribute.max);
EditorGUI.HelpBox(new Rect(position.x, position.y, position.width, 30),
string.Format("范围 {0} ~ {1}", attribute.min, attribute.max), MessageType.Info);

EditorGUI.PropertyField(new Rect(position.x, position.y + 35, position.width, 20), property, label);
}
}
#endif
image

4.单例脚本

使用:Global脚本不需要在编辑模式下绑定在某个对象上,运行时直接获取它的实例就能操作它了

本人认为最完美的单例解决方案

1
2
3
4
5
6
7
8
9
10
11
public class Global : MonoBehaviour 
{
public static Global instance;

static Global()
{
GameObject go = new GameObject("#Global#");
DontDestroyOnLoad(go);
instance = go.AddComponent<Global>();
}
}

Ch5.UGUI游戏界面

P112 基础元素

1.使用ScrollRect组件制作游戏摇杆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyScript5_5 : ScrollRect 
{
protected float mRadius = 0f;

protected override void Start()
{
base.Start();
//计算摇杆半径
mRadius = (transform as RectTransform).sizeDelta.x * 0.5f;
}

public override void OnDrag(PointerEventData eventData)
{
base.OnDrag(eventData);
var contentPosition = this.content.anchoredPosition;
if (contentPosition.magnitude > mRadius)
{
contentPosition = contentPosition.normalized * mRadius;
SetContentAnchoredPosition(contentPosition);
}
}
}
image

P121 事件系统

1.UI事件(所有监听接口)

P122 自己看

2.UI事件管理

为Text和Image等组件添加监听方法

准备一个帮助类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UGUIEventListener : EventTrigger 
{
public UnityAction<GameObject> onClick;

public override void OnPointerClick(PointerEventData eventData)
{
base.OnPointerClick(eventData);
onClick?.Invoke(gameObject);
}

//获取或添加UGUIEventListener 脚本来实现对游戏对象的监听
static public UGUIEventListener Get(GameObject go)
{
UGUIEventListener listener = go.GetComponent<UGUIEventListener>();
if (listener == null)
{
listener = go.AddComponent<UGUIEventListener>();
}
return listener;
}
}

实现监听

1
UGUIEventListener.Get(text.gameObject).onClick = OnClick;

3.RaycastTarget优化

概述:UGUI的点击事件是基于射线的。如果不需要响应事件,千万不要在Image 和 Text 组件上勾选 RaycastTarget

原因:UI事件会在EventSystem的Update() 方法中调用 Process 时触发。UGUI会遍历屏幕中所有 RaycastTarget 是true的UI,接着就会发射射线,并且排序找到玩家最先触发的那个UI,再抛出事件给逻辑层去响应。这无形中就会带来很多开销。

RaycastTarget线框显示脚本

原理:重写OnDrawGizmos()方法,同时把场景中的所有UI组件找出来,如果勾选了 RaycastTarget,计算出元素的4个顶点,最终用Gizmos.DrawLine()绘制出来即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyScript5_9 : MonoBehaviour 
{
#if UNITY_EDITOR
static Vector3[] fourCorners = new Vector3[4];
private void OnDrawGizmos()
{
foreach(MaskableGraphic g in GameObject.FindObjectsOfType<MaskableGraphic>())
{
RectTransform rectTransform = g.transform as RectTransform;
rectTransform.GetWorldCorners(fourCorners);
Gizmos.color = Color.blue;
for (int i =0; i < 4; i++)
{
Gizmos.DrawLine(fourCorners[i], fourCorners[(i + 1) % 4]);
}
}
}
#endif
}
image

4.渗透UI事件

PointerEventData类

CSDN:https://blog.csdn.net/qq_41056203/article/details/84875282

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
public class MyScript5_10 : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
{
//监听按下
public void OnPointerDown(PointerEventData eventData)
{
PassEvent(eventData, ExecuteEvents.pointerDownHandler);
}

//监听抬起
public void OnPointerUp(PointerEventData eventData)
{
throw new System.NotImplementedException();
}

//监听点击
public void OnPointerClick(PointerEventData eventData)
{
throw new System.NotImplementedException();
}

//把事件传递下去
public void PassEvent<T>(PointerEventData data, ExecuteEvents.EventFunction<T> function)
where T : IEventSystemHandler
{
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(data, results);
GameObject current = data.pointerCurrentRaycast.gameObject;
for(int i = 0; i < results.Count; i++)
{
if (current != results[i].gameObject)
{
ExecuteEvents.Execute(results[i].gameObject, data, function);
//如果只想响应渗透下去的第一个游戏对象,使用break语句跳出循环即可
//break;
}
}
}
}

5.例子.新手引导聚合动画

image image

Shader代码

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
Shader "UI/Default_Mask"
{
Properties
{
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)

_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255

_ColorMask ("Color Mask", Float) = 15


[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0


//-------------------add----------------------
_Center("Center", vector) = (0, 0, 0, 0)
_Silder ("_Silder", Range (0,1000)) = 1000 // sliders
//-------------------add----------------------
}

SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
}

Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}

Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]

Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile __ UNITY_UI_ALPHACLIP

struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO

};

fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
//-------------------add----------------------
float _Silder;
float2 _Center;
//-------------------add----------------------
v2f vert(appdata_t IN)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = IN.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

OUT.texcoord = IN.texcoord;

OUT.color = IN.color * _Color;
return OUT;
}

sampler2D _MainTex;

fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);

#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
//-------------------add----------------------
color.a*=(distance(IN.worldPosition.xy,_Center.xy) > _Silder);
color.rgb*= color.a;
//-------------------add----------------------
return color;

}
ENDCG
}
}
}

脚本代码

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class Script_05_11 : MonoBehaviour
{
//需要聚合的对象(例子中的Unity图标)
public Image target;
//Canvas对象
public Canvas canvas;

private Vector4 m_Center;
private Material m_Material;
private float m_Diameter; // 直径
private float m_Current =0f;

Vector3[] corners = new Vector3[4];

void Awake ()
{

target.rectTransform.GetWorldCorners (corners);
m_Diameter = Vector2.Distance (WordToCanvasPos(canvas,corners [0]), WordToCanvasPos(canvas,corners [2])) / 2f;

float x =corners [0].x + ((corners [3].x - corners [0].x) / 2f);
float y =corners [0].y + ((corners [1].y - corners [0].y) / 2f);

Vector3 center = new Vector3 (x, y, 0f);
Vector2 position = Vector2.zero;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, center, canvas.GetComponent<Camera>(), out position);




center = new Vector4 (position.x,position.y,0f,0f);
m_Material = GetComponent<Image>().material;
m_Material.SetVector ("_Center", center);



(canvas.transform as RectTransform).GetWorldCorners (corners);
for (int i = 0; i < corners.Length; i++) {
m_Current = Mathf.Max(Vector3.Distance (WordToCanvasPos(canvas,corners [i]), center),m_Current);
}

m_Material.SetFloat ("_Silder", m_Current);
}


float yVelocity = 0f;
void Update () {
float value = Mathf.SmoothDamp(m_Current, m_Diameter, ref yVelocity, 0.3f);
if (!Mathf.Approximately (value, m_Current)) {
m_Current = value;
m_Material.SetFloat ("_Silder", m_Current);
}
}

void OnGUI(){
if(GUILayout.Button("Test")){
Awake ();
}
}


Vector2 WordToCanvasPos(Canvas canvas,Vector3 world){
Vector2 position = Vector2.zero;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, world, canvas.GetComponent<Camera>(), out position);
return position;
}
}

P135 Canvas组件

1.图集

SpriteAtlas 类

精灵图集是在 Unity 中创建的一种资源。它是内置精灵打包解决方案的一部分

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/U2D.SpriteAtlas.html

操作流程:

1.确保 Sprite Packer 已启用。在Editor Settings中,设置 Sprite Packer中的Mode为 Always Enabled

2.创建图集。在Project 视图中选择 Create->Sprite Atlas命令创建图集,选择Sprite加入图集

3.读取图集,更换Sprite。使用 Resources.Load() 读取Atlas,接着使用 GetSprite() 读取某张Sprite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyScript5_12 : MonoBehaviour 
{
public Image image;

private SpriteAtlas m_SpriteAtlas = null;

private void Start()
{
m_SpriteAtlas = Resources.Load<SpriteAtlas>("SpriteAtlas");
}

private void OnGUI()
{
if (GUILayout.Button("<size=80>更换sprite</size>"))
{
image.sprite = m_SpriteAtlas.GetSprite("unity");
}
}
}

P142 典型UI技术实例

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
[DisallowMultipleComponent]
public class UIGray : MonoBehaviour
{
private bool _isGray = false;
public bool isGray
{
get{ return _isGray;}
set
{
if(_isGray != value)
{
_isGray = value;
SetGray(isGray);
}
}
}

static private Material _defaultGrayMaterial;
static private Material grayMaterial
{
get
{
if(_defaultGrayMaterial == null)
{
_defaultGrayMaterial = new Material(Shader.Find("UI/Gray"));
}
return _defaultGrayMaterial;
}
}

void SetGray(bool isGray)
{
int i =0, count = 0;
Image [] images = transform.GetComponentsInChildren<Image>();
count = images.Length;
for(i =0; i< count; i++)
{
Image g = images[i];
if(isGray)
{
g.material = grayMaterial;
}else
{
g.material = null;
}
}
}
}
#if UNITY_EDITOR
[CustomEditor (typeof(UIGray))]
public class UIGrayInspector : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
UIGray gray = target as UIGray;
gray.isGray = GUILayout.Toggle( gray.isGray ," isGray");
if(GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
}
#endif

2.粒子特效与UI的排序

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
46
47
48
49
50
51
52
53
54
55
56
57
58
[AddComponentMenu("UI/UIOrder")]
public class UIOrder : MonoBehaviour
{

[SerializeField]
private int _sortingOrder=0;
public int sortingOrder
{
get{
return _sortingOrder;
}
set{
if(_sortingOrder !=value){
_sortingOrder = value;
Refresh();
}
}
}

private Canvas _canvas = null;
public Canvas canvas
{
get
{
if(_canvas == null)
{
_canvas = gameObject.GetComponent<Canvas>();
if(_canvas==null)
_canvas = gameObject.AddComponent<Canvas>();
_canvas.hideFlags = HideFlags.NotEditable;
}
return _canvas;
}
}

public void Refresh()
{
canvas.overrideSorting = true;
canvas.sortingOrder = _sortingOrder;
foreach(ParticleSystemRenderer pariicle in transform.GetComponentsInChildren<ParticleSystemRenderer>(true))
{
pariicle.sortingOrder = _sortingOrder;
}
}

#if UNITY_EDITOR
void OnValidate()
{
Refresh();
}

void Reset()
{
Refresh();
}
#endif

}

3.粒子自适应

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
46
47
48
49
50
51
52
53
54
55
56
public class UIParticleScale : MonoBehaviour 
{
struct ScaleData
{
public Transform transform;
public Vector3 beginScale;
}

const float DESIGN_WIDTH = 1136f;//开发时分辨率宽
const float DESIGN_HEIGHT = 640f;//开发时分辨率高

private Dictionary<Transform,ScaleData> m_ScaleData = new Dictionary<Transform, ScaleData> ();

void Start ()
{
Refresh ();
}

void Refresh()
{
float designScale = DESIGN_WIDTH / DESIGN_HEIGHT;
float scaleRate = (float)Screen.width/(float)Screen.height;

foreach (ParticleSystem p in transform.GetComponentsInChildren<ParticleSystem>(true)) {
if (!m_ScaleData.ContainsKey (p.transform)) {
m_ScaleData [p.transform] = new ScaleData (){ transform = p.transform, beginScale = p.transform.localScale };
}
}
foreach(var item in m_ScaleData)
{
if(scaleRate<designScale)
{
float scaleFactor = scaleRate / designScale;
item.Value.transform.localScale = item.Value.beginScale * scaleFactor;
}else{
item.Value.transform.localScale = item.Value.beginScale;
}
}
}

/// <summary>
/// 子节点发生变化重新刷深度
/// </summary>
void OnTransformChildrenChanged()
{
Refresh ();
}
#if UNITY_EDITOR
//编辑模式下修改分辨率在后Update中刷新
private void Update()
{
Refresh();
}
#endif

}

4.InputField的输入事件

onValueChanged:用于监听输入后的事件

onValidateInput:用于监听每次输入的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Script_05_20 : MonoBehaviour {
public InputField inputField;
public Text tips;
void Start () {
//监听输入后事件
inputField.onValueChanged.AddListener((string content) => {
tips.text = string.Format("已经输入{0}个字符", content.Length);
});

//监听输入文字变化,当出现a时替换成*号
inputField.onValidateInput += delegate (string input, int charIndex, char addedChar)
{
if(addedChar == 'a'){
addedChar = '*';
}
return addedChar;
};
}
}

Ch7.动画系统

P196 模型

1.Prefab

监听Prefab保存

1
2
3
4
5
6
7
8
9
10
public class MyScript7_1 {
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
//监听 Prefab 保存事件
PrefabUtility.prefabInstanceUpdated = delegate(GameObject instance) {
Debug.Log($"Debug.Log(Prefab {AssetDatabase.GetAssetPath(PrefabUtility.GetPrefabParent(instance))}) 被保存");
}
}
}

P203 动画控制器

1.状态机脚本

Animator 类

用于控制 Mecanim 动画系统的接口

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/Animator.html

AnimatorStateInfo 类

有关当前或下一个状态的信息

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/AnimatorStateInfo.html

概述:我们可以给每个状态添加脚本来监听一些状态事件,比如状态开启、状态更新和状态退出等

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
public class NewMachineBehaviour : StateMachineBehaviour
{
//进入当前状态,调用OnStateEnter()方法
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("OnStateEnter");
}

//当前状态更新时,调用 OnStateUpdate() 方法。它在 OnStateEnter() 和 OnStateExit() 之间
//每帧调用
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}

//离开当前状态,调用 OnStateExit() 方法
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}

//OnStateMove() 方法在 Animator.OnAnimatorMove() 之后调用,这里可以处理动画根节点的位移
override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}

// OnStateIK() 方法在 Animator.OnAnimatorIK() 之后调用,这里可以处理IK(反向动力学) 动画
override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{

}
}

2.非运行播放动画

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
46
47
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
using System.Linq;
#endif
[RequireComponent(typeof(Animator))]
public class Script_07_06 : MonoBehaviour {


}

#if UNITY_EDITOR
[CustomEditor(typeof(Script_07_06))]
public class ScriptEditor_07_06 :Editor
{
private AnimationClip[] m_Clips = null;
private Script_07_06 m_Script = null;
void OnEnable()
{
m_Script = (target as Script_07_06);
Animator animator = m_Script.gameObject.GetComponent<Animator> ();
AnimatorController controller = (AnimatorController)animator.runtimeAnimatorController;
m_Clips = controller.animationClips;
}

private int m_SelectIndex = 0;
private float m_SliderValue = 0;
public override void OnInspectorGUI ()
{
base.OnInspectorGUI ();

EditorGUI.BeginChangeCheck ();
m_SelectIndex = EditorGUILayout.Popup("动画",m_SelectIndex,m_Clips.Select(pkg => pkg.name).ToArray());
m_SliderValue = EditorGUILayout.Slider ("进度",m_SliderValue, 0f, 1f);
if (EditorGUI.EndChangeCheck ()) {
AnimationClip clip = m_Clips [m_SelectIndex];
float time = clip.length * m_SliderValue;
clip.SampleAnimation(m_Script.gameObject, time);
}
}

}
#endif

Ch8.持久化数据

P247 XML

1.创建XML

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
public class MyScript8_13 : MonoBehaviour 
{
private void Start()
{
//创建XmlDocument
XmlDocument xmlDoc = new XmlDocument();
XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null);
xmlDoc.AppendChild(xmlDeclaration);

//在节点中写入数据
XmlElement root = xmlDoc.CreateElement("XmlRoot");
xmlDoc.AppendChild(root);
XmlElement group = xmlDoc.CreateElement("Group");
group.SetAttribute("username", "聪头dada");
group.SetAttribute("password", "123");
root.AppendChild(group);

//读取节点并输出 XML 字符串
using (StringWriter sw = new StringWriter())
{
using (XmlTextWriter xmlTextWriter = new XmlTextWriter(sw))
{
xmlDoc.WriteTo(xmlTextWriter);
xmlTextWriter.Flush();
Debug.Log(sw.ToString());
}
}
}
}

image

2.读取与修改

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
public class MyScript_8_14 : MonoBehaviour 
{
void Start()
{
//xml字符串
string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XmlRoot><Group username=\"聪头dada\" password=\"123456\" /></XmlRoot>";

//读取字符串xml
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xml);
//遍历节点
XmlNode nodes = xmlDoc.SelectSingleNode("XmlRoot");
foreach (XmlNode node in nodes.ChildNodes)
{
string username = node.Attributes["username"].Value;
string password = node.Attributes["password"].Value;
Debug.LogFormat("username={0} password={1}", username, password);
//修改其中一条数据
node.Attributes["password"].Value = "88888888";
}

//读取节点并输出xml字符串
using (StringWriter stringwriter = new StringWriter())
{
using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringwriter))
{
xmlDoc.WriteTo(xmlTextWriter);
xmlTextWriter.Flush();
Debug.Log(stringwriter.ToString());
}
}
}
}

image

P251 YAML

1.序列化与反序列化

概述:使用AssetStore提供的YamlDotNet,对于参与序列化类中的变量,其属性必须设置成get或者set,不然无法序列化

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
public class Script_08_16  :MonoBehaviour
{

private void Start()
{
//创建对象
Data data = new Data();
data.name = "雨松momo";
data.password = "123456";
data.list = new List<string>(){"a","b","c"};

//序列化yaml字符串
Serializer serializer = new Serializer();
string yaml = serializer.Serialize(data);
Debug.LogFormat("serializer : \n{0}",yaml);

//反序列化成类对象
Deserializer deserializer = new Deserializer();
Data data1 = deserializer.Deserialize<Data>(yaml);
Debug.LogFormat("deserializer : name={0} password={1}", data1.name,data1.password);
}


class Data
{
public string name { get; set; }
public string password { get; set; }
public List<string> list { get; set; }
}

}

2.读取配置

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
public class Script_08_17  :MonoBehaviour
{
private IDictionary<YamlNode, YamlNode> m_MappingData;
private void Start()
{
//读取yaml字符串
string document = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "yaml.txt"));
var input = new StringReader(document);
var yaml = new YamlStream();
yaml.Load(input);

//读取root节点
var mapping =
(YamlMappingNode)yaml.Documents[0].RootNode;

m_MappingData = mapping.Children;
}


private void OnGUI()
{
GUILayout.Label(string.Format("<size=50>服务器列表:{0}</size>", m_MappingData["ServerList"]));
GUILayout.Label(string.Format("<size=50>服务器端口:{0}</size>", m_MappingData["Port"]));
GUILayout.Label(string.Format("<size=50>是否启动调试:{0}</size>", m_MappingData["Debug"]));
}

}

image

Ch9.静态对象

P261 动态加载烘焙信息

使用动态加载生成的预制体会丢失烘焙数据,此时需要在编辑器内为Prefab挂载PrefabLightmap脚本,烘焙完光照贴图后,使用GameObject/Light/ToPrefab记录烘焙数据。这样在动态加载过程中就会有烘焙信息了

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
46
47
48
49
50
51
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class PrefabLightmap : MonoBehaviour {

public int lightmapIndex;
public Vector4 lightmapScaleOffset;

void Awake ()
{
//Prefab实例化后赋值
Renderer renderer = GetComponent<Renderer> ();
if (renderer) {
renderer.lightmapIndex = lightmapIndex;
renderer.lightmapScaleOffset = lightmapScaleOffset;
}
}
#if UNITY_EDITOR
[MenuItem("GameObject/Light/ToPrefab")]
static void ToPrefab()
{
//确保选择一个Hierarchy视图下的游戏对象
if (Selection.activeTransform) {
Renderer renderer = Selection.activeTransform.GetComponent<Renderer> ();
//确保有renderer组件
if (renderer) {

PrefabLightmap prefabLightmap = Selection.activeTransform.GetComponent<PrefabLightmap> ();
if (!prefabLightmap) {
prefabLightmap = Selection.activeTransform.gameObject.AddComponent<PrefabLightmap> ();
}
prefabLightmap.lightmapIndex = renderer.lightmapIndex;
prefabLightmap.lightmapScaleOffset = renderer.lightmapScaleOffset;

Object prefab = PrefabUtility.GetPrefabParent (renderer.gameObject) ;
//如果有Prefab文件就更新,没有就创建新的
if (prefab) {
PrefabUtility.ReplacePrefab (Selection.activeTransform.gameObject, prefab, ReplacePrefabOptions.ConnectToPrefab);
} else {
PrefabUtility.CreatePrefab (string.Format ("Assets/Resources/{0}.prefab", Selection.activeTransform.name), Selection.activeTransform.gameObject, ReplacePrefabOptions.ConnectToPrefab);
}
}
}
}
#endif
}

P262 复制游戏对象(带烘焙)

注意:使用Ctrl + Shift + D复制生成的游戏对象仅带有原物体的烘焙信息,不带有阴影信息

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 UnityEditor;
using System.Collections.Generic;

public class Script_09_03 :MonoBehaviour
{

[MenuItem("Tool/DuplicateGameObject %#d")]
static void DuplicateGameObject()
{
if (Selection.activeTransform)
{
Dictionary<string, Renderer> save = new Dictionary<string, Renderer> ();

//根据相对路径保存Renderer信息
foreach (var renderer in Selection.activeTransform.GetComponentsInChildren<Renderer> ()) {
string path = AnimationUtility.CalculateTransformPath (renderer.transform, Selection.activeTransform);
save [path] = renderer;
}
//执行复制
EditorApplication.ExecuteMenuItem ("Edit/Duplicate");
//还原烘焙信息
foreach (var renderer in Selection.activeTransform.GetComponentsInChildren<Renderer> ()) {
string path = AnimationUtility.CalculateTransformPath (renderer.transform, Selection.activeTransform);
if (save.ContainsKey (path)) {
renderer.lightmapIndex = save [path].lightmapIndex;
renderer.lightmapScaleOffset = save [path].lightmapScaleOffset;
}
}
}
}
}

P269 脚本静态合批

使用脚本静态合批,不需要选中Static标记。运行时可移动Root节点

1
2
3
4
5
6
7
8
9
10
public class MyScript9_5 : MonoBehaviour
{
public GameObject[] datas;

void Start()
{
//将数组中的游戏对象合并在同一个Root节点下
StaticBatchingUtility.Combine(datas, gameObject);
}
}

P272 获取寻路路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Script_09_08 :MonoBehaviour
{
public NavMeshAgent navMeshAgent;
public Transform target;
private NavMeshPath m_Path = null;
void Start()
{
m_Path = new NavMeshPath ();
//计算路径
NavMesh.CalculatePath(transform.position, target.position, NavMesh.AllAreas, m_Path);

}

void Update () {
//绘制路径
for (int i = 0; i < m_Path.corners.Length-1; i++)
Debug.DrawLine(m_Path.corners[i], m_Path.corners[i+1], Color.red);
}

}

P274 导出寻路网格信息

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.AI;
using System.IO;

using UnityEditor;
using System.Text;


public class Script_09_09 :MonoBehaviour
{
//X坐标格子的数量
public int width;
//Y坐标格子的数量
public int height;
//每个格子的大小
public int size;


void OnDrawGizmosSelected()
{
//确保当前场景烘焙过
if (NavMesh.CalculateTriangulation ().indices.Length > 0) {
//获取场景名
string scenePath = UnityEditor.SceneManagement.EditorSceneManager.GetSceneAt(0).path;
string sceneName = System.IO.Path.GetFileName(scenePath);
string filePath = Path.ChangeExtension(Path.Combine (Application.dataPath, sceneName),"txt");
if (File.Exists (filePath)) {
File.Delete (filePath);
}
//准备写入数据
StringBuilder sb = new StringBuilder ();
sb.AppendFormat ("scene={0}", sceneName).AppendLine ();
sb.AppendFormat ("width={0}", width).AppendLine ();
sb.AppendFormat ("height={0}", height).AppendLine ();
sb.AppendFormat ("size={0}", size).AppendLine ();
sb.Append ("data={").AppendLine ();

Gizmos.color = Color.yellow;
Gizmos.DrawSphere (transform.position, 1);

float widthHalf = (float)width / 2f;
float heightHalf = (float)height / 2f;
float sizeHalf = (float)size / 2f;
//从左到右从下到上一次写入每个格子的数据
for (int i = 0; i < height; i++) {
sb.Append("\t{");
Vector3 startPos = new Vector3 (-widthHalf + sizeHalf, 0, -heightHalf + (i * size) + sizeHalf);
for (int j = 0; j < width; j++) {
Vector3 source = startPos + Vector3.right * size * j;
NavMeshHit hit;
Color color = Color.red;
int a = 0;
//检测当前格子是否可以行走
if (NavMesh.SamplePosition (source, out hit, 0.2f, NavMesh.AllAreas)) {
color = Color.blue;
a = 1;
}
sb.AppendFormat (j > 0?",{0}":"{0}", a);
Debug.DrawRay (source, Vector3.up, color);
}
sb.Append ("}").AppendLine ();
}
sb.Append ("}").AppendLine ();
//绘制格子的总区域
Gizmos.DrawLine (new Vector3 (-widthHalf, 0, -heightHalf), new Vector3 (widthHalf, 0, -heightHalf));
Gizmos.DrawLine (new Vector3 (widthHalf, 0, -heightHalf), new Vector3 (widthHalf, 0, heightHalf));
Gizmos.DrawLine (new Vector3 (widthHalf, 0, heightHalf), new Vector3 (-widthHalf, 0, heightHalf));
Gizmos.DrawLine (new Vector3 (-widthHalf, 0, heightHalf), new Vector3 (-widthHalf, 0, -heightHalf));

//写入文件
File.WriteAllText (filePath, sb.ToString ());
}
}
}

#endif

Ch.11 资源加载与优化

P299 卸载无用资源

游戏对象与资源是一种引用关系,调用GameObject.Destroy()GameObject.DestroyImmediate()时,只会卸载掉它的对象,它身上引用的贴图和Mesh还在内存中。此时,需要使用EditorUtility.UnloadUnusedAssetsImmediate()方法可以卸载编辑器下无用的资源

1
2
3
4
5
6
7
8
public class MyScript11_6
{
[MenuItem("Assets/My Tools/UnloadUnusedAssetsImmediate", false, 3)]
static void UnloadUnusedAssetsImmediate()
{
EditorUtility.UnloadUnusedAssetsImmediate();
}
}

P305 Resources

Resources文件夹是Unity中标志性目录,这个目录下的资源无论是否有引用关系,都会被强制打包在游戏中。该目录下的资源尽量不要直接引用在场景中,不然这个资源会被场景和Resources打包成两份

可以使用Resouces.UnloadAsset()以及Resources.UnloadUnusedAssets方法强制卸载资源。该操作是异步的,可以使用isDone来判断是否完成

Ch12.自动化与打包

P336 监听资源导入

AssetPostprocessor 类

AssetPostprocessor 允许您挂接到导入管线并在导入资源前后运行脚本

文档:https://docs.unity.cn/cn/2019.4/ScriptReference/AssetPostprocessor.html

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
46
47
48
49
50
using UnityEngine;
using UnityEditor;

public class Script_12_01 : AssetPostprocessor
{
//导入声音前
void OnPreprocessAudio()
{
AudioImporter audioImporter = (AudioImporter)assetImporter;
}
//导入动画前
void OnPreprocessAnimation()
{
ModelImporter modelImporter = (ModelImporter)assetImporter;
}
//导入模型前
void OnPreprocessModel ()
{
ModelImporter modelImporter = (ModelImporter)assetImporter;
}
//导入贴图前
void OnPreprocessTexture()
{
TextureImporter textureImporter = (TextureImporter)assetImporter;
}
//导入声音后
void OnPostprocessAudio(AudioClip clip)
{
Debug.Log (AssetDatabase.GetAssetPath (clip));
}
//导入模型后
void OnPostprocessModel(GameObject g)
{
Debug.Log (AssetDatabase.GetAssetPath (g));
}
//导入材质后
void OnPostprocessMaterial(Material material)
{
Debug.Log (AssetDatabase.GetAssetPath (material));
}
//导入精灵后
void OnPostprocessSprites (Texture2D texture ,Sprite[] sprites ) {
Debug.Log("Sprites: " + sprites.Length);
}
//导入贴图
void OnPostprocessTexture (Texture2D texture) {
Debug.Log("Texture2D: (" + texture.width + "x" + texture.height + ")");
}
}

P342 主动设置资源格式

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;

public class Script_12_07 : AssetPostprocessor
{
[MenuItem("Assets/SetTextureFormat",false,-1)]
static void SetTextureFormat()
{
//确保在Project视图中选择一个文件
if(Selection.assetGUIDs.Length > 0){

AssetImporter import = AssetImporter.GetAtPath (AssetDatabase.GetAssetPath (Selection.activeObject));
//确保选择的是一个贴图文件
if (import is TextureImporter) {
(import as TextureImporter).SetPlatformTextureSettings ("Standalone", 2048, TextureImporterFormat.RGBA32, true);
//保存并且重新导入
import.SaveAndReimport ();
}
}
}
}

P343 待保存状态

只有对象变成dirty后,才可以进行保存(修改后的*就是dirty标志)。有时如果通过代码区设置游戏对象或者对象身上的信息时,很可能就不会造成dirty,这样数据是无法保存的。

  • 使用EditorSceneManager.MarkSceneDirty()强制设置某个场景为dirty状态
  • 使用EditorUtility.SetDirty()设置某个资源变成dirty状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;

public class Script_12_08 : AssetPostprocessor
{
[MenuItem("Assets/SetSceneDirty",false,-1)]
static void SetSceneDirty()
{
//设置场景dirty
EditorSceneManager.MarkSceneDirty (EditorSceneManager.GetActiveScene ());
//设置Prefab dirty
EditorUtility.SetDirty (AssetDatabase.LoadAssetAtPath<GameObject> ("Assets/Cube.prefab"));
}
}

P344 自动执行MenuItem

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

public class Script_12_09 : AssetPostprocessor
{
[MenuItem("Assets/AutoMenuItem",false,-1)]
static void AutoMenuItem()
{
//自动选择场景
Selection.activeObject = AssetDatabase.LoadAssetAtPath ("Assets/Scene.unity", typeof(Object));
//执行Command + D 复制
EditorApplication.ExecuteMenuItem ("Edit/Duplicate");

//自动选择Prefab
Selection.activeObject = AssetDatabase.LoadAssetAtPath ("Assets/Cube.prefab", typeof(Object));
//执行Command + D 复制
EditorApplication.ExecuteMenuItem ("Edit/Duplicate");
}
}

P356 打包前后的事件

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
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;

public class Script_12_14 :IPreprocessBuild, IPostprocessBuild
{

int IOrderedCallback.callbackOrder {
get {
return 0;
}
}

//打包前事件
void IPreprocessBuild.OnPreprocessBuild (BuildTarget target, string path)
{
//设置版本号和游戏名
PlayerSettings.bundleVersion = "2.0.0";
PlayerSettings.productName = "雨松momo";
}

//打包后事件
void IPostprocessBuild.OnPostprocessBuild (BuildTarget target, string path)
{
Debug.LogFormat ("游戏包生成路径:{0}", path);
}

}

Ch13.3D游戏开发

P398 多场景烘焙

使用脚本烘焙,可以保证没有接缝

1
2
3
4
5
6
7
8
9
10
11
12
public class MyScript3_15 {
[MenuItem("Tool/BakeMultipleScenes")]
static void BakeMultipleScenes()
{
//指定烘焙场景
Lightmapping.BakeMultipleScenes(new string[]{
"Assets/Scene.unity",
"Assets/Scene 1.unity",
"Assets/Scene 2.unity"
})
}
}

P399 射线检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyScript13_17 : MonoBehaviour 
{
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

RaycastHit hit;

if (Physics.Raycast(ray, out hit))
{
Debug.Log($"Raycast: { hit.collider.name } 3D坐标: {hit.point }");
}
RaycastHit[] hits = Physics.RaycastAll(ray);
foreach(var h in hits)
{
Debug.Log($"Raycast: { h.collider.name } 3D坐标: { h.point }");
}
}
}
}

P410 图片数字

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
using UnityEngine;

public class UINumber : MonoBehaviour {

//精灵原始资源
[SerializeField]
private Sprite[] numberRes;
//输入区域
[TextArea(2,4)][SerializeField]
private string text;

void Start ()
{
Refresh();
}


void Refresh()
{
for (int i = 0; i < text.Length; i++)
{
int a;
if (int.TryParse(text[i].ToString(), out a))
{
//如果缓存中没有则创建新的
SpriteRenderer spriteRenderer = new GameObject().AddComponent<SpriteRenderer>();
spriteRenderer.sprite = numberRes[a];
spriteRenderer.gameObject.SetActive(true);
spriteRenderer.gameObject.name = a.ToString();
spriteRenderer.transform.SetParent(transform, false);
spriteRenderer.transform.localPosition = new Vector3(i * 0.2f, 0f, 0f);
}
}
}
}

P414 运行时合并网格

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

public class Script_13_26 : MonoBehaviour
{

public SkinnedMeshRenderer[] combinMeshs;
public SkinnedMeshRenderer skinnedMeshRenderer;

private void Awake()
{

CombineInstance[] combines = new CombineInstance[combinMeshs.Length];

for (int i = 0; i < combinMeshs.Length; i++)
{
combines[i].mesh = combinMeshs[i].sharedMesh;
combines[i].transform = combinMeshs[i].transform.localToWorldMatrix;
}
var mesh = new Mesh();
mesh.name = "combine";
mesh.CombineMeshes(combines);
skinnedMeshRenderer.sharedMesh = mesh;
}
}
 评论