webGL Template

It seems like the cloud build doesn’t use my selected template for building, and I can’t seem find the setting for it to select it anywhere.

Would be great if someone knows how to set this up could advice me. Else I would have to move tons of html and js code to jslib or eval it at the start scene which is not fun thing to do.

Thanks.

This is not something we plan to support at the moment. We always build with the default template so that we can host the html properly and so the styling is consistent with the rest of our site, and the window sizing acts correctly. If your project requires more complex interaction with the page, unfortunately your only real option is to self host builds.

Thinking about how this might work in the future - what kinds of things are you doing in your template?

nothing fancy, just some bootstrap modal and input=“file” forms since you can’t seem to trigger it from unity itself (Being block by all browsers popup blocker by default), large file upload/download script, drag and drop file js event, parse.com js sdk and its custom helper script currently.

In a nutshell we are experimenting with more of a commercial cms type of system rather then games (and to see how far we can go with it, and hoping to make a better user experience), which unfortunately needs more interaction with the pages.

We would also like to select a WebGL template and connect to the javascript code. Especially, we are working on integration of Parse.com and PeerJS.

Hope you can make the template available soon.

+1 for option to use custom template during cloudbuild. Ours has a custom loading script and expands the canvas to full screen.

Why aren’t custom templates supported yet?

After looking for a solution online but still getting nowhere, I decided to look into writing a workaround myself.

Just put this code anywhere in your project (Preferrably under an /Editor folder) and don’t forget to replace the part with your actual template name.

Although this solution works for now, I still hope Unity will remove this limitation and let developers make the full use of their cloud build system.

#if UNITY_EDITOR && UNITY_WEBGL
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Callbacks;
using UnityEditor;
using System.IO;
using System;
using System.Text.RegularExpressions;
using System.Linq;

public class PostProcessWebGL
{
    //The name of the WebGLTemplate. Location in project should be Assets/WebGLTemplates/<YOUR TEMPLATE NAME>
    const string __TemplateToUse = "<YOUR TEMPLATE NAME>";

    [PostProcessBuild]
    public static void ChangeWebGLTemplate(BuildTarget buildTarget, string pathToBuiltProject)
    {
        if (buildTarget != BuildTarget.WebGL) return;


        //create template path
        var templatePath = Paths.Combine(Application.dataPath, "WebGLTemplates", __TemplateToUse);

        //Clear the TemplateData folder, built by Unity.
        FileUtilExtended.CreateOrCleanDirectory(Paths.Combine(pathToBuiltProject, "TemplateData"));

        //Copy contents from WebGLTemplate. Ignore all .meta files
        FileUtilExtended.CopyDirectoryFiltered(templatePath, pathToBuiltProject, true, @".*/\.+|\.meta$", true);

        //Replace contents of index.html
        FixIndexHtml(pathToBuiltProject);
    }

    //Replaces %...% defines in index.html
    static void FixIndexHtml(string pathToBuiltProject)
    {
        //Fetch filenames to be referenced in index.html
        string
            webglBuildUrl,
            webglLoaderUrl;
     
        if (File.Exists(Paths.Combine(pathToBuiltProject, "Build", "UnityLoader.js")))
        {
            webglLoaderUrl = "Build/UnityLoader.js";
        }
        else
        {
            webglLoaderUrl = "Build/UnityLoader.min.js";
        }

        string buildName = pathToBuiltProject.Substring(pathToBuiltProject.LastIndexOf("/") + 1);
        webglBuildUrl = string.Format("Build/{0}.json", buildName);

        //webglLoaderUrl = EditorUserBuildSettings.development? "Build/UnityLoader.js": "Build/UnityLoader.min.js";
        Dictionary<string, string> replaceKeywordsMap = new Dictionary<string, string> {
                {
                    "%UNITY_WIDTH%",
                    PlayerSettings.defaultWebScreenWidth.ToString()
                },
                {
                    "%UNITY_HEIGHT%",
                    PlayerSettings.defaultWebScreenHeight.ToString()
                },
                {
                    "%UNITY_WEB_NAME%",
                    PlayerSettings.productName
                },
                {
                    "%UNITY_WEBGL_LOADER_URL%",
                    webglLoaderUrl
                },
                {
                    "%UNITY_WEBGL_BUILD_URL%",
                    webglBuildUrl
                }
            };

        string indexFilePath = Paths.Combine(pathToBuiltProject, "index.html");
       Func<string, KeyValuePair<string, string>, string> replaceFunction = (current, replace) => current.Replace(replace.Key, replace.Value);
        if (File.Exists(indexFilePath))
        {
           File.WriteAllText(indexFilePath, replaceKeywordsMap.Aggregate<KeyValuePair<string, string>, string>(File.ReadAllText(indexFilePath), replaceFunction));
        }

    }

