Ga naar hoofdinhoud

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)

Volgende