Skip to content

Commit 0dbd993

Browse files
committed
Sync current scene with URL hash
Currently there is only one playable scene in the game, but the learning team would like to add another playable scene that has a number of deliberate gameplay bugs. Learners will playtest that scene and identify bugs and/or game design problems. In order to have a stable link to this deliberately-broken scene, add the ability to deep-link to a scene via the URL hash. This is derived from the code I previously wrote for the same functionality first in Candy Collective, then in Threadbare. Each time I write it, I think: wouldn't this be a nice addon? But I can never quite make it generic enough.
1 parent da03d70 commit 0dbd993

3 files changed

Lines changed: 107 additions & 0 deletions

File tree

project.godot

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ config/icon="res://icon.svg"
2020

2121
Global="*res://scripts/global.gd"
2222
Actions="*res://scripts/actions.gd"
23+
WebSceneSelector="uid://cag3i6auntfwn"
2324

2425
[display]
2526

scripts/web_scene_selector.gd

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
extends Node
2+
## Global script to sync current scene with URL hash on web platform
3+
##
4+
## On the web platform, this script allows loading a specific scene by placing its filename in the
5+
## URL hash; and updates the URL hash when the scene changes.
6+
7+
# Prefixes to try adding to non-absolute path in URL hash, which may have been stripped to make it
8+
# more human-readable.
9+
const _SCENE_PREFIXES = [
10+
"res://",
11+
]
12+
13+
# Suffix stripped from path to make it more human-readable
14+
const _SCENE_SUFFIX = ".tscn"
15+
16+
# Proxy object for the 'window' DOM object, or null if not running on the web
17+
var _window: JavaScriptObject
18+
19+
# Proxy object for the [method _on_hash_changed] callback, or null if not running on the web
20+
var _on_hash_changed_ref: JavaScriptObject
21+
22+
# The last URL that was set by [method _set_hash], if running on the web.
23+
# If we observe the URL changing to something different, the user has edited the URL manually.
24+
var _current_url: String
25+
26+
# Matches the expected absolute path for a scene, with a capture group
27+
# representing a more human-readable substring.
28+
var _scene_rx := RegEx.create_from_string(
29+
"^" + _SCENE_PREFIXES[-1] + "(?<scene>.+)\\" + _SCENE_SUFFIX + "$"
30+
)
31+
32+
# The main scene of the game. The URL hash will be cleared if this is the current scene.
33+
@onready var _main_scene: String = ProjectSettings.get("application/run/main_scene")
34+
35+
36+
func _ready() -> void:
37+
if OS.has_feature("web"):
38+
_window = JavaScriptBridge.get_interface("window")
39+
# Load any scene specified in the URL hash
40+
_restore_from_hash.call_deferred()
41+
42+
# Monitor the URL hash for changes
43+
_on_hash_changed_ref = JavaScriptBridge.create_callback(_on_hash_changed)
44+
_window.onhashchange = _on_hash_changed_ref
45+
46+
# Monitor for the current scene changing. There is no built-in way to switch scenes but this
47+
# may change when the game is modded!
48+
get_tree().scene_changed.connect(_on_scene_changed)
49+
50+
51+
# On the web, load the world indicated by the URL hash, if any.
52+
func _restore_from_hash() -> void:
53+
var url_hash: String = _window.location.hash as String
54+
if url_hash:
55+
var path: String = url_hash.right(-1).uri_decode()
56+
57+
if path.is_relative_path():
58+
if not path.ends_with(_SCENE_SUFFIX):
59+
path += _SCENE_SUFFIX
60+
61+
for prefix: String in _SCENE_PREFIXES:
62+
if ResourceLoader.exists(prefix + path, "PackedScene"):
63+
path = prefix + path
64+
break
65+
# otherwise, this is an absolute uid:// or res:// path
66+
67+
if ResourceLoader.exists(path, "PackedScene"):
68+
get_tree().change_scene_to_file(path)
69+
else:
70+
prints("Path", path, "from URL hash", url_hash, "is not a scene; ignoring")
71+
72+
73+
# On the web, update or clear the URL hash to indicate the current scene.
74+
func _set_hash(resource_path: String) -> void:
75+
if _window:
76+
var rx_match: RegExMatch = _scene_rx.search(resource_path)
77+
var url_hash: String
78+
79+
if resource_path == _main_scene:
80+
url_hash = ""
81+
elif rx_match:
82+
url_hash = rx_match.get_string("scene")
83+
else:
84+
url_hash = resource_path
85+
86+
var url: JavaScriptObject = JavaScriptBridge.create_object("URL", _window.location.href)
87+
url.hash = "#" + url_hash
88+
# Replace the current URL rather than simply updating window.location to
89+
# avoid creating misleading history entries that don't work if you press
90+
# the browser's back button.
91+
_current_url = url.href
92+
_window.location.replace(url.href)
93+
94+
95+
# When the browser tells us the hash has changed, potentially switch scene.
96+
func _on_hash_changed(args: Array) -> void:
97+
var event := args[0] as JavaScriptObject
98+
var new_url := event.newURL as String
99+
if new_url != _current_url:
100+
_restore_from_hash()
101+
102+
103+
# When Godot tells us the current scene has changed, update the URL hash.
104+
func _on_scene_changed() -> void:
105+
_set_hash(get_tree().current_scene.scene_file_path)

scripts/web_scene_selector.gd.uid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://cag3i6auntfwn

0 commit comments

Comments
 (0)