Powrót do Bloga

Projektowanie Thread-Safe integracji z backendem w Godot 4.7: Rozwiązywanie Network Bottlenecks w Godot 4.7 RC 3

Opublikowano 17 czerwca 2026
Projektowanie Thread-Safe integracji z backendem w Godot 4.7: Rozwiązywanie Network Bottlenecks w Godot 4.7 RC 3

W skrócie

Artykuł omawia architekturę bezpiecznego wątkowo (Thread-Safe) managera połączeń HTTP w środowisku silnika Godot 4.7 RC 3. Autor szczegółowo wyjaśnia, jak zaimplementować wydajną pulę zapytań w GDScript, zapobiegając typowym błędom nakładania się żądań oraz crashom SceneTree. Przedstawiono również sposoby radzenia sobie z ograniczeniami platform mobilnych i webowych, w tym z polityką CORS oraz jednowątkowością WebAssembly. Na koniec artykuł porównuje samodzielną budowę infrastruktury backendowej z gotowym rozwiązaniem SDK od horizOn.

Każdy twórca indie, który wydał grę multiplayer w Godot, doskonale zna ten moment, kiedy jego klient HTTP zgłasza cichy connection timeout lub wyjątek thread-safety. Te problemy często pozostają niewidoczne podczas testów w lokalnym edytorze, a ujawniają się dopiero w wersji produkcyjnej, powodując crash produkcyjnego builda, gdy setki graczy jednocześnie uderza w endpointy logowania. Rozwiązanie tych problemów wymaga głębokiego zrozumienia asynchronicznej warstwy sieciowej (networking layer) silnika.

Godot 4.7 RC 3 Arrives: The Stability Run-Up and Core Regression Fixes

Rozwiązywanie krytycznych regresji w Release Candidate

Wydanie Godot 4.7 RC 3 przybliża silnik do bardzo oczekiwanej wersji stabilnej. Obecnie, w fazie feature freeze, zespół deweloperski skupił się całkowicie na eliminowaniu krytycznych regresji wykrytych w fazie beta. Dla deweloperów pracujących z zewnętrznymi API, poprawki te zapewniają stabilność w złożonych cyklach życia wykonania (execution lifecycles). Konkretnie, RC 3 eliminuje błąd stretch mode dla custom_timeline w systemie animacji. Rozwiązuje także bugi związane z listowaniem assetów w AssetLib, gdzie licencje oznaczone jako „Other” były błędnie odfiltrowywane. Na koniec, deweloperzy XR docenią poprawkę crasha wywoływanego przez trackery spatial entity marker.

Poprawka Area Event Queuing w Jolt Physics

Jolt stał się najpopularniejszym pluginem fizyki dla Godot 4.x ze względu na swoją szybkość i stabilność. Jednak regresja w buildach beta wymuszała kolejkowanie zdarzeń obszaru (area event queuing) podczas body exit – co oznacza, że za każdym razem, gdy rigid body opuszczało dany obszar, silnik wykonywał nadmiarowe dodawania do kolejki. W dynamicznym lobby multiplayer z 64 graczami i dziesiątkami trigger areas, ten narzut na CPU błyskawicznie obniżał tick rate serwera z 60 Hz do poziomu poniżej 20 Hz. Wyeliminowanie tej regresji sprawia, że lokalne checki triggerów nie zakłócają pracy network threadu.

Refaktoryzacja REST API w AssetLib

Kolejnym ważnym punktem w Godot 4.7 jest przebudowa API Asset Library (AssetLib). Połączenie z backendem zostało przeniesione na zmodernizowaną strukturę REST, co rozwiązało problemy z kolejnością wydań i błędami ładowania. Ta aktualizacja służy jako wzór tego, jak deweloperzy gier powinni strukturyzować własne integracje z zewnętrznymi API. Dzięki zastosowaniu przejrzystych endpointów i paginated requests, możesz zoptymalizować czas ładowania przy dostarczaniu treści. Z kolei użycie tablic JSON o ujednoliconej strukturze schematu pozwala uniknąć wąskich gardeł związanych z deserializacją w GDScript.

