Preserve folders sturcture of Unity local package in Visual Studio


 In previous tutorial we created local package for sharing common code between multiple Unity projects. Unfortunately, when this package is displayed in Visual Studio, it is missing original structure of folders. All files are put onto a big pile. In this tutorial we will fix it.

 Just to remind, this is how files of local package look now in Visual Studio – original structure of folders is lost:

 I looked into generated .csproj file and found several Compile tags with source files, that look like this:

    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\AbstractGeneratorRequest.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\AbstractMapGenerator.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\BspNode.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\BspNodeFactory.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\Interfaces\IBspNodeFactory.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\Interfaces\IGeneratorRequest.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\Interfaces\IGeneratorResult.cs" />
    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\ScriptableObjects\AbstractGeneratorParameters.cs" />

 After comparing it with multiple other .csproj files across my disk, I found it is missing child Link tag in each Compile tag. I tried manually change each line to something like this:

    <Compile Include="D:\MyUnityApps\_Common_\BSPMapGenerator\Runtime\AbstractGeneratorRequest.cs">
        <Link>Runtime\AbstractGeneratorRequest.cs</Link>  
    </Compile>

 It worked! Unfortunately, .csproj files are generated every time you open Visual Studio from Unity. So, next time all changes were overwritten. As I was lost, I sent question to Unity forum. There I got hint saying that you can enter process of generating .csproj file and you can make changes you need. In fact, there is also helpful example in documentation for Visual Studio Tools for Unity.

 I ended with following script which takes generated .csproj, looks for every Compile tag and adds child Link tag to it. It must be placed into Editor folder. You can name it as you want.
 As all generated .csproj files are sent into my new script one by one, I first had to select which are for my local packages. For my local packages I use name SBC.<package-name>, so I check whether full .csproj path contains “SBC.” – this is done in ProcessFile() method.
 Second issue was what value to use for Link tag. As I am structuring my local packages according to Unity packages layout conventions (see https://docs.unity3d.com/Manual/cus-layout.html), I had to cut out part of original file path beginning either with word Runtime or Editor.

#if ENABLE_VSTU
using System.IO;
using System.Text;
using System.Xml.Linq;
using UnityEditor;
using SyntaxTree.VisualStudio.Unity.Bridge;
using System.Text.RegularExpressions;
using System.Collections.Generic;
[InitializeOnLoad]
public class ProjectFileHook {
    // necessary for XLinq to save the xml project file in utf8
    class Utf8StringWriter : StringWriter {
        // -----------------------------------------------------------
        public override Encoding Encoding {
            get { return Encoding.UTF8; }
        }
    }
    // -----------------------------------------------------------
    static ProjectFileHook() {
        ProjectFilesGenerator.ProjectFileGeneration += (string name, string content) => {
            // process only file beginning SBC - my custom packages
            if (!ProcessFile(name)) {
                return content;
            }
            // parse the document and make some changes
            XDocument document = XDocument.Parse(content);
            AdjustDocument(document);
            // save the changes using the Utf8StringWriter
            Utf8StringWriter str = new Utf8StringWriter();
            document.Save(str);
            return str.ToString();
        };
    }
    // -----------------------------------------------------------
    static bool ProcessFile(string name) {
        Regex regex = new Regex(@"[\/\\]SBC\..*\.csproj$");
        Match match = regex.Match(name);
        return match.Success;
    }
    // -----------------------------------------------------------
    static void AdjustDocument(XDocument document) {
        // get namespace of document
        XNamespace ns = document.Root.Name.Namespace;
        // get all Compile elements
        IEnumerable<XElement> compileElements = document.Root.Descendants(ns + "Compile");
        // regex to find which part of Include attribute of Compile element to use for Link element value
        // check for Editor or Runtime (recommended folders: https://docs.unity3d.com/Manual/cus-layout.html)
        Regex regex = new Regex(@"\\(Runtime|Editor)\\.*\.cs$");
        // add child Link element to each Compile element
        foreach (XElement el in compileElements) {
            string fileName = el.Attribute("Include").Value;
            Match match = regex.Match(fileName);
            if (match.Success) {
                // substr from 1 to exclude initial slash character
                XElement link = new XElement(ns + "Link") {
                    Value = match.Value.Substring(1)
                };
                el.Add(link);
            }
        }
    }
}
#endif

 After that folder structure is back – my local package in Visual Studio looks like this:


2 responses to “Preserve folders sturcture of Unity local package in Visual Studio”

  1. Thanks for sharing this.

    For future readers, there’s a few changes you have to make now:

    1. Class needs to be an AssetPostprocessor

    2. Implemenet the following method:
    static string OnGeneratedCSProject(string path, string content);

    Thankfully the rest of the code can stay the same 🙂

  2. Looks like sometime since this was published, Unity stopped supporting this trick. In 2020.3.4 it no longer works.

Leave a Reply

Your email address will not be published. Required fields are marked *