العودة إلى المدونة

تصميم تكامل Backend متوافق مع Thread-Safe في Godot 4.7: حل Network Bottlenecks في Godot 4.7 RC 3

نُشر في 17 يونيو 2026
تصميم تكامل Backend متوافق مع Thread-Safe في Godot 4.7: حل Network Bottlenecks في Godot 4.7 RC 3

باختصار

يقدم هذا المقال دليلاً شاملاً لمطوري الألعاب حول كيفية بناء تكامل backend متوافق مع thread-safety في محرك Godot 4.7. يستعرض المقال الإصدار الأخير Godot 4.7 RC 3 والإصلاحات الحيوية المضمنة فيه، مثل معالجة regressions في Jolt Physics والـ AssetLib. كما يوفر حلاً عملياً بالـ GDScript لإنشاء HTTP request pool يمنع حدوث تداخل الطلبات وانهيار الـ main thread. وأخيراً، يتطرق المقال إلى كيفية التغلب على قيود منصات الويب والهواتف المحمولة مع مقارنة البناء الذاتي للـ game servers بالحلول المدارة مثل [horizOn](https://horizon.pm).

كل مطور مستقل قام بإطلاق لعبة Multiplayer في Godot يعرف تماماً تلك اللحظة التي يُصدر فيها الـ HTTP client الخاص به خطأ connection timeout صامتاً أو thread-safety exception. غالباً ما تظل هذه المشكلات غير مرئية أثناء الـ local editor play، لتتسبب لاحقاً في انهيار الـ production build عندما يقوم مئات الـ concurrent players بطلب الـ login endpoints. حل هذه المشاكل يتطلب فهماً عميقاً لـ asynchronous networking layer الخاصة بالمحرك.

وصول Godot 4.7 RC 3: رحلة الاستقرار وإصلاحات الـ Core Regressions

معالجة الـ Critical Regressions في الـ Release Candidate

يقربنا إصدار Godot 4.7 RC 3 أكثر من النسخة المستقرة المنتظرة بشدة. ففي الوقت الحالي الذي يمر فيه المحرك بمرحلة feature freeze، ركز فريق التطوير بالكامل على معالجة الـ regressions الحرجة التي تم اكتشافها خلال مرحلة الـ beta. بالنسبة للمطورين الذين يعملون مع external APIs، تضمن هذه الإصلاحات استقرار الـ core تحت دورات حياة التنفيذ المعقدة (execution lifecycles). وتحديداً، يقوم إصدار RC 3 بإصلاح bug في الـ stretch mode على custom_timeline في نظام الأنيميشن. كما يحل أيضاً مشاكل ظهور الأصول في الـ AssetLib حيث كانت الـ licenses المصنفة كـ "Other" يتم تصفيتها بشكل خاطئ. وأخيراً، سيقدر مطورو الـ XR إيجاد حل للانهيار الناتج عن الـ spatial entity marker trackers.

إصلاح الـ Area Event Queuing في Jolt Physics

أصبح Jolt هو الـ physics plugin الأساسي والمفضل لـ Godot 4.x نظراً لسرعته واستقراره. ومع ذلك، فإن الـ regression في إصدارات الـ beta فرض Area event queuing عند خروج الـ body (body exit)، مما يعني أنه في كل مرة يغادر فيها rigid body منطقة ما، يقوم المحرك بعمليات إدخال متكررة وغير ضرورية في الـ queue. وفي lobby للعبة Multiplayer سريعة الوتيرة تضم 64 لاعباً وعشرات الـ trigger areas، تسبب هذا الحمل الإضافي على الـ CPU (CPU overhead) في تدهور الـ server tick rates بسرعة من 60Hz إلى أقل من 20Hz. يضمن حل هذا الـ regression عدم تداخل فحوصات الـ local triggers مع الـ network thread.

إعادة هيكلة الـ AssetLib REST API

من الميزات البارزة الأخرى في Godot 4.7 هي إعادة هيكلة الـ Asset Library (AssetLib) API. تم نقل اتصال الـ backend إلى بنية REST حديثة، مما يحل مشاكل ترتيب الإصدارات وفشل التحميل. يُعد هذا الترقية نموذجاً يحتذى به لكيفية قيام مطوري الألعاب بهيكلة تكاملات الـ external API الخاصة بهم. من خلال استخدام endpoints واضحة و paginated requests، يمكنك تحسين أوقات التحميل لتوصيل المحتوى (content delivery). استخدام JSON arrays ذات هياكل schema موحدة يتجنب اختناقات إلغاء التسلسل (deserialization bottlenecks) في GDScript.

لماذا يتطلب تكامل الـ Backend في Godot 4.7 عناية معمارية خاصة

حدود الـ HTTPRequest Node

يتضمن دمج الـ backend مع Godot 4.7 إدارة العمليات غير المتزامنة (asynchronous operations) دون التسبب في اختناق الـ main thread الخاص باللعبة. يعتمد Godot على non-blocking nodes مثل HTTPRequest لإدارة الاستدعاءات، ولكن هذه الـ nodes تأتي مع عيب رئيسي: لا يمكنها معالجة الـ concurrent requests. فإذا حاولت استدعاء request() على node ينتظر حالياً استجابة، فسيقوم المحرك بإصدار خطأ. هذا الخطأ "Request already in progress" يمكن أن يعطل وظائف اللعب الأساسية إذا لم يتم التعامل معه.

الـ Thread Safety وتعديلات الـ SceneTree

لتجنب هذه التعارضات، يجب عليك بناء queue أو pool قوي يقوم بتخصيص الطلبات ديناميكياً للـ nodes الشاغرة. بالإضافة إلى ذلك، يمثل الـ thread safety تحدياً مستمراً عند معالجة استجابات الشبكة. فإذا حاول background network thread تعديل الـ SceneTree مباشرة، فسينهار Godot أو يتصرف بشكل غير متوقع. يجب عليك دائماً تأجيل تغييرات واجهة المستخدم (UI changes) وإرسالها إلى الـ main thread لضمان الاستقرار.

التغلب على قيود SSL/TLS و CORS

حماية بيانات اللاعبين أمر بالغ الأهمية؛ وكما تمت مناقشته في مقال تصميم أنظمة الـ game backends للنجاة من الاختراقات، تبدأ حماية البيانات بـ TLS وتوثيق قوي من جانب الخادم (server-side authentication). يجب أن تتم عملية الـ handshaking مع الـ backend الخاص بك عبر بروتوكولات آمنة مثل https:// أو wss://، مما يتطلب SSL/TLS certificate handshakes سليمة. على منصات الهواتف المحمولة (mobile platforms)، قد تؤدي أبسط الأخطاء في إعدادات الشبكة إلى قطع الاتصال بصمت. علاوة على ذلك، تضيف الـ web exports (HTML5) طبقة من التعقيد لأن المتصفحات تفرض قواعد صارمة لـ Cross-Origin Resource Sharing (CORS).

الكود: تطبيق Thread-Safe HTTP Request Pool في GDScript 2.0

تطبيق الـ Pool Manager باستخدام GDScript

يوفر الـ GDScript singleton التالي مدير HTTP يعتمد على الـ pool ويكون thread-safe يمنع أخطاء تداخل الطلبات (overlapping requests). يقوم الكود ديناميكياً بإنشاء pool من الـ HTTPRequest nodes ويقوم بجدولة الطلبات الواردة (queues incoming requests) عندما تكون جميع الـ nodes مشغولة. كما أنه يستخدم طرقاً آمنة لعمل الـ JSON parsing لتجنب حدوث runtime crashes نتيجة للبيانات غير المتوقعة القادمة من الخادم (server payloads).

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()

تفصيل آلية عمل الـ Pool والـ Queue

يقوم هذا المدير بتهيئة static array من الـ HTTPRequest child nodes أثناء استدعاء الـ _ready(). ومن خلال تقييد حجم الـ pool بقيمة ثابتة محددة مثل MAX_CONCURRENT_REQUESTS، فإنك تتحكم في ازدحام الشبكة من جانب العميل (client-side network congestion). يتم ربط كل node بمعالج استجابة مركزي (central response handler)، مع تمرير المرجع الخاص به (its own reference) لتتبع أي طلب قد انتهى.

تقوم الدالة send_request() بإضافة wrapper class يحتوي على الـ URL، والـ payload، والـ callable callback إلى FIFO queue. وعند اكتمال الطلب، يتم وسم الـ pool node على أنه idle، ويقوم المدير على الفور بمعالجة العنصر التالي في الـ queue. هذا يمنع خطأ تداخل الطلبات (overlapping requests error) تماماً.

معالجة الـ JSON Payloads بأمان في Godot 4.7

لقد تغيرت طريقة معالجة البيانات (Data parsing) في Godot 4.7 مقارنة بالإصدارات الرئيسية السابقة. نحن نقوم الآن بإنشاء كائن JSON جديد تماماً ونستدعي دالة parse() بدلاً من استخدام الدوال العامة القديمة (deprecated global functions). ومن خلال التحقق من أن json.data من نوع TYPE_DICTIONARY، فإننا نحمي العميل (client) من الانهيار في حال أرجع الـ backend استجابة تالفة أو غير صالحة (malformed response).

أخيراً، نستخدم call_deferred لتنفيذ الـ callback على الـ main thread. يضمن ذلك أن أي تحديثات لواجهة المستخدم (UI updates) أو تعديلات على الـ SceneTree ناتجة عن استجابة الشبكة ستتم بأمان. يمنع تشغيل هذه الـ callbacks بشكل غير متزامن (asynchronously) تعارضات الـ threading ويحافظ على استقرار وسلاسة معدل الإطارات (frame rates).

حل عقبات التكامل الخاصة بمنصات معينة: الويب والهواتف المحمولة

الـ Web Exports وقيود الـ WASM أحادية الخيوط (Single-Threaded)

لا تتصرف أهداف WebAssembly بنفس طريقة الـ desktop builds. ففي بيئات المتصفح، يعمل Godot في single-threaded loop ما لم يتم تعيين SharedArrayBuffer headers معينة. وإذا استخدمت عمليات HTTP متزامنة وحاصرة (synchronous blocking HTTP operations)، فستتوقف نافذة المتصفح بالكامل عن الاستجابة، مما يتسبب في تجربة مستخدم سيئة للغاية.

لتجنب ذلك، استخدم دائماً طلبات غير حاصرة وتعتمد على الإشارات (signal-driven, non-blocking requests) للاعبي الويب. يجب عليك أيضاً التأكد من تكوين الـ backend endpoints الخاصة بك لإرسال CORS headers المناسبة. وتحديداً، يجب أن تسمح الـ headers مثل Access-Control-Allow-Origin و Access-Control-Allow-Headers صراحة بالنطاق (domain) الذي يستضيف لعبة الويب الخاصة بك.

الـ Android Exports وصلاحيات الشبكة

تقدم منصات الهواتف المحمولة تحديات خاصة بها. فبالنسبة لـ Android exports، يُعد نسيان تفعيل صلاحية الـ INTERNET في الـ export preset خطأً شائعاً يعطل جميع اتصالات الشبكة. بالإضافة إلى ذلك، يقدم Godot 4.7 تخصيصات محسنة لـ Android splash screens وتغيير حجم النافذة (window resizing)، مما يساعد على منع حدوث مشكلات في الشاشة أثناء فحوصات الشبكة المبكرة عند بدء التشغيل.

إذا كنت تقوم بالتصدير لمنصات الهواتف المحمولة، فمن الضروري فهم البيئة التنظيمية. يتطلب التعامل مع الـ third-party billing إجراء عمليات API handshakes آمنة، كما هو موضح في دليلنا حول تصميم أنظمة الـ third party mobile billing. إن التأكد من أن billing callbacks والـ client handshakes آمنة يمنع اللاعبين من تخطي الـ microtransactions.

أفضل الممارسات لتوسيع نطاق تكامل الـ Backend في Godot 4.7

1. تطبيق Exponential Backoff مع Jitter

تجنب التحميل الزائد على خادمك عند حدوث عواصف إعادة الاتصال (reconnection storms). إذا فقد أحد اللاعبين الاتصال، فلا تحاول إعادة المحاولة فوراً بفواصل زمنية ثابتة. بدلاً من ذلك، ضاعف مهلة إعادة المحاولة بمقدار 1.5 أو 2.0 في كل مرة، وأضف إزاحة عشوائية صغيرة (jitter) لمنع جميع العملاء من محاولة الاتصال في نفس اللحظة.

على سبيل المثال، إذا كانت المحاولة الأولى عند 1.0 ثانية، فيجب أن تحدث المحاولات التالية عند 2.0 و 4.0 و 8.0 ثانية. ولتجنب قيام جميع العملاء المنقطعين بإعادة الاتصال في نفس الملي ثانية بالضبط، أضف قيمة عشوائية (random float) بين 0.1 و 0.5 ثانية لكل مهلة تأخير. يوزع هذا الأمر الحمل على سيرفرات الـ backend الخاصة بك ويمنع تعاقب فشل الـ API (cascading API failures) أثناء فترات انقطاع الخدمة.

2. التحقق من صحة الـ Payloads في كلا الطرفين

لا تثق أبداً ببيانات العميل (client data)، ولا تفترض أبداً أن بيانات السيرفر مصممة بشكل مثالي. تحقق من صحة جميع الـ dictionaries والـ keys قبل قراءتها في GDScript. وبالمثل، تأكد من قيام الـ backend بالتحقق من صحة الـ request bodies الواردة لمنع ثغرات الـ SQL injection أو ثغرات التشغيل عن بعد (remote execution).

من الثغرات الشائعة في ألعاب الـ multiplayer هي الثقة الزائدة في جانب العميل (client-side trust). إذا تلقى الـ client script الخاص بك dictionary من الـ backend، استخدم Dictionary.has() للتحقق من وجود كل key مطلوب قبل الوصول إليه. حيث أن محاولة الوصول لـ key مفقود في GDScript يطلق runtime error، مما يوقف تنفيذ الـ script بالكامل. يمنع هذا التحقق تعطل واجهة المستخدم (UI) الخاصة بك عندما يتم تحديث server endpoints.

3. فصل خدمات الشبكة عن الـ UI

تجنب كتابة أكواد الشبكة داخل أكواد الـ UI الخاصة بك. قم بإنشاء autoload singleton مخصص للتعامل مع جميع حركة مرور HTTP (HTTP traffic) وإدارة الحالة (state management). يجب أن يتصل الـ UI الخاص بك فقط بالـ signals المرسلة من هذا المدير، مما يحافظ على كود الـ frontend الخاص بك نموذجياً وقابلاً للاختبار (modular and testable).

على سبيل المثال، عندما يفتح اللاعب شاشة المخزن (inventory screen)، يجب أن يرسل الـ UI إشارة مخصصة (custom signal) لطلب البيانات. يلتقط الـ network autoload هذه الإشارة، ويقوم بإنشاء استدعاء HTTP، ويرسل إشارة نجاح (success signal) بمجرد تعبئة الـ dictionary. يستمع الـ UI لإشارة النجاح هذه لملء شبكة المخزن الخاصة به. يحافظ هذا الفصل (decoupling) على استجابة الـ UI ويجعل تصحيح أخطاء منطق الشبكة (debugging network logic) أمراً سهلاً ومباشراً.

4. إجراء اختبارات CORS Pre-Flight مبكرة

قم دائماً باختبار لعبتك على ويب سيرفر محلي مع تفعيل CORS قبل رفعها إلى منصات مثل itch.io. يكتشف العديد من المطورين بعد تجميع اللعبة (compiling the game) أن استدعاءات HTTP تفشل على الويب بسبب سياسات المصدر (origin policies). يمنع الاختبار المبكر حدوث حالات الطوارئ المتعلقة بالإعدادات يوم الإطلاق.

ترسل المتصفحات طلب HTTP من نوع OPTIONS كفحص pre-flight قبل تنفيذ طلبات الـ post. إذا لم يكن الـ backend الخاص بك مهيأً للاستجابة لـ OPTIONS بحالة 200 OK، فسيقوم المتصفح بحظر الطلب اللاحق. يجب عليك التحقق من الـ console logs (F12) الخاصة بالمتصفح لتشخيص مشاكل المصدر هذه (origin issues). إن اكتشاف هذه الأخطاء خلال مرحلة الـ staging يمنع حدوث مشاكل تسجيل الدخول في الـ multiplayer عند الإطلاق.

معيرة البنية التحتية للـ Backend: الكتابة اليدوية للكود مقابل الحلول المدارة

التكلفة الحقيقية لبناء الـ Game Servers من الصفر

يتطلب بناء backend مخصص للعبتك في Godot جهداً كبيراً. يجب عليك كتابة كود السيرفر، وتطبيق الـ JWT token authentication، وتهيئة فهارس قواعد البيانات (database indexing)، وإدارة الـ load balancers. قد يستغرق إعداد هذه البنية التحتية يدوياً ما بين 4 إلى 6 أسابيع من وقت التطوير المخصص، مما يشتت تركيزك عن أسلوب اللعب (gameplay). يتطلب الـ production backend الآمن تجميع قواعد البيانات (database clustering)، وانتهاء صلاحية رمز المصادقة (auth token expiration)، وعمليات ترحيل قواعد البيانات (database schema migrations). كما يتعين عليك أيضاً كتابة سكربتات مخصصة لمراقبة صحة السيرفر (server health) وتسجيل مشاكل الشبكة.

تسريع التطوير باستخدام horizOn

يزيل استخدام الـ horizOn SDK هذا العبء الإداري. تتعامل المنصة مع الـ user persistence والـ telemetry والـ leaderboards خلف الكواليس، مما يتيح لك التركيز على اللعبة. بدلاً من تصحيح أخطاء الـ HTTP connection pools، يمكنك إجراء استدعاءات API بسيطة تعمل على بنية تحتية محسّنة. يتيح لك ذلك تقديم ميزات جديدة لقاعدة لاعبيك في غضون دقائق بدلاً من أسابيع. قد يستغرق إعداد هذه البنية التحتية يدوياً من 4 إلى 6 أسابيع من وقت التطوير المخصص. ولكن مع horizOn، تأتي خدمات الـ backend هذه مجهزة مسبقاً، مما يسمح لك بإطلاق لعبتك بدلاً من إطلاق بنيتك التحتية.

جاهز لتوسيع نطاق الـ multiplayer backend الخاص بك؟ جرب horizOn مجاناً أو تصفح الـ API documentation للبدء في مشروعك القادم.


المصدر: Release candidate: Godot 4.7 RC 3