瀏覽代碼

Deploying to gh-pages from @ godotengine/godot-demo-projects@bf4d1038d623c355f3b49e613a2c9b686eebb312 🚀

aaronfranke 1 月之前
當前提交
f98c3f5e53
共有 100 個文件被更改,包括 4232 次插入0 次删除
  1. 0 0
      .nojekyll
  2. 二進制
      2d/bullet_shower/index.144x144.png
  3. 二進制
      2d/bullet_shower/index.180x180.png
  4. 二進制
      2d/bullet_shower/index.512x512.png
  5. 二進制
      2d/bullet_shower/index.apple-touch-icon.png
  6. 213 0
      2d/bullet_shower/index.audio.worklet.js
  7. 200 0
      2d/bullet_shower/index.html
  8. 二進制
      2d/bullet_shower/index.icon.png
  9. 8 0
      2d/bullet_shower/index.js
  10. 1 0
      2d/bullet_shower/index.manifest.json
  11. 41 0
      2d/bullet_shower/index.offline.html
  12. 二進制
      2d/bullet_shower/index.pck
  13. 二進制
      2d/bullet_shower/index.png
  14. 166 0
      2d/bullet_shower/index.service.worker.js
  15. 1 0
      2d/bullet_shower/index.wasm
  16. 5 0
      2d/bullet_shower/index.worker.js
  17. 二進制
      2d/custom_drawing/index.144x144.png
  18. 二進制
      2d/custom_drawing/index.180x180.png
  19. 二進制
      2d/custom_drawing/index.512x512.png
  20. 二進制
      2d/custom_drawing/index.apple-touch-icon.png
  21. 213 0
      2d/custom_drawing/index.audio.worklet.js
  22. 200 0
      2d/custom_drawing/index.html
  23. 二進制
      2d/custom_drawing/index.icon.png
  24. 8 0
      2d/custom_drawing/index.js
  25. 1 0
      2d/custom_drawing/index.manifest.json
  26. 41 0
      2d/custom_drawing/index.offline.html
  27. 二進制
      2d/custom_drawing/index.pck
  28. 二進制
      2d/custom_drawing/index.png
  29. 166 0
      2d/custom_drawing/index.service.worker.js
  30. 1 0
      2d/custom_drawing/index.wasm
  31. 5 0
      2d/custom_drawing/index.worker.js
  32. 二進制
      2d/dodge_the_creeps/index.144x144.png
  33. 二進制
      2d/dodge_the_creeps/index.180x180.png
  34. 二進制
      2d/dodge_the_creeps/index.512x512.png
  35. 二進制
      2d/dodge_the_creeps/index.apple-touch-icon.png
  36. 213 0
      2d/dodge_the_creeps/index.audio.worklet.js
  37. 200 0
      2d/dodge_the_creeps/index.html
  38. 二進制
      2d/dodge_the_creeps/index.icon.png
  39. 8 0
      2d/dodge_the_creeps/index.js
  40. 1 0
      2d/dodge_the_creeps/index.manifest.json
  41. 41 0
      2d/dodge_the_creeps/index.offline.html
  42. 二進制
      2d/dodge_the_creeps/index.pck
  43. 二進制
      2d/dodge_the_creeps/index.png
  44. 166 0
      2d/dodge_the_creeps/index.service.worker.js
  45. 1 0
      2d/dodge_the_creeps/index.wasm
  46. 5 0
      2d/dodge_the_creeps/index.worker.js
  47. 二進制
      2d/dynamic_tilemap_layers/index.144x144.png
  48. 二進制
      2d/dynamic_tilemap_layers/index.180x180.png
  49. 二進制
      2d/dynamic_tilemap_layers/index.512x512.png
  50. 二進制
      2d/dynamic_tilemap_layers/index.apple-touch-icon.png
  51. 213 0
      2d/dynamic_tilemap_layers/index.audio.worklet.js
  52. 200 0
      2d/dynamic_tilemap_layers/index.html
  53. 二進制
      2d/dynamic_tilemap_layers/index.icon.png
  54. 8 0
      2d/dynamic_tilemap_layers/index.js
  55. 1 0
      2d/dynamic_tilemap_layers/index.manifest.json
  56. 41 0
      2d/dynamic_tilemap_layers/index.offline.html
  57. 二進制
      2d/dynamic_tilemap_layers/index.pck
  58. 二進制
      2d/dynamic_tilemap_layers/index.png
  59. 166 0
      2d/dynamic_tilemap_layers/index.service.worker.js
  60. 1 0
      2d/dynamic_tilemap_layers/index.wasm
  61. 5 0
      2d/dynamic_tilemap_layers/index.worker.js
  62. 二進制
      2d/finite_state_machine/index.144x144.png
  63. 二進制
      2d/finite_state_machine/index.180x180.png
  64. 二進制
      2d/finite_state_machine/index.512x512.png
  65. 二進制
      2d/finite_state_machine/index.apple-touch-icon.png
  66. 213 0
      2d/finite_state_machine/index.audio.worklet.js
  67. 200 0
      2d/finite_state_machine/index.html
  68. 二進制
      2d/finite_state_machine/index.icon.png
  69. 8 0
      2d/finite_state_machine/index.js
  70. 1 0
      2d/finite_state_machine/index.manifest.json
  71. 41 0
      2d/finite_state_machine/index.offline.html
  72. 二進制
      2d/finite_state_machine/index.pck
  73. 二進制
      2d/finite_state_machine/index.png
  74. 166 0
      2d/finite_state_machine/index.service.worker.js
  75. 1 0
      2d/finite_state_machine/index.wasm
  76. 5 0
      2d/finite_state_machine/index.worker.js
  77. 二進制
      2d/hexagonal_map/index.144x144.png
  78. 二進制
      2d/hexagonal_map/index.180x180.png
  79. 二進制
      2d/hexagonal_map/index.512x512.png
  80. 二進制
      2d/hexagonal_map/index.apple-touch-icon.png
  81. 213 0
      2d/hexagonal_map/index.audio.worklet.js
  82. 200 0
      2d/hexagonal_map/index.html
  83. 二進制
      2d/hexagonal_map/index.icon.png
  84. 8 0
      2d/hexagonal_map/index.js
  85. 1 0
      2d/hexagonal_map/index.manifest.json
  86. 41 0
      2d/hexagonal_map/index.offline.html
  87. 二進制
      2d/hexagonal_map/index.pck
  88. 二進制
      2d/hexagonal_map/index.png
  89. 166 0
      2d/hexagonal_map/index.service.worker.js
  90. 1 0
      2d/hexagonal_map/index.wasm
  91. 5 0
      2d/hexagonal_map/index.worker.js
  92. 二進制
      2d/instancing/index.144x144.png
  93. 二進制
      2d/instancing/index.180x180.png
  94. 二進制
      2d/instancing/index.512x512.png
  95. 二進制
      2d/instancing/index.apple-touch-icon.png
  96. 213 0
      2d/instancing/index.audio.worklet.js
  97. 200 0
      2d/instancing/index.html
  98. 二進制
      2d/instancing/index.icon.png
  99. 8 0
      2d/instancing/index.js
  100. 1 0
      2d/instancing/index.manifest.json

+ 0 - 0
.nojekyll


二進制
2d/bullet_shower/index.144x144.png


