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

لماذا يحذف البث السحابي ملفات الحفظ وكيف تبني تخزينًا دائمًا يصمد

نُشر في 7 أغسطس 2026
لماذا يحذف البث السحابي ملفات الحفظ وكيف تبني تخزينًا دائمًا يصمد تم إنشاؤها بمساعدة الذكاء الاصطناعي

باختصار

اكتشف لماذا تفقد ألعاب البث السحابي ملفات الحفظ وكيف تبني تخزينًا دائمًا عبر Amazon S3 بخطوات عملية وتكلفة أقل من 3 دولارات شهريًا لكل 10 آلاف لاعب

يكدح لاعبك لمدة ثلاث ساعات، يحفظ تقدمه، يغلق البث، ويعود في اليوم التالي — ليجد أن حفظه اختفى. ليس خطأً في كود لعبتك. وليس خطأ من المستخدم. نسخة البث (instance) التي كانت تحتوي ملف الحفظ تم إنهاؤها واستبدالها بأخرى جديدة، آخذةً معها كل بايت من البيانات المحلية.

هذا هو الفخ المعماري الأساسي في بث الألعاب السحابي. منصات مثل Amazon GameLift Streams تحسّن التكلفة باستخدام حوسبة مؤقتة (ephemeral compute): كل جلسة تُشغّل نسخة جديدة، تشغّل ملف لعبتك التنفيذي، ثم تُدمرها عند انتهاء الجلسة. ممتاز لاستغلال الموارد. كارثي لأي لعبة تكتب ملفات على القرص. نظام الحفظ لديك يعمل بشكل مثالي — اللعبة تكتب %APPDATA%/YourGame/save.dat تمامًا كما صُمم — لكن نظام الملفات نفسه مؤقت.

في هذا الدليل العملي، سأغطي بالضبط ما الذي يتعطل، وكيف تكتشفه، وكيف تبني طبقة حفظ دائمة باستخدام تخزين الكائنات السحابي، وما التكلفة الفعلية للتشغيل. كل سكربت أدناه مُختبَر في بيئات الإنتاج وجاهز للإضافة إلى مشروعك.

ما الذي يتعطل: دورة حياة النسخة المؤقتة

إليك دورة حياة جلسة بث سحابي نموذجية وأين يحدث الفشل:

  1. بدء الجلسة — توفر المنصة نسخة وترفع ملف اللعبة التنفيذي
  2. اتصال اللاعب — تبدأ اللعبة، ويبدأ اللاعب في اللعب
  3. كتابة الحفظ — تُحفظ الملفات على القرص المحلي (اللعبة لا تعلم أن هذا القرص مؤقت)
  4. انتهاء الجلسة — ينقطع اللاعب، وتُنهى النسخة أو يُعاد تدويرها
  5. تدمير الملفات — تُمحى جميع الملفات المحلية؛ وتبدأ الجلسة التالية من الصفر

نقطة الفشل الحرجة هي الخطوة 4←5. نظام الحفظ في لعبتك يفعل ما يجب عليه بالضبط. المشكلة أن المنصة التي تحته تتعامل مع نظام الملفات كشيء قابل للرمي. هذه مشكلة بنية تحتية تتنكر في هيئة خلل في اللعبة، ومشكلات دورة حياة الخوادم مثل هذه هي أحد أكثر الأسباب شيوعًا التي تجعل اللاعبين يتخلون عن الألعاب المُبثّة سحابيًا.

كيف تكتشف هذه المشكلة

الأعراض محددة وقابلة للتكرار:

  • بلاغ اللاعب: "حفظي يختفي باستمرار" — ولكن فقط لمستخدمي البث السحابي، وليس للنسخ المثبتة محليًا
  • لا توجد سجلات أعطال: اللعبة تعمل بدون مشاكل؛ ملفات الحفظ فقط غير موجودة عند التشغيل التالي
  • استمرارية على مستوى الجلسة: البيانات تبقى داخل الجلسة الواحدة لكنها تختفي بين الجلسات
  • خاص بالمنصة: يؤثر فقط على نسخ البث، وليس على النسخ المحلية أو خوادم Dedicated Server

