[Solved] Pointing at an object that's out of sight

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
Gunroar:Cannon()
Party member
Posts: 200
Joined: Thu Dec 10, 2020 1:57 am

[Solved] Pointing at an object that's out of sight

Post by Gunroar:Cannon() »

How(what's the logic) do you draw an arrow, at the edge of the screen, to point at an object (in a 2d game) that can't be seen in the screen, like another player or a power up.
Last edited by Gunroar:Cannon() on Fri Jan 08, 2021 10:21 pm, edited 1 time in total.
me: I don't always code but when I do it's done flawlessly.
also me:

Code: Select all

 function Gunroar:Cannon()
    for x, enemy in ipairs(self.allEnemies) do
        self:Cannon(enemy)
    end
end

Code: Select all

Lua Error: [file Gunroar.lua]:18: C stack overflow
Xugro
Citizen
Posts: 89
Joined: Wed Sep 29, 2010 8:14 pm

Re: Pointing at an object that's out of sight

Post by Xugro »

There are many ways to do this. It depends on what you want. I can see two possibilities:
  1. A simple approach, where you just point in the approximate direction:
    simple.jpg
    simple.jpg (132.57 KiB) Viewed 4169 times
    Hereby you determine in which part of the playing field the object/enemy (X) is relative to the player (P): the number shown. Here the screen of the player is the shown by thick black lines. If it is above (2), below (8), to the left (4) or the right (6) just draw an arrow at the position of the object on the border (example in light blue). If the object/enemy is in one of the other parts of the playing field (1, 3, 7 or 9), just draw an arrow at the corresponding corner (example in purple).
  2. A complex approach, where you point directly at the object with the correct angle:
    complex.jpg
    complex.jpg (126.36 KiB) Viewed 4169 times
    Here you have to calculate the angle between the player (P) and the enemy/object (X) and the intersection point between the screen border (thick black lines) and the line from player to enemy/object (pencil). Then you can draw the arrow at the right position with the correct angle. Hereby the angle is the easy part, but the calculation the intersection is more difficult. I need about a page of math to get a formula for calculation the intersection point. And the formula won't look pretty.
Attachments
simple.jpg
simple.jpg (132.57 KiB) Viewed 4169 times
User avatar
Gunroar:Cannon()
Party member
Posts: 200
Joined: Thu Dec 10, 2020 1:57 am

Re: Pointing at an object that's out of sight

Post by Gunroar:Cannon() »

Thnx, I want 2. Don't you use something like math.atan2(obj.y-p.y,obj.x-p.x)?
me: I don't always code but when I do it's done flawlessly.
also me:

Code: Select all

 function Gunroar:Cannon()
    for x, enemy in ipairs(self.allEnemies) do
        self:Cannon(enemy)
    end
end

Code: Select all

Lua Error: [file Gunroar.lua]:18: C stack overflow
Xugro
Citizen
Posts: 89
Joined: Wed Sep 29, 2010 8:14 pm

Re: Pointing at an object that's out of sight

Post by Xugro »

Yes, that is the formula to calculate the angle. But that is the easy part. Calculating the point on the border is harder. But I found a simpler in my sleep (simpler than what I thought of yesterday):
calculate_intersection.jpg
calculate_intersection.jpg (143.93 KiB) Viewed 4119 times
  1. The first step is to find out which screen-border the pointer needs to be. This can be done by calculating the angles of the screen-corners (e.g. alpha and gamma) and compare them to the angle between player and object (beta). In my case this is the lower screen border (since alpha <= beta <= gamma). With this you know the "height" (y-value) of the lower screen border (I_y in my notation).
  2. The second step is to calculate the x-value of the intersection (I_x). This can be done with simple trigonometry (see my sketch).
With the angle beta and the point of intersection I you can draw a pointer to the object.
User avatar
pgimeno
Party member
Posts: 2545
Joined: Sun Oct 18, 2015 2:58 pm

Re: Pointing at an object that's out of sight

Post by pgimeno »

Dealing with angles is prone to bugs due to lack of handling special cases, depending on the signs of the angles. This task can be solved by casting 4 rays, each to an axis-aligned line. The formula for line-line intersection is very simple when one of the lines is axis-aligned.

Problem: calculate the intersection between a line of the form v*t + p (where v is a vector from the player to the enemy, p is the player position and t is an arbitrary number; each value of t yields a point in the line) and either a vertical line (of the form x = constant) or a horizontal one (of the form y = constant). We need to find the point at which one line intersects the other, that is, we need to find the value of t that gives the point where the lines meet. We then have a linear equation which is quickly solved:

vx*t + px = x (for vertical line) -> t = (x - px) / vx
vy*t + py = y (for horizontal line) -> t = (y - py) / vy

Do that for each of the four lines, and take the smallest positive t of all four. Plug the obtained t into v*t + p and you will have the missing coordinate.

But if the arrow is an image, you still need to use atan2 to find the angle with which to draw it, because Löve doesn't allow specifying the rotation as a matrix for drawing, you need an angle. Furthermore, the image needs to rotate over its centre, and the lines must be at a certain distance from the border (half the image's width and height).

