扩展Unity3D编辑器的脚本模板

  近期在学习Shader时感受Shader语言參数众多、语法诡异,假设每次都从头開始写Shader必定是一件痛苦的事情。假设可以在本地定义好一组标准的Shader模板,这样当咱们需要实现某些效果类似的Shader时,就可以在这个Shader模板的基础上进行改动。编程

因为Shader文件是一个文本文件,因此咱们可以很easy地建立这样一个模板,在这个模板中咱们可以进一步无缺相关的參数凝视,这样就不用每次写Shader的时候都需要查文档了,从这个角度出发,就进入了这篇文章的正题:扩展Unity3D编辑器的脚本模板。
markdown

按图索骥,模板在哪里?

  Unity3D默认的脚本模版位于/Editor/Data/Resources/ScriptTemplates/文件夹下,注意该文件夹相对Unity3D的安装文件夹而言,在这个文件夹中咱们可以找到Unity3D中脚本模板的某些蛛丝马迹,首先,脚本模板是一个简单的文本文件,这个文本文件里预先填充了内容,咱们在编辑器中建立模脚本或者Shader的时候其实是读取这些文件而后在写入项目中的指定路径的。编辑器

其次。这些模板文件里#SCRIPTNAME#或者#NAME#这种标记。当咱们在编辑器中建立文件的时候,这个标记会被替换成指定的文件名称。比方Unity3D中继承自MonoBehaviour的脚本。有一个很重要的特性是文件名称必须和类名保持一致。这当然是Unity3D引擎的一个设定,可是在这里亦可以找到一个可以称得上理由的理由。ide

咱们注意到这些模板的文件名称中都有一个独一无二的数字,比方C#脚本的模板中的数字是8一、Shader模板中的数字是83,这些数字是什么呢,博主这里将其称为来自星星的黑科技。post

来自星星的黑科技

  做为一个常常捣鼓Unity3D编辑器的人。假设说你不知道MenuItem、EditorWindow、ScriptableWizard这些黑科技。那么说明你不是一个喜欢折腾和探索的人。学习

从Unity3D的API文档中,咱们知道MenuItem的原型为:lua

MenuItem(string itemName,bool isValidateFunction,int priority)

我知道咱们一般使用MenuItem经常使用的是它的第一个參数,即定义一个菜单项的名称,咱们可以使用”/”这种分隔符来表示菜单的层级。MenuItem需要配合一个静态方法来使用,可以理解为当咱们点击当前定义的菜单后就会去运行静态方法中的代码。所以MenuItem常常可以帮助咱们作些编辑器扩展开发的工做。好了,第二个參数做为一个验证的标志。假设该标志为true,意味着咱们定义的静态方法是一个验证方法在运行静态方法前会首先对方法进行验证,这个咱们暂且不管,因为今天咱们这个来自星星的黑科技主要和第三个參数有关,第三个參数表示一个优先级,它表示菜单项在菜单条中的展现顺序,优先级大的菜单项会展现在优先级小的菜单项如下,由此咱们就明确了了模板文件名称中的类似8一、83这种数字的真实含义,注意到模板文件的排列顺序和编辑器中的菜单项顺序是同样的,咱们作一个尝试。编写如下的代码:spa

[MenuItem("Assets/Create/Lua Scripts", false, 85)]
static void CreateLuaScripts()
{

}

[MenuItem("Assets/Create/固定功能着色器", false, 86)]
static void CreateFixedFunctionShader()
{

}

[MenuItem("Assets/Create/表面着色器", false, 87)]
static void CreateSurfaceShader()
{

}

[MenuItem("Assets/Create/可编程着色器", false, 88)]
static void CreateVertexAndFragmentShader()
{

}

注意到咱们依照已知的优先级继续写了四个方法,现在咱们在编辑器中可以发现默认的菜单条发生了变化:code

黑科技让菜单条发生了变化

咱们可以看到咱们编写的这四个菜单都生效了,尽管它们临时什么都作不了。但顺着这个方向去探索,咱们是可以实现最初的梦想的。orm

