diff --git a/packages/parser/src/sceneParser.ts b/packages/parser/src/sceneParser.ts index 68d130c0b..e6528be25 100644 --- a/packages/parser/src/sceneParser.ts +++ b/packages/parser/src/sceneParser.ts @@ -38,12 +38,13 @@ export const sceneParser = ( let assetsList: Array = []; // 场景资源列表 let subSceneList: Array = []; // 子场景列表 const sentenceList: Array = rawSentenceListWithoutEmpty.map( - (sentence) => { + (sentence, index) => { const returnSentence: ISentence = scriptParser( sentence, assetSetter, ADD_NEXT_ARG_LIST, SCRIPT_CONFIG_MAP, + index, ); // 在这里解析出语句可能携带的资源和场景,合并到 assetsList 和 subSceneList assetsList = [...assetsList, ...returnSentence.sentenceAssets]; diff --git a/packages/parser/src/scriptParser/assetsScanner.ts b/packages/parser/src/scriptParser/assetsScanner.ts index cdff61be5..8cf74e03e 100644 --- a/packages/parser/src/scriptParser/assetsScanner.ts +++ b/packages/parser/src/scriptParser/assetsScanner.ts @@ -12,6 +12,7 @@ export const assetsScanner = ( command: commandType, content: string, args: Array, + lineNumber: number, ): Array => { let hasVocalArg = false; const returnAssetsList: Array = []; @@ -22,7 +23,7 @@ export const assetsScanner = ( returnAssetsList.push({ name: e.value as string, url: e.value as string, - lineNumber: 0, + lineNumber, type: fileType.vocal, }); } @@ -36,7 +37,7 @@ export const assetsScanner = ( returnAssetsList.push({ name: content, url: content, - lineNumber: 0, + lineNumber, type: fileType.background, }); } @@ -44,7 +45,7 @@ export const assetsScanner = ( returnAssetsList.push({ name: content, url: content, - lineNumber: 0, + lineNumber, type: fileType.figure, }); } @@ -52,7 +53,7 @@ export const assetsScanner = ( returnAssetsList.push({ name: content, url: content, - lineNumber: 0, + lineNumber, type: fileType.figure, }); } @@ -60,7 +61,7 @@ export const assetsScanner = ( returnAssetsList.push({ name: content, url: content, - lineNumber: 0, + lineNumber, type: fileType.video, }); } @@ -68,7 +69,7 @@ export const assetsScanner = ( returnAssetsList.push({ name: content, url: content, - lineNumber: 0, + lineNumber, type: fileType.bgm, }); } diff --git a/packages/parser/src/scriptParser/scriptParser.ts b/packages/parser/src/scriptParser/scriptParser.ts index 62e6020dc..4ecb68e73 100644 --- a/packages/parser/src/scriptParser/scriptParser.ts +++ b/packages/parser/src/scriptParser/scriptParser.ts @@ -24,6 +24,7 @@ export const scriptParser = ( assetSetter: any, ADD_NEXT_ARG_LIST: commandType[], SCRIPT_CONFIG_MAP: ConfigMap, + lineNumber = 0, ): ISentence => { let command: commandType; // 默认为对话 let content: string; // 语句内容 @@ -105,7 +106,7 @@ export const scriptParser = ( } content = contentParser(newSentenceRaw.trim(), command, assetSetter); // 将语句内容里的文件名转为相对或绝对路径 - sentenceAssets = assetsScanner(command, content, args); // 扫描语句携带资源 + sentenceAssets = assetsScanner(command, content, args, lineNumber); // 扫描语句携带资源 subScene = subSceneScanner(command, content); // 扫描语句携带子场景 return { command: command, // 语句类型 diff --git a/packages/parser/test/parser.test.ts b/packages/parser/test/parser.test.ts index 42b1e1d4d..87d1cd94b 100644 --- a/packages/parser/test/parser.test.ts +++ b/packages/parser/test/parser.test.ts @@ -49,7 +49,7 @@ test("args", async () => { { key: "left", value: true }, { key: "next", value: true } ], - sentenceAssets: [{ name: "m2.png", url: 'm2.png', type: fileType.figure, lineNumber: 0 }], + sentenceAssets: [{ name: "m2.png", url: 'm2.png', type: fileType.figure, lineNumber: 24 }], subScene: [], inlineComment: "" }; diff --git a/packages/webgal/public/webgal-serviceworker.js b/packages/webgal/public/webgal-serviceworker.js index 892ecb1d6..6b3bab90f 100644 --- a/packages/webgal/public/webgal-serviceworker.js +++ b/packages/webgal/public/webgal-serviceworker.js @@ -50,6 +50,24 @@ async function cacheFirst(request) { return response; } +async function prefetchFromMessage(urlString) { + const requestUrl = new URL(urlString, self.location.origin).toString(); + const request = new Request(requestUrl, { method: 'GET' }); + if (!isCriticalGameRequest(request)) { + return; + } + const cache = await caches.open(CACHE_NAME); + const hasCached = await cache.match(requestUrl); + if (hasCached) { + return; + } + const response = await fetch(request); + if (response.ok && response.status === 200) { + await cache.put(requestUrl, response.clone()); + logOnce(`message-cache:${requestUrl}`, 'message cached:', new URL(requestUrl).pathname); + } +} + self.addEventListener('fetch', (event) => { const { request } = event; if (!isCriticalGameRequest(request)) return; @@ -67,3 +85,15 @@ self.addEventListener('fetch', (event) => { }), ); }); + +self.addEventListener('message', (event) => { + const data = event.data || {}; + if (data.type !== 'WEBGAL_PREFETCH_ASSET' || typeof data.url !== 'string') { + return; + } + event.waitUntil( + prefetchFromMessage(data.url).catch((error) => { + console.warn(LOG_PREFIX, 'message prefetch failed:', error); + }), + ); +}); diff --git a/packages/webgal/src/Core/Modules/scene.ts b/packages/webgal/src/Core/Modules/scene.ts index 1f72069f1..89816968b 100644 --- a/packages/webgal/src/Core/Modules/scene.ts +++ b/packages/webgal/src/Core/Modules/scene.ts @@ -24,8 +24,8 @@ export const initSceneData = { }; export class SceneManager { - public settledScenes: Array = []; - public settledAssets: Array = []; + public settledScenes: Set = new Set(); + public settledAssets: Set = new Set(); public sceneData: ISceneData = cloneDeep(initSceneData); public lockSceneWrite = false; @@ -33,5 +33,7 @@ export class SceneManager { this.sceneData.currentSentenceId = 0; this.sceneData.sceneStack = []; this.sceneData.currentScene = cloneDeep(initSceneData.currentScene); + this.settledScenes.clear(); + this.settledAssets.clear(); } } diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 04f5c997b..74e8f6242 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -13,6 +13,7 @@ import { IBacklogItem } from '@/Core/Modules/backlog'; import { SYSTEM_CONFIG } from '@/config'; import { WebGAL } from '@/Core/WebGAL'; import { getBooleanArgByKey, getStringArgByKey } from '@/Core/util/getSentenceArg'; +import { prefetchCurrentSceneByProgress } from '@/Core/util/prefetcher/progressPrefetcher'; export const whenChecker = (whenValue: string | undefined): boolean => { if (whenValue === undefined) { @@ -39,6 +40,7 @@ export const whenChecker = (whenValue: string | undefined): boolean => { * 执行语句,同步场景状态,并根据情况立即执行下一句或者加入backlog */ export const scriptExecutor = () => { + prefetchCurrentSceneByProgress(); // 超过总语句数量,则从场景栈拿出一个需要继续的场景,然后继续流程。若场景栈清空,则停止流程 if ( WebGAL.sceneManager.sceneData.currentSentenceId > diff --git a/packages/webgal/src/Core/controller/scene/callScene.ts b/packages/webgal/src/Core/controller/scene/callScene.ts index ead6e3bd1..53aadd554 100644 --- a/packages/webgal/src/Core/controller/scene/callScene.ts +++ b/packages/webgal/src/Core/controller/scene/callScene.ts @@ -2,8 +2,7 @@ import { sceneFetcher } from './sceneFetcher'; import { sceneParser } from '../../parser/sceneParser'; import { logger } from '../../util/logger'; import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; -import uniqWith from 'lodash/uniqWith'; -import { scenePrefetcher } from '@/Core/util/prefetcher/scenePrefetcher'; +import { clearPrefetchLinks } from '@/Core/util/prefetcher/assetsPrefetcher'; import { WebGAL } from '@/Core/WebGAL'; @@ -28,11 +27,8 @@ export const callScene = (sceneUrl: string, sceneName: string) => { .then((rawScene) => { WebGAL.sceneManager.sceneData.currentScene = sceneParser(rawScene, sceneName, sceneUrl); WebGAL.sceneManager.sceneData.currentSentenceId = 0; - // 开始场景的预加载 - const subSceneList = WebGAL.sceneManager.sceneData.currentScene.subSceneList; - WebGAL.sceneManager.settledScenes.push(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 - const subSceneListUniq = uniqWith(subSceneList); // 去重 - scenePrefetcher(subSceneListUniq); + clearPrefetchLinks(); + WebGAL.sceneManager.settledScenes.add(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 logger.debug('现在调用场景,调用结果:', WebGAL.sceneManager.sceneData); WebGAL.sceneManager.lockSceneWrite = false; nextSentence(); diff --git a/packages/webgal/src/Core/controller/scene/changeScene.ts b/packages/webgal/src/Core/controller/scene/changeScene.ts index 67df0879a..4bac9b37d 100644 --- a/packages/webgal/src/Core/controller/scene/changeScene.ts +++ b/packages/webgal/src/Core/controller/scene/changeScene.ts @@ -2,8 +2,7 @@ import { sceneFetcher } from './sceneFetcher'; import { sceneParser } from '../../parser/sceneParser'; import { logger } from '../../util/logger'; import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; -import uniqWith from 'lodash/uniqWith'; -import { scenePrefetcher } from '@/Core/util/prefetcher/scenePrefetcher'; +import { clearPrefetchLinks } from '@/Core/util/prefetcher/assetsPrefetcher'; import { WebGAL } from '@/Core/WebGAL'; @@ -22,11 +21,8 @@ export const changeScene = (sceneUrl: string, sceneName: string) => { .then((rawScene) => { WebGAL.sceneManager.sceneData.currentScene = sceneParser(rawScene, sceneName, sceneUrl); WebGAL.sceneManager.sceneData.currentSentenceId = 0; - // 开始场景的预加载 - const subSceneList = WebGAL.sceneManager.sceneData.currentScene.subSceneList; - WebGAL.sceneManager.settledScenes.push(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 - const subSceneListUniq = uniqWith(subSceneList); // 去重 - scenePrefetcher(subSceneListUniq); + clearPrefetchLinks(); + WebGAL.sceneManager.settledScenes.add(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 logger.debug('现在切换场景,切换后的结果:', WebGAL.sceneManager.sceneData); WebGAL.sceneManager.lockSceneWrite = false; nextSentence(); diff --git a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts index 2ebf4ce6b..81a459a8a 100644 --- a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts +++ b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts @@ -8,8 +8,6 @@ import { setVisibility } from '@/store/GUIReducer'; import { runScript } from '@/Core/controller/gamePlay/runScript'; import { stopAllPerform } from '@/Core/controller/gamePlay/stopAllPerform'; import cloneDeep from 'lodash/cloneDeep'; -import uniqWith from 'lodash/uniqWith'; -import { scenePrefetcher } from '@/Core/util/prefetcher/scenePrefetcher'; import { WebGAL } from '@/Core/WebGAL'; @@ -44,11 +42,7 @@ export const jumpFromBacklog = (index: number, refetchScene = true) => { backlogFile.saveScene.sceneName, backlogFile.saveScene.sceneUrl, ); - // 开始场景的预加载 - const subSceneList = WebGAL.sceneManager.sceneData.currentScene.subSceneList; - WebGAL.sceneManager.settledScenes.push(WebGAL.sceneManager.sceneData.currentScene.sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 - const subSceneListUniq = uniqWith(subSceneList); // 去重 - scenePrefetcher(subSceneListUniq); + WebGAL.sceneManager.settledScenes.add(WebGAL.sceneManager.sceneData.currentScene.sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 }); WebGAL.sceneManager.sceneData.currentSentenceId = backlogFile.saveScene.currentSentenceId; WebGAL.sceneManager.sceneData.sceneStack = cloneDeep(backlogFile.saveScene.sceneStack); diff --git a/packages/webgal/src/Core/controller/storage/loadGame.ts b/packages/webgal/src/Core/controller/storage/loadGame.ts index ae471ab7e..865000bee 100644 --- a/packages/webgal/src/Core/controller/storage/loadGame.ts +++ b/packages/webgal/src/Core/controller/storage/loadGame.ts @@ -8,8 +8,6 @@ import { setVisibility } from '@/store/GUIReducer'; import { restorePerform } from './jumpFromBacklog'; import { stopAllPerform } from '@/Core/controller/gamePlay/stopAllPerform'; import cloneDeep from 'lodash/cloneDeep'; -import uniqWith from 'lodash/uniqWith'; -import { scenePrefetcher } from '@/Core/util/prefetcher/scenePrefetcher'; import { setEbg } from '@/Core/gameScripts/changeBg/setEbg'; import { WebGAL } from '@/Core/WebGAL'; @@ -40,11 +38,7 @@ export function loadGameFromStageData(stageData: ISaveData) { loadFile.sceneData.sceneName, loadFile.sceneData.sceneUrl, ); - // 开始场景的预加载 - const subSceneList = WebGAL.sceneManager.sceneData.currentScene.subSceneList; - WebGAL.sceneManager.settledScenes.push(WebGAL.sceneManager.sceneData.currentScene.sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 - const subSceneListUniq = uniqWith(subSceneList); // 去重 - scenePrefetcher(subSceneListUniq); + WebGAL.sceneManager.settledScenes.add(WebGAL.sceneManager.sceneData.currentScene.sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 }); WebGAL.sceneManager.sceneData.currentSentenceId = loadFile.sceneData.currentSentenceId; WebGAL.sceneManager.sceneData.sceneStack = cloneDeep(loadFile.sceneData.sceneStack); diff --git a/packages/webgal/src/Core/initializeScript.ts b/packages/webgal/src/Core/initializeScript.ts index a1b17960c..2cba7ae38 100644 --- a/packages/webgal/src/Core/initializeScript.ts +++ b/packages/webgal/src/Core/initializeScript.ts @@ -8,8 +8,6 @@ import { sceneFetcher } from './controller/scene/sceneFetcher'; import { sceneParser } from './parser/sceneParser'; import { bindExtraFunc } from '@/Core/util/coreInitialFunction/bindExtraFunc'; import { webSocketFunc } from '@/Core/util/syncWithEditor/webSocketFunc'; -import uniqWith from 'lodash/uniqWith'; -import { scenePrefetcher } from './util/prefetcher/scenePrefetcher'; import PixiStage from '@/Core/controller/stage/pixi/PixiController'; import axios from 'axios'; import { __INFO } from '@/config/info'; @@ -51,11 +49,7 @@ export const initializeScript = (): void => { // 场景写入到运行时 sceneFetcher(sceneUrl).then((rawScene) => { WebGAL.sceneManager.sceneData.currentScene = sceneParser(rawScene, 'start.txt', sceneUrl); - // 开始场景的预加载 - const subSceneList = WebGAL.sceneManager.sceneData.currentScene.subSceneList; - WebGAL.sceneManager.settledScenes.push(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 - const subSceneListUniq = uniqWith(subSceneList); // 去重 - scenePrefetcher(subSceneListUniq); + WebGAL.sceneManager.settledScenes.add(sceneUrl); // 放入已加载场景列表,避免递归加载相同场景 }); /** * 启动Pixi diff --git a/packages/webgal/src/Core/util/prefetcher/assetsPrefetcher.ts b/packages/webgal/src/Core/util/prefetcher/assetsPrefetcher.ts index f3176f0cd..712e36402 100644 --- a/packages/webgal/src/Core/util/prefetcher/assetsPrefetcher.ts +++ b/packages/webgal/src/Core/util/prefetcher/assetsPrefetcher.ts @@ -2,36 +2,146 @@ import { IAsset } from '@/Core/controller/scene/sceneInterface'; import { logger } from '../logger'; import { WebGAL } from '@/Core/WebGAL'; +import { fileType } from '@/Core/util/gameAssetsAccess/assetSetter'; + +interface IAssetsPrefetcherOptions { + /** + * 默认会限制为“场景开头窗口”资源,避免 parser 一次性触发整场景预加载。 + */ + ignoreLineGate?: boolean; +} + +const INITIAL_PARSE_LINE_LOOKAHEAD = 24; +const ASSET_PREFETCH_INTERVAL_MS = 220; +const assetPrefetchQueue: Array = []; +const queuedAssetUrlSet = new Set(); +let isAssetPrefetchQueueRunning = false; + +const uniqueAssetsByUrl = (assetList: Array) => { + const seenUrlSet = new Set(); + return assetList.filter((asset) => { + if (!asset.url || seenUrlSet.has(asset.url)) { + return false; + } + seenUrlSet.add(asset.url); + return true; + }); +}; + +const inferPrefetchAs = (assetType: fileType): string => { + switch (assetType) { + case fileType.background: + case fileType.figure: + return 'image'; + case fileType.bgm: + case fileType.vocal: + return 'audio'; + case fileType.video: + return 'video'; + default: + return ''; + } +}; + +const prefetchByLinkElement = (asset: IAsset) => { + const newLink = document.createElement('link'); + newLink.setAttribute('rel', 'prefetch'); + newLink.setAttribute('href', asset.url); + const prefetchAs = inferPrefetchAs(asset.type); + if (prefetchAs) { + newLink.setAttribute('as', prefetchAs); + } + const head = document.getElementsByTagName('head'); + if (!head.length) { + return; + } + try { + head[0].appendChild(newLink); + } catch (e) { + logger.warn('预加载资源挂载 link 失败:', e); + } +}; + +const prefetchByServiceWorkerMessage = (assetUrl: string): boolean => { + if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) { + return false; + } + const controller = navigator.serviceWorker.controller; + if (!controller) { + return false; + } + try { + controller.postMessage({ + type: 'WEBGAL_PREFETCH_ASSET', + url: assetUrl, + }); + return true; + } catch (e) { + logger.warn('通过 Service Worker 发送预加载消息失败,将回退 link prefetch:', e); + return false; + } +}; + +const runAssetsPrefetchQueue = () => { + if (isAssetPrefetchQueueRunning || assetPrefetchQueue.length === 0) { + return; + } + isAssetPrefetchQueueRunning = true; + const nextAsset = assetPrefetchQueue.shift() as IAsset; + setTimeout(() => { + try { + const useServiceWorker = prefetchByServiceWorkerMessage(nextAsset.url); + if (!useServiceWorker) { + prefetchByLinkElement(nextAsset); + } + } catch (e) { + logger.warn(`预加载资源失败,将允许重试:${nextAsset.url}`, e); + WebGAL.sceneManager.settledAssets.delete(nextAsset.url); + } finally { + queuedAssetUrlSet.delete(nextAsset.url); + isAssetPrefetchQueueRunning = false; + runAssetsPrefetchQueue(); + } + }, ASSET_PREFETCH_INTERVAL_MS); +}; + +/** + * 清理 中的 prefetch link 元素,在切换场景时调用以避免累积。 + */ +export const clearPrefetchLinks = () => { + const head = document.getElementsByTagName('head')[0]; + if (!head) return; + const links = head.querySelectorAll('link[rel="prefetch"]'); + links.forEach((link) => link.remove()); +}; /** * 预加载函数 * @param assetList 场景资源列表 */ -export const assetsPrefetcher = (assetList: Array) => { +export const assetsPrefetcher = (assetList: Array, options: IAssetsPrefetcherOptions = {}) => { // @ts-ignore // 未必要移除,加载到内存里也有用 // if (window?.isElectron) { // return; // } - - for (const asset of assetList) { + const filteredAssetList = uniqueAssetsByUrl(assetList).filter((asset) => { + if (options.ignoreLineGate) { + return true; + } + return asset.lineNumber <= INITIAL_PARSE_LINE_LOOKAHEAD; + }); + for (const asset of filteredAssetList) { // 判断是否已经存在 - const hasPrefetch = WebGAL.sceneManager.settledAssets.includes(asset.url); + const hasPrefetch = WebGAL.sceneManager.settledAssets.has(asset.url) || queuedAssetUrlSet.has(asset.url); if (hasPrefetch) { logger.debug(`该资源${asset.url}已在预加载列表中,无需重复加载`); } else { - const newLink = document.createElement('link'); - newLink.setAttribute('rel', 'prefetch'); - newLink.setAttribute('href', asset.url); - const head = document.getElementsByTagName('head'); - if (head.length) { - try { - head[0].appendChild(newLink); - } catch (e) { - console.log('预加载出错', e); - } - } - WebGAL.sceneManager.settledAssets.push(asset.url); + logger.info(`现在预加载资源${asset.url},触发行号:${asset.lineNumber}`); + WebGAL.sceneManager.settledAssets.add(asset.url); + queuedAssetUrlSet.add(asset.url); + assetPrefetchQueue.push(asset); + runAssetsPrefetchQueue(); } } }; diff --git a/packages/webgal/src/Core/util/prefetcher/progressPrefetcher.ts b/packages/webgal/src/Core/util/prefetcher/progressPrefetcher.ts new file mode 100644 index 000000000..78157a3f7 --- /dev/null +++ b/packages/webgal/src/Core/util/prefetcher/progressPrefetcher.ts @@ -0,0 +1,57 @@ +import { IScene } from '@/Core/controller/scene/sceneInterface'; +import { assetsPrefetcher } from '@/Core/util/prefetcher/assetsPrefetcher'; +import { scenePrefetcher } from '@/Core/util/prefetcher/scenePrefetcher'; +import { WebGAL } from '@/Core/WebGAL'; + +const PROGRESS_ASSET_LOOKAHEAD = 20; +const PROGRESS_SUB_SCENE_LOOKAHEAD = 36; +let lastProgressPrefetchMark = ''; + +const uniqueAssetsByUrl = (scene: IScene, startLine: number, lookahead: number) => { + const assetMap = new Map(); + for (const sentence of scene.sentenceList.slice(startLine, startLine + lookahead + 1)) { + for (const asset of sentence.sentenceAssets) { + if (asset.url && !assetMap.has(asset.url)) { + assetMap.set(asset.url, asset); + } + } + } + return [...assetMap.values()]; +}; + +const uniqueSubScenes = (scene: IScene, startLine: number, lookahead: number) => { + const sceneSet = new Set(); + for (const sentence of scene.sentenceList.slice(startLine, startLine + lookahead + 1)) { + for (const subScene of sentence.subScene) { + if (subScene) { + sceneSet.add(subScene); + } + } + } + return [...sceneSet]; +}; + +export const prefetchSceneByProgress = (scene: IScene, currentSentenceId: number, force = false) => { + if (!scene.sceneUrl) { + return; + } + const mark = `${scene.sceneUrl}#${currentSentenceId}`; + if (!force && mark === lastProgressPrefetchMark) { + return; + } + lastProgressPrefetchMark = mark; + const startLine = Math.max(0, currentSentenceId); + const nextAssets = uniqueAssetsByUrl(scene, startLine, PROGRESS_ASSET_LOOKAHEAD); + const nextSubScenes = uniqueSubScenes(scene, startLine, PROGRESS_SUB_SCENE_LOOKAHEAD); + if (nextAssets.length > 0) { + assetsPrefetcher(nextAssets, { ignoreLineGate: true }); + } + if (nextSubScenes.length > 0) { + scenePrefetcher(nextSubScenes); + } +}; + +export const prefetchCurrentSceneByProgress = (force = false) => { + const { currentScene, currentSentenceId } = WebGAL.sceneManager.sceneData; + prefetchSceneByProgress(currentScene, currentSentenceId, force); +}; diff --git a/packages/webgal/src/Core/util/prefetcher/scenePrefetcher.ts b/packages/webgal/src/Core/util/prefetcher/scenePrefetcher.ts index a09822b7d..30eee280b 100644 --- a/packages/webgal/src/Core/util/prefetcher/scenePrefetcher.ts +++ b/packages/webgal/src/Core/util/prefetcher/scenePrefetcher.ts @@ -8,15 +8,51 @@ import { logger } from '@/Core/util/logger'; import { WebGAL } from '@/Core/WebGAL'; +const SCENE_PREFETCH_INTERVAL_MS = 320; +const scenePrefetchQueue: Array = []; +const queuedSceneUrlSet = new Set(); +let isScenePrefetchQueueRunning = false; + +const uniqueSceneUrls = (sceneList: Array) => [...new Set(sceneList.filter((sceneUrl) => !!sceneUrl))]; + +const runScenePrefetchQueue = () => { + if (isScenePrefetchQueueRunning || scenePrefetchQueue.length === 0) { + return; + } + isScenePrefetchQueueRunning = true; + const sceneUrl = scenePrefetchQueue.shift() as string; + setTimeout(async () => { + if (WebGAL.sceneManager.settledScenes.has(sceneUrl)) { + queuedSceneUrlSet.delete(sceneUrl); + isScenePrefetchQueueRunning = false; + runScenePrefetchQueue(); + return; + } + WebGAL.sceneManager.settledScenes.add(sceneUrl); + queuedSceneUrlSet.delete(sceneUrl); + try { + logger.info(`现在预加载场景${sceneUrl}`); + const rawScene = await sceneFetcher(sceneUrl); + // 注意:这里只做深度 1。sceneParser 内部会触发 assetsPrefetcher, + // 并只预加载该子场景前 N 行资源(由 assetsPrefetcher 的 line gate 控制)。 + sceneParser(rawScene, sceneUrl, sceneUrl); + } catch (e) { + logger.error(`场景预加载失败:${sceneUrl}`, e); + WebGAL.sceneManager.settledScenes.delete(sceneUrl); + } finally { + isScenePrefetchQueueRunning = false; + runScenePrefetchQueue(); + } + }, SCENE_PREFETCH_INTERVAL_MS); +}; + export const scenePrefetcher = (sceneList: Array): void => { - for (const e of sceneList) { - if (!WebGAL.sceneManager.settledScenes.includes(e)) { - logger.info(`现在预加载场景${e}`); - sceneFetcher(e).then((r) => { - sceneParser(r, e, e); - }); - } else { - logger.warn(`场景${e}已经加载过,无需再次加载`); + for (const sceneUrl of uniqueSceneUrls(sceneList)) { + if (WebGAL.sceneManager.settledScenes.has(sceneUrl) || queuedSceneUrlSet.has(sceneUrl)) { + continue; } + queuedSceneUrlSet.add(sceneUrl); + scenePrefetchQueue.push(sceneUrl); } + runScenePrefetchQueue(); };