PoC attached. Mouse wheel to zoom out, LMB to place player, RMB to place enemy. The algorithm fails when the player is not in the screen, due to the condition that t must be positive, but that shouldn't be a problem.
Attachments
arrow-to-enemy.love
(1.49 KiB) Downloaded 120 times
User avatar
Gunroar:Cannon()
Party member
Posts: 200
Joined: Thu Dec 10, 2020 1:57 am

Re: Pointing at an object that's out of sight

Post by Gunroar:Cannon() »

Thnxs alot, Xugro for your input and help! And your example wrapped it all up and fixed my problem, pgimeno, thnx!! I definitely couldn't have done it myself :P.
me: I don't always code but when I do it's done flawlessly.
also me:

Code: Select all

 function Gunroar:Cannon()
    for x, enemy in ipairs(self.allEnemies) do
        self:Cannon(enemy)
    end
end

Code: Select all

Lua Error: [file Gunroar.lua]:18: C stack overflow
RNavega
Citizen
Posts: 69
Joined: Sun Aug 16, 2020 1:28 pm

Re: Pointing at an object that's out of sight

Post by RNavega »

pgimeno wrote: Thu Jan 07, 2021 2:37 pm This task can be solved by casting 4 rays, each to an axis-aligned line. The formula for line-line intersection is very simple when one of the lines is axis-aligned.
That's a cool solution pgimeno, very fast and elegant.
The method I came up with when thinking of this is more verbose and I think slower aswell, using the point-slope form of the line equation to get the intersections, but I wanted to put it to practice anyway. The code is inline as I made the arrow with a mesh object so it can be standalone:

Code: Select all

local arrow
local arrowHalfW, arrowHalfH
local zoom = 0.75

local playerX, playerY, playerW, playerH = 100, 200, 20, 40
local enemyX, enemyY, enemyW, enemyH = 300, 400, 20, 40
local isEnemyOff = false
local arrowPosX, arrowPosY

function love.load()
    arrow = love.graphics.newMesh(
      {{'VertexPosition', 'float', 2}},
      {{0, 10}, {15, 10}, {15, 20}, {15, 20}, {0, 20}, {0, 10}, {15, 0}, {30, 15}, {15, 30}},
      'triangles',
      'static'
    )
    arrowHalfW = 15
    arrowHalfH = 15
end


function love.wheelmoved(x, y)
    zoom = zoom * 1.1 ^ y
    if zoom > 1 then zoom = 1 end
end


-- Using the point-slope method, by RNavega.
function love.update(dt)
  local screenW, screenH = love.graphics.getDimensions()
  isEnemyOff = enemyX < 0 or enemyX > screenW
            or enemyY < 0 or enemyY > screenH

  if isEnemyOff then
    local dx, dy = enemyX - playerX, enemyY - playerY

    if dx == 0.0 then -- Happens when (enemyX == playerX).
      arrowPosX = enemyX
      arrowPosY = (enemyY > screenH) and (screenH - arrowHalfH) or arrowHalfH

    elseif dy == 0.0 then -- Happens when (enemyY == playerY).
      arrowPosX = (enemyX > screenW) and (screenW - arrowHalfW) or arrowHalfW
      arrowPosY = enemyY

    else
      -- The point-slope equation of a line:
      -- y - y1 = m * (x - x1)

      local m = dy / dx

      -- We actually test intersections with the edges of a smaller screen, padded by
      -- arrowHalfW horizontally and arrowHalfH vertically.
      local screenRightX = screenW - arrowHalfW
      local screenBottomY = screenH - arrowHalfH

      -- Isolating 'y' for use with the vertical edges:
      -- y = m * (x - x1) + y1

      if enemyX < arrowHalfW then
        -- Left edge.
        local intersectY = m * (arrowHalfW - playerX) + playerY
        if intersectY >= arrowHalfH and intersectY <= screenBottomY then
          arrowPosX = arrowHalfW
          arrowPosY = intersectY
        end
      elseif enemyX > screenRightX then
        -- Right edge.
        local intersectY = m * (screenRightX - playerX) + playerY
        if intersectY >= arrowHalfH and intersectY <= screenBottomY then
          arrowPosX = screenRightX
          arrowPosY = intersectY
        end
      end

      -- Isolating 'x' for use with the horizontal edges:
      -- x = (y - y1 + m*x1) / m

      if enemyY < arrowHalfH then
        -- Top edge.
        local intersectX = (arrowHalfH - playerY + m * playerX) / m
        if intersectX >= arrowHalfW and intersectX <= screenRightX then
          arrowPosX = intersectX
          arrowPosY = arrowHalfH
        end
      elseif enemyY > screenBottomY then
        -- Bottom edge.
        local intersectX = (screenBottomY - playerY + m * playerX) / m
        if intersectX >= arrowHalfW and intersectX <= screenRightX then
          arrowPosX = intersectX
          arrowPosY = screenBottomY
        end
      end
    end
  end