    private class FileUtilExtended
    {
     
        internal static void CreateOrCleanDirectory(string dir)
        {
            if (Directory.Exists(dir))
            {
                Directory.Delete(dir, true);
            }
            Directory.CreateDirectory(dir);
        }

        //Fix forward slashes on other platforms than windows
        internal static string FixForwardSlashes(string unityPath)
        {
            return ((Application.platform != RuntimePlatform.WindowsEditor) ? unityPath : unityPath.Replace("/", @"\"));
        }



        //Copies the contents of one directory to another.
       public static void CopyDirectoryFiltered(string source, string target, bool overwrite, string regExExcludeFilter, bool recursive)
        {
            RegexMatcher excluder = new RegexMatcher()
            {
                exclude = null
            };
            try
            {
                if (regExExcludeFilter != null)
                {
                    excluder.exclude = new Regex(regExExcludeFilter);
                }
            }
            catch (ArgumentException)
            {
               UnityEngine.Debug.Log("CopyDirectoryRecursive: Pattern '" + regExExcludeFilter + "' is not a correct Regular Expression. Not excluding any files.");
                return;
            }
            CopyDirectoryFiltered(source, target, overwrite, excluder.CheckInclude, recursive);
        }
       internal static void CopyDirectoryFiltered(string sourceDir, string targetDir, bool overwrite, Func<string, bool> filtercallback, bool recursive)
        {
            // Create directory if needed
            if (!Directory.Exists(targetDir))
            {
                Directory.CreateDirectory(targetDir);
                overwrite = false;
            }

            // Iterate all files, files that match filter are copied.
            foreach (string filepath in Directory.GetFiles(sourceDir))
            {
                if (filtercallback(filepath))
                {
                    string fileName = Path.GetFileName(filepath);
                    string to = Path.Combine(targetDir, fileName);

                 
                    File.Copy(FixForwardSlashes(filepath),FixForwardSlashes(to), overwrite);
                }
            }

            // Go into sub directories
            if (recursive)
            {
                foreach (string subdirectorypath in Directory.GetDirectories(sourceDir))
                {
                    if (filtercallback(subdirectorypath))
                    {
                        string directoryName = Path.GetFileName(subdirectorypath);
                       CopyDirectoryFiltered(Path.Combine(sourceDir, directoryName), Path.Combine(targetDir, directoryName), overwrite, filtercallback, recursive);
                    }
                }
            }
        }

        internal struct RegexMatcher
        {
            public Regex exclude;
            public bool CheckInclude(string s)
            {
                return exclude == null || !exclude.IsMatch(s);
            }
        }

    }

    private class Paths
    {
        //Combine multiple paths using Path.Combine
        public static string Combine(params string[] components)
        {
            if (components.Length < 1)
            {
                throw new ArgumentException("At least one component must be provided!");
            }
            string str = components[0];
            for (int i = 1; i < components.Length; i++)
            {
                str = Path.Combine(str, components[i]);
            }
            return str;
        }
    }

}

#endif

Good luck!

What are you talking about? I don’t want you to host my WebGL build, I want you to build it. It’s called Cloud Build not Cloud Host. What a ridiculous excuse.

What the hell am I paying for?

Exactly.
I don’t see why anyone would want to host their game on Unity using the default template.
While cloud build is supposed to make a developer’s life easier (quote: “Cloud Build - Build games faster”) It’s actually giving us more work because we’d have to:

  • Press “Start Cloud Build”

  • Wait 35 minutes for Unity to build the project.

  • download and unzip the generated build

  • replace the templatedata folder and index.html with the correct one

  • Upload the contents to our ftp

