For background, I am working on a top-down fighting game. I'm trying to create an archer class, in which the arrow sort of predicts the inclines/declines (moving it about the Y-axis [or up/down]). A great example of what I'm trying to accomplish is League of Legend's projectiles: https://gyazo.com/ba1e2e6ead871136bf4faad99287315e

In the gif, you can see the projectile scaling up the wall to match the floor on the incline. If you were at the top (where the projectile went originally) and shot the projectile down, it would do the opposite, sending the projectile down the decline to match the low ground.

I have thought up a couple of solutions, but they all seem extremely expensive (a ton of raycasting to predict the inclines/declines). If anybody has resources on how this could be done or could point me in the right direction, I'd appreciate it.

  • \$\begingroup\$ The height of your map at each point is static, so this could be as simple as an array lookup into a low-resolution heightmap, no? \$\endgroup\$
    – DMGregory
    Commented May 20, 2022 at 20:14
  • \$\begingroup\$ @DMGregory Aye thanks a ton! Great idea. Just got it done :) gyazo.com/cedd496486eb5a0e0dbafc72f5ffe702 \$\endgroup\$
    – foshes
    Commented May 21, 2022 at 0:48
  • \$\begingroup\$ If you've solved your problem, want to write up a description of your solution as an Answer below? \$\endgroup\$
    – DMGregory
    Commented May 21, 2022 at 2:04
  • \$\begingroup\$ @DMGregory Roger that. Thanks again :) \$\endgroup\$
    – foshes
    Commented May 21, 2022 at 16:02

1 Answer 1


Thanks to @DMGregory, they guided me in the right direction and I got it done. Briefly, what I did is this: Every time a new map is created, I also create a heightmap (a 2D array containing all heights of the map). This is done by taking the bounds of the map and firing rays to find the height of specific places around the map. In my case, I know my maps will never be larger than 400x400 (studs is the ROBLOX unit of measurement). Using that, I decide on a resolution-- I used the number '80'. What this means is that I will have 80 different heights across each row of the map. I feel like what I'm writing doesn't make sense, so I created some debugging visuals that might help with understanding.

function MapService.InitMap()
    local origin = SETTINGS.Origin + Vector3.new(0,150,0)
    for i = 0, SETTINGS.Resolution do
        HeightMap[i] = {
        for j = 0, SETTINGS.Resolution do
            local curr = origin + Vector3.new(i, 0, j) * Inc
            local hit = workspace:Raycast(curr, Vector3.new(0,-250,0), params)
-- i will never have a height below -100, so I use that as the 'void' number in the matrix
            HeightMap[i][j] = (if hit ~= nil then hit.Position.Y else -100)

Each square represents a height within the heightmap.

Close up of the actual map's height.

Now that I have that, I need to be able to figure out which cell each XZ position falls into and the Y position at that cell.

local Inc = MapSize/SETTINGS.Resolution

--@ret [y_position of cell], [coordinates of cell in Vector2]
function MapService.PointInHeightMap(position)
   local map_position = position * Vector3.new(1,0,1)
-- math.round is the equivalent of math.floor(n + .5)
   local x, z = math.round(map_position.X/Inc), math.round(map_position.Z/Inc)
   return HeightMap[x][z], Vector2.new(x, z)

Now that we have that, we need to do a problem that sort of reminds me of a coding interview question (LOL). If given an origin and end position, find all possible cells that line between both will cross.

My solution actually misses some cells, but it is accurate enough that I am pleased. The code snippet below includes the solution to make it more accurate if you were curious.

function MapService.PointsInDirection(origin, end_pos)
    local in_order_list = {
    local magnitude = (end_pos - origin).Magnitude -- distance between both
    local u = (end_pos - origin).Unit -- direction vector
    local size_u = u.Magnitude

-- if you wanted to make this more accurate, divide 'u' by any number you'd like. do this at the cost of more operations

    local x = 0
    while magnitude > x do -- every 1 stud, check the cell we're in
        local _, coords = MapService.PointInHeightMap(origin)
        if table.find(in_order_list, coords) == nil then
            table.insert(in_order_list, coords) -- insert if not already in array
        origin += u
        x += size_u
    return in_order_list

Finally, I create the arrow and when I shoot, I calculate the end position. Using our handy function, I'll say MapService.PointsInDirection(origin, end_pos) and in a nutshell, calculate for any 'y' changes about the arrow's path and adjust accordingly. Now, I'm sure you're all waiting for the end result... Here it is. :)

https://gyazo.com/dd303cf4c42014b2d01cccd48fae876f https://gyazo.com/3efc5b2cdd878f43dc9955e1de1015cb https://gyazo.com/38210d0aad290d05ef48c442767afb0f

Some stats for nerds:

The heightmap generates in about .005 seconds (at the creation of each map). Heightmap generating in .0052 seconds

After about 1000 random runs of checking for potential paths of arrows, the average time to find each was .00021 seconds, which I am more than happy with.

Average time to check potential paths.

Again, I appreciate DMGregory for guiding me in the right direction. If anybody has any questions, feel free to ask.


You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .