《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的资源
自定义标签:
P16 Hierarchy视图 概述:Project视图中的游戏资源,如果需要出现在正式游戏中,就需要Hierarchy视图了。通常只有游戏对象才能放进
游戏对象分为:预先编辑和运行时代码动态生成
搜索对象:
P17 Inspector视图 概述:Inspector视图承载着所有游戏对象以及游戏资源组件参数的编辑工作
原理:就是键入一些数据并将其序列化在这个对象身上。在Hierarchy选中,数据就是保存在这个对象所在场景上;在Project视图中选中,数据就是保存在这个资源本身上
P19 Scene视图 概述:Scene视图就是游戏最终画面的自由视角
Scene 导航栏
操作原点:
Pivot:表示父对象的操作原点就是自身的坐标点
Center:表示父对象的操作原点是所有子对象共同的中心点
Scene 标题栏
开关音频:
P22 Game视图 概述:Game视图就是最终展示给玩家的一面,把游戏主摄像机看到的内容显示到了这里
Game 标题栏
主要功能就是控制Game视图的显示,这里的设置并不能影响最终发布游戏的结果,但是可以让开发更方便一些
开关音频:
P23 导航栏视图 概述:所有视图通用的一些功能以及设置信息
播放器及使用技巧:
技巧1:点击暂停再运行,游戏会被暂停到第一帧
技巧2:可以利用 Ctrl + Shift + P
组合键暂停/恢复游戏
P25 其他功能 概述:隐藏的小功能
1.小锁头,锁住Inspector面板
2.窗口菜单
3.保存组件参数
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("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); } [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 { [InitializeOnLoadMethod ] static void InitializeOnLoadMethod () { EditorApplication.projectWindowItemOnGUI = delegate (string guid, Rect selectionRect) { 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 { [InitializeOnLoadMethod ] static void InitializeOnLoadMethod () { EditorApplication.projectChanged += () => Debug.Log("change" ); } public static void OnWillCreateAsset (string path ) { Debug.Log("OnWillCreateAsset Path: " + path); } public static string [] OnWillSaveAssets (string [] paths ) { if (paths != null ) Debug.Log("OnWillSaveAssets Path: " + string .Join("," , paths)); return paths; } public static AssetMoveResult OnWillMoveAsset (string oldPath, string newPath ) { Debug.Log("OnWillMoveAsset from: " + oldPath + "to: " + newPath); return AssetMoveResult.DidMove; } public static AssetDeleteResult OnWillDeleteAsset (string assetPath, RemoveAssetOptions option ) { Debug.Log("delete: " + assetPath); 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 { [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 )); } [MenuItem("Extension/Undo/AddComponent" ) ] static void AddComponent () { GameObject go = Selection.activeGameObject; Rigidbody body = Undo.AddComponent<Rigidbody>(go); } [MenuItem("Extension/Undo/DestroyObjectImmediate" ) ] static void DestroyObjectImmediate () { GameObject go = Selection.activeGameObject; Undo.DestroyObjectImmediate(go); } [MenuItem("Extension/Undo/SetTransform" ) ] static void SetTransform () { Transform root = Camera.main.transform; Transform transform = Selection.activeTransform; Undo.SetTransformParent(transform, root, "Main Camera" ); } [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操作分组,撤销到指定位置 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状态" )) { 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 { [InitializeOnLoadMethod ] static void InitializeOnLoadMethod () { EditorApplication.hierarchyWindowItemOnGUI = delegate (int instanceID, Rect selectionRect) { if (Selection.activeObject && instanceID == Selection.activeObject.GetInstanceID()) { float width = 50f ; selectionRect.x += (selectionRect.width - width); selectionRect.width = width; if (GUI.Button(selectionRect, AssetDatabase.LoadAssetAtPath<Texture>("Assets/Chapter3/unity.png" ))) { Debug.Log($"click: { Selection.activeObject.name } " ); } } }; } }
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 () { 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) { GameObject selectedGameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject; if (selectedGameObject) { Vector2 mousePosition = Event.current.mousePosition; EditorUtility.DisplayPopupMenu(new Rect(mousePosition.x, mousePosition.y, 0 , 0 ), "Window/Test" , null ); Event.current.Use(); } } } }
脚本:重写 Image 菜单项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 { public override void OnInspectorGUI () { if (GUILayout.Button("拓展按钮" )) { Debug.Log(target.name); } base .OnInspectorGUI(); } }
2.拓展继承组件 概述:有些系统组件可能在Unity内部已经重写了绘制方法,但是外部访问不了内部代码,修改起来比较麻烦。例如Transform组件,如果直接使用上节方式进行扩展,会出问题(如下图)
分析: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 () { 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(); } }
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 [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("拓展按钮下" )) { } } }
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/组件名/
开头
MenuCommand 类 用于提取 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 { [MenuItem("CONTEXT/Rigidbody/Do Something" ) ] static void DoSomething (MenuCommand command ) { 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 属性用于向上下文菜单添加命令。
在该附加脚本的 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 { [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; #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 视图中进行视觉调试或设置
所有辅助图标绘图都必须在此脚本的 OnDrawGizmos 或 OnDrawGizmosSelected 函数中进行。\ 每一帧都调用 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.BeginGUI(); GUI.backgroundColor = Color.red; if (GUILayout.Button("click" , GUILayout.Width(200f ))) { Debug.Log($"click = { camera.name } " ); } GUILayout.Label("Label" ); Handles.EndGUI(); } } }
脚本:常驻辅助UI 原理:重写SceneView.onSceneGUIDelegate,依然需要在Handles.BeginGUI()
和Handles.EndGUI()
中间完成绘制
1 2 3 4 5 6 7 8 9 10 11 12 [InitializeOnLoadMethod ] static void InitializeOnLoadMethod (){ 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!" ); } }
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可序列化变量而来的。此外,也可以通过EditorGUI
或EditorGUILayout
来对它进行绘制,让面板具有可操作性
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(); 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
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 () { } private void OnLostFocus () { Debug.Log("失去焦点" ); } private void OnProjectChange () { Debug.Log("Project 视图发生改变时调用" ); } private void OnSelectionChange () { Debug.Log("在 Hierarchy 或者 Project 视图中选择一个对象时调用" ); } private void Update () { } }
3.EditorWindows 下拉菜单 概述:在EditorWindows编辑窗口的右上角,有个下拉菜单,我们也可以对该菜单中的选项进行扩展,这里需要实现IHasCustomMenu
接口
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" )); 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(); } }
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" ); } }
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 ) { 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文件
最终效果
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数据,外部无法访问。如果使用拓展编辑器来编辑这些数据的话,可以使用这两个类
官方解释:SerializedProperty
和 SerializedObject
这两个类能够以完全通用的方式编辑对象上的属性(可自动处理撤销),同时还能调整预制件的 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 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 ; } 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
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 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); } } }
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); } 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 }
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); } } } }
5.例子.新手引导聚合动画
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" , 2 D) = "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 _Center("Center" , vector ) = (0 , 0 , 0 , 0 ) _Silder ("_Silder" , Range (0 ,1000 )) = 1000 } 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; float _Silder; float2 _Center; 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 color.a*=(distance(IN.worldPosition.xy,_Center.xy) > _Silder); color.rgb*= color.a; 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 { public Image target; 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; } } } void OnTransformChildrenChanged () { Refresh (); } #if UNITY_EDITOR private void Update () { Refresh(); } #endif }
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); }); 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 () { 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 { override public void OnStateEnter (Animator animator, AnimatorStateInfo stateInfo, int layerIndex ) { Debug.Log("OnStateEnter" ); } override public void OnStateUpdate (Animator animator, AnimatorStateInfo stateInfo, int layerIndex ) { } override public void OnStateExit (Animator animator, AnimatorStateInfo stateInfo, int layerIndex ) { } override public void OnStateMove (Animator animator, AnimatorStateInfo stateInfo, int layerIndex ) { } 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 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 ); using (StringWriter sw = new StringWriter()) { using (XmlTextWriter xmlTextWriter = new XmlTextWriter(sw)) { xmlDoc.WriteTo(xmlTextWriter); xmlTextWriter.Flush(); Debug.Log(sw.ToString()); } } } }
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 () { string xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><XmlRoot><Group username=\"聪头dada\" password=\"123456\" /></XmlRoot>" ; 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" ; } using (StringWriter stringwriter = new StringWriter()) { using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringwriter)) { xmlDoc.WriteTo(xmlTextWriter); xmlTextWriter.Flush(); Debug.Log(stringwriter.ToString()); } } } }
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" }; 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 () { string document = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "yaml.txt" )); var input = new StringReader(document); var yaml = new YamlStream(); yaml.Load(input); 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" ])); } }
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 () { Renderer renderer = GetComponent<Renderer> (); if (renderer) { renderer.lightmapIndex = lightmapIndex; renderer.lightmapScaleOffset = lightmapScaleOffset; } } #if UNITY_EDITOR [MenuItem("GameObject/Light/ToPrefab" ) ] static void ToPrefab () { if (Selection.activeTransform) { Renderer renderer = Selection.activeTransform.GetComponent<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) ; 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> (); 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 () { 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 { public int width; 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 () { 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 () { EditorSceneManager.MarkSceneDirty (EditorSceneManager.GetActiveScene ()); EditorUtility.SetDirty (AssetDatabase.LoadAssetAtPath<GameObject> ("Assets/Cube.prefab" )); } }
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)); EditorApplication.ExecuteMenuItem ("Edit/Duplicate" ); Selection.activeObject = AssetDatabase.LoadAssetAtPath ("Assets/Cube.prefab" , typeof (Object)); 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; } }