  • Hope that we didn’t break anything and that we won’t have to go back to 1.

Anyway, right now we finally managed to fully automate our our build pipeline for WebGL and Android:
After cloud build, Unity will notify a webhook I have set up, running in Azure, which will download and extract the .zip file and then upload it to our own FTP.

Seriously? This makes either cloud build or custom templates useless.
We use Cloud Build for building our WebGL version and webhooks to download and deploy the game on our own servers once the build is complete. Please remove this limitation.

As forums are not the good place to complain about missing features I created an issue on Unity Feedback :

https://feedback.unity3d.com/suggestions/webgl-cloud-build-should-support-cusom-templates

Please vote for it :slight_smile:

There was another one already

https://feedback.unity3d.com/suggestions/allow-building-with-webgltemplates-instead-of-just-the-default

Damn, I didn’t see it even though I made a search. Thanks your reporting it, and let’s gather votes on the older one.

I didn’t see it either, at first, but I noticed I had ten new votes available (problem with unity feedback is I run out of votes quickly!) and started browsing through all the issues…

Thanks a lot for this! it seems to be the only workaround for now.

Thanks @PlayItSafe_Fries for your code!
I modified it a bit to make it more generic.
Now the Custom Template is automatically taken from your build settings.
Also, if it’s not using a custom template, we don’t do anything (as built-in templates don’t have issues).
Also, I made the regex to filter based on the local path only. I use Jenkins, and all this happens in a “.jenkins” folder, so every file was filtered out because of the parent hidden folder.
So here is the new version:

#if UNITY_EDITOR && UNITY_WEBGL
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Callbacks;
using UnityEditor;
using System.IO;
using System;
using System.Text.RegularExpressions;
using System.Linq;

public class PostProcessWebGL
{

    const string CUSTOM_TEMPLATE_PREFIX = "PROJECT:";

    static bool __UsesCustomTemplate { get { return PlayerSettings.WebGL.template.StartsWith(CUSTOM_TEMPLATE_PREFIX); } }

    static string __CustomTemplateName { get { return PlayerSettings.WebGL.template.Substring(CUSTOM_TEMPLATE_PREFIX.Length); } }

    static string __CustomTemplatePath { get { return Paths.Combine(Application.dataPath, "WebGLTemplates", __CustomTemplateName); } }

    [PostProcessBuild]
    public static void ChangeWebGLTemplate(BuildTarget buildTarget, string pathToBuiltProject)
    {
        if (buildTarget != BuildTarget.WebGL) return;

        if (!__UsesCustomTemplate) return;

        Debug.Log("Add Custom WebGL Template: " + __CustomTemplateName);

        //Clear the TemplateData folder, built by Unity.
        FileUtilExtended.CreateOrCleanDirectory(Paths.Combine(pathToBuiltProject, "TemplateData"));

        //Copy contents from WebGLTemplate. Ignore all .meta files
        FileUtilExtended.CopyDirectoryFiltered(__CustomTemplatePath, pathToBuiltProject, true, @".*/\.+|\.meta$", true);

        //Replace contents of index.html
        FixIndexHtml(pathToBuiltProject);
    }

    //Replaces %...% defines in index.html
    static void FixIndexHtml(string pathToBuiltProject)
    {
        //Fetch filenames to be referenced in index.html
        string
            webglBuildUrl,
            webglLoaderUrl;

        if (File.Exists(Paths.Combine(pathToBuiltProject, "Build", "UnityLoader.js")))
        {
            webglLoaderUrl = "Build/UnityLoader.js";
        }
        else
        {
            webglLoaderUrl = "Build/UnityLoader.min.js";
        }

        string buildName = pathToBuiltProject.Substring(pathToBuiltProject.LastIndexOf("/") + 1);
        webglBuildUrl = string.Format("Build/{0}.json", buildName);

        //webglLoaderUrl = EditorUserBuildSettings.development? "Build/UnityLoader.js": "Build/UnityLoader.min.js";
        Dictionary<string, string> replaceKeywordsMap = new Dictionary<string, string> {
                    {
                        "%UNITY_WIDTH%",
                        PlayerSettings.defaultWebScreenWidth.ToString()
                    },
                    {
                        "%UNITY_HEIGHT%",
                        PlayerSettings.defaultWebScreenHeight.ToString()
                    },
                    {
                        "%UNITY_WEB_NAME%",
                        PlayerSettings.productName
                    },
                    {
                        "%UNITY_WEBGL_LOADER_URL%",
                        webglLoaderUrl
                    },
                    {
                        "%UNITY_WEBGL_BUILD_URL%",
                        webglBuildUrl
                    }
                };

        string indexFilePath = Paths.Combine(pathToBuiltProject, "index.html");
        Func<string, KeyValuePair<string, string>, string> replaceFunction = (current, replace) => current.Replace(replace.Key, replace.Value);
        if (File.Exists(indexFilePath))
        {
            File.WriteAllText(indexFilePath, replaceKeywordsMap.Aggregate<KeyValuePair<string, string>, string>(File.ReadAllText(indexFilePath), replaceFunction));
        }

    }

    private class FileUtilExtended
    {

        internal static void CreateOrCleanDirectory(string dir)
        {
            if (Directory.Exists(dir))
            {
                Directory.Delete(dir, true);
            }
            Directory.CreateDirectory(dir);
        }

