代码生成

发表于2016-03-02
评论4 2k浏览

在此文开头,我想感谢 Lior Tal 发表的原文。对他原文的阅读让我开始了代码生成之路。你可以在这里阅读他的Gamasutra博客。


为了制作这个教程,我做了一个公开的BitBucket repo,在下面可以找到链接地址。开头的起点是一个C#项目和一个Unity项目。csproj扩展了MSBuild以将所有脚本导出到unity工程。此项目在Visual Studio中建立,但也涉及到XBuild(Xamarin Studios)。 “completed”分支与第一个是一样的,但包含在本教程中使用的所有代码。


什么是代码生成?


代码生成是每个开发者都可能会使用到的强大工具。它最基本的表现是用代码输出文本。这些文本有许多的用途。输出一个cs文件,让unity将它编译并添加到工程文件中。轮询当前unity工程中的资源文件,将它们打印输出到一个xml文件中。没有什么限制。下面是一个非常简单的不用T4模板的代码生成器案例。此类获取一个类名,一个保存地址,和一个枚举名的列表。我们所有的工作是从类定义开始写起,然后一句句添加。




如你所见,这个简单的类已经相当庞大,随着输出变得更加多样化后,情况会更糟。将这种方式用于较大的生成器,那它将难以为继,难以维护、调试。该T4模板出场了。


文本转换工具包内置于Monodevelop和visual studio。让创建代码生成器更加的快捷和高效。T4省去了一行行写出一个类的过程。语法最初时会让人感觉到有点别扭,但当你一旦理解了它,它就会变得越来越容易使用。不幸的是,刚起步时你会发现难以跟进这个文档,而且当考虑到在unity开发环境中工作,这些将变的更难。Unity中有一个长期存在的bug,而官方拒绝修复,所以我们需要做一些工作来绕开它,这是通过扩展MSBuild、XBuild以将脚本导出到unity中来完成的。


有一个关于T4如何使事情变得简单的精彩案例,他就是我最近使用的工具:Jiffy Editor。Jiffy Editor使用T4模板来通过点击按钮来编写自定义编辑器、属性栏。设置一个自定义的编辑器和属性栏有时会花费一个小时的时间,有了T4,这可以瞬间完成。





利用反射,我可以读取system.type,查找此对象所有的可序列化字段。将这些数据发送到Jiffy Editor里的生成器,生成器将其转换成一个类的定义并输出到磁盘以供编辑。你所要做的只是在全部输出一个自定义编辑器前实现自己的功能。可能要花费20分钟来设置的编辑器或者属性栏,现在点击一个按钮即可完成。


这仅仅只是一个使用代码生成器的案例。在此文的下一部分,我们将在以上内容的铺垫上编写一个完整的生成器。


编写自己的生成器


对于此案例,我们将解决很多开发者都会遇到的简单问题。在unity中,我们需要访问layer,tags,和sorting  layers.不幸的是,它们有一个只能在编辑器中设置的缺点。




为了在代码中处理tags,必须使用字符串比较在检视面板中定义好的ID。为了简化以上操作,许多开发者会创建一个包含许多与面板上的数值相关的常量的静态类。在这些值没有变化的情况下,上述方法方便且表现良好。一旦有人交换了Ammo和Mana的tags,你的代码仍然可以工作,游戏通过编译,但所有的事都会乱的一塌糊涂。当你从Ammo箱子旁经过却得到了Mana,与Mana罐子发生碰撞时却得到Ammo。这一个很简单的错误现在却会引来无数的烦恼,最糟的是浪费了本可以将游戏做的更好的时间。


为解决这些常见的问题,我们将创建我们第一个代码生成器来为我们管理这些静态值。如果你想跟随我们教程的进度亲自编写这些脚本,先确认下你获取了此文开头提到的一些初始文件。


创建我们自己的生成器


启动visual studio并打开工程文件。我们在proj.cs/CodeGenerationWithUnity.csproj目录下看到cs工程项目。


首先创建一个新的模板。此例中,我将使用visual studio,你也可以使用MonoDevelop。创建一个新的runtime text template,取名为LayersGenerator.tt.




有两种类型的模板:Design Time Text Template(vs中是 Text Template )和Runtime texttemplate(其它的模板来自Tangible T4)。. Design Time Text Template已定义了所有需要的内容,当你点击保存时就会输出最终的结果。输出内容和项目本身一同生成。一个运行文本模板会创建一个类,这是非常类似于我们在本文开头所做的枚举。它输出一个半加工的,包含一个transform Template函数的类,而不是输出一个完整的类。transform Template函数返回类定义的字符串。当你不断变化数据以输出不同的类定义时,可以复用这种类型的模板。


