Web Export
Godot naar HTML5/WebAssembly exporteren.
Web Export Overzicht
┌─────────────────────────────────────────────────────────────────┐
│ Web Export Stack │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Browser │
│ ├── HTML5 Container │
│ │ ├── JavaScript Glue Code │
│ │ ├── WebAssembly (WASM) - Game Logic │
│ │ └── WebGL 2.0 - Rendering │
│ │ │
│ Data: │
│ ├── .wasm - Compiled game code │
│ ├── .pck - Game resources (compressed) │
│ ├── .js - JavaScript loader │
│ └── .html - Entry point │
│ │
│ Storage: │
│ ├── IndexedDB - Save data │
│ └── LocalStorage - Settings │
│ │
└─────────────────────────────────────────────────────────────────┘
Export Configuration
Project Settings
# project.godot - Web-specific settings
[rendering]
# WebGL 2.0 compatible
renderer/rendering_method = "gl_compatibility"
# Texture compression
textures/vram_compression/import_etc2_astc = true
[display]
# Canvas resize policy
window/stretch/mode = "canvas_items"
window/stretch/aspect = "expand"
[network]
# CORS settings
limits/websocket/max_pending_packets = 4096
Export Presets
# export_presets.cfg
[preset.web]
platform = "Web"
name = "Web"
runnable = true
custom_features = "web"
# HTML
html/export_icon = true
html/custom_html_shell = "res://export/web_shell.html"
html/head_include = "<meta name='viewport' content='width=device-width, initial-scale=1'>"
# Canvas
html/canvas_resize_policy = 2 # Project setting
# Threading (experimental)
variant/thread_support = false
# Progressive download
progressive_web_app/enabled = true
progressive_web_app/offline_page = "res://export/offline.html"
WASM Optimalisatie
File Size Reductie
# Verminder export size:
# 1. Exclude unused modules
# Project -> Export -> Web -> Features
# Disable: 3D Physics, 3D Navigation, etc.
# 2. Compress resources
# Project -> Export -> Resources
# - Compress mode: ZIP
# - Split PCK: true (voor progressive loading)
# 3. Strip debug info
# Export as Release, not Debug
# Target sizes:
# - WASM: < 15 MB (compressed)
# - PCK: < 50 MB (compressed)
# - Total initial load: < 20 MB
Loading Strategy
// Custom loading screen in web_shell.html
const GODOT_CONFIG = {
"canvas": document.getElementById("canvas"),
"executable": "index",
"mainPack": "index.pck",
"progressFunc": updateProgress,
"canvasResizePolicy": 2
};
function updateProgress(current, total) {
const percent = (current / total * 100).toFixed(0);
document.getElementById("loading-bar").style.width = percent + "%";
document.getElementById("loading-text").innerText = `Loading... ${percent}%`;
}
// Start engine when ready
const engine = new Engine(GODOT_CONFIG);
engine.startGame().then(() => {
document.getElementById("loading-screen").style.display = "none";
});
Browser Compatibility
Feature Detection
# web_compatibility.gd
extends Node
var is_web: bool = false
var has_webgl2: bool = true
var supports_threads: bool = false
func _ready() -> void:
is_web = OS.get_name() == "Web"
if is_web:
_check_browser_features()
func _check_browser_features() -> void:
# Check via JavaScript interface
if JavaScriptBridge.is_available():
has_webgl2 = JavaScriptBridge.eval("!!document.createElement('canvas').getContext('webgl2')")
supports_threads = JavaScriptBridge.eval("typeof SharedArrayBuffer !== 'undefined'")
func get_browser_info() -> Dictionary:
if not is_web:
return {}
return {
"user_agent": JavaScriptBridge.eval("navigator.userAgent"),
"platform": JavaScriptBridge.eval("navigator.platform"),
"language": JavaScriptBridge.eval("navigator.language")
}
WebGL Fallbacks
func _ready() -> void:
if OS.get_name() == "Web":
# Disable features niet ondersteund in WebGL
_disable_unsupported_features()
func _disable_unsupported_features() -> void:
# Sommige shaders werken anders in WebGL
# Gebruik compatibility shaders
# Disable HDR (niet breed ondersteund)
ProjectSettings.set_setting("rendering/environment/defaults/default_clear_color", Color.BLACK)
JavaScript Integratie
Godot naar JavaScript
# Data naar browser sturen
func send_to_browser(data: Dictionary) -> void:
if not OS.get_name() == "Web":
return
var json = JSON.stringify(data)
JavaScriptBridge.eval("window.postMessage(" + json + ", '*')")
# JavaScript functie aanroepen
func call_js_function(func_name: String, args: Array) -> Variant:
if not JavaScriptBridge.is_available():
return null
var js_args = JSON.stringify(args)
return JavaScriptBridge.eval("%s.apply(null, %s)" % [func_name, js_args])
JavaScript naar Godot
// In HTML/JavaScript
function sendToGodot(eventType, data) {
if (window.godotCallback) {
window.godotCallback(eventType, JSON.stringify(data));
}
}
// Example: Send authentication result
function onAuthComplete(token) {
sendToGodot("auth_complete", { token: token });
}
# GDScript: Register callback
func _ready() -> void:
if OS.get_name() == "Web":
_setup_js_callback()
func _setup_js_callback() -> void:
var callback = JavaScriptBridge.create_callback(_on_js_event)
JavaScriptBridge.eval("window.godotCallback = " + str(callback))
func _on_js_event(args: Array) -> void:
var event_type = args[0]
var data = JSON.parse_string(args[1])
match event_type:
"auth_complete":
_handle_auth(data.token)
Persistent Storage
IndexedDB
# Web-specifieke save/load
func save_game_web(data: Dictionary) -> void:
var json = JSON.stringify(data)
JavaScriptBridge.eval("""
const request = indexedDB.open('MilenasTreehouse', 1);
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(['saves'], 'readwrite');
const store = transaction.objectStore('saves');
store.put({ id: 'main', data: '%s' });
};
""" % json)
func load_game_web() -> void:
var callback = JavaScriptBridge.create_callback(_on_load_complete)
JavaScriptBridge.eval("""
const request = indexedDB.open('MilenasTreehouse', 1);
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction(['saves'], 'readonly');
const store = transaction.objectStore('saves');
const get = store.get('main');
get.onsuccess = function() {
if (get.result) {
%s(get.result.data);
}
};
};
""" % str(callback))
func _on_load_complete(args: Array) -> void:
var data = JSON.parse_string(args[0])
_apply_save_data(data)
Storage Quota
func check_storage_quota() -> void:
if OS.get_name() != "Web":
return
JavaScriptBridge.eval("""
navigator.storage.estimate().then(function(estimate) {
console.log('Storage used: ' + estimate.usage);
console.log('Storage quota: ' + estimate.quota);
});
""")
PWA Support
Service Worker
// sw.js - Service Worker
const CACHE_NAME = 'milenas-treehouse-v1';
const ASSETS = [
'/',
'/index.html',
'/index.js',
'/index.wasm',
'/index.pck',
'/offline.html'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(ASSETS);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
}).catch(() => {
return caches.match('/offline.html');
})
);
});
Web Manifest
// manifest.json
{
"name": "Milena's Cozy Treehouse Club",
"short_name": "Treehouse",
"description": "Een gezellig multiplayer spel",
"start_url": "/",
"display": "standalone",
"background_color": "#8B5A2B",
"theme_color": "#D4A574",
"orientation": "landscape",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Audio in Browsers
# Browser audio context moet door user gesture gestart worden
func _ready() -> void:
if OS.get_name() == "Web":
# Wacht op eerste interactie
_wait_for_user_interaction()
func _wait_for_user_interaction() -> void:
set_process_input(true)
AudioServer.set_bus_mute(0, true)
# Toon "Click to start" overlay
start_overlay.visible = true
func _input(event: InputEvent) -> void:
if OS.get_name() == "Web":
if event is InputEventMouseButton or event is InputEventScreenTouch:
_enable_audio()
func _enable_audio() -> void:
AudioServer.set_bus_mute(0, false)
start_overlay.visible = false
set_process_input(false)
# Resume audio context
JavaScriptBridge.eval("if (window.AudioContext) { new AudioContext().resume(); }")
Hosting
Server Configuration
# nginx.conf
server {
listen 80;
server_name milenas-treehouse.com;
# Compression
gzip on;
gzip_types application/javascript application/wasm;
# CORS headers
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
# WASM MIME type
types {
application/wasm wasm;
}
# Cache static assets
location ~* \.(wasm|pck|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
root /var/www/treehouse;
index index.html;
}
}
CORS voor SpacetimeDB
# WebSocket connectie moet CORS-compliant zijn
func connect_spacetimedb() -> void:
var ws_url = "wss://mainnet.spacetimedb.com"
if OS.get_name() == "Web":
# Browser enforced CORS
_client.connect_to_url(ws_url)
else:
_client.connect_to_url(ws_url)