الإصدار الأول — 2026

Luau & Roblox Studio

الدليل الشامل من الصفر إلى الاحتراف

كتاب عربي متكامل لتعلم برمجة ألعاب Roblox من الأساسيات تمامًا حتى بناء أنظمة احترافية كاملة: القتال، الواجهات، حفظ البيانات، الحماية من المخترقين، وتنظيم المشاريع.

للمبتدئين · للمطورين · لكل من يريد بناء لعبة Roblox حقيقية

فهرس المحتويات

  1. مقدمة إلى Roblox و Roblox Studio و Luau3
  2. واجهة Roblox Studio والخدمات الأساسية9
  3. Script و LocalScript و ModuleScript — و Server/Client16
  4. أساسيات Luau: أول سكربت، print، التعليقات، المتغيرات23
  5. أنواع البيانات Data Types بالتفصيل30
  6. العمليات Operators38
  7. الشروط Conditions44
  8. الحلقات Loops50
  9. الدوال Functions57
  10. الجداول Tables65
  11. النصوص Strings74
  12. الـ Instances في Roblox80
  13. الخصائص Properties88
  14. الأحداث Events93
  15. Players و Characters100
  16. Client و Server — المفهوم الأهم107
  17. RemoteEvents114
  18. RemoteFunctions121
  19. برمجة الواجهات GUI / UI126
  20. UserInputService — إدخال اللاعب135
  21. ContextActionService141
  22. TweenService — الحركة والأنيميشن146
  23. RunService152
  24. Vector3 و CFrame157
  25. الفيزياء Physics165
  26. Raycasting — الكشف بالأشعة171
  27. الأدوات Tools والأسلحة178
  28. نظام القتال M1 Combat184
  29. الأنيميشن Animations193
  30. DataStore — حفظ البيانات199
  31. Leaderstats208
  32. ModuleScripts213
  33. البرمجة الكائنية OOP في Luau220
  34. Metatables228
  35. نظام الأنواع Type Checking234
  36. Luau المتقدم: Coroutines و task و pcall240
  37. تحسين الأداء Performance248
  38. الأمان ومكافحة الاستغلال Anti-Exploit255
  39. تنظيم المشاريع Project Organization263
  40. مشاريع حقيقية كاملة270
  41. تصحيح الأخطاء Debugging290
  42. أشهر أخطاء المبتدئين وحلولها296
  43. أفضل الممارسات Best Practices303
  44. تمارين شاملة308
  45. حلول التمارين315
  46. مشروع التخرج الكامل325
  47. قاموس المصطلحات340
  48. خاتمة348

الفصل الأول: مقدمة إلى Roblox و Roblox Studio و Luau

1.1 ما هو Roblox؟

Roblox هي منصة ألعاب ضخمة تتيح للمستخدمين إنشاء ألعابهم الخاصة ونشرها للعالم كله. الفرق الأساسي بين Roblox وأي منصة أخرى هو أن اللاعبين أنفسهم هم من يصنعون الألعاب، وليس شركة واحدة. الملايين من اللاعبين يدخلون يوميًا لتجربة ألعاب صنعها أشخاص مثلهم — من عمر 10 سنوات إلى محترفين يعملون بدوام كامل.

داخل Roblox، لا تشتري "لعبة واحدة" بل تدخل إلى تجارب (Experiences) متنوعة: ألعاب قتال، ألعاب مغامرات، محاكاة، ألعاب سباق، ألعاب بناء، وأكثر من ذلك بكثير. كل هذه الألعاب صُنعت بأداة واحدة اسمها Roblox Studio.

1.2 ما هو Roblox Studio؟

Roblox Studio هو البرنامج المجاني الذي تستخدمه لإنشاء ألعاب Roblox. هو بيئة تطوير كاملة (IDE) تحتوي على:

📘 ملاحظة Roblox Studio مجاني تمامًا، ويعمل على Windows و macOS. لا تحتاج إلى أي خبرة سابقة في البرمجة لتبدأ — سنبني كل شيء من الصفر.

1.3 ما هي Luau؟

Luau (تُلفظ "لو-آو") هي لغة البرمجة المستخدمة داخل Roblox Studio. هي نسخة مطوّرة ومحسّنة من لغة Lua الشهيرة، وقد طوّرتها شركة Roblox خصيصًا لتناسب احتياجات الألعاب الحديثة.

Luau هي اللغة التي ستكتب بها كل شيء: حركة اللاعب، الأسلحة، الواجهات، حفظ البيانات، الأنظمة، المنطق، وكل ما يجعل اللعبة "حية".

لماذا Luau وليس لغة أخرى؟

1.4 الفرق بين Lua و Luau

كثيرًا ما يخلط المبتدئون بين الاثنين. الفرق باختصار:

الميزةLuaLuau
المطوّرفريق Lua العالميشركة Roblox
الاستخدام الأساسيبرامج عامة وألعابRoblox فقط
السرعةجيدةأسرع بكثير (JIT + تحسينات)
نظام الأنواعلا يوجديوجد (Type Checking)
ميزات حديثةمحدودةcontinue، +=، وغيرها
التوافقمع أي تطبيق Luaمع Roblox Studio فقط
💡 نصيحة إذا قرأت درسًا قديمًا عن Lua على الإنترنت، فغالبًا سينفعك في Luau لأن الأساس واحد. لكن احذر من بعض الفروقات الصغيرة التي سنوضحها.

1.5 كيف تعمل ألعاب Roblox؟

عندما يشغّل اللاعب لعبتك، يحدث التالي:

  1. اللاعب يفتح اللعبة من تطبيق Roblox.
  2. جهازه يتصل بـ سيرفر (Server) تابع لـ Roblox يشغّل نسخة من لعبتك.
  3. السيرفر يُرسل للاعب نسخة من المشهد (Parts، Terrain، إلخ).
  4. جهاز اللاعب (الـ Client) يعرض اللعبة ويتعامل مع المدخلات.
  5. كل ما يحدث يتم التنسيق بين السيرفر والـ Client عبر شبكة Roblox.

هذا المفهوم Server ↔ Client هو أهم مفهوم في برمجة Roblox، وسنخصص له فصلاً كاملاً لاحقًا. لكن الآن تذكّر فقط:

⚠️ تحذير مهم جدًا أي شيء يحدث على الـ Client يمكن للاعب تعديله. لذلك القرارات المهمة (المال، الضرر، النقاط) يجب أن تكون دائمًا في السيرفر. سنتعلم هذا بالتفصيل.

1.6 تمارين الفصل الأول

🟢 تمرين سهل اذكر ثلاثة فروقات بين Lua و Luau.
🟡 تمرين متوسط اشرح بأسلوبك لماذا لا يجب الوثوق بالـ Client في قرارات مثل "منح اللاعب 1000 عملة".
🔴 تمرين متقدم تخيّل لعبة قتال. حدّد خمس مهام يجب أن تحدث في السيرفر، وخمس مهام يمكن أن تحدث في الـ Client.

الفصل الثاني: واجهة Roblox Studio والخدمات الأساسية

2.1 تحميل Roblox Studio وتشغيله

حمّل Roblox Studio من الموقع الرسمي لـ Roblox، ثم افتحه وسجّل الدخول بحساب Roblox. بعد الدخول، اضغط على New لإنشاء مشروع جديد. ستظهر لك خريطة افتراضية فيها منصة (Baseplate).

2.2 واجهة البرنامج

الواجهة مقسّمة إلى مناطق رئيسية. تَعرّف عليها:

1) Explorer (المستكشف)

يعرض كل عناصر المشروع على شكل شجرة. من هنا تستطيع رؤية كل شيء داخل اللعبة: اللاعبين، الإضاءة، الأصوات، السكربتات، المجلدات… كل شيء.

2) Properties (الخصائص)

عند تحديد أي عنصر في Explorer، تظهر خصائصه في Properties. مثلاً إذا حددت Part، ستظهر خصائصه مثل الحجم واللون والموقع والشفافية.

3) Output (الإخراج)

نافذة تعرض الرسائل والأخطاء. هي أهم نافذة عند تصحيح أخطائك — سنعود إليها كثيرًا في فصل Debugging.

4) Toolbar (شريط الأدوات)

يحتوي على أدوات التحكم في المشهد: Select، Move، Scale، Rotate، وأدوات أخرى. جرّبها على منصة البداية Baseplate.

5) Viewport (منطقة العرض)

هي النافذة الكبيرة في الوسط حيث ترى العالم ثلاثي الأبعاد وتتحرك فيه.

💡 نصيحة استخدم زر Play لتشغيل اللعبة في وضع الاختبار. يمكنك التبديل بين "Server" و "Client" في نافذة صغيرة أعلى الشاشة لرؤية ما يحدث من منظور كل جهة.

2.3 الخدمات الأساسية في Explorer

عند فتح مشروع جديد سترى قائمة بأهم الخدمات (Services). كل خدمة لها وظيفة محددة، ومن المهم جدًا أن تعرف أين تضع كل شيء:

الخدمةالوظيفةماذا توضع فيها؟
Workspace العالم ثلاثي الأبعاد Parts، Models، Terrain، كل ما تراه في اللعبة
ServerScriptService سكربتات السيرفر Scripts العادية (منطق اللعبة، الحماية)
ServerStorage تخزين للسيرفر فقط أسلحة، أدوات، نماذج لا يراها اللاعب
ReplicatedStorage مشترك بين السيرفر والـ Client Modules، RemoteEvents، Assets مشتركة
StarterGui واجهات البداية ScreenGuis التي تظهر لكل لاعب
StarterPlayer إعدادات اللاعب الافتراضية StarterPlayerScripts، StarterCharacterScripts
StarterPack حقيبة البداية Tools التي يحصل عليها اللاعب عند الدخول
Lighting الإضاءة والجو Atmosphere، Sky، SunRays
SoundService إدارة الأصوات مجموعات الأصوات، SoundGroups
Teams الفرق فريق أحمر، فريق أزرق…
Terrain الأرض والتضاريس الجبال، المياه، الأرضيات الطبيعية
⚠️ خطأ شائع وضع سكربت سيرفر داخل Workspace سيؤدي إلى تشغيله لكن تصبح إدارته صعبة. القاعدة الذهبية: ضع كل شيء في مكانه الصحيح من البداية.

2.4 كيف تُشغّل سكربتك الأول؟

لنقم بتجربة سريعة. اتبع هذه الخطوات:

  1. في Explorer، مرّر على ServerScriptService واضغط +.
  2. اختر Script.
  3. ستظهر نافذة سكربت جديدة. اكتب داخلها:
print("مرحبًا بك في Roblox Studio!")
  1. اضغط Play أعلى الشاشة.
  2. افتح نافذة Output وسترى الرسالة.
📘 ملاحظة print هي أول دالة تتعلمها. وظيفتها طباعة أي شيء في نافذة Output. سنستخدمها كثيرًا لاحقًا للتصحيح والتجربة.

2.5 تمارين الفصل الثاني

🟢 تمرين سهل افتح Explorer وتحقق من وجود الخدمات التالية: Workspace، ServerScriptService، ReplicatedStorage، StarterGui.
🟡 تمرين متوسط أنشئ Part داخل Workspace، غيّر لونه وحجمه من Properties، ثم شغّل اللعبة.
🔴 تمرين متقدم أنشئ Script داخل ServerScriptService واكتب فيه كودًا يطبع اسم اللعبة باستخدام game.Name، ثم افتح Output وتحقق من النتيجة.

الفصل الثالث: Script و LocalScript و ModuleScript — و Server/Client

3.1 الأنواع الثلاثة للسكربتات

في Roblox Studio، لديك ثلاثة أنواع رئيسية من السكربتات. الفرق بينها ليس شكليًا — بل يحدد أين يعمل الكود ومن يستطيع رؤيته:

النوعالمكانماذا يفعل
Script السيرفر يدير منطق اللعبة، يحفظ البيانات، يحمي من المخترقين
LocalScript الـ Client يتعامل مع الواجهات، المدخلات، التأثيرات البصرية
ModuleScript أي مكان مكتبة كود قابلة لإعادة الاستخدام (لا يعمل وحده)

أين يوضع كل نوع؟

النوعالأماكن المسموحة
Script ServerScriptService، Workspace، ServerStorage
LocalScript StarterPlayerScripts، StarterCharacterScripts، StarterGui، StarterPack
ModuleScript ReplicatedStorage، ServerScriptService، ServerStorage، أي مكان
⚠️ خطأ شائع جدًا وضع LocalScript في ServerScriptService لن يعمل أبدًا. هذه أشهر مشكلة يواجهها المبتدئ: "كتبت كودًا ولا يعمل!" — السبب غالبًا أنه في المكان الخطأ.

3.2 Server و Client — الفرق الجوهري

ما هو Server؟

هو الجهاز المركزي الذي يشغّل نسخة واحدة من اللعبة. كل اللاعبين المتصلين يرون نفس السيرفر. السيرفر هو المصدر الوحيد للحقيقة في اللعبة.

ما يستطيع Server فعله:

ما هو Client؟

هو جهاز كل لاعب على حدة. كل لاعب له Client خاص به. الـ Client مسؤول عن العرض والتفاعل السريع، لكنه غير موثوق أمنيًا.

ما يستطيع Client فعله:

⚠️ القاعدة الذهبية لا تثق أبدًا بالـ Client. أي لاعب يستطيع تعديل أي شيء في الـ Client باستخدام برامج خارجية. لذلك:
• لا تحسب الضرر في العميل.
• لا تمنح المال في العميل.
• لا تتحقق من صحة الحركة في العميل.
• اجعل كل قرار مهم في السيرفر.

3.3 ReplicatedStorage — الجسر بين الطرفين

ReplicatedStorage مكان خاص: كل ما يوضع فيه يظهر لكل من السيرفر والـ Client. لهذا نضع فيه:

💡 نصيحة اجعل دائمًا بنية ReplicatedStorage منظمة:
ReplicatedStorage
├── Remotes
│   ├── DamageEvent
│   └── PurchaseEvent
├── Modules
│   ├── Utilities
│   └── Config
└── Assets
    ├── Sounds
    └── Animations

3.4 مثال شامل: Server و Client يتكلمان

لنرَ مثالًا بسيطًا يوضح الفرق.

السيرفر — داخل ServerScriptService

-- ServerScriptService/HelloServer.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

-- إنشاء RemoteEvent برمجيًا
local helloEvent = Instance.new("RemoteEvent")
helloEvent.Name = "HelloEvent"
helloEvent.Parent = ReplicatedStorage

Players.PlayerAdded:Connect(function(player)
    print("اللاعب " .. player.Name .. " دخل اللعبة.")

    -- إرسال رسالة للاعب نفسه فقط
    helloEvent:FireClient(player, "مرحبًا " .. player.Name .. "!")
end)

الـ Client — داخل StarterPlayerScripts

-- StarterPlayer/StarterPlayerScripts/HelloClient.client.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local helloEvent = ReplicatedStorage:WaitForChild("HelloEvent")

helloEvent.OnClientEvent:Connect(function(message)
    print("استقبلت من السيرفر: " .. message)
end)
📘 شرح الكود السيرفر يستمع لدخول اللاعبين عبر PlayerAdded، وعند دخول لاعب يُرسل له رسالة عبر FireClient.
الـ Client ينتظر وصول الحدث عبر OnClientEvent ويطبع الرسالة. لاحظ أن الـ Client لا يستطيع قراءة الرسالة قبل وصولها، ولهذا نستخدم WaitForChild.

3.5 تمارين الفصل الثالث

🟢 تمرين سهل ضع Script في ServerScriptService يطبع "أنا في السيرفر"، وضع LocalScript في StarterPlayerScripts يطبع "أنا في العميل". شغّل اللعبة ولاحظ الفرق.
🟡 تمرين متوسط أنشئ RemoteEvent باسم TestEvent في ReplicatedStorage، وأرسل من السيرفر رسالة للاعب عند دخوله.
🔴 تمرين متقدم اشرح لماذا لا يجب حساب الضرر في LocalScript حتى لو كان أسرع في التنفيذ.

الفصل الرابع: أساسيات Luau — أول سكربت، print، التعليقات، المتغيرات

4.1 أول سكربت حقيقي

لنكتب كودًا بسيطًا يطبع رسائل متعددة:

print("أول رسالة")
print("ثاني رسالة")
print(2 + 3)
print("النتيجة:", 2 + 3)

عندما تشغّل هذا في Script داخل ServerScriptService، ستظهر في Output:

أول رسالة
ثاني رسالة
5
النتيجة: 5
📘 شرح print print() تأخذ أي عدد من القيم وتطبعها مفصولة بمسافة. مفيدة جدًا لمعرفة ما يحدث داخل الكود لحظة بلحظة.

4.2 التعليقات Comments

التعليقات سطور لا ينفذها Luau. تُستخدم لشرح الكود لنفسك أو لغيرك:

-- تعليق من سطر واحد

--[[
   تعليق
   متعدد
   الأسطر
]]

print("هذا الكود يعمل") -- تعليق بجانب السطر
💡 نصيحة اكتب تعليقات قصيرة وواضحة، لكن لا تُكثر منها في السطور البديهية. التعليق الجيد يشرح "لماذا" لا "ماذا".

4.3 المتغيرات Variables

المتغير هو صندوق يحمل قيمة، ويمكن استخدامه لاحقًا:

local name = "Ahmed"
local age = 15
local isStudent = true

print(name, age, isStudent)

الكلمة local تعني: أنشئ متغيرًا محليًا (Local Variable). المحلي هو المتغير الذي يعيش داخل نطاق معين فقط (Scope).

الفرق بين local و Global

في Luau، يمكنك أيضًا إنشاء متغير بدون local، فيصبح Global (عام):

myGlobalVar = 100    -- Global (غير محبب)
local myLocalVar = 50 -- Local (الأفضل)
النوعالمدىالأداءالتوصية
Localداخل النطاق الحاليأسرع✅ استخدمه دائمًا
Globalفي كل المشروعأبطأ❌ تجنّبه
⚠️ قاعدة ذهبية استخدم local دائمًا إلا لو كان لديك سبب خاص جدًا. Global يُبطئ الكود ويسبب مشاكل لا يمكن تتبعها.

4.4 إعادة تعيين المتغيرات

local score = 0
print(score)  -- 0

score = 10
print(score)  -- 10

score = score + 5
print(score)  -- 15

4.5 التسمية الصحيحة للمتغيرات

اتفق مطورو Roblox على أساليب تسمية واضحة:

النوعالأسلوبمثال
متغير عاديcamelCaseplayerScore
ثابتUPPER_SNAKEMAX_HEALTH
دالةcamelCasegetPlayerData
ModulePascalCasePlayerDataModule
💡 نصيحة اختر أسماء واضحة: playerHealth أفضل بكثير من ph أو x1. ستشكر نفسك بعد أسبوعين عندما تعود لكودك.

4.6 تمارين الفصل الرابع

🟢 تمرين سهل أنشئ متغيرات: اسمك، عمرك، مدينتك، واطبعها في Output.
🟡 تمرين متوسط أنشئ متغير coins قيمته 100، ثم أضف إليه 50، ثم اطرح 30، واطبع النتيجة النهائية.
🔴 تمرين متقدم اشرح لماذا المتغير Local أسرع من Global، ولماذا يُفضل استخدامه دائمًا.

الفصل الخامس: أنواع البيانات Data Types

كل قيمة في Luau لها نوع. النوع يحدد ما يمكنك فعله بهذه القيمة. مثلاً لا يمكنك جمع نص مع رقم — لأن النوعين مختلفان.

5.1 الأنواع الأساسية

النوعالوصفمثال
stringنص"hello"
numberرقم42, 3.14
booleanصحيح/خطأtrue / false
nilلا قيمةnil
tableجدول{1, 2, 3}
functionدالةfunction() end
Instanceعنصر Robloxworkspace.Part
Vector3إحداثيات 3DVector3.new(0, 5, 0)
CFrameموقع + اتجاهCFrame.new(0, 5, 0)
Color3لونColor3.fromRGB(255, 0, 0)
UDim2قياس UIUDim2.new(0.5, 0, 0.5, 0)

5.2 String — النصوص

local greeting = "مرحبًا"
local name = 'Ahmed'    -- علامات مفردة أيضًا
local long = [[
  نص على
  عدة أسطر
]]

