1

I'm working on a procedural asset generation system and I want it to be able to detect if the source file of a particular asset has changed so that it only has to regenerate assets that will actually be different.

Some googling has told me that there is no way to get the source file of a type using simple reflection, so I'm trying to come up with a workaround. Here's what I'm doing:

  1. Get a list of all .cs files in the project directory using Directory.GetFiles.
  2. Compile each file into its own assembly using CSharpCodeProvider.CompileAssemblyFromFile.
  3. If compiledAssembly.GetType(targetType.FullName) exists, then that is the source file of targetType.

The problem is that step 2 is giving compile errors without any sort of description:

  • (0,0) : error :
  • (0,0) : error : at (wrapper managed-to-native) System.Reflection.Assembly:GetTypes (bool)
  • (0,0) : error : at (wrapper managed-to-native) System.Reflection.Assembly:GetTypes (bool)
  • etc.

I think that this may be because the current assembly or app domain or whatever already contains the exact type it's trying to compile, but that's just a guess. Does anyone know what might be causing those errors or how I can avoid them?

Edit: here's the main code for step 2:

private static readonly CSharpCodeProvider CodeProvider = new CSharpCodeProvider();
private static readonly CompilerParameters CompilerOptions = new CompilerParameters();

static SourceInterpreter()
{
    // We want a DLL in memory.
    CompilerOptions.GenerateExecutable = false;
    CompilerOptions.GenerateInMemory = true;

    // Add references for UnityEngine and UnityEditor DLLs.
    CompilerOptions.ReferencedAssemblies.Add(typeof(Application).Assembly.Location);
    CompilerOptions.ReferencedAssemblies.Add(typeof(EditorApplication).Assembly.Location);
}

private static Assembly CompileCS(string filePath, out CompilerErrorCollection errors)
{
    // Compile the assembly from the source script text.
    CompilerResults result = CodeProvider.CompileAssemblyFromFile(CompilerOptions, filePath);

    // Store any errors and warnings.
    errors = result.Errors;

    foreach (CompilerError e in errors)
    {
        if (!e.IsWarning) Debug.Log(e);
    }

    return result.CompiledAssembly;
}
svick
  • 236,525
  • 50
  • 385
  • 514
SilentSin
  • 1,026
  • 9
  • 18
  • Can you provide an example file? – Sergei Zhukov Sep 11 '15 at 09:18
  • please provide some related code, by only description it is very hard to understand –  Sep 11 '15 at 09:31
  • Each procedural asset is simply declared as a type that inherits from Procedural.Asset. Such a type could be alone in its own file, for example my current project has a Mage class where it makes a creature, sets its stats, gives it a model, etc. Or it could be nested inside another type, for example, my CameraMan class manages camera movement, and has a nested Asset class that sets up the camera size, near and far planes, second camera for HUD, etc. – SilentSin Sep 11 '15 at 09:36
  • My goal is to get it as robust as possible so I can release it on the Unity Asset Store. That's why I don't want to just make my own simple string interpreter. – SilentSin Sep 11 '15 at 09:36

1 Answers1

1

Scripts in unity are treated as assets as well. Unity uses the MonoScript class to represent the script file itself. An instance of the MonoScript class has the GetClass method to get the System.Type object for the class that is declared inside that script file.

The MonoScript class is derived from TextAsset and as such a UnityEngine.Object as well. Loaded instances usually can be found with Resources.FindObjectsOfTypeAll. Additionally there are the two static methods FromMonoBehaviour and FromScriptableObject which find and return the correct MonoScript instance for a given MonoBehaviour or ScriptableObject instance.

Once you have a MonoScript instance you can use GetAssetPath to determine where the script file is stored.

I wouldn't suggest to manually compile each class since, as you already mentioned, you can't load the same class twice from different assemblies if they are in the same namespace.

Unfortunately there is no FindFromType method in the MonoScript class so you have to have an instance of your class or check each MonoScript in the project manually.

Bunny83
  • 620
  • 8
  • 23
  • I had initially tried this, but it only works for MonoBehaviours, which my Asset classes are not. Also, GetClass wouldn't work if the Asset class is nested inside another class. – SilentSin Sep 11 '15 at 10:59
  • Well, in that case there is no solution because a "normal" C# class has no relation to a script file. You can declare multiple classes within one file and it's also possible to declare a single (partial) class in multiple files. So there's no one-to-one relation between classes and script files. Also keep in mind that classes could also be declared in pre compiled assemblies. To me that sounds like a strange approach to extend the editors functionality. If you want a robust system, you should use more classes and function that are provided by unity itself. – Bunny83 Sep 11 '15 at 11:04
  • There doesn't need to be a one to one relation. Once I get one to one working, it will inherently support multiple to one, and I can then extend it to support one class to one or more files. Pre-compiled assemblies are just another special case that I can add once I get the basics working. When the assembly file changes, any procedural assets coming from it will get generated again. – SilentSin Sep 11 '15 at 11:17
  • I still don't understand why you need to know which one changed. Unity compiles all script files into one assembly. If one file is changed and is reimported / updated, Unity recompiles all scripts at once. – Bunny83 Sep 11 '15 at 18:54
  • Procedurally generating an asset takes time. If I know an asset's source file hasn't changed, I don't need to generate that asset again. – SilentSin Sep 12 '15 at 01:51