مثال addBoxZone من ox_target - استهداف FiveM باستخدام Raycast بلغة Lua
مثال addBoxZone من ox_target مع كود Lua للنسخ واللصق لاستهداف FiveM باستخدام Raycast. يشمل أيضًا addSpherezone و qb-target. QBCore، ESX وStandalone.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
ما هو Raycasting في FiveM؟
Raycasting هي تقنية إطلاق خط غير مرئي (شعاع) من نقطة في الفضاء ثلاثي الأبعاد إلى أخرى والتحقق مما يصطدم به على طول الطريق. في FiveM، يُستخدم Raycasting لأنظمة الاستهداف، التفاعل مع الكائنات، فحوصات خط الرؤية، اكتشاف السطح، وآليات التصويب المخصصة. يوفر محرك اللعبة عدة دوال Raycast تتيح لك اكتشاف الكيانات والأسطح والهندسة العالمية بدقة. فهم Raycasting يفتح آليات لعب متقدمة مثل الاستهداف المخصص، أنظمة الوضع الذكية، مؤشرات الليزر، والتفاعلات الحساسة للسياق التي تستجيب لما ينظر إليه اللاعب.
رسم الأشعة الأساسي مع StartShapeTestRay
أبسط شكل من أشكال raycasting يستخدم StartShapeTestRay الذي يلقي شعاعًا بين إحداثيين ويعيد معلومات عن أول شيء يصطدم به. النتيجة تشمل حالة الاصطدام، الإحداثيات الدقيقة لنقطة التأثير، متجه السطح الطبيعي، ومقبض الكيان إذا تم الاصطدام بكيان. يجب عليك استدعاء GetShapeTestResult على الإطار التالي لاسترجاع النتائج.
-- client/raycast_basic.lua
local function Raycast(origin, direction, maxDistance, flags, ignoreEntity)
local destination = origin + direction * maxDistance
local shapeTest = StartShapeTestRay(
origin.x, origin.y, origin.z,
destination.x, destination.y, destination.z,
flags or -1, -- flags: -1 = everything
ignoreEntity or 0, -- entity to ignore
0
)
local _, hit, endCoords, surfaceNormal, entityHit = GetShapeTestResult(shapeTest)
return {
hit = hit == 1,
coords = endCoords,
normal = surfaceNormal,
entity = entityHit,
}
end
-- Raycast from camera to world (what the player is looking at)
local function GetPlayerAimTarget(maxDist)
local camCoords = GetGameplayCamCoord()
local camRot = GetGameplayCamRot(2)
-- Convert rotation to direction vector
local radX = math.rad(camRot.x)
local radZ = math.rad(camRot.z)
local direction = vector3(
-math.sin(radZ) * math.abs(math.cos(radX)),
math.cos(radZ) * math.abs(math.cos(radX)),
math.sin(radX)
)
local playerPed = PlayerPedId()
return Raycast(camCoords, direction, maxDist or 50.0, -1, playerPed)
end
أعلام وفلترة Raycast
معامل flags في StartShapeTestRay يتحكم في أنواع الأجسام التي يمكن أن يصيبها الشعاع. استخدام العلامات الصحيحة أمر حاسم للأداء والدقة. التحقق من كل شيء (-1) مكلف وغالبًا ما يعيد نتائج غير مرغوب فيها مثل حدود التصادم غير المرئية. استخدم العلامات المحددة لاستهداف فقط ما تحتاجه.
-- client/raycast_flags.lua
-- Common flag values for StartShapeTestRay
local RayFlags = {
WORLD = 1, -- Static world geometry (buildings, terrain)
VEHICLES = 2, -- Vehicles
PEDS = 4, -- Pedestrians and players (on foot)
OBJECTS = 16, -- Props and objects
WATER = 32, -- Water surfaces
VEGETATION = 256, -- Trees and bushes
-- Common combinations
WORLD_AND_VEHICLES = 3,
WORLD_AND_OBJECTS = 17,
ENTITIES_ONLY = 22, -- Vehicles + Peds + Objects
ALL = -1, -- Everything
}
-- Example: Only detect vehicles (for a speed camera script)
local function DetectVehicleAhead(origin, direction)
return Raycast(origin, direction, 100.0, RayFlags.VEHICLES, PlayerPedId())
end
-- Example: Detect ground position (for object placement)
local function GetGroundPosition(x, y, z)
local result = Raycast(
vector3(x, y, z + 50.0),
vector3(0.0, 0.0, -1.0),
100.0,
RayFlags.WORLD
)
if result.hit then
return result.coords
end
return nil
end
بناء نظام الاستهداف
نظام الاستهداف يجمع بين raycasting وتصنيف الكيانات لتمكين اللاعبين من اختيار والتفاعل مع كيانات محددة في عالم اللعبة. هذا هو الأساس لأنظمة التفاعل مثل ox_target و qb-target. يقوم النظام بإطلاق شعاع من الكاميرا في كل إطار، يتحقق إذا كانت الكيان المصاب يطابق أي أهداف مسجلة، ويعرض مطالبات التفاعل.
-- client/targeting.lua
local TargetSystem = {
targets = {},
currentTarget = nil,
enabled = true,
}
function TargetSystem.AddTarget(entity, options)
TargetSystem.targets[entity] = {
label = options.label or 'Interact',
icon = options.icon or 'fas fa-hand',
distance = options.distance or 3.0,
canInteract = options.canInteract,
onSelect = options.onSelect,
}
end
function TargetSystem.RemoveTarget(entity)
TargetSystem.targets[entity] = nil
end
-- Main targeting loop
CreateThread(function()
while true do
if TargetSystem.enabled then
local aimResult = GetPlayerAimTarget(10.0)
if aimResult.hit and aimResult.entity ~= 0 then
local entity = aimResult.entity
local target = TargetSystem.targets[entity]
if target then
local playerCoords = GetEntityCoords(PlayerPedId())
local entityCoords = GetEntityCoords(entity)
local dist = #(playerCoords - entityCoords)
if dist <= target.distance then
local canInteract = true
if target.canInteract then
canInteract = target.canInteract(entity)
end
if canInteract then
TargetSystem.currentTarget = entity
-- Draw interaction prompt
DrawText3D(entityCoords.x, entityCoords.y, entityCoords.z + 1.0,
target.label)
-- Handle interaction key press
if IsControlJustPressed(0, 38) then -- E key
target.onSelect(entity)
end
end
end
end
else
TargetSystem.currentTarget = nil
end
end
Wait(0)
end
end)
-- Helper: Draw 3D text at world coordinates
function DrawText3D(x, y, z, text)
SetTextScale(0.35, 0.35)
SetTextFont(4)
SetTextProportional(true)
SetTextColour(255, 255, 255, 215)
SetTextEntry('STRING')
SetTextCentre(true)
AddTextComponentString(text)
SetDrawOrigin(x, y, z, 0)
DrawText(0.0, 0.0)
ClearDrawOrigin()
end
كشف السطح ووضع الأجسام
يعد Raycasting ضروريًا لأنظمة وضع الأشياء حيث يشير اللاعب إلى سطح ويتبع معاينة الكائن هدفه. تخبرك العادية السطحية التي يعيدها Raycast باتجاه السطح، مما يسمح لك بمحاذاة الكائنات الموضوعة بشكل صحيح على المنحدرات والجدران والأسقف.
-- client/placement.lua
local Placement = {
active = false,
previewEntity = nil,
modelName = nil,
}
function Placement.Start(modelName)
Placement.modelName = modelName
Placement.active = true
-- Create preview prop
local model = joaat(modelName)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
Placement.previewEntity = CreateObject(model, 0.0, 0.0, 0.0, false, true, false)
SetEntityAlpha(Placement.previewEntity, 150, false)
SetEntityCollision(Placement.previewEntity, false, false)
FreezeEntityPosition(Placement.previewEntity, true)
SetModelAsNoLongerNeeded(model)
CreateThread(function()
while Placement.active do
local result = GetPlayerAimTarget(15.0)
if result.hit then
local coords = result.coords
SetEntityCoords(Placement.previewEntity,
coords.x, coords.y, coords.z, false, false, false, false)
-- Align to surface normal
local normal = result.normal
local pitch = math.deg(math.asin(normal.z)) - 90.0
SetEntityRotation(Placement.previewEntity, pitch, 0.0,
GetEntityHeading(PlayerPedId()), 2, true)
-- Color: green if valid, red if not
local valid = IsPlacementValid(coords)
if valid then
SetEntityDrawOutline(Placement.previewEntity, true)
end
-- Confirm placement
if IsControlJustPressed(0, 38) and valid then
Placement.Confirm(coords)
end
end
-- Cancel placement
if IsControlJustPressed(0, 73) then -- X key
Placement.Cancel()
end
Wait(0)
end
end)
end
function Placement.Confirm(coords)
Placement.active = false
if Placement.previewEntity then
DeleteEntity(Placement.previewEntity)
Placement.previewEntity = nil
end
TriggerServerEvent('myresource:placeObject', Placement.modelName, coords)
end
function Placement.Cancel()
Placement.active = false
if Placement.previewEntity then
DeleteEntity(Placement.previewEntity)
Placement.previewEntity = nil
end
end
function IsPlacementValid(coords)
local playerCoords = GetEntityCoords(PlayerPedId())
local dist = #(playerCoords - coords)
return dist >= 1.0 and dist <= 10.0
end
فحوصات خط الرؤية
Raycasting هو الطريقة القياسية للتحقق مما إذا كان بإمكان نقطتين رؤية بعضهما البعض دون عوائق. يُستخدم هذا في أنظمة التخفي، ووعي الذكاء الاصطناعي، وآليات القناص، وأي طريقة لعب تعتمد على الرؤية. أطلق شعاعًا من نقطة إلى أخرى وتحقق مما إذا كان يصطدم بشيء قبل الوصول إلى الهدف.
-- shared/los.lua (can run on both client and server)
local function HasLineOfSight(from, to, ignoreEntity)
local direction = to - from
local distance = #direction
direction = direction / distance -- normalize
local result = Raycast(from, direction, distance, RayFlags.WORLD, ignoreEntity)
if not result.hit then
return true -- nothing blocking the path
end
-- Check if the hit point is past the target
local hitDist = #(from - result.coords)
return hitDist >= distance * 0.95
end
-- Usage: Check if NPC can see the player
local function CanNPCSeePlayer(npcPed, playerPed)
local npcCoords = GetEntityCoords(npcPed) + vector3(0, 0, 0.7)
local playerCoords = GetEntityCoords(playerPed) + vector3(0, 0, 0.7)
return HasLineOfSight(npcCoords, playerCoords, npcPed)
end
تحسين الأداء لعمليات Raycasts
- تحديد تردد raycast. لا تقم بإرسال أشعة في كل إطار إلا إذا كان ذلك ضروريًا للغاية. بالنسبة لفحوصات التفاعل، يكون كل 100 مللي ثانية عادةً كافيًا. استخدم إرسال الأشعة لكل إطار فقط لأنظمة التصويب النشطة وأنظمة التمركز.
- استخدم العلامات المحددة. الإلقاء ضد
-1(كل شيء) أبطأ بكثير من استهداف أنواع الكيانات المحددة. قم بالتصفية لتشمل فقط ما تحتاجه. - خزن النتائج مؤقتًا. إذا لم يتحرك اللاعب أو ينظر في اتجاه مختلف، ستكون نتيجة raycast هي نفسها. تخطى عمليات الإرسال المكررة بمقارنة موقع الكاميرا ودورانها.
- استخدم فحوصات المسافة أولاً. قبل استخدام رسم الأشعة للتحقق من تفاعل الكيان، تحقق من أن اللاعب ضمن نصف قطر معقول. فحوصات المسافة أرخص بكثير من رسم الأشعة.
- يفضل استخدام StartShapeTestLosProbe لخط الرؤية. لفحوصات خط الرؤية البسيطة بين نقطتين معروفتين,
StartShapeTestLosProbeأخف منStartShapeTestRayلأنه يعيد فقط قيمة منطقية ضربة/لا ضربة. - تجنب استخدام raycasting عبر الخريطة بأكملها. حافظ على مسافات قصوى معقولة. شعاع 50 وحدة أرخص بكثير من شعاع 1000 وحدة.