print(greeting)
print(#greeting)  -- عدد الأحرف

دمج النصوص

local first = "Ahmed"
local last = "Ali"

local full = first .. " " .. last
print(full)  -- Ahmed Ali

-- أو بأسلوب حديث
local full2 = string.format("%s %s", first, last)
print(full2)

5.3 Number — الأرقام

local integer = 10
local decimal = 3.14
local negative = -5
local big = 1e6   -- 1000000

print(integer + decimal)  -- 13.14
📘 ملاحظة Luau لا تفرق بين int و float كما في لغات أخرى — كلها Number.

5.4 Boolean — المنطقي

local isAlive = true
local isDead = false

if isAlive then
    print("اللاعب على قيد الحياة")
end

5.5 nil — لا قيمة

local x = nil
print(x)  -- nil

local t = {}
print(t.notExist)  -- nil
⚠️ تحذير nil هو سبب أشهر خطأ في Roblox: attempt to index nil. يحدث عندما تحاول الوصول إلى خاصية شيء غير موجود.

5.6 Vector3 — الإحداثيات الثلاثية

local position = Vector3.new(10, 5, 20)
print(position.X)  -- 10
print(position.Y)  -- 5
print(position.Z)  -- 20

-- عمليات
local v = Vector3.new(1, 2, 3) + Vector3.new(4, 5, 6)
print(v)  -- 5, 7, 9

5.7 CFrame — الموقع والاتجاه

local cf = CFrame.new(0, 10, 0)
print(cf.Position)  -- 0, 10, 0

local rotated = CFrame.new(0, 10, 0) * CFrame.Angles(0, math.rad(90), 0)

5.8 Color3 — الألوان

local red = Color3.fromRGB(255, 0, 0)
local green = Color3.fromRGB(0, 255, 0)
local fromHex = Color3.fromHex("#3498db")

5.9 UDim2 — قياس الواجهات

local size = UDim2.new(0, 200, 0, 50)
--                 X.scale X.offset Y.scale Y.offset

5.10 Instance — العناصر

local part = workspace:FindFirstChild("Part")
if part then
    print(part.Name)
    print(part.ClassName)
end

5.11 معرفة نوع أي قيمة

print(typeof("hello"))   -- string
print(typeof(42))        -- number
print(typeof(true))      -- boolean
print(typeof(nil))       -- nil
print(typeof({}))        -- table
print(typeof(workspace)) -- Instance
💡 نصيحة typeof() من Roblox، أما type() فهي من Lua الأصلية. استخدم typeof() دائمًا في Roblox.

5.12 تمارين الفصل الخامس

🟢 تمرين سهل أنشئ متغيرًا من كل نوع أساسي واطبعه باستخدام print و typeof.
🟡 تمرين متوسط أنشئ متغير Vector3 يمثل موقعًا في السماء، واطبع إحداثياته الثلاثة بشكل منفصل.
🔴 تمرين متقدم ما الفرق بين Vector3 و CFrame؟ اذكر مثالًا تحتاج فيه كل واحد منهما.

الفصل السادس: العمليات Operators

6.1 العمليات الحسابية Arithmetic

الرمزالمعنىمثال
+جمع5 + 3 = 8
-طرح5 - 3 = 2
*ضرب5 * 3 = 15
/قسمة10 / 2 = 5
%باقي القسمة10 % 3 = 1
^أس2 ^ 3 = 8
print(5 + 3)   -- 8
print(5 - 3)   -- 2
print(5 * 3)   -- 15
print(10 / 2)  -- 5
print(10 % 3)  -- 1
print(2 ^ 3)   -- 8

6.2 عمليات المقارنة Comparison

الرمزالمعنى
==يساوي
~=لا يساوي
>أكبر من
<أصغر من
>=أكبر أو يساوي
<=أصغر أو يساوي
print(5 == 5)   -- true
print(5 ~= 5)   -- false
print(10 > 5)   -- true
print(10 < 5)   -- false
print(10 >= 10) -- true

6.3 العمليات المنطقية Logical

print(true and true)   -- true
print(true and false)  -- false
print(true or false)   -- true
print(not true)        -- false

استخدام عملي:

local hasKey = true
local hasMoney = false

if hasKey and hasMoney then
    print("يمكنك الدخول")
else
    print("لا يمكنك الدخول")
end

6.4 العمليات المُركّبة Compound Assignment

Luau تدعم عمليات حديثة:

local x = 10
x += 5   -- x = x + 5  → 15
x -= 3   -- x = x - 3  → 12
x *= 2   -- x = x * 2  → 24
x /= 4   -- x = x / 4  → 6
print(x) -- 6
📘 ملاحظة هذه الميزات غير موجودة في Lua الأصلية، لكنها متوفرة في Luau.

6.5 أسبقية العمليات Operator Precedence

الترتيب من الأعلى إلى الأدنى:

  1. () الأقواس
  2. ^ الأس
  3. not و # و - (سالب)
  4. * / %
  5. + -
  6. .. دمج النصوص
  7. < > <= >= ~= ==
  8. and
  9. or
print(2 + 3 * 4)      -- 14 وليس 20
print((2 + 3) * 4)    -- 20
print(2 ^ 3 ^ 2)      -- 512 (تُقيّم من اليمين)

6.6 تمارين الفصل السادس

🟢 تمرين سهل اكتب كودًا يطبع نتيجة كل عملية من العمليات الأساسية على العددين 12 و 5.
🟡 تمرين متوسط اكتب كودًا يحدد هل العدد 37 زوجي أم فردي باستخدام %.
🔴 تمرين متقدم اكتب تعبيرًا منطقيًا يعود true فقط عندما يكون اللاعب بين 10 و 20 سنة ولديه أكثر من 100 نقطة.

الفصل السابع: الشروط Conditions

7.1 الصيغة الأساسية if

if condition then
    -- ينفذ فقط إذا كانت الشرط صحيحة
end

مثال:

local age = 18
if age >= 18 then
    print("أنت راشد")
end

7.2 if / else

local age = 15
if age >= 18 then
    print("أنت راشد")
else
    print("أنت قاصر")
end

7.3 if / elseif / else

local score = 75

if score >= 90 then
    print("ممتاز")
elseif score >= 75 then
    print("جيد جدًا")
elseif score >= 50 then
    print("جيد")
else
    print("راسب")
end
💡 نصيحة Luau تُقيّم الشروط بالترتيب من الأعلى للأسفل، وأول شرط يتحقق ينفذ ثم تتوقف.

7.4 الشروط المتداخلة Nested if

local hasKey = true
local hasMoney = true

if hasKey then
    if hasMoney then
        print("يمكنك فتح الصندوق")
    else
        print("تحتاج نقود")
    end
else
    print("تحتاج مفتاح")
end

بديل أنظف:

if hasKey and hasMoney then
    print("يمكنك فتح الصندوق")
end

7.5 أمثلة عملية

التحقق من عمر اللاعب

local playerAge = 16
if playerAge >= 18 then
    print("مرحبًا بك")
else
    print("معذرة، اللعبة للبالغين فقط")
end

التحقق من امتلاك أداة

local inventory = {"سيف", "درع", "جرعة"}

local hasSword = false
for _, item in ipairs(inventory) do
    if item == "سيف" then
        hasSword = true
        break
    end
end

if hasSword then
    print("يمكنك القتال")
else
    print("تحتاج سيفًا أولاً")
end

التحقق من النقاط

local playerCoins = 250
local itemPrice = 100

if playerCoins >= itemPrice then
    playerCoins -= itemPrice
    print("تم الشراء! رصيدك الآن: " .. playerCoins)
else
    print("رصيدك غير كافٍ")
end

7.6 التحقق الآمن من nil

local part = workspace:FindFirstChild("MyPart")

if part then
    print("الجزء موجود: " .. part.Name)
else
    print("الجزء غير موجود")
end
⚠️ مهم جدًا استخدم دائمًا if part then قبل استخدام part. هذا يمنع خطأ attempt to index nil.

7.7 الشرط المختصر (Ternary Style)

local age = 20
local status = age >= 18 and "راشد" or "قاصر"
print(status)  -- راشد

7.8 تمارين الفصل السابع

🟢 تمرين سهل اكتب كودًا يحدد هل الرقم الذي اخترته سالب أم موجب أم صفر.
🟡 تمرين متوسط اكتب كودًا يحدد رتبة اللاعب حسب نقاطه: أقل من 50 "مبتدئ"، 50-100 "متوسط"، أكثر من 100 "محترف".
🔴 تمرين متقدم اكتب كودًا يعطي خصمًا 10% إذا كان اللاعب عضواً مميزاً (isPremium) و 5% إذا اشترى أكثر من 3 مرات.

الفصل الثامن: الحلقات Loops

8.1 حلقة while

تنفذ الكود طالما الشرط صحيح:

local counter = 0
while counter < 5 do
    print("العدد: " .. counter)
    counter += 1
end
⚠️ تحذير إذا نسيت زيادة counter ستدخل في حلقة لا نهائية وقد تتوقف اللعبة. تأكد دائمًا أن الشرط سيتحول إلى false في النهاية.

8.2 حلقة repeat until

تشبه while لكن تنفذ مرة واحدة على الأقل:

local counter = 0
repeat
    print("مرة: " .. counter)
    counter += 1
until counter >= 5

8.3 حلقة for الرقمية (Numeric For)

for i = 1, 5 do
    print("رقم: " .. i)
end

-- بخطوة زيادة
for i = 0, 10, 2 do
    print(i)  -- 0, 2, 4, 6, 8, 10
end

-- عكسي
for i = 5, 1, -1 do
    print(i)  -- 5, 4, 3, 2, 1
end

8.4 الحلقة العامة (Generic For) و pairs و ipairs

ipairs — للتتابعات (Arrays)

local fruits = {"تفاح", "موز", "عنب"}

for index, fruit in ipairs(fruits) do
    print(index, fruit)
end

pairs — لكل الجداول

local player = {
    name = "Ahmed",
    level = 10,
    coins = 500
}

for key, value in pairs(player) do
    print(key .. " = " .. tostring(value))
end
الأداةالاستخدامالترتيب
ipairsللجداول المتتابعة (1, 2, 3...)مضمون
pairsلكل الجداولغير مضمون

8.5 break و continue

break — إيقاف الحلقة

for i = 1, 10 do
    if i == 5 then
        break
    end
    print(i)  -- 1, 2, 3, 4
end

continue — تخطي الدورة الحالية

for i = 1, 10 do
    if i % 2 == 0 then
        continue  -- تخطي الأرقام الزوجية
    end
    print(i)  -- 1, 3, 5, 7, 9
end
📘 ملاحظة continue متوفرة في Luau وليست في Lua الأصلية.

8.6 متى تستخدم أي حلقة؟

الحالةالحلقة المناسبة
عدد معروف من التكراراتfor رقمي
تكرار حتى شرطwhile أو repeat
المرور على عناصر جدولipairs
المرور على خصائص قاموسpairs

8.7 تمارين الفصل الثامن

🟢 تمرين سهل اطبع الأرقام من 1 إلى 20 باستخدام for.
🟡 تمرين متوسط اطبع فقط الأرقام الزوجية من 1 إلى 30 باستخدام continue.
🔴 تمرين متقدم أنشئ جدولًا فيه 5 لاعبين (لكل واحد اسم وعمر ونقاط)، ثم اطبع اللاعبين الذين أعمارهم أكبر من 18 ونقاطهم أكثر من 100.

الفصل التاسع: الدوال Functions

الدالة (Function) هي كتلة كود لها اسم، تُنفَّذ عند استدعائها. هي أساس تنظيم الكود في Luau: بدلًا من كتابة نفس السطور في عشرين مكانًا، تكتبها مرة واحدة داخل دالة وتستدعيها عشرين مرة.

9.1 إنشاء دالة واستدعاؤها

local function greet()
    print("مرحبًا بك!")
end

greet()  -- استدعاء الدالة

لاحظ: local function أفضل من function وحدها، لأنها تجعل الدالة محلية (أسرع وأكثر أمانًا).

9.2 Parameters و Arguments

Parameter = متغير داخل تعريف الدالة.
Argument = القيمة الفعلية عند الاستدعاء.

local function greet(name)
    print("مرحبًا " .. name)
end

greet("Ahmed")   -- Ahmed
greet("Sara")    -- Sara
greet("Ali")     -- Ali

أكثر من Parameter

local function introduce(name, age, city)
    print(name .. " عمره " .. age .. " يسكن في " .. city)
end

introduce("Ahmed", 15, "القاهرة")

9.3 Return — إرجاع قيمة

local function add(a, b)
    return a + b
end

local result = add(5, 3)
print(result)  -- 8

بعد return تتوقف الدالة فورًا:

local function checkAge(age)
    if age < 0 then
        return "عمر غير صالح"
    end
    if age >= 18 then
        return "راشد"
    end
    return "قاصر"
end

print(checkAge(-5))   -- عمر غير صالح
print(checkAge(20))   -- راشد
print(checkAge(12))   -- قاصر

9.4 إرجاع عدة قيم Multiple Returns

local function minMax(numbers)
    local min = math.huge
    local max = -math.huge
    for _, n in ipairs(numbers) do
        if n < min then min = n end
        if n > max then max = n end
    end
    return min, max
end

local low, high = minMax({3, 7, 1, 9, 4})
print(low, high)  -- 1  9

9.5 Functions داخل Tables

هذا أساس البرمجة الكائنية (OOP) لاحقًا:

local player = {
    name = "Ahmed",
    health = 100
}

function player:takeDamage(amount)
    self.health -= amount
    print(self.name .. " صحته الآن: " .. self.health)
end

player:takeDamage(30)  -- Ahmed صحته الآن: 70
player:takeDamage(20)  -- Ahmed صحته الآن: 50
📘 ملاحظة النقطتان : تخبر Luau بتمرير الجدول نفسه كأول Parameter باسم self. هذا اختصار لطيف يوفّر عليك كتابة player.takeDamage(player, 30).

9.6 دوال مجهولة Anonymous Functions

local greet = function(name)
    print("أهلاً " .. name)
end

greet("Sara")

تُستخدم كثيرًا مع Events:

workspace.Part.Touched:Connect(function(hit)
    print(hit.Name .. " لمس الجزء")
end)

9.7 دوال الاستدعاء Callback Functions

الـ Callback هي دالة تمرَّر لدالة أخرى لتنفذها لاحقًا. مثل ما في المثال السابق: الدالة المجهولة هي callback.

local function performAction(action)
    print("قبل التنفيذ")
    action()
    print("بعد التنفيذ")
end

performAction(function()
    print("أنا داخل الـ callback")
end)

9.8 Scope — النطاق

النطاق يحدد من يمكنه رؤية المتغير:

local x = 10

local function test()
    local y = 20
    print(x + y)  -- 30 (يستطيع رؤية x)
end

test()
print(y)  -- خطأ! y محلية داخل test
⚠️ خطأ شائع محاولة استخدام متغير خارج نطاقه تُنتج nil أو خطأ. احذر من إعلان متغير داخل if ثم استخدامه خارجه.

9.9 Closures — الإغلاقات

الـ Closure دالة "تتذكر" المتغيرات التي حولها حتى بعد انتهاء تنفيذ الدالة الأم.

local function makeCounter()
    local count = 0
    return function()
        count += 1
        return count
    end
end

local counter = makeCounter()
print(counter())  -- 1
print(counter())  -- 2
print(counter())  -- 3

مثال عملي — Debounce:

local function makeDebounce()
    local lastTime = 0
    return function(cooldown)
        local now = tick()
        if now - lastTime >= cooldown then
            lastTime = now
            return true
        end
        return false
    end
end

local canAct = makeDebounce()
print(canAct(1))  -- true
print(canAct(1))  -- false (نفس الثانية)

9.10 تمارين الفصل التاسع

🟢 تمرين سهل اكتب دالة multiply(a, b) تُرجع حاصل الضرب، واختبرها.
🟡 تمرين متوسط اكتب دالة تُرجع أكبر قيمة في جدول من الأرقام.
🔴 تمرين متقدم اكتب دالة makeMultiplier(n) تُرجع دالة تضرب أي رقم في n.

الفصل العاشر: الجداول Tables

الجدول (Table) هو أهم بنية بيانات في Luau. هو في الحقيقة الشيء الوحيد في اللغة — كل شيء آخر (Objects، Modules، Classes) مبني عليه.

10.1 إنشاء Table

local empty = {}
local numbers = {1, 2, 3, 4, 5}
local mixed = {1, "two", true, 3.14}
local player = {
    name = "Ahmed",
    level = 10,
    coins = 500
}

10.2 Array — المصفوفة

جدول مفاتيحه أرقام متسلسلة تبدأ من 1:

local fruits = {"تفاح", "موز", "عنب"}

print(fruits[1])  -- تفاح
print(fruits[2])  -- موز
print(fruits[3])  -- عنب
print(#fruits)    -- 3
📘 ملاحظة في Luau، العد يبدأ من 1 وليس 0. هذا مختلف عن معظم اللغات.

10.3 Dictionary — القاموس

جدول مفاتيحه نصوص:

local player = {
    name = "Sara",
    level = 25,
    coins = 1200
}

print(player.name)      -- Sara
print(player["level"])  -- 25

10.4 Indexing — الوصول للقيم

local t = {a = 1, b = 2}

print(t.a)      -- 1  (نقطة)
print(t["a"])   -- 1  (أقواس)

local key = "b"
print(t[key])   -- 2  (لازم أقواس للمتغيرات)

10.5 تعديل وإضافة قيم

local t = {}
t.x = 10           -- إضافة
t["y"] = 20        -- إضافة
t.x = 30           -- تعديل
t.x = nil          -- حذف

print(t.x)  -- nil

10.6 Nested Tables — جداول داخل جداول

local game = {
    players = {
        {name = "Ahmed", score = 100},
        {name = "Sara",  score = 200}
    },
    round = 1
}

print(game.players[1].name)   -- Ahmed
print(game.players[2].score)  -- 200

10.7 دوال الجداول المهمة

table.insert — إضافة عنصر

local list = {"a", "b"}
table.insert(list, "c")        -- في النهاية
table.insert(list, 1, "start") -- في البداية

-- النتيجة: {"start", "a", "b", "c"}

table.remove — حذف عنصر

local list = {"a", "b", "c", "d"}
table.remove(list, 2)   -- يحذف "b"
print(#list)            -- 3

table.remove(list)      -- يحذف الأخير

table.find — البحث

local list = {"سيف", "درع", "جرعة"}
local index = table.find(list, "درع")
print(index)  -- 2

if table.find(list, "سيف") then
    print("لديك سيف")
end

table.sort — الترتيب

local nums = {5, 2, 9, 1, 7}
table.sort(nums)
print(nums)  -- 1, 2, 5, 7, 9

-- تنازلي
table.sort(nums, function(a, b) return a > b end)
print(nums)  -- 9, 7, 5, 2, 1

10.8 # operator — عدد العناصر

local list = {10, 20, 30}
print(#list)  -- 3
⚠️ تحذير # يعمل بشكل صحيح فقط مع الجداول المتتابعة (بدون فراغات). لا تستخدمه على القواميس.

10.9 المرور على الجداول

ipairs — للتتابعات

for i, v in ipairs({"a", "b", "c"}) do
    print(i, v)
end

pairs — للجميع

local player = {name = "Ahmed", level = 5}
for k, v in pairs(player) do
    print(k, v)
end

10.10 النسخ: Reference vs Copy

مهم جدًا: عند إسناد جدول لمتغير آخر، لا يُنسخ — بل تشير الاثنان إلى نفس الجدول.

local a = {1, 2, 3}
local b = a       -- نفس الجدول!
b[1] = 999
print(a[1])       -- 999 (تأثر!)

-- نسخة حقيقية (Surface Copy)
local function shallowCopy(t)
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = v
    end
    return copy
end

local c = shallowCopy(a)
c[1] = 0
print(a[1])  -- 999 (لم يتأثر)
💡 نصيحة إذا احتجت نسخة عميقة (Deep Copy) للجداول المتداخلة، استخدم: local deep = game:GetService("HttpService"):JSONDecode(game:GetService("HttpService"):JSONEncode(t))

10.11 استخدام الجداول في الأنظمة الحقيقية

مثال — بيانات لاعب:

local PlayerData = {
    Ahmed = {coins = 500, level = 10, inventory = {"سيف", "درع"}},
    Sara  = {coins = 1200, level = 25, inventory = {"قوس"}}
}

print(PlayerData.Ahmed.coins)               -- 500
print(PlayerData.Sara.inventory[1])         -- قوس
PlayerData.Ahmed.coins += 100
print(PlayerData.Ahmed.coins)               -- 600

10.12 تمارين الفصل العاشر

🟢 تمرين سهل أنشئ جدولًا فيه 5 أسماء أصدقاء، ثم اطبع عددهم، ثم اطبع كل واحد.
🟡 تمرين متوسط أنشئ قاموسًا فيه بيانات لاعب (اسم، مستوى، نقاط)، وأضف مفتاح "membership" قيمته "Premium".
🔴 تمرين متقدم أنشئ جدولًا فيه 5 لاعبين (لكل واحد اسم ودرجة)، ورتّبهم حسب الدرجة تنازليًا، واطبع النتيجة.

الفصل الحادي عشر: النصوص Strings

التعامل مع النصوص جزء أساسي من بناء الواجهات والرسائل وأسماء اللاعبين. Luau توفّر مكتبة string قوية جدًا.

11.1 دمج النصوص Concatenation

local first = "Ahmed"
local last = "Ali"

print(first .. " " .. last)  -- Ahmed Ali

local coins = 500
print("رصيدك: " .. coins .. " عملة")

11.2 string.format — الصياغة المتقدمة

local name = "Ahmed"
local coins = 500

print(string.format("اللاعب %s لديه %d عملة", name, coins))
-- اللاعب Ahmed لديه 500 عملة

print(string.format("النسبة: %.2f%%", 85.6789))
-- النسبة: 85.68%
الرمزالمعنى
%sنص
%dعدد صحيح
%.2fرقم عشري بخانتين
%xرقم سادس عشري
%%علامة % نفسها

11.3 string.upper و string.lower

local s = "Hello World"
print(string.upper(s))  -- HELLO WORLD
print(string.lower(s))  -- hello world

11.4 string.sub — استخراج جزء

local s = "Roblox Studio"
print(string.sub(s, 1, 6))    -- Roblox
print(string.sub(s, 8))       -- Studio
print(string.sub(s, -6))      -- Studio (من النهاية)
print(string.sub(s, 2, -2))   -- oblox Studi

11.5 string.find — البحث

local s = "Welcome to Roblox"
local start, finish = string.find(s, "Roblox")
print(start, finish)  -- 12  17

if string.find(s, "Roblox") then
    print("موجود!")
end

11.6 string.gsub — الاستبدال

local s = "Hello World Hello"
local new = string.gsub(s, "Hello", "Hi")
print(new)  -- Hi World Hi

-- استبدال كل المسافات
local compact = string.gsub("a b c d", " ", "")
print(compact)  -- abcd

11.7 string.match — المطابقة

local s = "Player_123"
local number = string.match(s, "%d+")
print(number)  -- 123

local email = "test@example.com"
local user = string.match(email, "(.+)@")
print(user)  -- test

11.8 Patterns — الأنماط

النمطالمعنى
.أي حرف
%aحرف
%dرقم
%sمسافة
%wحرف أو رقم
+واحد أو أكثر
*صفر أو أكثر
?اختياري

11.9 split — تقسيم النص

Luau لا تحتوي split جاهزة، لكن نكتبها بسهولة:

local function split(text, sep)
    local result = {}
    for word in string.gmatch(text, "([^" .. sep .. "]+)") do
        table.insert(result, word)
    end
    return result
end

print(split("a,b,c", ","))  -- {"a", "b", "c"}

11.10 تمارين الفصل الحادي عشر

🟢 تمرين سهل اطبع اسمك 5 مرات باستخدام string.format.
🟡 تمرين متوسط أنشئ دالة تأخذ نصًا وتُرجع عدد أحرفه وعدد كلماته.
🔴 تمرين متقدم اكتب دالة تتحقق إن كان النص بريدًا إلكترونيًا صالحًا باستخدام Patterns.

الفصل الثاني عشر: الـ Instances في Roblox

كل شيء في Roblox هو Instance: Part، Model، Folder، Script، Player، GUI، Light، Sound… كل عنصر في Explorer هو Instance.

12.1 Instance.new()

local part = Instance.new("Part")
part.Name = "MyPart"
part.Size = Vector3.new(4, 1, 4)
part.Position = Vector3.new(0, 10, 0)
part.Color = Color3.fromRGB(255, 0, 0)
part.Parent = workspace
💡 نصيحة أداء عيّن Parent في آخر سطر، ليس أولًا. Roblox يُنفّذ تحسينات على الـ Instance بعد وضعه في الـ Parent.

12.2 Parent — الأب

الخاصية Parent تحدد أين يوجد الـ Instance. لو لم تضع Parent، سيبقى في الذاكرة لكن لن يظهر في اللعبة.

part.Parent = workspace
part.Parent = workspace.Folder
part.Parent = game.ReplicatedStorage

12.3 Name و ClassName

local p = Instance.new("Part")
p.Name = "RedPart"

print(p.Name)       -- RedPart
print(p.ClassName)  -- Part

12.4 البحث عن Instances

FindFirstChild

local part = workspace:FindFirstChild("MyPart")
if part then
    print("موجود: " .. part.Name)
end

WaitForChild

local remote = game.ReplicatedStorage:WaitForChild("MyRemote", 5)
if remote then
    print("وصل الـ Remote")
end
📘 متى تستخدم أي واحدة؟ FindFirstChild → للبحث الفوري، ترجع nil إذا لم تجد.
WaitForChild → للانتظار حتى يصل العنصر (مفيد في LocalScripts).

FindFirstChildOfClass

local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
    humanoid.Health = 0
end

12.5 IsA — فحص النوع

local obj = workspace:FindFirstChild("MyPart")
if obj and obj:IsA("BasePart") then
    print("إنه Part")
end

if obj:IsA("Model") then
    print("إنه Model")
end
💡 فرق مهم استخدم IsA وليس المقارنة بـ ClassName. لأن IsA("BasePart") تشمل Part و WedgePart و MeshPart… أما ClassName == "Part" فدقيقة جدًا وقد تفشل.

12.6 GetChildren و GetDescendants

local folder = workspace.MyFolder

for _, child in ipairs(folder:GetChildren()) do
    print(child.Name)
end

-- كل شيء داخل المجلد ومجلداته الفرعية
for _, item in ipairs(folder:GetDescendants()) do
    if item:IsA("BasePart") then
        item.Anchored = true
    end
end

12.7 Clone — النسخ

local original = workspace.OriginalPart
local copy = original:Clone()
copy.Position = Vector3.new(10, 5, 10)
copy.Parent = workspace

استخدام شائع: نسخ Tool من ServerStorage وإعطائه للاعب:

local tool = game.ServerStorage.Sword:Clone()
tool.Parent = player.Backpack

12.8 Destroy — التدمير

part:Destroy()
⚠️ تحذير لا تستخدم part.Parent = nil، لأنها تُبقي الـ Instance في الذاكرة وتسبب Memory Leak. استخدم دائمًا :Destroy().

12.9 ClearAllChildren

folder:ClearAllChildren()  -- يحذف كل الأبناء

12.10 التعامل مع الأنواع المختلفة

Part

local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Material = Enum.Material.Neon

Model

local model = Instance.new("Model")
model.Name = "MyModel"
model.PrimaryPart = somePart
model.Parent = workspace

Folder

local folder = Instance.new("Folder")
folder.Name = "Items"
folder.Parent = workspace

Tool

local tool = Instance.new("Tool")
tool.Name = "Sword"
tool.RequiresHandle = true
local handle = Instance.new("Part")
handle.Name = "Handle"
handle.Parent = tool
tool.Parent = player.Backpack

ScreenGui

local gui = Instance.new("ScreenGui")
gui.Name = "HUD"
gui.ResetOnSpawn = false
gui.Parent = player.PlayerGui

12.11 تمارين الفصل الثاني عشر

🟢 تمرين سهل أنشئ Part برمجيًا، ضعه فوق اللاعب على ارتفاع 20، اجعله Neon.
🟡 تمرين متوسط أنشئ Folder فيه 10 Parts بألوان مختلفة موزّعة على شكل دائرة.
🔴 تمرين متقدم أنشئ Model يحتوي على 4 Parts تمثل جدرانًا، واجعله قابلاً للنسخ عبر دالة.

الفصل الثالث عشر: الخصائص Properties

كل Instance له خصائص (Properties) يمكن قراءتها وتعديلها برمجيًا. في هذا الفصل نشرح أهم الخصائص وكيفية استخدامها.

13.1 قراءة وتعديل الخصائص

local part = workspace.Part

print(part.Name)          -- قراءة
print(part.Position)
part.Transparency = 0.5   -- تعديل
part.CanCollide = false

13.2 خصائص BasePart

الخاصيةالوظيفة
Positionالموقع (Vector3)
CFrameالموقع + الاتجاه
Sizeالحجم (Vector3)
Colorاللون (Color3)
Materialالمادة (Neon, Glass, Wood…)
Transparencyالشفافية (0 شفاف تمامًا؟ لا، 1 شفاف، 0 معتم)
CanCollideهل يتصادم مع الأجسام؟
Anchoredهل يبقى ثابتًا في مكانه؟
Masslessهل يتجاهل الفيزياء؟
Reflectanceالانعكاس (0 إلى 1)

13.3 مثال شامل معلّق

local part = workspace.Part

part.Transparency = 0.5
-- نصف شفاف (0 معتم تمامًا، 1 شفاف تمامًا)

part.CanCollide = false
-- يتجاهل التصادمات، يمر اللاعب من خلاله

part.Size = Vector3.new(10, 1, 10)
-- عرض 10، ارتفاع 1، عمق 10

part.Color = Color3.fromRGB(255, 0, 0)
-- اللون أحمر نقي

part.Anchored = true
-- لا يتحرك أبدًا مع الجاذبية أو اللمس

part.Material = Enum.Material.Neon
-- مادة نيون تلمع

part.Position = Vector3.new(0, 20, 0)
-- ارتفاعه 20 عن الأرض

13.4 Tween بين خصائص

-- تغيير الشفافية تدريجيًا
for i = 1, 10 do
    part.Transparency = i / 10
    task.wait(0.1)
end

أو باستخدام TweenService (سنشرحه لاحقًا):

local TweenService = game:GetService("TweenService")

local tween = TweenService:Create(
    part,
    TweenInfo.new(1),
    {Transparency = 1}
)
tween:Play()

13.5 Properties للـ Humanoid

الخاصيةالوظيفة
Healthالدم الحالي
MaxHealthالحد الأقصى
WalkSpeedسرعة المشي (افتراضي 16)
JumpPowerقوة القفز (افتراضي 50)
JumpHeightارتفاع القفز (بديل حديث)
UseJumpPowerاستخدام JumpPower بدل JumpHeight
local humanoid = character:FindFirstChildOfClass("Humanoid")
humanoid.WalkSpeed = 30
humanoid.JumpPower = 80
humanoid.MaxHealth = 200
humanoid.Health = 200

13.6 Properties للـ GUI

الخاصيةالوظيفة
PositionUDim2
SizeUDim2
AnchorPointنقطة الارتساء (Vector2)
BackgroundColor3لون الخلفية
TextColor3لون النص
Visibleهل يظهر؟
ZIndexالطبقة
Rotationالدوران
local frame = script.Parent
frame.Size = UDim2.new(0, 300, 0, 200)
frame.Position = UDim2.new(0.5, -150, 0.5, -100)
frame.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
frame.Visible = true

13.7 قراءة الخصائص الموجودة فقط

-- احصل على جميع الخصائص لشيء
for _, prop in ipairs(part:GetPropertyChangedSignal and {} or {}) do end

-- الطريقة الفعلية: استخدم GetProperties من Plugin
-- أو استخدم الاستكشاف اليدوي من Properties window
💡 نصيحة عملية عندما لا تتذكر اسم خاصية، اذهب إلى Explorer وحدد الـ Instance وابحث في Properties. الاسم الذي تراه هو نفسه ما تكتبه بالكود.

13.8 تمارين الفصل الثالث عشر

🟢 تمرين سهل أنشئ Part واجعله شفافًا بمقدار 0.7، ولونه أخضر، وثابتًا في مكانه.
🟡 تمرين متوسط اكتب كودًا يجعل Player.WalkSpeed = 25 و JumpPower = 60 بمجرد دخوله اللعبة.
🔴 تمرين متقدم أنشئ حلقة تجعل Part يتحول من مادة Neon إلى Glass كل ثانية، وتغيّر لونه تدريجيًا.

الفصل الرابع عشر: الأحداث Events

الحدث (Event) هو إشعار يصدره Roblox عندما يحدث شيء معيّن: لاعب لمس جزءًا، ضغط زرًا، تغيّر دمه، دخل اللعبة… بدلًا من أن تسأل باستمرار "هل حدث شيء؟"، تربط دالة (Callback) بالحدث، فيستدعيها Roblox تلقائيًا عند حدوثه.

14.1 البنية الأساسية

Event:Connect(function(...)
    -- الكود الذي ينفذ عند وقوع الحدث
end)

:Connect() تربط دالة بالحدث وترجع كائنًا يسمى Connection. هذا الكائن يمكن استخدامه لاحقًا لفصل الارتباط.

local connection = workspace.Part.Touched:Connect(function(hit)
    print(hit.Name .. " لمس الجزء")
end)

-- لاحقًا، عند الحاجة:
connection:Disconnect()

14.2 ما معنى Callback؟

الـ Callback هي الدالة التي تمرّرها لـ Connect. لا تُنفَّذ فورًا، بل يخزّنها Roblox، وعندما يقع الحدث يستدعيها ويمرّر لها القيم المناسبة.

local function onTouched(hit)
    print("لمسني: " .. hit.Name)
end

workspace.Part.Touched:Connect(onTouched)
📘 ملاحظة نفس الدالة يمكن ربطها بأكثر من حدث، وكل حدث يحفظ مرجعه الخاص.

14.3 أحداث BasePart المهمة

الحدثمتى يُستدعى؟
Touchedعند ملامسة أي جسم للجزء
TouchEndedعند انتهاء الملامسة
Changedعند تغيّر خاصية معينة
ChildAddedعند إضافة ابن للجزء
ChildRemovedعند إزالة ابن
AncestryChangedعند تغيّر الـ Parent

مثال — Touched

local part = workspace.LavaPart

part.Touched:Connect(function(hit)
    local character = hit:FindFirstAncestorOfClass("Model")
    if character then
        local humanoid = character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid.Health = 0
        end
    end
end)
⚠️ تحذير Touched ينطلق لأشياء كثيرة جدًا (بما فيها الديكور). أضف دائمًا فحوصات: هل هذا Humanoid؟ هل هو نفس اللاعب؟ وإلا فستحصل على أخطاء أو ضرر خاطئ. سنستخدم Raycasting لاحقًا لأنها أدق.

14.4 Changed و GetPropertyChangedSignal

-- Changed (أقدم — ينطلق لأي تغيير)
part.Changed:Connect(function(property)
    print("تغيّرت خاصية: " .. property)
end)

-- GetPropertyChangedSignal (أفضل — لخاصية واحدة)
part:GetPropertyChangedSignal("Transparency"):Connect(function()
    print("تغيّرت الشفافية إلى: " .. part.Transparency)
end)
💡 نصيحة أداء استخدم GetPropertyChangedSignal دائمًا. لأن Changed تُنفَّذ عند كل تغيير صغير وتبطئ اللعبة.

14.5 ChildAdded و ChildRemoved

local folder = workspace.Items

folder.ChildAdded:Connect(function(child)
    print("أُضيف: " .. child.Name)
end)

folder.ChildRemoved:Connect(function(child)
    print("أُزيل: " .. child.Name)
end)

14.6 أحداث Players

الحدثمتى؟
PlayerAddedعند دخول لاعب
PlayerRemovingعند خروج لاعب
CharacterAddedعند ظهور شخصيته
CharacterRemovingعند اختفاء شخصيته (موت/إعادة ظهور)
local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    print("دخل: " .. player.Name)

    player.CharacterAdded:Connect(function(character)
        print("ظهرت شخصية " .. player.Name)

        local humanoid = character:WaitForChild("Humanoid")
        humanoid.Died:Connect(function()
            print(player.Name .. " مات")
        end)
    end)
end)

Players.PlayerRemoving:Connect(function(player)
    print("خرج: " .. player.Name)
end)

14.7 أحداث الأزرار والـ GUI

local button = script.Parent

button.MouseButton1Click:Connect(function()
    print("تم الضغط!")
end)

button.MouseEnter:Connect(function()
    button.BackgroundColor3 = Color3.fromRGB(100, 200, 100)
end)

button.MouseLeave:Connect(function()
    button.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
end)

14.8 Activated و أحداث الأدوات

local tool = script.Parent

tool.Activated:Connect(function()
    print("تم استخدام الأداة")
end)

tool.Equipped:Connect(function()
    print("تم تجهيز الأداة")
end)

tool.Unequipped:Connect(function()
    print("تم إلغاء تجهيز الأداة")
end)

14.9 قاعدة ذهبية: فصل الأحداث

كل اتصال (Connection) يستهلك ذاكرة. إذا ربطت أحداثًا كثيرة دون فصلها، ستحصل على Memory Leak واللعبة تتباطأ.

-- مثال خطأ
while true do
    workspace.Part.Touched:Connect(function(hit)
        print("لمسني")
    end)
    task.wait(1)
end
-- كل ثانية تضيف Connection جديد!

-- مثال صحيح
local connection = workspace.Part.Touched:Connect(function(hit)
    print("لمسني")
end)

-- عندما لا نحتاجه:
-- connection:Disconnect()
⚠️ خطأ خطير عندما يستخدم اللاعب أداة ثم تُدمَّر، فصل الأحداث المرتبطة بها لتجنب التسرّب. كذلك عند خروج اللاعب، فصل كل ما يتعلق به.

14.10 تمارين الفصل الرابع عشر

🟢 تمرين سهل أنشئ Part، وعند لمسه اطبع اسم اللاعب الذي لمسه فقط (تجاهل الديكور).
🟡 تمرين متوسط أنشئ Part، وربط Changed عليه ليطبع كل تغيير في اللون.
🔴 تمرين متقدم اكتب كودًا يربط عند دخول اللاعب بأحداث CharacterAdded و Died، ويطبع عدد مرات موت اللاعب.

الفصل الخامس عشر: Players و Characters

فهم الفرق بين Player و Character حجر أساس لكل نظام لعبة. الخطأ فيهما سبب أغلب مشاكل المبتدئين.

15.1 الفرق بين Player و Character

المفهومما هو؟يوجد فيه
Player كائن اللاعب المنطقي UserId، Name، Backpack، PlayerGui، leaderstats
Character الجسم الفعلي في العالم Humanoid، Head، HumanoidRootPart، Limbs
📘 قاعدة ذهنية Player يبقى موجودًا طوال جلوس اللاعب. Character يموت ويُعاد إنشاؤه عند كل موت أو إعادة ظهور. لذلك لا تحفظ مرجع Character طويلًا.

15.2 Players Service

local Players = game:GetService("Players")

-- الوصول للاعبين
print(Players:GetPlayers())        -- قائمة كل اللاعبين
print(Players:GetPlayerFromCharacter(character))
print(Players:GetPlayerByUserId(123456))

15.3 Players.LocalPlayer

الخاصية LocalPlayer موجودة فقط في LocalScript. تمثل اللاعب الحالي صاحب الجهاز.

-- LocalScript فقط
local Players = game:GetService("Players")
local player = Players.LocalPlayer

print("أنا " .. player.Name)
⚠️ خطأ شائع في Script على السيرفر لا يوجد شيء اسمه LocalPlayer. إذا حاولت استخدامه ستحصل على nil. السيرفر يرى كل اللاعبين معًا، فاستخدم GetPlayers.

15.4 الوصول للـ Character

local player = Players.LocalPlayer

-- الطريقة القصيرة (قد تكون nil لحظيًا)
local character = player.Character

-- الطريقة الآمنة
local character = player.Character or player.CharacterAdded:Wait()

local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")

15.5 أجزاء الشخصية الرئيسية

الجزءالوظيفة
HumanoidRootPartمركز الجسم — يُستخدم للتحريك والمواقع
Headالرأس — يقبل Accessories
UpperTorso / Torsoالجذع العلوي
LowerTorso / Torsoالجذع السفلي
LeftHand / RightHandالأيدي (R15 فقط)
LeftFoot / RightFootالأقدام (R15 فقط)

15.6 الـ Humanoid — أهم كائن في الشخصية

local humanoid = character:WaitForChild("Humanoid")

print(humanoid.Health)      -- 100
print(humanoid.MaxHealth)   -- 100
print(humanoid.WalkSpeed)   -- 16
print(humanoid.JumpPower)   -- 50

-- تعديل
humanoid.WalkSpeed = 25
humanoid.JumpPower = 60

-- حالة اللاعب
print(humanoid:GetState())  -- Enum.HumanoidStateType.Running

-- أحداث الـ Humanoid
humanoid.Died:Connect(function()
    print("مات!")
end)

humanoid.StateChanged:Connect(function(old, new)
    print("تغيّر من " .. tostring(old) .. " إلى " .. tostring(new))
end)

15.7 CharacterAdded

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    -- عند أول ظهور للشخصية
    player.CharacterAdded:Connect(function(character)
        local humanoid = character:WaitForChild("Humanoid")
        humanoid.WalkSpeed = 20  -- سرعة مخصصة
        humanoid.MaxHealth = 200
        humanoid.Health = 200
    end)

    -- لو الشخصية موجودة أصلًا
    if player.Character then
        local humanoid = player.Character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid.WalkSpeed = 20
        end
    end
end)
💡 نصيحة لا تفترض أن CharacterAdded سيتأخر عن PlayerAdded. أحيانًا يكون الـ Character موجودًا مباشرة. لذلك افحص player.Character بعد ربط الحدث.

15.8 Animator

Animator هو الكائن المسؤول عن تشغيل الأنيميشن. موجود داخل Humanoid:

local animator = humanoid:FindFirstChildOfClass("Animator")
if not animator then
    animator = Instance.new("Animator")
    animator.Parent = humanoid
end

local animation = Instance.new("Animation")
animation.AnimationId = "rbxassetid://0000000000"

local track = animator:LoadAnimation(animation)
track:Play()

15.9 أدوات اللاعب — Backpack و PlayerGui

-- إعطاء اللاعب أداة
local tool = game.ServerStorage.Sword:Clone()
tool.Parent = player.Backpack

-- إضافة GUI للاعب
local gui = game.ReplicatedStorage.HUD:Clone()
gui.Parent = player.PlayerGui
📘 ملاحظة PlayerGui و Backpack تظهران في Explorer تحت اللاعب، لكنها لا تكون مرئية إلا للاعب نفسه عندما تكون في الوضع Client.

15.10 تمارين الفصل الخامس عشر

🟢 تمرين سهل اطبع أسماء كل اللاعبين المتصلين كل 5 ثوانٍ.
🟡 تمرين متوسط اجعل كل لاعب يدخل اللعبة يحصل على WalkSpeed = 22 و JumpPower = 55.
🔴 تمرين متقدم أنشئ نظامًا يمنح اللاعب 100 نقطة في leaderstats عند موته للمرة الأولى.

الفصل السادس عشر: Client و Server

هذا هو الفصل الأهم في الكتاب. لو فهمت Client و Server جيدًا، فأنت تجاوزت 80% من العقبات التي يواجهها المبتدئون.

16.1 إعادة التعريف

ServerClient
العدد واحد لكل سيرفر واحد لكل لاعب
يرى كل اللاعبين نفسه فقط (بشكل أساسي)
الصلاحيات كل شيء محدودة، غير موثوقة
الأداء مشترك (يؤثر على الجميع) محلي (لا يؤثر على الآخرين)
الثقة مطلقة صفر

16.2 ما يجب أن يحدث في Server فقط

16.3 ما يناسبه Client

16.4 لماذا لا يجب الوثوق بالـ Client؟

لأن أي شخص يمكنه تشغيل برامج مثل Synapse أو Script-Ware تعديل أي شيء في Client-side. يمكنه:

⚠️ القاعدة الذهبية أي شيء على Client يمكن للاعب تعديله. لذلك كل قرار مهم في Server. الـ Client يطلب، والـ Server يقرر.

16.5 مثال: كود ضعيف ثم كود آمن

❌ الكود الضعيف

-- LocalScript
local button = script.Parent
button.MouseButton1Click:Connect(function()
    game.Players.LocalPlayer.leaderstats.Coins.Value += 1000
end)

لماذا ضعيف؟

✅ الكود الآمن

-- ServerScriptService/Server.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local requestReward = ReplicatedStorage:WaitForChild("RequestReward")

local lastRequest = {}

requestReward.OnServerEvent:Connect(function(player)
    -- Rate limit: منع الطلبات المتكررة
    local now = tick()
    if lastRequest[player] and now - lastRequest[player] < 5 then
        return
    end
    lastRequest[player] = now

    -- التحقق من الصلاحية
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end
    local coins = stats:FindFirstChild("Coins")
    if not coins then return end

    coins.Value += 100
end)

Players.PlayerRemoving:Connect(function(player)
    lastRequest[player] = nil
end)

Client الجانب

-- StarterPlayerScripts/RewardClient.client.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local requestReward = ReplicatedStorage:WaitForChild("RequestReward")
local button = script.Parent:WaitForChild("RewardButton")

button.MouseButton1Click:Connect(function()
    requestReward:FireServer()
end)

الـ Client يطلب فقط. الـ Server يفحص كل شيء: هل الطلب مشروع؟ هل مضي وقت كافٍ؟ ثم يعطي المكافأة. هذا هو النمط الصحيح في Roblox.

16.6 LocalScript و Script — تمرين ذهني

اسأل نفسك دائمًا: أين يجب أن يعمل هذا الكود؟

الوظيفةالمكان المناسب
فتح GUI عند الضغط على زرClient
منح اللاعب 100 عملةServer
تشغيل أنيميشن هجومClient (+ Server للتحقق)
حساب الضرر على العدوServer
إظهار رسالة "لقد ربحت!"Client
حفظ تقدم اللاعبServer

16.7 تمارين الفصل السادس عشر

🟢 تمرين سهل اذكر 5 أشياء يجب أن تحدث في السيرفر و5 في العميل.
🟡 تمرين متوسط حول هذا الكود الضعيف إلى كود آمن:
-- LocalScript
game.Players.LocalPlayer.leaderstats.Level.Value += 1
🔴 تمرين متقدم لماذا حتى لو استخدمنا RemoteEvent، يبقى الـ Client غير موثوق؟ اشرح بمثال.

الفصل السابع عشر: RemoteEvents

RemoteEvent هو الجسر الذي يُمرّر الرسائل بين Client و Server. هو الأداة الأساسية في كل لعبة Roblox متعددة اللاعبين.

17.1 إنشاء RemoteEvent

الطريقة اليدوية:

  1. اذهب إلى ReplicatedStorage.
  2. اضغط + واختر RemoteEvent.
  3. سمّه مثلًا DamageEvent.

الطريقة البرمجية (في السيرفر):

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local event = Instance.new("RemoteEvent")
event.Name = "DamageEvent"
event.Parent = ReplicatedStorage
💡 نصيحة ضع كل الـ Remotes في Folder اسمه Remotes داخل ReplicatedStorage. هذا يجعل المشروع منظمًا.

17.2 الدوال الرئيسية

الدالةتُستدعى منالاستخدام
FireServer()Clientإرسال للسيرفر
OnServerEventServerاستقبال من عميل
FireClient(player)Serverإرسال لعميل واحد
FireAllClients()Serverإرسال لكل العملاء
OnClientEventClientاستقبال من السيرفر

17.3 من Client إلى Server

Client

-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local attackEvent = ReplicatedStorage:WaitForChild("AttackEvent")

attackEvent:FireServer("sword", 50)

Server

-- Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local attackEvent = ReplicatedStorage:WaitForChild("AttackEvent")

attackEvent.OnServerEvent:Connect(function(player, weaponName, damage)
    print(player.Name .. " استخدم " .. weaponName .. " بضرر " .. damage)
end)
📘 مهم جدًا أول Parameter في OnServerEvent هو دائمًا الـ player. لا تمرّره من الـ Client — Roblox يضيفه تلقائيًا. هذا يمنع اللاعب من تزوير هويته.

17.4 من Server إلى Client

Server — لعميل واحد

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    task.wait(3)
    attackEvent:FireClient(player, "تم تسجيل دخولك!")
end)

Server — لكل العملاء

attackEvent:FireAllClients("بدأت الجولة!")

Client يستقبل

attackEvent.OnClientEvent:Connect(function(message)
    print("وصل: " .. message)
end)

17.5 مشروع صغير: زر يعطي مكافأة

سنبني معًا نظامًا كاملًا وآمنًا.

الخطوة 1: Server — إعداد اللعبة

-- ServerScriptService/Main.server.lua
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

-- إنشاء RemoteEvent
local remotes = Instance.new("Folder")
remotes.Name = "Remotes"
remotes.Parent = ReplicatedStorage

local claimReward = Instance.new("RemoteEvent")
claimReward.Name = "ClaimReward"
claimReward.Parent = remotes

-- إعداد leaderstats عند دخول اللاعب
Players.PlayerAdded:Connect(function(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = stats

    stats.Parent = player
end)

-- استقبال الطلبات
local lastClaim = {}

claimReward.OnServerEvent:Connect(function(player)
    local now = tick()
    if lastClaim[player] and now - lastClaim[player] < 10 then
        return -- رفض
    end
    lastClaim[player] = now

    player.leaderstats.Coins.Value += 50
end)

Players.PlayerRemoving:Connect(function(player)
    lastClaim[player] = nil
end)

الخطوة 2: Client — الزر

-- StarterPlayerScripts/RewardClient.client.lua
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local player = Players.LocalPlayer
local remotes = ReplicatedStorage:WaitForChild("Remotes")
local claimReward = remotes:WaitForChild("ClaimReward")

-- إنشاء واجهة بسيطة برمجيًا
local gui = Instance.new("ScreenGui")
gui.Name = "RewardGui"
gui.Parent = player:WaitForChild("PlayerGui")

local button = Instance.new("TextButton")
button.Size = UDim2.new(0, 200, 0, 60)
button.Position = UDim2.new(0.5, -100, 0.8, 0)
button.BackgroundColor3 = Color3.fromRGB(50, 150, 50)
button.Text = "احصل على 50 عملة"
button.TextColor3 = Color3.fromRGB(255, 255, 255)
button.Font = Enum.Font.GothamBold
button.TextSize = 18
button.Parent = gui

-- عند الضغط
button.MouseButton1Click:Connect(function()
    claimReward:FireServer()
end)

الخطوة 3: التجربة

  1. شغّل اللعبة.
  2. ستظهر واجهة "احصل على 50 عملة" أسفل الشاشة.
  3. اضغط الزر → يزداد الرصيد 50.
  4. اضغط مرة أخرى بسرعة → لا شيء (Rate limit).

17.6 لماذا يجب التحقق في السيرفر؟

لأن أي لاعب يستطيع تشغيل كود في الـ Client يقول: claimReward:FireServer() ألف مرة في الثانية. لو كان السيرفر يثق بالعميل بدون فحص، سيصبح الرصيد مليونيًا في ثانية.

17.7 أخطاء شائعة

الخطأالسببالحل
الحدث لا يعمل RemoteEvent غير موجود في ReplicatedStorage تأكد من Parent
WaitForChild عالق الاسم خطأ تأكد من تطابق الأسماء
FireServer لا يفعل شيئًا OnServerEvent غير مربوط اربطه في Script داخل السيرفر
player اسمه nil مرّرت player يدويًا لا تمرّره — Roblox يمرّره

17.8 تمارين الفصل السابع عشر

🟢 تمرين سهل أنشئ RemoteEvent وأرسل من العميل نص "مرحبًا" عند الضغط على زر، واطبعه في السيرفر.
🟡 تمرين متوسط أنشئ RemoteEvent يعطي اللاعب 10 صحة عند استخدامه. تحقق من أن صحة اللاعب أقل من 100.
🔴 تمرين متقدم أنشئ نظام إشعار (Notification): السيرفر يرسل FireAllClients برسالة، وكل عملاء يظهرون TextLabel مؤقت لثلاث ثوانٍ.

الفصل الثامن عشر: RemoteFunctions

RemoteEvent مثل "طرق على الباب" — لا تعرف إن كان هناك رد. أما RemoteFunction فهو مثل "مكالمة هاتفية" — تسأل وتنتظر الجواب.

18.1 الفرق بين RemoteEvent و RemoteFunction

RemoteEventRemoteFunction
الاتجاه أحادي (Fire and Forget) ثنائي (سؤال وجواب)
هل ينتظر؟ لا نعم (Yield)
هل يرجع قيمة؟ لا نعم
الأداء أسرع أبطأ (يوقف التنفيذ)
الاستخدام أحداث، إشعارات، طلبات طلب بيانات يحتاج جوابًا
⚠️ تحذير مهم RemoteFunction يمكن أن تعلّق الكود إذا لم يصل الرد. في السيرفر قد يوقف كل شيء. Roblox تنصح باستخدام RemoteEvent في معظم الحالات.

18.2 الدوال الرئيسية

الدالةتُستدعى منالمكان
InvokeServer()Clientيسأل السيرفر
OnServerInvokeServerيُعرَّف كدالة واحدة
InvokeClient(player)Serverيسأل لاعبًا معيّنًا
OnClientInvokeClientيُعرَّف كدالة واحدة
📘 فرق مهم OnServerInvoke و OnClientInvoke ليست أحداثًا بل دوالًا تُسند مباشرة، ويمكن إسناد واحدة فقط.

18.3 مثال: Client يسأل عن بياناته

Server

-- ServerScriptService/DataServer.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local getData = Instance.new("RemoteFunction")
getData.Name = "GetPlayerData"
getData.Parent = ReplicatedStorage

-- بيانات افتراضية (عادةً من DataStore)
local playerData = {}

game.Players.PlayerAdded:Connect(function(player)
    playerData[player.UserId] = {
        level = 1,
        coins = 100,
        wins = 0
    }
end)

game.Players.PlayerRemoving:Connect(function(player)
    playerData[player.UserId] = nil
end)

-- إسناد الدالة (تُنفَّذ عند InvokeServer من أي عميل)
getData.OnServerInvoke = function(player)
    return playerData[player.UserId]
end

Client

-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local getData = ReplicatedStorage:WaitForChild("GetPlayerData")

local data = getData:InvokeServer()
print(data.level, data.coins, data.wins)

18.4 Server يسأل Client

هذا النمط نادر لأن السيرفر لا يجب أن يعتمد على العميل. لكنه موجود لأشياء مثل: سؤال اللاعب عن اختياره من واجهة.

Server

local askClient = ReplicatedStorage:WaitForChild("AskClient")

local answer = askClient:InvokeClient(player, "ما لونك المفضل؟")
print("اللاعب اختار: " .. answer)

Client

local askClient = ReplicatedStorage:WaitForChild("AskClient")

askClient.OnClientInvoke = function(question)
    print("السيرفر يسأل: " .. question)
    return "أزرق"
end
⚠️ خطر إذا خرج اللاعب أثناء InvokeClient، سيتعلّق السيرفر. لا تستخدم InvokeClient إلا في حالات نادرة جدًا.

18.5 مثال شامل: متجر

Server

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = Instance.new("Folder")
remotes.Name = "Remotes"
remotes.Parent = ReplicatedStorage

local buyItem = Instance.new("RemoteFunction")
buyItem.Name = "BuyItem"
buyItem.Parent = remotes

-- أسعار الأغراض
local prices = {
    Sword = 100,
    Shield = 200,
    Potion = 50
}

-- بيانات اللاعبين
local data = {}

Players.PlayerAdded:Connect(function(player)
    data[player.UserId] = {coins = 500, items = {}}
end)

Players.PlayerRemoving:Connect(function(player)
    data[player.UserId] = nil
end)

buyItem.OnServerInvoke = function(player, itemName)
    -- التحقق من صحة الغرض
    local price = prices[itemName]
    if not price then
        return false, "غرض غير موجود"
    end

    local playerInfo = data[player.UserId]
    if not playerInfo then
        return false, "بيانات غير متوفرة"
    end

    -- التحقق من الرصيد
    if playerInfo.coins < price then
        return false, "رصيد غير كافٍ"
    end

    -- تنفيذ الشراء
    playerInfo.coins -= price
    table.insert(playerInfo.items, itemName)

    return true, "تم الشراء بنجاح"
end

Client

-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local buyItem = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("BuyItem")

local success, message = buyItem:InvokeServer("Sword")
if success then
    print("✅ " .. message)
else
    print("❌ " .. message)
end

لاحظ: الـ Server يفحص: هل الغرض موجود؟ هل الرصيد يكفي؟ ثم يخصم ويعطي. الـ Client لا يقرر شيئًا.

18.6 متى تستخدم RemoteFunction؟

الحالةالأنسب
الحصول على بيانات اللاعبRemoteFunction
طلب شراءRemoteFunction (للحصول على النتيجة)
إشعار للجميعRemoteEvent
تحديث HUDRemoteEvent
إرسال أوامر قتالRemoteEvent
سؤال عن توفر غرضRemoteFunction

18.7 أخطاء شائعة

الخطأالسبب
InvokeServer لا يرجع OnServerInvoke غير مسندة
يعلق السيرفر InvokeClient واللاعب خرج
attempt to call nil RemoteFunction غير موجودة في ReplicatedStorage
استخدامها للقتال يجب استخدام RemoteEvent لأنه أسرع

18.8 تمارين الفصل الثامن عشر

🟢 تمرين سهل أنشئ RemoteFunction تُرجع نسخة من اسم اللاعب مقلوبة (reverse).
🟡 تمرين متوسط أنشئ نظامًا يسأل اللاعب السيرفر: "هل يمتلك هذا الغرض؟"، والسيرفر يجيب بـ true/false.
🔴 تمرين متقدم أعد تصميم نظام المتجر ليدعم خصمًا 10% للأعضاء (RemoteFunction تُرجع السعر النهائي قبل الشراء).

الفصل التاسع عشر: برمجة الواجهات GUI / UI

الواجهة (GUI) هي كل ما يراه اللاعب على الشاشة: الأزرار، الأشرطة، القوائم، الإشعارات، شريط الصحة… إتقان الواجهات يفرق بين لعبة مبتدئة ولعبة احترافية.

19.1 هيكل الواجهة في Roblox

كل واجهة تبدأ من ScreenGui، وتتفرع منه العناصر:

ScreenGui
├── Frame
│   ├── TextLabel
│   ├── TextButton
│   └── ImageLabel
├── TextButton
└── ImageLabel

19.2 العناصر الأساسية

العنصرالوظيفة
ScreenGuiحاوية الواجهة كلها (توضع في StarterGui أو PlayerGui)
Frameمستطيل ملون فارغ — أساس التصميم
TextLabelنص غير قابل للنقر
TextButtonزر قابل للنقر
ImageLabelصورة
ImageButtonصورة قابلة للنقر
ScrollingFrameإطار قابل للتمرير
ViewportFrameإطار ثلاثي الأبعاد للعرض

19.3 عناصر التزيين UI Modifiers

العنصرالوظيفة
UICornerزوايا دائرية
UIStrokeحدود ملونة للنص أو الإطار
UIGradientتدرج لوني
UIPaddingهامش داخلي
UISizeConstraintتحديد حد أدنى/أقصى للحجم
UIAspectRatioConstraintالحفاظ على نسبة العرض للارتفاع
UIScaleتكبير/تصغير شامل

19.4 عناصر التخطيط Layouts

العنصرالوظيفة
UIListLayoutترتيب الأبناء في صف أو عمود
UIGridLayoutترتيب الأبناء في شبكة
UIPageLayoutترتيب في صفحات (مثل كتاب)
UITableLayoutترتيب في جدول

19.5 الخصائص الأساسية

Position و Size و UDim2

UDim2.new(xScale, xOffset, yScale, yOffset)

-- نصف الشاشة عرضًا، 60 بيكسل ارتفاعًا
frame.Size = UDim2.new(0.5, 0, 0, 60)

-- في وسط الشاشة بالضبط (مع AnchorPoint 0.5, 0.5)
frame.AnchorPoint = Vector2.new(0.5, 0.5)
frame.Position = UDim2.new(0.5, 0, 0.5, 0)
💡 قاعدة ذهبية استخدم Scale للعناصر العامة (تتكيف مع أي شاشة)، وOffset للعناصر الصغيرة (أزرار، أيقونات). خلط الاثنين يعطي تصميمًا مرنًا.

AnchorPoint

نقطة الارتساء (0-1 من كل جهة). (0, 0) = أعلى يسار، (1, 1) = أسفل يمين، (0.5, 0.5) = المركز.

Visible و ZIndex

frame.Visible = false       -- إخفاء
button.ZIndex = 5           -- الطبقة (الأكبر فوق)

Rotation

label.Rotation = 45  -- دوران 45 درجة

19.6 إنشاء واجهة برمجيًا

local Players = game:GetService("Players")
local player = Players.LocalPlayer

-- ScreenGui
local gui = Instance.new("ScreenGui")
gui.Name = "MyHUD"
gui.ResetOnSpawn = false
gui.Parent = player:WaitForChild("PlayerGui")

-- Frame
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0, 300, 0, 100)
frame.Position = UDim2.new(0.5, -150, 0, 20)
frame.BackgroundColor3 = Color3.fromRGB(30, 30, 40)
frame.BorderSizePixel = 0
frame.Parent = gui

-- UICorner
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = frame

-- UIStroke
local stroke = Instance.new("UIStroke")
stroke.Color = Color3.fromRGB(0, 200, 200)
stroke.Thickness = 2
stroke.Parent = frame

-- TextLabel
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, 0, 1, 0)
label.BackgroundTransparency = 1
label.Text = "مرحبًا بك!"
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.Font = Enum.Font.GothamBold
label.TextSize = 22
label.Parent = frame

