كتاب عربي متكامل لتعلم برمجة ألعاب Roblox من الأساسيات تمامًا حتى بناء أنظمة احترافية كاملة: القتال، الواجهات، حفظ البيانات، الحماية من المخترقين، وتنظيم المشاريع.
Roblox هي منصة ألعاب ضخمة تتيح للمستخدمين إنشاء ألعابهم الخاصة ونشرها للعالم كله. الفرق الأساسي بين Roblox وأي منصة أخرى هو أن اللاعبين أنفسهم هم من يصنعون الألعاب، وليس شركة واحدة. الملايين من اللاعبين يدخلون يوميًا لتجربة ألعاب صنعها أشخاص مثلهم — من عمر 10 سنوات إلى محترفين يعملون بدوام كامل.
داخل Roblox، لا تشتري "لعبة واحدة" بل تدخل إلى تجارب (Experiences) متنوعة: ألعاب قتال، ألعاب مغامرات، محاكاة، ألعاب سباق، ألعاب بناء، وأكثر من ذلك بكثير. كل هذه الألعاب صُنعت بأداة واحدة اسمها Roblox Studio.
Roblox Studio هو البرنامج المجاني الذي تستخدمه لإنشاء ألعاب Roblox. هو بيئة تطوير كاملة (IDE) تحتوي على:
Luau (تُلفظ "لو-آو") هي لغة البرمجة المستخدمة داخل Roblox Studio. هي نسخة مطوّرة ومحسّنة من لغة Lua الشهيرة، وقد طوّرتها شركة Roblox خصيصًا لتناسب احتياجات الألعاب الحديثة.
Luau هي اللغة التي ستكتب بها كل شيء: حركة اللاعب، الأسلحة، الواجهات، حفظ البيانات، الأنظمة، المنطق، وكل ما يجعل اللعبة "حية".
كثيرًا ما يخلط المبتدئون بين الاثنين. الفرق باختصار:
| الميزة | Lua | Luau |
|---|---|---|
| المطوّر | فريق Lua العالمي | شركة Roblox |
| الاستخدام الأساسي | برامج عامة وألعاب | Roblox فقط |
| السرعة | جيدة | أسرع بكثير (JIT + تحسينات) |
| نظام الأنواع | لا يوجد | يوجد (Type Checking) |
| ميزات حديثة | محدودة | continue، +=، وغيرها |
| التوافق | مع أي تطبيق Lua | مع Roblox Studio فقط |
عندما يشغّل اللاعب لعبتك، يحدث التالي:
هذا المفهوم Server ↔ Client هو أهم مفهوم في برمجة Roblox، وسنخصص له فصلاً كاملاً لاحقًا. لكن الآن تذكّر فقط:
حمّل Roblox Studio من الموقع الرسمي لـ Roblox، ثم افتحه وسجّل الدخول
بحساب Roblox. بعد الدخول، اضغط على New لإنشاء مشروع جديد.
ستظهر لك خريطة افتراضية فيها منصة (Baseplate).
الواجهة مقسّمة إلى مناطق رئيسية. تَعرّف عليها:
يعرض كل عناصر المشروع على شكل شجرة. من هنا تستطيع رؤية كل شيء داخل اللعبة: اللاعبين، الإضاءة، الأصوات، السكربتات، المجلدات… كل شيء.
عند تحديد أي عنصر في Explorer، تظهر خصائصه في Properties. مثلاً إذا حددت Part، ستظهر خصائصه مثل الحجم واللون والموقع والشفافية.
نافذة تعرض الرسائل والأخطاء. هي أهم نافذة عند تصحيح أخطائك — سنعود إليها كثيرًا في فصل Debugging.
يحتوي على أدوات التحكم في المشهد: Select، Move، Scale، Rotate، وأدوات أخرى. جرّبها على منصة البداية Baseplate.
هي النافذة الكبيرة في الوسط حيث ترى العالم ثلاثي الأبعاد وتتحرك فيه.
Play لتشغيل اللعبة في وضع الاختبار.
يمكنك التبديل بين "Server" و "Client" في نافذة صغيرة أعلى الشاشة لرؤية
ما يحدث من منظور كل جهة.
عند فتح مشروع جديد سترى قائمة بأهم الخدمات (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 | الأرض والتضاريس | الجبال، المياه، الأرضيات الطبيعية |
لنقم بتجربة سريعة. اتبع هذه الخطوات:
ServerScriptService واضغط +.Script.print("مرحبًا بك في Roblox Studio!")
Play أعلى الشاشة.Output وسترى الرسالة.print هي أول دالة تتعلمها. وظيفتها طباعة أي شيء
في نافذة Output. سنستخدمها كثيرًا لاحقًا للتصحيح والتجربة.
game.Name، ثم افتح Output وتحقق من النتيجة.
في Roblox Studio، لديك ثلاثة أنواع رئيسية من السكربتات. الفرق بينها ليس شكليًا — بل يحدد أين يعمل الكود ومن يستطيع رؤيته:
| النوع | المكان | ماذا يفعل |
|---|---|---|
| Script | السيرفر | يدير منطق اللعبة، يحفظ البيانات، يحمي من المخترقين |
| LocalScript | الـ Client | يتعامل مع الواجهات، المدخلات، التأثيرات البصرية |
| ModuleScript | أي مكان | مكتبة كود قابلة لإعادة الاستخدام (لا يعمل وحده) |
| النوع | الأماكن المسموحة |
|---|---|
| Script | ServerScriptService، Workspace، ServerStorage |
| LocalScript | StarterPlayerScripts، StarterCharacterScripts، StarterGui، StarterPack |
| ModuleScript | ReplicatedStorage، ServerScriptService، ServerStorage، أي مكان |
هو الجهاز المركزي الذي يشغّل نسخة واحدة من اللعبة. كل اللاعبين المتصلين يرون نفس السيرفر. السيرفر هو المصدر الوحيد للحقيقة في اللعبة.
ما يستطيع Server فعله:
هو جهاز كل لاعب على حدة. كل لاعب له Client خاص به. الـ Client مسؤول عن العرض والتفاعل السريع، لكنه غير موثوق أمنيًا.
ما يستطيع Client فعله:
ReplicatedStorage مكان خاص: كل ما يوضع فيه
يظهر لكل من السيرفر والـ Client. لهذا نضع فيه:
ReplicatedStorage
├── Remotes
│ ├── DamageEvent
│ └── PurchaseEvent
├── Modules
│ ├── Utilities
│ └── Config
└── Assets
├── Sounds
└── Animations
لنرَ مثالًا بسيطًا يوضح الفرق.
-- 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)
-- 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.
OnClientEvent
ويطبع الرسالة. لاحظ أن الـ Client لا يستطيع قراءة الرسالة قبل وصولها،
ولهذا نستخدم WaitForChild.
TestEvent في ReplicatedStorage،
وأرسل من السيرفر رسالة للاعب عند دخوله.
لنكتب كودًا بسيطًا يطبع رسائل متعددة:
print("أول رسالة")
print("ثاني رسالة")
print(2 + 3)
print("النتيجة:", 2 + 3)
عندما تشغّل هذا في Script داخل ServerScriptService، ستظهر في Output:
أول رسالة
ثاني رسالة
5
النتيجة: 5
print() تأخذ أي عدد من القيم وتطبعها مفصولة بمسافة.
مفيدة جدًا لمعرفة ما يحدث داخل الكود لحظة بلحظة.
التعليقات سطور لا ينفذها Luau. تُستخدم لشرح الكود لنفسك أو لغيرك:
-- تعليق من سطر واحد
--[[
تعليق
متعدد
الأسطر
]]
print("هذا الكود يعمل") -- تعليق بجانب السطر
المتغير هو صندوق يحمل قيمة، ويمكن استخدامه لاحقًا:
local name = "Ahmed"
local age = 15
local isStudent = true
print(name, age, isStudent)
الكلمة local تعني: أنشئ متغيرًا محليًا (Local Variable).
المحلي هو المتغير الذي يعيش داخل نطاق معين فقط (Scope).
في Luau، يمكنك أيضًا إنشاء متغير بدون local،
فيصبح Global (عام):
myGlobalVar = 100 -- Global (غير محبب)
local myLocalVar = 50 -- Local (الأفضل)
| النوع | المدى | الأداء | التوصية |
|---|---|---|---|
| Local | داخل النطاق الحالي | أسرع | ✅ استخدمه دائمًا |
| Global | في كل المشروع | أبطأ | ❌ تجنّبه |
local دائمًا إلا لو كان
لديك سبب خاص جدًا. Global يُبطئ الكود ويسبب مشاكل لا يمكن تتبعها.
local score = 0
print(score) -- 0
score = 10
print(score) -- 10
score = score + 5
print(score) -- 15
اتفق مطورو Roblox على أساليب تسمية واضحة:
| النوع | الأسلوب | مثال |
|---|---|---|
| متغير عادي | camelCase | playerScore |
| ثابت | UPPER_SNAKE | MAX_HEALTH |
| دالة | camelCase | getPlayerData |
| Module | PascalCase | PlayerDataModule |
playerHealth أفضل بكثير من
ph أو x1.
ستشكر نفسك بعد أسبوعين عندما تعود لكودك.
coins قيمته 100، ثم أضف إليه 50،
ثم اطرح 30، واطبع النتيجة النهائية.
كل قيمة في Luau لها نوع. النوع يحدد ما يمكنك فعله بهذه القيمة. مثلاً لا يمكنك جمع نص مع رقم — لأن النوعين مختلفان.
| النوع | الوصف | مثال |
|---|---|---|
| string | نص | "hello" |
| number | رقم | 42, 3.14 |
| boolean | صحيح/خطأ | true / false |
| nil | لا قيمة | nil |
| table | جدول | {1, 2, 3} |
| function | دالة | function() end |
| Instance | عنصر Roblox | workspace.Part |
| Vector3 | إحداثيات 3D | Vector3.new(0, 5, 0) |
| CFrame | موقع + اتجاه | CFrame.new(0, 5, 0) |
| Color3 | لون | Color3.fromRGB(255, 0, 0) |
| UDim2 | قياس UI | UDim2.new(0.5, 0, 0.5, 0) |
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)
local integer = 10
local decimal = 3.14
local negative = -5
local big = 1e6 -- 1000000
print(integer + decimal) -- 13.14
local isAlive = true
local isDead = false
if isAlive then
print("اللاعب على قيد الحياة")
end
local x = nil
print(x) -- nil
local t = {}
print(t.notExist) -- nil
nil هو سبب أشهر خطأ في Roblox:
attempt to index nil.
يحدث عندما تحاول الوصول إلى خاصية شيء غير موجود.
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
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)
local red = Color3.fromRGB(255, 0, 0)
local green = Color3.fromRGB(0, 255, 0)
local fromHex = Color3.fromHex("#3498db")
local size = UDim2.new(0, 200, 0, 50)
-- X.scale X.offset Y.scale Y.offset
local part = workspace:FindFirstChild("Part")
if part then
print(part.Name)
print(part.ClassName)
end
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 + 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
| الرمز | المعنى |
|---|---|
| == | يساوي |
| ~= | لا يساوي |
| > | أكبر من |
| < | أصغر من |
| >= | أكبر أو يساوي |
| <= | أصغر أو يساوي |
print(5 == 5) -- true
print(5 ~= 5) -- false
print(10 > 5) -- true
print(10 < 5) -- false
print(10 >= 10) -- true
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
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
الترتيب من الأعلى إلى الأدنى:
print(2 + 3 * 4) -- 14 وليس 20
print((2 + 3) * 4) -- 20
print(2 ^ 3 ^ 2) -- 512 (تُقيّم من اليمين)
%.
if condition then
-- ينفذ فقط إذا كانت الشرط صحيحة
end
مثال:
local age = 18
if age >= 18 then
print("أنت راشد")
end
local age = 15
if age >= 18 then
print("أنت راشد")
else
print("أنت قاصر")
end
local score = 75
if score >= 90 then
print("ممتاز")
elseif score >= 75 then
print("جيد جدًا")
elseif score >= 50 then
print("جيد")
else
print("راسب")
end
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
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
local part = workspace:FindFirstChild("MyPart")
if part then
print("الجزء موجود: " .. part.Name)
else
print("الجزء غير موجود")
end
if part then قبل استخدام
part. هذا يمنع خطأ attempt to index nil.
local age = 20
local status = age >= 18 and "راشد" or "قاصر"
print(status) -- راشد
تنفذ الكود طالما الشرط صحيح:
local counter = 0
while counter < 5 do
print("العدد: " .. counter)
counter += 1
end
counter ستدخل في
حلقة لا نهائية وقد تتوقف اللعبة. تأكد دائمًا أن الشرط
سيتحول إلى false في النهاية.
تشبه while لكن تنفذ مرة واحدة على الأقل:
local counter = 0
repeat
print("مرة: " .. counter)
counter += 1
until counter >= 5
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
local fruits = {"تفاح", "موز", "عنب"}
for index, fruit in ipairs(fruits) do
print(index, fruit)
end
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 | لكل الجداول | غير مضمون |
for i = 1, 10 do
if i == 5 then
break
end
print(i) -- 1, 2, 3, 4
end
for i = 1, 10 do
if i % 2 == 0 then
continue -- تخطي الأرقام الزوجية
end
print(i) -- 1, 3, 5, 7, 9
end
continue متوفرة في Luau وليست في Lua الأصلية.
| الحالة | الحلقة المناسبة |
|---|---|
| عدد معروف من التكرارات | for رقمي |
| تكرار حتى شرط | while أو repeat |
| المرور على عناصر جدول | ipairs |
| المرور على خصائص قاموس | pairs |
الدالة (Function) هي كتلة كود لها اسم، تُنفَّذ عند استدعائها. هي أساس تنظيم الكود في Luau: بدلًا من كتابة نفس السطور في عشرين مكانًا، تكتبها مرة واحدة داخل دالة وتستدعيها عشرين مرة.
local function greet()
print("مرحبًا بك!")
end
greet() -- استدعاء الدالة
لاحظ: local function أفضل من function
وحدها، لأنها تجعل الدالة محلية (أسرع وأكثر أمانًا).
Parameter = متغير داخل تعريف الدالة.
Argument = القيمة الفعلية عند الاستدعاء.
local function greet(name)
print("مرحبًا " .. name)
end
greet("Ahmed") -- Ahmed
greet("Sara") -- Sara
greet("Ali") -- Ali
local function introduce(name, age, city)
print(name .. " عمره " .. age .. " يسكن في " .. city)
end
introduce("Ahmed", 15, "القاهرة")
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)) -- قاصر
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
هذا أساس البرمجة الكائنية (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).
local greet = function(name)
print("أهلاً " .. name)
end
greet("Sara")
تُستخدم كثيرًا مع Events:
workspace.Part.Touched:Connect(function(hit)
print(hit.Name .. " لمس الجزء")
end)
الـ Callback هي دالة تمرَّر لدالة أخرى لتنفذها لاحقًا. مثل ما في المثال السابق: الدالة المجهولة هي callback.
local function performAction(action)
print("قبل التنفيذ")
action()
print("بعد التنفيذ")
end
performAction(function()
print("أنا داخل الـ callback")
end)
النطاق يحدد من يمكنه رؤية المتغير:
local x = 10
local function test()
local y = 20
print(x + y) -- 30 (يستطيع رؤية x)
end
test()
print(y) -- خطأ! y محلية داخل test
nil أو خطأ.
احذر من إعلان متغير داخل if ثم استخدامه خارجه.
الـ 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 (نفس الثانية)
multiply(a, b) تُرجع حاصل الضرب، واختبرها.
makeMultiplier(n) تُرجع دالة تضرب أي رقم في n.
الجدول (Table) هو أهم بنية بيانات في Luau. هو في الحقيقة الشيء الوحيد في اللغة — كل شيء آخر (Objects، Modules، Classes) مبني عليه.
local empty = {}
local numbers = {1, 2, 3, 4, 5}
local mixed = {1, "two", true, 3.14}
local player = {
name = "Ahmed",
level = 10,
coins = 500
}
جدول مفاتيحه أرقام متسلسلة تبدأ من 1:
local fruits = {"تفاح", "موز", "عنب"}
print(fruits[1]) -- تفاح
print(fruits[2]) -- موز
print(fruits[3]) -- عنب
print(#fruits) -- 3
جدول مفاتيحه نصوص:
local player = {
name = "Sara",
level = 25,
coins = 1200
}
print(player.name) -- Sara
print(player["level"]) -- 25
local t = {a = 1, b = 2}
print(t.a) -- 1 (نقطة)
print(t["a"]) -- 1 (أقواس)
local key = "b"
print(t[key]) -- 2 (لازم أقواس للمتغيرات)
local t = {}
t.x = 10 -- إضافة
t["y"] = 20 -- إضافة
t.x = 30 -- تعديل
t.x = nil -- حذف
print(t.x) -- nil
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
local list = {"a", "b"}
table.insert(list, "c") -- في النهاية
table.insert(list, 1, "start") -- في البداية
-- النتيجة: {"start", "a", "b", "c"}
local list = {"a", "b", "c", "d"}
table.remove(list, 2) -- يحذف "b"
print(#list) -- 3
table.remove(list) -- يحذف الأخير
local list = {"سيف", "درع", "جرعة"}
local index = table.find(list, "درع")
print(index) -- 2
if table.find(list, "سيف") then
print("لديك سيف")
end
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
local list = {10, 20, 30}
print(#list) -- 3
# يعمل بشكل صحيح فقط مع الجداول المتتابعة (بدون فراغات).
لا تستخدمه على القواميس.
for i, v in ipairs({"a", "b", "c"}) do
print(i, v)
end
local player = {name = "Ahmed", level = 5}
for k, v in pairs(player) do
print(k, v)
end
مهم جدًا: عند إسناد جدول لمتغير آخر، لا يُنسخ — بل تشير الاثنان إلى نفس الجدول.
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 (لم يتأثر)
local deep = game:GetService("HttpService"):JSONDecode(game:GetService("HttpService"):JSONEncode(t))
مثال — بيانات لاعب:
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
التعامل مع النصوص جزء أساسي من بناء الواجهات والرسائل وأسماء اللاعبين.
Luau توفّر مكتبة string قوية جدًا.
local first = "Ahmed"
local last = "Ali"
print(first .. " " .. last) -- Ahmed Ali
local coins = 500
print("رصيدك: " .. coins .. " عملة")
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 | رقم سادس عشري |
| %% | علامة % نفسها |
local s = "Hello World"
print(string.upper(s)) -- HELLO WORLD
print(string.lower(s)) -- hello world
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
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
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
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
| النمط | المعنى |
|---|---|
| . | أي حرف |
| %a | حرف |
| %d | رقم |
| %s | مسافة |
| %w | حرف أو رقم |
| + | واحد أو أكثر |
| * | صفر أو أكثر |
| ? | اختياري |
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"}
كل شيء في Roblox هو Instance: Part، Model، Folder، Script، Player، GUI، Light، Sound… كل عنصر في Explorer هو Instance.
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.
الخاصية Parent تحدد أين يوجد الـ Instance. لو لم تضع Parent، سيبقى في الذاكرة لكن لن يظهر في اللعبة.
part.Parent = workspace
part.Parent = workspace.Folder
part.Parent = game.ReplicatedStorage
local p = Instance.new("Part")
p.Name = "RedPart"
print(p.Name) -- RedPart
print(p.ClassName) -- Part
local part = workspace:FindFirstChild("MyPart")
if part then
print("موجود: " .. part.Name)
end
local remote = game.ReplicatedStorage:WaitForChild("MyRemote", 5)
if remote then
print("وصل الـ Remote")
end
FindFirstChild → للبحث الفوري، ترجع nil إذا لم تجد.WaitForChild → للانتظار حتى يصل العنصر (مفيد في LocalScripts).
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
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" فدقيقة جدًا وقد تفشل.
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
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
part:Destroy()
part.Parent = nil،
لأنها تُبقي الـ Instance في الذاكرة وتسبب Memory Leak. استخدم دائمًا
:Destroy().
folder:ClearAllChildren() -- يحذف كل الأبناء
local part = Instance.new("Part")
part.Anchored = true
part.CanCollide = false
part.Material = Enum.Material.Neon
local model = Instance.new("Model")
model.Name = "MyModel"
model.PrimaryPart = somePart
model.Parent = workspace
local folder = Instance.new("Folder")
folder.Name = "Items"
folder.Parent = workspace
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
local gui = Instance.new("ScreenGui")
gui.Name = "HUD"
gui.ResetOnSpawn = false
gui.Parent = player.PlayerGui
كل Instance له خصائص (Properties) يمكن قراءتها وتعديلها برمجيًا. في هذا الفصل نشرح أهم الخصائص وكيفية استخدامها.
local part = workspace.Part
print(part.Name) -- قراءة
print(part.Position)
part.Transparency = 0.5 -- تعديل
part.CanCollide = false
| الخاصية | الوظيفة |
|---|---|
| Position | الموقع (Vector3) |
| CFrame | الموقع + الاتجاه |
| Size | الحجم (Vector3) |
| Color | اللون (Color3) |
| Material | المادة (Neon, Glass, Wood…) |
| Transparency | الشفافية (0 شفاف تمامًا؟ لا، 1 شفاف، 0 معتم) |
| CanCollide | هل يتصادم مع الأجسام؟ |
| Anchored | هل يبقى ثابتًا في مكانه؟ |
| Massless | هل يتجاهل الفيزياء؟ |
| Reflectance | الانعكاس (0 إلى 1) |
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 عن الأرض
-- تغيير الشفافية تدريجيًا
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()
| الخاصية | الوظيفة |
|---|---|
| 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
| الخاصية | الوظيفة |
|---|---|
| Position | UDim2 |
| Size | UDim2 |
| 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
-- احصل على جميع الخصائص لشيء
for _, prop in ipairs(part:GetPropertyChangedSignal and {} or {}) do end
-- الطريقة الفعلية: استخدم GetProperties من Plugin
-- أو استخدم الاستكشاف اليدوي من Properties window
الحدث (Event) هو إشعار يصدره Roblox عندما يحدث شيء معيّن: لاعب لمس جزءًا، ضغط زرًا، تغيّر دمه، دخل اللعبة… بدلًا من أن تسأل باستمرار "هل حدث شيء؟"، تربط دالة (Callback) بالحدث، فيستدعيها Roblox تلقائيًا عند حدوثه.
Event:Connect(function(...)
-- الكود الذي ينفذ عند وقوع الحدث
end)
:Connect() تربط دالة بالحدث وترجع كائنًا يسمى Connection. هذا الكائن يمكن استخدامه لاحقًا لفصل الارتباط.
local connection = workspace.Part.Touched:Connect(function(hit)
print(hit.Name .. " لمس الجزء")
end)
-- لاحقًا، عند الحاجة:
connection:Disconnect()
الـ Callback هي الدالة التي تمرّرها لـ Connect. لا تُنفَّذ فورًا، بل يخزّنها Roblox، وعندما يقع الحدث يستدعيها ويمرّر لها القيم المناسبة.
local function onTouched(hit)
print("لمسني: " .. hit.Name)
end
workspace.Part.Touched:Connect(onTouched)
| الحدث | متى يُستدعى؟ |
|---|---|
| Touched | عند ملامسة أي جسم للجزء |
| TouchEnded | عند انتهاء الملامسة |
| Changed | عند تغيّر خاصية معينة |
| ChildAdded | عند إضافة ابن للجزء |
| ChildRemoved | عند إزالة ابن |
| AncestryChanged | عند تغيّر الـ Parent |
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 لاحقًا لأنها أدق.
-- Changed (أقدم — ينطلق لأي تغيير)
part.Changed:Connect(function(property)
print("تغيّرت خاصية: " .. property)
end)
-- GetPropertyChangedSignal (أفضل — لخاصية واحدة)
part:GetPropertyChangedSignal("Transparency"):Connect(function()
print("تغيّرت الشفافية إلى: " .. part.Transparency)
end)
GetPropertyChangedSignal دائمًا. لأن
Changed تُنفَّذ عند كل تغيير صغير وتبطئ اللعبة.
local folder = workspace.Items
folder.ChildAdded:Connect(function(child)
print("أُضيف: " .. child.Name)
end)
folder.ChildRemoved:Connect(function(child)
print("أُزيل: " .. child.Name)
end)
| الحدث | متى؟ |
|---|---|
| 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)
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)
local tool = script.Parent
tool.Activated:Connect(function()
print("تم استخدام الأداة")
end)
tool.Equipped:Connect(function()
print("تم تجهيز الأداة")
end)
tool.Unequipped:Connect(function()
print("تم إلغاء تجهيز الأداة")
end)
كل اتصال (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()
فهم الفرق بين Player و Character حجر أساس لكل نظام لعبة. الخطأ فيهما سبب أغلب مشاكل المبتدئين.
| المفهوم | ما هو؟ | يوجد فيه |
|---|---|---|
| Player | كائن اللاعب المنطقي | UserId، Name، Backpack، PlayerGui، leaderstats |
| Character | الجسم الفعلي في العالم | Humanoid، Head، HumanoidRootPart، Limbs |
local Players = game:GetService("Players")
-- الوصول للاعبين
print(Players:GetPlayers()) -- قائمة كل اللاعبين
print(Players:GetPlayerFromCharacter(character))
print(Players:GetPlayerByUserId(123456))
الخاصية LocalPlayer موجودة فقط في LocalScript.
تمثل اللاعب الحالي صاحب الجهاز.
-- LocalScript فقط
local Players = game:GetService("Players")
local player = Players.LocalPlayer
print("أنا " .. player.Name)
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")
| الجزء | الوظيفة |
|---|---|
| HumanoidRootPart | مركز الجسم — يُستخدم للتحريك والمواقع |
| Head | الرأس — يقبل Accessories |
| UpperTorso / Torso | الجذع العلوي |
| LowerTorso / Torso | الجذع السفلي |
| LeftHand / RightHand | الأيدي (R15 فقط) |
| LeftFoot / RightFoot | الأقدام (R15 فقط) |
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)
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)
player.Character بعد
ربط الحدث.
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()
-- إعطاء اللاعب أداة
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.
هذا هو الفصل الأهم في الكتاب. لو فهمت Client و Server جيدًا، فأنت تجاوزت 80% من العقبات التي يواجهها المبتدئون.
| Server | Client | |
|---|---|---|
| العدد | واحد لكل سيرفر | واحد لكل لاعب |
| يرى | كل اللاعبين | نفسه فقط (بشكل أساسي) |
| الصلاحيات | كل شيء | محدودة، غير موثوقة |
| الأداء | مشترك (يؤثر على الجميع) | محلي (لا يؤثر على الآخرين) |
| الثقة | مطلقة | صفر |
لأن أي شخص يمكنه تشغيل برامج مثل Synapse أو Script-Ware تعديل أي شيء في Client-side. يمكنه:
-- 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)
-- 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.
اسأل نفسك دائمًا: أين يجب أن يعمل هذا الكود؟
| الوظيفة | المكان المناسب |
|---|---|
| فتح GUI عند الضغط على زر | Client |
| منح اللاعب 100 عملة | Server |
| تشغيل أنيميشن هجوم | Client (+ Server للتحقق) |
| حساب الضرر على العدو | Server |
| إظهار رسالة "لقد ربحت!" | Client |
| حفظ تقدم اللاعب | Server |
-- LocalScript
game.Players.LocalPlayer.leaderstats.Level.Value += 1
RemoteEvent هو الجسر الذي يُمرّر الرسائل بين Client و Server. هو الأداة الأساسية في كل لعبة Roblox متعددة اللاعبين.
الطريقة اليدوية:
+ واختر RemoteEvent.DamageEvent.الطريقة البرمجية (في السيرفر):
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local event = Instance.new("RemoteEvent")
event.Name = "DamageEvent"
event.Parent = ReplicatedStorage
Remotes داخل
ReplicatedStorage. هذا يجعل المشروع منظمًا.
| الدالة | تُستدعى من | الاستخدام |
|---|---|---|
| FireServer() | Client | إرسال للسيرفر |
| OnServerEvent | Server | استقبال من عميل |
| FireClient(player) | Server | إرسال لعميل واحد |
| FireAllClients() | Server | إرسال لكل العملاء |
| OnClientEvent | Client | استقبال من السيرفر |
-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local attackEvent = ReplicatedStorage:WaitForChild("AttackEvent")
attackEvent:FireServer("sword", 50)
-- Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local attackEvent = ReplicatedStorage:WaitForChild("AttackEvent")
attackEvent.OnServerEvent:Connect(function(player, weaponName, damage)
print(player.Name .. " استخدم " .. weaponName .. " بضرر " .. damage)
end)
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
task.wait(3)
attackEvent:FireClient(player, "تم تسجيل دخولك!")
end)
attackEvent:FireAllClients("بدأت الجولة!")
attackEvent.OnClientEvent:Connect(function(message)
print("وصل: " .. message)
end)
سنبني معًا نظامًا كاملًا وآمنًا.
-- 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)
-- 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)
لأن أي لاعب يستطيع تشغيل كود في الـ Client يقول:
claimReward:FireServer() ألف مرة في الثانية.
لو كان السيرفر يثق بالعميل بدون فحص، سيصبح الرصيد مليونيًا في ثانية.
| الخطأ | السبب | الحل |
|---|---|---|
| الحدث لا يعمل | RemoteEvent غير موجود في ReplicatedStorage | تأكد من Parent |
| WaitForChild عالق | الاسم خطأ | تأكد من تطابق الأسماء |
| FireServer لا يفعل شيئًا | OnServerEvent غير مربوط | اربطه في Script داخل السيرفر |
| player اسمه nil | مرّرت player يدويًا | لا تمرّره — Roblox يمرّره |
RemoteEvent مثل "طرق على الباب" — لا تعرف إن كان هناك رد. أما RemoteFunction فهو مثل "مكالمة هاتفية" — تسأل وتنتظر الجواب.
| RemoteEvent | RemoteFunction | |
|---|---|---|
| الاتجاه | أحادي (Fire and Forget) | ثنائي (سؤال وجواب) |
| هل ينتظر؟ | لا | نعم (Yield) |
| هل يرجع قيمة؟ | لا | نعم |
| الأداء | أسرع | أبطأ (يوقف التنفيذ) |
| الاستخدام | أحداث، إشعارات، طلبات | طلب بيانات يحتاج جوابًا |
| الدالة | تُستدعى من | المكان |
|---|---|---|
| InvokeServer() | Client | يسأل السيرفر |
| OnServerInvoke | Server | يُعرَّف كدالة واحدة |
| InvokeClient(player) | Server | يسأل لاعبًا معيّنًا |
| OnClientInvoke | Client | يُعرَّف كدالة واحدة |
OnServerInvoke و OnClientInvoke
ليست أحداثًا بل دوالًا تُسند مباشرة، ويمكن إسناد واحدة فقط.
-- 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
-- LocalScript
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local getData = ReplicatedStorage:WaitForChild("GetPlayerData")
local data = getData:InvokeServer()
print(data.level, data.coins, data.wins)
هذا النمط نادر لأن السيرفر لا يجب أن يعتمد على العميل. لكنه موجود لأشياء مثل: سؤال اللاعب عن اختياره من واجهة.
local askClient = ReplicatedStorage:WaitForChild("AskClient")
local answer = askClient:InvokeClient(player, "ما لونك المفضل؟")
print("اللاعب اختار: " .. answer)
local askClient = ReplicatedStorage:WaitForChild("AskClient")
askClient.OnClientInvoke = function(question)
print("السيرفر يسأل: " .. question)
return "أزرق"
end
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
-- 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 لا يقرر شيئًا.
| الحالة | الأنسب |
|---|---|
| الحصول على بيانات اللاعب | RemoteFunction |
| طلب شراء | RemoteFunction (للحصول على النتيجة) |
| إشعار للجميع | RemoteEvent |
| تحديث HUD | RemoteEvent |
| إرسال أوامر قتال | RemoteEvent |
| سؤال عن توفر غرض | RemoteFunction |
| الخطأ | السبب |
|---|---|
| InvokeServer لا يرجع | OnServerInvoke غير مسندة |
| يعلق السيرفر | InvokeClient واللاعب خرج |
| attempt to call nil | RemoteFunction غير موجودة في ReplicatedStorage |
| استخدامها للقتال | يجب استخدام RemoteEvent لأنه أسرع |
الواجهة (GUI) هي كل ما يراه اللاعب على الشاشة: الأزرار، الأشرطة، القوائم، الإشعارات، شريط الصحة… إتقان الواجهات يفرق بين لعبة مبتدئة ولعبة احترافية.
كل واجهة تبدأ من ScreenGui، وتتفرع منه العناصر:
ScreenGui
├── Frame
│ ├── TextLabel
│ ├── TextButton
│ └── ImageLabel
├── TextButton
└── ImageLabel
| العنصر | الوظيفة |
|---|---|
| ScreenGui | حاوية الواجهة كلها (توضع في StarterGui أو PlayerGui) |
| Frame | مستطيل ملون فارغ — أساس التصميم |
| TextLabel | نص غير قابل للنقر |
| TextButton | زر قابل للنقر |
| ImageLabel | صورة |
| ImageButton | صورة قابلة للنقر |
| ScrollingFrame | إطار قابل للتمرير |
| ViewportFrame | إطار ثلاثي الأبعاد للعرض |
| العنصر | الوظيفة |
|---|---|
| UICorner | زوايا دائرية |
| UIStroke | حدود ملونة للنص أو الإطار |
| UIGradient | تدرج لوني |
| UIPadding | هامش داخلي |
| UISizeConstraint | تحديد حد أدنى/أقصى للحجم |
| UIAspectRatioConstraint | الحفاظ على نسبة العرض للارتفاع |
| UIScale | تكبير/تصغير شامل |
| العنصر | الوظيفة |
|---|---|
| UIListLayout | ترتيب الأبناء في صف أو عمود |
| UIGridLayout | ترتيب الأبناء في شبكة |
| UIPageLayout | ترتيب في صفحات (مثل كتاب) |
| UITableLayout | ترتيب في جدول |
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)
نقطة الارتساء (0-1 من كل جهة). (0, 0) = أعلى يسار،
(1, 1) = أسفل يمين، (0.5, 0.5) = المركز.
frame.Visible = false -- إخفاء
button.ZIndex = 5 -- الطبقة (الأكبر فوق)
label.Rotation = 45 -- دوران 45 درجة
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
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)
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)
-- 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)
UserInputService (يُختصر UIS) هو المسؤول عن استقبال مدخلات اللاعب: الكيبورد، الماوس، اللمس، ويد التحكم. يعمل فقط في LocalScript.
local UserInputService = game:GetService("UserInputService")
| النوع | Enum |
|---|---|
| لوحة مفاتيح | Enum.UserInputType.Keyboard |
| ماوس | Enum.UserInputType.MouseButton1/2 |
| لمس | Enum.UserInputType.Touch |
| يد تحكم | Enum.UserInputType.Gamepad1 |
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return end -- تجاهل إذا كان داخل Chat مثلًا
if input.KeyCode == Enum.KeyCode.E then
print("ضغطت E")
end
end)
UserInputService.InputEnded:Connect(function(input, gameProcessed)
if gameProcessed then return end
if input.KeyCode == Enum.KeyCode.E then
print("رفعت E")
end
end)
true، فهذا يعني أن اللاعب يكتب في
الشات أو يضغط زرًا داخل واجهة. تجاهل الإدخال في هذه الحالة حتى لا يتفاعل
الكود مع الكتابة.
UserInputService.InputChanged:Connect(function(input, gameProcessed)
if input.UserInputType == Enum.UserInputType.MouseMovement then
-- print(input.Delta) -- حركة الماوس
end
end)
local mouse = game.Players.LocalPlayer:GetMouse()
-- موقع الماوس في العالم ثلاثي الأبعاد
print(mouse.Hit.Position)
-- الأهداف
print(mouse.Target)
-- عند الضغط
mouse.Button1Down:Connect(function()
print("زر الماوس الأيسر")
end)
-- 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)
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)
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if input.UserInputType == Enum.UserInputType.Touch then
print("لمس في: " .. tostring(input.Position))
end
end)
UserInputService.TouchEnabledUserInputService.KeyboardEnabledUserInputService.GamepadEnabled
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)
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)
أما على الجوال، تحتاج زرًا على الشاشة. عند الضغط عليه ينفذ نفس الإجراء — وهذا موضوع الفصل القادم.
ContextActionService (CAS) خدمة تدير الإجراءات (Actions) بطريقة موحّدة، وتوفر تلقائيًا أزرارًا للجوال عند الحاجة. هي أفضل من UserInputService في حالات كثيرة لأنها:
أنت تعرّف Action باسم، وتربطه بمفاتيح على الكيبورد وأزرار يد التحكم. CAS ينشئ زرًا على الجوال تلقائيًا.
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 -- زر يد التحكم
)
| Parameter | المعنى |
|---|---|
| actionName | اسم فريد للإجراء |
| functionToBind | الدالة التي تُنفَّذ |
| createTouchButton | هل نصنع زرًا للجوال؟ (true/false) |
| ...inputs | مفاتيح الكيبورد أو أزرار التحكم |
للتحكم بأي إجراء يُنفَّذ أولًا عند التعارض:
ContextActionService:BindActionAtPriority(
"AttackAction",
onAttack,
true,
2000, -- أولوية عالية
Enum.KeyCode.F
)
ContextActionService:UnbindAction("JumpAction")
-- أو إلغاء الكل
ContextActionService:UnbindAllActions()
ContextActionService:SetTitle("JumpAction", "اقفز")
ContextActionService:SetPosition("JumpAction", UDim2.new(1, -120, 1, -120))
ContextActionService:SetImage("JumpAction", "rbxassetid://0000000000")
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", "استخدم")
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
| الحالة | الأنسب |
|---|---|
| قراءة مدخلات عامة بدون واجهة | UserInputService |
| إجراء له زر مخصص (يعمل على الجوال تلقائيًا) | ContextActionService |
| نظام أدوات/أسلحة | ContextActionService |
| تبديل سلوتات 1-5 | ContextActionService |
| قراءة موضع الماوس | UserInputService |
TweenService يحرّك أي خاصية رقمية بشكل سلس بدلًا من القفزات الفجائية. هو أساس كل الأنيميشن الاحترافي في الواجهات والأجزاء.
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()
| Parameter | المعنى |
|---|---|
| Time | مدة الحركة بالثواني |
| EasingStyle | شكل التسارع (Linear, Quad, Sine, Bounce, Elastic…) |
| EasingDirection | In, Out, InOut |
| RepeatCount | عدد التكرار (0 = مرة، -1 = لا نهائي) |
| Reverses | هل تعود في الاتجاه المعاكس؟ |
| DelayTime | تأخير قبل البدء |
| النمط | الوصف |
|---|---|
| Linear | سرعة ثابتة |
| Quad / Cubic / Quart | تسارع متزايد |
| Sine | ناعم جدًا |
| Bounce | ارتداد مثل الكرة |
| Elastic | مرونة مثل المطاط |
| Back | يتراجع ثم يتحرك |
tween.Completed:Connect(function(playbackState)
if playbackState == Enum.PlaybackState.Completed then
print("انتهت الحركة")
end
end)
tween:Cancel() -- إيقاف نهائي
tween:Pause() -- إيقاف مؤقت
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
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()
-- يتحرك للأعلى والأسفل للأبد
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)
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)
local tween = TweenService:Create(...)tween:Play() في كل مرة تحتاجه.
RunService هي خدمة تُخبرك عند كل إطار (Frame) أو خطوة فيزيائية. هي أساس كل شيء مستمر: الحركة، الكاميرا، الأنظمة الزمنية.
| الحدث | متى يُستدعى؟ | المكان |
|---|---|---|
| Heartbeat | بعد تحديث الفيزياء | Client + Server |
| RenderStepped | قبل رسم كل إطار | Client فقط |
| Stepped | قبل Heartbeat (نادر الاستخدام) | Client + Server |
local RunService = game:GetService("RunService")
RunService.Heartbeat:Connect(function(deltaTime)
-- deltaTime = الوقت المنقضي منذ الإطار السابق (بالثواني)
print(deltaTime) -- حوالي 0.016 (60 FPS)
end)
deltaTime مهم جدًا: يجعل الكود مستقلًا عن سرعة الجهاز. عند ضرب السرعة بـ deltaTime، تتحرك الأشياء بنفس السرعة على جهاز قوي وضعيف.
RunService.RenderStepped:Connect(function(deltaTime)
-- شيء يتعلق بالكاميرا أو الواجهات — يعمل في Client فقط
workspace.CurrentCamera.CFrame = workspace.CurrentCamera.CFrame
end)
RunService.Stepped:Connect(function(time, deltaTime)
-- time = الوقت الكلي، deltaTime = وقت الإطار
end)
if RunService:IsClient() then
print("أنا عميل")
end
if RunService:IsServer() then
print("أنا سيرفر")
end
if RunService:IsStudio() then
print("يعمل في Roblox Studio")
end
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)
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)
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)
-- ❌ سيء: يعمل بأقصى سرعة ممكنة، يستنزف الموارد
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)
-- كل 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)()
| الحالة | الأداة |
|---|---|
| حركة كل إطار (سلسة) | Heartbeat / RenderStepped |
| تحديث الكاميرا | RenderStepped |
| عدّاد تنازلي أو نظام زمني | task.wait / Heartbeat |
| حركة أجزاء مرئية لكل لاعبين | Heartbeat على السيرفر |
| حركة بصرية محلية | RenderStepped |
هذان النوعان أساس التعامل مع العالم ثلاثي الأبعاد. كل حركة، اتجاه، دوران، أو حساب موقع يعتمد عليهما.
Vector3 يحتوي ثلاثة أرقام: X (يمين/يسار)، Y (أعلى/أسفل)، Z (أمام/خلف).
local v = Vector3.new(10, 20, 30)
print(v.X) -- 10
print(v.Y) -- 20
print(v.Z) -- 30
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
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
CFrame يحتوي الموقع والدوران. هو الأساس لتحريك الأجزاء بدقة.
local cf = CFrame.new(10, 5, 0)
print(cf.Position) -- 10, 5, 0
print(cf.X, cf.Y, cf.Z)
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
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.
-- 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
-- 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
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)
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)
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)
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)
نظام الفيزياء في Roblox هو ما يجعل الأجسام تتحرك، تسقط، تتصادم، وتتصرف بشكل طبيعي. فهم الفيزياء يفتح لك بابًا لبناء آلات، سيارات، مصاعد، قذائف، وألغاز فيزيائية.
كل جسم ثلاثي الأبعاد في Roblox يرث من BasePart:
Part، WedgePart، MeshPart، TrussPart، CornerWedgePart… كلها تشترك في نفس
الخصائص الفيزيائية الأساسية.
عندما تكون Anchored = true، يتجاهل الجزء الجاذبية
والاصطدامات ولا يتحرك أبدًا. استخدمه للأرضيات، المباني، المنصات الثابتة.
local platform = workspace.Platform
platform.Anchored = true
Anchored = true على المباني يجعلها تسقط
وتنهار عند تشغيل اللعبة. اضبط هذه الخاصية في Studio قبل الكتابة.
part.CanCollide = false -- يمر اللاعب من خلاله
| القيمة | الاستخدام |
|---|---|
| true | أرضيات، جدران، صناديق |
| false | مناطق تأثير، Trigger، Decoration |
الكتلة تُحدَّد تلقائيًا حسب الحجم والكثافة. كلما زادت الكتلة زادت صعوبة تحريك الجسم.
local part = workspace.HeavyPart
print(part:GetMass()) -- الكتلة الفعلية
-- لتقليل الكتلة دون تغيير الحجم
part.CustomPhysicalProperties = PhysicalProperties.new(
0.5, -- الكثافة
0.3, -- الاحتكاك (friction)
0.5, -- المرونة (elasticity)
0, -- وزن الاحتكاك
0 -- وزن المرونة
)
عندما تربط عدة أجزاء بـ Weld أو Motor أو أي Constraint، تُصبح Assembly واحد يتحرك كقطعة واحدة.
local weld = Instance.new("WeldConstraint")
weld.Part0 = part1
weld.Part1 = part2
weld.Parent = part1
-- الآن part1 و part2 يتحركان كأنهما قطعة واحدة
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
ملاحظة: 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()
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
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
| القيد | الوظيفة |
|---|---|
| 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
workspace.Gravity = 196.2 -- الافتراضي
workspace.Gravity = 50 -- جاذبية أخف (قفز أعلى)
workspace.Gravity = 0 -- انعدام جاذبية
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
-- تسقط طبيعيًا بفعل الجاذبية
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)
Raycasting أداة مهمة جدًا في الفيزياء، وسنخصص لها الفصل القادم بالكامل.
Raycasting يعني "إرسال شعاع" من نقطة في اتجاه، ومعرفة ما يصطدم به. هي أداة أساسية للقتال (Hit Detection)، الأسلحة، الرؤية، AI، الكاميرا.
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
| الخاصية | المعنى |
|---|---|
| Instance | الشيء الذي صُدم |
| Position | نقطة الاصطدام |
| Normal | اتجاه سطح الاصطدام |
| Distance | المسافة المقطوعة |
| Material | مادة السطح |
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)
| النوع | المعنى |
|---|---|
| Exclude | تجاهل المذكورين |
| Include | اصطدم فقط بالمذكورين |
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
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
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
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
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
-- احصل على نقطة فوهة السلاح
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)
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
| Raycast | Touched | |
|---|---|---|
| الدقة | عالية جدًا | منخفضة |
| الأداء | سريع | بطيء عند كثرة الأحداث |
| التحكم | كامل | محدود |
| الاستخدام | الأسلحة، الرؤية، AI | مناطق، لمس بسيط |
الـ Tool هي أداة يحملها اللاعب. توضع في Backpack، وعند اختيارها تظهر في يده. هي أساس الأسلحة، الفوانيس، الآلات، وأي شيء "يُحمل".
Tool
├── Handle (Part إلزامي إذا RequiresHandle = true)
├── (اختياري) Script
├── (اختياري) LocalScript
└── (اختياري) Attachment / Sounds
| الخاصية | المعنى |
|---|---|
| RequiresHandle | هل يحتاج Handle؟ (الافتراضي true) |
| CanBeDropped | هل يمكن إسقاطه؟ |
| ToolTip | نص يظهر عند المرور عليه |
| TextureId | أيقونة في الحقيبة |
| Grip | كيف يُمسكه اللاعب (CFrame) |
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)
-- تدوير الأداة في اليد
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)
-- من 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)
ReplicatedStorage
└── Remotes
└── SwordAttack (RemoteEvent)
ServerStorage
└── Sword (Tool)
├── Handle
└── ServerScript (Script)
StarterPack
└── (اختياري لو أردت الأداة تلقائيًا)
-- 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)
-- 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)
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)
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)
نظام M1 هو نظام الضربات المتتابعة (Combos) — أساس كل ألعاب القتال. سنبنيه خطوة بخطوة مع Combo، Animations، Cooldown، Server Validation، والأمان.
ReplicatedStorage
├── Remotes
│ ├── M1Attack
│ └── M1Effect
└── Modules
└── CombatConfig
ServerScriptService
└── CombatServer
StarterPlayerScripts
└── CombatClient
StarterPlayer/StarterCharacterScripts (أو الأداة)
└── Animations
-- 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
-- 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)
-- 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)
-- 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
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
الأنيميشن هو ما يعطي الحياة للشخصيات. Animator يتحكم في تشغيل الإيماءات والحركات، وفي القتال يعطي إحساسًا حقيقيًا بالضربة.
| العنصر | الوظيفة |
|---|---|
| Animation | كائن يحمل AnimationId |
| Animator | المشغّل — موجود داخل Humanoid |
| AnimationTrack | النسخة المُحمَّلة القابلة للتشغيل |
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().
track:Play(0.1) -- مع FadeIn
track:Stop(0.2) -- مع FadeOut
track:AdjustSpeed(1.5) -- تسريع
track:AdjustWeight(2) -- زيادة الوزن
print(track.Length) -- مدة الأنيميشن بالثواني
print(track.IsPlaying) -- هل يعمل الآن؟
لتشغيل أنيميشن فوق آخر (مثلًا فوق المشي):
track.Priority = Enum.AnimationPriority.Action
| الأولوية | المعنى |
|---|---|
| Core | الأنيميشن الأساسي (المشي/القفز) — لا تستخدم |
| Idle | الوقوف |
| Movement | الحركة |
| Action | الهجوم/الأفعال |
| Action2 | فوق Action |
| Action3 | فوق Action2 |
| Action4 | أعلى شيء |
track:Play()
track.Stopped:Connect(function()
print("انتهى الأنيميشن")
end)
track:GetMarkerReachedSignal("Hit"):Connect(function()
print("وصلت لحظة الضربة!")
end)
-- أو عن طريق KeyframeReached
track.KeyframeReached:Connect(function(name)
print("وصل الإطار: " .. name)
end)
GetMarkerReachedSignal
لتفعيل Hitbox في اللحظة المناسبة.
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
| الاستخدام | المكان |
|---|---|
| أنيميشن اللاعب نفسه (استجابة سريعة) | Client |
| أنيميشن يظهر لكل اللاعبين | Server |
| أنيميشن الأدوات | Client (Tool.LocalScript) |
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
Roblox يشغّل أنيميشن الـ Idle/Walk/Jump تلقائيًا عبر
Animate script داخل كل شخصية. لا تعدّله
إلا إذا كنت تريد نظامًا مخصصًا.
-- 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)
DataStore هي الخدمة التي تحفظ بيانات اللاعبين بشكل دائم على سيرفرات Roblox. بدونها، كل تقدم اللاعب يضيع عند خروجه.
| الدالة | الوظيفة |
|---|---|
| GetDataStore(name) | الحصول على مخزن |
| GetAsync(key) | قراءة بيانات |
| SetAsync(key, value) | حفظ بيانات |
| UpdateAsync(key, fn) | حفظ آمن (يقرأ ثم يعدّل) |
| RemoveAsync(key) | حذف بيانات |
| BindToClose(fn) | تنفيذ كود قبل إغلاق السيرفر |
-- 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)
DataStore قد تفشل بسبب الشبكة أو Rate Limit. يجب دائمًا لفّ استدعاءاتها في
pcall.
local success, resultOrError = pcall(function()
return store:GetAsync(key)
end)
if success then
print("نجح:", resultOrError)
else
warn("فشل:", resultOrError)
end
UpdateAsync يحمي من Race Conditions (لاعبان يعدّلان نفس البيانات):
store:UpdateAsync("player_" .. player.UserId, function(oldData)
oldData = oldData or {coins = 0, level = 1}
oldData.coins += 10
return oldData
end)
-- 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)
game:BindToClose(function()
-- تُنفَّذ قبل إغلاق السيرفر — نعطيها فرصة للحفظ
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(function()
PlayerData.save(player)
end)
end
task.wait(3) -- أعطِ وقتًا للحفظ
end)
BindToClose. احفظ أيضًا عند
PlayerRemoving وعند أحداث مهمة (شراء، ترقية).
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)
| القيود | القيمة |
|---|---|
| GetAsync | 60 + 10*اللاعبين في الدقيقة |
| SetAsync | 60 + 10*اللاعبين في الدقيقة |
| UpdateAsync | 60 + 10*اللاعبين في الدقيقة |
{
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
}
Leaderstats هو الجدول الذي يظهر أعلى يمين الشاشة (في لوحة اللاعبين). يعرض قيمًا مثل Coins و Kills و Wins. وهو الطريقة الرسمية لعرض إحصائيات اللاعبين.
-- 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 بالضبط
(حروف صغيرة، بلا مسافات). وأول عنصرين فقط يظهران في اللوحة على الجوال،
لكن الكل يظهر على الكمبيوتر.
| النوع | الاستخدام |
|---|---|
| IntValue | أرقام صحيحة (نقاط، عملات) |
| NumberValue | أرقام عشرية |
| StringValue | نصوص (رتبة) |
| BoolValue | صحيح/خطأ |
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)
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)
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
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 في Humanoid عند إصابة
لاعب آخر له. هذا يسهّل تحديد القاتل. يجب أن تستخدمها فقط عند التأكد أن
الإصدار الأخير من الضربة كان من لاعب.
الترتيب حسب LayoutOrder على كل قيمة:
local kills = Instance.new("IntValue")
kills.Name = "Kills"
kills.LayoutOrder = 1 -- أول
kills.Parent = stats
ModuleScript هو سكربت لا يعمل وحده، بل يُستخدم من سكربتات أخرى عبر
require(). هو الأداة الأساسية لتنظيم الكود،
وإعادة استخدامه، وفصل الأنظمة إلى ملفات صغيرة.
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
local A = require(module)
local B = require(module)
print(A == B) -- true (نفس الجدول!)
| النوع | المكان | الاستخدام |
|---|---|---|
| Shared | ReplicatedStorage | يستخدمه السيرفر والعميل معًا |
| Server | ServerScriptService / ServerStorage | فقط للسيرفر — منطق اللعبة |
| Client | StarterPlayerScripts | فقط للعميل — واجهات وتأثيرات |
-- 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
-- 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
-- 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
نظّم الـ Modules هكذا:
ReplicatedStorage
├── Modules
│ ├── Config (إعدادات)
│ ├── Utilities (دوال مساعدة)
│ └── ItemDatabase (بيانات الأغراض)
ServerScriptService
├── Modules
│ ├── PlayerData
│ ├── Combat
│ └── RoundSystem
└── Main.server.lua (نقطة البداية)
StarterPlayer
└── StarterPlayerScripts
├── Modules
│ └── UIManager
└── Client.client.lua
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
Config Module لكل الثوابت (DAMAGE، SPEED…).البرمجة الكائنية (Object-Oriented Programming) تجعل الكود أقرب لتمثيل الأشياء الحقيقية: لاعب، سلاح، عدو، مركبة… كل شيء "كائن" له خصائص ودوال.
في Luau، نبني الكائنات باستخدام:
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
function Player.new(name)
local self = setmetatable({}, Player)
-- ...
return self
end
setmetatable({}, Player) يخبر Luau: "إذا طُلب
شيء غير موجود في هذا الجدول، ابحث عنه في Player". هذا هو سر الوصول
لـ :takeDamage وغيرها.
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 (اختصار)
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
-- 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}
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
Metatable هو "جدول أُستاذ" يتحكم بسلوك جدول آخر. هو أساس OOP، وأساس كل شيء متقدم في Luau.
local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t) == mt) -- true
عندما تحاول الوصول لمفتاح غير موجود في الجدول، يبحث Luau في
__index.
local base = {greet = function() print("مرحبًا") end}
local obj = setmetatable({}, {__index = base})
obj.greet() -- مرحبًا
local t = setmetatable({}, {
__index = function(self, key)
return "المفتاح " .. key .. " غير موجود"
end
})
print(t.foo) -- المفتاح foo غير موجود
يُستدعى عند محاولة كتابة قيمة جديدة:
local t = setmetatable({}, {
__newindex = function(self, key, value)
print("تحاول كتابة " .. key .. " = " .. tostring(value))
rawset(self, key, value)
end
})
t.x = 10
-- يطبع: تحاول كتابة x = 10
rawset لتجاوز الـ Metatable.
local player = {name = "Ahmed", health = 100}
setmetatable(player, {
__tostring = function(self)
return self.name .. " (HP: " .. self.health .. ")"
end
})
print(player) -- Ahmed (HP: 100)
local t = setmetatable({}, {
__call = function(self, arg)
print("استدعيت بـ: " .. arg)
end
})
t("hello") -- استدعيت بـ: hello
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)
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
}
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
local locked = setmetatable({}, {
__metatable = "محمي"
})
print(getmetatable(locked)) -- محمي
-- setmetatable(locked, {}) -- خطأ!
للتحكم في جمع القمامة (Garbage Collection):
-- مفاتيح ضعيفة: تُحذف تلقائيًا عند عدم وجود مرجع آخر
local cache = setmetatable({}, {__mode = "k"})
| الـ metamethod | الوظيفة |
|---|---|
| __index | البحث عند عدم وجود مفتاح |
| __newindex | الكتابة لمفتاح جديد |
| __tostring | تحويل لنص |
| __call | استدعاء الجدول كدالة |
| __add / __sub / __mul / __div | عمليات حسابية |
| __mod / __pow / __unm | عمليات أخرى |
| __concat | دمج بـ .. |
| __eq / __lt / __le | مقارنات |
| __len | طول # |
| __metatable | حماية |
| __mode | Weak references |
| __gc | عند جمع القمامة |
Luau تدعم Type Checking — نظام اختياري يحدد نوع كل متغير. هذا يساعدك على اكتشاف الأخطاء قبل تشغيل اللعبة.
local name: string = "Ahmed"
local age: number = 20
local isAlive: boolean = true
local position: Vector3 = Vector3.new(0, 0, 0)
local function add(a: number, b: number): number
return a + b
end
local function greet(name: string): string
return "مرحبًا " .. name
end
local function processPlayer(player: {name: string, age: number})
print(player.name, player.age)
end
processPlayer({name = "Ahmed", age = 20})
type Vector2D = {x: number, y: number}
local function length(v: Vector2D): number
return math.sqrt(v.x^2 + v.y^2)
end
local function process(value: string | number)
if type(value) == "string" then
print("نص: " .. value)
else
print("رقم: " .. value)
end
end
process("hello")
process(42)
local function findUser(name: string): string?
-- قد يرجع string أو nil
if name == "Ahmed" then
return "Found"
end
return nil
end
Luau ذكية: تعرف أن القيمة لم تعد nil بعد الفحص:
local function getName(player: Player?): string
if not player then
return "لا أحد"
end
return player.Name -- Luau تعرف أن player ليس nil
end
local function process(value: any)
if typeof(value) == "Vector3" then
print(value.X) -- Luau تعرف أنه Vector3
end
end
local part = workspace:FindFirstChild("Part") :: BasePart
-- :: تخبر Luau "أنا متأكد أنه BasePart"
-- 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}
أضف هذا في أول السكربت لتفعيل الفحص الصارم:
--!strict
| الوضع | السلوك |
|---|---|
| --!nocheck | لا يوجد فحص |
| --!nonstrict | افتراضي — فحص خفيف |
| --!strict | فحص كامل وصارم |
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)
تخيل أنك نسيت Parameter في دالة. Strict Mode سينبهك. تخيل أنك مررت string بدل number. سينبهك. هذا يوفر عليك ساعات Debugging.
هذا الفصل يجمع تقنيات Luau المتقدمة: Closures، Coroutines، task، معالجة الأخطاء، والتصحيح.
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
Coroutine = دالة يمكن إيقافها مؤقتًا واستئنافها. مفيدة للعمليات الطويلة دون تعطيل باقي الكود.
local co = coroutine.create(function()
print("البداية")
coroutine.yield() -- إيقاف مؤقت
print("الاستئناف")
end)
coroutine.resume(co) -- البداية
coroutine.resume(co) -- الاستئناف
task.spawn(function()
print("يعمل في الخلفية")
end)
-- أو تمرير دالة جاهزة
local function work()
print("عمل")
end
task.spawn(work)
task.delay(2, function()
print("بعد ثانيتين")
end)
task.wait(1) -- ينظر ثانية
task.wait() -- ينظر إطارًا واحدًا
يشبه spawn لكن ينتظر حتى نهاية الإطار الحالي:
task.defer(function()
print("يُنفّذ بعد الإطار الحالي")
end)
| الأداة | الوصف |
|---|---|
| task.spawn | ينفذ فورًا في الخلفية |
| task.delay | ينفذ بعد وقت |
| task.wait | ينظر |
| task.defer | يؤجل لنهاية الإطار |
| coroutine | يدوي — تحكم كامل |
local function divide(a, b)
if b == 0 then
error("قسمة على صفر!")
end
return a / b
end
divide(10, 0) -- سيرمي خطأ
local success, result = pcall(function()
return divide(10, 0)
end)
if success then
print("النتيجة: " .. result)
else
warn("خطأ: " .. result)
end
local function onError(err)
return "خطأ: " .. err .. "\n" .. debug.traceback()
end
local success, result = xpcall(function()
error("مشكلة!")
end, onError)
print(result)
local function setHealth(player, hp)
assert(type(hp) == "number", "hp يجب أن يكون رقمًا")
assert(hp > 0, "hp يجب أن يكون موجبًا")
-- ...
end
setHealth(player, -5) -- خطأ: hp يجب أن يكون موجبًا
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
-- اسم السكربت الحالي
print(script.Name)
-- معلومات عن دالة
local info = debug.info(1, "s")
print(info)
-- Stack trace
print(debug.traceback())
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)
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)
للتحكم بعدد المهام المتزامنة:
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
الأداء هو الفرق بين لعبة سلسة ولعبة تتعطل. Roblox تعطي كل سيرفر موارد محدودة. كل شيء تستهلكه (Instances، Memory، Connections) يؤثر على الجميع.
كل Instance يُنشَأ يستهلك ذاكرة. احذف ما لم تعد تحتاجه:
part:Destroy() -- يحرر الذاكرة
-- لا تستخدم: part.Parent = nil
Luau يحرر تلقائيًا الجداول غير المستخدمة. لكن هذا يستهلك CPU. لذلك:
-- ❌ سيء: جدول جديد كل إطار
RunService.Heartbeat:Connect(function()
local temp = {1, 2, 3} -- 60 جدول/ثانية!
end)
-- ✅ أفضل
local temp = {1, 2, 3}
RunService.Heartbeat:Connect(function()
-- استخدم temp
end)
-- ❌ سيء: 1000 Part في Workspace
for i = 1, 1000 do
local p = Instance.new("Part")
p.Parent = workspace
end
-- ✅ أفضل: استخدم StreamingEnabled أو تجميع أو Rendering
-- أو استخدم Parts فقط عند الحاجة
-- ❌ سيء: لا تفصل
while true do
part.Touched:Connect(function() end)
task.wait(1)
end
-- ✅ أفضل
local connection = part.Touched:Connect(function() end)
-- لاحقًا
connection:Disconnect()
-- ❌ سيء: 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)
-- ❌ سيء: حسابات ثقيلة في كل إطار
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)
local lastUsed = 0
local COOLDOWN = 0.5
local function doAction()
local now = tick()
if now - lastUsed < COOLDOWN then return end
lastUsed = now
-- نفّذ الفعل
end
فعّلها من Workspace → StreamingEnabled = true. Roblox يحمّل فقط الأجزاء القريبة من اللاعب، مما يحسّن الأداء بشكل كبير.
-- ❌ بطيء
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 -- أخيرًا
-- ❌ بطيء
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 -- تحديث واحد
في Roblox Studio افتح: View → Performance Stats → Summary. سترى:
Ctrl + F6 لتشغيل MicroProfiler. يعرض
بدقة أي جزء من كودك يستهلك أكثر وقت.
هذا الفصل هو الأهم لأي مطور Roblox. أي لعبة بدون حماية ستُخترق في غضون أيام. ستحميك هذه المعرفة من الخسائر ومن المشاكل مع اللاعبين.
لأن أي لاعب يستطيع تشغيل برامج تُعدّل Client-side بالكامل. يستطيع:
-- 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)
-- 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)
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
اللاعب يعدّل 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)
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)
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)
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)
| الهجوم | الحل |
|---|---|
| إرسال RemoteEvents بقيم ضخمة | Type check + Range check |
| FireServer بسرعة | Rate limiting |
| تعديل WalkSpeed | فحص دوري على السيرفر |
| Teleport | فحص المسافة بين الإطارات |
| حذف أجزاء لعرض ميزة | لا تعتمد على Client للقرارات |
| Clone أغراض | التحقق من المصدر |
| تعديل Humanoid.Health | السيرفر يتحكم بالصحة |
الفرق بين مشروع ينمو بسهولة ومشروع ينهار بعد شهر هو التنظيم. هذا الفصل يعلمك البنية الاحترافية التي يستخدمها المطورون المحترفون في Roblox.
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
| المجلد | الوظيفة |
|---|---|
| ReplicatedStorage/Remotes | كل التواصل بين Client و Server |
| ReplicatedStorage/Modules | Modules مشتركة (Utilities، Config) |
| ServerScriptService/Modules | Modules السرية — لا يراها اللاعب |
| ServerScriptService/Systems | أنظمة السيرفر (Shop، Quest…) |
| StarterPlayerScripts/Controllers | أنظمة العميل (UI، Combat…) |
| ServerStorage | أدوات ونماذج سرية |
بدلًا من سكربتات كثيرة تُشغّل نفسها، استخدم سكربت واحد يبدأ كل شيء:
-- 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("✅ السيرفر بدأ بنجاح")
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
-- 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
| النوع | الصيغة | مثال |
|---|---|---|
| Module | PascalCase | PlayerData, CombatSystem |
| Script (Server) | PascalCase.server | CombatHandler.server.lua |
| Script (Client) | PascalCase.client | HUDController.client.lua |
| دالة | camelCase | getPlayer, dealDamage |
| متغير | camelCase | playerHealth, currentCoins |
| ثابت | UPPER_SNAKE | MAX_HEALTH, DEFAULT_SPEED |
| RemoteEvent | PascalCase | AttackEvent, ShopPurchase |
| Folder | PascalCase | Remotes, GameAssets |
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
استخدم Rojo مع Git لمشاريع احترافية. يسمح بكتابة الكود في VS Code مع مراقبة الإصدارات. هذا الموضوع اختياري لكن مفيد جدًا.
هذا الفصل يجمع 10 مشاريع عملية من الصفر. كل مشروع مستقل، والفصول التالية ستوضح طريقة اختبارها وحل مشاكلها.
عملات ذهبية تدور في الأرض، اللاعب يجمعها لتزيد نقاطه.
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)
مسار عوائق مع 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)
متجر يشتري منه اللاعب أغراضًا بالعملات.
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)
-- 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)
-- 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)
-- 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)
راجع الفصل 28 — نظام M1 كامل مع Combo و Hitbox و Server Validation.
-- 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)
-- 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)
اجمع كل الأنظمة السابقة: Coin Collector + Kill/Death + Shop + Inventory + Round System. هذا هو أساس "مشروع التخرج" في الفصل 46.
Debugging هو 50% من وقت المطور. هذا الفصل يعلمك كيف تجد الأخطاء بسرعة بدلًا من التخبط.
افتحها من View → Output. إذا ظهر خطأ أحمر، اقرأه جيدًا. عادةً يخبرك:
print("بدأ السكربت")
print("المتغير x =", x)
-- اطبع داخل دالة
local function test()
print("[test] دخلت الدالة")
print("[test] x =", x)
end
print("[Combat]", damage)
warn("شيء غريب حدث:", x)
يظهر بلون برتقالي، ويسهل ملاحظته.
ServerScriptService.Main:5: attempt to index nil with 'Value'
الترجمة:
Stack Begin
Script 'ServerScriptService.Main', Line 5 - function doDamage
Script 'ServerScriptService.Main', Line 20
Stack End
يقول: الخطأ في السطر 5 داخل دالة doDamage، والدالة استُدعيت من السطر 20. اتبع الأثر من الداخل للخارج.
-- معلومات عن الدالة الحالية
print(debug.info(1, "s")) -- source
print(debug.info(1, "n")) -- name
-- طباعة stack trace
print(debug.traceback())
-- الحصول على جميع المتغيرات المحلية (متقدم)
-- لكن هذا معقد قليلًا
-- الكود الأصلي
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
| الأداة | الوظيفة |
|---|---|
| Output | الرسائل والأخطاء |
| Breakpoints | إيقاف مؤقت |
| Watch | مراقبة متغيرات |
| MicroProfiler | تحليل الأداء (Ctrl+F6) |
| Developer Console | F9 داخل اللعبة |
| Command Bar | تنفيذ أوامر فورية |
عند اختبار اللعبة، جرّب التبديل بين:
اضغط على علامة التبديل أعلى الشاشة لاختيار أي جهة تراقب.
هذه قائمة بأشهر 12 خطأ يقع فيها المبتدئ، مع السبب والحل.
السبب: تحاول الوصول لخاصية 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
السبب: WaitForChild ينتظر
شيئًا لا يصل أبدًا (الاسم خطأ، أو في مكان آخر).
-- ❌ خطأ: اسم مختلف
game.ReplicatedStorage:WaitForChild("REmoteEvent")
-- ✅ الحل: تأكد من التطابق التام
game.ReplicatedStorage:WaitForChild("RemoteEvent")
-- ✅ الحل المؤقت: حدد timeout
game.ReplicatedStorage:WaitForChild("RemoteEvent", 5)
السبب: LocalPlayer متاح
فقط في LocalScript. السيرفر يخدم كل اللاعبين.
-- ❌ خطأ في Script
local player = game.Players.LocalPlayer -- nil
-- ✅ الحل: استخدم GetPlayers
for _, player in ipairs(game.Players:GetPlayers()) do
-- ...
end
LocalScript يجب أن يكون في:
لو وُضع في ServerScriptService لن يعمل أبدًا.
الأسباب الشائعة:
WaitForChild من Client.OnServerEvent.السبب: الشيء سيُنشأ لكن من مكان آخر، أو لن يُنشأ أصلًا.
-- تأكد أن الشيء يُنشأ بالفعل
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
-- ❌ خطأ شائع
local function setup()
local value = 100
end
setup()
print(value) -- nil (خارج النطاق)
-- ✅ الحل
local function setup()
return 100
end
local value = setup()
print(value) -- 100
-- ❌ خطأ
local text = "10"
print(text + 5) -- خطأ
-- ✅ الحل
print(tonumber(text) + 5) -- 15
-- أو
print(text .. 5) -- "105"
-- ❌ خطأ: لا يوجد خروج
while true do
-- لا شيء
end
-- ✅ الحل: أضف break شرط
while true do
if condition then break end
task.wait()
end
-- ✅ ✅ الحل الأفضل: حلقة زمنية
while true do
task.wait(1)
print("ثانية")
end
-- ❌ خطأ: Connections لا تُفصل
while true do
workspace.Part.Touched:Connect(function() end)
task.wait(1)
end
-- ✅ الحل
local connection = workspace.Part.Touched:Connect(function() end)
-- عند عدم الحاجة:
-- connection:Disconnect()
-- ❌ خطأ: يترك الـ Instance في الذاكرة
part.Parent = nil
-- ✅ الحل
part:Destroy()
-- LocalScript في Workspace لا يعمل في Studio
-- ✅ الحل: انقله إلى StarterPlayerScripts
-- أو استخدم RemoteEvent للتواصل مع السيرفر
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)
-- ❌ خطأ
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
هذه القواعد تلخص سنوات من خبرة المطورين. اتبعها من البداية لتوفير وقت لاحقًا.
-- ❌ سيء
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
| الشيء | الصيغة | مثال |
|---|---|---|
| متغير | camelCase | playerHealth |
| ثابت | UPPER_SNAKE | MAX_SPEED |
| دالة | camelCase | dealDamage |
| Module | PascalCase | PlayerData |
| Event | PascalCase | AttackEvent |
-- ❌ تعليق بديهي
-- يزيد 1 إلى i
i += 1
-- ✅ تعليق مفيد
-- نحتفظ بالفهرس لأن الحلقة التالية تحتاجه
i += 1
-- ❌ دالة تفعل كل شيء
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
-- ❌ نفس الكود في 5 أماكن
function A.f() print("مكرر") end
function B.f() print("مكرر") end
-- ...
-- ✅ Module واحد
local Helper = {}
function Helper.print() print("مرة واحدة") end
return Helper
-- استخدم pcall في كل عملية قد تفشل
local success, data = pcall(function()
return store:GetAsync(key)
end)
if not success then
warn("فشل التحميل: " .. tostring(data))
-- أعطِ اللاعب بيانات افتراضية
end
--- تحمّل بيانات لاعب من DataStore
-- @param player : Player
-- @return table
function PlayerData.load(player)
-- ...
end
هذه تمارين تجمع مهارات متعددة. جرّبها بعد قراءة كل الكتاب. الحلول في الفصل القادم.
هذه الحلول المقترحة. تذكر: هناك أكثر من طريقة صحيحة لكل تمرين.
for i = 1, 10 do
print(i)
end
local function greet(name)
return "مرحبًا " .. name
end
print(greet("Ahmed"))
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
for i = 1, 20 do
if i % 2 == 0 then
print(i)
end
end
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
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
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
local part = workspace.Part
local TweenService = game:GetService("TweenService")
local tween = TweenService:Create(
part,
TweenInfo.new(2),
{Transparency = 1}
)
tween:Play()
-- Server
local remote = game.ReplicatedStorage.RemoteEvent
remote.OnServerEvent:Connect(function(player, text)
print(player.Name .. ": " .. text)
end)
-- Client
game.ReplicatedStorage.RemoteEvent:FireServer("مرحبًا")
-- 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)
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)
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
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
راجع الفصل 28 — النظام كامل هناك.
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)
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.
لعبة قتال جماعية: لاعبون يقتلون بعضهم للحصول على Coins، يشترون بها أسلحة وقدرات، والفائز بالأكثر قتلًا يفوز بالجولة.
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/
-- 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
}
}
-- 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
-- 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
-- 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
-- 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
-- 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
-- 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
-- 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
-- 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("✅ السيرفر جاهز")
-- 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
-- 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)
-- 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)
| المصطلح | المعنى | شرح |
|---|---|---|
| API | واجهة برمجة | مجموعة الدوال التي تتيح لك استخدام خدمة (مثل DataStoreService). |
| Argument | وسيطة | القيمة التي تمررها لدالة عند استدعائها. |
| AnchorPoint | نقطة ارتساء | نقطة مرجعية لتموضع GUI (0-1). |
| Anchored | مثبّت | خاصية تجعل الجزء ثابتًا لا يتحرك. |
| المصطلح | المعنى | شرح |
|---|---|---|
| BasePart | جزء أساسي | الأصل لكل الأجزاء ثلاثية الأبعاد. |
| BindToClose | الربط بالإغلاق | تنفيذ كود قبل إغلاق السيرفر. |
| Boolean | منطقي | قيمة true أو false. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Callback | دالة استدعاء | دالة تُمرَّر لدالة أخرى لتنفيذها لاحقًا. |
| CFrame | إطار إحداثي | موقع + اتجاه ثلاثي الأبعاد. |
| Client | عميل | جهاز اللاعب. غير موثوق أمنيًا. |
| Closure | إغلاق | دالة تحتفظ بمتغيرات محيطها. |
| Color3 | لون RGB | لون بثلاث قيم (أحمر، أخضر، أزرق). |
| Connection | ارتباط | كائن يُعاد من Connect() ويمكن فصله. |
| Cooldown | فترة انتظار | منع تكرار الفعل خلال فترة زمنية. |
| Coroutine | روتين تعاوني | دالة يمكن إيقافها واستئنافها. |
| المصطلح | المعنى | شرح |
|---|---|---|
| DataStore | مخزن البيانات | خدمة حفظ البيانات دائمًا على Roblox. |
| Debounce | مانع تكرار | تقنية لمنع تكرار الأحداث السريعة. |
| Debugg | تصحيح الأخطاء | عملية إيجاد وحل الأخطاء. |
| Dictionary | قاموس | جدول بمفاتيح نصية. |
| Destroy | تدمير | حذف Instance وتحرير ذاكرته. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Event | حدث | إشعار يقع عند تغيير أو تفاعل. |
| Explorer | المستكشف | نافذة تعرض كل عناصر المشروع. |
| المصطلح | المعنى | شرح |
|---|---|---|
| FireServer | إطلاق للسيرفر | إرسال RemoteEvent من العميل للسيرفر. |
| Function | دالة | كتلة كود لها اسم وتنفذ عند الاستدعاء. |
| المصطلح | المعنى | شرح |
|---|---|---|
| GUI | واجهة رسومية | كل ما يراه اللاعب على الشاشة. |
| Heartbeat | نبض | حدث RunService يُستدعى كل إطار. |
| Humanoid | كائن الشخصية | يمثل صحة وسلوك الشخصية. |
| Hitbox | صندوق إصابة | منطقة تُحتسب فيها الإصابة. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Instance | عنصر | كل شيء في Roblox (Part، Script، GUI…). |
| Leaderstats | لوحة الإحصاء | مجلد يعرض إحصائيات اللاعب. |
| LocalScript | سكربت محلي | سكربت يعمل في جهاز اللاعب. |
| Luau | لغة روبلوكس | نسخة مطورة من Lua. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Metatable | جدول أستاذ | يتحكم بسلوك جدول آخر. |
| ModuleScript | وحدة برمجية | سكربت يُستخدم عبر require. |
| nil | لا شيء | قيمة تعني انعدام الوجود. |
| OOP | البرمجة الكائنية | نمط برمجة يعتمد على الكائنات. |
| OnServerEvent | حدث السيرفر | يستقبل RemoteEvents في السيرفر. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Parent | الأب | الخاصية التي تحدد مكان Instance. |
| pcall | استدعاء محمي | يلتقط الأخطاء بدون إيقاف الكود. |
| Player | لاعب | كائن اللاعب المنطقي. |
| Property | خاصية | صفة يمكن قراءتها/تعديلها. |
| Raycast | شعاع | إطلاق شعاع لكشف الاصطدامات. |
| RemoteEvent | حدث بعيد | يتواصل بين Client و Server. |
| RemoteFunction | دالة بعيدة | يعيد قيمة بعد سؤال. |
| ReplicatedStorage | تخزين مشترك | مكان يظهر للسيرفر والعميل. |
| RunService | خدمة التشغيل | توفر Heartbeat و RenderStepped. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Scope | نطاق | المنطقة التي يظهر فيها المتغير. |
| Server | سيرفر | المصدر المركزي للحقيقة. |
| ServerScriptService | خدمة سكربتات السيرفر | مكان سكربتات السيرفر. |
| ServerStorage | تخزين السيرفر | مخزن لا يراه اللاعب. |
| StarterGui | واجهة البداية | واجهات تُنسخ لكل لاعب. |
| StarterPack | حقيبة البداية | أدوات تبدأ مع اللاعب. |
| StarterPlayer | إعدادات اللاعب | سكربتات اللاعب الافتراضية. |
| StreamingEnabled | البث المفعّل | يحمّل الأجزاء القريبة فقط. |
| المصطلح | المعنى | شرح |
|---|---|---|
| Table | جدول | بنية بيانات أساسية في Luau. |
| task.spawn | تشغيل متوازي | تشغيل دالة في الخلفية. |
| Tool | أداة | شيء يحمله اللاعب. |
| Tween | حركة سلسة | تحريك خاصية تدريجيًا. |
| UDim2 | قياس UI | موقع/حجم عناصر الواجهة. |
| Vector3 | متجه ثلاثي | ثلاثة أرقام (X, Y, Z). |
| WaitForChild | انتظر الابن | ينتظر وصول Instance معين. |
| Workspace | فضاء العمل | العالم ثلاثي الأبعاد. |
مبروك! 🎉 لقد أكملت رحلة تعلّم Luau و Roblox Studio من الصفر إلى الاحتراف. هذا الكتاب احتوى 48 فصلًا غطّت:
البرمجة ليست موهبة، بل مهارة تُبنى بالممارسة. كل مطور محترف بدأ من صفر مثلك. الفرق الوحيد هو أنه استمر.
ابدأ اليوم بمشروع صغير. يومًا بعد يوم، ستُفاجأ بما وصلت إليه. Roblox تحتاج مطورين عربًا موهوبين — وأنت الآن واحد منهم. 🚀