二進制
2d/bullet_shower/index.180x180.png


二進制
2d/bullet_shower/index.512x512.png


二進制
2d/bullet_shower/index.apple-touch-icon.png


+ 213 - 0
2d/bullet_shower/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/bullet_shower/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Bullet Shower</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":24352,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/bullet_shower/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/bullet_shower/index.js


+ 1 - 0
2d/bullet_shower/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Bullet Shower","orientation":"landscape","start_url":"./index.html"}

+ 41 - 0
2d/bullet_shower/index.offline.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>You are offline</title>
+		<style>
+html {
+	background-color: #000000;
+	color: #ffffff;
+}
+
+body {
+	font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+	margin: 2rem;
+}
+
+p {
+	margin-block: 1rem;
+}
+
+button {
+	display: block;
+	padding: 1rem 2rem;
+	margin: 3rem auto 0;
+}
+		</style>
+	</head>
+	<body>
+		<h1>You are offline</h1>
+		<p>This application requires an Internet connection to run for the first time.</p>
+		<p>Press the button below to try reloading:</p>
+		<button type="button">Reload</button>
+		<script>
+document.querySelector('button').addEventListener('click', () => {
+	window.location.reload();
+});
+		</script>
+	</body>
+</html>

二進制
2d/bullet_shower/index.pck


二進制
2d/bullet_shower/index.png


+ 166 - 0
2d/bullet_shower/index.service.worker.js

@@ -0,0 +1,166 @@
+// This service worker is required to expose an exported Godot project as a
+// Progressive Web App. It provides an offline fallback page telling the user
+// that they need an Internet connection to run the project if desired.
+// Incrementing CACHE_VERSION will kick off the install event and force
+// previously cached resources to be updated from the network.
+/** @type {string} */
+const CACHE_VERSION = '1754353283|4272132';
+/** @type {string} */
+const CACHE_PREFIX = 'Bullet Shower-sw-cache-';
+const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
+/** @type {string} */
+const OFFLINE_URL = 'index.offline.html';
+/** @type {boolean} */
+const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = true;
+// Files that will be cached on load.
+/** @type {string[]} */
+const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
+// Files that we might not want the user to preload, and will only be cached on first load.
+/** @type {string[]} */
+const CACHABLE_FILES = ["index.wasm","index.pck"];
+const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
+
+self.addEventListener('install', (event) => {
+	event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
+});
+
+self.addEventListener('activate', (event) => {
+	event.waitUntil(caches.keys().then(
+		function (keys) {
+			// Remove old caches.
+			return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
+		}
+	).then(function () {
+		// Enable navigation preload if available.
+		return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
+	}));
+});
+
+/**
+ * Ensures that the response has the correct COEP/COOP headers
+ * @param {Response} response
+ * @returns {Response}
+ */
+function ensureCrossOriginIsolationHeaders(response) {
+	if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
+		&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
+		return response;
+	}
+
+	const crossOriginIsolatedHeaders = new Headers(response.headers);
+	crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
+	crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
+	const newResponse = new Response(response.body, {
+		status: response.status,
+		statusText: response.statusText,
+		headers: crossOriginIsolatedHeaders,
+	});
+
+	return newResponse;
+}
+
+/**
+ * Calls fetch and cache the result if it is cacheable
+ * @param {FetchEvent} event
+ * @param {Cache} cache
+ * @param {boolean} isCacheable
+ * @returns {Response}
+ */
+async function fetchAndCache(event, cache, isCacheable) {
+	// Use the preloaded response, if it's there
+	/** @type { Response } */
+	let response = await event.preloadResponse;
+	if (response == null) {
+		// Or, go over network.
+		response = await self.fetch(event.request);
+	}
+
+	if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+		response = ensureCrossOriginIsolationHeaders(response);
+	}
+
+	if (isCacheable) {
+		// And update the cache
+		cache.put(event.request, response.clone());
+	}
+
+	return response;
+}
+
+self.addEventListener(
+	'fetch',
+	/**
+	 * Triggered on fetch
+	 * @param {FetchEvent} event
+	 */
+	(event) => {
+		const isNavigate = event.request.mode === 'navigate';
+		const url = event.request.url || '';
+		const referrer = event.request.referrer || '';
+		const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
+		const local = url.startsWith(base) ? url.replace(base, '') : '';
+		const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
+		if (isNavigate || isCachable) {
+			event.respondWith((async () => {
+				// Try to use cache first
+				const cache = await caches.open(CACHE_NAME);
+				if (isNavigate) {
+					// Check if we have full cache during HTML page request.
+					/** @type {Response[]} */
+					const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
+					const missing = fullCache.some((v) => v === undefined);
+					if (missing) {
+						try {
+							// Try network if some cached file is missing (so we can display offline page in case).
+							const response = await fetchAndCache(event, cache, isCachable);
+							return response;
+						} catch (e) {
+							// And return the hopefully always cached offline page in case of network failure.
+							console.error('Network error: ', e); // eslint-disable-line no-console
+							return caches.match(OFFLINE_URL);
+						}
+					}
+				}
+				let cached = await cache.match(event.request);
+				if (cached != null) {
+					if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+						cached = ensureCrossOriginIsolationHeaders(cached);
+					}
+					return cached;
+				}
+				// Try network if don't have it in cache.
+				const response = await fetchAndCache(event, cache, isCachable);
+				return response;
+			})());
+		} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+			event.respondWith((async () => {
+				let response = await fetch(event.request);
+				response = ensureCrossOriginIsolationHeaders(response);
+				return response;
+			})());
+		}
+	}
+);
+
+self.addEventListener('message', (event) => {
+	// No cross origin
+	if (event.origin !== self.origin) {
+		return;
+	}
+	const id = event.source.id || '';
+	const msg = event.data || '';
+	// Ensure it's one of our clients.
+	self.clients.get(id).then(function (client) {
+		if (!client) {
+			return; // Not a valid client.
+		}
+		if (msg === 'claim') {
+			self.skipWaiting().then(() => self.clients.claim());
+		} else if (msg === 'clear') {
+			caches.delete(CACHE_NAME);
+		} else if (msg === 'update') {
+			self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
+		}
+	});
+});
+

+ 1 - 0
2d/bullet_shower/index.wasm

@@ -0,0 +1 @@
+../../index.wasm

+ 5 - 0
2d/bullet_shower/index.worker.js

@@ -0,0 +1,5 @@
+// This file is no longer used by emscripten and has been created as a placeholder
+// to allow build systems to transition away from depending on it.
+//
+// Future versions of emscripten will likely stop generating this file at all.
+throw new Error('Dummy worker.js file should never be used');

二進制
2d/custom_drawing/index.144x144.png


二進制
2d/custom_drawing/index.180x180.png


二進制
2d/custom_drawing/index.512x512.png


二進制
2d/custom_drawing/index.apple-touch-icon.png


+ 213 - 0
2d/custom_drawing/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/custom_drawing/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Custom Drawing in 2D</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":23936,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/custom_drawing/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/custom_drawing/index.js


+ 1 - 0
2d/custom_drawing/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Custom Drawing in 2D","orientation":"landscape","start_url":"./index.html"}

