FiveM Offscreen

Render a triangle to the screen based on a world coordinate.

It’s not perfect, but it does the job.

Preview

Download

GitHub - nidi21/fivem-offscreen

11 Likes

Nice idea :+1:

Could be used for any sort of direction indicator, most notably damage indicator or where a grenade has landed etc.

3 Likes

Great job!

1 Like

hope you dont mind, here is a edited version that always points towards a active waypoint on the map
offscreen.lua (3.3 KB)

2 Likes

That is really cool! :fire:What about the performance?

1 Like

The performance is not that great, but not terrible, It is 0.06ms when rendering.

I have made a damage indicator with it (and yes the color is red :smile:)

local damageIndicators = {}
local debugMode = true

local function draw_line(first, second, width, color) 
    local x,y = GetActiveScreenResolution()
    DrawLine_2d(first.x / x, first.y / y, second.x / x, second.y / y, width, color.r, color.g, color.b, color.a)
end

local function sign(x)
    return x > 0 and 1 or x < 0 and -1 or 0
end

local function rotate_point(point, cos, sin)
    return vec2(point.x * cos - point.y * sin, point.x * sin + point.y * cos)
end

local function get_camera_forward()
    local cam_rot = GetFinalRenderedCamRot(2)
    local heading = math.rad(cam_rot.z)
    local pitch = math.rad(cam_rot.x)
    return vec3(-math.sin(heading) * math.cos(pitch), math.cos(heading) * math.cos(pitch), math.sin(pitch))
end

local function draw_triangle(worldVec, width, color, ignoreScreen)
    local on_screen, screen_x, screen_y = GetScreenCoordFromWorldCoord(worldVec.x, worldVec.y, worldVec.z)
    if not on_screen and not ignoreScreen then return end

    local res_x, res_y = GetActiveScreenResolution()
    local res_center_x = res_x * 0.5
    local res_center_y = res_y * 0.5

    local player_position = GetEntityCoords(PlayerPedId())
    local delta = worldVec - player_position
    delta = norm(delta)
    local forward = get_camera_forward()

    local dotted = dot(delta, forward)
    local crossed = cross(delta, forward)
    local angle = math.atan2(#crossed, dotted)
    local sign = sign(crossed.z)

    angle = angle + 1.570796251
    if sign == 1 then angle = math.pi - angle end
    
    local sin_angle = math.sin(angle)
    local cos_angle = math.cos(angle)

    local radius = 120.0
    local triangle_size = 14
    local triangle_center = vec2(res_center_x + radius * cos_angle, res_center_y - radius * sin_angle)

    local triangle_point_one = rotate_point(vec2(1.0, 0.0), cos_angle, -sin_angle) * triangle_size
    local triangle_point_two = rotate_point(vec2(-1.0, 0.75), cos_angle, -sin_angle) * triangle_size
    local triangle_point_three = rotate_point(vec2(-1.0, -0.75), cos_angle, -sin_angle) * triangle_size

    draw_line(triangle_center + triangle_point_one, triangle_center + triangle_point_two, width, color)
    draw_line(triangle_center + triangle_point_two, triangle_center + triangle_point_three, width, color)
    draw_line(triangle_center + triangle_point_three, triangle_center + triangle_point_one, width, color)
end

local function AddDamageIndicator(attacker)
    local attackerPos = GetEntityCoords(attacker)
    table.insert(damageIndicators, {
        position = attackerPos,
        color = {r = 255, g = 0, b = 0, a = 255},
        width = 0.0005,
        timeAdded = GetGameTimer(),
        duration = 2000
    })
end

local function SpawnAttackingPed()
    local playerPed = PlayerPedId()
    local playerCoords = GetEntityCoords(playerPed)
    
    local spawnDistance = 10.0
    local spawnAngle = math.random() * 2 * math.pi
    local spawnX = playerCoords.x + spawnDistance * math.cos(spawnAngle)
    local spawnY = playerCoords.y + spawnDistance * math.sin(spawnAngle)
    
    local pedHash = GetHashKey("s_m_y_swat_01")
    RequestModel(pedHash)
    while not HasModelLoaded(pedHash) do
        Wait(1)
    end
    
    local ped = CreatePed(4, pedHash, spawnX, spawnY, playerCoords.z, 0.0, true, true)
    SetPedArmour(ped, 100)
    SetPedAccuracy(ped, 50)
    SetPedCombatAttributes(ped, 46, true)
    SetPedCombatAbility(ped, 100)
    
    local weaponHash = GetHashKey("WEAPON_PISTOL")
    GiveWeaponToPed(ped, weaponHash, 999, false, true)
    
    TaskCombatPed(ped, playerPed, 0, 16)
    
    Citizen.SetTimeout(10000, function()
        if DoesEntityExist(ped) then
            DeleteEntity(ped)
        end
    end)
end

AddEventHandler('gameEventTriggered', function(name, args)
    if name == "CEventNetworkEntityDamage" then
        local victim = args[1]
        local attacker = args[2]
        local isDead = args[4]
        local weapon = args[7]
        
        if victim == PlayerPedId() and DoesEntityExist(attacker) then
            AddDamageIndicator(attacker)
        end
    end
end)

Citizen.CreateThread(function()
    while true do
        local currentTime = GetGameTimer()
        
        for i = #damageIndicators, 1, -1 do
            local indicator = damageIndicators[i]
            
            local timeAlive = currentTime - indicator.timeAdded
            if timeAlive < indicator.duration then
                indicator.color.a = math.floor(255 * (1 - (timeAlive / indicator.duration)))
                draw_triangle(indicator.position, indicator.width, indicator.color, true)
            else
                table.remove(damageIndicators, i)
            end
        end
        
        Wait(0)
    end
end)

Citizen.CreateThread(function()
    while debugMode do
        SpawnAttackingPed()
        Wait(10000)
    end
end)
1 Like