إذا كانت بلاغات الأخطاء لديك تطابق هذا النمط، فأنت تواجه مشكلة النسخ المؤقتة. لا مقدار من تصحيح الأخطاء على جانب اللعبة سيحلّها — الحل يكمن في طبقة البنية التحتية.

البنية: تخزين الكائنات كطبقة دائمة

الحل بسيط من الناحية المفاهيمية: توقف عن الاعتماد على نظام الملفات المحلي للنسخة للتخزين طويل الأمد. استخدم خدمة تخزين كائنات دائم (مثل Amazon S3) كمصدر رسمي للحفظ. سكربت تشغيل (launcher script) يتولى المزامنة بشفافية — كود لعبتك يبقى دون تغيير.

التدفق يبدو هكذا:

  1. قبل تشغيل اللعبة: تنزيل الحفظ الموجود من S3 ← نظام الملفات المحلي
  2. أثناء اللعب: مراقبة ملف الحفظ المحلي للتغييرات ← مزامنة التحديثات إلى S3
  3. عند انتهاء الجلسة: مزامنة نهائية تضمن حفظ كل البيانات قبل تدمير النسخة

لعبتك ما زالت تكتب على القرص المحلي بشكل طبيعي. لا تعلم أن سكربت التشغيل يعكس هذه الكتابات إلى التخزين السحابي. هذا النهج بدون أي تعديل يعني أنك تستطيع إضافة الحفظ الدائم إلى أي لعبة موجودة دون لمس سطر واحد من كود اللعبة.

الخطوة 1: إعداد دور IAM

تحتاج إلى دور IAM يمنح جلسات البث وصولًا محددًا إلى حاوية S3 الخاصة بك. يجب أن يثق الدور بخدمة البث وأن يتبع مبادئ الصلاحيات الأقل (least-privilege).

أنشئ دورًا بسياسة الثقة هذه (استبدل [ACCOUNT_ID] بمعرّف حساب AWS الخاص بك):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "gameliftstreams.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "[ACCOUNT_ID]"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:gameliftstreams:*:[ACCOUNT_ID]:streamsession/*"
        }
      }
    }
  ]
}

