Page 1 of 2

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

Posted: Wed Jan 06, 2021 10:47 pm
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.

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

Posted: Wed Jan 06, 2021 11:50 pm
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 6582 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 6582 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.

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

Posted: Thu Jan 07, 2021 9:55 am
by Gunroar:Cannon()
Thnx, I want 2. Don't you use something like math.atan2(obj.y-p.y,obj.x-p.x)?

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

Posted: Thu Jan 07, 2021 11:58 am
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 6532 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.

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

Posted: Thu Jan 07, 2021 2:37 pm
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.

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

Posted: Fri Jan 08, 2021 10:20 pm
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.

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

Posted: Sat Jan 09, 2021 6:09 am
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

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

Posted: Sat Jan 09, 2021 6:10 am
by RNavega
...I should add, I had the idea while looking at the diagrams drawn by Xugro above, so thanks for that!

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

Posted: Sat Jan 09, 2021 10:41 am
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.

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

Posted: Sat Jan 09, 2021 12:34 pm
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]