+ 41 - 0
2d/custom_drawing/index.offline.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>You are offline</title>
+		<style>
+html {
+	background-color: #000000;
+	color: #ffffff;
+}
+
+body {
+	font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+	margin: 2rem;
+}
+
+p {
+	margin-block: 1rem;
+}
+
+button {
+	display: block;
+	padding: 1rem 2rem;
+	margin: 3rem auto 0;
+}
+		</style>
+	</head>
+	<body>
+		<h1>You are offline</h1>
+		<p>This application requires an Internet connection to run for the first time.</p>
+		<p>Press the button below to try reloading:</p>
+		<button type="button">Reload</button>
+		<script>
+document.querySelector('button').addEventListener('click', () => {
+	window.location.reload();
+});
+		</script>
+	</body>
+</html>

二進制
2d/custom_drawing/index.pck


二進制
2d/custom_drawing/index.png


+ 166 - 0
2d/custom_drawing/index.service.worker.js

@@ -0,0 +1,166 @@
+// This service worker is required to expose an exported Godot project as a
+// Progressive Web App. It provides an offline fallback page telling the user
+// that they need an Internet connection to run the project if desired.
+// Incrementing CACHE_VERSION will kick off the install event and force
+// previously cached resources to be updated from the network.
+/** @type {string} */
+const CACHE_VERSION = '1754353287|3572420';
+/** @type {string} */
+const CACHE_PREFIX = 'Custom Drawing i-sw-cache-';
+const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
+/** @type {string} */
+const OFFLINE_URL = 'index.offline.html';
+/** @type {boolean} */
+const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = true;
+// Files that will be cached on load.
+/** @type {string[]} */
+const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
+// Files that we might not want the user to preload, and will only be cached on first load.
+/** @type {string[]} */
+const CACHABLE_FILES = ["index.wasm","index.pck"];
+const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
+
+self.addEventListener('install', (event) => {
+	event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
+});
+
+self.addEventListener('activate', (event) => {
+	event.waitUntil(caches.keys().then(
+		function (keys) {
+			// Remove old caches.
+			return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
+		}
+	).then(function () {
+		// Enable navigation preload if available.
+		return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
+	}));
+});
+
+/**
+ * Ensures that the response has the correct COEP/COOP headers
+ * @param {Response} response
+ * @returns {Response}
+ */
+function ensureCrossOriginIsolationHeaders(response) {
+	if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
+		&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
+		return response;
+	}
+
+	const crossOriginIsolatedHeaders = new Headers(response.headers);
+	crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
+	crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
+	const newResponse = new Response(response.body, {
+		status: response.status,
+		statusText: response.statusText,
+		headers: crossOriginIsolatedHeaders,
+	});
+
+	return newResponse;
+}
+
+/**
+ * Calls fetch and cache the result if it is cacheable
+ * @param {FetchEvent} event
+ * @param {Cache} cache
+ * @param {boolean} isCacheable
+ * @returns {Response}
+ */
+async function fetchAndCache(event, cache, isCacheable) {
+	// Use the preloaded response, if it's there
+	/** @type { Response } */
+	let response = await event.preloadResponse;
+	if (response == null) {
+		// Or, go over network.
+		response = await self.fetch(event.request);
+	}
+
+	if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+		response = ensureCrossOriginIsolationHeaders(response);
+	}
+
+	if (isCacheable) {
+		// And update the cache
+		cache.put(event.request, response.clone());
+	}
+
+	return response;
+}
+
+self.addEventListener(
+	'fetch',
+	/**
+	 * Triggered on fetch
+	 * @param {FetchEvent} event
+	 */
+	(event) => {
+		const isNavigate = event.request.mode === 'navigate';
+		const url = event.request.url || '';
+		const referrer = event.request.referrer || '';
+		const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
+		const local = url.startsWith(base) ? url.replace(base, '') : '';
+		const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
+		if (isNavigate || isCachable) {
+			event.respondWith((async () => {
+				// Try to use cache first
+				const cache = await caches.open(CACHE_NAME);
+				if (isNavigate) {
+					// Check if we have full cache during HTML page request.
+					/** @type {Response[]} */
+					const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
+					const missing = fullCache.some((v) => v === undefined);
+					if (missing) {
+						try {
+							// Try network if some cached file is missing (so we can display offline page in case).
+							const response = await fetchAndCache(event, cache, isCachable);
+							return response;
+						} catch (e) {
+							// And return the hopefully always cached offline page in case of network failure.
+							console.error('Network error: ', e); // eslint-disable-line no-console
+							return caches.match(OFFLINE_URL);
+						}
+					}
+				}
+				let cached = await cache.match(event.request);
+				if (cached != null) {
+					if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+						cached = ensureCrossOriginIsolationHeaders(cached);
+					}
+					return cached;
+				}
+				// Try network if don't have it in cache.
+				const response = await fetchAndCache(event, cache, isCachable);
+				return response;
+			})());
+		} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+			event.respondWith((async () => {
+				let response = await fetch(event.request);
+				response = ensureCrossOriginIsolationHeaders(response);
+				return response;
+			})());
+		}
+	}
+);
+
+self.addEventListener('message', (event) => {
+	// No cross origin
+	if (event.origin !== self.origin) {
+		return;
+	}
+	const id = event.source.id || '';
+	const msg = event.data || '';
+	// Ensure it's one of our clients.
+	self.clients.get(id).then(function (client) {
+		if (!client) {
+			return; // Not a valid client.
+		}
+		if (msg === 'claim') {
+			self.skipWaiting().then(() => self.clients.claim());
+		} else if (msg === 'clear') {
+			caches.delete(CACHE_NAME);
+		} else if (msg === 'update') {
+			self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
+		}
+	});
+});
+

+ 1 - 0
2d/custom_drawing/index.wasm

@@ -0,0 +1 @@
+../../index.wasm

+ 5 - 0
2d/custom_drawing/index.worker.js

@@ -0,0 +1,5 @@
+// This file is no longer used by emscripten and has been created as a placeholder
+// to allow build systems to transition away from depending on it.
+//
+// Future versions of emscripten will likely stop generating this file at all.
+throw new Error('Dummy worker.js file should never be used');

二進制
2d/dodge_the_creeps/index.144x144.png


二進制
2d/dodge_the_creeps/index.180x180.png


二進制
2d/dodge_the_creeps/index.512x512.png


二進制
2d/dodge_the_creeps/index.apple-touch-icon.png


+ 213 - 0
2d/dodge_the_creeps/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/dodge_the_creeps/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Dodge the Creeps</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":1932208,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/dodge_the_creeps/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/dodge_the_creeps/index.js


+ 1 - 0
2d/dodge_the_creeps/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Dodge the Creeps","orientation":"landscape","start_url":"./index.html"}

+ 41 - 0
2d/dodge_the_creeps/index.offline.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>You are offline</title>
+		<style>
+html {
+	background-color: #000000;
+	color: #ffffff;
+}
+
+body {
+	font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+	margin: 2rem;
+}
+
+p {
+	margin-block: 1rem;
+}
+
+button {
+	display: block;
+	padding: 1rem 2rem;
+	margin: 3rem auto 0;
+}
+		</style>
+	</head>
+	<body>
+		<h1>You are offline</h1>
+		<p>This application requires an Internet connection to run for the first time.</p>
+		<p>Press the button below to try reloading:</p>
+		<button type="button">Reload</button>
+		<script>
+document.querySelector('button').addEventListener('click', () => {
+	window.location.reload();
+});
+		</script>
+	</body>
+</html>

