上周,我花了一些时间在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()
,它具有Rect
和Texture
,所以让我们使用它。 因此,我们添加了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
来查看按下了哪个键。 在我们的例子中,我们将使用A和D左右移动。 最后,在事件上调用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()
。 现在,如果您打开窗口并填充属性,则可以使用A和D左右移动播放器。
私有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();
}
至此,您对我的代码库中发生的所有事情有了一个很好的概览,因此您可以自己完成游戏的编写,也可以在此处签出代码以了解我的工作方式。 如果有任何需要进一步说明的地方,请告诉我,当然感谢您的阅读。