我们选择Runtime类型,这样以来我们可以在unity编辑器中复用我们的模板。


默认情况下,vs并不支持T4语法高亮(Mono也一样)。我使用免费版TangibleT4 Editor 来解决这个问题并使模板更具可读性。TangibleT4 Editor包含基本的智能感知和语法高亮。



 

在你创建一个新类后,你将看到图上所示的代码。你首先注意到的是所有的预先生成的代码都包裹在<#和#>中。这些标记被T4用于定义逻辑部分和输出到文件的部分。让我们回顾一下以上显示的预生成的代码段后我们再继续。


此句告诉模板我们在生成器的逻辑部分使用哪种语言。在本里中我们使用C#(也可以使用VB)。我们所选语言可使用的任何库都可以在模板中访问。


<#@ template language="C#"#>


此句告诉模板我们在生成器的逻辑部分使用哪种语言。在本里中我们使用C#(也可以使用VB)。我们所选语言可使用的任何库都可以在模板中访问。


<#@ assemblyname="System.Core" #>


我们将在模板逻辑中使用System.Core程序集。


<#@ import namespace=””#>


告诉生成器从包含的程序集中导入哪个命名空间。


所有你在<# #>外输入的内容将原样输出。这样以来我们可以在生成器逻辑中混入和匹配直接输出来得到我们想要的结果。这就是编写模板的强大之处。


 


现在我们回顾完默认的T4内容,我们接下来看看它生成的内容。在解决方案管理器中找到你的模板,点击它左边的箭头以展开具体内容。你将看到一个和你模板同名的cs文件。此文件就是生成器的生成内容。双击打开此文件,看看我们起始的内容。虽然它现在并没有太多的意义,别担心,我们稍后会解释


准备好后就跳转回来,输入以下内容。



 

你可以看到我稍微清理了一些代码,移除一些,加入了一些。我们不需要额外的命名空间,所以它们被我们去掉了。然后我们利用T4中的参数功能来定义类的名字和一个存储我们标识符的数组。


linePragmas=”false”


默认情况下,T4引擎会在源文件中以硬编码路径的方式输出行指令。这将为源代码控制带来麻烦。它可以被用来调试,但我不觉得它们会有很多的帮助。


<#@ paramter #>


此标识符用于定义模板中会使用到的变量。在运行我们的生成器前,我们要填写这些参数值。在此例中,我们赋予类名一个字符串,为unity中包含的所有层赋予一个字符数组。注意:您必须使用全名,包括定义类型时的命名空间

我们从定义类的内容开始。由于“public static class”文本和括号并不在尖括号之间,他们将被原样输出。然而,类名与等号一起在尖括号之间。此语法告诉生成器我们希望将提供的变量值做为类名输出。用户定义的m_ClassName变量将被写入这里。


既然我们已经做出了一些改动,我们应该测试模板,以确保它仍然正常工作。再次找到你的模板,并打开小箭头,双击cs文件将其打开。




你将看到的是预先处理过的模板。之所以会输出这些是因为我们开始时选择了Runtime Text Template。如果你看下TransformText函数下面的内容(在上面截图的底部),将会看到我们到.tt文件中预先定义的参数。此生成器已经创建了m_UnityLayers 和m_ClassName,但只是赋予它get访问器,我们必须添加set访问器才能更改数据。


此时你可能想知道我们定义的类发生了什么。如你所见,它看起来可能并不是你希望的样子。正如我所说,这就是预处理模板。模板的工作全部是通过Transform Text函数来完成的。这是我们创建的类定义。从上面我们看到我们现在的类仅仅创建了4行代码。此函数的大小会随着类越来越复杂而变得越来越庞大。。模板其实是幕后一个字符串生成器。T4会给你一些自定义的语法和一堆辅助功能,这将节省你大量的时间。


实际测试runtime template会是一种痛苦。直到它被使用,你才知道它是什么样子。痛苦之一是为了保持好的格式而需要无穷的斗争。由于这样的设计是在Unity中处理,我们必须将模板导出到unity中以便测试它。这将创建一个我个人想避免的不必要的测试步骤。我们现在要做的就是建立一个Design Time Template测试我们ourruntime template。两者之间唯一的区别是Design Time Template在编辑器中调用TransferText函数并将它作为一个源文件添加。是你获得满意的结果的时候了。