二進制
2d/dodge_the_creeps/index.pck


二進制
2d/dodge_the_creeps/index.png


+ 166 - 0
2d/dodge_the_creeps/index.service.worker.js

@@ -0,0 +1,166 @@
+// This service worker is required to expose an exported Godot project as a
+// Progressive Web App. It provides an offline fallback page telling the user
+// that they need an Internet connection to run the project if desired.
+// Incrementing CACHE_VERSION will kick off the install event and force
+// previously cached resources to be updated from the network.
+/** @type {string} */
+const CACHE_VERSION = '1754353291|3599835';
+/** @type {string} */
+const CACHE_PREFIX = 'Dodge the Creeps-sw-cache-';
+const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
+/** @type {string} */
+const OFFLINE_URL = 'index.offline.html';
+/** @type {boolean} */
+const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = true;
+// Files that will be cached on load.
+/** @type {string[]} */
+const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
+// Files that we might not want the user to preload, and will only be cached on first load.
+/** @type {string[]} */
+const CACHABLE_FILES = ["index.wasm","index.pck"];
+const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
+
+self.addEventListener('install', (event) => {
+	event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
+});
+
+self.addEventListener('activate', (event) => {
+	event.waitUntil(caches.keys().then(
+		function (keys) {
+			// Remove old caches.
+			return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
+		}
+	).then(function () {
+		// Enable navigation preload if available.
+		return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
+	}));
+});
+
+/**
+ * Ensures that the response has the correct COEP/COOP headers
+ * @param {Response} response
+ * @returns {Response}
+ */
+function ensureCrossOriginIsolationHeaders(response) {
+	if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
+		&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
+		return response;
+	}
+
+	const crossOriginIsolatedHeaders = new Headers(response.headers);
+	crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
+	crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
+	const newResponse = new Response(response.body, {
+		status: response.status,
+		statusText: response.statusText,
+		headers: crossOriginIsolatedHeaders,
+	});
+
+	return newResponse;
+}
+
+/**
+ * Calls fetch and cache the result if it is cacheable
+ * @param {FetchEvent} event
+ * @param {Cache} cache
+ * @param {boolean} isCacheable
+ * @returns {Response}
+ */
+async function fetchAndCache(event, cache, isCacheable) {
+	// Use the preloaded response, if it's there
+	/** @type { Response } */
+	let response = await event.preloadResponse;
+	if (response == null) {
+		// Or, go over network.
+		response = await self.fetch(event.request);
+	}
+
+	if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+		response = ensureCrossOriginIsolationHeaders(response);
+	}
+
+	if (isCacheable) {
+		// And update the cache
+		cache.put(event.request, response.clone());
+	}
+
+	return response;
+}
+
+self.addEventListener(
+	'fetch',
+	/**
+	 * Triggered on fetch
+	 * @param {FetchEvent} event
+	 */
+	(event) => {
+		const isNavigate = event.request.mode === 'navigate';
+		const url = event.request.url || '';
+		const referrer = event.request.referrer || '';
+		const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
+		const local = url.startsWith(base) ? url.replace(base, '') : '';
+		const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
+		if (isNavigate || isCachable) {
+			event.respondWith((async () => {
+				// Try to use cache first
+				const cache = await caches.open(CACHE_NAME);
+				if (isNavigate) {
+					// Check if we have full cache during HTML page request.
+					/** @type {Response[]} */
+					const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
+					const missing = fullCache.some((v) => v === undefined);
+					if (missing) {
+						try {
+							// Try network if some cached file is missing (so we can display offline page in case).
+							const response = await fetchAndCache(event, cache, isCachable);
+							return response;
+						} catch (e) {
+							// And return the hopefully always cached offline page in case of network failure.
+							console.error('Network error: ', e); // eslint-disable-line no-console
+							return caches.match(OFFLINE_URL);
+						}
+					}
+				}
+				let cached = await cache.match(event.request);
+				if (cached != null) {
+					if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+						cached = ensureCrossOriginIsolationHeaders(cached);
+					}
+					return cached;
+				}
+				// Try network if don't have it in cache.
+				const response = await fetchAndCache(event, cache, isCachable);
+				return response;
+			})());
+		} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+			event.respondWith((async () => {
+				let response = await fetch(event.request);
+				response = ensureCrossOriginIsolationHeaders(response);
+				return response;
+			})());
+		}
+	}
+);
+
+self.addEventListener('message', (event) => {
+	// No cross origin
+	if (event.origin !== self.origin) {
+		return;
+	}
+	const id = event.source.id || '';
+	const msg = event.data || '';
+	// Ensure it's one of our clients.
+	self.clients.get(id).then(function (client) {
+		if (!client) {
+			return; // Not a valid client.
+		}
+		if (msg === 'claim') {
+			self.skipWaiting().then(() => self.clients.claim());
+		} else if (msg === 'clear') {
+			caches.delete(CACHE_NAME);
+		} else if (msg === 'update') {
+			self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
+		}
+	});
+});
+

+ 1 - 0
2d/dodge_the_creeps/index.wasm

@@ -0,0 +1 @@
+../../index.wasm

+ 5 - 0
2d/dodge_the_creeps/index.worker.js

@@ -0,0 +1,5 @@
+// This file is no longer used by emscripten and has been created as a placeholder
+// to allow build systems to transition away from depending on it.
+//
+// Future versions of emscripten will likely stop generating this file at all.
+throw new Error('Dummy worker.js file should never be used');

二進制
2d/dynamic_tilemap_layers/index.144x144.png


二進制
2d/dynamic_tilemap_layers/index.180x180.png


二進制
2d/dynamic_tilemap_layers/index.512x512.png


二進制
2d/dynamic_tilemap_layers/index.apple-touch-icon.png


+ 213 - 0
2d/dynamic_tilemap_layers/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/dynamic_tilemap_layers/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Dynamic TileMap Layers</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":23968,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/dynamic_tilemap_layers/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/dynamic_tilemap_layers/index.js


+ 1 - 0
2d/dynamic_tilemap_layers/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Dynamic TileMap Layers","orientation":"landscape","start_url":"./index.html"}

+ 41 - 0
2d/dynamic_tilemap_layers/index.offline.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>You are offline</title>
+		<style>
+html {
+	background-color: #000000;
+	color: #ffffff;
+}
+
+body {
+	font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+	margin: 2rem;
+}
+
+p {
+	margin-block: 1rem;
+}
+
+button {
+	display: block;
+	padding: 1rem 2rem;
+	margin: 3rem auto 0;
+}
+		</style>
+	</head>
+	<body>
+		<h1>You are offline</h1>
+		<p>This application requires an Internet connection to run for the first time.</p>
+		<p>Press the button below to try reloading:</p>
+		<button type="button">Reload</button>
+		<script>
+document.querySelector('button').addEventListener('click', () => {
+	window.location.reload();
+});
+		</script>
+	</body>
+</html>

二進制
2d/dynamic_tilemap_layers/index.pck


二進制
2d/dynamic_tilemap_layers/index.png


