You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-11-05 12:10:55 +00:00
HTML5 start-up overhaul
- Implement promise-based JS interface for custom HTML page integration - Add download progress callback - Add progress bar and indeterminate spinner to default HTML page - Try downloading files multiple times when failing - Get rid of godotfs.js - Separate steps for engine initialization, game initialization and game start - Allow multiple games on one HTML page - Substitution placeholders only used in .html file - Placeholders renamed: $GODOT_BASE => $GODOT_BASENAME, $GODOT_TMEM -> $GODOT_TOTAL_MEMORY - Emscripten Module is now Engine.RuntimeEnvironment (no longer a global)
This commit is contained in:
366
platform/javascript/engine.js
Normal file
366
platform/javascript/engine.js
Normal file
@@ -0,0 +1,366 @@
|
||||
return Module;
|
||||
},
|
||||
};
|
||||
|
||||
(function() {
|
||||
var engine = Engine;
|
||||
|
||||
var USING_WASM = engine.USING_WASM;
|
||||
var DOWNLOAD_ATTEMPTS_MAX = 4;
|
||||
|
||||
var basePath = null;
|
||||
var engineLoadPromise = null;
|
||||
|
||||
var loadingFiles = {};
|
||||
|
||||
function getBasePath(path) {
|
||||
|
||||
if (path.endsWith('/'))
|
||||
path = path.slice(0, -1);
|
||||
if (path.lastIndexOf('.') > path.lastIndexOf('/'))
|
||||
path = path.slice(0, path.lastIndexOf('.'));
|
||||
return path;
|
||||
}
|
||||
|
||||
function getBaseName(path) {
|
||||
|
||||
path = getBasePath(path);
|
||||
return path.slice(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
Engine = function Engine() {
|
||||
|
||||
this.rtenv = null;
|
||||
|
||||
var gameInitPromise = null;
|
||||
var unloadAfterInit = true;
|
||||
var memorySize = 268435456;
|
||||
|
||||
var progressFunc = null;
|
||||
var pckProgressTracker = {};
|
||||
var lastProgress = { loaded: 0, total: 0 };
|
||||
|
||||
var canvas = null;
|
||||
var stdout = null;
|
||||
var stderr = null;
|
||||
|
||||
this.initGame = function(mainPack) {
|
||||
|
||||
if (!gameInitPromise) {
|
||||
|
||||
if (mainPack === undefined) {
|
||||
if (basePath !== null) {
|
||||
mainPack = basePath + '.pck';
|
||||
} else {
|
||||
return Promise.reject(new Error("No main pack to load specified"));
|
||||
}
|
||||
}
|
||||
if (basePath === null)
|
||||
basePath = getBasePath(mainPack);
|
||||
|
||||
gameInitPromise = Engine.initEngine().then(
|
||||
instantiate.bind(this)
|
||||
);
|
||||
var gameLoadPromise = loadPromise(mainPack, pckProgressTracker).then(function(xhr) { return xhr.response; });
|
||||
gameInitPromise = Promise.all([gameLoadPromise, gameInitPromise]).then(function(values) {
|
||||
// resolve with pck
|
||||
return new Uint8Array(values[0]);
|
||||
});
|
||||
if (unloadAfterInit)
|
||||
gameInitPromise.then(Engine.unloadEngine);
|
||||
requestAnimationFrame(animateProgress);
|
||||
}
|
||||
return gameInitPromise;
|
||||
};
|
||||
|
||||
function instantiate(initializer) {
|
||||
|
||||
var rtenvOpts = {
|
||||
noInitialRun: true,
|
||||
thisProgram: getBaseName(basePath),
|
||||
engine: this,
|
||||
};
|
||||
if (typeof stdout === 'function')
|
||||
rtenvOpts.print = stdout;
|
||||
if (typeof stderr === 'function')
|
||||
rtenvOpts.printErr = stderr;
|
||||
if (typeof WebAssembly === 'object' && initializer instanceof WebAssembly.Module) {
|
||||
rtenvOpts.instantiateWasm = function(imports, onSuccess) {
|
||||
WebAssembly.instantiate(initializer, imports).then(function(result) {
|
||||
onSuccess(result);
|
||||
});
|
||||
return {};
|
||||
};
|
||||
} else if (initializer.asm && initializer.mem) {
|
||||
rtenvOpts.asm = initializer.asm;
|
||||
rtenvOpts.memoryInitializerRequest = initializer.mem;
|
||||
rtenvOpts.TOTAL_MEMORY = memorySize;
|
||||
} else {
|
||||
throw new Error("Invalid initializer");
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
rtenvOpts.onRuntimeInitialized = resolve;
|
||||
rtenvOpts.onAbort = reject;
|
||||
rtenvOpts.engine.rtenv = Engine.RuntimeEnvironment(rtenvOpts);
|
||||
});
|
||||
}
|
||||
|
||||
this.start = function(mainPack) {
|
||||
|
||||
return this.initGame(mainPack).then(synchronousStart.bind(this));
|
||||
};
|
||||
|
||||
function synchronousStart(pckView) {
|
||||
// TODO don't expect canvas when runninng as cli tool
|
||||
if (canvas instanceof HTMLCanvasElement) {
|
||||
this.rtenv.canvas = canvas;
|
||||
} else {
|
||||
var firstCanvas = document.getElementsByTagName('canvas')[0];
|
||||
if (firstCanvas instanceof HTMLCanvasElement) {
|
||||
this.rtenv.canvas = firstCanvas;
|
||||
} else {
|
||||
throw new Error("No canvas found");
|
||||
}
|
||||
}
|
||||
|
||||
var actualCanvas = this.rtenv.canvas;
|
||||
var context = false;
|
||||
try {
|
||||
context = actualCanvas.getContext('webgl2') || actualCanvas.getContext('experimental-webgl2');
|
||||
} catch (e) {}
|
||||
if (!context) {
|
||||
throw new Error("WebGL 2 not available");
|
||||
}
|
||||
|
||||
// canvas can grab focus on click
|
||||
if (actualCanvas.tabIndex < 0) {
|
||||
actualCanvas.tabIndex = 0;
|
||||
}
|
||||
// necessary to calculate cursor coordinates correctly
|
||||
actualCanvas.style.padding = 0;
|
||||
actualCanvas.style.borderWidth = 0;
|
||||
actualCanvas.style.borderStyle = 'none';
|
||||
// until context restoration is implemented
|
||||
actualCanvas.addEventListener('webglcontextlost', function(ev) {
|
||||
alert("WebGL context lost, please reload the page");
|
||||
ev.preventDefault();
|
||||
}, false);
|
||||
|
||||
this.rtenv.FS.createDataFile('/', this.rtenv.thisProgram + '.pck', pckView, true, true, true);
|
||||
gameInitPromise = null;
|
||||
this.rtenv.callMain();
|
||||
}
|
||||
|
||||
this.setProgressFunc = function(func) {
|
||||
progressFunc = func;
|
||||
};
|
||||
|
||||
function animateProgress() {
|
||||
|
||||
var loaded = 0;
|
||||
var total = 0;
|
||||
var totalIsValid = true;
|
||||
var progressIsFinal = true;
|
||||
|
||||
[loadingFiles, pckProgressTracker].forEach(function(tracker) {
|
||||
Object.keys(tracker).forEach(function(file) {
|
||||
if (!tracker[file].final)
|
||||
progressIsFinal = false;
|
||||
if (!totalIsValid || tracker[file].total === 0) {
|
||||
totalIsValid = false;
|
||||
total = 0;
|
||||
} else {
|
||||
total += tracker[file].total;
|
||||
}
|
||||
loaded += tracker[file].loaded;
|
||||
});
|
||||
});
|
||||
if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
|
||||
lastProgress.loaded = loaded;
|
||||
lastProgress.total = total;
|
||||
if (typeof progressFunc === 'function')
|
||||
progressFunc(loaded, total);
|
||||
}
|
||||
if (!progressIsFinal)
|
||||
requestAnimationFrame(animateProgress);
|
||||
}
|
||||
|
||||
this.setCanvas = function(elem) {
|
||||
canvas = elem;
|
||||
};
|
||||
|
||||
this.setAsmjsMemorySize = function(size) {
|
||||
memorySize = size;
|
||||
};
|
||||
|
||||
this.setUnloadAfterInit = function(enabled) {
|
||||
|
||||
if (enabled && !unloadAfterInit && gameInitPromise) {
|
||||
gameInitPromise.then(Engine.unloadEngine);
|
||||
}
|
||||
unloadAfterInit = enabled;
|
||||
};
|
||||
|
||||
this.setStdoutFunc = function(func) {
|
||||
|
||||
var print = function(text) {
|
||||
if (arguments.length > 1) {
|
||||
text = Array.prototype.slice.call(arguments).join(" ");
|
||||
}
|
||||
func(text);
|
||||
};
|
||||
if (this.rtenv)
|
||||
this.rtenv.print = print;
|
||||
stdout = print;
|
||||
};
|
||||
|
||||
this.setStderrFunc = function(func) {
|
||||
|
||||
var printErr = function(text) {
|
||||
if (arguments.length > 1)
|
||||
text = Array.prototype.slice.call(arguments).join(" ");
|
||||
func(text);
|
||||
};
|
||||
if (this.rtenv)
|
||||
this.rtenv.printErr = printErr;
|
||||
stderr = printErr;
|
||||
};
|
||||
|
||||
|
||||
}; // Engine()
|
||||
|
||||
Engine.RuntimeEnvironment = engine.RuntimeEnvironment;
|
||||
|
||||
Engine.initEngine = function(newBasePath) {
|
||||
|
||||
if (newBasePath !== undefined) basePath = getBasePath(newBasePath);
|
||||
if (engineLoadPromise === null) {
|
||||
if (USING_WASM) {
|
||||
if (typeof WebAssembly !== 'object')
|
||||
return Promise.reject(new Error("Browser doesn't support WebAssembly"));
|
||||
// TODO cache/retrieve module to/from idb
|
||||
engineLoadPromise = loadPromise(basePath + '.wasm').then(function(xhr) {
|
||||
return WebAssembly.compile(xhr.response);
|
||||
});
|
||||
} else {
|
||||
var asmjsPromise = loadPromise(basePath + '.asm.js').then(function(xhr) {
|
||||
return asmjsModulePromise(xhr.response);
|
||||
});
|
||||
var memPromise = loadPromise(basePath + '.mem');
|
||||
engineLoadPromise = Promise.all([asmjsPromise, memPromise]).then(function(values) {
|
||||
return { asm: values[0], mem: values[1] };
|
||||
});
|
||||
}
|
||||
engineLoadPromise = engineLoadPromise.catch(function(err) {
|
||||
engineLoadPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return engineLoadPromise;
|
||||
};
|
||||
|
||||
function asmjsModulePromise(module) {
|
||||
var elem = document.createElement('script');
|
||||
var script = new Blob([
|
||||
'Engine.asm = (function() { var Module = {};',
|
||||
module,
|
||||
'return Module.asm; })();'
|
||||
]);
|
||||
var url = URL.createObjectURL(script);
|
||||
elem.src = url;
|
||||
return new Promise(function(resolve, reject) {
|
||||
elem.addEventListener('load', function() {
|
||||
URL.revokeObjectURL(url);
|
||||
var asm = Engine.asm;
|
||||
Engine.asm = undefined;
|
||||
setTimeout(function() {
|
||||
// delay to reclaim compilation memory
|
||||
resolve(asm);
|
||||
}, 1);
|
||||
});
|
||||
elem.addEventListener('error', function() {
|
||||
URL.revokeObjectURL(url);
|
||||
reject("asm.js faiilure");
|
||||
});
|
||||
document.body.appendChild(elem);
|
||||
});
|
||||
}
|
||||
|
||||
Engine.unloadEngine = function() {
|
||||
engineLoadPromise = null;
|
||||
};
|
||||
|
||||
function loadPromise(file, tracker) {
|
||||
if (tracker === undefined)
|
||||
tracker = loadingFiles;
|
||||
return new Promise(function(resolve, reject) {
|
||||
loadXHR(resolve, reject, file, tracker);
|
||||
});
|
||||
}
|
||||
|
||||
function loadXHR(resolve, reject, file, tracker) {
|
||||
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.open('GET', file);
|
||||
if (!file.endsWith('.js')) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
['loadstart', 'progress', 'load', 'error', 'timeout', 'abort'].forEach(function(ev) {
|
||||
xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker));
|
||||
});
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function onXHREvent(resolve, reject, file, tracker, ev) {
|
||||
|
||||
if (this.status >= 400) {
|
||||
|
||||
if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
||||
reject(new Error("Failed loading file '" + file + "': " + this.statusText));
|
||||
this.abort();
|
||||
return;
|
||||
} else {
|
||||
loadXHR(resolve, reject, file);
|
||||
}
|
||||
}
|
||||
|
||||
switch (ev.type) {
|
||||
case 'loadstart':
|
||||
if (tracker[file] === undefined) {
|
||||
tracker[file] = {
|
||||
total: ev.total,
|
||||
loaded: ev.loaded,
|
||||
attempts: 0,
|
||||
final: false,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'progress':
|
||||
tracker[file].loaded = ev.loaded;
|
||||
tracker[file].total = ev.total;
|
||||
break;
|
||||
|
||||
case 'load':
|
||||
tracker[file].final = true;
|
||||
resolve(this);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
case 'timeout':
|
||||
if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
||||
tracker[file].final = true;
|
||||
reject(new Error("Failed loading file '" + file + "'"));
|
||||
} else {
|
||||
loadXHR(resolve, reject, file);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'abort':
|
||||
tracker[file].final = true;
|
||||
reject(new Error("Loading file '" + file + "' was aborted."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user