Debug.Log (or print)是一般的调试手段,用来查看变量的状态,但万一输出的多了,它可是都写入硬盘的,随着时间它会逐步影响程序的性能,造成之后的程序调试会不准确,所以正规的调试还是推荐使用MonoDevelop Debugger,类似于VS的断点调试。
图像左上角就是调试开始的按钮,在中间红色框中左键确定断点后,先点击开始MonoDevelop的调试,在到Unity中点击执行,便会自动跳回到MonoDevelop中的断点位置中,我们也可以在下方的Debug界面查看到变量的状态。
点击上图红色箭头按钮可以进行单步调试,下方可以自行添加需要查看的变量
右键断点,我们还可以选择BreakPoint Properties,里面可以设置断点的属性,例如到这个断点就打印一条消息,判断值大小等等,如下图
对很多Unity开发者来说,这可能不算什么,但是这确实是非常重要的话题,保持这些习惯,能使你编程更加简单。
简单来说,就是一个类只负责一个任务。
每个类都有自己的小小任务,你可以通过结合这些小任务来解决大目标。如果这听起来我像在描述一个组件,这是因为两者配合起来特别完美。
举个例子,我们有一个Player类。Player处理输入,物理特性,武器,血量,财产。
Player有太多需要负责,我们应该打破它成多个组件,每个组件做一件事。
- PlayerInput - 负责处理输入
- PlayerPhysics - 负责驱动物理模拟
- WeaponManager - 负责管理玩家武器
- Health - 负责玩家健康度
- PlayerInventory - 负责玩家的财产
哈,这样就好多了。现在,这些类相互依赖,但这还会导致一些问题,让我们看看后面如何解决。
如果一个类依赖其他类,使依赖成为抽象依赖。
依赖倒置是当一个类调用其他类,我们应该用一些抽象来替代,这样能较好地隔离其他类。最好的就是接口。所以A类调用B类的方法,实际调用的是ISomeInterface接口的方法,方法由B类实现。
另一种是B类继承的抽象基类,A类作为B类的基类而不是A类直接调用B类。
当然最不可取的依赖就是A类直接依赖于B类,这是我们需要避免的。
所以对于我们之前的例子,这些组件可能有相当紧密的耦合。PlayerInput可能依赖于PlayerPhysics的存在,WeaponManager可能依赖于Inventory的存在,等等。
让我们抽象他们:
- IActorPhysics - 接收输入和物理模拟效果的类的接口
- IDamageable - 接收伤害的类的接口
- IInventory - 储存和检索物品的类的接口
现在我们的PlayerPhysics实现IActorPhysics,我们的Health实现IDamageable,我们的PlayInventory实现IInventory。
所以PlayerInput只依赖于一些IActorPhysics接口,WeaponManager依赖于一些IInventory接口,可能一些处理对玩家的伤害依赖于一些IDamageable接口。
依赖倒置加强了独立责任。PlayerInput不应该关心玩家如何移动,也不应该关心什么移动了玩家。
在一些情况下,SendMessage也能完成同意的目标。
在很多Unity中看过这样的代码:
switch( behavior )
{
case 1: // some behavior
case 2: // another behavior
case 3: // yet another behavior
case 4: // the last behavior
}
它确实正常运行,但我们能做的更好。这段代码实际上是可以模块化的。每个行为都是一个模块。“behavior”应该是一个实例(假设我们的模块都是一个委托,即使可能只是一个类或者结构体),我们的代码可以是这样:
behavior();
然后你可以像这样分配现有的模块:
MyClass.behavior = someBehavior;
someBehavior可能是一个函数,或者我们的模块是一个类,一个类实例或者其他。
这是非常有用的,特别是应用在状态机和菜单系统中。当增加或减少新的状态或者菜单时,我们就没必要每次扩展我们的switch语句添加我们想要的东西。
Unity编辑器允许添加自定义菜单,而看起来像是内建的菜单。对于添加需要经常通过编辑器界面频繁直接访问的通用功能来说,这是非常有用的。接下来就会描述如何一个新的菜单添加到Unity编辑器中,尝试提供一个真实生活中的例子。
为了在顶部工具栏添加新的菜单,你应该创建一个editor脚本(脚本放在Editor文件夹中的任何位置)。菜单项在脚本中作为静态方法创建。
比如,添加一个新的“Tools”菜单,提供一些选项,以下是添加一个新的工具菜单,附带一个选项(清除所有PlayerPrefs数据):
using UnityEngine;
using UnityEditor;
public class MenuItems
{
[MenuItem("Tools/Clear PlayerPrefs")]
private static void NewMenuOption()
{
PlayerPrefs.DeleteAll();
}
}
这创建了一个新的叫做Tools的编辑器菜单,菜单中的选项叫做Clear PlayerPrefs:
当然也可以在已存在的菜单下创建菜单项,也能为更好的结构和组织创建多级菜单:
using UnityEngine;
using UnityEditor;
public class MenuItemsExample
{
// Add a new menu item under an existing menu
[MenuItem("Window/New Option")]
private static void NewMenuOption()
{
}
// Add a menu item with multiple levels of nesting
[MenuItem("Tools/SubMenu/Option")]
private static void NewNestedOption()
{
}
}
为了使代码老手和键盘党工作更快,新的菜单项可以分配热键 - 快捷组合键将自动启动。
这里有一些支持的健(也可以组合):
- % - CTRL on Window/CMD on OSX
- # - Shift
- & - Alt
- LEFT/RIGHT/UP/DOWN - Arrow keys
- F1 … F2 - F keys
- HOME, END, PGUP, PGDN
字符键不是键序列的一部分,添加的话需要前面加一个下划线(如_g就是热键“G”)
热键组合放在菜单项之后,并以空格隔开,例:
// Add a new menu item with hotkey CTRL-SHIFT-A
[MenuItem("Tools/New Option %#a")]
private static void NewMenuOption()
{
}
// Add a new menu item with hotkey CTRL-G
[MenuItem("Tools/Item %g")]
private static void NewNestedOption()
{
}
// Add a new menu item with hotkey G
[MenuItem("Tools/Item2 _g")]
private static void NewOptionWithHotkey()
{
}
菜单项与热线会以组合的形式展现,比如上图的代码会产生这样的菜单:
注意:系统没有验证重复的热键!定义多个相同热键的菜单项,只有一个菜单项会响应。
如所见,菜单属性控制顶级菜单。
Unity也有一些特殊的路径表现菜单内容(能直接通过右键访问)
以下是一些例子:
// Add a new menu item that is accessed by right-clicking on an asset in the project view
[MenuItem("Assets/Load Additive Scene")]
private static void LoadAdditiveScene()
{
var selected = Selection.activeObject;
EditorApplication.OpenSceneAdditive(AssetDatabase.GetAssetPath(selected));
}
// Adding a new menu item under Assets/Create
[MenuItem("Assets/Create/Add Configuration")]
private static void AddConfig()
{
// Create and add a new ScriptableObject for storing configuration
}
// Add a new menu item that is accessed by right-clicking inside the RigidBody component
[MenuItem("CONTEXT/Rigidbody/New Option")]
private static void NewOpenForRigidBody()
{
}
一些菜单项只在一些特定的场景下有意义,有些时候不应该有效。通过添加validation方法,根据使用场景有效/无效菜单项。
validation方法是静态方法,标记菜单项属性,传递true验证参数。
validation方法应该有相同的菜单路径,应该返回一个boolean值确定菜单想是否激活。
比如:
[MenuItem("Assets/ProcessTexture")]
private static void DoSomethingWithTexture()
{
}
// Note that we pass the same path, and also pass "true" to the second argument.
[MenuItem("Assets/ProcessTexture", true)]
private static bool NewMenuOptionValidation()
{
// This returns true when the selected object is a Texture2D (the menu item will be disabled otherwise).
return Selection.activeObject.GetType() == typeof(Texture2D);
}
当右键任何东西,不是texture类型,菜单项就不会被激活。
优先级是一个数字,能分配到菜单项,通过MenuIten参数传递,来控制菜单项在根菜单的顺序。
菜单项也根据分配的数字自动分组(增量为50):
[MenuItem("NewMenu/Option1", false, 1)]
private static void NewMenuOption()
{
}
[MenuItem("NewMenu/Option2", false, 2)]
private static void NewMenuOption2()
{
}
[MenuItem("NewMenu/Option3", false, 3)]
private static void NewMenuOption3()
{
}
[MenuItem("NewMenu/Option4", false, 51)]
private static void NewMenuOption4()
{
}
[MenuItem("NewMenu/Option5", false, 52)]
private static void NewMenuOption5()
{
}
以下是一些额外关于添加新菜单项的类的列表
当在一个inspector中添加一个新的菜单项(使用“CONTEXT/Component“,就像之前描述的),有时有必要得到引用给实际的组件。(比如修改它的数据)。
这能通过添加MenuCommand参数给静态方法来定义一个新菜单项:
[MenuItem("CONTEXT/RigidBody/New Option")]
private static void NewMenuOption(MenuCommand menuCommand)
{
// The RigidBody component can be extracted from the menu command using the context field.
var rigid = menuCommand.context as RigidBody;
}
如所见代码例子,当触发菜单项,这组件能通过context范围进行访问。
这参数允许定义context菜单项。它的效果跟通过MenuItem参数“CONTEXT/…”定义菜单项一模一样。
使用这参数的区别是,你只是定义本组件的菜单,而使用MenuItem方法,你可以定义其他组件的菜单。
比如:
public class NameBehaviour : MonoBehaviour
{
public string Name;
[ContextMenu("Reset Name")]
private static void ResetName()
{
Name = string.Empty;
}
}
这参数用于MonoBehaviour组件,允许添加context菜单。
这个参数添加在属性前面,而不是方法,它接收两个参数:菜单项名字和当菜单项选择后要调用的方法名。
例子:
public class NameBehaviour : MonoBehaviour
{
[ContextMenuItem("Randomize Name", "Randomize")]
public string Name;
private void Randomize()
{
Name = "Some Random Name";
}
}
这个代码结果就是当右键这个组件的属性时会产生菜单