+ 166 - 0
2d/dynamic_tilemap_layers/index.service.worker.js

@@ -0,0 +1,166 @@
+// This service worker is required to expose an exported Godot project as a
+// Progressive Web App. It provides an offline fallback page telling the user
+// that they need an Internet connection to run the project if desired.
+// Incrementing CACHE_VERSION will kick off the install event and force
+// previously cached resources to be updated from the network.
+/** @type {string} */
+const CACHE_VERSION = '1754353296|3739787';
+/** @type {string} */
+const CACHE_PREFIX = 'Dynamic TileMap -sw-cache-';
+const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
+/** @type {string} */
+const OFFLINE_URL = 'index.offline.html';
+/** @type {boolean} */
+const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = true;
+// Files that will be cached on load.
+/** @type {string[]} */
+const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
+// Files that we might not want the user to preload, and will only be cached on first load.
+/** @type {string[]} */
+const CACHABLE_FILES = ["index.wasm","index.pck"];
+const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
+
+self.addEventListener('install', (event) => {
+	event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
+});
+
+self.addEventListener('activate', (event) => {
+	event.waitUntil(caches.keys().then(
+		function (keys) {
+			// Remove old caches.
+			return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
+		}
+	).then(function () {
+		// Enable navigation preload if available.
+		return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
+	}));
+});
+
+/**
+ * Ensures that the response has the correct COEP/COOP headers
+ * @param {Response} response
+ * @returns {Response}
+ */
+function ensureCrossOriginIsolationHeaders(response) {
+	if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
+		&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
+		return response;
+	}
+
+	const crossOriginIsolatedHeaders = new Headers(response.headers);
+	crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
+	crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
+	const newResponse = new Response(response.body, {
+		status: response.status,
+		statusText: response.statusText,
+		headers: crossOriginIsolatedHeaders,
+	});
+
+	return newResponse;
+}
+
+/**
+ * Calls fetch and cache the result if it is cacheable
+ * @param {FetchEvent} event
+ * @param {Cache} cache
+ * @param {boolean} isCacheable
+ * @returns {Response}
+ */
+async function fetchAndCache(event, cache, isCacheable) {
+	// Use the preloaded response, if it's there
+	/** @type { Response } */
+	let response = await event.preloadResponse;
+	if (response == null) {
+		// Or, go over network.
+		response = await self.fetch(event.request);
+	}
+
+	if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+		response = ensureCrossOriginIsolationHeaders(response);
+	}
+
+	if (isCacheable) {
+		// And update the cache
+		cache.put(event.request, response.clone());
+	}
+
+	return response;
+}
+
+self.addEventListener(
+	'fetch',
+	/**
+	 * Triggered on fetch
+	 * @param {FetchEvent} event
+	 */
+	(event) => {
+		const isNavigate = event.request.mode === 'navigate';
+		const url = event.request.url || '';
+		const referrer = event.request.referrer || '';
+		const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
+		const local = url.startsWith(base) ? url.replace(base, '') : '';
+		const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
+		if (isNavigate || isCachable) {
+			event.respondWith((async () => {
+				// Try to use cache first
+				const cache = await caches.open(CACHE_NAME);
+				if (isNavigate) {
+					// Check if we have full cache during HTML page request.
+					/** @type {Response[]} */
+					const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
+					const missing = fullCache.some((v) => v === undefined);
+					if (missing) {
+						try {
+							// Try network if some cached file is missing (so we can display offline page in case).
+							const response = await fetchAndCache(event, cache, isCachable);
+							return response;
+						} catch (e) {
+							// And return the hopefully always cached offline page in case of network failure.
+							console.error('Network error: ', e); // eslint-disable-line no-console
+							return caches.match(OFFLINE_URL);
+						}
+					}
+				}
+				let cached = await cache.match(event.request);
+				if (cached != null) {
+					if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+						cached = ensureCrossOriginIsolationHeaders(cached);
+					}
+					return cached;
+				}
+				// Try network if don't have it in cache.
+				const response = await fetchAndCache(event, cache, isCachable);
+				return response;
+			})());
+		} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+			event.respondWith((async () => {
+				let response = await fetch(event.request);
+				response = ensureCrossOriginIsolationHeaders(response);
+				return response;
+			})());
+		}
+	}
+);
+
+self.addEventListener('message', (event) => {
+	// No cross origin
+	if (event.origin !== self.origin) {
+		return;
+	}
+	const id = event.source.id || '';
+	const msg = event.data || '';
+	// Ensure it's one of our clients.
+	self.clients.get(id).then(function (client) {
+		if (!client) {
+			return; // Not a valid client.
+		}
+		if (msg === 'claim') {
+			self.skipWaiting().then(() => self.clients.claim());
+		} else if (msg === 'clear') {
+			caches.delete(CACHE_NAME);
+		} else if (msg === 'update') {
+			self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
+		}
+	});
+});
+

+ 1 - 0
2d/dynamic_tilemap_layers/index.wasm

@@ -0,0 +1 @@
+../../index.wasm

+ 5 - 0
2d/dynamic_tilemap_layers/index.worker.js

@@ -0,0 +1,5 @@
+// This file is no longer used by emscripten and has been created as a placeholder
+// to allow build systems to transition away from depending on it.
+//
+// Future versions of emscripten will likely stop generating this file at all.
+throw new Error('Dummy worker.js file should never be used');

二進制
2d/finite_state_machine/index.144x144.png


二進制
2d/finite_state_machine/index.180x180.png


二進制
2d/finite_state_machine/index.512x512.png


二進制
2d/finite_state_machine/index.apple-touch-icon.png


+ 213 - 0
2d/finite_state_machine/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/finite_state_machine/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Hierarchical Finite State Machine</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":198304,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/finite_state_machine/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/finite_state_machine/index.js


+ 1 - 0
2d/finite_state_machine/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Hierarchical Finite State Machine","orientation":"landscape","start_url":"./index.html"}

+ 41 - 0
2d/finite_state_machine/index.offline.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>You are offline</title>
+		<style>
+html {
+	background-color: #000000;
+	color: #ffffff;
+}
+
+body {
+	font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+	margin: 2rem;
+}
+
+p {
+	margin-block: 1rem;
+}
+
+button {
+	display: block;
+	padding: 1rem 2rem;
+	margin: 3rem auto 0;
+}
+		</style>
+	</head>
+	<body>
+		<h1>You are offline</h1>
+		<p>This application requires an Internet connection to run for the first time.</p>
+		<p>Press the button below to try reloading:</p>
+		<button type="button">Reload</button>
+		<script>
+document.querySelector('button').addEventListener('click', () => {
+	window.location.reload();
+});
+		</script>
+	</body>
+</html>

二進制
2d/finite_state_machine/index.pck


二進制
2d/finite_state_machine/index.png


+ 166 - 0
2d/finite_state_machine/index.service.worker.js