19.7 التعامل مع الأزرار

local button = Instance.new("TextButton")
button.Size = UDim2.new(0, 200, 0, 60)
button.Position = UDim2.new(0.5, -100, 0.5, -30)
button.BackgroundColor3 = Color3.fromRGB(50, 100, 200)
button.Text = "ابدأ"
button.TextColor3 = Color3.fromRGB(255, 255, 255)
button.Font = Enum.Font.GothamBold
button.TextSize = 20
button.Parent = gui

-- UICorner
Instance.new("UICorner", button).CornerRadius = UDim.new(0, 10)

-- عند الضغط
button.MouseButton1Click:Connect(function()
    print("تم الضغط")
end)

-- عند دخول الماوس
button.MouseEnter:Connect(function()
    button.BackgroundColor3 = Color3.fromRGB(70, 130, 230)
end)

-- عند خروج الماوس
button.MouseLeave:Connect(function()
    button.BackgroundColor3 = Color3.fromRGB(50, 100, 200)
end)

-- عند الضغط باستمرار
button.MouseButton1Down:Connect(function()
    button.Size = UDim2.new(0, 190, 0, 55)
end)

button.MouseButton1Up:Connect(function()
    button.Size = UDim2.new(0, 200, 0, 60)
end)

19.8 UIListLayout — ترتيب تلقائي

local list = Instance.new("UIListLayout")
list.Padding = UDim.new(0, 8)
list.SortOrder = Enum.SortOrder.LayoutOrder
list.HorizontalAlignment = Enum.HorizontalAlignment.Center
list.Parent = frame

-- ثم أضف الأبناء مع LayoutOrder
local function addItem(text, order)
    local lbl = Instance.new("TextLabel")
    lbl.Size = UDim2.new(1, -20, 0, 40)
    lbl.BackgroundColor3 = Color3.fromRGB(60, 60, 80)
    lbl.Text = text
    lbl.TextColor3 = Color3.fromRGB(255, 255, 255)
    lbl.Font = Enum.Font.Gotham
    lbl.TextSize = 16
    lbl.LayoutOrder = order
    lbl.Parent = frame
end

addItem("عنصر 1", 1)
addItem("عنصر 2", 2)
addItem("عنصر 3", 3)

19.9 مثال شامل: HUD للعبة

-- LocalScript في StarterPlayerScripts
local Players = game:GetService("Players")
local player = Players.LocalPlayer

local gui = Instance.new("ScreenGui")
gui.Name = "HUD"
gui.ResetOnSpawn = false
gui.Parent = player:WaitForChild("PlayerGui")

-- شريط الصحة
local healthBar = Instance.new("Frame")
healthBar.Size = UDim2.new(0, 250, 0, 25)
healthBar.Position = UDim2.new(0, 20, 1, -50)
healthBar.BackgroundColor3 = Color3.fromRGB(40, 40, 50)
healthBar.BorderSizePixel = 0
healthBar.Parent = gui

local fill = Instance.new("Frame")
fill.Size = UDim2.new(1, 0, 1, 0)
fill.BackgroundColor3 = Color3.fromRGB(50, 200, 80)
fill.BorderSizePixel = 0
fill.Parent = healthBar

Instance.new("UICorner", healthBar).CornerRadius = UDim.new(0, 12)
Instance.new("UICorner", fill).CornerRadius = UDim.new(0, 12)

local healthText = Instance.new("TextLabel")
healthText.Size = UDim2.new(1, 0, 1, 0)
healthText.BackgroundTransparency = 1
healthText.Text = "100 / 100"
healthText.TextColor3 = Color3.fromRGB(255, 255, 255)
healthText.Font = Enum.Font.GothamBold
healthText.TextSize = 16
healthText.Parent = healthBar

-- ربطها بصحة اللاعب
local function bindHumanoid(humanoid)
    local function update()
        local pct = humanoid.Health / humanoid.MaxHealth
        fill.Size = UDim2.new(pct, 0, 1, 0)
        healthText.Text = math.floor(humanoid.Health) .. " / " .. math.floor(humanoid.MaxHealth)
        if pct < 0.3 then
            fill.BackgroundColor3 = Color3.fromRGB(200, 50, 50)
        else
            fill.BackgroundColor3 = Color3.fromRGB(50, 200, 80)
        end
    end

    humanoid.HealthChanged:Connect(update)
    update()
end

-- ربط الشخصية
local function onCharacter(character)
    local humanoid = character:WaitForChild("Humanoid")
    bindHumanoid(humanoid)
end

if player.Character then onCharacter(player.Character) end
player.CharacterAdded:Connect(onCharacter)

19.10 تمارين الفصل التاسع عشر

🟢 تمرين سهل أنشئ ScreenGui فيه TextLabel يكتب "أهلًا بك في لعبتي".
🟡 تمرين متوسط أنشئ زرًا يغيّر لونه عند المرور عليه، ويطبع رسالة عند الضغط عليه.
🔴 تمرين متقدم أنشئ قائمة أصدقاء باستخدام UIListLayout، وأضف 5 أسماء برمجيًا مع LayoutOrder.

الفصل العشرون: UserInputService — إدخال اللاعب

UserInputService (يُختصر UIS) هو المسؤول عن استقبال مدخلات اللاعب: الكيبورد، الماوس، اللمس، ويد التحكم. يعمل فقط في LocalScript.

20.1 الحصول على الخدمة

local UserInputService = game:GetService("UserInputService")

20.2 أنواع الإدخال

النوعEnum
لوحة مفاتيحEnum.UserInputType.Keyboard
ماوسEnum.UserInputType.MouseButton1/2
لمسEnum.UserInputType.Touch
يد تحكمEnum.UserInputType.Gamepad1

20.3 InputBegan و InputEnded

InputBegan — عند بدء الإدخال

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end -- تجاهل إذا كان داخل Chat مثلًا

    if input.KeyCode == Enum.KeyCode.E then
        print("ضغطت E")
    end
end)

InputEnded — عند رفع الإصبع/الزر

UserInputService.InputEnded:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    if input.KeyCode == Enum.KeyCode.E then
        print("رفعت E")
    end
end)
📘 gameProcessed عندما يكون true، فهذا يعني أن اللاعب يكتب في الشات أو يضغط زرًا داخل واجهة. تجاهل الإدخال في هذه الحالة حتى لا يتفاعل الكود مع الكتابة.

20.4 InputChanged — عند استمرار الإدخال

UserInputService.InputChanged:Connect(function(input, gameProcessed)
    if input.UserInputType == Enum.UserInputType.MouseMovement then
        -- print(input.Delta) -- حركة الماوس
    end
end)

20.5 الماوس

local mouse = game.Players.LocalPlayer:GetMouse()

-- موقع الماوس في العالم ثلاثي الأبعاد
print(mouse.Hit.Position)

-- الأهداف
print(mouse.Target)

-- عند الضغط
mouse.Button1Down:Connect(function()
    print("زر الماوس الأيسر")
end)

20.6 مثال: زر E لفتح صندوق

-- LocalScript
local UserInputService = game:GetService("UserInputService")
local player = game.Players.LocalPlayer

local isNearChest = false
local isOpen = false

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end

    if input.KeyCode == Enum.KeyCode.E and isNearChest and not isOpen then
        isOpen = true
        print("فتحت الصندوق!")
        -- يمكنك إرسال RemoteEvent هنا
    end
end)

20.7 مثال: ضغط الزر في الواجهة

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    if input.UserInputType == Enum.UserInputType.MouseButton1 then
        print("زر الماوس الأيسر")
    elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
        print("زر الماوس الأيمن")
    end
end)

20.8 اللمس (Mobile)

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if input.UserInputType == Enum.UserInputType.Touch then
        print("لمس في: " .. tostring(input.Position))
    end
end)
💡 نصيحة لمعرفة إن كان اللاعب على الجوال: UserInputService.TouchEnabled
على الكيبورد: UserInputService.KeyboardEnabled
على يد التحكم: UserInputService.GamepadEnabled

20.9 يد التحكم Gamepad

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if input.KeyCode == Enum.KeyCode.ButtonA then
        print("ضغط A")
    elseif input.KeyCode == Enum.KeyCode.ButtonX then
        print("ضغط X")
    end
end)

20.10 كتابة نظام يعمل على كل الأجهزة

local UserInputService = game:GetService("UserInputService")

local function onAction()
    print("تم تنفيذ الإجراء")
end

UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end

    -- كيبورد
    if input.KeyCode == Enum.KeyCode.F then
        onAction()
    end

    -- يد تحكم
    if input.KeyCode == Enum.KeyCode.ButtonY then
        onAction()
    end

    -- ماوس
    if input.UserInputType == Enum.UserInputType.MouseButton2 then
        onAction()
    end
end)

أما على الجوال، تحتاج زرًا على الشاشة. عند الضغط عليه ينفذ نفس الإجراء — وهذا موضوع الفصل القادم.

20.11 تمارين الفصل العشرون

🟢 تمرين سهل اطبع "hello" عند ضغط أي زر في الكيبورد.
🟡 تمرين متوسط اجعل ضغط زر Q يطبع موقع الماوس الحالي في العالم.
🔴 تمرين متقدم أنشئ نظامًا يعمل على كيبورد ويد تحكم وجوال: عند الضغط على E / ButtonY / زر على الشاشة، يطبع "تم".

الفصل الحادي والعشرون: ContextActionService

ContextActionService (CAS) خدمة تدير الإجراءات (Actions) بطريقة موحّدة، وتوفر تلقائيًا أزرارًا للجوال عند الحاجة. هي أفضل من UserInputService في حالات كثيرة لأنها:

21.1 الفكرة الأساسية

أنت تعرّف Action باسم، وتربطه بمفاتيح على الكيبورد وأزرار يد التحكم. CAS ينشئ زرًا على الجوال تلقائيًا.

21.2 الاستخدام الأساسي

local ContextActionService = game:GetService("ContextActionService")

local function onJumpAction(actionName, inputState, inputObject)
    if inputState == Enum.UserInputState.Begin then
        print("قفز!")
    end
    return Enum.ContextActionResult.Pass
end