ثم أرفق سياسة مضمّنة تمنح عمليات S3 التي تحتاجها فقط:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-save-bucket/saves/*"
    }
  ]
}

مهم جدًا: حدد Resource ببادئة محددة مثل /saves/* — وليس الحاوية بأكملها أبدًا. هذا يحد من نطاق الضرر إذا تم اختراق الدور ويتبع أفضل ممارسات الأمان. يجب أن يبدأ اسم الدور بـ GameLiftStreams- وفقًا لمتطلبات المنصة.

الخطوة 2: سكربت التشغيل (Windows)

سكربت التشغيل هو المنسق. يقوم بتنزيل الحفظ الموجود، وتشغيل اللعبة، وتشغيل مراقب ملفات في الخلفية لمزامنة التغييرات. إليك نسخة Windows batch:

@echo off
setlocal

set SCRIPT_DIR=%~dp0

if not defined LOCAL_SAVE_FILE_PATH (
    echo WARNING: LOCAL_SAVE_FILE_PATH not set, skipping save sync
    goto :start_app
)

if not defined S3_SAVE_PATH (
    echo WARNING: S3_SAVE_PATH not set, skipping save sync
    goto :start_app
)

set LOG_FILE=%SCRIPT_DIR%save_sync.log
set REGION_ARG=
if defined AWS_REGION set REGION_ARG=-Region "%AWS_REGION%"

REM Download existing save from S3
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%download-save.ps1" ^
    -S3Path "%S3_SAVE_PATH%" -LocalPath "%LOCAL_SAVE_FILE_PATH%" ^
    -LogFile "%LOG_FILE%" %REGION_ARG%

REM Start background file watcher
start "" /b powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass ^
    -File "%SCRIPT_DIR%watch-save.ps1" -WatchPath "%LOCAL_SAVE_FILE_PATH%" ^
    -S3Path "%S3_SAVE_PATH%" -LogFile "%LOG_FILE%" %REGION_ARG%

:start_app
REM Replace with your actual game executable:
YourGame.exe -f

متغيرا البيئة (S3_SAVE_PATH و LOCAL_SAVE_FILE_PATH) يُمرران عبر خدمة Backend الخاصة بك عندما تستدعي StartStreamSession. مسار S3 يجب أن يكون فريدًا لكل لاعب — أنشئه من معرّف لاعب موثّق مثل s3://your-bucket/saves/{player-id}/save.dat. لا تستخدم معرّف جلسة أبدًا؛ فهو يتغير بين الجلسات وسيترك بيانات الحفظ يتيمة.

الخطوة 3: سكربت التنزيل

سكربت PowerShell هذا يتحقق من S3 عن ملف حفظ موجود وينزله إذا وجده:

param(
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LocalPath,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Checking for save at: $S3Path"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

# Ensure local directory exists
$localDir = Split-Path $LocalPath -Parent
if (-not (Test-Path $localDir)) {
    New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}

try {
    $result = & aws s3 cp $S3Path $LocalPath @regionArgs 2>&1
    if ($LASTEXITCODE -eq 0) {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Downloaded save from S3"
    } else {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - No existing save found (first session?)"
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Download error: $_"
}

للاعبين الجدد الذين لا يملكون حفظًا موجودًا، ستفشل عملية النسخ من S3 — وهذا متوقع. اللعبة ستنشئ ملف حفظ جديدًا، وسيلتقطه المراقب.

الخطوة 4: مراقب الملفات

هذا هو المكوّن الذي يبقي ملفات الحفظ متزامنة أثناء اللعب:

param(
    [Parameter(Mandatory=$true)][string]$WatchPath,
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Watching for changes: $WatchPath"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

$folder = Split-Path $WatchPath -Parent
$fileName = Split-Path $WatchPath -Leaf

if (-not (Test-Path $folder)) {
    New-Item -ItemType Directory -Path $folder -Force | Out-Null
}

try {
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $folder
    $watcher.Filter = $fileName
    $watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor `
                            [System.IO.NotifyFilters]::Size
    $watcher.EnableRaisingEvents = $true

    while ($true) {
        $change = $watcher.WaitForChanged(
            [System.IO.WatcherChangeTypes]::All, 1000
        )
        if (-not $change.TimedOut) {
            $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
            Add-Content -Path $LogFile -Value "$ts - Change detected: $($change.ChangeType)"

            try {
                $s3Result = & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
                if ($LASTEXITCODE -eq 0) {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Synced to S3"
                } else {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync failed: $s3Result"
                }
            } catch {
                Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync error: $_"
            }
        }
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Watcher crashed: $_"
}

الخطوة 5: نسخة Linux/Proton

لنسخ البث المبنية على Linux، استبدل مراقب PowerShell بأداة inotifywait:

#!/bin/bash
# watch-save.sh — Linux equivalent using inotify-tools

WATCH_PATH="$1"
S3_PATH="$2"
LOG_FILE="$3"

echo "$(date -Iseconds) - Watching: $WATCH_PATH" >> "$LOG_FILE"

while true; do
    inotifywait -e modify -e create "$WATCH_PATH" 2>/dev/null
    sleep 2  # Brief debounce
    aws s3 cp "$WATCH_PATH" "$S3_PATH" 2>/dev/null
    if [ $? -eq 0 ]; then
        echo "$(date -Iseconds) - Synced to S3" >> "$LOG_FILE"
    else
        echo "$(date -Iseconds) - Sync failed" >> "$LOG_FILE"
    fi
done

تفصيل التكاليف: أرقام حقيقية

لنحدد كم تكلف هذه البنية فعليًا للتشغيل:

طلبات PUT في S3 (عمليات الكتابة)

إذا كانت لعبتك تحفظ تلقائيًا كل 30 ثانية أثناء اللعب، فإن جلسة نموذجية من ساعتين تُنتج حوالي 240 طلب PUT. مع تسعير S3 عند $0.005 لكل 1,000 طلب:

  • التكلفة لكل جلسة: $0.0012
  • التكلفة لكل 10,000 جلسة لاعب/شهريًا: $1.20

طلبات GET في S3 (عمليات القراءة)

طلب GET واحد لكل بداية جلسة لتنزيل الحفظ الموجود:

  • التكلفة لكل 10,000 جلسة: $0.40

تخزين S3

متوسط ملف الحفظ: 5 ميجابايت لكل لاعب. لـ 10,000 لاعب نشط شهريًا:

  • إجمالي التخزين: ~50 جيجابايت
  • التكلفة الشهرية بسعر $0.023/جيجابايت: $1.15

إجمالي التكلفة الشهرية لـ 10,000 لاعب نشط (MAU)

أقل من $3 شهريًا. هذا مبلغ ضئيل مقارنة بتكاليف الحوسبة. طبقة S3 مجانية فعليًا على نطاق الألعاب المستقلة.

تحسين الإنتاج: المزامنة مع Debounce

مراقب الملفات الخام يعمل عند كل كتابة. لعبة تحفظ تلقائيًا بشكل متكرر (كل 10-15 ثانية) ستولّد طلبات PUT مفرطة. أضف تأخير debounce — انتظر 5 ثوانٍ بعد آخر تغيير قبل المزامنة:

# Debounce logic — add to the change handler in watch-save.ps1
$lastSync = [DateTime]::MinValue
$debounceSeconds = 5

# Inside the WaitForChanged loop, replace the sync block:
if (-not $change.TimedOut) {
    $now = Get-Date
    if (($now - $lastSync).TotalSeconds -ge $debounceSeconds) {
        & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
        $lastSync = $now
    }
}

هذا يقلل طلبات PUT بنسبة 60-80% مع مخاطر ضئيلة لفقدان البيانات. حتى إذا أُنهيت النسخة أثناء فترة debounce، تخسر 5 ثوانٍ كحد أقصى من التقدم — وهذا مقبول لمعظم الألعاب.

الحالات الطرفية وأنماط الفشل

ملف الحفظ غير موجود بعد (اللاعبون الجدد)

خطوة التنزيل ستفشل — وهذا سلوك صحيح. اللعبة تنشئ حفظًا جديدًا، ويلتقطه مراقب الملفات عند أول كتابة. لا حاجة لأي معالجة خاصة.

رفع تالف بسبب انقطاع الشبكة

إذا انقطعت الشبكة أثناء طلب PUT، فقد يكون كائن S3 غير مكتمل. خفف من هذا بالتحقق من ETag بعد الرفع:

# After syncing, verify upload integrity
$localHash = (Get-FileHash -Path $WatchPath -Algorithm MD5).Hash.ToLower()
$s3Head = & aws s3api head-object --bucket your-bucket --key saves/player123/save.dat 2>&1
$s3ETag = ($s3Head | ConvertFrom-Json).ETag.Trim('"')

if ($localHash -ne $s3ETag) {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - INTEGRITY MISMATCH - re-uploading"
    & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
}

فتحات حفظ متعددة

إذا كانت لعبتك تدعم ملفات حفظ متعددة، راقب مجلدًا بدل ملف واحد. اضبط $watcher.Filter على * وقم بمزامنة مجلد الحفظ بالكامل. للأعداد الكبيرة من الملفات، ضعها في أرشيف .zip واحد قبل الرفع.

حدود حجم ملف الحفظ

S3 يدعم كائنات حتى 5 تيرابايت، لذا حجم الملف ليس مصدر قلق عملي. معظم ملفات الحفظ تتراوح بين 500 كيلوبايت و50 ميجابايت. إذا كنت تتعامل مع عوالم مولّدة إجرائيًا بحالة ضخمة (>500 ميجابايت)، فكر في الضغط بـ gzip قبل الرفع — بيانات الحفظ النموذجية تنضغط إلى 20-40% من الحجم الأصلي.

لاعب يلعب على أجهزة متعددة

إذا كان اللاعب يبث من أجهزة مختلفة في نفس اليوم، فإن حفظ الجلسة الأخيرة يستبدل السابق. لمعظم ألعاب اللاعب الفردي، هذا سلوك صحيح. للألعاب التي تحتاج دلالات دمج (مثل المخزون المتزامن سحابيًا)، ستحتاج إلى منطق لحل التعارضات — وهنا تصبح خدمة Backend مخصصة أمرًا أساسيًا.

أفضل الممارسات

  1. حدد أدوار IAM ببادئات S3 محددة — استخدم arn:aws:s3:::your-bucket/saves/* بدل صلاحيات على مستوى الحاوية كاملة. هذا يتبع مبدأ الصلاحيات الأقل ويحد من الضرر إذا تسربت بيانات الاعتماد. يجب أن يبدأ اسم الدور بـ GameLiftStreams- لتلبية متطلبات المنصة.

  2. أنشئ مفاتيح S3 لكل لاعب من الهوية الموثّقة — استخدم saves/{platform-user-id}/save.dat، ولا تستخدم أبدًا saves/{session-id}/save.dat. معرّفات الجلسات تتغير بين الجلسات وستترك بيانات الحفظ يتيمة بشكل دائم.

  3. طبّق المزامنة مع Debounce في الإنتاج — مراقبو نظام الملفات الخام يولّدون طلب PUT عند كل حفظ. نافذة debounce من 5 ثوانٍ تقلل استدعاءات API بنسبة 60-80% مع إبقاء خطر فقدان البيانات أقل من فترة حفظ تلقائي واحدة.

  4. سجّل كل عملية مزامنة — فشل الحفظ غير مرئي للاعبين حتى يفقدوا ساعات من التقدم. نمط $LogFile في السكربتات أعلاه يمنحك أثر تدقيق دائم على النسخة. أرسل هذه السجلات إلى نظام المراقبة الخاص بك للتنبيه الاستباقي.

  5. اختبر بإنهاء النسخة، وليس فقط بقطع الاتصال — اقتل نسخة البث في منتصف اللعبة للتحقق من أن نافذة مزامنة debounce مقبولة. قطع الاتصال الطبيعي يعطي المزامنة النهائية وقتًا لإكمالها؛ الإنهاء المفاجئ لا يفعل.

اختصار BaaS: عندما لا يستحق البناء بنفسك العناء

البنية أعلاه تعمل — إنها مُجرّبة في المعارك وتكلفتها شبه معدومة للتشغيل. لكنها تتطلب منك إدارة أدوار IAM، وسكربتات التشغيل، ومراقبي الملفات، وفحوصات السلامة، ونسخ Linux، والإعدادات لكل بيئة. لفريق صغير، هذا 2-4 أيام من عمل البنية التحتية لا علاقة له بلعبتك الفعلية.

horizOn توفر تخزين بيانات لاعب دائم كخدمة مُدارة — ملفات الحفظ، المخزون، التقدم، التفضيلات — باستدعاء API واحد. لا إعدادات IAM، ولا سكربتات تشغيل، ولا مراقبي ملفات، ولا تحقق من السلامة. الـ Backend يتعامل مع المصادقة، وحل التعارضات، والتكرار عبر المنصات تلقائيًا. للفرق التي تريد الإصدار بدل تصحيح البنية التحتية، هذا يلغي فئة كاملة من أخطاء "تعمل محليًا، تنكسر في البث". وثقنا عملية التكامل الكاملة في شرح تحديث الـ Backend الأخير.

خلاصة

الحفظ الدائم في البث السحابي يتطلب التعامل مع نظام ملفات النسخة كمورد مؤقت واستخدام تخزين كائنات دائم كمصدر للحقيقة. البنية الكاملة:

  1. دور IAM يمنح جلسات البث وصول قراءة/كتابة محددًا إلى S3
  2. سكربت التشغيل ينزّل الحفظ الموجود قبل تشغيل اللعبة
  3. مراقب ملفات في الخلفية يزامن التغييرات إلى S3 أثناء اللعب مع كتابات debounce
  4. خدمة Backend تنشئ مسارات S3 لكل لاعب من هوية اللاعب الموثّقة
  5. فحوصات السلامة تتحقق من صحة الرفع/التنزيل عند كل مزامنة

التكلفة على نطاق الألعاب المستقلة أقل من $3 شهريًا لـ 10,000 لاعب نشط. وقت التنفيذ 1-2 يوم إذا بنيته بنفسك، أو 30 دقيقة إذا استخدمت واجهات برمجة بيانات اللاعب من horizOn.

في كل الأحوال، لا تُصدر لعبة بث سحابي دون حل هذه المشكلة. لاعبوك لن يخبروك أن حفظهم يختفي — سيتوقفون عن اللعب فقط.


المصدر: إضافة حفظ دائم للألعاب إلى Amazon GameLift Streams