@@ -0,0 +1,166 @@
+// This service worker is required to expose an exported Godot project as a
+// Progressive Web App. It provides an offline fallback page telling the user
+// that they need an Internet connection to run the project if desired.
+// Incrementing CACHE_VERSION will kick off the install event and force
+// previously cached resources to be updated from the network.
+/** @type {string} */
+const CACHE_VERSION = '1754353300|3605441';
+/** @type {string} */
+const CACHE_PREFIX = 'Hierarchical Fin-sw-cache-';
+const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
+/** @type {string} */
+const OFFLINE_URL = 'index.offline.html';
+/** @type {boolean} */
+const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = true;
+// Files that will be cached on load.
+/** @type {string[]} */
+const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
+// Files that we might not want the user to preload, and will only be cached on first load.
+/** @type {string[]} */
+const CACHABLE_FILES = ["index.wasm","index.pck"];
+const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
+
+self.addEventListener('install', (event) => {
+	event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
+});
+
+self.addEventListener('activate', (event) => {
+	event.waitUntil(caches.keys().then(
+		function (keys) {
+			// Remove old caches.
+			return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
+		}
+	).then(function () {
+		// Enable navigation preload if available.
+		return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
+	}));
+});
+
+/**
+ * Ensures that the response has the correct COEP/COOP headers
+ * @param {Response} response
+ * @returns {Response}
+ */
+function ensureCrossOriginIsolationHeaders(response) {
+	if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
+		&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
+		return response;
+	}
+
+	const crossOriginIsolatedHeaders = new Headers(response.headers);
+	crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
+	crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
+	const newResponse = new Response(response.body, {
+		status: response.status,
+		statusText: response.statusText,
+		headers: crossOriginIsolatedHeaders,
+	});
+
+	return newResponse;
+}
+
+/**
+ * Calls fetch and cache the result if it is cacheable
+ * @param {FetchEvent} event
+ * @param {Cache} cache
+ * @param {boolean} isCacheable
+ * @returns {Response}
+ */
+async function fetchAndCache(event, cache, isCacheable) {
+	// Use the preloaded response, if it's there
+	/** @type { Response } */
+	let response = await event.preloadResponse;
+	if (response == null) {
+		// Or, go over network.
+		response = await self.fetch(event.request);
+	}
+
+	if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+		response = ensureCrossOriginIsolationHeaders(response);
+	}
+
+	if (isCacheable) {
+		// And update the cache
+		cache.put(event.request, response.clone());
+	}
+
+	return response;
+}
+
+self.addEventListener(
+	'fetch',
+	/**
+	 * Triggered on fetch
+	 * @param {FetchEvent} event
+	 */
+	(event) => {
+		const isNavigate = event.request.mode === 'navigate';
+		const url = event.request.url || '';
+		const referrer = event.request.referrer || '';
+		const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
+		const local = url.startsWith(base) ? url.replace(base, '') : '';
+		const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
+		if (isNavigate || isCachable) {
+			event.respondWith((async () => {
+				// Try to use cache first
+				const cache = await caches.open(CACHE_NAME);
+				if (isNavigate) {
+					// Check if we have full cache during HTML page request.
+					/** @type {Response[]} */
+					const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
+					const missing = fullCache.some((v) => v === undefined);
+					if (missing) {
+						try {
+							// Try network if some cached file is missing (so we can display offline page in case).
+							const response = await fetchAndCache(event, cache, isCachable);
+							return response;
+						} catch (e) {
+							// And return the hopefully always cached offline page in case of network failure.
+							console.error('Network error: ', e); // eslint-disable-line no-console
+							return caches.match(OFFLINE_URL);
+						}
+					}
+				}
+				let cached = await cache.match(event.request);
+				if (cached != null) {
+					if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+						cached = ensureCrossOriginIsolationHeaders(cached);
+					}
+					return cached;
+				}
+				// Try network if don't have it in cache.
+				const response = await fetchAndCache(event, cache, isCachable);
+				return response;
+			})());
+		} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+			event.respondWith((async () => {
+				let response = await fetch(event.request);
+				response = ensureCrossOriginIsolationHeaders(response);
+				return response;
+			})());
+		}
+	}
+);
+
+self.addEventListener('message', (event) => {
+	// No cross origin
+	if (event.origin !== self.origin) {
+		return;
+	}
+	const id = event.source.id || '';
+	const msg = event.data || '';
+	// Ensure it's one of our clients.
+	self.clients.get(id).then(function (client) {
+		if (!client) {
+			return; // Not a valid client.
+		}
+		if (msg === 'claim') {
+			self.skipWaiting().then(() => self.clients.claim());
+		} else if (msg === 'clear') {
+			caches.delete(CACHE_NAME);
+		} else if (msg === 'update') {
+			self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
+		}
+	});
+});
+

+ 1 - 0
2d/finite_state_machine/index.wasm

@@ -0,0 +1 @@
+../../index.wasm

+ 5 - 0
2d/finite_state_machine/index.worker.js

@@ -0,0 +1,5 @@
+// This file is no longer used by emscripten and has been created as a placeholder
+// to allow build systems to transition away from depending on it.
+//
+// Future versions of emscripten will likely stop generating this file at all.
+throw new Error('Dummy worker.js file should never be used');

二進制
2d/hexagonal_map/index.144x144.png


二進制
2d/hexagonal_map/index.180x180.png


二進制
2d/hexagonal_map/index.512x512.png


二進制
2d/hexagonal_map/index.apple-touch-icon.png


+ 213 - 0
2d/hexagonal_map/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/hexagonal_map/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Hexagonal Game</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":498816,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/hexagonal_map/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/hexagonal_map/index.js


+ 1 - 0
2d/hexagonal_map/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Hexagonal Game","orientation":"landscape","start_url":"./index.html"}

+ 41 - 0
2d/hexagonal_map/index.offline.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<meta name="viewport" content="width=device-width, initial-scale=1">
+		<title>You are offline</title>
+		<style>
+html {
+	background-color: #000000;
+	color: #ffffff;
+}
+
+body {
+	font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+	margin: 2rem;
+}
+
+p {
+	margin-block: 1rem;
+}
+
+button {
+	display: block;
+	padding: 1rem 2rem;
+	margin: 3rem auto 0;
+}
+		</style>
+	</head>
+	<body>
+		<h1>You are offline</h1>
+		<p>This application requires an Internet connection to run for the first time.</p>
+		<p>Press the button below to try reloading:</p>
+		<button type="button">Reload</button>
+		<script>
+document.querySelector('button').addEventListener('click', () => {
+	window.location.reload();
+});
+		</script>
+	</body>
+</html>

二進制
2d/hexagonal_map/index.pck


二進制
2d/hexagonal_map/index.png


+ 166 - 0
2d/hexagonal_map/index.service.worker.js