现在咱们来思考怎样依据模板来建立文件,这个对咱们来讲简直太简单了,经过StreamReader来读取模板。而后再用StreamWriter来生成文件就可以了。

可是这样建立的文件的文件名称是固定的。在建立文件的时候咱们无法改动。而且即便改动了文件内定义的名字并不会改变啊。因此咱们需要一个更好的解决方式。Unity3D提供了一个UnityEditor.ProjectWindowCallback的命名空间,在这个空间中提供了一个称为EndNameEditAction的类,咱们仅仅需要继承这个类就可以完毕这个任务。这个类需要重写Action的方法,咱们知道建立一个文件的完整步骤是建立文件而后使其高亮显示,所以这部分代码实现例如如下:

/// <summary>
/// 定义一个建立资源的Action类并实现其Action方法
/// </summary>
class CreateAssetAction : EndNameEditAction
{

    public override void Action(int instanceId, string pathName, string resourceFile)
    {
        //建立资源
        Object obj = CreateAssetFormTemplate(pathName, resourceFile);
        //高亮显示该资源
        ProjectWindowUtil.ShowCreatedAsset(obj);
    }

    internal static Object CreateAssetFormTemplate(string pathName, string resourceFile)
    {

        //获取要建立资源的绝对路径
        string fullName = Path.GetFullPath(pathName);
        //读取本地模版文件
        StreamReader reader = new StreamReader(resourceFile);
        string content = reader.ReadToEnd();
        reader.Close();

        //获取资源的文件名称
        string fileName = Path.GetFileNameWithoutExtension(pathName);
        //替换默认的文件名称
        content = content.Replace("#NAME", fileName);

        //写入新文件
        StreamWriter writer = new StreamWriter(fullName, false, System.Text.Encoding.UTF8);
        writer.Write(content);
        writer.Close();

        //刷新本地资源
        AssetDatabase.ImportAsset(pathName);
        AssetDatabase.Refresh();

        return AssetDatabase.LoadAssetAtPath(pathName, typeof(Object));
    }
}

这部分代码相对来讲比較简单,就是读取本地模板文件而后生成新文件,在生成新文件的时候会将#NAME替换成实际的文件名称,这样咱们就完毕了文件资源的建立。

现在的问题是怎样在建立文件的时候获取实际的路径,这部分代码实现例如如下:

private static string GetSelectedPath()
{
    //默认路径为Assets
    string selectedPath = "Assets";

    //获取选中的资源
    Object[] selection = Selection.GetFiltered(typeof(Object), SelectionMode.Assets);

    //遍历选中的资源以返回路径
    foreach (Object obj in selection)
    {
        selectedPath = AssetDatabase.GetAssetPath(obj);
        if (!string.IsNullOrEmpty(selectedPath) && File.Exists(selectedPath))
        {
            selectedPath = Path.GetDirectoryName(selectedPath);
            break;
        }
    }

    return selectedPath;
}

现在攻克了建立资源的问题。咱们接下来仅仅要调用ProjectWindowUtil的StartNameEditingIfProjectWindowExists方法就能够。该方法需要传入一个继承自EndNameEditAction的类的实例、目标文件路径和模板文件的路径。

好比要建立一个Lua脚本可以这样实现:

[MenuItem("Assets/Create/Lua Scripts", false, 85)]
static void CreateLuaScripts()
{
    ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0,
        ScriptableObject.CreateInstance<CreateAssetAction>(),
        GetSelectedPath() + "/NewLuaScript.lua", null,
        "Assets/Editor/Template/85-Lua-NewLuaScript.lua.txt");
}

小结

  现在有了这个黑科技之后,咱们可以建立不少其它的模板来扩展编辑器的功能,比方对Shader而言,咱们可以建立些基础性的Shader模板,而后每次需要写Shader的时候直接从模板库中选择一个功能类似的Shader而后在此基础上进行改动,这样比从头開始写一个新的Shader应该会轻松很多,这段时间学习Shader,感受进程缓慢离图形学高手遥遥无期,行了,这篇博客就是这样了。

效果演示

相关文章
相关标签/搜索