Dlaczego integracja z backendem w Godot 4.7 wymaga dbałości o architekturę

Ograniczenia node'a HTTPRequest

Integracja backendu z Godot 4.7 wiąże się z zarządzaniem operacjami asynchronicznymi bez blokowania głównego wątku (main thread) gry. Godot opiera się na nieblokujących node'ach, takich jak HTTPRequest, do obsługi zapytań, jednak wiążą się one z poważnym ograniczeniem: nie potrafią obsługiwać współbieżnych żądań (concurrent requests). Jeśli spróbujesz wywołać request() na node, który aktualnie oczekuje na odpowiedź, silnik zgłosi błąd. Ten błąd „Request already in progress” może zatrzymać kluczowe funkcje rozgrywki, jeśli nie zostanie odpowiednio obsłużony.

Thread Safety i modyfikacje SceneTree

Aby uniknąć tych konfliktów, musisz zbudować solidną kolejkę lub pulę (pool), która dynamicznie przydziela żądania do wolnych node'ów. Dodatkowo, thread safety stanowi stałe wyzwanie podczas przetwarzania odpowiedzi sieciowych. Jeśli poboczny wątek sieciowy (network thread) spróbuje bezpośrednio zmodyfikować SceneTree, Godot ulegnie crashowi lub zacznie zachowywać się niestabilnie. Zawsze musisz deferować zmiany UI z powrotem do main threadu, aby zagwarantować stabilność.

Pokonywanie ograniczeń SSL/TLS oraz CORS

Zabezpieczenie danych graczy to kluczowa kwestia; jak opisano w artykule architecting game backends to survive compromises, ochrona danych zaczyna się od TLS i solidnej autoryzacji po stronie serwera. Handshake z Twoim backendem musi odbywać się przez bezpieczne protokoły https:// lub wss://, co wymaga poprawnego uwierzytelnienia certyfikatów SSL/TLS. Na platformach mobilnych nawet drobne błędy w konfiguracji sieci mogą prowadzić do cichego zrywania połączeń. Co więcej, eksporty webowe (HTML5) wprowadzają dodatkowy poziom skomplikowania, ponieważ przeglądarki wymuszają rygorystyczne reguły CORS (Cross-Origin Resource Sharing).

The Code: Implementing a Thread-Safe HTTP Request Pool in GDScript 2.0

Implementacja managera puli w GDScript

Poniższy singleton w GDScript udostępnia Thread-Safe manager HTTP oparty na puli (pool), który zapobiega błędom nakładających się żądań. Dynamicznie tworzy instancje puli node'ów HTTPRequest i kolejkuje przychodzące żądania, gdy wszystkie node'y są zajęte. Wykorzystuje również bezpieczne praktyki parsowania JSON, aby zapobiec crashom w czasie działania (runtime crashes) wywołanym nieoczekiwanym payloadem z serwera.

extends Node
class_name BackendHTTPManager

# Maximum concurrent HTTP requests allowed in the pool
const MAX_CONCURRENT_REQUESTS = 4

# Structure to hold queued request data
class PendingRequest:
	var url: String
	var method: HTTPClient.Method
	var headers: PackedStringArray
	var body: String
	var callback: Callable
	
	func _init(p_url: String, p_method: HTTPClient.Method, p_headers: PackedStringArray, p_body: String, p_callback: Callable):
		self.url = p_url
		self.method = p_method
		self.headers = p_headers
		self.body = p_body
		self.callback = p_callback

# Internal tracking
var _request_pool: Array[HTTPRequest] = []
var _active_requests: Dictionary = {}
var _request_queue: Array[PendingRequest] = []

func _ready() -> void:
	# Initialize the HTTPRequest pool
	for i in range(MAX_CONCURRENT_REQUESTS):
		var http_node = HTTPRequest.new()
		add_child(http_node)
		http_node.request_completed.connect(_on_request_completed.bind(http_node))
		_request_pool.append(http_node)