ContextActionService:BindAction(
    "JumpAction",                    -- اسم الإجراء
    onJumpAction,                    -- الدالة
    true,                            -- إنشاء زر للجوال؟
    Enum.KeyCode.Space,              -- مفتاح الكيبورد
    Enum.KeyCode.ButtonA             -- زر يد التحكم
)

21.3 شرح Parameters

Parameterالمعنى
actionNameاسم فريد للإجراء
functionToBindالدالة التي تُنفَّذ
createTouchButtonهل نصنع زرًا للجوال؟ (true/false)
...inputsمفاتيح الكيبورد أو أزرار التحكم

21.4 القيم المُمرّرة للدالة

21.5 BindActionAtPriority

للتحكم بأي إجراء يُنفَّذ أولًا عند التعارض:

ContextActionService:BindActionAtPriority(
    "AttackAction",
    onAttack,
    true,
    2000,  -- أولوية عالية
    Enum.KeyCode.F
)

21.6 إلغاء الربط

ContextActionService:UnbindAction("JumpAction")

-- أو إلغاء الكل
ContextActionService:UnbindAllActions()

21.7 تعديل زر الجوال

ContextActionService:SetTitle("JumpAction", "اقفز")

ContextActionService:SetPosition("JumpAction", UDim2.new(1, -120, 1, -120))

ContextActionService:SetImage("JumpAction", "rbxassetid://0000000000")

21.8 مثال: نظام استخدام

local ContextActionService = game:GetService("ContextActionService")

local function onUse(actionName, inputState, inputObject)
    if inputState ~= Enum.UserInputState.Begin then
        return Enum.ContextActionResult.Pass
    end

    print("استخدمت!")
    -- استدعاء RemoteEvent للسيرفر هنا

    return Enum.ContextActionResult.Sink
end

ContextActionService:BindAction(
    "UseAction",
    onUse,
    true,
    Enum.KeyCode.E,
    Enum.KeyCode.ButtonX
)

-- تسمية زر الجوال
ContextActionService:SetTitle("UseAction", "استخدم")
📘 Sink و Pass Pass = دع الحدث يكمل للأنظمة الأخرى.
Sink = أوقف الحدث هنا (مفيد لأزرار مخصصة).

21.9 مثال: تبديل الأدوات 1-5

local ContextActionService = game:GetService("ContextActionService")

local function onSlot(actionName, inputState, inputObject)
    if inputState ~= Enum.UserInputState.Begin then return end

    local num = tonumber(actionName:match("%d+"))
    print("اخترت السلاح رقم " .. num)

    -- هنا: تبديل السلاح
end

for i = 1, 5 do
    ContextActionService:BindAction(
        "Slot" .. i,
        onSlot,
        false,
        Enum["KeyCode"]["One"]  -- مفتاح رقم i (احسبه حسب المفتاح)
    )
end

21.10 متى تستخدم UIS ومتى CAS؟

الحالةالأنسب
قراءة مدخلات عامة بدون واجهةUserInputService
إجراء له زر مخصص (يعمل على الجوال تلقائيًا)ContextActionService
نظام أدوات/أسلحةContextActionService
تبديل سلوتات 1-5ContextActionService
قراءة موضع الماوسUserInputService
💡 توصية إذا كان الإجراء يتطلب زرًا يظهر للاعب (سواء كيبورد أو جوال)، استخدم CAS. إذا كنت تحتاج قراءة حالة فقط، استخدم UIS.

21.11 تمارين الفصل الحادي والعشرون

🟢 تمرين سهل أنشئ إجراء "Greet" على مفتاح G يطبع "مرحبًا".
🟡 تمرين متوسط أنشئ إجراء "Sprint" على Shift، وعند الضغط اطبع "أسرع!"، وعند الرفع اطبع "طبيعي".
🔴 تمرين متقدم أنشئ إجراء يظهر كزر على الجوال باسم "هجوم" ويُشغَّل بمفتاح F على الكيبورد و ButtonX على يد التحكم.

الفصل الثاني والعشرون: TweenService — الحركة السلسة

TweenService يحرّك أي خاصية رقمية بشكل سلس بدلًا من القفزات الفجائية. هو أساس كل الأنيميشن الاحترافي في الواجهات والأجزاء.

22.1 إنشاء Tween

local TweenService = game:GetService("TweenService")

local part = workspace.Part

local info = TweenInfo.new(
    2,                          -- المدة بالثواني
    Enum.EasingStyle.Quad,      -- نوع التسارع
    Enum.EasingDirection.Out,   -- الاتجاه
    0,                          -- التكرار (0 = مرة واحدة)
    false,                      -- العكس؟
    0                           -- التأخير قبل البدء
)

local goal = {
    Position = Vector3.new(20, 10, 0),
    Color = Color3.fromRGB(255, 0, 0)
}

local tween = TweenService:Create(part, info, goal)
tween:Play()

22.2 شرح TweenInfo

Parameterالمعنى
Timeمدة الحركة بالثواني
EasingStyleشكل التسارع (Linear, Quad, Sine, Bounce, Elastic…)
EasingDirectionIn, Out, InOut
RepeatCountعدد التكرار (0 = مرة، -1 = لا نهائي)
Reversesهل تعود في الاتجاه المعاكس؟
DelayTimeتأخير قبل البدء

22.3 EasingStyle — أنواع التسارع

النمطالوصف
Linearسرعة ثابتة
Quad / Cubic / Quartتسارع متزايد
Sineناعم جدًا
Bounceارتداد مثل الكرة
Elasticمرونة مثل المطاط
Backيتراجع ثم يتحرك

22.4 Completed — عند انتهاء الحركة

tween.Completed:Connect(function(playbackState)
    if playbackState == Enum.PlaybackState.Completed then
        print("انتهت الحركة")
    end
end)

22.5 Cancel و Pause

tween:Cancel()  -- إيقاف نهائي
tween:Pause()   -- إيقاف مؤقت

22.6 مثال: فتح GUI

local TweenService = game:GetService("TweenService")

local frame = script.Parent
local info = TweenInfo.new(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out)

local function openGui()
    frame.Visible = true
    frame.Position = UDim2.new(0.5, 0, 1.5, 0) -- خارج الشاشة
    local tween = TweenService:Create(frame, info, {
        Position = UDim2.new(0.5, 0, 0.5, 0)
    })
    tween:Play()
end

local function closeGui()
    local tween = TweenService:Create(frame, info, {
        Position = UDim2.new(0.5, 0, 1.5, 0)
    })
    tween:Play()
    tween.Completed:Wait()
    frame.Visible = false
end

22.7 مثال: تحريك Part

local TweenService = game:GetService("TweenService")

local part = workspace.Platform
local info = TweenInfo.new(2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, -1, true)

local tween = TweenService:Create(part, info, {
    Position = part.Position + Vector3.new(0, 20, 0)
})
tween:Play()
-- يتحرك للأعلى والأسفل للأبد

22.8 مثال: تلاشي Transparency

local TweenService = game:GetService("TweenService")

local function fadeOut(part, duration)
    local tween = TweenService:Create(
        part,
        TweenInfo.new(duration),
        {Transparency = 1}
    )
    tween:Play()
    tween.Completed:Wait()
    part:Destroy()
end

fadeOut(workspace.Part, 1)

22.9 مثال: زر متحرك عند المرور

local TweenService = game:GetService("TweenService")

local button = script.Parent
local info = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)

button.MouseEnter:Connect(function()
    local tween = TweenService:Create(button, info, {
        Size = UDim2.new(0, 220, 0, 70),
        BackgroundColor3 = Color3.fromRGB(80, 150, 255)
    })
    tween:Play()
end)

button.MouseLeave:Connect(function()
    local tween = TweenService:Create(button, info, {
        Size = UDim2.new(0, 200, 0, 60),
        BackgroundColor3 = Color3.fromRGB(50, 100, 200)
    })
    tween:Play()
end)

22.10 ملاحظات الأداء

💡 نصيحة بدلًا من إنشاء Tween جديد كل مرة، أنشئه مرة واحدة وأعد استخدامه:
local tween = TweenService:Create(...)
tween:Play() في كل مرة تحتاجه.
⚠️ تحذير TweenService تعمل على Client و Server، لكن للأجزاء التي تُحرّك لكل اللاعبين، يُفضل تشغيلها على السيرفر. أما للواجهات فدائمًا Client.

22.11 تمارين الفصل الثاني والعشرون

🟢 تمرين سهل حرّك Part من موقعه الحالي إلى (50, 10, 0) في 3 ثوانٍ.
🟡 تمرين متوسط أنشئ زرًا يكبر قليلًا عند المرور عليه، ويعود لحجمه عند الخروج.
🔴 تمرين متقدم أنشئ نافذة GUI تظهر بحركة Elastic عند فتحها، وتختفي بحركة Quad عند إغلاقها.

الفصل الثالث والعشرون: RunService

RunService هي خدمة تُخبرك عند كل إطار (Frame) أو خطوة فيزيائية. هي أساس كل شيء مستمر: الحركة، الكاميرا، الأنظمة الزمنية.

23.1 الأحداث الثلاثة الرئيسية

الحدثمتى يُستدعى؟المكان
Heartbeatبعد تحديث الفيزياءClient + Server
RenderSteppedقبل رسم كل إطارClient فقط
Steppedقبل Heartbeat (نادر الاستخدام)Client + Server

23.2 Heartbeat — الأنسب للأنظمة

local RunService = game:GetService("RunService")

RunService.Heartbeat:Connect(function(deltaTime)
    -- deltaTime = الوقت المنقضي منذ الإطار السابق (بالثواني)
    print(deltaTime)  -- حوالي 0.016 (60 FPS)
end)

deltaTime مهم جدًا: يجعل الكود مستقلًا عن سرعة الجهاز. عند ضرب السرعة بـ deltaTime، تتحرك الأشياء بنفس السرعة على جهاز قوي وضعيف.

23.3 RenderStepped — للحركات المرئية

RunService.RenderStepped:Connect(function(deltaTime)
    -- شيء يتعلق بالكاميرا أو الواجهات — يعمل في Client فقط
    workspace.CurrentCamera.CFrame = workspace.CurrentCamera.CFrame
end)
⚠️ خطأ شائع استخدام RenderStepped على Server يسبب خطأ. RenderStepped للـ Client فقط.

23.4 Stepped

RunService.Stepped:Connect(function(time, deltaTime)
    -- time = الوقت الكلي، deltaTime = وقت الإطار
end)

23.5 IsClient و IsServer

if RunService:IsClient() then
    print("أنا عميل")
end

if RunService:IsServer() then
    print("أنا سيرفر")
end

if RunService:IsStudio() then
    print("يعمل في Roblox Studio")
end

23.6 مثال: تكبير Part تدريجيًا

local RunService = game:GetService("RunService")

local part = workspace.GrowingPart
local speed = 1 -- وحدة في الثانية

local connection
connection = RunService.Heartbeat:Connect(function(dt)
    local size = part.Size + Vector3.new(dt * speed, dt * speed, dt * speed)
    if size.X > 20 then
        connection:Disconnect()
        return
    end
    part.Size = size
end)

23.7 مثال: طائرة ورقية تتحرك

local RunService = game:GetService("RunService")

local part = workspace.Kite
local t = 0

RunService.Heartbeat:Connect(function(dt)
    t += dt
    part.Position = Vector3.new(
        math.sin(t) * 10,
        10 + math.sin(t * 2) * 2,
        math.cos(t) * 10
    )
end)

23.8 مثال: عد تنازلي باستخدام Heartbeat

local RunService = game:GetService("RunService")

local remaining = 30 -- ثانية
local connection

connection = RunService.Heartbeat:Connect(function(dt)
    remaining -= dt
    if remaining <= 0 then
        connection:Disconnect()
        print("انتهى الوقت!")
    end
end)

23.9 لماذا لا يجب استخدام while true؟

-- ❌ سيء: يعمل بأقصى سرعة ممكنة، يستنزف الموارد
while true do
    part.CFrame = part.CFrame * CFrame.new(0, 0.1, 0)
end

-- ✅ أفضل: عند كل إطار بشكل منظم
RunService.Heartbeat:Connect(function(dt)
    part.CFrame = part.CFrame * CFrame.new(0, dt * 10, 0)
end)
💡 نصيحة أداء RunService يُنفَّذ عند كل إطار (~60 مرة في الثانية). لا تضع فيه عمليات ثقيلة (حسابات معقدة، GetDescendants…) وإلا ستقتل الأداء.

23.10 استخدام task.wait بدلًا منه

-- كل 0.5 ثانية
task.spawn(function()
    while true do
        task.wait(0.5)
        print("نصف ثانية")
    end
end)

-- أو داخل coroutine
coroutine.wrap(function()
    while true do
        task.wait(1)
        print("ثانية")
    end
end)()

23.11 متى تستخدم أي واحدة؟

الحالةالأداة
حركة كل إطار (سلسة)Heartbeat / RenderStepped
تحديث الكاميراRenderStepped
عدّاد تنازلي أو نظام زمنيtask.wait / Heartbeat
حركة أجزاء مرئية لكل لاعبينHeartbeat على السيرفر
حركة بصرية محليةRenderStepped

23.12 تمارين الفصل الثالث والعشرون

🟢 تمرين سهل استخدم Heartbeat لطباعة الوقت المتبقي في 10 ثوانٍ.
🟡 تمرين متوسط اجعل Part يدور حول نفسه باستمرار باستخدام Heartbeat.
🔴 تمرين متقدم أنشئ نظام "قمر" يدور حول Player في دائرة باستخدام Heartbeat و CFrame.

الفصل الرابع والعشرون: Vector3 و CFrame

هذان النوعان أساس التعامل مع العالم ثلاثي الأبعاد. كل حركة، اتجاه، دوران، أو حساب موقع يعتمد عليهما.

24.1 Vector3 — نقطة أو اتجاه في الفراغ

Vector3 يحتوي ثلاثة أرقام: X (يمين/يسار)، Y (أعلى/أسفل)، Z (أمام/خلف).

local v = Vector3.new(10, 20, 30)
print(v.X)  -- 10
print(v.Y)  -- 20
print(v.Z)  -- 30

24.2 عمليات على Vector3

local a = Vector3.new(1, 2, 3)
local b = Vector3.new(4, 5, 6)

print(a + b)         -- 5, 7, 9
print(a - b)         -- -3, -3, -3
print(a * 2)         -- 2, 4, 6
print(a / 2)         -- 0.5, 1, 1.5

-- الضرب النقطي (Dot)
print(a:Dot(b))      -- 32

-- الضرب الاتجاهي (Cross)
print(a:Cross(b))    -- -3, 6, -3

-- الطول
print(a.Magnitude)   -- 3.74...

-- اتجاه الوحدة (طول 1)
print(a.Unit)        -- 0.267, 0.535, 0.802

24.3 دوال Vector3 المهمة

local p1 = Vector3.new(0, 0, 0)
local p2 = Vector3.new(10, 0, 0)

-- المسافة بين نقطتين
print((p2 - p1).Magnitude)  -- 10

-- Lerp: نقطة بين نقطتين بنسبة
local mid = p1:Lerp(p2, 0.5)
print(mid)  -- 5, 0, 0

24.4 CFrame — الموقع + الاتجاه

CFrame يحتوي الموقع والدوران. هو الأساس لتحريك الأجزاء بدقة.

local cf = CFrame.new(10, 5, 0)
print(cf.Position)  -- 10, 5, 0
print(cf.X, cf.Y, cf.Z)

24.5 إنشاء CFrame بأنواعه

موقع فقط

local cf = CFrame.new(0, 10, 0)

موقع + اتجاه لهدف

local from = Vector3.new(0, 10, 0)
local to = workspace.Target.Position
local cf = CFrame.lookAt(from, to)

دوران بأنغولات

-- ملاحظة: CFrame.Angles يأخذ راديان، ليس درجات
local cf = CFrame.Angles(0, math.rad(90), 0)  -- دورة 90° حول المحور Y

-- تحويل من درجات إلى راديان
local function degToRad(deg)
    return deg * math.pi / 180
end

24.6 CFrame و Movement

local part = workspace.Part

-- نقل بمقدار 5 وحدات على Y
part.CFrame = part.CFrame * CFrame.new(0, 5, 0)
-- أو
part.CFrame = part.CFrame + Vector3.new(0, 5, 0)

-- تدوير بمقدار 45° حول Y
part.CFrame = part.CFrame * CFrame.Angles(0, math.rad(45), 0)
📘 فرق مهم part.Position = ... يعدّل الموقع لكن يحفظ الدوران الحالي.
part.CFrame = CFrame.new(...) يعدّل الموقع ويصفّر الدوران ما لم تستخدم Angles.

24.7 ToWorldSpace و ToObjectSpace

-- ToWorldSpace: حوّل إحداثي محلي إلى عالمي
local offset = CFrame.new(0, 0, -5)  -- 5 وحدات للأمام من منظور الجزء
local worldCF = part.CFrame * offset

-- ToObjectSpace: العكس
local localCF = part.CFrame:ToObjectSpace(worldCF)

مثال عملي: نضع جزءًا أمام اللاعب بمترين:

local character = player.Character
local root = character.HumanoidRootPart

local frontCF = root.CFrame * CFrame.new(0, 0, -2)
local newPart = Instance.new("Part")
newPart.Anchored = true
newPart.Position = frontCF.Position
newPart.Parent = workspace

24.8 Lerp — الاستيفاء

-- CFrame Lerp
local startCF = CFrame.new(0, 10, 0)
local endCF = CFrame.new(50, 10, 0)

local midCF = startCF:Lerp(endCF, 0.5)
print(midCF.Position)  -- 25, 10, 0

24.9 مثال: هز الجزء

local RunService = game:GetService("RunService")

local part = workspace.ShakyPart
local base = part.CFrame
local t = 0

RunService.Heartbeat:Connect(function(dt)
    t += dt
    local offset = Vector3.new(
        math.sin(t * 30) * 0.1,
        math.cos(t * 30) * 0.1,
        0
    )
    part.CFrame = base + offset
end)

24.10 مثال: اتجاه الماوس

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local part = workspace.AimingPart

RunService.RenderStepped:Connect(function()
    local mousePos = UserInputService:GetMouseLocation()
    local ray = camera:ViewportPointToRay(mousePos.X, mousePos.Y)

    -- نضع الجزء في اتجاه الماوس
    part.CFrame = CFrame.lookAt(part.Position, part.Position + ray.Direction)
end)

24.11 مثال: السير نحو الهدف

local RunService = game:GetService("RunService")

local part = workspace.Mover
local target = workspace.Target
local speed = 10

RunService.Heartbeat:Connect(function(dt)
    local current = part.Position
    local goal = target.Position
    local direction = (goal - current).Unit
    local step = direction * speed * dt

    part.Position = current + step

    if (goal - part.Position).Magnitude < 1 then
        part.Position = goal
    end
end)

24.12 حساب المسافة والاتجاه

local a = workspace.PartA.Position
local b = workspace.PartB.Position

local distance = (b - a).Magnitude
print("المسافة: " .. distance)

local direction = (b - a).Unit
print("الاتجاه: " .. tostring(direction))

-- نقطة على الطريق بعد 30%
local midPoint = a:Lerp(b, 0.3)

24.13 تمارين الفصل الرابع والعشرون

🟢 تمرين سهل أنشئ Vector3 قيمته (10, 20, 30)، واطبع مجموع إحداثياته.
🟡 تمرين متوسط أنشئ جزءًا على يمين اللاعب بمقدار 3 وحدات باستخدام CFrame.
🔴 تمرين متقدم اجعل Part يتحرك في دائرة حول نقطة ثابتة باستخدام CFrame و sin/cos.

الفصل الخامس والعشرون: الفيزياء Physics

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

25.1 BasePart — أساس كل شيء فيزيائي

كل جسم ثلاثي الأبعاد في Roblox يرث من BasePart: Part، WedgePart، MeshPart، TrussPart، CornerWedgePart… كلها تشترك في نفس الخصائص الفيزيائية الأساسية.

25.2 Anchored — التثبيت

عندما تكون Anchored = true، يتجاهل الجزء الجاذبية والاصطدامات ولا يتحرك أبدًا. استخدمه للأرضيات، المباني، المنصات الثابتة.

local platform = workspace.Platform
platform.Anchored = true
⚠️ خطأ شائع نسيان Anchored = true على المباني يجعلها تسقط وتنهار عند تشغيل اللعبة. اضبط هذه الخاصية في Studio قبل الكتابة.

25.3 CanCollide — التصادم

part.CanCollide = false  -- يمر اللاعب من خلاله
القيمةالاستخدام
trueأرضيات، جدران، صناديق
falseمناطق تأثير، Trigger، Decoration

25.4 Mass — الكتلة

الكتلة تُحدَّد تلقائيًا حسب الحجم والكثافة. كلما زادت الكتلة زادت صعوبة تحريك الجسم.

local part = workspace.HeavyPart
print(part:GetMass())  -- الكتلة الفعلية

-- لتقليل الكتلة دون تغيير الحجم
part.CustomPhysicalProperties = PhysicalProperties.new(
    0.5,   -- الكثافة
    0.3,   -- الاحتكاك (friction)
    0.5,   -- المرونة (elasticity)
    0,     -- وزن الاحتكاك
    0      -- وزن المرونة
)

25.5 Assembly — التجمع

عندما تربط عدة أجزاء بـ Weld أو Motor أو أي Constraint، تُصبح Assembly واحد يتحرك كقطعة واحدة.

WeldConstraint — ربط جزأين

local weld = Instance.new("WeldConstraint")
weld.Part0 = part1
weld.Part1 = part2
weld.Parent = part1

-- الآن part1 و part2 يتحركان كأنهما قطعة واحدة

Motor6D — للأنيميشن والأعضاء

local motor = Instance.new("Motor6D")
motor.Part0 = rootPart
motor.Part1 = armPart
motor.C0 = CFrame.new(0, 0, 0)
motor.C1 = CFrame.new(0, 1, 0)
motor.Parent = rootPart

25.6 LinearVelocity — الحركة المستمرة (النظام الحديث)

ملاحظة: BodyVelocity لم تعد مستحسنة. استخدم LinearVelocity.

local attachment = Instance.new("Attachment")
attachment.Parent = part

local velocity = Instance.new("LinearVelocity")
velocity.Attachment0 = attachment
velocity.MaxForce = math.huge     -- قوة غير محدودة
velocity.VectorVelocity = Vector3.new(0, 0, -50)  -- 50 وحدة للأمام
velocity.Parent = part

-- لإيقافه
-- velocity:Destroy()

25.7 AngularVelocity — الدوران المستمر

local attachment = Instance.new("Attachment")
attachment.Parent = part

local spin = Instance.new("AngularVelocity")
spin.Attachment0 = attachment
spin.MaxTorque = math.huge
spin.AngularVelocity = Vector3.new(0, 10, 0)  -- يدور حول Y
spin.Parent = part

25.8 Torque — عزم دوراني

local attachment = Instance.new("Attachment")
attachment.Parent = part

local torque = Instance.new("Torque")
torque.Attachment0 = attachment
torque.MaxTorque = 1000
torque.RelativeTo = Enum.ActuatorRelativeTo.World
torque.Torque = Vector3.new(0, 500, 0)
torque.Parent = part

25.9 Constraints الأساسية

القيدالوظيفة
HingeConstraintمفصلة — كباب يدور
SpringConstraintنابض يمدد ويضغط
RopeConstraintحبل بين جزأين
RodConstraintقضيب ثابت الطول
BallSocketConstraintكرة في تجويف — دوران حر
UniversalConstraintدوران حول محورين
PrismaticConstraintحركة خطية فقط
AlignPositionمحاذاة الموقع
AlignOrientationمحاذاة الاتجاه

مثال: باب يدور

local door = workspace.Door
local frame = workspace.DoorFrame

local hingeAttachment1 = Instance.new("Attachment")
hingeAttachment1.Position = Vector3.new(-2, 0, 0)  -- طرف الباب
hingeAttachment1.Parent = door

local hingeAttachment2 = Instance.new("Attachment")
hingeAttachment2.Position = Vector3.new(-2, 0, 0)
hingeAttachment2.Parent = frame

local hinge = Instance.new("HingeConstraint")
hinge.Attachment0 = hingeAttachment1
hinge.Attachment1 = hingeAttachment2
hinge.Parent = door

25.10 Gravity — الجاذبية

workspace.Gravity = 196.2  -- الافتراضي
workspace.Gravity = 50     -- جاذبية أخف (قفز أعلى)
workspace.Gravity = 0      -- انعدام جاذبية

25.11 مثال: كرة تسقط

local ball = Instance.new("Part")
ball.Shape = Enum.PartType.Ball
ball.Size = Vector3.new(4, 4, 4)
ball.Position = Vector3.new(0, 50, 0)
ball.Anchored = false
ball.Material = Enum.Material.SmoothPlastic
ball.BrickColor = BrickColor.new("Bright red")
ball.Parent = workspace

-- تسقط طبيعيًا بفعل الجاذبية

25.12 مثال: قاذفة

local part = workspace.Ball
local attachment = Instance.new("Attachment")
attachment.Parent = part

local impulse = Instance.new("LinearVelocity")
impulse.Attachment0 = attachment
impulse.MaxForce = math.huge
impulse.VectorVelocity = Vector3.new(0, 100, -200)
impulse.Parent = part

-- نوقف القوة بعد 0.1 ثانية حتى يكون تأثيرها لحظيًا
task.delay(0.1, function()
    impulse:Destroy()
end)

25.13 Raycasting — مذكور هنا للإشارة

Raycasting أداة مهمة جدًا في الفيزياء، وسنخصص لها الفصل القادم بالكامل.

25.14 تمارين الفصل الخامس والعشرون

🟢 تمرين سهل أنشئ كرة تسقط من ارتفاع 100، واجعل الاحتكاك صفرًا.
🟡 تمرين متوسط أنشئ منصة تهتز للأمام والخلف باستخدام LinearVelocity متكررة.
🔴 تمرين متقدم أنشئ نظام باب يفتح تلقائيًا عندما يقترب اللاعب، ويغلق عند الابتعاد.

الفصل السادس والعشرون: Raycasting

Raycasting يعني "إرسال شعاع" من نقطة في اتجاه، ومعرفة ما يصطدم به. هي أداة أساسية للقتال (Hit Detection)، الأسلحة، الرؤية، AI، الكاميرا.

26.1 إنشاء Raycast

local origin = Vector3.new(0, 10, 0)     -- من
local direction = Vector3.new(0, 0, -50) -- اتجاه وطول

local result = workspace:Raycast(origin, direction)

if result then
    print("أصاب: " .. result.Instance.Name)
    print("في: " .. tostring(result.Position))
    print("المسافة: " .. result.Distance)
else
    print("لم يصب شيئًا")
end

26.2 مكونات RaycastResult

الخاصيةالمعنى
Instanceالشيء الذي صُدم
Positionنقطة الاصطدام
Normalاتجاه سطح الاصطدام
Distanceالمسافة المقطوعة
Materialمادة السطح

26.3 RaycastParams — تخصيص البحث

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {player.Character}  -- تجاهل الشخصية
params.IgnoreWater = true
params.RespectCanCollide = false

local result = workspace:Raycast(origin, direction, params)

FilterType

النوعالمعنى
Excludeتجاهل المذكورين
Includeاصطدم فقط بالمذكورين
📘 ملاحظة Enum.RaycastFilterType.Blacklist و Whitelist قديمة. استخدم Exclude و Include.

26.4 مثال: سلاح Raycast

local function fireWeapon(player, origin, direction)
    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {player.Character}

    local result = workspace:Raycast(origin, direction * 500, params)

    if result then
        local hit = result.Instance
        local character = hit:FindFirstAncestorOfClass("Model")
        if character then
            local humanoid = character:FindFirstChildOfClass("Humanoid")
            if humanoid and humanoid.Health > 0 then
                humanoid:TakeDamage(25)
            end
        end
    end
end

26.5 مثال: فحص وجود لاعب في مرمى البصر

local function canSee(from, target)
    local origin = from.Position
    local direction = (target.Position - origin).Unit * 1000

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {from.Parent}  -- تجاهل نفس اللاعب

    local result = workspace:Raycast(origin, direction, params)
    if not result then return false end

    local hitCharacter = result.Instance:FindFirstAncestorOfClass("Model")
    return hitCharacter == target.Parent
end

26.6 مثال: اكتشاف الأرض

local function isOnGround(character)
    local root = character:FindFirstChild("HumanoidRootPart")
    if not root then return false end

    local origin = root.Position
    local direction = Vector3.new(0, -5, 0)  -- 5 وحدات للأسفل

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {character}

    local result = workspace:Raycast(origin, direction, params)
    return result ~= nil
end

26.7 مثال: تتبع الماوس

local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera

local function getMouseTarget()
    local mousePos = UserInputService:GetMouseLocation()
    local ray = camera:ViewportPointToRay(mousePos.X, mousePos.Y)

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {player.Character}

    return workspace:Raycast(ray.Origin, ray.Direction * 1000, params)
end

local result = getMouseTarget()
if result then
    print("الماوس على: " .. result.Instance.Name)
end

26.8 Raycast من الكاميرا (First Person)

local camera = workspace.CurrentCamera

local function shootFromCamera(range)
    local origin = camera.CFrame.Position
    local direction = camera.CFrame.LookVector * range

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {game.Players.LocalPlayer.Character}

    return workspace:Raycast(origin, direction, params)
end

26.9 Raycast من السلاح (Muzzle)

-- احصل على نقطة فوهة السلاح
local muzzle = tool:FindFirstChild("Muzzle")
local origin = muzzle.Position
local target = mouse.Hit.Position
local direction = (target - origin).Unit

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {player.Character}

local result = workspace:Raycast(origin, direction * 1000, params)

26.10 مثال شامل: نظام Hit Detection

local function hitScan(player, origin, target)
    local direction = (target - origin)
    local distance = direction.Magnitude
    direction = direction.Unit

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {player.Character}

    local result = workspace:Raycast(origin, direction * distance, params)
    if not result then return nil end

    local hitModel = result.Instance:FindFirstAncestorOfClass("Model")
    if not hitModel then return nil end

    local humanoid = hitModel:FindFirstChildOfClass("Humanoid")
    if not humanoid then return nil end

    return {
        character = hitModel,
        humanoid = humanoid,
        position = result.Position,
        normal = result.Normal
    }
end

26.11 Raycast vs Touched

RaycastTouched
الدقةعالية جدًامنخفضة
الأداءسريعبطيء عند كثرة الأحداث
التحكمكاملمحدود
الاستخدامالأسلحة، الرؤية، AIمناطق، لمس بسيط
💡 توصية في نظام القتال دائمًا استخدم Raycast. Touched يسبب مشاكل (ينطلق مرات عديدة، يتأثر بالديكور).

26.12 تمارين الفصل السادس والعشرون