@@ -0,0 +1,166 @@
+// This service worker is required to expose an exported Godot project as a
+// Progressive Web App. It provides an offline fallback page telling the user
+// that they need an Internet connection to run the project if desired.
+// Incrementing CACHE_VERSION will kick off the install event and force
+// previously cached resources to be updated from the network.
+/** @type {string} */
+const CACHE_VERSION = '1754353305|3648592';
+/** @type {string} */
+const CACHE_PREFIX = 'Hexagonal Game-sw-cache-';
+const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
+/** @type {string} */
+const OFFLINE_URL = 'index.offline.html';
+/** @type {boolean} */
+const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = true;
+// Files that will be cached on load.
+/** @type {string[]} */
+const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.worker.js","index.audio.worklet.js"];
+// Files that we might not want the user to preload, and will only be cached on first load.
+/** @type {string[]} */
+const CACHABLE_FILES = ["index.wasm","index.pck"];
+const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
+
+self.addEventListener('install', (event) => {
+	event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES)));
+});
+
+self.addEventListener('activate', (event) => {
+	event.waitUntil(caches.keys().then(
+		function (keys) {
+			// Remove old caches.
+			return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key)));
+		}
+	).then(function () {
+		// Enable navigation preload if available.
+		return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
+	}));
+});
+
+/**
+ * Ensures that the response has the correct COEP/COOP headers
+ * @param {Response} response
+ * @returns {Response}
+ */
+function ensureCrossOriginIsolationHeaders(response) {
+	if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp'
+		&& response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') {
+		return response;
+	}
+
+	const crossOriginIsolatedHeaders = new Headers(response.headers);
+	crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
+	crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
+	const newResponse = new Response(response.body, {
+		status: response.status,
+		statusText: response.statusText,
+		headers: crossOriginIsolatedHeaders,
+	});
+
+	return newResponse;
+}
+
+/**
+ * Calls fetch and cache the result if it is cacheable
+ * @param {FetchEvent} event
+ * @param {Cache} cache
+ * @param {boolean} isCacheable
+ * @returns {Response}
+ */
+async function fetchAndCache(event, cache, isCacheable) {
+	// Use the preloaded response, if it's there
+	/** @type { Response } */
+	let response = await event.preloadResponse;
+	if (response == null) {
+		// Or, go over network.
+		response = await self.fetch(event.request);
+	}
+
+	if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+		response = ensureCrossOriginIsolationHeaders(response);
+	}
+
+	if (isCacheable) {
+		// And update the cache
+		cache.put(event.request, response.clone());
+	}
+
+	return response;
+}
+
+self.addEventListener(
+	'fetch',
+	/**
+	 * Triggered on fetch
+	 * @param {FetchEvent} event
+	 */
+	(event) => {
+		const isNavigate = event.request.mode === 'navigate';
+		const url = event.request.url || '';
+		const referrer = event.request.referrer || '';
+		const base = referrer.slice(0, referrer.lastIndexOf('/') + 1);
+		const local = url.startsWith(base) ? url.replace(base, '') : '';
+		const isCachable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0]));
+		if (isNavigate || isCachable) {
+			event.respondWith((async () => {
+				// Try to use cache first
+				const cache = await caches.open(CACHE_NAME);
+				if (isNavigate) {
+					// Check if we have full cache during HTML page request.
+					/** @type {Response[]} */
+					const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name)));
+					const missing = fullCache.some((v) => v === undefined);
+					if (missing) {
+						try {
+							// Try network if some cached file is missing (so we can display offline page in case).
+							const response = await fetchAndCache(event, cache, isCachable);
+							return response;
+						} catch (e) {
+							// And return the hopefully always cached offline page in case of network failure.
+							console.error('Network error: ', e); // eslint-disable-line no-console
+							return caches.match(OFFLINE_URL);
+						}
+					}
+				}
+				let cached = await cache.match(event.request);
+				if (cached != null) {
+					if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+						cached = ensureCrossOriginIsolationHeaders(cached);
+					}
+					return cached;
+				}
+				// Try network if don't have it in cache.
+				const response = await fetchAndCache(event, cache, isCachable);
+				return response;
+			})());
+		} else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) {
+			event.respondWith((async () => {
+				let response = await fetch(event.request);
+				response = ensureCrossOriginIsolationHeaders(response);
+				return response;
+			})());
+		}
+	}
+);
+
+self.addEventListener('message', (event) => {
+	// No cross origin
+	if (event.origin !== self.origin) {
+		return;
+	}
+	const id = event.source.id || '';
+	const msg = event.data || '';
+	// Ensure it's one of our clients.
+	self.clients.get(id).then(function (client) {
+		if (!client) {
+			return; // Not a valid client.
+		}
+		if (msg === 'claim') {
+			self.skipWaiting().then(() => self.clients.claim());
+		} else if (msg === 'clear') {
+			caches.delete(CACHE_NAME);
+		} else if (msg === 'update') {
+			self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url)));
+		}
+	});
+});
+

+ 1 - 0
2d/hexagonal_map/index.wasm

@@ -0,0 +1 @@
+../../index.wasm

+ 5 - 0
2d/hexagonal_map/index.worker.js

@@ -0,0 +1,5 @@
+// This file is no longer used by emscripten and has been created as a placeholder
+// to allow build systems to transition away from depending on it.
+//
+// Future versions of emscripten will likely stop generating this file at all.
+throw new Error('Dummy worker.js file should never be used');

二進制
2d/instancing/index.144x144.png


二進制
2d/instancing/index.180x180.png


二進制
2d/instancing/index.512x512.png


二進制
2d/instancing/index.apple-touch-icon.png


+ 213 - 0
2d/instancing/index.audio.worklet.js