## Queue an asynchronous HTTP request
func send_request(url: String, method: HTTPClient.Method, headers: PackedStringArray, body: String, callback: Callable) -> void:
	var new_req = PendingRequest.new(url, method, headers, body, callback)
	_request_queue.append(new_req)
	_process_queue()

# Process next items in the queue if a pool node is free
func _process_queue() -> void:
	if _request_queue.is_empty():
		return
		
	# Find an idle HTTPRequest node
	var free_node: HTTPRequest = null
	for node in _request_pool:
		if not _active_requests.has(node):
			free_node = node
			break
			
	if free_node == null:
		# All nodes are busy; request remains in queue
		return
		
	var req = _request_queue.pop_front()
	_active_requests[free_node] = req
	
	var err = free_node.request(req.url, req.headers, req.method, req.body)
	if err != OK:
		# Immediately notify failure if the request failed to initiate
		_active_requests.erase(free_node)
		req.callback.call_deferred(false, -1, {}, "Failed to initiate request")
		_process_queue()

# Callback triggered when a request completes
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray, http_node: HTTPRequest) -> void:
	if not _active_requests.has(http_node):
		return
		
	var req = _active_requests[http_node]
	_active_requests.erase(http_node)
	
	var response_string = body.get_string_from_utf8()
	var parsed_data = {}
	var success = (result == HTTPRequest.RESULT_SUCCESS) and (response_code >= 200 and response_code < 300)
	var error_message = ""
	
	if success:
		var json = JSON.new()
		var parse_err = json.parse(response_string)
		if parse_err == OK:
			if typeof(json.data) == TYPE_DICTIONARY:
				parsed_data = json.data
			else:
				success = false
				error_message = "Parsed JSON is not a dictionary"
						else:
				success = false
				error_message = "JSON parse error code: " + str(parse_err)
	else:
		error_message = "HTTP error code: " + str(response_code) + " or result failure: " + str(result)
				# Execute callback on the main thread safely
	req.callback.call_deferred(success, response_code, parsed_data, error_message)
	
	# Process next queued request
	_process_queue()

Szczegółowa analiza działania puli i kolejki

Ten manager inicjalizuje statyczną tablicę (static array) child node'ów HTTPRequest w callbacku _ready(). Poprzez ograniczenie rozmiaru puli do stałej MAX_CONCURRENT_REQUESTS, kontrolujesz ruch sieciowy po stronie klienta. Każdy node jest podpięty pod centralny handler odpowiedzi (response handler) i przekazuje własną referencję, co pozwala śledzić, które żądanie właśnie się zakończyło.

Funkcja send_request() dodaje do kolejki FIFO klasę opakowującą (wrapper class) zawierającą URL, payload oraz wywoływalny callback (callable callback). Kiedy żądanie się kończy, dany node z puli jest oznaczany jako wolny (idle), a manager natychmiast przetwarza kolejny element z kolejki. To całkowicie zapobiega błędom nakładających się żądań (overlapping request errors).

Bezpieczne parsowanie payloadów JSON w Godot 4.7

Parsowanie danych w Godot 4.7 zmieniło się w porównaniu do poprzednich wersji silnika. Teraz tworzymy nową instancję obiektu JSON i wywołujemy parse(), rezygnując ze zdeprecjonowanych funkcji globalnych. Weryfikując, czy json.data jest typu TYPE_DICTIONARY, chronimy klienta przed crashem w przypadku, gdy backend zwróci niepoprawnie sformatowany payload.

Na koniec używamy call_deferred, aby uruchomić callback w głównym wątku (main thread). Gwarantuje to, że wszelkie aktualizacje UI lub modyfikacje SceneTree wywołane odpowiedzią sieciową zostaną wykonane w bezpieczny sposób. Asynchroniczne uruchamianie tych callbacków zapobiega konfliktom wątków i pozwala utrzymać płynną liczbę klatek na sekundę (frame rate).

Resolving Platform-Specific Integration Obstacles: Web and Mobile

Eksporty webowe i ograniczenia jednowątkowego WASM