🟢 تمرين سهل أرسل Raycast من موضعك للأمام 100 وحدة، واطبع اسم أول شيء يصطدم به.
🟡 تمرين متوسط اجعل Raycast يتجاهل شخصيتك، واطبع موقع الاصطدام فقط.
🔴 تمرين متقدم أنشئ أداة Laser: عند الضغط، يُرسم Line بين فوهة الأداة ونقطة الاصطدام لمدة ثانية.

الفصل السابع والعشرون: الأدوات Tools والأسلحة

الـ Tool هي أداة يحملها اللاعب. توضع في Backpack، وعند اختيارها تظهر في يده. هي أساس الأسلحة، الفوانيس، الآلات، وأي شيء "يُحمل".

27.1 هيكل Tool

Tool
├── Handle (Part إلزامي إذا RequiresHandle = true)
├── (اختياري) Script
├── (اختياري) LocalScript
└── (اختياري) Attachment / Sounds

27.2 خصائص Tool الأساسية

الخاصيةالمعنى
RequiresHandleهل يحتاج Handle؟ (الافتراضي true)
CanBeDroppedهل يمكن إسقاطه؟
ToolTipنص يظهر عند المرور عليه
TextureIdأيقونة في الحقيبة
Gripكيف يُمسكه اللاعب (CFrame)

27.3 أحداث Tool

local tool = script.Parent

tool.Equipped:Connect(function(mouse)
    print("تم تجهيز الأداة")
end)

tool.Unequipped:Connect(function()
    print("تم إلغاء التجهيز")
end)

tool.Activated:Connect(function()
    print("تم الاستخدام (نقر)")
end)

tool.Deactivated:Connect(function()
    print("تم إيقاف الاستخدام")
end)

27.4 Grip — كيفية الإمساك

-- تدوير الأداة في اليد
tool.Grip = CFrame.new(0, 0, 0) * CFrame.Angles(math.rad(90), 0, 0)

-- أو استخدام GripPos / GripForward / GripRight / GripUp (أقدم)
tool.GripForward = Vector3.new(0, 0, -1)
tool.GripRight = Vector3.new(1, 0, 0)
tool.GripUp = Vector3.new(0, 1, 0)

27.5 إعطاء Tool للاعب

-- من ServerStorage
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")

Players.PlayerAdded:Connect(function(player)
    -- نسخ الأداة وإعطاءها للاعب
    local sword = ServerStorage.Sword:Clone()
    sword.Parent = player.Backpack
end)

27.6 نظام سلاح كامل: Client + Server

الهيكل

ReplicatedStorage
└── Remotes
    └── SwordAttack (RemoteEvent)

ServerStorage
└── Sword (Tool)
    ├── Handle
    └── ServerScript (Script)

StarterPack
└── (اختياري لو أردت الأداة تلقائيًا)

Client — يلتقط الضغط ويرسل للسيرفر

-- LocalScript داخل Sword
local tool = script.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local swordAttack = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("SwordAttack")

local cooldown = false

tool.Activated:Connect(function()
    if cooldown then return end
    cooldown = true

    -- نرسل الموقع الذي يريد ضربه
    local mouse = game.Players.LocalPlayer:GetMouse()
    swordAttack:FireServer(mouse.Hit.Position)

    task.wait(0.5)
    cooldown = false
end)

Server — يتحقق وينفذ

-- Script داخل Sword (في ServerStorage)
local tool = script.Parent
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local swordAttack = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("SwordAttack")

local DAMAGE = 25
local RANGE = 8
local COOLDOWN = 0.5

local lastAttack = {}

swordAttack.OnServerEvent:Connect(function(player, targetPosition)
    -- 1. تحقق أن الأداة في يد اللاعب فعلًا
    local character = player.Character
    if not character then return end

    local equippedTool = character:FindFirstChildOfClass("Tool")
    if equippedTool ~= tool then return end

    -- 2. منع السبام
    local now = tick()
    if lastAttack[player] and now - lastAttack[player] < COOLDOWN then
        return
    end
    lastAttack[player] = now

    -- 3. تحقق من نوع targetPosition
    if typeof(targetPosition) ~= "Vector3" then return end

    -- 4. Raycast من اليد نحو الهدف
    local root = character:FindFirstChild("HumanoidRootPart")
    local hand = character:FindFirstChild("RightHand") or character:FindFirstChild("Right Arm")
    if not root then return end

    local origin = hand and hand.Position or root.Position
    local direction = (targetPosition - origin)

    if direction.Magnitude > RANGE then
        direction = direction.Unit * RANGE
    end

    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {character}

    local result = workspace:Raycast(origin, direction, params)
    if not result then return end

    local hitCharacter = result.Instance:FindFirstAncestorOfClass("Model")
    if not hitCharacter then return end

    local humanoid = hitCharacter:FindFirstChildOfClass("Humanoid")
    if humanoid and humanoid.Health > 0 then
        humanoid:TakeDamage(DAMAGE)
    end
end)

game.Players.PlayerRemoving:Connect(function(player)
    lastAttack[player] = nil
end)
📘 لماذا هذا الكود آمن؟

27.7 Tool مع Animate — تغيير الحركة

local tool = script.Parent
local Players = game:GetService("Players")
local player = Players.LocalPlayer

tool.Equipped:Connect(function()
    local character = player.Character
    if character then
        local humanoid = character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid.WalkSpeed = 10  -- يبطئ اللاعب
        end
    end
end)

tool.Unequipped:Connect(function()
    local character = player.Character
    if character then
        local humanoid = character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid.WalkSpeed = 16  -- يرجع
        end
    end
end)

27.8 مثال: مصباح يدوي

local tool = script.Parent
local handle = tool.Handle

local light = Instance.new("PointLight")
light.Brightness = 5
light.Range = 30
light.Color = Color3.fromRGB(255, 240, 200)
light.Parent = handle

tool.Equipped:Connect(function()
    light.Enabled = true
end)

tool.Unequipped:Connect(function()
    light.Enabled = false
end)

27.9 تمارين الفصل السابع والعشرون

🟢 تمرين سهل أنشئ Tool مع Handle، وأعطه للاعب عند دخوله اللعبة.
🟡 تمرين متوسط اطبع في Output "تم الهجوم!" عند Activated و "انتهى الهجوم" عند Deactivated.
🔴 تمرين متقدم أنشئ عصا سحرية عند استخدامها تُشغّل صوتًا وتُظهر Part صغير يطير أمام اللاعب.

الفصل الثامن والعشرون: نظام القتال M1 Combat

نظام M1 هو نظام الضربات المتتابعة (Combos) — أساس كل ألعاب القتال. سنبنيه خطوة بخطوة مع Combo، Animations، Cooldown، Server Validation، والأمان.

28.1 الفكرة العامة

  1. اللاعب يضغط زر الهجوم.
  2. Client يشغّل الأنيميشن محليًا (استجابة سريعة).
  3. Client يرسل RemoteEvent للسيرفر.
  4. السيرفر يتحقق ويحسب الضرر بـ Raycast أو Hitbox.
  5. السيرفر يطبّق الضرر ويُبلّغ جميع العملاء.
  6. يُعاد للـ Combo التالي إذا ضغط اللاعب بسرعة.

28.2 الهيكل المقترح

ReplicatedStorage
├── Remotes
│   ├── M1Attack
│   └── M1Effect
└── Modules
    └── CombatConfig

ServerScriptService
└── CombatServer

StarterPlayerScripts
└── CombatClient

StarterPlayer/StarterCharacterScripts (أو الأداة)
└── Animations

28.3 CombatConfig — إعدادات مركزية

-- ReplicatedStorage/Modules/CombatConfig
local Config = {}

Config.COMBO_RESET_TIME = 1.5     -- الوقت لتصفير الـ Combo
Config.COMBO_COOLDOWN = 0.35      -- بين كل ضربة وأخرى
Config.MAX_COMBO = 4              -- عدد الضربات
Config.DAMAGE_PER_HIT = 10        -- الضرر
Config.HITBOX_RANGE = 8           -- مدى الضربة
Config.HITBOX_ANGLE = 90          -- زاوية المروحة (درجات)

Config.ANIMATIONS = {
    "rbxassetid://0",  -- ضربة 1
    "rbxassetid://0",  -- ضربة 2
    "rbxassetid://0",  -- ضربة 3
    "rbxassetid://0"   -- ضربة 4
}

Config.SOUNDS = {
    "rbxassetid://0",
    "rbxassetid://0",
    "rbxassetid://0",
    "rbxassetid://0"
}

return Config

28.4 السيرفر — CombatServer

-- ServerScriptService/CombatServer
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Config = require(ReplicatedStorage.Modules.CombatConfig)

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local m1Attack = remotes:WaitForChild("M1Attack")
local m1Effect = remotes:WaitForChild("M1Effect")

-- حالة كل لاعب
local playerState = {}

local function getState(player)
    if not playerState[player] then
        playerState[player] = {
            combo = 0,
            lastHit = 0,
            isAttacking = false
        }
    end
    return playerState[player]
end

-- حساب الزاوية بين اتجاهين (بالدرجات)
local function angleBetween(a, b)
    local dot = a.Unit:Dot(b.Unit)
    dot = math.clamp(dot, -1, 1)
    return math.deg(math.acos(dot))
end

-- كشف الضربات
local function findTargets(player, character)
    local root = character:FindFirstChild("HumanoidRootPart")
    if not root then return {} end

    local targets = {}
    local origin = root.Position
    local lookDir = root.CFrame.LookVector

    for _, other in ipairs(Players:GetPlayers()) do
        if other == player then continue end

        local otherChar = other.Character
        if not otherChar then continue end

        local otherHumanoid = otherChar:FindFirstChildOfClass("Humanoid")
        if not otherHumanoid or otherHumanoid.Health <= 0 then continue end

        local otherRoot = otherChar:FindFirstChild("HumanoidRootPart")
        if not otherRoot then continue end

        local diff = otherRoot.Position - origin
        local distance = diff.Magnitude

        if distance > Config.HITBOX_RANGE then continue end

        local angle = angleBetween(lookDir, diff)
        if angle > Config.HITBOX_ANGLE / 2 then continue end

        table.insert(targets, otherHumanoid)
    end

    return targets
end

-- الاستقبال
m1Attack.OnServerEvent:Connect(function(player)
    local state = getState(player)
    local now = tick()

    -- Cooldown
    if now - state.lastHit < Config.COMBO_COOLDOWN then
        return
    end

    -- تجاوز المدة → إعادة تصفير Combo
    if now - state.lastHit > Config.COMBO_RESET_TIME then
        state.combo = 0
    end

    state.lastHit = now
    state.combo = state.combo % Config.MAX_COMBO + 1

    local character = player.Character
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid or humanoid.Health <= 0 then return end

    -- التطبيق الفعلي للضرر
    local targets = findTargets(player, character)
    for _, targetHumanoid in ipairs(targets) do
        targetHumanoid:TakeDamage(Config.DAMAGE_PER_HIT)
    end

    -- إشعار العملاء
    m1Effect:FireAllClients(player, state.combo)
end)

Players.PlayerRemoving:Connect(function(player)
    playerState[player] = nil
end)

28.5 العميل — CombatClient

-- StarterPlayerScripts/CombatClient
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")

local Config = require(ReplicatedStorage.Modules.CombatConfig)

local player = Players.LocalPlayer
local remotes = ReplicatedStorage:WaitForChild("Remotes")
local m1Attack = remotes:WaitForChild("M1Attack")
local m1Effect = remotes:WaitForChild("M1Effect")

local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")

-- تحميل الأنيميشن
local animationTracks = {}
for i, id in ipairs(Config.ANIMATIONS) do
    local anim = Instance.new("Animation")
    anim.AnimationId = id
    animationTracks[i] = animator:LoadAnimation(anim)
end

local localCombo = 0
local localLastHit = 0
local isAttacking = false

local function playAttack()
    local now = tick()

    if now - localLastHit > Config.COMBO_RESET_TIME then
        localCombo = 0
    end

    if now - localLastHit < Config.COMBO_COOLDOWN then
        return
    end

    localLastHit = now
    localCombo = localCombo % Config.MAX_COMBO + 1

    -- تشغيل الأنيميشن
    local track = animationTracks[localCombo]
    if track then
        track:Play(0.1)
    end

    -- تشغيل الصوت
    local soundId = Config.SOUNDS[localCombo]
    if soundId and soundId ~= "rbxassetid://0" then
        local sound = Instance.new("Sound")
        sound.SoundId = soundId
        sound.Parent = character.HumanoidRootPart
        sound:Play()
        sound.Ended:Connect(function() sound:Destroy() end)
    end

    -- إبلاغ السيرفر
    m1Attack:FireServer()
end

-- ربط الزر
UserInputService.InputBegan:Connect(function(input, gameProcessed)
    if gameProcessed then return end
    if input.UserInputType == Enum.UserInputType.MouseButton1 then
        playAttack()
    end
end)

-- تحديث localCombo عند إشعار من السيرفر
m1Effect.OnClientEvent:Connect(function(attacker, combo)
    if attacker == player then
        -- قد نضيف تأثيرات بصرية هنا
    end
end)

28.6 إنشاء RemoteEvents

-- ServerScriptService/Setup.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = Instance.new("Folder")
remotes.Name = "Remotes"
remotes.Parent = ReplicatedStorage

local m1Attack = Instance.new("RemoteEvent")
m1Attack.Name = "M1Attack"
m1Attack.Parent = remotes

local m1Effect = Instance.new("RemoteEvent")
m1Effect.Name = "M1Effect"
m1Effect.Parent = remotes

28.7 إضافة Hitbox مرئي (اختياري)

local function visualizeHitbox(character)
    local root = character:FindFirstChild("HumanoidRootPart")
    if not root then return end

    local hitbox = Instance.new("Part")
    hitbox.Size = Vector3.new(Config.HITBOX_RANGE, 4, Config.HITBOX_RANGE)
    hitbox.Anchored = true
    hitbox.CanCollide = false
    hitbox.Transparency = 0.7
    hitbox.Color = Color3.fromRGB(255, 100, 100)
    hitbox.Material = Enum.Material.Neon
    hitbox.CFrame = root.CFrame * CFrame.new(0, 0, -Config.HITBOX_RANGE/2)
    hitbox.Parent = workspace

    task.delay(0.3, function()
        hitbox:Destroy()
    end)
end

28.8 لماذا هذا النظام آمن؟

28.9 تحسينات مستقبلية

28.10 تمارين الفصل الثامن والعشرون

🟢 تمرين سهل أضف زرًا بديلًا للهجوم على الكيبورد (مثل F) يعمل بنفس الطريقة.
🟡 تمرين متوسط اجعل الضربة الرابعة أقوى من الثلاث الأولى (ضرر مضاعف).
🔴 تمرين متقدم أضف نظام Knockback: عند الضربة الثالثة يدفع الخصم خطوة للخلف.

الفصل التاسع والعشرون: الأنيميشن Animations

الأنيميشن هو ما يعطي الحياة للشخصيات. Animator يتحكم في تشغيل الإيماءات والحركات، وفي القتال يعطي إحساسًا حقيقيًا بالضربة.

29.1 العناصر الأساسية

العنصرالوظيفة
Animationكائن يحمل AnimationId
Animatorالمشغّل — موجود داخل Humanoid
AnimationTrackالنسخة المُحمَّلة القابلة للتشغيل

29.2 LoadAnimation و Play

local character = player.Character
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")

local animation = Instance.new("Animation")
animation.AnimationId = "rbxassetid://0000000000"

local track = animator:LoadAnimation(animation)
track:Play()
📘 ملاحظة في الأنظمة القديمة كانت humanoid:LoadAnimation() مستخدمة، لكنها deprecated. استخدم animator:LoadAnimation().

29.3 AnimationTrack — التحكم الكامل

track:Play(0.1)        -- مع FadeIn
track:Stop(0.2)        -- مع FadeOut
track:AdjustSpeed(1.5) -- تسريع
track:AdjustWeight(2)  -- زيادة الوزن

print(track.Length)    -- مدة الأنيميشن بالثواني
print(track.IsPlaying) -- هل يعمل الآن؟

29.4 AnimationPriority — الأولوية

لتشغيل أنيميشن فوق آخر (مثلًا فوق المشي):

track.Priority = Enum.AnimationPriority.Action
الأولويةالمعنى
Coreالأنيميشن الأساسي (المشي/القفز) — لا تستخدم
Idleالوقوف
Movementالحركة
Actionالهجوم/الأفعال
Action2فوق Action
Action3فوق Action2
Action4أعلى شيء

29.5 الأحداث مع AnimationTrack

track:Play()

track.Stopped:Connect(function()
    print("انتهى الأنيميشن")
end)

track:GetMarkerReachedSignal("Hit"):Connect(function()
    print("وصلت لحظة الضربة!")
end)

-- أو عن طريق KeyframeReached
track.KeyframeReached:Connect(function(name)
    print("وصل الإطار: " .. name)
end)
💡 نصيحة احترافية استخدم Marker Events في Roblox Animation Editor لوضع علامات على الأنيميشن (مثل "لحظة الضربة")، ثم استخدم GetMarkerReachedSignal لتفعيل Hitbox في اللحظة المناسبة.

29.6 Combo Animations

local comboAnimations = {}

local function setup(animator)
    local ids = {
        "rbxassetid://1",
        "rbxassetid://2",
        "rbxassetid://3",
        "rbxassetid://4"
    }

    for i, id in ipairs(ids) do
        local anim = Instance.new("Animation")
        anim.AnimationId = id
        comboAnimations[i] = animator:LoadAnimation(anim)
        comboAnimations[i].Priority = Enum.AnimationPriority.Action
    end
end

local combo = 0

local function playNextCombo()
    combo = combo % #comboAnimations + 1
    local track = comboAnimations[combo]
    track:Play(0.1)
end

29.7 Animator من Server أو Client؟

الاستخدامالمكان
أنيميشن اللاعب نفسه (استجابة سريعة)Client
أنيميشن يظهر لكل اللاعبينServer
أنيميشن الأدواتClient (Tool.LocalScript)
⚠️ تحذير تشغيل الأنيميشن على السيرفر يُظهرها للجميع لكن مع تأخير. أما على Client فهي فورية لكن لا يراها إلا اللاعب. للحصول على الأفضل، شغّلها على Client وأبلغ السيرفر عبر RemoteEvent لإعادة تشغيلها للآخرين.

29.8 إيقاف الأنيميشن الحالي عند تشغيل جديد

local function play(track)
    for _, other in ipairs(animator:GetPlayingAnimationTracks()) do
        if other ~= track then
            other:Stop(0.1)
        end
    end
    track:Play(0.1)
end

29.9 الأنيميشن الافتراضي للشخصية

Roblox يشغّل أنيميشن الـ Idle/Walk/Jump تلقائيًا عبر Animate script داخل كل شخصية. لا تعدّله إلا إذا كنت تريد نظامًا مخصصًا.

29.10 مثال شامل: سلاح مع أنيميشن

-- LocalScript داخل Tool
local tool = script.Parent
local player = game.Players.LocalPlayer

local idleAnim, attackAnim

tool.Equipped:Connect(function()
    local character = player.Character
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    local animator = humanoid:FindFirstChildOfClass("Animator")
    if not animator then return end

    -- Idle للإمساك
    if not idleAnim then
        local a = Instance.new("Animation")
        a.AnimationId = "rbxassetid://idle_id"
        idleAnim = animator:LoadAnimation(a)
    end
    idleAnim:Play()

    -- استعداد للهجوم
    if not attackAnim then
        local a = Instance.new("Animation")
        a.AnimationId = "rbxassetid://attack_id"
        attackAnim = animator:LoadAnimation(a)
        attackAnim.Priority = Enum.AnimationPriority.Action
    end
end)

tool.Unequipped:Connect(function()
    if idleAnim then idleAnim:Stop() end
    if attackAnim then attackAnim:Stop() end
end)

tool.Activated:Connect(function()
    if attackAnim then
        attackAnim:Play(0.1)
    end
end)

29.11 تمارين الفصل التاسع والعشرون

🟢 تمرين سهل حمّل أنيميشن وشغّله عند ضغط زر Space.
🟡 تمرين متوسط اجعل الأنيميشن يتغير حسب سرعة اللاعب (WalkSpeed).
🔴 تمرين متقدم أنشئ نظام Combo كامل: 4 ضربات، كل ضربة لها أنيميشن مختلف، وتُصفَّر إذا مرّ وقت.

الفصل الثلاثون: DataStore — حفظ البيانات

DataStore هي الخدمة التي تحفظ بيانات اللاعبين بشكل دائم على سيرفرات Roblox. بدونها، كل تقدم اللاعب يضيع عند خروجه.

⚠️ شرط مهم DataStore تعمل فقط في اللعبة المنشورة (Published) أو في Studio مع تفعيل Enable Studio Access to API Services من Game Settings → Security.

30.1 الدوال الأساسية

الدالةالوظيفة
GetDataStore(name)الحصول على مخزن
GetAsync(key)قراءة بيانات
SetAsync(key, value)حفظ بيانات
UpdateAsync(key, fn)حفظ آمن (يقرأ ثم يعدّل)
RemoveAsync(key)حذف بيانات
BindToClose(fn)تنفيذ كود قبل إغلاق السيرفر

30.2 نظام حفظ بسيط

-- ServerScriptService/DataStoreSimple.server.lua
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local store = DataStoreService:GetDataStore("PlayerData_v1")

Players.PlayerAdded:Connect(function(player)
    -- القيمة الافتراضية
    local defaultData = {
        coins = 0,
        level = 1,
        xp = 0
    }

    local success, savedData = pcall(function()
        return store:GetAsync("player_" .. player.UserId)
    end)

    local data
    if success and savedData then
        data = savedData
        print("تم تحميل بيانات " .. player.Name)
    else
        data = defaultData
        print("بيانات جديدة لـ " .. player.Name)
    end

    -- leaderstats
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = data.coins
    coins.Parent = stats

    local level = Instance.new("IntValue")
    level.Name = "Level"
    level.Value = data.level
    level.Parent = stats

    stats.Parent = player

    -- نحفظ مرجع البيانات في اللاعب
    player:SetAttribute("Data", data)  -- بديل خفيف
end)

Players.PlayerRemoving:Connect(function(player)
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end

    local data = {
        coins = stats.Coins.Value,
        level = stats.Level.Value,
        xp = 0
    }

    local success, err = pcall(function()
        store:SetAsync("player_" .. player.UserId, data)
    end)

    if not success then
        warn("فشل الحفظ: " .. tostring(err))
    end
end)

30.3 pcall — التعامل مع الأخطاء

DataStore قد تفشل بسبب الشبكة أو Rate Limit. يجب دائمًا لفّ استدعاءاتها في pcall.

local success, resultOrError = pcall(function()
    return store:GetAsync(key)
end)

if success then
    print("نجح:", resultOrError)
else
    warn("فشل:", resultOrError)
end

30.4 UpdateAsync — الأفضل للأمان

UpdateAsync يحمي من Race Conditions (لاعبان يعدّلان نفس البيانات):

store:UpdateAsync("player_" .. player.UserId, function(oldData)
    oldData = oldData or {coins = 0, level = 1}
    oldData.coins += 10
    return oldData
end)

30.5 نظام متكامل: Coins + Level + XP + Inventory

ModuleScript — PlayerData

-- ServerScriptService/Modules/PlayerData
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local store = DataStoreService:GetDataStore("PlayerData_v2")
local cache = {}  -- بيانات مؤقتة في الذاكرة

local PlayerData = {}

function PlayerData.load(player)
    local key = "p_" .. player.UserId

    local success, data = pcall(function()
        return store:GetAsync(key)
    end)

    if not success or not data then
        data = {
            coins = 0,
            level = 1,
            xp = 0,
            inventory = {}
        }
    end

    cache[player.UserId] = data
    return data
end

function PlayerData.save(player)
    local data = cache[player.UserId]
    if not data then return end

    local key = "p_" .. player.UserId
    local success, err = pcall(function()
        store:SetAsync(key, data)
    end)

    if not success then
        warn("فشل الحفظ لـ " .. player.Name .. ": " .. tostring(err))
    end
end

function PlayerData.get(player)
    return cache[player.UserId]
end

function PlayerData.addCoins(player, amount)
    local data = cache[player.UserId]
    if not data then return end
    data.coins += amount
end

function PlayerData.addXP(player, amount)
    local data = cache[player.UserId]
    if not data then return end
    data.xp += amount

    -- نظام الترقية
    while data.xp >= data.level * 100 do
        data.xp -= data.level * 100
        data.level += 1
    end
end

function PlayerData.addItem(player, itemName)
    local data = cache[player.UserId]
    if not data then return end
    table.insert(data.inventory, itemName)
end

return PlayerData

الاستخدام

local Players = game:GetService("Players")
local PlayerData = require(game.ServerScriptService.Modules.PlayerData)

Players.PlayerAdded:Connect(function(player)
    local data = PlayerData.load(player)

    -- leaderstats
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = data.coins
    coins.Parent = stats

    local level = Instance.new("IntValue")
    level.Name = "Level"
    level.Value = data.level
    level.Parent = stats

    stats.Parent = player

    -- ربط القيم مع cache
    coins.Changed:Connect(function(v)
        PlayerData.get(player).coins = v
    end)

    level.Changed:Connect(function(v)
        PlayerData.get(player).level = v
    end)
end)

Players.PlayerRemoving:Connect(function(player)
    PlayerData.save(player)
end)

30.6 الحفظ عند إغلاق السيرفر

game:BindToClose(function()
    -- تُنفَّذ قبل إغلاق السيرفر — نعطيها فرصة للحفظ
    for _, player in ipairs(Players:GetPlayers()) do
        task.spawn(function()
            PlayerData.save(player)
        end)
    end

    task.wait(3)  -- أعطِ وقتًا للحفظ
end)
⚠️ مهم جدًا لا تعتمد فقط على BindToClose. احفظ أيضًا عند PlayerRemoving وعند أحداث مهمة (شراء، ترقية).

30.7 الحفظ التلقائي الدوري

task.spawn(function()
    while true do
        task.wait(60)  -- كل دقيقة
        for _, player in ipairs(Players:GetPlayers()) do
            task.spawn(function()
                PlayerData.save(player)
            end)
        end
    end
end)

30.8 Rate Limits — تجنّب الحظر

القيودالقيمة
GetAsync60 + 10*اللاعبين في الدقيقة
SetAsync60 + 10*اللاعبين في الدقيقة
UpdateAsync60 + 10*اللاعبين في الدقيقة
💡 نصائح

30.9 هيكل DataStore جيد

{
    coins = 1500,
    level = 12,
    xp = 340,
    inventory = {"Sword", "Shield", "Potion"},
    equipped = "Sword",
    stats = {
        kills = 45,
        deaths = 12,
        wins = 8
    },
    settings = {
        musicEnabled = true,
        sfxEnabled = true
    },
    lastLogin = 1699999999
}

30.10 تمارين الفصل الثلاثون

🟢 تمرين سهل احفظ عدد العملات عند خروج اللاعب، وحمّلها عند دخوله من جديد.
🟡 تمرين متوسط أضف نظام XP مع ترقية تلقائية كل 100 XP.
🔴 تمرين متقدم أضف نظام Inventory كامل مع إمكانية إضافة غرض وقراءته وحفظه.

الفصل الحادي والثلاثون: Leaderstats

Leaderstats هو الجدول الذي يظهر أعلى يمين الشاشة (في لوحة اللاعبين). يعرض قيمًا مثل Coins و Kills و Wins. وهو الطريقة الرسمية لعرض إحصائيات اللاعبين.

31.1 الإنشاء الأساسي

-- ServerScriptService/Leaderstats.server.lua
local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"  -- الاسم إلزامي بالضبط!

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = stats

    stats.Parent = player
end)
📘 مهم اسم المجلد يجب أن يكون leaderstats بالضبط (حروف صغيرة، بلا مسافات). وأول عنصرين فقط يظهران في اللوحة على الجوال، لكن الكل يظهر على الكمبيوتر.

31.2 أنواع القيم المدعومة

النوعالاستخدام
IntValueأرقام صحيحة (نقاط، عملات)
NumberValueأرقام عشرية
StringValueنصوص (رتبة)
BoolValueصحيح/خطأ

31.3 نظام إحصائيات كامل: Coins + Kills + Wins + Level

local Players = game:GetService("Players")