@@ -0,0 +1,213 @@
+/**************************************************************************/
+/*  audio.worklet.js                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+class RingBuffer {
+	constructor(p_buffer, p_state, p_threads) {
+		this.buffer = p_buffer;
+		this.avail = p_state;
+		this.threads = p_threads;
+		this.rpos = 0;
+		this.wpos = 0;
+	}
+
+	data_left() {
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
+	}
+
+	space_left() {
+		return this.buffer.length - this.data_left();
+	}
+
+	read(output) {
+		const size = this.buffer.length;
+		let from = 0;
+		let to_write = output.length;
+		if (this.rpos + to_write > size) {
+			const high = size - this.rpos;
+			output.set(this.buffer.subarray(this.rpos, size));
+			from = high;
+			to_write -= high;
+			this.rpos = 0;
+		}
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
+		this.rpos += to_write;
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
+	}
+
+	write(p_buffer) {
+		const to_write = p_buffer.length;
+		const mw = this.buffer.length - this.wpos;
+		if (mw >= to_write) {
+			this.buffer.set(p_buffer, this.wpos);
+			this.wpos += to_write;
+			if (mw === to_write) {
+				this.wpos = 0;
+			}
+		} else {
+			const high = p_buffer.subarray(0, mw);
+			const low = p_buffer.subarray(mw);
+			this.buffer.set(high, this.wpos);
+			this.buffer.set(low);
+			this.wpos = low.length;
+		}
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
+	}
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+	constructor() {
+		super();
+		this.threads = false;
+		this.running = true;
+		this.lock = null;
+		this.notifier = null;
+		this.output = null;
+		this.output_buffer = new Float32Array();
+		this.input = null;
+		this.input_buffer = new Float32Array();
+		this.port.onmessage = (event) => {
+			const cmd = event.data['cmd'];
+			const data = event.data['data'];
+			this.parse_message(cmd, data);
+		};
+	}
+
+	process_notify() {
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
+	}
+
+	parse_message(p_cmd, p_data) {
+		if (p_cmd === 'start' && p_data) {
+			const state = p_data[0];
+			let idx = 0;
+			this.threads = true;
+			this.lock = state.subarray(idx, ++idx);
+			this.notifier = state.subarray(idx, ++idx);
+			const avail_in = state.subarray(idx, ++idx);
+			const avail_out = state.subarray(idx, ++idx);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
+		} else if (p_cmd === 'stop') {
+			this.running = false;
+			this.output = null;
+			this.input = null;
+			this.lock = null;
+			this.notifier = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
+		}
+	}
+
+	static array_has_data(arr) {
+		return arr.length && arr[0].length && arr[0][0].length;
+	}
+
+	process(inputs, outputs, parameters) {
+		if (!this.running) {
+			return false; // Stop processing.
+		}
+		if (this.output === null) {
+			return true; // Not ready yet, keep processing.
+		}
+		const process_input = GodotProcessor.array_has_data(inputs);
+		if (process_input) {
+			const input = inputs[0];
+			const chunk = input[0].length * input.length;
+			if (this.input_buffer.length !== chunk) {
+				this.input_buffer = new Float32Array(chunk);
+			}
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.input.write(this.input_buffer);
+			} else {
+				// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
+			}
+		}
+		const process_output = GodotProcessor.array_has_data(outputs);
+		if (process_output) {
+			const output = outputs[0];
+			const chunk = output[0].length * output.length;
+			if (this.output_buffer.length !== chunk) {
+				this.output_buffer = new Float32Array(chunk);
+			}
+			if (this.output.data_left() >= chunk) {
+				this.output.read(this.output_buffer);
+				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
+			} else {
+				// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
+			}
+		}
+		this.process_notify();
+		return true;
+	}
+
+	static write_output(dest, source) {
+		const channels = dest.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < dest[ch].length; sample++) {
+				dest[ch][sample] = source[sample * channels + ch];
+			}
+		}
+	}
+
+	static write_input(dest, source) {
+		const channels = source.length;
+		for (let ch = 0; ch < channels; ch++) {
+			for (let sample = 0; sample < source[ch].length; sample++) {
+				dest[sample * channels + ch] = source[ch][sample];
+			}
+		}
+	}
+}
+
+registerProcessor('godot-processor', GodotProcessor);

+ 200 - 0
2d/instancing/index.html

@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+		<title>Scene Instancing Demo</title>
+		<style>
+html, body, #canvas {
+	margin: 0;
+	padding: 0;
+	border: 0;
+}
+
+body {
+	color: white;
+	background-color: black;
+	overflow: hidden;
+	touch-action: none;
+}
+
+#canvas {
+	display: block;
+}
+
+#canvas:focus {
+	outline: none;
+}
+
+#status, #status-splash, #status-progress {
+	position: absolute;
+	left: 0;
+	right: 0;
+}
+
+#status, #status-splash {
+	top: 0;
+	bottom: 0;
+}
+
+#status {
+	background-color: #242424;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	visibility: hidden;
+}
+
+#status-splash {
+	max-height: 100%;
+	max-width: 100%;
+	margin: auto;
+}
+
+#status-progress, #status-notice {
+	display: none;
+}
+
+#status-progress {
+	bottom: 10%;
+	width: 50%;
+	margin: 0 auto;
+}
+
+#status-notice {
+	background-color: #5b3943;
+	border-radius: 0.5rem;
+	border: 1px solid #9b3943;
+	color: #e0e0e0;
+	font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
+	line-height: 1.3;
+	margin: 0 2rem;
+	overflow: hidden;
+	padding: 1rem;
+	text-align: center;
+	z-index: 1;
+}
+		</style>
+		<link id="-gd-engine-icon" rel="icon" type="image/png" href="index.icon.png" />
+<link rel="apple-touch-icon" href="index.apple-touch-icon.png"/>
+<link rel="manifest" href="index.manifest.json">
+
+	</head>
+	<body>
+		<canvas id="canvas">
+			Your browser does not support the canvas tag.
+		</canvas>
+
+		<noscript>
+			Your browser does not support JavaScript.
+		</noscript>
+
+		<div id="status">
+			<img id="status-splash" src="index.png" alt="">
+			<progress id="status-progress"></progress>
+			<div id="status-notice"></div>
+		</div>
+
+		<script src="index.js"></script>
+		<script>
+const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":true,"executable":"index","experimentalVK":true,"fileSizes":{"index.pck":33216,"index.wasm":33829596},"focusCanvas":true,"gdextensionLibs":[],"serviceWorker":"index.service.worker.js"};
+const GODOT_THREADS_ENABLED = true;
+const engine = new Engine(GODOT_CONFIG);
+
+(function () {
+	const statusOverlay = document.getElementById('status');
+	const statusProgress = document.getElementById('status-progress');
+	const statusNotice = document.getElementById('status-notice');
+
+	let initializing = true;
+	let statusMode = '';
+
+	function setStatusMode(mode) {
+		if (statusMode === mode || !initializing) {
+			return;
+		}
+		if (mode === 'hidden') {
+			statusOverlay.remove();
+			initializing = false;
+			return;
+		}
+		statusOverlay.style.visibility = 'visible';
+		statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
+		statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
+		statusMode = mode;
+	}
+
+	function setStatusNotice(text) {
+		while (statusNotice.lastChild) {
+			statusNotice.removeChild(statusNotice.lastChild);
+		}
+		const lines = text.split('\n');
+		lines.forEach((line) => {
+			statusNotice.appendChild(document.createTextNode(line));
+			statusNotice.appendChild(document.createElement('br'));
+		});
+	}
+
+	function displayFailureNotice(err) {
+		console.error(err);
+		if (err instanceof Error) {
+			setStatusNotice(err.message);
+		} else if (typeof err === 'string') {
+			setStatusNotice(err);
+		} else {
+			setStatusNotice('An unknown error occured');
+		}
+		setStatusMode('notice');
+		initializing = false;
+	}
+
+	const missing = Engine.getMissingFeatures({
+		threads: GODOT_THREADS_ENABLED,
+	});
+
+	if (missing.length !== 0) {
+		if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
+			// There's a chance that installing the service worker would fix the issue
+			Promise.race([
+				navigator.serviceWorker.getRegistration().then((registration) => {
+					if (registration != null) {
+						return Promise.reject(new Error('Service worker already exists.'));
+					}
+					return registration;
+				}).then(() => engine.installServiceWorker()),
+				// For some reason, `getRegistration()` can stall
+				new Promise((resolve) => {
+					setTimeout(() => resolve(), 2000);
+				}),
+			]).catch((err) => {
+				console.error('Error while registering service worker:', err);
+			}).then(() => {
+				window.location.reload();
+			});
+		} else {
+			// Display the message as usual
+			const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
+			displayFailureNotice(missingMsg + missing.join('\n'));
+		}
+	} else {
+		setStatusMode('progress');
+		engine.startGame({
+			'onProgress': function (current, total) {
+				if (current > 0 && total > 0) {
+					statusProgress.value = current;
+					statusProgress.max = total;
+				} else {
+					statusProgress.removeAttribute('value');
+					statusProgress.removeAttribute('max');
+				}
+			},
+		}).then(() => {
+			setStatusMode('hidden');
+		}, displayFailureNotice);
+	}
+}());
+		</script>
+	</body>
+</html>
+

二進制
2d/instancing/index.icon.png


File diff suppressed because it is too large
+ 8 - 0
2d/instancing/index.js


+ 1 - 0
2d/instancing/index.manifest.json

@@ -0,0 +1 @@
+{"background_color":"#000000","display":"fullscreen","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"Scene Instancing Demo","orientation":"landscape","start_url":"./index.html"}

Some files were not shown because too many files changed in this diff