Hey,
While we worked on a WebGL port for our game, we needed an option to stream levels like in the WebPlayer.
@jonas-echterhoff_1 wrote a basic script that did that on the beta version of Unity5.
We updated it, and added the option to check download progress.
Instead of Application.CanStreamedLevelBeLoaded() and Application.GetStreamProgressForLevel()
We now have
WebGLLevelStreaming.CanStreamedLevelBeLoaded()
WebGLLevelStreaming.GetStreamProgressForLevel()
Use “firstStreamedLevelWithResources” like in the “First Streamed Level” option of the WebPlayer
http://docs.unity3d.com/Manual/class-PlayerSettingsWeb.html
So, the code for WebGLLevelStreaming.cs
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Callbacks;
#endif
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
public class WebGLLevelStreaming {
#if UNITY_EDITOR && UNITY_WEBGL
private const string kOutputDataExtension = ".data";
private const string kOutputFileLoaderFileName = "fileloader.js";
private const string kResourcesDirName = "Resources";
private const string kResourcesFileName = "unity_default_resources";
private const string kResourcesExtraFileName = "unity_builtin_extra";
public static string buildToolsDir
{
get {
// Find WebGL build tools
var webGLBuildTools = Path.Combine("WebGLSupport", "BuildTools");
var playbackEngines = Path.Combine(EditorApplication.applicationContentsPath, "PlaybackEngines");
var path = Path.Combine(playbackEngines, webGLBuildTools);
if (Directory.Exists(path))
return path;
else
return Path.Combine(Path.Combine(EditorApplication.applicationPath, "../../"), webGLBuildTools);
}
}
public static string emscriptenDir
{
get { return buildToolsDir + "/Emscripten"; }
}
static string packager
{
get { return emscriptenDir + "/" + "tools/file_packager.py"; }
}
public static string pythonExecutable
{
get
{
if (Application.platform == RuntimePlatform.WindowsEditor)
return "\"" + buildToolsDir + "/" + "Emscripten_Win/python/2.7.5.3_64bit/python.exe\"";
return "python";
}
}
public static int firstStreamedLevelWithResources = 1;
// Run emscripten file packager to pack a .data file
private static bool RunPackager (string filename, string stagingAreaData, IEnumerable<string> filesToShip)
{
var argumentsForPacker = "\"" + packager + "\"" ;
argumentsForPacker += string.Format(" \"{0}\"", filename + kOutputDataExtension);
argumentsForPacker += " --no-heap-copy";
argumentsForPacker += " --js-output=\"" + filename + ".loader.js" + "\"";
if (PlayerSettings.GetPropertyBool("dataCaching", BuildTargetGroup.WebGL))
argumentsForPacker += " --use-preload-cache";
argumentsForPacker += " --preload";
argumentsForPacker += filesToShip.Aggregate("", (current, file) => current + (" \"" + file + "\""));
var processStartInfo = new ProcessStartInfo(pythonExecutable)
{
Arguments = argumentsForPacker,
WorkingDirectory = stagingAreaData,
UseShellExecute = false,
};
var p = Process.Start(processStartInfo);
p.WaitForExit();
if (p.ExitCode == 0)
return true;
throw new System.Exception("Failed running " + processStartInfo.FileName + " " + processStartInfo.Arguments);
}
private static bool PackageData (string filename, string stagingAreaData, IEnumerable<string> _filesToShip)
{
var filesToShip = new HashSet<string>(_filesToShip.Select(o => Path.GetFileName(o)));
filesToShip.Add(Path.Combine(kResourcesDirName, kResourcesFileName));
filesToShip.Add(Path.Combine(kResourcesDirName, kResourcesExtraFileName));
if (firstStreamedLevelWithResources < 0)
firstStreamedLevelWithResources = 0;
if (firstStreamedLevelWithResources >= Application.levelCount)
firstStreamedLevelWithResources = Application.levelCount-1;
var loaderJSStart = "var StreamProgressForLevelArray = [];\nStreamProgressForLevelArray[0]=1;\n";
var loaderJS = "";
var loaderJS0 = "";
int packageIndex = 0;
// Generate a data file for each streaming level
while (true)
{
// find all the files for this level
var fileNames = new List<string>();
int levelIndex = packageIndex;
fileNames.Add("sharedassets" + levelIndex + ".resource");
fileNames.Add("sharedassets" + levelIndex + ".assets");
if(packageIndex>0)
{
fileNames.Add("level" + (levelIndex-1) + "");
}else{
fileNames.Add(Path.Combine(kResourcesDirName, kResourcesFileName));
fileNames.Add(Path.Combine(kResourcesDirName, kResourcesExtraFileName));
fileNames.Add("mainData");
}
if(levelIndex==firstStreamedLevelWithResources)
{
fileNames.Add("resources.assets");
fileNames.Add("resources.resource");
}
var files = new List<string>();
foreach (var f in fileNames)
{
if (filesToShip.Contains(f))
{
filesToShip.Remove(f);
files.Add(f);
}
}
if (files.Count == 0)
break;
var packageFile = filename + "." + packageIndex;
if (!RunPackager(packageFile, stagingAreaData, files))
return false;
// Extract loader script for this data file, and put it's content into a JavaScript function
if(packageIndex>0)
{
var loaderText = File.ReadAllText(Path.Combine(stagingAreaData, packageFile + ".loader.js"));
loaderText = loaderText.Substring(loaderText.IndexOf("\n(function() {"));
loaderText = loaderText.Replace("if (Module['setStatus']) Module['setStatus']('Downloading data... (' + loaded + '/' + total + ')');",
"StreamProgressForLevelArray["+packageIndex+"]=event.loaded/size;");
loaderJS += "function DownloadDataForPackage" + packageIndex + "(){\n" + loaderText + "}\n";
loaderJSStart +="StreamProgressForLevelArray[" + packageIndex + "]=0;\n";
}else{
loaderJS0 = File.ReadAllText(Path.Combine(stagingAreaData, filename + ".0.loader.js"));
loaderJS0 = loaderJS0.Replace("if (Module['setStatus']) Module['setStatus']('Downloading data... (' + loaded + '/' + total + ')');",
"if (Module['setStatus']) Module['setStatus']('Downloading data... (' + event.loaded + '/' + size + ')');");
}
packageIndex++;
}
if(filesToShip.Count>0)
{
var packageFile = filename + "." + packageIndex;
if (!RunPackager(packageFile, stagingAreaData, filesToShip))
return false;
var loaderText2 = File.ReadAllText(Path.Combine(stagingAreaData, packageFile + ".loader.js"));
loaderText2 = loaderText2.Substring(loaderText2.IndexOf("\n(function() {"));
loaderJS += "function DownloadDataForPackage" + packageIndex + "(){\n" + loaderText2 + "}\n";
packageIndex++;
}
loaderJS = loaderJSStart + loaderJS + loaderJS0;
for (int j=0; j < packageIndex-1; j++)
{
// Patch loader scripts to invoke the function to start loading the next data file when it's done
var line = "Module['removeRunDependency']('datafile_" + filename + "." + j + ".data');";
// Call function to load next data file in a callback to avoid calling it while clearing pre-run dependencies.
loaderJS = loaderJS.Replace (line, line + "\nStreamProgressForLevelArray[" + j + "]=1;\nwindow.setTimeout(DownloadDataForPackage" + (j+1) + @",1);");
}
// write out loader script for all data files
File.WriteAllText(Path.Combine(stagingAreaData, kOutputFileLoaderFileName), loaderJS);
return true;
}
// Generate gzip compressed versions of files
private static void CompressFilesInOutputDirectory (string dir, string outputDir)
{
var filesToCompress = Directory.GetFiles(dir).Where(f => f.EndsWith(kOutputFileLoaderFileName) || f.EndsWith(".data"));
foreach (var file in filesToCompress)
{
var processName = "7za";
if (Application.platform == RuntimePlatform.WindowsEditor)
processName = "7z.exe";
var processStartInfo = new ProcessStartInfo(EditorApplication.applicationContentsPath+"/Tools/"+processName)
{
Arguments = "a -tgzip \""+Path.Combine(outputDir, Path.GetFileName(file))+"gz\" \""+file+"\"",
UseShellExecute = false,
CreateNoWindow = true
};
var p = Process.Start(processStartInfo);
p.WaitForExit();
if (p.ExitCode != 0)
throw new System.Exception("Failed running " + processStartInfo.FileName + " " + processStartInfo.Arguments);
}
}
[PostProcessBuild]
public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) {
if (target != BuildTarget.WebGL)
return;
var stagingAreaData = Path.Combine(Path.Combine("Temp", "StagingArea"), "Data");
var filename = Path.GetFileName(pathToBuiltProject);
// Find all files to package
IEnumerable<string> filesToShip = Directory.GetFiles(stagingAreaData, "*.resource");
filesToShip = filesToShip.Concat(Directory.GetFiles(stagingAreaData, "*.assets"));
filesToShip = filesToShip.Concat(Directory.GetFiles(stagingAreaData, "mainData"));
filesToShip = filesToShip.Concat(Directory.GetFiles(stagingAreaData, "level*"));
// put files into packages
PackageData (filename, stagingAreaData, filesToShip);
// delete old data file
File.Delete (Path.Combine(pathToBuiltProject, Path.Combine("Release", filename+".data")));
// copy new data files into build
File.Copy(Path.Combine(stagingAreaData, kOutputFileLoaderFileName), Path.Combine(Path.Combine(pathToBuiltProject, "Release"),kOutputFileLoaderFileName), true);
foreach (var f in Directory.GetFiles(stagingAreaData, "*.*.data"))
File.Copy(f, Path.Combine(Path.Combine(pathToBuiltProject, "Release"),Path.GetFileName(f)), true);
// compress new data files
CompressFilesInOutputDirectory (Path.Combine(pathToBuiltProject, "Release"), Path.Combine(pathToBuiltProject, "Compressed"));
// delete old compressed data file
File.Delete (Path.Combine(pathToBuiltProject, Path.Combine("Compressed", filename+".datagz")));
}
#endif
#if UNITY_WEBGL
[DllImport("__Internal")]
private static extern float GetStreamProgressForLevelFromWeb(int levelIndex);
#endif
static public bool CanStreamedLevelBeLoaded (int levelIndex)
{
#if UNITY_WEBGL && !UNITY_EDITOR
if (levelIndex == 0) // file = mainData
return true;
return File.Exists("level"+(levelIndex-1));
#else
return Application.CanStreamedLevelBeLoaded(levelIndex);
#endif
}
static public float GetStreamProgressForLevel (int levelIndex)
{
#if UNITY_WEBGL && !UNITY_EDITOR
if (levelIndex == 0) // file = mainData
return 1;
return GetStreamProgressForLevelFromWeb(levelIndex);
#else
return Application.GetStreamProgressForLevel(levelIndex);
#endif
}
}
And code for the JS file, WebGLLevelStreamingJS.jslib
var WebGLLevelStreamingJS = {
GetStreamProgressForLevelFromWeb: function(levelIndex)
{
return StreamProgressForLevelArray[levelIndex];
}
};
mergeInto(LibraryManager.library, WebGLLevelStreamingJS);
IMPORTANT
Tested on Unity5.0.0f4.
Unity still working on the WebGL build process, so this script may not work on versions newer than 5.0.0f4.
Known Issues:
GetStreamProgressForLevel() return 0 if loading from cache.
Some other issues with the progress that we need to check, but weren’t critical for us.
You all are welcome to improve the script