local function createLeaderstats(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = stats

    local kills = Instance.new("IntValue")
    kills.Name = "Kills"
    kills.Value = 0
    kills.Parent = stats

    local wins = Instance.new("IntValue")
    wins.Name = "Wins"
    wins.Value = 0
    wins.Parent = stats

    local level = Instance.new("IntValue")
    level.Name = "Level"
    level.Value = 1
    level.Parent = stats

    stats.Parent = player
end

Players.PlayerAdded:Connect(createLeaderstats)

31.4 ربط Leaderstats مع DataStore

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local store = DataStoreService:GetDataStore("Leaderstats_v1")

Players.PlayerAdded:Connect(function(player)
    -- تحميل البيانات
    local success, data = pcall(function()
        return store:GetAsync("ls_" .. player.UserId)
    end)

    data = data or {coins = 0, kills = 0, wins = 0, level = 1}

    -- بناء leaderstats
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    for _, key in ipairs({"Coins", "Kills", "Wins", "Level"}) do
        local value = Instance.new("IntValue")
        value.Name = key
        value.Value = data[key:lower()] or 0
        value.Parent = stats
    end

    stats.Parent = player
end)

Players.PlayerRemoving:Connect(function(player)
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end

    local data = {
        coins = stats.Coins.Value,
        kills = stats.Kills.Value,
        wins = stats.Wins.Value,
        level = stats.Level.Value
    }

    pcall(function()
        store:SetAsync("ls_" .. player.UserId, data)
    end)
end)

31.5 تعديل القيم

local function addCoins(player, amount)
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end
    local coins = stats:FindFirstChild("Coins")
    if coins then
        coins.Value += amount
    end
end

local function addKill(player)
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end
    stats.Kills.Value += 1
end

31.6 مثال شامل: Kills و Deaths

local Players = game:GetService("Players")

local function setupStats(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local kills = Instance.new("IntValue")
    kills.Name = "Kills"
    kills.Value = 0
    kills.Parent = stats

    local deaths = Instance.new("IntValue")
    deaths.Name = "Deaths"
    deaths.Value = 0
    deaths.Parent = stats

    stats.Parent = player
end

local function bindCharacter(player, character)
    local humanoid = character:WaitForChild("Humanoid")

    humanoid.Died:Connect(function()
        -- زيادة عدد الموت
        local deaths = player.leaderstats:FindFirstChild("Deaths")
        if deaths then
            deaths.Value += 1
        end

        -- البحث عن القاتل
        local creator = humanoid:FindFirstChild("creator")
        if creator and creator.Value then
            local killer = creator.Value
            if killer ~= player and killer:IsA("Player") then
                local kills = killer.leaderstats:FindFirstChild("Kills")
                if kills then
                    kills.Value += 1
                end
            end
        end
    end)
end

Players.PlayerAdded:Connect(function(player)
    setupStats(player)
    player.CharacterAdded:Connect(function(character)
        bindCharacter(player, character)
    end)
end)
📘 creator value Roblox يضع قيمة creator في Humanoid عند إصابة لاعب آخر له. هذا يسهّل تحديد القاتل. يجب أن تستخدمها فقط عند التأكد أن الإصدار الأخير من الضربة كان من لاعب.

31.7 الترتيب في اللوحة

الترتيب حسب LayoutOrder على كل قيمة:

local kills = Instance.new("IntValue")
kills.Name = "Kills"
kills.LayoutOrder = 1  -- أول
kills.Parent = stats

31.8 ملاحظات مهمة

⚠️ تحذير

31.9 تمارين الفصل الحادي والثلاثون

🟢 تمرين سهل أنشئ leaderstats فيه Coins يبدأ من 100.
🟡 تمرين متوسط اجعل اللاعب يحصل على 10 Coins عند قتل عدو.
🔴 تمرين متقدم اربط Coins وKills وWins بـ DataStore مع حفظ كل 60 ثانية.

الفصل الثاني والثلاثون: ModuleScripts

ModuleScript هو سكربت لا يعمل وحده، بل يُستخدم من سكربتات أخرى عبر require(). هو الأداة الأساسية لتنظيم الكود، وإعادة استخدامه، وفصل الأنظمة إلى ملفات صغيرة.

32.1 الفكرة الأساسية

ModuleScript يشبه "صندوق أدوات". تعرّف داخله دوال وقيمًا، وترجعها بـ return. أي سكربت آخر يستطيع "استعارتها".

-- ModuleScript: MathUtils
local MathUtils = {}

function MathUtils.add(a, b)
    return a + b
end

function MathUtils.multiply(a, b)
    return a * b
end

return MathUtils

الاستخدام في سكربت آخر:

local MathUtils = require(game.ReplicatedStorage.Modules.MathUtils)

print(MathUtils.add(3, 4))       -- 7
print(MathUtils.multiply(3, 4))  -- 12

32.2 require() — القواعد

local A = require(module)
local B = require(module)
print(A == B)  -- true (نفس الجدول!)
💡 فائدة عظيمة هذه الميزة تجعل ModuleScript ممتازًا لتخزين حالة مشتركة (Shared State). كل من يستدعيه يرى نفس البيانات.

32.3 أنواع ModuleScripts

النوعالمكانالاستخدام
SharedReplicatedStorageيستخدمه السيرفر والعميل معًا
ServerServerScriptService / ServerStorageفقط للسيرفر — منطق اللعبة
ClientStarterPlayerScriptsفقط للعميل — واجهات وتأثيرات
⚠️ تحذير لا تضع Module يحتوي على منطق حساس (مثل فحص العملات) في ReplicatedStorage. لأن اللاعب يمكنه قراءة كوده! ضعه في ServerScriptService أو ServerStorage.

32.4 مثال: Module utilities

-- ReplicatedStorage/Modules/Utilities
local Utilities = {}

function Utilities.waitForChild(parent, name, timeout)
    local start = tick()
    local child = parent:FindFirstChild(name)
    while not child and tick() - start < (timeout or 5) do
        task.wait(0.1)
        child = parent:FindFirstChild(name)
    end
    return child
end

function Utilities.getHumanoid(character)
    return character and character:FindFirstChildOfClass("Humanoid") or nil
end

function Utilities.formatNumber(n)
    if n >= 1_000_000 then
        return string.format("%.1fM", n / 1_000_000)
    elseif n >= 1_000 then
        return string.format("%.1fK", n / 1_000)
    end
    return tostring(n)
end

return Utilities

32.5 مثال: Module Combat

-- ServerScriptService/Modules/Combat
local Players = game:GetService("Players")

local Combat = {}

local DAMAGE = 20
local RANGE = 8

function Combat.damageArea(attacker, position, radius)
    local origin = position or (attacker.Character.HumanoidRootPart.Position)

    for _, player in ipairs(Players:GetPlayers()) do
        if player == attacker then continue end

        local char = player.Character
        if not char then continue end

        local hrp = char:FindFirstChild("HumanoidRootPart")
        if not hrp then continue end

        if (hrp.Position - origin).Magnitude <= radius then
            local hum = char:FindFirstChildOfClass("Humanoid")
            if hum then
                hum:TakeDamage(DAMAGE)
            end
        end
    end
end

return Combat

32.6 مثال: Module PlayerData

-- ServerScriptService/Modules/PlayerData
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local store = DataStoreService:GetDataStore("PlayerData_v3")
local cache = {}

local PlayerData = {}

function PlayerData.load(player)
    local key = "p_" .. player.UserId
    local success, data = pcall(function()
        return store:GetAsync(key)
    end)

    if not success or not data then
        data = {coins = 0, level = 1, inventory = {}}
    end

    cache[player.UserId] = data
    return data
end

function PlayerData.save(player)
    local data = cache[player.UserId]
    if not data then return end

    pcall(function()
        store:SetAsync("p_" .. player.UserId, data)
    end)
end

function PlayerData.get(player)
    return cache[player.UserId]
end

function PlayerData.addCoins(player, amount)
    local data = cache[player.UserId]
    if data then
        data.coins += amount
    end
end

function PlayerData.addItem(player, item)
    local data = cache[player.UserId]
    if data then
        table.insert(data.inventory, item)
    end
end

return PlayerData

32.7 التهيئة والتخطيط

نظّم الـ Modules هكذا:

ReplicatedStorage
├── Modules
│   ├── Config         (إعدادات)
│   ├── Utilities      (دوال مساعدة)
│   └── ItemDatabase   (بيانات الأغراض)

ServerScriptService
├── Modules
│   ├── PlayerData
│   ├── Combat
│   └── RoundSystem
└── Main.server.lua    (نقطة البداية)

StarterPlayer
└── StarterPlayerScripts
    ├── Modules
    │   └── UIManager
    └── Client.client.lua

32.8 Module مع دوال وأحداث

local Signal = {}

function Signal.new()
    local s = {handlers = {}}

    function s:Connect(fn)
        table.insert(self.handlers, fn)
        return {
            Disconnect = function()
                local i = table.find(self.handlers, fn)
                if i then table.remove(self.handlers, i) end
            end
        }
    end

    function s:Fire(...)
        for _, fn in ipairs(self.handlers) do
            task.spawn(fn, ...)
        end
    end

    return s
end

return Signal

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

32.10 تمارين الفصل الثاني والثلاثون

🟢 تمرين سهل أنشئ Module فيه دالة ترجع مجموع جدول من الأرقام.
🟡 تمرين متوسط أنشئ Config Module يحتوي على كل إعدادات لعبتك (السرعة، الصحة، أسعار المتجر).
🔴 تمرين متقدم أنشئ Module PlayerData يدير Coins و Level و Inventory مع دوال add/remove/get.

الفصل الثالث والثلاثون: البرمجة الكائنية OOP في Luau

البرمجة الكائنية (Object-Oriented Programming) تجعل الكود أقرب لتمثيل الأشياء الحقيقية: لاعب، سلاح، عدو، مركبة… كل شيء "كائن" له خصائص ودوال.

33.1 الفكرة العامة

في Luau، نبني الكائنات باستخدام:

33.2 Class بسيطة

local Player = {}
Player.__index = Player

-- Constructor
function Player.new(name, health)
    local self = setmetatable({}, Player)
    self.name = name
    self.health = health or 100
    return self
end

-- Methods
function Player:takeDamage(amount)
    self.health -= amount
    if self.health < 0 then self.health = 0 end
end

function Player:heal(amount)
    self.health += amount
    if self.health > 100 then self.health = 100 end
end

function Player:isAlive()
    return self.health > 0
end

return Player

الاستخدام:

local Player = require(game.ReplicatedStorage.Modules.Player)

local p = Player.new("Ahmed", 100)
p:takeDamage(30)
print(p.health)     -- 70
print(p:isAlive())  -- true

33.3 شرح setmetatable

function Player.new(name)
    local self = setmetatable({}, Player)
    -- ...
    return self
end

setmetatable({}, Player) يخبر Luau: "إذا طُلب شيء غير موجود في هذا الجدول، ابحث عنه في Player". هذا هو سر الوصول لـ :takeDamage وغيرها.

33.4 Method vs Function

function Player.takeDamage(self, amount)  -- Function
    self.health -= amount
end

function Player:takeDamage(amount)  -- Method (نفس الشيء)
    self.health -= amount
end

-- الاستدعاء
p.takeDamage(p, 10)   -- Function
p:takeDamage(10)      -- Method (اختصار)

33.5 Class Weapon

local Weapon = {}
Weapon.__index = Weapon

function Weapon.new(name, damage, cooldown)
    local self = setmetatable({}, Weapon)
    self.name = name
    self.damage = damage or 10
    self.cooldown = cooldown or 0.5
    self.lastUsed = 0
    return self
end

function Weapon:canUse()
    return tick() - self.lastUsed >= self.cooldown
end

function Weapon:use()
    if not self:canUse() then
        return false
    end
    self.lastUsed = tick()
    return true
end

function Weapon:getDamage()
    return self.damage
end

return Weapon

33.6 الوراثة Inheritance

-- Base
local Item = {}
Item.__index = Item

function Item.new(name)
    local self = setmetatable({}, Item)
    self.name = name
    return self
end

function Item:describe()
    return "Item: " .. self.name
end

-- Derived
local Weapon = setmetatable({}, {__index = Item})
Weapon.__index = Weapon

function Weapon.new(name, damage)
    local self = setmetatable({}, Weapon)
    self.name = name
    self.damage = damage
    return self
end

function Weapon:describe()
    return "Weapon: " .. self.name .. " (" .. self.damage .. " dmg)"
end

return {Item = Item, Weapon = Weapon}

33.7 Class Player كاملة مع Inventory

local Player = {}
Player.__index = Player

function Player.new(name)
    local self = setmetatable({}, Player)
    self.name = name
    self.health = 100
    self.coins = 0
    self.inventory = {}
    return self
end

function Player:addCoins(amount)
    self.coins += amount
end

function Player:addItem(item)
    table.insert(self.inventory, item)
end

function Player:hasItem(item)
    return table.find(self.inventory, item) ~= nil
end

function Player:removeItem(item)
    local i = table.find(self.inventory, item)
    if i then
        table.remove(self.inventory, i)
    end
end

function Player:takeDamage(amount)
    self.health -= amount
    if self.health <= 0 then
        self.health = 0
        self:onDeath()
    end
end

function Player:onDeath()
    print(self.name .. " مات")
end

return Player

33.8 لماذا OOP مفيدة؟

33.9 مساوئ OOP (احذر)

💡 نصيحة استخدم OOP للأنظمة المعقدة (Combat، Inventory، Round System). لا تستخدمه لأشياء بسيطة (دالة حسابية، إشعار).

33.10 تمارين الفصل الثالث والثلاثون

🟢 تمرين سهل أنشئ Class باسم Animal فيه name و sound، ودالة speak() تطبع الصوت.
🟡 تمرين متوسط أنشئ Class Pet يرث من Animal ويضيف خاصية owner.
🔴 تمرين متقدم أنشئ Class BankAccount فيه إيداع وسحب، مع منع الرصيد السلبي.

الفصل الرابع والثلاثون: Metatables

Metatable هو "جدول أُستاذ" يتحكم بسلوك جدول آخر. هو أساس OOP، وأساس كل شيء متقدم في Luau.

34.1 setmetatable و getmetatable

local t = {}
local mt = {}

setmetatable(t, mt)
print(getmetatable(t) == mt)  -- true

34.2 __index — الأهم

عندما تحاول الوصول لمفتاح غير موجود في الجدول، يبحث Luau في __index.

local base = {greet = function() print("مرحبًا") end}
local obj = setmetatable({}, {__index = base})

obj.greet()  -- مرحبًا

__index كدالة

local t = setmetatable({}, {
    __index = function(self, key)
        return "المفتاح " .. key .. " غير موجود"
    end
})

print(t.foo)  -- المفتاح foo غير موجود

34.3 __newindex

يُستدعى عند محاولة كتابة قيمة جديدة:

local t = setmetatable({}, {
    __newindex = function(self, key, value)
        print("تحاول كتابة " .. key .. " = " .. tostring(value))
        rawset(self, key, value)
    end
})

t.x = 10
-- يطبع: تحاول كتابة x = 10
⚠️ خطر إذا كتبت داخل __newindex قيمة عادية ستحدث حلقة لا نهائية. استخدم rawset لتجاوز الـ Metatable.

34.4 __tostring

local player = {name = "Ahmed", health = 100}
setmetatable(player, {
    __tostring = function(self)
        return self.name .. " (HP: " .. self.health .. ")"
    end
})

print(player)  -- Ahmed (HP: 100)

34.5 __call — جعل الجدول قابلًا للاستدعاء

local t = setmetatable({}, {
    __call = function(self, arg)
        print("استدعيت بـ: " .. arg)
    end
})

t("hello")  -- استدعيت بـ: hello

34.6 __add, __sub, __mul, __div

local Vector = {}
Vector.__index = Vector

function Vector.new(x, y)
    return setmetatable({x = x, y = y}, Vector)
end

Vector.__add = function(a, b)
    return Vector.new(a.x + b.x, a.y + b.y)
end

Vector.__tostring = function(v)
    return "(" .. v.x .. ", " .. v.y .. ")"
end

local v1 = Vector.new(1, 2)
local v2 = Vector.new(3, 4)
print(v1 + v2)  -- (4, 6)

34.7 __eq, __lt, __le

local mt = {
    __eq = function(a, b) return a.id == b.id end,
    __lt = function(a, b) return a.score < b.score end,
    __le = function(a, b) return a.score <= b.score end
}

34.8 __len

local bag = setmetatable({}, {
    __len = function(self)
        local count = 0
        for _ in pairs(self) do count += 1 end
        return count
    end
})

bag.a = 1
bag.b = 2
bag.c = 3
print(#bag)  -- 3

34.9 __metatable — منع التعديل

local locked = setmetatable({}, {
    __metatable = "محمي"
})

print(getmetatable(locked))  -- محمي
-- setmetatable(locked, {}) -- خطأ!

34.10 __mode — Weak References

للتحكم في جمع القمامة (Garbage Collection):

-- مفاتيح ضعيفة: تُحذف تلقائيًا عند عدم وجود مرجع آخر
local cache = setmetatable({}, {__mode = "k"})

34.11 جدول كامل بالـ Metamethods

الـ metamethodالوظيفة
__indexالبحث عند عدم وجود مفتاح
__newindexالكتابة لمفتاح جديد
__tostringتحويل لنص
__callاستدعاء الجدول كدالة
__add / __sub / __mul / __divعمليات حسابية
__mod / __pow / __unmعمليات أخرى
__concatدمج بـ ..
__eq / __lt / __leمقارنات
__lenطول #
__metatableحماية
__modeWeak references
__gcعند جمع القمامة

34.12 تمارين الفصل الرابع والثلاثون

🟢 تمرين سهل أنشئ Metatable بـ __tostring يطبع وصفًا مخصصًا لجدول.
🟡 تمرين متوسط أنشئ Vector2D يدعم + و - و __tostring.
🔴 تمرين متقدم أنشئ Class Fraction يدعم + و * ويبسّط الكسور تلقائيًا.

الفصل الخامس والثلاثون: نظام الأنواع Type Checking

Luau تدعم Type Checking — نظام اختياري يحدد نوع كل متغير. هذا يساعدك على اكتشاف الأخطاء قبل تشغيل اللعبة.

35.1 Type Annotations — إعلان النوع

local name: string = "Ahmed"
local age: number = 20
local isAlive: boolean = true
local position: Vector3 = Vector3.new(0, 0, 0)

35.2 دوال بأنواع

local function add(a: number, b: number): number
    return a + b
end

local function greet(name: string): string
    return "مرحبًا " .. name
end

35.3 أنوع الجداول

local function processPlayer(player: {name: string, age: number})
    print(player.name, player.age)
end

processPlayer({name = "Ahmed", age = 20})

35.4 Type Aliases

type Vector2D = {x: number, y: number}

local function length(v: Vector2D): number
    return math.sqrt(v.x^2 + v.y^2)
end

35.5 Union Types

local function process(value: string | number)
    if type(value) == "string" then
        print("نص: " .. value)
    else
        print("رقم: " .. value)
    end
end

process("hello")
process(42)

35.6 Optional Types

local function findUser(name: string): string?
    -- قد يرجع string أو nil
    if name == "Ahmed" then
        return "Found"
    end
    return nil
end

35.7 Type Narrowing

Luau ذكية: تعرف أن القيمة لم تعد nil بعد الفحص:

local function getName(player: Player?): string
    if not player then
        return "لا أحد"
    end
    return player.Name  -- Luau تعرف أن player ليس nil
end

35.8 typeof في الفحوصات

local function process(value: any)
    if typeof(value) == "Vector3" then
        print(value.X)  -- Luau تعرف أنه Vector3
    end
end

35.9 Type Assertions

local part = workspace:FindFirstChild("Part") :: BasePart
-- :: تخبر Luau "أنا متأكد أنه BasePart"

35.10 Export Type

-- Module: Types
export type Player = {
    name: string,
    health: number,
    coins: number
}

-- استخدمها في ملف آخر
local Types = require(script.Parent.Types)
local p: Types.Player = {name = "Ahmed", health = 100, coins = 0}

35.11 Strict Mode

أضف هذا في أول السكربت لتفعيل الفحص الصارم:

--!strict
الوضعالسلوك
--!nocheckلا يوجد فحص
--!nonstrictافتراضي — فحص خفيف
--!strictفحص كامل وصارم
💡 نصيحة ابدأ بـ nonstrict في المشاريع الصغيرة. عندما تكبر، حوّل الملفات الحرجة إلى strict لتحصل على أخطاء أبكر.

35.12 الأنواع في Roblox

local part: BasePart = workspace.Part
local humanoid: Humanoid = character.Humanoid
local model: Model = workspace.MyModel
local gui: ScreenGui = player.PlayerGui.MyGui
local player: Player = game.Players.LocalPlayer
local cframe: CFrame = CFrame.new(0, 0, 0)
local color: Color3 = Color3.fromRGB(255, 0, 0)
local udim2: UDim2 = UDim2.new(0, 100, 0, 50)

35.13 فائدة حقيقية

تخيل أنك نسيت Parameter في دالة. Strict Mode سينبهك. تخيل أنك مررت string بدل number. سينبهك. هذا يوفر عليك ساعات Debugging.

35.14 تمارين الفصل الخامس والثلاثون

🟢 تمرين سهل اكتب دالة تأخذ اسم اللاعب (string) وترجع تحية (string) مع الأنواع.
🟡 تمرين متوسط عرّف نوع Point = {x: number, y: number, z: number} واكتب دالة تأخذ نقطتين وترجع المسافة.
🔴 تمرين متقدم حوّل ملف ModuleScript إلى --!strict وأصلح الأخطاء الناتجة.

الفصل السادس والثلاثون: Advanced Luau

هذا الفصل يجمع تقنيات Luau المتقدمة: Closures، Coroutines، task، معالجة الأخطاء، والتصحيح.

36.1 Closures — تذكّر البيئة

local function counter()
    local n = 0
    return function()
        n += 1
        return n
    end
end

local c = counter()
print(c())  -- 1
print(c())  -- 2
print(c())  -- 3

36.2 Coroutines — التنفيذ المتوازي

Coroutine = دالة يمكن إيقافها مؤقتًا واستئنافها. مفيدة للعمليات الطويلة دون تعطيل باقي الكود.

local co = coroutine.create(function()
    print("البداية")
    coroutine.yield()  -- إيقاف مؤقت
    print("الاستئناف")
end)

coroutine.resume(co)  -- البداية
coroutine.resume(co)  -- الاستئناف

36.3 task.spawn — الأسهل

task.spawn(function()
    print("يعمل في الخلفية")
end)

-- أو تمرير دالة جاهزة
local function work()
    print("عمل")
end
task.spawn(work)

36.4 task.delay

task.delay(2, function()
    print("بعد ثانيتين")
end)

36.5 task.wait

task.wait(1)  -- ينظر ثانية
task.wait()   -- ينظر إطارًا واحدًا

36.6 task.defer

يشبه spawn لكن ينتظر حتى نهاية الإطار الحالي:

task.defer(function()
    print("يُنفّذ بعد الإطار الحالي")
end)

36.7 المقارنة بينهم

الأداةالوصف
task.spawnينفذ فورًا في الخلفية
task.delayينفذ بعد وقت
task.waitينظر
task.deferيؤجل لنهاية الإطار
coroutineيدوي — تحكم كامل

36.8 error — إثارة خطأ

local function divide(a, b)
    if b == 0 then
        error("قسمة على صفر!")
    end
    return a / b
end

divide(10, 0)  -- سيرمي خطأ

36.9 pcall — التقاط الأخطاء

local success, result = pcall(function()
    return divide(10, 0)
end)

if success then
    print("النتيجة: " .. result)
else
    warn("خطأ: " .. result)
end

36.10 xpcall مع Stack Trace

local function onError(err)
    return "خطأ: " .. err .. "\n" .. debug.traceback()
end

local success, result = xpcall(function()
    error("مشكلة!")
end, onError)

print(result)

36.11 assert — التحقق

local function setHealth(player, hp)
    assert(type(hp) == "number", "hp يجب أن يكون رقمًا")
    assert(hp > 0, "hp يجب أن يكون موجبًا")
    -- ...
end

setHealth(player, -5)  -- خطأ: hp يجب أن يكون موجبًا

36.12 مثال عملي: تحميل آمن

local function safeLoad(userId)
    local success, data = pcall(function()
        return store:GetAsync("p_" .. userId)
    end)

    if not success then
        warn("فشل تحميل " .. userId .. ": " .. tostring(data))
        return nil
    end

    return data
end

36.13 Debug — أدوات التصحيح

-- اسم السكربت الحالي
print(script.Name)

-- معلومات عن دالة
local info = debug.info(1, "s")
print(info)

-- Stack trace
print(debug.traceback())

36.14 Retry Logic

local function retry(fn, attempts)
    attempts = attempts or 3
    for i = 1, attempts do
        local success, result = pcall(fn)
        if success then
            return true, result
        end
        task.wait(0.5)
    end
    return false, nil
end

-- الاستخدام
local ok, data = retry(function()
    return store:GetAsync("key")
end, 5)

36.15 Debounce Pattern

local debounces = {}

local function isDebounced(player, cooldown)
    local now = tick()
    if debounces[player] and now - debounces[player] < cooldown then
        return true
    end
    debounces[player] = now
    return false
end

-- عند خروج اللاعب
game.Players.PlayerRemoving:Connect(function(player)
    debounces[player] = nil
end)

36.16 Semaphore Pattern

للتحكم بعدد المهام المتزامنة:

local Semaphore = {}
Semaphore.__index = Semaphore

function Semaphore.new(max)
    return setmetatable({max = max, current = 0, queue = {}}, Semaphore)
end

function Semaphore:acquire()
    if self.current >= self.max then
        local thread = coroutine.running()
        table.insert(self.queue, thread)
        coroutine.yield()
    end
    self.current += 1
end

function Semaphore:release()
    self.current -= 1
    local next = table.remove(self.queue, 1)
    if next then
        task.spawn(next)
    end
end

return Semaphore

36.17 تمارين الفصل السادس والثلاثون

🟢 تمرين سهل استخدم task.delay لطباعة "انتهى" بعد 5 ثوانٍ.
🟡 تمرين متوسط اكتب دالة retry تعيد المحاولة 3 مرات قبل أن تستسلم.
🔴 تمرين متقدم اكتب نظام Debounce عام يعمل مع أي دالة ومدى زمني.

الفصل السابع والثلاثون: تحسين الأداء Performance

الأداء هو الفرق بين لعبة سلسة ولعبة تتعطل. Roblox تعطي كل سيرفر موارد محدودة. كل شيء تستهلكه (Instances، Memory، Connections) يؤثر على الجميع.

37.1 الأعداء الأساسية للأداء

37.2 Memory — الذاكرة

كل Instance يُنشَأ يستهلك ذاكرة. احذف ما لم تعد تحتاجه:

part:Destroy()  -- يحرر الذاكرة
-- لا تستخدم: part.Parent = nil

37.3 Garbage Collection

Luau يحرر تلقائيًا الجداول غير المستخدمة. لكن هذا يستهلك CPU. لذلك:

-- ❌ سيء: جدول جديد كل إطار
RunService.Heartbeat:Connect(function()
    local temp = {1, 2, 3}  -- 60 جدول/ثانية!
end)

-- ✅ أفضل
local temp = {1, 2, 3}
RunService.Heartbeat:Connect(function()
    -- استخدم temp
end)

37.4 Instances — لا تُكثرها

-- ❌ سيء: 1000 Part في Workspace
for i = 1, 1000 do
    local p = Instance.new("Part")
    p.Parent = workspace
end

-- ✅ أفضل: استخدم StreamingEnabled أو تجميع أو Rendering
-- أو استخدم Parts فقط عند الحاجة

37.5 Connections — فصلها

-- ❌ سيء: لا تفصل
while true do
    part.Touched:Connect(function() end)
    task.wait(1)
end

-- ✅ أفضل
local connection = part.Touched:Connect(function() end)
-- لاحقًا
connection:Disconnect()
⚠️ قاعدة ذهبية كل Connection يستهلك ذاكرة. إذا كان الحدث مؤقتًا، افصله بعد الانتهاء. خصوصًا الأحداث المرتبطة باللاعبين الذين يخرجون.

37.6 Remotes — لا تُكثر

-- ❌ سيء: 100 RemoteEvent
for i = 1, 100 do
    local r = Instance.new("RemoteEvent")
    r.Name = "Event" .. i
    r.Parent = ReplicatedStorage
end

-- ✅ أفضل: RemoteEvent واحد يستقبل نوعًا كوسيط
-- event:FireServer("attack", data)
-- event:FireServer("useItem", itemId)

37.7 RunService — استخدمه بحكمة

-- ❌ سيء: حسابات ثقيلة في كل إطار
RunService.Heartbeat:Connect(function(dt)
    for _, obj in ipairs(workspace:GetDescendants()) do
        -- حسابات ثقيلة
    end
end)

-- ✅ أفضل: خزن النتائج مسبقًا
local objects = workspace:GetDescendants()
RunService.Heartbeat:Connect(function(dt)
    for _, obj in ipairs(objects) do
        -- حسابات خفيفة
    end
end)

37.8 Debounce

local lastUsed = 0
local COOLDOWN = 0.5

local function doAction()
    local now = tick()
    if now - lastUsed < COOLDOWN then return end
    lastUsed = now
    -- نفّذ الفعل
end

37.9 StreamingEnabled

فعّلها من Workspace → StreamingEnabled = true. Roblox يحمّل فقط الأجزاء القريبة من اللاعب، مما يحسّن الأداء بشكل كبير.

37.10 Instance.new — ترتيب صحيح

-- ❌ بطيء
local part = Instance.new("Part")
part.Parent = workspace
part.Size = Vector3.new(4, 1, 4)
part.Color = Color3.new(1, 0, 0)

-- ✅ أسرع
local part = Instance.new("Part")
part.Size = Vector3.new(4, 1, 4)
part.Color = Color3.new(1, 0, 0)
part.Parent = workspace  -- أخيرًا

37.11 Bulking — اجمع التعديلات

-- ❌ بطيء
for i = 1, 100 do
    part.Size = Vector3.new(i, i, i)  -- 100 تحديث
end

-- ✅ أسرع
local lastSize
for i = 1, 100 do
    lastSize = Vector3.new(i, i, i)
end
part.Size = lastSize  -- تحديث واحد

37.12 Server Performance

37.13 Client Performance

37.14 Profiler — قياس الأداء

في Roblox Studio افتح: View → Performance Stats → Summary. سترى:

💡 MicroProfiler استخدم Ctrl + F6 لتشغيل MicroProfiler. يعرض بدقة أي جزء من كودك يستهلك أكثر وقت.

37.15 قائمة تحقق سريعة للأداء

37.16 تمارين الفصل السابع والثلاثون

🟢 تمرين سهل أنشئ 100 Part ثم احذفها بـ Destroy، ولاحظ الذاكرة قبل وبعد.
🟡 تمرين متوسط حسّن كودك السابق بإضافة Debounce على كل RemoteEvent.
🔴 تمرين متقدم اكتب نظام Spawn موزّع لـ 1000 Part بدون تعطيل اللعبة.

الفصل الثامن والثلاثون: الأمان ومكافحة الاستغلال Anti-Exploit

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

38.1 لماذا لا تثق بالـ Client؟

لأن أي لاعب يستطيع تشغيل برامج تُعدّل Client-side بالكامل. يستطيع:

⚠️ القاعدة الذهبية كل شيء في الـ Client يمكن التلاعب به. لذلك كل قرار مهم يكون في السيرفر.

38.2 RemoteEvent Abuse

❌ كود ضعيف

-- Server
buyItem.OnServerEvent:Connect(function(player, itemName)
    player.leaderstats.Coins.Value -= 100
    giveItem(player, itemName)
end)

المشاكل:

✅ كود آمن

local VALID_ITEMS = {
    Sword = {price = 100, id = 1},
    Shield = {price = 200, id = 2}
}

local lastBuy = {}

buyItem.OnServerEvent:Connect(function(player, itemName)
    -- 1. تحقق من النوع
    if type(itemName) ~= "string" then return end

    -- 2. تحقق من الغرض
    local item = VALID_ITEMS[itemName]
    if not item then return end

    -- 3. Cooldown
    local now = tick()
    if lastBuy[player] and now - lastBuy[player] < 1 then return end
    lastBuy[player] = now

    -- 4. تحقق من الرصيد
    local coins = player.leaderstats.Coins
    if coins.Value < item.price then return end

    -- 5. تنفيذ
    coins.Value -= item.price
    giveItem(player, itemName)
end)

38.3 Damage Exploits

❌ ضعيف

-- Client يقرر الضرر
damageEvent.OnServerEvent:Connect(function(player, target, damage)
    target.Humanoid:TakeDamage(damage)
end)

اللاعب يرسل Damage = 999999 ويقتل الجميع!

✅ آمن

local DAMAGE = 25
local RANGE = 8

damageEvent.OnServerEvent:Connect(function(player, targetPosition)
    if typeof(targetPosition) ~= "Vector3" then return end

    local character = player.Character
    if not character then return end

    local root = character:FindFirstChild("HumanoidRootPart")
    if not root then return end

    -- المسافة من اللاعب للهدف
    if (targetPosition - root.Position).Magnitude > RANGE then
        return
    end

    -- Raycast
    local params = RaycastParams.new()
    params.FilterType = Enum.RaycastFilterType.Exclude
    params.FilterDescendantsInstances = {character}

    local direction = (targetPosition - root.Position).Unit * RANGE
    local result = workspace:Raycast(root.Position, direction, params)

    if not result then return end

    local hitCharacter = result.Instance:FindFirstAncestorOfClass("Model")
    if not hitCharacter then return end

    local humanoid = hitCharacter:FindFirstChildOfClass("Humanoid")
    if humanoid then
        humanoid:TakeDamage(DAMAGE)
    end
end)

38.4 Money Exploits

❌ ضعيف

addCoins.OnServerEvent:Connect(function(player, amount)
    player.leaderstats.Coins.Value += amount
end)

اللاعب يرسل amount = 9999999999.

✅ آمن

-- لا تقبل القيمة من العميل مطلقًا
-- بل استخدم مصادر موثوقة

local function giveReward(player, source)
    local rewards = {
        DailyChest = 100,
        QuestComplete = 500,
        KillStreak = 50
    }

    local amount = rewards[source]
    if not amount then return end

    player.leaderstats.Coins.Value += amount
end

38.5 Speed Exploits

اللاعب يعدّل WalkSpeed محليًا. على السيرفر لاحظ الفرق:

local MAX_SPEED = 25

local function checkPlayerSpeed(player)
    local character = player.Character
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end

    if humanoid.WalkSpeed > MAX_SPEED then
        humanoid.WalkSpeed = MAX_SPEED  -- أعده للطبيعي
        warn(player.Name .. " حاول تسريع نفسه")
    end
end

-- كل 5 ثوانٍ
task.spawn(function()
    while true do
        task.wait(5)
        for _, player in ipairs(game.Players:GetPlayers()) do
            checkPlayerSpeed(player)
        end
    end
end)

38.6 Teleport Exploits

local MAX_TELEPORT_DISTANCE = 50

local lastPositions = {}

RunService.Heartbeat:Connect(function(dt)
    for _, player in ipairs(game.Players:GetPlayers()) do
        local char = player.Character
        if not char then continue end

        local hrp = char:FindFirstChild("HumanoidRootPart")
        if not hrp then continue end

        local last = lastPositions[player]
        if last then
            local distance = (hrp.Position - last).Magnitude
            -- أكثر من 100 وحدة في إطار واحد = تليبورت
            if distance > MAX_TELEPORT_DISTANCE then
                hrp.CFrame = CFrame.new(last)
            end
        end
        lastPositions[player] = hrp.Position
    end
end)

38.7 Spam Prevention

Rate Limiting

local rateLimits = {}

local function isRateLimited(player, action, maxPerSecond)
    rateLimits[player] = rateLimits[player] or {}
    rateLimits[player][action] = rateLimits[player][action] or {}

    local now = tick()
    local calls = rateLimits[player][action]

    -- احذف ما أقدم من ثانية
    for i = #calls, 1, -1 do
        if now - calls[i] > 1 then
            table.remove(calls, i)
        end
    end

    if #calls >= maxPerSecond then
        return true
    end

    table.insert(calls, now)
    return false
end

-- الاستخدام
attackEvent.OnServerEvent:Connect(function(player)
    if isRateLimited(player, "attack", 5) then
        return  -- رفض
    end
    -- نفذ
end)

38.8 Server Validation Checklist

38.9 Kick أو Ban للمخترقين

local function logViolation(player, reason)
    warn("[ANTI-CHEAT] " .. player.Name .. " - " .. reason)
    player:Kick("تم رصد نشاط غير مشروع: " .. reason)
end

-- مثال
attackEvent.OnServerEvent:Connect(function(player, damage)
    if type(damage) ~= "number" or damage > 100 then
        logViolation(player, "damage تزوير")
        return
    end
end)
💡 نصيحة لا تطرد فورًا في الحالات المشكوك فيها. سجّل فقط. الطرد الفوري يعطي المخترق معلومة أنك اكتشفته، فيتعلم تجنّب الفحص.

38.10 طرق شائعة يستخدمها المخترقون

الهجومالحل
إرسال RemoteEvents بقيم ضخمةType check + Range check
FireServer بسرعةRate limiting
تعديل WalkSpeedفحص دوري على السيرفر
Teleportفحص المسافة بين الإطارات
حذف أجزاء لعرض ميزةلا تعتمد على Client للقرارات
Clone أغراضالتحقق من المصدر
تعديل Humanoid.Healthالسيرفر يتحكم بالصحة

38.11 نصائح أخيرة

38.12 تمارين الفصل الثامن والثلاثون

🟢 تمرين سهل أضف Type check على كل RemoteEvent في مشروعك.
🟡 تمرين متوسط أضف Rate Limiting لنظام الشراء بحيث لا يمكن شراء أكثر من 3 أغراض في الثانية.
🔴 تمرين متقدم اكتب Anti-Cheat يكتشف إذا تحرك اللاعب بسرعة أعلى من المسموح، ويطرده بعد 3 تحذيرات.

الفصل التاسع والثلاثون: تنظيم المشاريع Project Organization

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

39.1 البنية الكاملة

ReplicatedStorage
├── Remotes/           (كل RemoteEvents & RemoteFunctions)
│   ├── Combat/
│   ├── Shop/
│   └── UI/
├── Modules/           (Shared Modules)
│   ├── Config
│   ├── Utilities
│   └── ItemDatabase
├── Assets/            (نماذج/أصوات/أنيميشن مشتركة)
│   ├── Sounds/
│   ├── Animations/
│   └── Models/
└── UI/                (واجهات جاهزة للنسخ)

ServerScriptService
├── Modules/           (Server-only Modules)
│   ├── PlayerData
│   ├── Combat
│   ├── Inventory
│   ├── RoundSystem
│   └── AntiCheat
├── Systems/           (أنظمة السيرفر)
│   ├── ShopSystem
│   └── QuestSystem
└── Bootstrap.server.lua    (نقطة البداية — يُشغّل كل شيء)

ServerStorage
├── Tools/
├── Weapons/
└── GameAssets/

StarterPlayer
└── StarterPlayerScripts
    ├── Modules/
    │   ├── UIManager
    │   └── InputHandler
    ├── Controllers/
    │   ├── CombatController
    │   ├── InventoryController
    │   └── HUDController
    └── Bootstrap.client.lua

StarterGui
└── (واجهات تبدأ في اللعبة)

Workspace
├── Map/
├── Spawns/
├── Interactables/
└── Effects/

Lighting
SoundService
Players

39.2 وظيفة كل مجلد

المجلدالوظيفة
ReplicatedStorage/Remotesكل التواصل بين Client و Server
ReplicatedStorage/ModulesModules مشتركة (Utilities، Config)
ServerScriptService/ModulesModules السرية — لا يراها اللاعب
ServerScriptService/Systemsأنظمة السيرفر (Shop، Quest…)
StarterPlayerScripts/Controllersأنظمة العميل (UI، Combat…)
ServerStorageأدوات ونماذج سرية

39.3 Bootstrap Pattern

بدلًا من سكربتات كثيرة تُشغّل نفسها، استخدم سكربت واحد يبدأ كل شيء:

Bootstrap.server.lua

-- ServerScriptService/Bootstrap.server.lua
local ServerScriptService = game:GetService("ServerScriptService")
local Modules = ServerScriptService.Modules

-- تشغيل الأنظمة بالترتيب
local PlayerData = require(Modules.PlayerData)
local Combat = require(Modules.Combat)
local Inventory = require(Modules.Inventory)
local AntiCheat = require(Modules.AntiCheat)

PlayerData.start()
Combat.start()
Inventory.start()
AntiCheat.start()

print("✅ السيرفر بدأ بنجاح")

كل Module فيه start()

local PlayerData = {}

function PlayerData.start()
    game.Players.PlayerAdded:Connect(function(player)
        PlayerData.load(player)
    end)

    game.Players.PlayerRemoving:Connect(function(player)
        PlayerData.save(player)
    end)
end

-- بقية الدوال
return PlayerData

39.4 Config Module — مصدر واحد للحقيقة

-- ReplicatedStorage/Modules/Config
local Config = {}

-- إعدادات اللعبة
Config.GAME = {
    MAX_PLAYERS = 30,
    ROUND_TIME = 300
}

-- إعدادات القتال
Config.COMBAT = {
    M1_DAMAGE = 15,
    M1_RANGE = 8,
    M1_COOLDOWN = 0.35,
    COMBO_RESET = 1.5,
    MAX_COMBO = 4
}

-- إعدادات الاقتصاد
Config.ECONOMY = {
    START_COINS = 100,
    KILL_REWARD = 25,
    WIN_REWARD = 500
}

-- إعدادات الحماية
Config.SECURITY = {
    MAX_SPEED = 25,
    MAX_TELEPORT = 50,
    RATE_LIMIT_ATTACK = 5
}

return Config

39.5 Naming Conventions

النوعالصيغةمثال
ModulePascalCasePlayerData, CombatSystem
Script (Server)PascalCase.serverCombatHandler.server.lua
Script (Client)PascalCase.clientHUDController.client.lua
دالةcamelCasegetPlayer, dealDamage
متغيرcamelCaseplayerHealth, currentCoins
ثابتUPPER_SNAKEMAX_HEALTH, DEFAULT_SPEED
RemoteEventPascalCaseAttackEvent, ShopPurchase
FolderPascalCaseRemotes, GameAssets

39.6 مبادئ SOLID (مبسطة)

39.7 مثال: إعادة هيكلة مشروع صغير

قبل التنظيم

ServerScriptService
├── Script  (كل شيء هنا!)
└── Script1
StarterPlayerScripts
├── LocalScript
└── LocalScript1

بعد التنظيم

ReplicatedStorage
├── Remotes
│   ├── AttackEvent
│   └── BuyItem
└── Modules
    ├── Config
    └── Utilities

ServerScriptService
├── Modules
│   ├── PlayerData
│   ├── Combat
│   └── Shop
└── Bootstrap.server.lua

StarterPlayer/StarterPlayerScripts
├── Controllers
│   ├── CombatController
│   └── ShopController
└── Bootstrap.client.lua

39.8 Git for Roblox

استخدم Rojo مع Git لمشاريع احترافية. يسمح بكتابة الكود في VS Code مع مراقبة الإصدارات. هذا الموضوع اختياري لكن مفيد جدًا.

39.9 تمارين الفصل التاسع والثلاثون

🟢 تمرين سهل أعد ترتيب مشروعك بحيث يكون فيه مجلد Remotes و Modules.
🟡 تمرين متوسط أنشئ Config Module يجمع كل الإعدادات، واستخدمه في 3 سكربتات مختلفة.
🔴 تمرين متقدم أعد بناء مشروعك بنمط Bootstrap بحيث يُشغّل كل الأنظمة من سكربت واحد.

الفصل الأربعون: مشاريع حقيقية كاملة

هذا الفصل يجمع 10 مشاريع عملية من الصفر. كل مشروع مستقل، والفصول التالية ستوضح طريقة اختبارها وحل مشاكلها.

40.1 مشروع 1: Coin Collector

الفكرة

عملات ذهبية تدور في الأرض، اللاعب يجمعها لتزيد نقاطه.

الهيكل

ServerScriptService
└── CoinSystem.server.lua

ReplicatedStorage
└── Remotes
    └── CoinCollected  (اختياري للأصوات)

السكربت

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local SPAWN_INTERVAL = 3
local MAX_COINS = 20
local COIN_VALUE = 1

local folder = Instance.new("Folder")
folder.Name = "Coins"
folder.Parent = workspace

local function createCoin(position)
    local coin = Instance.new("Part")
    coin.Shape = Enum.PartType.Cylinder
    coin.Size = Vector3.new(1, 3, 3)
    coin.Position = position + Vector3.new(0, 3, 0)
    coin.Anchored = true
    coin.CanCollide = false
    coin.Color = Color3.fromRGB(255, 215, 0)
    coin.Material = Enum.Material.Neon
    coin.Name = "Coin"
    coin.Parent = folder

    -- دوران
    task.spawn(function()
        while coin.Parent do
            coin.CFrame = coin.CFrame * CFrame.Angles(math.rad(5), 0, 0)
            RunService.Heartbeat:Wait()
        end
    end)

    -- جمع
    local debounce = false
    coin.Touched:Connect(function(hit)
        if debounce then return end
        local character = hit:FindFirstAncestorOfClass("Model")
        if not character then return end

        local player = Players:GetPlayerFromCharacter(character)
        if not player then return end

        debounce = true
        local stats = player:FindFirstChild("leaderstats")
        if stats and stats:FindFirstChild("Coins") then
            stats.Coins.Value += COIN_VALUE
        end

        coin:Destroy()
    end)
end

-- Leaderstats
Players.PlayerAdded:Connect(function(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = stats

    stats.Parent = player
end)

-- Spawner
task.spawn(function()
    while true do
        task.wait(SPAWN_INTERVAL)
        if #folder:GetChildren() < MAX_COINS then
            local x = math.random(-50, 50)
            local z = math.random(-50, 50)
            createCoin(Vector3.new(x, 0, z))
        end
    end
end)

طريقة التجربة

  1. ضع Baseplate كبيرة في Workspace.
  2. ألصق السكربت.
  3. اضغط Play واجمع العملات.

تحسينات

40.2 مشروع 2: Obby

الفكرة

مسار عوائق مع Checkpoints ونهاية.

السكربت

-- ServerScriptService/Obby.server.lua
local Players = game:GetService("Players")

local checkpoints = {}
local finished = {}

-- ضع Checkpoints في Workspace/Checkpoints (Parts)
local cpFolder = workspace:WaitForChild("Checkpoints")
for _, cp in ipairs(cpFolder:GetChildren()) do
    if cp:IsA("BasePart") then
        cp.Anchored = true
        cp.CanCollide = false
        cp.Transparency = 0.5
        table.insert(checkpoints, cp)

        local index = #checkpoints
        cp.Touched:Connect(function(hit)
            local character = hit:FindFirstAncestorOfClass("Model")
            if not character then return end

            local player = Players:GetPlayerFromCharacter(character)
            if not player then return end

            if not checkpoints[player] or checkpoints[player] < index then
                checkpoints[player] = index
                print(player.Name .. " وصل النقطة " .. index)
            end
        end)
    end
end

-- نقطة النهاية
local finish = workspace:WaitForChild("Finish")
finish.Touched:Connect(function(hit)
    local character = hit:FindFirstAncestorOfClass("Model")
    if not character then return end

    local player = Players:GetPlayerFromCharacter(character)
    if not player then return end

    if checkpoints[player] == #checkpoints and not finished[player] then
        finished[player] = true
        print(player.Name .. " أكمل الـ Obby!")
    end
end)

-- إعادة اللاعب لآخر Checkpoint عند الموت
Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(character)
        local hrp = character:WaitForChild("HumanoidRootPart")
        task.wait(0.1)
        local idx = checkpoints[player]
        if idx then
            hrp.CFrame = checkpoints[idx].CFrame + Vector3.new(0, 5, 0)
        end
    end)
end)

Players.PlayerRemoving:Connect(function(player)
    checkpoints[player] = nil
    finished[player] = nil
end)

40.3 مشروع 3: Shop System

الفكرة

متجر يشتري منه اللاعب أغراضًا بالعملات.

Server

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local buyEvent = remotes:WaitForChild("BuyItem")

local ITEMS = {
    Sword = {price = 100},
    Shield = {price = 200},
    Potion = {price = 50}
}

local inventories = {}

Players.PlayerAdded:Connect(function(player)
    inventories[player.UserId] = {}
    -- leaderstats كما في الأمثلة السابقة
end)

Players.PlayerRemoving:Connect(function(player)
    inventories[player.UserId] = nil
end)

buyEvent.OnServerEvent:Connect(function(player, itemName)
    if type(itemName) ~= "string" then return end

    local item = ITEMS[itemName]
    if not item then return end

    local stats = player:FindFirstChild("leaderstats")
    if not stats or not stats:FindFirstChild("Coins") then return end

    if stats.Coins.Value < item.price then
        return
    end

    stats.Coins.Value -= item.price
    table.insert(inventories[player.UserId], itemName)
    print(player.Name .. " اشترى " .. itemName)
end)

40.4 مشروع 4: Inventory System

-- ServerScriptService/Inventory.server.lua
local Players = game:GetService("Players")

local inventories = {}

local function addItem(player, itemName)
    inventories[player.UserId] = inventories[player.UserId] or {}
    table.insert(inventories[player.UserId], itemName)
end

local function hasItem(player, itemName)
    local inv = inventories[player.UserId]
    return inv and table.find(inv, itemName) ~= nil
end

local function removeItem(player, itemName)
    local inv = inventories[player.UserId]
    if not inv then return end
    local i = table.find(inv, itemName)
    if i then table.remove(inv, i) end
end

Players.PlayerAdded:Connect(function(player)
    inventories[player.UserId] = {}
end)

Players.PlayerRemoving:Connect(function(player)
    inventories[player.UserId] = nil
end)

-- مثال الاستخدام
Players.PlayerAdded:Connect(function(player)
    task.wait(2)
    addItem(player, "Sword")
    print(hasItem(player, "Sword"))  -- true
end)

40.5 مشروع 5: Round System

-- ServerScriptService/RoundSystem.server.lua
local Players = game:GetService("Players")

local ROUND_TIME = 60
local INTERMISSION = 10

local function startRound()
    print("🎮 بدأت الجولة!")

    -- انقل كل اللاعبين لبداية
    for _, player in ipairs(Players:GetPlayers()) do
        if player.Character then
            player.Character:PivotTo(workspace.SpawnZone.CFrame)
        end
    end

    task.wait(ROUND_TIME)
    print("⏰ انتهت الجولة!")
end

local function intermission()
    print("⏸ استراحة " .. INTERMISSION .. " ثواني")
    task.wait(INTERMISSION)
end

task.spawn(function()
    while true do
        intermission()
        startRound()
    end
end)

40.6 مشروع 6: Kill/Death System

-- ServerScriptService/KillDeath.server.lua
local Players = game:GetService("Players")

local function setupStats(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local kills = Instance.new("IntValue")
    kills.Name = "Kills"
    kills.Value = 0
    kills.Parent = stats

    local deaths = Instance.new("IntValue")
    deaths.Name = "Deaths"
    deaths.Value = 0
    deaths.Parent = stats

    stats.Parent = player
end

local function onCharacter(player, character)
    local humanoid = character:WaitForChild("Humanoid")

    humanoid.Died:Connect(function()
        player.leaderstats.Deaths.Value += 1

        local killer = humanoid:FindFirstChild("creator")
        if killer and killer.Value then
            local kp = killer.Value
            if typeof(kp) == "Instance" and kp:IsA("Player") and kp ~= player then
                kp.leaderstats.Kills.Value += 1
            end
        end
    end)
end

Players.PlayerAdded:Connect(function(player)
    setupStats(player)
    player.CharacterAdded:Connect(function(c)
        onCharacter(player, c)
    end)
end)

40.7 مشروع 7: Combat System

راجع الفصل 28 — نظام M1 كامل مع Combo و Hitbox و Server Validation.

40.8 مشروع 8: Ability System

-- ServerScriptService/AbilitySystem.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local abilityEvent = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("UseAbility")

local ABILITIES = {
    Dash = {cooldown = 5, handler = function(player)
        local char = player.Character
        if not char then return end
        local hrp = char:FindFirstChild("HumanoidRootPart")
        if not hrp then return end
        hrp.CFrame = hrp.CFrame + hrp.CFrame.LookVector * 20
    end},

    Heal = {cooldown = 10, handler = function(player)
        local char = player.Character
        if not char then return end
        local hum = char:FindFirstChildOfClass("Humanoid")
        if hum then
            hum.Health = math.min(hum.Health + 30, hum.MaxHealth)
        end
    end}
}

local cooldowns = {}

abilityEvent.OnServerEvent:Connect(function(player, abilityName)
    if type(abilityName) ~= "string" then return end

    local ability = ABILITIES[abilityName]
    if not ability then return end

    cooldowns[player] = cooldowns[player] or {}
    local now = tick()
    if cooldowns[player][abilityName] and now - cooldowns[player][abilityName] < ability.cooldown then
        return
    end
    cooldowns[player][abilityName] = now

    ability.handler(player)
end)

Players.PlayerRemoving:Connect(function(player)
    cooldowns[player] = nil
end)

40.9 مشروع 9: RNG Game System

-- ServerScriptService/RNGSystem.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local rollEvent = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("Roll")

local POOL = {
    {name = "Common",   chance = 60,  color = Color3.fromRGB(180, 180, 180)},
    {name = "Uncommon", chance = 25,  color = Color3.fromRGB(80, 200, 80)},
    {name = "Rare",     chance = 10,  color = Color3.fromRGB(50, 120, 255)},
    {name = "Epic",     chance = 4,   color = Color3.fromRGB(160, 50, 220)},
    {name = "Legendary",chance = 1,   color = Color3.fromRGB(255, 180, 0)}
}

local function roll()
    local total = 0
    for _, item in ipairs(POOL) do total += item.chance end

    local r = math.random() * total
    local acc = 0
    for _, item in ipairs(POOL) do
        acc += item.chance
        if r <= acc then
            return item
        end
    end
    return POOL[1]
end

rollEvent.OnServerEvent:Connect(function(player)
    local result = roll()
    print(player.Name .. " حصل على " .. result.name)
    -- أعطِه الجائزة، سجل في inventory، أرسل إشعارًا للعميل…
end)

40.10 مشروع 10: لعبة صغيرة متكاملة

اجمع كل الأنظمة السابقة: Coin Collector + Kill/Death + Shop + Inventory + Round System. هذا هو أساس "مشروع التخرج" في الفصل 46.

40.11 تمارين الفصل الأربعون

🟢 تمرين سهل نفّذ مشروع Coin Collector مع تغيير قيمة العملة إلى 5.
🟡 تمرين متوسط طوّر مشروع Shop بحيث يحفظ الغرض في Inventory اللاعب.
🔴 تمرين متقدم ادمج Round System مع Kill/Death لحساب الفائز في كل جولة.

الفصل الحادي والأربعون: تصحيح الأخطاء Debugging

Debugging هو 50% من وقت المطور. هذا الفصل يعلمك كيف تجد الأخطاء بسرعة بدلًا من التخبط.

41.1 نافذة Output — صديقتك الأولى

افتحها من View → Output. إذا ظهر خطأ أحمر، اقرأه جيدًا. عادةً يخبرك:

41.2 print — الأداة الأبسط

print("بدأ السكربت")
print("المتغير x =", x)

-- اطبع داخل دالة
local function test()
    print("[test] دخلت الدالة")
    print("[test] x =", x)
end
💡 نصيحة استخدم "tags" بين أقواس لتمييز المصدر: print("[Combat]", damage)

41.3 warn — للتحذيرات

warn("شيء غريب حدث:", x)

يظهر بلون برتقالي، ويسهل ملاحظته.

41.4 قراءة الأخطاء

مثال خطأ

ServerScriptService.Main:5: attempt to index nil with 'Value'

الترجمة:

41.5 Stack Trace

Stack Begin
Script 'ServerScriptService.Main', Line 5 - function doDamage
Script 'ServerScriptService.Main', Line 20
Stack End

يقول: الخطأ في السطر 5 داخل دالة doDamage، والدالة استُدعيت من السطر 20. اتبع الأثر من الداخل للخارج.

41.6 Breakpoints — في Roblox Studio

  1. افتح السكربت.
  2. اضغط على الرقم في اليسار لوضع نقطة توقف (dot أحمر).
  3. شغّل اللعبة — سيتوقف التنفيذ عندها.
  4. ستظهر نافذة Watch لمراقبة المتغيرات.

41.7 Debugger Library

-- معلومات عن الدالة الحالية
print(debug.info(1, "s"))   -- source
print(debug.info(1, "n"))   -- name

-- طباعة stack trace
print(debug.traceback())

-- الحصول على جميع المتغيرات المحلية (متقدم)
-- لكن هذا معقد قليلًا

41.8 منهجية البحث عن الخطأ

  1. اقرأ الخطأ كاملًا — لا تتخطاه.
  2. اذهب للسطر المذكور — غالبًا هو مصدر المشكلة.
  3. اطبع المتغيرات المشكوك بها قبل السطر.
  4. أضف if للتأكد من وجود الشيء.
  5. اختبر تدريجيًا — علّق أجزاء، وأعد التشغيل.
  6. راجع Stack Trace لمعرفة مسار التنفيذ.

41.9 مثال: خطأ واقعي

-- الكود الأصلي
local function dealDamage(player, amount)
    local character = player.Character
    local humanoid = character.Humanoid
    humanoid:TakeDamage(amount)
end

-- الخطأ:
-- "attempt to index nil with 'Humanoid'"

-- السبب: character قد يكون nil (لاعب خرج مثلًا)
-- الحل:
local function dealDamage(player, amount)
    local character = player.Character
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end

    humanoid:TakeDamage(amount)
end

41.10 أدوات مساعدة

الأداةالوظيفة
Outputالرسائل والأخطاء
Breakpointsإيقاف مؤقت
Watchمراقبة متغيرات
MicroProfilerتحليل الأداء (Ctrl+F6)
Developer ConsoleF9 داخل اللعبة
Command Barتنفيذ أوامر فورية

41.11 Debugging Client vs Server

عند اختبار اللعبة، جرّب التبديل بين:

اضغط على علامة التبديل أعلى الشاشة لاختيار أي جهة تراقب.

41.12 تمارين الفصل الحادي والأربعون

🟢 تمرين سهل اكتب كودًا فيه خطأ متعمد، واقرأ الخطأ في Output.
🟡 تمرين متوسط ضع Breakpoint داخل دالة، وشاهد قيمة المتغيرات عند التوقف.
🔴 تمرين متقدم استخدم xpcall مع traceback لطباعة مسار التنفيذ الكامل عند حدوث خطأ.

الفصل الثاني والأربعون: أشهر أخطاء المبتدئين

هذه قائمة بأشهر 12 خطأ يقع فيها المبتدئ، مع السبب والحل.

42.1 attempt to index nil with 'X'

السبب: تحاول الوصول لخاصية X على شيء غير موجود (nil).

-- ❌ خطأ
local part = workspace:FindFirstChild("MissingPart")
part.Transparency = 0.5  -- خطأ! part = nil

-- ✅ الحل
local part = workspace:FindFirstChild("MissingPart")
if part then
    part.Transparency = 0.5
end

42.2 Infinite yield possible

السبب: WaitForChild ينتظر شيئًا لا يصل أبدًا (الاسم خطأ، أو في مكان آخر).

-- ❌ خطأ: اسم مختلف
game.ReplicatedStorage:WaitForChild("REmoteEvent")

-- ✅ الحل: تأكد من التطابق التام
game.ReplicatedStorage:WaitForChild("RemoteEvent")

-- ✅ الحل المؤقت: حدد timeout
game.ReplicatedStorage:WaitForChild("RemoteEvent", 5)

42.3 LocalPlayer على Server

السبب: LocalPlayer متاح فقط في LocalScript. السيرفر يخدم كل اللاعبين.

-- ❌ خطأ في Script
local player = game.Players.LocalPlayer  -- nil

-- ✅ الحل: استخدم GetPlayers
for _, player in ipairs(game.Players:GetPlayers()) do
    -- ...
end

42.4 LocalScript في المكان الخطأ

LocalScript يجب أن يكون في:

لو وُضع في ServerScriptService لن يعمل أبدًا.

42.5 RemoteEvent لا يعمل

الأسباب الشائعة:

42.6 WaitForChild لا ينتهي

السبب: الشيء سيُنشأ لكن من مكان آخر، أو لن يُنشأ أصلًا.

-- تأكد أن الشيء يُنشأ بالفعل
task.spawn(function()
    task.wait(3)
    local part = Instance.new("Part")
    part.Name = "LatePart"
    part.Parent = workspace
end)

-- في Client
local part = workspace:WaitForChild("LatePart")  -- سينتظر 3 ثواني
print(part.Name)  -- LatePart

42.7 Variable is nil

-- ❌ خطأ شائع
local function setup()
    local value = 100
end

setup()
print(value)  -- nil (خارج النطاق)

-- ✅ الحل
local function setup()
    return 100
end

local value = setup()
print(value)  -- 100

42.8 Incorrect type

-- ❌ خطأ
local text = "10"
print(text + 5)  -- خطأ

-- ✅ الحل
print(tonumber(text) + 5)  -- 15

-- أو
print(text .. 5)  -- "105"

42.9 Infinite loops

-- ❌ خطأ: لا يوجد خروج
while true do
    -- لا شيء
end

-- ✅ الحل: أضف break شرط
while true do
    if condition then break end
    task.wait()
end

-- ✅ ✅ الحل الأفضل: حلقة زمنية
while true do
    task.wait(1)
    print("ثانية")
end

42.10 Memory Leaks

-- ❌ خطأ: Connections لا تُفصل
while true do
    workspace.Part.Touched:Connect(function() end)
    task.wait(1)
end

-- ✅ الحل
local connection = workspace.Part.Touched:Connect(function() end)
-- عند عدم الحاجة:
-- connection:Disconnect()

42.11 استخدام Parent = nil بدل Destroy

-- ❌ خطأ: يترك الـ Instance في الذاكرة
part.Parent = nil

-- ✅ الحل
part:Destroy()

42.12 خطأ في LocalScript داخل Workspace

-- LocalScript في Workspace لا يعمل في Studio
-- ✅ الحل: انقله إلى StarterPlayerScripts

-- أو استخدم RemoteEvent للتواصل مع السيرفر

42.13 جدول مرجعي بدلًا من Copy

local a = {1, 2, 3}
local b = a           -- ليس نسخة!
b[1] = 999
print(a[1])           -- 999 (تأثر)

-- ✅ الحل
local function shallowCopy(t)
    local c = {}
    for k, v in pairs(t) do c[k] = v end
    return c
end

local b = shallowCopy(a)

42.14 table.remove داخل الحلقة

-- ❌ خطأ
for i, v in ipairs(list) do
    if v == "remove" then
        table.remove(list, i)  -- يفسد الفهرس!
    end
end

-- ✅ الحل: من النهاية
for i = #list, 1, -1 do
    if list[i] == "remove" then
        table.remove(list, i)
    end
end

42.15 تمارين الفصل الثاني والأربعون

🟢 تمرين سهل اكتب كودًا يسبب خطأ "attempt to index nil"، ثم أصلحه.
🟡 تمرين متوسط أنشئ RemoteEvent، وأرسل من Client، وتأكد أن السيرفر يستقبل (بعد كتابة كل الشروط).
🔴 تمرين متقدم ابحث في مشروعك عن Connection لم يُفصل، وأضف فصلًا له.

الفصل الثالث والأربعون: أفضل الممارسات Best Practices

هذه القواعد تلخص سنوات من خبرة المطورين. اتبعها من البداية لتوفير وقت لاحقًا.

43.1 Clean Code

-- ❌ سيء
local x = 100
local y = x * 0.2
local z = x - y

-- ✅ جيد
local BASE_PRICE = 100
local TAX_RATE = 0.2
local tax = BASE_PRICE * TAX_RATE
local finalPrice = BASE_PRICE - tax

43.2 Naming Conventions

الشيءالصيغةمثال
متغيرcamelCaseplayerHealth
ثابتUPPER_SNAKEMAX_SPEED
دالةcamelCasedealDamage
ModulePascalCasePlayerData
EventPascalCaseAttackEvent

43.3 Comments

-- ❌ تعليق بديهي
-- يزيد 1 إلى i
i += 1

-- ✅ تعليق مفيد
-- نحتفظ بالفهرس لأن الحلقة التالية تحتاجه
i += 1

43.4 Functions صغيرة

-- ❌ دالة تفعل كل شيء
local function processPlayer(player)
    -- 100 سطر
end

-- ✅ دوال صغيرة
local function loadData(player) end
local function setupUI(player) end
local function setupCharacter(player) end

local function processPlayer(player)
    loadData(player)
    setupUI(player)
    setupCharacter(player)
end

43.5 Modules — لا تكرر الكود

-- ❌ نفس الكود في 5 أماكن
function A.f() print("مكرر") end
function B.f() print("مكرر") end
-- ...

-- ✅ Module واحد
local Helper = {}
function Helper.print() print("مرة واحدة") end
return Helper

43.6 Server Authority

43.7 Security Checklist

43.8 Performance Checklist

43.9 File Organization

43.10 Error Handling

-- استخدم pcall في كل عملية قد تفشل
local success, data = pcall(function()
    return store:GetAsync(key)
end)

if not success then
    warn("فشل التحميل: " .. tostring(data))
    -- أعطِ اللاعب بيانات افتراضية
end

43.11 Testing

43.12 Version Control Mindset

43.13 Comments & Documentation

--- تحمّل بيانات لاعب من DataStore
-- @param player : Player
-- @return table
function PlayerData.load(player)
    -- ...
end

43.14 تمارين الفصل الثالث والأربعون

🟢 تمرين سهل راجع سكربتًا قديمًا وأعد تسمية متغيراته لتصبح أوضح.
🟡 تمرين متوسط قسّم دالة طويلة عندك إلى 3 دوال أصغر.
🔴 تمرين متقدم أضف Config Module لكل مشروعك واستبدل الأرقام المتفرقة بـ Config.X.

الفصل الرابع والأربعون: تمارين شاملة

هذه تمارين تجمع مهارات متعددة. جرّبها بعد قراءة كل الكتاب. الحلول في الفصل القادم.

44.1 المستوى السهل 🟢

🟢 تمرين 1 اطبع أرقامًا من 1 إلى 10 باستخدام حلقة for.
🟢 تمرين 2 اكتب دالة تأخذ اسمًا وتُرجع تحية.
🟢 تمرين 3 أنشئ Part، ولونه أزرق، وثبّته في الهواء.
🟢 تمرين 4 اطبع الأرقام الزوجية من 1 إلى 20.
🟢 تمرين 5 أنشئ TextLabel كتب فيه "أهلاً" بداخل ScreenGui.

44.2 المستوى المتوسط 🟡

🟡 تمرين 1 اكتب دالة تأخذ جدول أرقام وتُرجع مجموعها ومتوسطها.
🟡 تمرين 2 أنشئ Tool، وعند استخدامه اطبع "استُخدم!".
🟡 تمرين 3 اجعل Part يتلاشى تدريجيًا حتى Transparency = 1 في ثانيتين.
🟡 تمرين 4 أنشئ RemoteEvent وأرسل نصًا من Client إلى Server واطبعه.
🟡 تمرين 5 أنشئ leaderstats فيه Coins، وزدها 10 عند الضغط على زر.

44.3 المستوى المتقدم 🔴

🔴 تمرين 1 أنشئ نظام Save/Load مع DataStore + pcall.
🔴 تمرين 2 أنشئ Class Player فيه Methods: addCoins, addXP, levelUp.
🔴 تمرين 3 أنشئ نظام Quest: عند إتمام شرط (مثل قتل 5 أعداء)، تُمنح مكافأة.
🔴 تمرين 4 أنشئ نظام Combat M1 كامل مع Combo و Cooldown و Server Validation.
🔴 تمرين 5 أنشئ نظام Anti-Cheat يكتشف Speed Hack وTeleport ويطرد بعد 3 تحذيرات.

44.4 تمارين المشاريع

🔴 مشروع 1 قم ببناء نظام Inventory كامل: عرض، إضافة، حذف، حفظ.
🔴 مشروع 2 قم ببناء نظام Shop كامل مع واجهة + Server Validation + DataStore.
🔴 مشروع 3 قم ببناء لعبة صغيرة تجمع Coin Collector + Shop + Round System.

الفصل الخامس والأربعون: حلول التمارين

هذه الحلول المقترحة. تذكر: هناك أكثر من طريقة صحيحة لكل تمرين.

45.1 حلول المستوى السهل 🟢

حل تمرين 1

for i = 1, 10 do
    print(i)
end

حل تمرين 2

local function greet(name)
    return "مرحبًا " .. name
end

print(greet("Ahmed"))

حل تمرين 3

local part = Instance.new("Part")
part.Color = Color3.fromRGB(0, 0, 255)
part.Anchored = true
part.Position = Vector3.new(0, 20, 0)
part.Parent = workspace

حل تمرين 4

for i = 1, 20 do
    if i % 2 == 0 then
        print(i)
    end
end

حل تمرين 5

local gui = Instance.new("ScreenGui")
gui.Parent = game.Players.LocalPlayer:WaitForChild("PlayerGui")

local label = Instance.new("TextLabel")
label.Size = UDim2.new(0, 200, 0, 50)
label.Position = UDim2.new(0, 20, 0, 20)
label.Text = "أهلاً"
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.Parent = gui

45.2 حلول المستوى المتوسط 🟡

حل تمرين 1

local function analyze(numbers)
    local sum = 0
    for _, n in ipairs(numbers) do
        sum += n
    end
    local avg = sum / #numbers
    return sum, avg
end

local s, a = analyze({10, 20, 30, 40})
print(s, a)  -- 100  25

حل تمرين 2

local tool = Instance.new("Tool")
tool.RequiresHandle = true

local handle = Instance.new("Part")
handle.Name = "Handle"
handle.Size = Vector3.new(1, 1, 2)
handle.Parent = tool

tool.Activated:Connect(function()
    print("استُخدم!")
end)

tool.Parent = game.Players.LocalPlayer.Backpack

حل تمرين 3

local part = workspace.Part
local TweenService = game:GetService("TweenService")

local tween = TweenService:Create(
    part,
    TweenInfo.new(2),
    {Transparency = 1}
)
tween:Play()

حل تمرين 4

-- Server
local remote = game.ReplicatedStorage.RemoteEvent
remote.OnServerEvent:Connect(function(player, text)
    print(player.Name .. ": " .. text)
end)

-- Client
game.ReplicatedStorage.RemoteEvent:FireServer("مرحبًا")

حل تمرين 5

-- Server
game.Players.PlayerAdded:Connect(function(player)
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = stats

    stats.Parent = player
end)

local event = game.ReplicatedStorage:WaitForChild("AddCoins")
event.OnServerEvent:Connect(function(player)
    player.leaderstats.Coins.Value += 10
end)

45.3 حلول المستوى المتقدم 🔴

حل تمرين 1

local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local store = DataStoreService:GetDataStore("Save_v1")

Players.PlayerAdded:Connect(function(player)
    local success, data = pcall(function()
        return store:GetAsync("p_" .. player.UserId)
    end)

    if not success or not data then
        data = {coins = 0}
    end

    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"
    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = data.coins
    coins.Parent = stats
    stats.Parent = player
end)

Players.PlayerRemoving:Connect(function(player)
    local stats = player:FindFirstChild("leaderstats")
    if not stats then return end

    local data = {coins = stats.Coins.Value}

    pcall(function()
        store:SetAsync("p_" .. player.UserId, data)
    end)
end)

حل تمرين 2

local Player = {}
Player.__index = Player

function Player.new(name)
    return setmetatable({
        name = name, coins = 0, xp = 0, level = 1
    }, Player)
end

function Player:addCoins(amount)
    self.coins += amount
end

function Player:addXP(amount)
    self.xp += amount
    while self.xp >= self.level * 100 do
        self.xp -= self.level * 100
        self.level += 1
    end
end

function Player:levelUp()
    self.level += 1
end

return Player

حل تمرين 3 (نظام Quest مبسط)

local Players = game:GetService("Players")

local quests = {}
local progress = {}

local QUEST = {name = "Kill 5 Enemies", target = 5, reward = 500}

Players.PlayerAdded:Connect(function(player)
    progress[player.UserId] = 0
    quests[player.UserId] = QUEST
end)

-- افترض أننا نزيد التقدم عند كل قتل
local function onKill(player)
    progress[player.UserId] += 1
    if progress[player.UserId] >= quests[player.UserId].target then
        player.leaderstats.Coins.Value += quests[player.UserId].reward
        print(player.Name .. " أكمل الـ Quest!")
    end
end

حل تمرين 4 — Combat M1

راجع الفصل 28 — النظام كامل هناك.

حل تمرين 5 — Anti-Cheat

local Players = game:GetService("Players")

local violations = {}
local MAX_SPEED = 25

local function check(player)
    local char = player.Character
    if not char then return end

    local hum = char:FindFirstChildOfClass("Humanoid")
    if not hum then return end

    if hum.WalkSpeed > MAX_SPEED then
        hum.WalkSpeed = MAX_SPEED
        violations[player] = (violations[player] or 0) + 1
        if violations[player] >= 3 then
            player:Kick("Speed hack")
        end
    end
end

task.spawn(function()
    while true do
        task.wait(3)
        for _, p in ipairs(Players:GetPlayers()) do
            check(p)
        end
    end
end)

45.4 حلول المشاريع

حل مشروع 1 — Inventory

local Players = game:GetService("Players")
local inv = {}

Players.PlayerAdded:Connect(function(player)
    inv[player.UserId] = {}
end)

Players.PlayerRemoving:Connect(function(player)
    inv[player.UserId] = nil
end)

local Inventory = {}

function Inventory.add(player, item)
    table.insert(inv[player.UserId], item)
end

function Inventory.remove(player, item)
    local i = table.find(inv[player.UserId], item)
    if i then table.remove(inv[player.UserId], i) end
end

function Inventory.has(player, item)
    return table.find(inv[player.UserId], item) ~= nil
end

function Inventory.get(player)
    return inv[player.UserId]
end

return Inventory

الحلول الكاملة للمشاريع 2 و3 موجودة في مشروع التخرج (الفصل 46) لأنها تشترك معه في نفس الأنظمة.

الفصل السادس والأربعون: مشروع التخرج الكامل

هذا هو المشروع الذي يجمع كل ما تعلمته: UI، Combat، Abilities، Coins، Shop، Inventory، DataStore، Leaderstats، RemoteEvents، Modules، Animations، Sounds، Server Validation، Anti-Exploit، Round System.

46.1 الفكرة

لعبة قتال جماعية: لاعبون يقتلون بعضهم للحصول على Coins، يشترون بها أسلحة وقدرات، والفائز بالأكثر قتلًا يفوز بالجولة.

46.2 البنية الكاملة

ReplicatedStorage
├── Remotes/
│   ├── M1Attack
│   ├── UseAbility
│   ├── BuyItem
│   └── EquipItem
├── Modules/
│   ├── Config
│   ├── Utilities
│   └── ItemDatabase
└── UI/
    ├── ShopGui
    ├── InventoryGui
    └── HUD

ServerScriptService
├── Modules/
│   ├── PlayerData
│   ├── Combat
│   ├── Inventory
│   ├── Shop
│   ├── RoundSystem
│   └── AntiCheat
└── Bootstrap.server.lua

StarterPlayerScripts
├── Controllers/
│   ├── CombatController
│   ├── HUDController
│   ├── ShopController
│   └── InventoryController
└── Bootstrap.client.lua

ServerStorage
└── Tools/

46.3 Config

-- ReplicatedStorage/Modules/Config
return {
    COMBAT = {
        M1_DAMAGE = 15,
        M1_RANGE = 8,
        M1_COOLDOWN = 0.4,
        MAX_COMBO = 4
    },
    ECONOMY = {
        START_COINS = 100,
        KILL_REWARD = 50,
        WIN_REWARD = 1000
    },
    ROUND = {
        TIME = 180,
        INTERMISSION = 15,
        MIN_PLAYERS = 2
    },
    SECURITY = {
        MAX_SPEED = 25,
        RATE_LIMIT = 5
    }
}

46.4 ItemDatabase

-- ReplicatedStorage/Modules/ItemDatabase
local Items = {
    Sword = {
        Name = "Sword",
        Price = 100,
        Damage = 25,
        Type = "Weapon"
    },
    Bow = {
        Name = "Bow",
        Price = 200,
        Damage = 30,
        Type = "Weapon"
    },
    Shield = {
        Name = "Shield",
        Price = 150,
        Type = "Armor"
    },
    Potion = {
        Name = "Potion",
        Price = 50,
        Type = "Consumable"
    }
}

return Items

46.5 PlayerData

-- ServerScriptService/Modules/PlayerData
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local store = DataStoreService:GetDataStore("AdvancedGame_v1")
local cache = {}

local PlayerData = {}

function PlayerData.start()
    Players.PlayerAdded:Connect(function(player)
        PlayerData.load(player)
    end)

    Players.PlayerRemoving:Connect(function(player)
        PlayerData.save(player)
        cache[player.UserId] = nil
    end)

    -- حفظ دوري
    task.spawn(function()
        while true do
            task.wait(60)
            for _, p in ipairs(Players:GetPlayers()) do
                task.spawn(PlayerData.save, p)
            end
        end
    end)
end

function PlayerData.load(player)
    local success, data = pcall(function()
        return store:GetAsync("p_" .. player.UserId)
    end)

    if not success or not data then
        data = {
            coins = 100,
            inventory = {},
            kills = 0,
            deaths = 0,
            wins = 0
        }
    end

    cache[player.UserId] = data

    -- leaderstats
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"

    for _, key in ipairs({"Coins", "Kills", "Wins"}) do
        local v = Instance.new("IntValue")
        v.Name = key
        v.Value = data[key:lower()] or 0
        v.Parent = stats
        v.Changed:Connect(function(newVal)
            if cache[player.UserId] then
                cache[player.UserId][key:lower()] = newVal
            end
        end)
    end

    stats.Parent = player
end

function PlayerData.save(player)
    local data = cache[player.UserId]
    if not data then return end

    pcall(function()
        store:SetAsync("p_" .. player.UserId, data)
    end)
end

function PlayerData.get(player)
    return cache[player.UserId]
end

return PlayerData

46.6 Inventory

-- ServerScriptService/Modules/Inventory
local PlayerData = require(script.Parent.PlayerData)

local Inventory = {}

function Inventory.start() end

function Inventory.add(player, itemName)
    local data = PlayerData.get(player)
    if not data then return end
    table.insert(data.inventory, itemName)
end

function Inventory.has(player, itemName)
    local data = PlayerData.get(player)
    if not data then return false end
    return table.find(data.inventory, itemName) ~= nil
end

function Inventory.remove(player, itemName)
    local data = PlayerData.get(player)
    if not data then return end
    local i = table.find(data.inventory, itemName)
    if i then table.remove(data.inventory, i) end
end

return Inventory

46.7 Shop

-- ServerScriptService/Modules/Shop
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Items = require(ReplicatedStorage.Modules.ItemDatabase)
local PlayerData = require(script.Parent.PlayerData)
local Inventory = require(script.Parent.Inventory)

local Shop = {}

function Shop.start()
    local buyEvent = ReplicatedStorage.Remotes.BuyItem

    buyEvent.OnServerEvent:Connect(function(player, itemName)
        if type(itemName) ~= "string" then return end

        local item = Items[itemName]
        if not item then return end

        local data = PlayerData.get(player)
        if not data then return end

        if data.coins < item.Price then
            return
        end

        data.coins -= item.Price
        player.leaderstats.Coins.Value = data.coins
        Inventory.add(player, itemName)

        print(player.Name .. " اشترى " .. itemName)
    end)
end

return Shop

46.8 Combat

-- ServerScriptService/Modules/Combat
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Config = require(ReplicatedStorage.Modules.Config)

local Combat = {}

local state = {}

local function getState(player)
    state[player] = state[player] or {lastHit = 0, combo = 0}
    return state[player]
end

local function angleBetween(a, b)
    local dot = math.clamp(a.Unit:Dot(b.Unit), -1, 1)
    return math.deg(math.acos(dot))
end

local function findTargets(player, character, range, angle)
    local root = character:FindFirstChild("HumanoidRootPart")
    if not root then return {} end

    local look = root.CFrame.LookVector
    local targets = {}

    for _, other in ipairs(Players:GetPlayers()) do
        if other == player then continue end
        local c = other.Character
        if not c then continue end

        local hum = c:FindFirstChildOfClass("Humanoid")
        if not hum or hum.Health <= 0 then continue end

        local hrp = c:FindFirstChild("HumanoidRootPart")
        if not hrp then continue end

        local diff = hrp.Position - root.Position
        if diff.Magnitude > range then continue end
        if angleBetween(look, diff) > angle / 2 then continue end

        table.insert(targets, hum)
    end
    return targets
end

function Combat.start()
    local m1 = ReplicatedStorage.Remotes.M1Attack

    m1.OnServerEvent:Connect(function(player)
        local s = getState(player)
        local now = tick()

        if now - s.lastHit < Config.COMBAT.M1_COOLDOWN then return end
        s.lastHit = now
        s.combo = (s.combo % Config.COMBAT.MAX_COMBO) + 1

        local char = player.Character
        if not char then return end

        local targets = findTargets(
            player, char,
            Config.COMBAT.M1_RANGE,
            90
        )

        for _, hum in ipairs(targets) do
            hum:TakeDamage(Config.COMBAT.M1_DAMAGE)
        end
    end)
end

return Combat

46.9 RoundSystem

-- ServerScriptService/Modules/RoundSystem
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Config = require(ReplicatedStorage.Modules.Config)

local RoundSystem = {}

local roundActive = false

local function countPlayers()
    return #Players:GetPlayers()
end

local function teleportAll()
    for _, p in ipairs(Players:GetPlayers()) do
        if p.Character then
            p.Character:PivotTo(workspace.SpawnZone.CFrame)
        end
    end
end

local function resetScores()
    for _, p in ipairs(Players:GetPlayers()) do
        local stats = p:FindFirstChild("leaderstats")
        if stats then stats.Kills.Value = 0 end
    end
end

local function announceWinner()
    local top, topKills = nil, 0
    for _, p in ipairs(Players:GetPlayers()) do
        local k = p.leaderstats.Kills.Value
        if k > topKills then
            top, topKills = p, k
        end
    end

    if top then
        top.leaderstats.Wins.Value += 1
        print("🏆 الفائز: " .. top.Name .. " (" .. topKills .. " قتلة)")
    end
end

local function startRound()
    roundActive = true
    resetScores()
    teleportAll()
    print("🎮 بدأت الجولة")

    task.wait(Config.ROUND.TIME)

    roundActive = false
    announceWinner()
end

local function intermission()
    print("⏸ استراحة")
    task.wait(Config.ROUND.INTERMISSION)
end

function RoundSystem.start()
    task.spawn(function()
        while true do
            if countPlayers() >= Config.ROUND.MIN_PLAYERS then
                intermission()
                startRound()
            else
                task.wait(5)
            end
        end
    end)
end

return RoundSystem

46.10 AntiCheat

-- ServerScriptService/Modules/AntiCheat
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Config = require(ReplicatedStorage.Modules.Config)

local AntiCheat = {}
local violations = {}

function AntiCheat.start()
    task.spawn(function()
        while true do
            task.wait(3)
            for _, p in ipairs(Players:GetPlayers()) do
                local char = p.Character
                if not char then continue end

                local hum = char:FindFirstChildOfClass("Humanoid")
                if not hum then continue end

                if hum.WalkSpeed > Config.SECURITY.MAX_SPEED then
                    hum.WalkSpeed = Config.SECURITY.MAX_SPEED
                    violations[p] = (violations[p] or 0) + 1

                    if violations[p] >= 3 then
                        p:Kick("🚫 Anti-Cheat: Speed")
                    end
                end
            end
        end
    end)

    Players.PlayerRemoving:Connect(function(p)
        violations[p] = nil
    end)
end

return AntiCheat

46.11 Bootstrap

-- ServerScriptService/Bootstrap.server.lua
local Modules = script.Parent.Modules

local PlayerData = require(Modules.PlayerData)
local Inventory  = require(Modules.Inventory)
local Shop       = require(Modules.Shop)
local Combat     = require(Modules.Combat)
local RoundSystem= require(Modules.RoundSystem)
local AntiCheat  = require(Modules.AntiCheat)

PlayerData.start()
Inventory.start()
Shop.start()
Combat.start()
RoundSystem.start()
AntiCheat.start()

print("✅ السيرفر جاهز")

46.12 إعداد Remotes

-- ServerScriptService/SetupRemotes.server.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = Instance.new("Folder")
remotes.Name = "Remotes"
remotes.Parent = ReplicatedStorage

for _, name in ipairs({"M1Attack", "UseAbility", "BuyItem", "EquipItem"}) do
    local ev = Instance.new("RemoteEvent")
    ev.Name = name
    ev.Parent = remotes
end

46.13 العميل — HUDController

-- StarterPlayerScripts/Controllers/HUDController
local Players = game:GetService("Players")
local player = Players.LocalPlayer

local gui = Instance.new("ScreenGui")
gui.Name = "HUD"
gui.ResetOnSpawn = false
gui.Parent = player:WaitForChild("PlayerGui")

-- عرض Coins
local coinsLabel = Instance.new("TextLabel")
coinsLabel.Size = UDim2.new(0, 200, 0, 40)
coinsLabel.Position = UDim2.new(0, 20, 0, 20)
coinsLabel.BackgroundColor3 = Color3.fromRGB(20, 20, 30)
coinsLabel.BackgroundTransparency = 0.3
coinsLabel.TextColor3 = Color3.fromRGB(255, 220, 80)
coinsLabel.Font = Enum.Font.GothamBold
coinsLabel.TextSize = 20
coinsLabel.Text = "💰 0"
coinsLabel.Parent = gui

Instance.new("UICorner", coinsLabel).CornerRadius = UDim.new(0, 8)

local function updateCoins()
    local stats = player:FindFirstChild("leaderstats")
    if stats and stats:FindFirstChild("Coins") then
        coinsLabel.Text = "💰 " .. stats.Coins.Value
    end
end

player.ChildAdded:Connect(function(child)
    if child.Name == "leaderstats" then
        task.wait(0.1)
        child:WaitForChild("Coins").Changed:Connect(updateCoins)
        updateCoins()
    end
end)

46.14 العميل — CombatController

-- StarterPlayerScripts/Controllers/CombatController
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")

local m1Event = ReplicatedStorage:WaitForChild("Remotes"):WaitForChild("M1Attack")
local cooldown = false

UserInputService.InputBegan:Connect(function(input, gp)
    if gp then return end
    if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end

    if cooldown then return end
    cooldown = true

    m1Event:FireServer()

    task.wait(0.4)
    cooldown = false
end)

46.15 التجربة

  1. انشر اللعبة أو شغّل Studio مع تفعيل API Services.
  2. أنشئ Baseplate و SpawnZone (Part كبير).
  3. ضع كل Modules في أماكنها.
  4. اضغط Play.
  5. سيرى كل لاعب leaderstats و Coins.
  6. عند ضغط الزر الأيسر، يهجم.
  7. عند قتل لاعب آخر، يحصل القاتل على Coins (إن ربطت الإضافة).
  8. بعد 180 ثانية، تظهر النتيجة ويُمنح الفائز Wins.

46.16 تحسينات مستقبلية

الفصل السابع والأربعون: قاموس المصطلحات

A

المصطلحالمعنىشرح
APIواجهة برمجة مجموعة الدوال التي تتيح لك استخدام خدمة (مثل DataStoreService).
Argumentوسيطة القيمة التي تمررها لدالة عند استدعائها.
AnchorPointنقطة ارتساء نقطة مرجعية لتموضع GUI (0-1).
Anchoredمثبّت خاصية تجعل الجزء ثابتًا لا يتحرك.

B

المصطلحالمعنىشرح
BasePartجزء أساسيالأصل لكل الأجزاء ثلاثية الأبعاد.
BindToCloseالربط بالإغلاقتنفيذ كود قبل إغلاق السيرفر.
Booleanمنطقيقيمة true أو false.

C

المصطلحالمعنىشرح
Callbackدالة استدعاءدالة تُمرَّر لدالة أخرى لتنفيذها لاحقًا.
CFrameإطار إحداثيموقع + اتجاه ثلاثي الأبعاد.
Clientعميلجهاز اللاعب. غير موثوق أمنيًا.
Closureإغلاقدالة تحتفظ بمتغيرات محيطها.
Color3لون RGBلون بثلاث قيم (أحمر، أخضر، أزرق).
Connectionارتباطكائن يُعاد من Connect() ويمكن فصله.
Cooldownفترة انتظارمنع تكرار الفعل خلال فترة زمنية.
Coroutineروتين تعاونيدالة يمكن إيقافها واستئنافها.

D

المصطلحالمعنىشرح
DataStoreمخزن البياناتخدمة حفظ البيانات دائمًا على Roblox.
Debounceمانع تكرارتقنية لمنع تكرار الأحداث السريعة.
Debuggتصحيح الأخطاءعملية إيجاد وحل الأخطاء.
Dictionaryقاموسجدول بمفاتيح نصية.
Destroyتدميرحذف Instance وتحرير ذاكرته.

E

المصطلحالمعنىشرح
Eventحدثإشعار يقع عند تغيير أو تفاعل.
Explorerالمستكشفنافذة تعرض كل عناصر المشروع.

F

المصطلحالمعنىشرح
FireServerإطلاق للسيرفرإرسال RemoteEvent من العميل للسيرفر.
Functionدالةكتلة كود لها اسم وتنفذ عند الاستدعاء.

G-H

المصطلحالمعنىشرح
GUIواجهة رسوميةكل ما يراه اللاعب على الشاشة.
Heartbeatنبضحدث RunService يُستدعى كل إطار.
Humanoidكائن الشخصيةيمثل صحة وسلوك الشخصية.
Hitboxصندوق إصابةمنطقة تُحتسب فيها الإصابة.

I-L

المصطلحالمعنىشرح
Instanceعنصركل شيء في Roblox (Part، Script، GUI…).
Leaderstatsلوحة الإحصاءمجلد يعرض إحصائيات اللاعب.
LocalScriptسكربت محليسكربت يعمل في جهاز اللاعب.
Luauلغة روبلوكسنسخة مطورة من Lua.

M-O

المصطلحالمعنىشرح
Metatableجدول أستاذيتحكم بسلوك جدول آخر.
ModuleScriptوحدة برمجيةسكربت يُستخدم عبر require.
nilلا شيءقيمة تعني انعدام الوجود.
OOPالبرمجة الكائنيةنمط برمجة يعتمد على الكائنات.
OnServerEventحدث السيرفريستقبل RemoteEvents في السيرفر.

P-R

المصطلحالمعنىشرح
Parentالأبالخاصية التي تحدد مكان Instance.
pcallاستدعاء محمييلتقط الأخطاء بدون إيقاف الكود.
Playerلاعبكائن اللاعب المنطقي.
Propertyخاصيةصفة يمكن قراءتها/تعديلها.
Raycastشعاعإطلاق شعاع لكشف الاصطدامات.
RemoteEventحدث بعيديتواصل بين Client و Server.
RemoteFunctionدالة بعيدةيعيد قيمة بعد سؤال.
ReplicatedStorageتخزين مشتركمكان يظهر للسيرفر والعميل.
RunServiceخدمة التشغيلتوفر Heartbeat و RenderStepped.

S

المصطلحالمعنىشرح
Scopeنطاقالمنطقة التي يظهر فيها المتغير.
Serverسيرفرالمصدر المركزي للحقيقة.
ServerScriptServiceخدمة سكربتات السيرفرمكان سكربتات السيرفر.
ServerStorageتخزين السيرفرمخزن لا يراه اللاعب.
StarterGuiواجهة البدايةواجهات تُنسخ لكل لاعب.
StarterPackحقيبة البدايةأدوات تبدأ مع اللاعب.
StarterPlayerإعدادات اللاعبسكربتات اللاعب الافتراضية.
StreamingEnabledالبث المفعّليحمّل الأجزاء القريبة فقط.

T-Z

المصطلحالمعنىشرح
Tableجدولبنية بيانات أساسية في Luau.
task.spawnتشغيل متوازيتشغيل دالة في الخلفية.
Toolأداةشيء يحمله اللاعب.
Tweenحركة سلسةتحريك خاصية تدريجيًا.
UDim2قياس UIموقع/حجم عناصر الواجهة.
Vector3متجه ثلاثيثلاثة أرقام (X, Y, Z).
WaitForChildانتظر الابنينتظر وصول Instance معين.
Workspaceفضاء العملالعالم ثلاثي الأبعاد.

الفصل الثامن والأربعون: الخاتمة

مبروك! 🎉 لقد أكملت رحلة تعلّم Luau و Roblox Studio من الصفر إلى الاحتراف. هذا الكتاب احتوى 48 فصلًا غطّت:

48.1 خطواتك التالية

  1. ابنِ لعبة صغيرة كاملة بنفسك. لا تنسخ. الهدف ليس الجمال، الهدف أن تخطئ وتتعلم.
  2. اقرأ كود مطورين آخرين من نماذج مفتوحة المصدر أو DevForum. ستتعلم أنماطًا جديدة.
  3. شارك في Roblox DevForum. المجتمع متعاون، ويمكن أن تجد حلولًا لمشاكل واجهتها.
  4. تابع تحديثات Roblox. المنصة تتطور، وهناك APIs جديدة بشكل مستمر.
  5. تخصص. اختر مجالك: Combat، UI، AI، أدوات، أنظمة بيانات… وكن الأفضل فيه.

48.2 مصادر للاستمرار

48.3 نصائح أخيرة

48.4 كلمة أخيرة

البرمجة ليست موهبة، بل مهارة تُبنى بالممارسة. كل مطور محترف بدأ من صفر مثلك. الفرق الوحيد هو أنه استمر.

ابدأ اليوم بمشروع صغير. يومًا بعد يوم، ستُفاجأ بما وصلت إليه. Roblox تحتاج مطورين عربًا موهوبين — وأنت الآن واحد منهم. 🚀