Skip to content

Commit

Permalink
get_line() refactor (#5452)
Browse files Browse the repository at this point in the history
# About the pull request

The codebase currently has three line-drawing algorithms:
- `get_line()` - never used
- `getline()` - used once
- `getline2()` - used many times

They are all almost identical. There is no real use-case for one over
the the other.

This PR removes `getline()` and `getline2()` and replaces those proc
calls with `get_line()`. Furthermore, `get_line()` is replaced with a
different method using linear interpolation, based on the examples here:
https://www.redblobgames.com/grids/line-drawing/#optimization

The pros of this new method are consistent, reversible projectile paths
(if you can shoot them they can shoot you), and that it is *possibly*
more performant. Bresenham's line algorithm is famously fast -- for the
1960s. This new one looks to be faster on modern computer hardware.
Floats aren't scary anymore.

<!-- Remove this text and explain what the purpose of your PR is.

Mention if you have tested your changes. If you changed a map, make sure
you used the mapmerge tool.
If this is an Issue Correction, you can type "Fixes Issue #169420" to
link the PR to the corresponding Issue number #169420.

Remember: something that is self-evident to you might not be to others.
Explain your rationale fully, even if you feel it goes without saying.
-->

# Explain why it's good for the game

Less duplicated code. Consistent projectile paths.
# Testing Photographs and Procedure
<details>
<summary>Screenshots & Videos</summary>

The functions of interest are:
- `get_line()`, `getline()`, `getline2()` - in our codebase
- `getline_tgmc()` - TGMC's "Reasonably Optimized" version:
https://github.com/tgstation/TerraGov-Marine-Corps/blob/2da5c237640d73e3e66ad79e34861e9682f4609c/code/__HELPERS/unsorted.dm#L816-L869
- `get_line_testA()` - the proposed replacement

![image](https://github.com/cmss13-devs/cmss13/assets/14267245/b12935c2-31ac-4b36-b2ff-fa892472ef94)

The output of `get_line_testA()` is *not* identical to `getline2()`. A
path starting at the center pillar and ending on a tiled floor has
different results.

![image](https://github.com/cmss13-devs/cmss13/assets/14267245/923a77aa-4c6a-4dfa-8147-1001430a0529)

The following examples are a comparison between `getline2()` and
`get_line_testA()`. All paths start at the wall and end at the ghost.
Purple tiles are where both functions picked the same turf, red is
unique to `getline2()`, blue is unique to `get_line_testA()`. Note that
`get_line_testA()` results in the same path taken regardless of
direction.

Example 1a: SW to NE

![image](https://github.com/cmss13-devs/cmss13/assets/14267245/b39dd576-0486-4524-b6ec-eeac1b7fdf52)
Example 1b: NE to SW

![image](https://github.com/cmss13-devs/cmss13/assets/14267245/0994f93d-6cbb-4864-a3b8-40575881f603)

Example 2a: NW to SE

![image](https://github.com/cmss13-devs/cmss13/assets/14267245/f557284b-0e19-4392-921a-adec86fea4cf)
Example 2b: SE to NW

![image](https://github.com/cmss13-devs/cmss13/assets/14267245/1ee59440-d9d7-424b-b3aa-6a6883054fe1)

</details>


# Changelog

:cl:
refactor: projectile paths are the same in both directions, A->B and
B->A
/:cl:
  • Loading branch information
Doubleumc authored Jan 28, 2024
1 parent db084de commit 12f5f35
Show file tree
Hide file tree
Showing 22 changed files with 61 additions and 149 deletions.
47 changes: 0 additions & 47 deletions code/__HELPERS/#maths.dm
Original file line number Diff line number Diff line change
Expand Up @@ -84,53 +84,6 @@ GLOBAL_LIST_INIT(sqrtTable, list(1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4,
return "[round((powerused * 0.000001),0.001)] MW"
return "[round((powerused * 0.000000001),0.0001)] GW"

/**
* Get a list of turfs in a line from `starting_atom` to `ending_atom`.
*
* Uses the ultra-fast [Bresenham Line-Drawing Algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm).
*/
/proc/get_line(atom/starting_atom, atom/ending_atom)
var/current_x_step = starting_atom.x//start at x and y, then add 1 or -1 to these to get every turf from starting_atom to ending_atom
var/current_y_step = starting_atom.y
var/starting_z = starting_atom.z

var/list/line = list(get_turf(starting_atom))//get_turf(atom) is faster than locate(x, y, z)

var/x_distance = ending_atom.x - current_x_step //x distance
var/y_distance = ending_atom.y - current_y_step

var/abs_x_distance = abs(x_distance)//Absolute value of x distance
var/abs_y_distance = abs(y_distance)

var/x_distance_sign = SIGN(x_distance) //Sign of x distance (+ or -)
var/y_distance_sign = SIGN(y_distance)

var/x = abs_x_distance >> 1 //Counters for steps taken, setting to distance/2
var/y = abs_y_distance >> 1 //Bit-shifting makes me l33t. It also makes get_line() unnessecarrily fast.

if(abs_x_distance >= abs_y_distance) //x distance is greater than y
for(var/distance_counter in 0 to (abs_x_distance - 1))//It'll take abs_x_distance steps to get there
y += abs_y_distance

if(y >= abs_x_distance) //Every abs_y_distance steps, step once in y direction
y -= abs_x_distance
current_y_step += y_distance_sign

current_x_step += x_distance_sign //Step on in x direction
line += locate(current_x_step, current_y_step, starting_z)//Add the turf to the list
else
for(var/distance_counter in 0 to (abs_y_distance - 1))
x += abs_x_distance

if(x >= abs_y_distance)
x -= abs_y_distance
current_x_step += x_distance_sign

current_y_step += y_distance_sign
line += locate(current_x_step, current_y_step, starting_z)
return line


///chances are 1:value. anyprob(1) will always return true
/proc/anyprob(value)
return (rand(1,value)==value)
Expand Down
115 changes: 37 additions & 78 deletions code/__HELPERS/unsorted.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1494,88 +1494,47 @@ GLOBAL_LIST_INIT(WALLITEMS, list(
/proc/format_text(text)
return replacetext(replacetext(text,"\proper ",""),"\improper ","")

/proc/getline(atom/M, atom/N, include_from_atom = TRUE)//Ultra-Fast Bresenham Line-Drawing Algorithm
var/px=M.x //starting x
var/py=M.y
var/line[] = list(locate(px,py,M.z))
var/dx=N.x-px //x distance
var/dy=N.y-py
var/dxabs=abs(dx)//Absolute value of x distance
var/dyabs=abs(dy)
var/sdx=sign(dx) //Sign of x distance (+ or -)
var/sdy=sign(dy)
var/x=dxabs>>1 //Counters for steps taken, setting to distance/2
var/y=dyabs>>1 //Bit-shifting makes me l33t. It also makes getline() unnessecarrily fast.
var/j //Generic integer for counting
if(dxabs>=dyabs) //x distance is greater than y
for(j=0;j<dxabs;j++)//It'll take dxabs steps to get there
y+=dyabs
if(y>=dxabs) //Every dyabs steps, step once in y direction
y-=dxabs
py+=sdy
px+=sdx //Step on in x direction
if(j > 0 || include_from_atom)
line+=locate(px,py,M.z)//Add the turf to the list
else
for(j=0;j<dyabs;j++)
x+=dxabs
if(x>=dyabs)
x-=dyabs
px+=sdx
py+=sdy
if(j > 0 || include_from_atom)
line+=locate(px,py,M.z)
return line
/**
* Get a list of turfs in a line from `start_atom` to `end_atom`.
*
* Based on a linear interpolation method from [Red Blob Games](https://www.redblobgames.com/grids/line-drawing/#optimization).
*
* Arguments:
* * start_atom - starting point of the line
* * end_atom - ending point of the line
* * include_start_atom - when truthy includes start_atom in the list, default TRUE
*
* Returns:
* list - turfs from start_atom (in/exclusive) to end_atom (inclusive)
*/
/proc/get_line(atom/start_atom, atom/end_atom, include_start_atom = TRUE)
var/turf/start_turf = get_turf(start_atom)
var/turf/end_turf = get_turf(end_atom)
var/start_z = start_turf.z

//Bresenham's algorithm. This one deals efficiently with all 8 octants.
//Just don't ask me how it works.
/proc/getline2(atom/from_atom, atom/to_atom, include_from_atom = TRUE)
if(!from_atom || !to_atom) return 0
var/list/turf/turfs = list()

var/cur_x = from_atom.x
var/cur_y = from_atom.y

var/w = to_atom.x - from_atom.x
var/h = to_atom.y - from_atom.y
var/dx1 = 0
var/dx2 = 0
var/dy1 = 0
var/dy2 = 0
if(w < 0)
dx1 = -1
dx2 = -1
else if(w > 0)
dx1 = 1
dx2 = 1
if(h < 0) dy1 = -1
else if(h > 0) dy1 = 1
var/longest = abs(w)
var/shortest = abs(h)
if(!(longest > shortest))
longest = abs(h)
shortest = abs(w)
if(h < 0) dy2 = -1
else if (h > 0) dy2 = 1
dx2 = 0

var/numerator = longest >> 1
var/i
for(i = 0; i <= longest; i++)
if(i > 0 || include_from_atom)
turfs += locate(cur_x,cur_y,from_atom.z)
numerator += shortest
if(!(numerator < longest))
numerator -= longest
cur_x += dx1
cur_y += dy1
else
cur_x += dx2
cur_y += dy2
var/list/line = list()
if(include_start_atom)
line += start_turf

var/step_count = get_dist(start_turf, end_turf)
if(!step_count)
return line

return turfs
//as step_count and step size (1) are known can pre-calculate a lerp step, tiny number (1e-5) for rounding consistency
var/step_x = (end_turf.x - start_turf.x) / step_count + 1e-5
var/step_y = (end_turf.y - start_turf.y) / step_count + 1e-5

//locate() truncates the fraction, adding 0.5 so its effectively rounding to nearest coords for free
var/x = start_turf.x + 0.5
var/y = start_turf.y + 0.5
for(var/step in 1 to (step_count - 1)) //increment then locate() skips start_turf (in 1), since end_turf is known can skip that step too (step_count - 1)
x += step_x
y += step_y
line += locate(x, y, start_z)

line += end_turf

return line

//Key thing that stops lag. Cornerstone of performance in ss13, Just sitting here, in unsorted.dm.

Expand Down
2 changes: 1 addition & 1 deletion code/_onclick/adjacent.dm
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ Quick adjacency (to turf):

var/turf/curT = get_turf(A)
var/is_turf = isturf(A)
for(var/turf/T in getline2(A, src))
for(var/turf/T in get_line(A, src))
if(curT == T)
continue
if(T.density)
Expand Down
2 changes: 1 addition & 1 deletion code/game/objects/items/devices/binoculars.dm
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
to_chat(user, SPAN_WARNING("INVALID TARGET: target must be on the surface."))
return FALSE
if(user.sight & SEE_TURFS)
var/list/turf/path = getline2(user, targeted_atom, include_from_atom = FALSE)
var/list/turf/path = get_line(user, targeted_atom, include_start_atom = FALSE)
for(var/turf/T in path)
if(T.opacity)
to_chat(user, SPAN_WARNING("There is something in the way of the laser!"))
Expand Down
2 changes: 1 addition & 1 deletion code/game/objects/items/hoverpack.dm
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
var/t_dist = get_dist(user, t_turf)
if(!(t_dist > max_distance))
return
var/list/turf/path = getline2(user, t_turf, FALSE)
var/list/turf/path = get_line(user, t_turf, FALSE)
warning.forceMove(path[max_distance])

/obj/item/hoverpack/proc/can_use_hoverpack(mob/living/carbon/human/user)
Expand Down
2 changes: 1 addition & 1 deletion code/modules/cm_aliens/XenoStructures.dm
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@
var/turf/last_turf = loc
var/atom/temp_atom = new acid_type()
var/current_pos = 1
for(var/i in getline(src, current_mob))
for(var/i in get_line(src, current_mob))
current_turf = i
if(LinkBlocked(temp_atom, last_turf, current_turf))
qdel(temp_atom)
Expand Down
2 changes: 1 addition & 1 deletion code/modules/defenses/bell_tower.dm
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
if(M.get_target_lock(faction))
return

var/list/turf/path = getline2(src, linked_bell, include_from_atom = TRUE)
var/list/turf/path = get_line(src, linked_bell)
for(var/turf/PT in path)
if(PT.density)
return
Expand Down
2 changes: 1 addition & 1 deletion code/modules/defenses/sentry.dm
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@
targets.Remove(A)
continue

var/list/turf/path = getline2(src, A, include_from_atom = FALSE)
var/list/turf/path = get_line(src, A, include_start_atom = FALSE)
if(!path.len || get_dist(src, A) > sentry_range)
if(A == target)
target = null
Expand Down
2 changes: 1 addition & 1 deletion code/modules/defenses/tesla_coil.dm
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
if(!istype(M))
return FALSE

var/list/turf/path = getline2(src, M, include_from_atom = FALSE)
var/list/turf/path = get_line(src, M, include_start_atom = FALSE)

var/blocked = FALSE
for(var/turf/T in path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
var/range = base_range + stacks*range_per_stack
var/damage = base_damage + stacks*damage_per_stack
var/turfs_visited = 0
for (var/turf/turf in getline2(get_turf(xeno), affected_atom))
for (var/turf/turf in get_line(get_turf(xeno), affected_atom))
if(turf.density || turf.opacity)
break

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@

// Build our list of target turfs based on
if (spray_type == ACID_SPRAY_LINE)
X.do_acid_spray_line(getline2(X, A, include_from_atom = FALSE), spray_effect_type, spray_distance)
X.do_acid_spray_line(get_line(X, A, include_start_atom = FALSE), spray_effect_type, spray_distance)

else if (spray_type == ACID_SPRAY_CONE)
X.do_acid_spray_cone(get_turf(A), spray_effect_type, spray_distance)
Expand Down Expand Up @@ -929,7 +929,7 @@
if(distance > 2)
return FALSE

var/list/turf/path = getline2(stabbing_xeno, targetted_atom, include_from_atom = FALSE)
var/list/turf/path = get_line(stabbing_xeno, targetted_atom, include_start_atom = FALSE)
for(var/turf/path_turf as anything in path)
if(path_turf.density)
to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
if(distance > 2)
return

var/list/turf/path = getline2(xeno, hit_target, include_from_atom = FALSE)
var/list/turf/path = get_line(xeno, hit_target, include_start_atom = FALSE)
for(var/turf/path_turf as anything in path)
if(path_turf.density)
to_chat(xeno, SPAN_WARNING("There's something blocking us from striking!"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
return

//X = xeno user, A = target atom
var/list/turf/target_turfs = getline2(source_xeno, targetted_atom, include_from_atom = FALSE)
var/list/turf/target_turfs = get_line(source_xeno, targetted_atom, include_start_atom = FALSE)
var/length_of_line = LAZYLEN(target_turfs)
if(length_of_line > 3)
target_turfs = target_turfs.Copy(1, 4)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@
if(A.z != M.z)
return FALSE

var/list/turf/path = getline2(M, A, include_from_atom = FALSE)
var/list/turf/path = get_line(M, A, include_start_atom = FALSE)
var/distance = 0
for(var/turf/T in path)
if(distance >= max_distance)
Expand Down
2 changes: 1 addition & 1 deletion code/modules/movement/launching/launching.dm
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
add_temp_pass_flags(pass_flags)

var/turf/start_turf = get_step_towards(src, LM.target)
var/list/turf/path = getline2(start_turf, LM.target)
var/list/turf/path = get_line(start_turf, LM.target)
var/last_loc = loc

var/early_exit = FALSE
Expand Down
2 changes: 1 addition & 1 deletion code/modules/projectiles/gun_attachables.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2941,7 +2941,7 @@ Defined in conflicts.dm of the #defines folder.

/obj/item/attachable/attached_gun/flamer/proc/unleash_flame(atom/target, mob/living/user)
set waitfor = 0
var/list/turf/turfs = getline2(user,target)
var/list/turf/turfs = get_line(user,target)
var/distance = 0
var/turf/prev_T
var/stop_at_turf = FALSE
Expand Down
2 changes: 1 addition & 1 deletion code/modules/projectiles/guns/flamer/flamer.dm
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
if(R.rangefire == -1)
max_range = current_mag.reagents.max_fire_rad

var/turf/temp[] = getline2(get_turf(user), get_turf(target))
var/turf/temp[] = get_line(get_turf(user), get_turf(target))

var/turf/to_fire = temp[2]

Expand Down
6 changes: 3 additions & 3 deletions code/modules/projectiles/guns/flamer/flameshape.dm
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@

for(var/dirn in dirs)
var/endturf = get_ranged_target_turf(F, dirn, fire_spread_amount)
var/list/turfs = getline2(source_turf, endturf)
var/list/turfs = get_line(source_turf, endturf)

var/turf/prev_T = source_turf
for(var/turf/T in turfs)
Expand Down Expand Up @@ -124,7 +124,7 @@
var/distance = 1
var/stop_at_turf = FALSE

var/list/turfs = getline2(source_turf, F.target_clicked)
var/list/turfs = get_line(source_turf, F.target_clicked)
for(var/turf/T in turfs)
if(istype(T, /turf/open/space))
break
Expand Down Expand Up @@ -174,7 +174,7 @@
user = F.weapon_cause_data.resolve_mob()

var/unleash_dir = user.dir
var/list/turf/turfs = getline2(F, F.target_clicked)
var/list/turf/turfs = get_line(F, F.target_clicked)
var/distance = 1
var/hit_dense_atom_mid = FALSE
var/turf/prev_T = user.loc
Expand Down
2 changes: 1 addition & 1 deletion code/modules/projectiles/guns/shotguns.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,7 @@ can cause issues with ammo types getting mixed up during the burst.
if(!T) //Off edge of map.
throw_turfs.Remove(T)
continue
var/list/turf/path = getline2(get_step_towards(src, T), T) //Same path throw code will calculate from.
var/list/turf/path = get_line(get_step_towards(src, T), T) //Same path throw code will calculate from.
if(!path.len)
throw_turfs.Remove(T)
continue
Expand Down
2 changes: 1 addition & 1 deletion code/modules/projectiles/guns/smartgun.dm
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@
if((angledegree*2) > angle_list[angle])
continue

path = getline2(user, M)
path = get_line(user, M)

if(path.len)
var/blocked = FALSE
Expand Down
2 changes: 1 addition & 1 deletion code/modules/projectiles/guns/specialist/sniper.dm
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@
return TRUE

/datum/action/item_action/specialist/aimed_shot/proc/check_shot_is_blocked(mob/firer, mob/target, obj/projectile/P)
var/list/turf/path = getline2(firer, target, include_from_atom = FALSE)
var/list/turf/path = get_line(firer, target, include_start_atom = FALSE)
if(!path.len || get_dist(firer, target) > P.ammo.max_range)
return TRUE

Expand Down
4 changes: 2 additions & 2 deletions code/modules/projectiles/projectile.dm
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@
if(ammo.bonus_projectiles_amount && ammo.bonus_projectiles_type)
ammo.fire_bonus_projectiles(src)

path = getline2(starting, target_turf)
path = get_line(starting, target_turf)
p_x += clamp((rand()-0.5)*scatter*3, -8, 8)
p_y += clamp((rand()-0.5)*scatter*3, -8, 8)
update_angle(starting, target_turf)
Expand Down Expand Up @@ -381,7 +381,7 @@

/obj/projectile/proc/retarget(atom/new_target, keep_angle = FALSE)
var/turf/current_turf = get_turf(src)
path = getline2(current_turf, new_target)
path = get_line(current_turf, new_target)
path.Cut(1, 2) // remove the turf we're already on
var/atom/source = keep_angle ? original : current_turf
update_angle(source, new_target)
Expand Down

0 comments on commit 12f5f35

Please sign in to comment.