mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
Fix NavMesh memory leaks (#9175)
This commit is contained in:
@@ -389,7 +389,7 @@ module.exports = {
|
||||
.addIncludeFile(
|
||||
'Extensions/NavMeshPathfinding/NavMeshObstacleRuntimeBehavior.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
.addRequiredFile(
|
||||
'Extensions/NavMeshPathfinding/recast-navigation.wasm.js'
|
||||
)
|
||||
.addRequiredFile(
|
||||
@@ -858,7 +858,7 @@ module.exports = {
|
||||
.addIncludeFile(
|
||||
'Extensions/NavMeshPathfinding/NavMeshObstacleRuntimeBehavior.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
.addRequiredFile(
|
||||
'Extensions/NavMeshPathfinding/recast-navigation.wasm.js'
|
||||
)
|
||||
.addRequiredFile(
|
||||
@@ -886,7 +886,9 @@ module.exports = {
|
||||
.addIncludeFile(
|
||||
'Extensions/NavMeshPathfinding/NavMeshObstacleRuntimeBehavior.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/NavMeshPathfinding/recast-navigation.wasm.js')
|
||||
// `recast-navigation.wasm.js` is an ES module loaded with `import()` by
|
||||
// NavMeshCharacterRuntimeBehavior.js: it's a required file of the
|
||||
// behaviors and must not be included as a classic script.
|
||||
.addIncludeFile(
|
||||
'Extensions/NavMeshPathfinding/recast-navigation-generators.js'
|
||||
)
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
namespace gdjs {
|
||||
const loadRecast = async () => {
|
||||
try {
|
||||
const module = await import('./recast-navigation.wasm.js');
|
||||
const initializeRecast = module.default;
|
||||
if (!initializeRecast) {
|
||||
throw new Error('No default export found in Recast.');
|
||||
}
|
||||
|
||||
const Recast = await initializeRecast();
|
||||
|
||||
await RecastNav.init();
|
||||
//@ts-ignore
|
||||
window.Recast = Recast;
|
||||
// Give the module instantiated from `recast-navigation.wasm.wasm` to
|
||||
// RecastNav. Without an implementation, `RecastNav.init` instantiates
|
||||
// the "compat" build embedded (in base64) in
|
||||
// `recast-navigation-generators.js`, so each game would create 2
|
||||
// WebAssembly instances (each with its own memory) instead of 1.
|
||||
// Previews of the in-game editor and gameplay tests run in the editor
|
||||
// process: this halves the WebAssembly memory reserved by each preview.
|
||||
// `RecastNav.init` does nothing (and doesn't instantiate anything) if
|
||||
// Recast is already initialized, for instance if this script is
|
||||
// reloaded by the hot-reloader.
|
||||
// The vendored `init` expects a function returning the module, but the
|
||||
// typings of `@recast-navigation/core` declare the module itself.
|
||||
const instantiateRecastModule = async () => {
|
||||
const module = await import('./recast-navigation.wasm.js');
|
||||
const initializeRecast = module.default;
|
||||
if (!initializeRecast) {
|
||||
throw new Error('No default export found in Recast.');
|
||||
}
|
||||
return initializeRecast();
|
||||
};
|
||||
await RecastNav.init(
|
||||
instantiateRecastModule as unknown as Parameters<
|
||||
typeof RecastNav.init
|
||||
>[0]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Unable to load Recast navigation mesh library.', err);
|
||||
throw err;
|
||||
|
||||
@@ -116,6 +116,17 @@ namespace gdjs {
|
||||
this.meshes.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the debug drawer from the WebAssembly memory.
|
||||
*/
|
||||
dispose() {
|
||||
this.removeFor3D();
|
||||
if (this.debugDrawerUtils) {
|
||||
this.debugDrawerUtils.dispose();
|
||||
this.debugDrawerUtils = null;
|
||||
}
|
||||
}
|
||||
|
||||
renderFor3D() {
|
||||
this.removeFor3D();
|
||||
if (!this.enabled || !this.obstaclesManager.is3D) {
|
||||
|
||||
@@ -582,8 +582,43 @@ namespace gdjs {
|
||||
character._agent = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the navigation mesh and the crowd from the WebAssembly memory.
|
||||
* The manager must not be used after this.
|
||||
*/
|
||||
dispose(): void {
|
||||
for (const character of this.characters) {
|
||||
character._agent = null;
|
||||
}
|
||||
this.characters.clear();
|
||||
this.obstacles.clear();
|
||||
if (this.crowd) {
|
||||
this.crowd.destroy();
|
||||
this.crowd = null;
|
||||
}
|
||||
if (this.navMesh) {
|
||||
this.navMesh.destroy();
|
||||
this.navMesh = null;
|
||||
}
|
||||
if (this.debuggerRenderer) {
|
||||
this.debuggerRenderer.dispose();
|
||||
this.debuggerRenderer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gdjs.registerRuntimeSceneUnloadedCallback(function (runtimeScene) {
|
||||
// Without this, the navigation mesh and the crowd of each unloaded scene
|
||||
// would stay allocated in the WebAssembly memory.
|
||||
const navMeshObstaclesManager = runtimeScene.navMeshObstaclesManager;
|
||||
if (navMeshObstaclesManager) {
|
||||
navMeshObstaclesManager.dispose();
|
||||
// @ts-ignore The manager is set back to undefined like before its creation.
|
||||
runtimeScene.navMeshObstaclesManager = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** @category Behaviors > NavMesh pathfinding */
|
||||
export namespace NavMeshObstaclesManager {
|
||||
/**
|
||||
|
||||
@@ -315,6 +315,24 @@ describe('gdjs.NavMeshCharacterRuntimeBehavior', function () {
|
||||
expect(getPathLength(character)).to.be.within(180, 220);
|
||||
});
|
||||
|
||||
it('frees the navigation mesh and the crowd when the scene is unloaded', function () {
|
||||
character.setPosition(400, 300);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
getCharacterBehavior(character).moveTo(600, 300, 0);
|
||||
runtimeScene.renderAndStep(1000 / 60);
|
||||
|
||||
const navMeshObstaclesManager = runtimeScene.navMeshObstaclesManager;
|
||||
expect(navMeshObstaclesManager.navMesh).not.to.be(null);
|
||||
expect(navMeshObstaclesManager.crowd).not.to.be(null);
|
||||
|
||||
runtimeScene.unloadScene();
|
||||
|
||||
expect(navMeshObstaclesManager.navMesh).to.be(null);
|
||||
expect(navMeshObstaclesManager.crowd).to.be(null);
|
||||
expect(navMeshObstaclesManager.characters.size).to.be(0);
|
||||
expect(runtimeScene.navMeshObstaclesManager).to.be(undefined);
|
||||
});
|
||||
|
||||
describe('(network synchronization)', function () {
|
||||
it('can sync an idle character over an idle character without error', function () {
|
||||
const otherCharacter = addCharacter(runtimeScene);
|
||||
|
||||
@@ -116,6 +116,9 @@ namespace gdjs {
|
||||
if (
|
||||
// Don't reload Box2d as it would confuse and crash the asm.js library.
|
||||
endsWith(srcFilename, 'box2d.js') ||
|
||||
// Don't reload the Recast navigation library as it would lose
|
||||
// its initialized WebAssembly module.
|
||||
endsWith(srcFilename, 'recast-navigation-generators.js') ||
|
||||
// Don't reload sha256.js library.
|
||||
endsWith(srcFilename, 'sha256.js') ||
|
||||
// Don't reload shopify-buy library.
|
||||
|
||||
@@ -198,6 +198,11 @@ export const installAnalyticsEvents = () => {
|
||||
posthogLoaded = true;
|
||||
},
|
||||
autocapture: false, // we disable autocapture because we want to control which events we send.
|
||||
session_recording: {
|
||||
// Iframes (in-game editor, gameplay tests) are not recorded: the recording would
|
||||
// keep a reference to every document loaded in them (including game WebAssembly memories).
|
||||
blockSelector: 'iframe',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user