end


-- Using 2D vectors, by pgimeno.
function love.updateOriginal(dt)
    isEnemyOff = enemyX < 0 or enemyX >= love.graphics.getWidth()
              or enemyY < 0 or enemyY >= love.graphics.getHeight()
    if isEnemyOff then
      local screenW, screenH = love.graphics.getDimensions()
      local vx, vy = enemyX - playerX, enemyY - playerY
      local t, minT
      minT = math.huge
      t = (arrowHalfW - playerX) / vx
      if t == t and t > 0 and t < minT then
        minT = t
        arrowPosX = arrowHalfW
        arrowPosY = vy * t + playerY
      end
      t = (arrowHalfH - playerY) / vy
      if t == t and t > 0 and t < minT then
        minT = t
        arrowPosX = vx * t + playerX
        arrowPosY = arrowHalfH
      end
      t = (screenW - arrowHalfW - playerX) / vx
      if t == t and t > 0 and t < minT then
        minT = t
        arrowPosX = screenW - arrowHalfW
        arrowPosY = vy * t + playerY
      end
      t = (screenH - arrowHalfH - playerY) / vy
      if t > 0 and t < minT then
        -- minT = t -- doesn't matter anymore
        arrowPosX = vx * t + playerX
        arrowPosY = screenH - arrowHalfH
      end
    end
end

function love.draw()
    local screenW, screenH = love.graphics.getDimensions()
    love.graphics.translate(screenW/2, screenH/2)
    love.graphics.scale(zoom)
    love.graphics.translate(screenW/-2, screenH/-2)

    -- Draw screen border
    love.graphics.setLineWidth(1/zoom)
    love.graphics.rectangle("line", -.5, -.5, screenW+1, screenH+1)

    -- Draw player and enemy
    love.graphics.setColor(0, 1, 1)
    love.graphics.rectangle("fill", playerX - playerW/2, playerY - playerH/2, playerW, playerH)
    love.graphics.setColor(1, 0, 0)
    love.graphics.rectangle("fill", enemyX - enemyW/2, enemyY - enemyH/2, enemyW, enemyH)

    -- Draw arrow if visible
    if isEnemyOff then
      love.graphics.setColor(1, 1, 0)
      love.graphics.draw(arrow, arrowPosX, arrowPosY,
        math.atan2(enemyY - playerY, enemyX - playerX),
        1, 1, arrowHalfW, arrowHalfH)
    end

    -- Restore colour
    love.graphics.setColor(1, 1, 1)
end


function love.mousemoved(x, y)
    -- transform screen to zoomed coordinates
    x = (x - love.graphics.getWidth() / 2) / zoom + love.graphics.getWidth() / 2
    y = (y - love.graphics.getHeight() / 2) / zoom + love.graphics.getHeight() / 2

    if love.mouse.isDown(1) then
      playerX = x
      playerY = y
    elseif love.mouse.isDown(2) then
      enemyX = x
      enemyY = y
    end
end
love.mousepressed = love.mousemoved
love.mousereleased = love.mousemoved


function love.keypressed(key)
  if key == 'escape' then
    love.event.quit()
  end
end
RNavega
Citizen
Posts: 69
Joined: Sun Aug 16, 2020 1:28 pm

Re: [Solved] Pointing at an object that's out of sight

Post by RNavega »

...I should add, I had the idea while looking at the diagrams drawn by Xugro above, so thanks for that!
User avatar
ivan
Party member
Posts: 1719
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: [Solved] Pointing at an object that's out of sight

Post by ivan »

I have dealt with this issue before and the easiest way is to use my algorithm for SegmentVSAABB:
https://2dengine.com/?p=intersections#section_15
The segment is the ray from the center of the screen to the target point and the AABB is defined by the edges of the screen.
User avatar
pgimeno
Party member
Posts: 2545
Joined: Sun Oct 18, 2015 2:58 pm

Re: Pointing at an object that's out of sight

Post by pgimeno »

I forgot one 't == t' (to discard this edge when t is NaN) in the condition for the last screen border. The last lines in love.update should be like this:

Code: Select all

    t = (screenH - arrowHalfH - playerY) / vy
    if t == t and t > 0 and t < minT then --<<-- 't == t' was missing here ***
      -- minT = t -- doesn't matter anymore
      arrowPosX = vx * t + playerX
      arrowPosY = screenH - arrowHalfH
    end
  end
end
[edited to remove excess pickiness]
Post Reply

Who is online

Users browsing this forum: No registered users and 32 guests