        //Fix forward slashes on other platforms than windows
        internal static string FixForwardSlashes(string unityPath)
        {
            return ((Application.platform != RuntimePlatform.WindowsEditor) ? unityPath : unityPath.Replace("/", @"\"));
        }



        //Copies the contents of one directory to another.
        public static void CopyDirectoryFiltered(string source, string target, bool overwrite, string regExExcludeFilter, bool recursive)
        {
            RegexMatcher excluder = new RegexMatcher()
            {
                exclude = null
            };
            try
            {
                if (regExExcludeFilter != null)
                {
                    excluder.exclude = new Regex(regExExcludeFilter);
                }
            }
            catch (ArgumentException)
            {
                UnityEngine.Debug.Log("CopyDirectoryRecursive: Pattern '" + regExExcludeFilter + "' is not a correct Regular Expression. Not excluding any files.");
                return;
            }
            CopyDirectoryFiltered(source, target, overwrite, excluder.CheckInclude, recursive);
        }
        internal static void CopyDirectoryFiltered(string sourceDir, string targetDir, bool overwrite, Func<string, bool> filtercallback, bool recursive)
        {
            // Create directory if needed
            if (!Directory.Exists(targetDir))
            {
                Directory.CreateDirectory(targetDir);
                overwrite = false;
            }

            // Iterate all files, files that match filter are copied.
            foreach (string filepath in Directory.GetFiles(sourceDir))
            {
                string localPath = filepath.Substring(sourceDir.Length);
                if (filtercallback(localPath))
                {
                    string fileName = Path.GetFileName(filepath);
                    string to = Path.Combine(targetDir, fileName);

                    File.Copy(FixForwardSlashes(filepath), FixForwardSlashes(to), overwrite);
                }
            }

            // Go into sub directories
            if (recursive)
            {
                foreach (string subdirectorypath in Directory.GetDirectories(sourceDir))
                {
                    string localPath = subdirectorypath.Substring(sourceDir.Length);
                    if (filtercallback(localPath))
                    {
                        string directoryName = Path.GetFileName(subdirectorypath);
                        CopyDirectoryFiltered(Path.Combine(sourceDir, directoryName), Path.Combine(targetDir, directoryName), overwrite, filtercallback, recursive);
                    }
                }
            }
        }

        internal struct RegexMatcher
        {
            public Regex exclude;
            public bool CheckInclude(string s)
            {
                return exclude == null || !exclude.IsMatch(s);
            }
        }

    }

    private class Paths
    {
        //Combine multiple paths using Path.Combine
        public static string Combine(params string[] components)
        {
            if (components.Length < 1)
            {
                throw new ArgumentException("At least one component must be provided!");
            }
            string str = components[0];
            for (int i = 1; i < components.Length; i++)
            {
                str = Path.Combine(str, components[i]);
            }
            return str;
        }
    }

}

#endif

In Unity Cloud Build, it seems that PostProcessBuildAttribute cannot be used.

It builds locally, but not on UCB.

Is anyone else having this experience?

hmm…it’s 2020 and the webGL cloud build still cannot use a custom template. I guess I’ll have to build my 20 webGL html on my own laptop?

You can theoretically do a Unity build in Azure DevOps. I’ve had a lot more luck managing my own build server than using a hosted agent. The Android tools won’t work without a manual install step. The Mac stuff, of course, doesn’t work.

Also, a $150 piece of hardware seems to completely run circles around Unity Cloud Build and is pretty fast when compared with Azure DevOps hosted agents.

WebGL and Windows standalone seem to be the only builds people are reliably getting to work on Azure DevOps… so you may be in luck, if you choose that route.

Since this thread was so helpful to me recently I wanted to contribute. I wrote a blog post on how I solved this issue with the above code since some of the function signatures seem to have changed and I wanted a solution that didn’t rely on the [PostProcessBuild] attribute but rather on my Unity Cloud configuation settings.

Big thanks to @PlayItSafe_Fries and @bourriquet in the thread. It’s still insane we can’t use Custom WebGL templates with Unity Cloud Build, when they’re already well supported by Unity.

TLDR for my blog post:

The function signature for a “Post-Export Method” is

public static void func(string)

This should come as no big surprise, but when I was trying to solve this I ran into trying to use the solutions by other users’ function signatures, which don’t work if you’re doing it via configuration options.

@bourriquet 's code doesn’t work anymore because when Unity Cloud builds the project it sets

PlayerSettings.WebGL.template = "Application:smile:efault"

instead of what it does locally and his solution relys on:

PlayerSettings.WebGL.template = "PROJECT:CustomTemplate"

Here’s the full info for how I got it all working with Unity 2019.2.x recently.

Hope this helps!
Colin