Predicting Inclines and Declines for projectiles

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.

• 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? Commented May 20, 2022 at 20:14
• @DMGregory Aye thanks a ton! Great idea. Just got it done :) gyazo.com/cedd496486eb5a0e0dbafc72f5ffe702 Commented May 21, 2022 at 0:48
• If you've solved your problem, want to write up a description of your solution as an Answer below? Commented May 21, 2022 at 2:04
• @DMGregory Roger that. Thanks again :) Commented May 21, 2022 at 16:02

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)
end
end
end


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)
end


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
end

origin += u
x += size_u
end

return in_order_list
end


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. :)

Some stats for nerds:

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

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.

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

• Nice thorough answer! I appreciate you not assuming my pronouns. You can find them in my profile though - I use he/him/his. 😉 Commented May 21, 2022 at 16:40