Buildy pod WebAssembly (WASM) zachowują się inaczej niż wersje desktopowe. W środowisku przeglądarkowym Godot działa w jednowątkowej pętli (single-threaded loop), chyba że ustawione są odpowiednie nagłówki SharedArrayBuffer. Jeśli użyjesz synchronicznych, blokujących operacji HTTP, całe okno przeglądarki zawiesi się, co drastycznie pogorszy user experience.

Aby tego uniknąć, w przypadku wersji webowych zawsze stosuj nieblokujące żądania sterowane sygnałami (signal-driven, non-blocking requests). Musisz również upewnić się, że endpointy Twojego backendu odsyłają właściwe nagłówki CORS. Dokładniej mówiąc, nagłówki Access-Control-Allow-Origin oraz Access-Control-Allow-Headers must wprost zezwalać na dostęp domenie, na której zahostowana jest Twoja gra.

Eksporty na Androida i uprawnienia sieciowe

Platformy mobilne niosą ze sobą specyficzne wyzwania. Przy eksportach na Androida częstym przeoczeniem jest brak zaznaczenia uprawnienia INTERNET w ustawieniach eksportu (export presets), co całkowicie blokuje wywołania sieciowe. Ponadto Godot 4.7 wprowadza bardziej precyzyjne opcje customizacji splash screenów i skalowania okna na Androidzie, co zapobiega glitchom graficznym podczas wczesnych zapytań sieciowych przy starcie aplikacji.

Jeśli eksportujesz grę na platformy mobilne, kluczowe jest zrozumienie otoczenia regulacyjnego. Obsługa płatności zewnętrznych (third-party billing) wymaga bezpiecznych handshake'ów API, jak opisano w naszym przewodniku architecting third party mobile billing. Upewnienie się, że Twoje callbacki płatności i handshake'y po stronie klienta są bezpieczne, uniemożliwia graczom omijanie mikropłatności.

Najlepsze praktyki skalowania integracji z backendem w Godot 4.7

1. Zaimplementuj Exponential Backoff z mechanizmem Jitter

Unikaj przeciążania serwera w przypadku wystąpienia lawiny ponownych połączeń (reconnection storms). Jeśli gracz utraci połączenie, nie ponawiaj prób natychmiast w stałych odstępach czasu. Zamiast tego za każdym razem mnóż opóźnienie ponownej próby przez 1.5 lub 2.0 i dodaj niewielkie losowe przesunięcie (jitter), aby zapobiec jednoczesnemu odpytywaniu serwera przez wszystkich klientów.

Przykładowo, jeśli pierwsza próba następuje po 1,0 sekundy, kolejne powinny odbywać się po 2,0, 4,0 i 8,0 sekundy. Aby uniknąć sytuacji, w której wszyscy rozłączeni klienci próbują połączyć się w tej samej milisekundzie, dodaj losową wartość float z przedziału od 0,1 do 0,5 sekundy do każdego opóźnienia. Pozwoli to rozłożyć obciążenie na serwerach backendowych i zapobiegnie kaskadowym awariom API podczas przerw w działaniu usług.

2. Waliduj payloady po obu stronach

Nygdy nie ufaj danym klienckim i nigdy nie zakładaj, że dane z serwera są idealnie sformatowane. Waliduj wszystkie słowniki (dictionaries) i klucze przed odczytaniem ich w GDScript. Analogicznie upewnij się, że Twój backend waliduje treść przychodzących żądań (request bodies), aby zapobiec podatnościom na SQL injection lub zdalnemu wykonaniu kodu (remote execution).

Częstą podatnością w grach multiplayer jest zbytnie zaufanie po stronie klienta. Jeśli skrypt klienta otrzymuje słownik (dictionary) z backendu, użyj metody Dictionary.has(), aby upewnić się o istnieniu każdego wymaganego klucza przed próbą uzyskania do niego dostępu. Odwołanie się do nieistniejącego klucza w GDScript rzuca błędem w czasie wykonywania (runtime error), co zatrzymuje działanie skryptu. Ta walidacja zapobiega awariom interfejsu (UI) przy aktualizacjach endpointów na serwerze.

