Unity编辑器脚本:如何在Unity编辑器窗口中制作太空侵略者

上周,我花了一些时间在Unity Editor窗口中制作一个可用的(虽然是基本的)Space Invaders版本。 我为什么要这样做? 因为学习新事物要比使事情变得有趣但更好的方法还完全没有用。 如果您想跳过业务,可以在GitHub上签出代码存储库。 否则,我将尝试解释本教程中相对较广的步骤。

1.打开一个窗口

首先,我们将创建一个新的“编辑器”窗口。 为此,创建一个新的C#类,然后using UnityEditor;添加using UnityEditor; 到顶部,以便我们可以访问所有UnityEditor类。 然后让这个新类从EditorWindow继承。
接下来,我们创建一个菜单项以打开我们的窗口。 创建一个名为ShowWindow的新静态函数,并向其中添加Unity的MenuItem属性。 MenuItem属性允许您将项目添加到Unity的菜单中,以调用任何给定的静态函数。 在我们的例子中,我们将菜单项放在“ Window / Custom / Space Invaders”下,这样我们的MenuItem属性将如下所示:

  [MenuItem(“ Window / Custom / Space Invaders%#&0”, false ,0)] 

我还通过在菜单项名称后添加符号%#&1来添加打开快捷方式(ctrl + alt + shift + 1或cmd + alt + shift + 1)的快捷方式。 要了解有关此属性的更多信息,请阅读此处的文档。

最后,我们使用GetWindow()创建一个新窗口,或者将焦点放在已在编辑器中某个位置打开的窗口上。 我们必须指定要查找的窗口的类型,以及是否要弹出浮动窗口或停靠在编辑器中的窗格。 因此,此时我们的文件应如下所示:

  使用 UnityEditor; 
使用 UnityEngine;

公共类 SpaceInvadersWindow:EditorWindow
{
[MenuItem(“ Window / Custom / Space Invaders%#&0”,false,0)]
私有静态无效 ShowInvaders()
{
GetWindow (true);
}
}

现在,您可以使用快捷键(ctrl + alt + shift + 1或cmd + alt + shift + 1)或导航至“窗口”>“自定义”>“空间侵略者”下的菜单项,以打开空白编辑器窗口。

2.在窗口中绘图

为了在编辑器窗口中绘制,我们必须在类中添加一个OnGUI()函数。 据我了解,只要编辑器收到事件,就会调用此函数。 事件可以是按钮按下,鼠标单击,鼠标移动,窗口大小更改,布局更改等。这意味着编辑器窗口不会像我们的游戏一样以30或60 fps的速度更新,但是稍后我们将对其进行处理。 现在最重要的事情是我们该如何塑造我们的角色。 经过研究后,我发现了函数GUI.DrawTexture() ,它具有RectTexture ,所以让我们使用它。 因此,我们添加了Rect和Texture作为成员变量。 接下来,我们将添加一些字段以填充变量。 EditorGUILayout具有用于显示大多数类型的属性的内置函数,但不幸的是没有Texture2D,因此我们使用EditorGUILayout.ObjectField()并传入typeof (Texture2D)作为对象类型。 对于我们的矩形,我们可以简单地使用EditorGUILayout.RectField() 。 现在让我们继续添加 GUI.DrawTexture()到函数末尾。 此时,我们的课程应如下所示:

  使用 UnityEditor; 
使用 UnityEngine;
  公共类 SpaceInvadersWindow:EditorWindow 
{
私人贴图m_PlayerTexture;
私有 Rect m_PlayerPositionRect;
  [MenuItem(“ Window / Custom / Space Invaders%#&0”,false,0)] 
私有静态无效 ShowInvaders()
{
GetWindow (true);
}

私有void OnGUI()
{
m_PlayerTexture = EditorGUILayout.ObjectField(“ Player Texture”,m_PlayerTexture,typeof(Texture2D),false)作为Texture2D;

m_PlayerPositionRect = EditorGUILayout.RectField(“ Player Position”,m_PlayerPositionRect);

如果 (m_PlayerTexture!= null)
{
GUI.DrawTexture(m_PlayerPositionRect,m_PlayerTexture);
}
}
}

如果我们打开窗口并填充属性,则可以看到它正在工作:

3.处理输入

玩游戏的下一个最重要的步骤是处理输入。 要在编辑器中获取输入,我们需要查看当前事件并决定如何使用它。 为此,我们从Event.current获取当前事件,并对其进行检查以查看它是什么类型的事件。 我们可以使用isKey来查看事件是否为按键类型,然后可以检查keyCode来查看按下了哪个键。 在我们的例子中,我们将使用AD左右移动。 最后,在事件上调用Use()很重要,以防止其他编辑器使用我们已经处理过的事件。 现在,我们可以将其包装为UpdateGame()函数,并在OnGUI()的末尾调用它。 因此,现在我们的代码应如下所示:

  私有void OnGUI() 
{
...
UpdateGame();
}
  私人void UpdateGame() 
{
var evt = Event.current;
如果 (evt.isKey)
{
如果 (evt.keyCode == KeyCode.D)
{
m_PlayerPositionRect.x + = 1;
evt.Use();
}
否则 (evt.keyCode == KeyCode.A)
{
m_PlayerPositionRect.x-= 1;
evt.Use();
}
}
}

很高兴知道EditorWindow类也可以具有Update()方法,就像MonoBehaviours一样。 但是,我还没有找到在Update()EditorWindow版本内获取任何键盘输入的方法,因此我没有使用它。

4.更新每一帧

此时,仅当编辑器调用OnGUI()时才绘制窗口,而这仅是Unity编辑器接收到事件的时间,而不是游戏以每秒30或60帧的速度更新的游戏。 为了解决此问题,我们将在OnGUI()末尾的编辑器窗口中调用Repaint() ,这会创建一个Repaint事件,从而迫使编辑器在下一帧窗口中再次调用OnGUI() 。 现在,如果您打开窗口并填充属性,则可以使用AD左右移动播放器。

  私有void OnGUI() 
{
...

Repaint();
}

5.波兰语

其余代码只是重复我们已经介绍的相同模式和想法,因此我将跳过它。 如有必要,可在此处全文获取代码。 但是,我将介绍一些有趣的API,我们可以使用它们来完善窗口。 第一个是EditorGUI.BeginChangeCheck()EditorGUI.EndChangeCheck() ,这是检查用户是否在GUI中更改了值的简便方法。 当用户填充m_PlayerPositionRect时,我用它来自动填充m_PlayerTexture 。 为此,我们在EditorGUI.BeginChangeCheck()之前调用EditorGUI.BeginChangeCheck() ,然后检查EditorGUI.EndChangeCheck()返回true。 如果是这样, m_PlayerPositionRect使用默认值填充m_PlayerPositionRect 。 看起来像这样:

  私有void OnGUI() 
{
EditorGUI.BeginChangeCheck();
m_PlayerTexture = EditorGUILayout.ObjectField(“ Player Texture”,m_PlayerTexture, typeof (Texture2D), false作为 Texture2D;
如果 (EditorGUI.EndChangeCheck()&& m_PlayerTexture!= null
{
m_PlayerPositionRect.width = m_PlayerTexture.width;
m_PlayerPositionRect.height = m_PlayerTexture.height;

m_PlayerPositionRect.x = position.width / 2;
m_PlayerPositionRect.y = position.height-m_PlayerPositionRect.height;
}

m_PlayerPositionRect = EditorGUILayout.RectField(“ Player Position”,m_PlayerPositionRect);

如果 (m_PlayerTexture!= null
{
GUI.DrawTexture(m_PlayerPositionRect,m_PlayerTexture);
}

UpdateGame();
Repaint();
}

我们要做的最后一件事是用默认值填充纹理。 我使用AssetDatabase.LoadAssetAtPath()并将其传递到Ship,Bullet和Enemy纹理的路径中。 在检查我们的任何纹理是否为空之后,我将其简单地放在OnGUI()的开头,就像这样:

  私有void OnGUI() 
{
如果 (m_PlayerTexture == null
{
m_PlayerTexture = AssetDatabase.LoadAssetAtPath (“资产/编辑器/SpaceInvadersWindow/Ship.png”);
m_PlayerPositionRect.width = m_PlayerTexture.width;
m_PlayerPositionRect.height = m_PlayerTexture.height;
}

EditorGUI.BeginChangeCheck();
m_PlayerTexture =
EditorGUILayout.ObjectField(“ Player Texture”,m_PlayerTexture, typeof (Texture2D), false作为 Texture2D;
如果 (EditorGUI.EndChangeCheck()&& m_PlayerTexture!= null
{
m_PlayerPositionRect.width = m_PlayerTexture.width;
m_PlayerPositionRect.height = m_PlayerTexture.height;

m_PlayerPositionRect.x = position.width / 2;
m_PlayerPositionRect.y = position.height-m_PlayerPositionRect.height;
}

m_PlayerPositionRect = EditorGUILayout.RectField(“ Player Position”,m_PlayerPositionRect);

如果 (m_PlayerTexture!= null
{
GUI.DrawTexture(m_PlayerPositionRect,m_PlayerTexture);
}

UpdateGame();
Repaint();
}

至此,您对我的代码库中发生的所有事情有了一个很好的概览,因此您可以自己完成游戏的编写,也可以在此处签出代码以了解我的工作方式。 如果有任何需要进一步说明的地方,请告诉我,当然感谢您的阅读。