在你的工程中再次添加一个新模板,取名为 LayersGeneratorDemo ,确认一下选择的是Text Template而不是Runtime Text Template。如果你按照前面的方式展开这个.tt文件,你会发现输出的一个什么也没有.txt文件。显然,我们需要改变一些内容。




和上面的类的处理方式一样,我们清理一下它,移除不需要的包含内容。在runtime template 中,我们会看到一个以前没有见过的新命令。


<# output extension=”.cs”#>


因为design time template每次在你保存的时候都会被处理,所以你必须填写你想保存文件的扩展名。此例中,我们生成cs文件,所以我们在这里定义它。


我们使用这个模板的目的是让它根据我们给定的变量值来输出我们其它模板的内容。我们首先要做的是将 LayersGenerator.tt导入到LayersGeneratorDemo.tt。确保你完成时保存了模板,这会引起Design Time template的执行。




Visualstudio会执行TransformText函数并爆出错误。我们必须解决掉第一个错误才能正常运行。


如果你打开输出文件,你会发现模板在获取我们先前定义的类名时停止了写入。T4不能很好的处理nulls,所以我们必须定义这些值。


有几种方式可以为我们的变量赋值。我将给大家展示最容易的一种,以便使我们的模板能更早开始工作。




我们在T4中定义的每个参数都一个对应的生成的字段。每个字段在我们定义的名字的开头加上下划线,结尾处添加‘Field’。知道了这些,我们只要输入字段,将它们的值赋予这些名字的代码将会自动生成。点击保存,你将看到新创建的类…..,真是太棒了!


正如你可能已经注意到的,虽然我们为UnityLayers设置了值,实际上我们并没有做任何事情。这是我们要采取下一步行动。打开LayersGenerator.tt和复制下面的代码。



 

在这里我们获取了在LayersGeneratorDemo.tt文件中的m_UnityLayers,并处理后输出。第一个代码段为每一个层创建了一个Int常量值。第二个代码段创建了一个新的名为values的枚举,并为我们定义的每个层都添加了一个条目。如果工作正常的话,你将看到如同下面的输出。




祝贺你创建了自己第一个代码生成器!有了这个输出内容,我们知道这就是我们想要的格式。我们的下一个目标就是将其挂钩以使unity填写我们的信息。


我们必须扩展已经生成的类以便我们将它挂载到unity中。在工程中创建一个新的cs类,命名为LayersGeneratorInterface.cs。复制以下代码。




因为LayersGeneratorInterface.cs文件是自动生成的,所以我们不希望向其中添加代码或是每当我们保存模板时它都会被清除。这就是partial关键词的作用所在。当项目被编译时,这两个类将会被合并成一个。我们将在这里定义我们unity的逻辑部分。




以上是类我们正在编写的类的第一部分。如果你在过去创建过unity 这部分的扩展,那这些对你来说就比较熟悉。


我们首先要求用户选择一个保存地址并将其保存为输出路径。然后我们创建LayersGenerator类的一个新实例,我们用它老定义类。接下来我们根据用户选择的输出路径来保存类名。最后,我们告诉生成器我们要设置它的类名称字段。


当生成器运行的时候,它将以两种方式中的一种来查找字段值。CallContext或者使用一个叫做Session的字典。下面是它试图获取m_ClassName值时运行的代码段。



现在我们已经给类名赋了值,然后需要层的列表。幸运的是,unity已有一种完成此工作的方法。用以下代码来更新你的GenerateLayers,评论内容为你将此过程分解为一个个步骤。



 

此模板还不能在unity中使用。我们漏掉了一件事情:我们需要调用它。为了做到这一点,我们将在GenerateLayers函数上方添加MenuItem属性。



  

到此可以准备开始了。编译项目,你所有的脚本都会被MSBuild导出。如果你看一下输出日志,你将看到如下图所示的内容。只有你用我的两个项目中的一个时,这才会发生。




如果出现了一些问题,那你就你创建了自己的项目然后复制源文件并将其粘贴到你的unity项目。您应该看到工具菜单弹出在unity界面的上部。


 

点击菜单项并悬着类的保存地址。你现在下来,已经完成了第一次(也许?)代码生成器!



作为一个尾注,本教程是为开发人员展示了如何在unity中使用代码生成。这不是说你应该在所有情况下都使用它。


如果你是好奇脚本是如何导出到Unity中;在项目中检查Properties /UnityScriptExporter.xml脚本。这确实有些繁重。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引