3. Decoupluj usługi sieciowe od UI

Unikaj pisania kodu sieciowego bezpośrednio w skryptach UI. Stwórz dedykowany singleton z funkcją Autoload do obsługi całego ruchu HTTP i zarządzania stanem. Twój interfejs powinien jedynie łączyć się z sygnałami (signals) emitowanymi przez ten manager, co pozwoli zachować modułowość i testowalność kodu frontendu.

Na przykład, gdy gracz otwiera ekran ekwipunku (inventory), UI powinno wyemitować własny sygnał (custom signal) z żądaniem danych. Autoload sieciowy przechwytuje ten sygnał, wykonuje połączenie HTTP i emituje sygnał sukcesu po zapełnieniu słownika. UI nasłuchuje tego sygnału sukcesu, aby uzupełnić siatkę przedmiotów. Taki decoupling utrzymuje responsywność UI i sprawia, że debugowanie logiki sieciowej staje się proste.

4. Przeprowadzaj wczesne testy CORS Pre-Flight

Zawsze testuj swoją grę na lokalnym serwerze webowym z włączonym CORS przed wrzuceniem jej na platformy pokroju itch.io. Wielu deweloperów kompiluje swoją grę tylko po to, by przekonać się, że zapytania HTTP w przeglądarce kończą się niepowodzeniem ze względu na politykę origin (origin policies). Wczesne testy pozwalają uniknąć gorączkowego poprawiania konfiguracji w dniu premiery.

Przeglądarki wysyłają żądanie HTTP OPTIONS jako pre-flight check przed wykonaniem żądań POST. Jeśli Twój backend nie jest skonfigurowany tak, by odpowiadać na OPTIONS statusem 200 OK, przeglądarka zablokuje właściwe zapytanie. Diagnozuj te problemy z origin, sprawdzając logi konsoli przeglądarki (klawisz F12). Wychwycenie tych błędów w środowisku testowym (staging) zapobiegnie problemom z logowaniem w grze multiplayer podczas premiery.

Standaryzacja infrastruktury backendowej: własnoręczne kodowanie kontra gotowe rozwiązania

Rzeczywisty koszt budowania serwerów gry od zera

Budowanie niestandardowego backendu dla gry w Godot wymaga ogromnego nakładu pracy. Musisz napisać kod serwera, zaimplementować uwierzytelnianie za pomocą JWT token, skonfigurować indeksowanie baz danych i zarządzać load balancerami. Ręczne stawianie tej infrastruktury może zająć od 4 do 6 tygodni pracy deweloperskiej, odciągając Twoją uwagę od samej rozgrywki. Bezpieczny backend produkcyjny wymaga klastrowania baz danych, obsługi wygasania tokenów autoryzacyjnych oraz migracji schematów baz danych. Musisz także napisać własne skrypty monitorujące stan serwerów i logujące problemy sieciowe.

Przyspieszenie pracy dzięki horizOn

Zastosowanie SDK od horizOn eliminuje ten administracyjny narzut. Platforma obsługuje pod maską user persistence, telemetry oraz leaderboards, pozwalając Ci w pełni skupić się na grze. Zamiast debugować pule połączeń HTTP, wykonujesz proste wywołania API, które działają na zoptymalizowanej infrastrukturze. Dzięki temu wdrażasz nowe funkcjonalności dla graczy w kilka minut, a nie tygodni. Samodzielne konfigurowanie tej infrastruktury mogłoby zająć od 4 do 6 tygodni dedykowanej pracy deweloperskiej. Z horizOn usługi te są prekonfigurowane, dzięki czemu wydajesz grę, a nie walczysz z infrastrukturą.

Chcesz skalować swój backend multiplayer? Wypróbuj horizOn za darmo lub zajrzyj do dokumentacji API, aby rozpocząć swój kolejny projekt.


Źródło: Release candidate: Godot 4.7 RC 3