Skip to content

Commit

Permalink
Quadtrees shapes (#6681)
Browse files Browse the repository at this point in the history
# About the pull request

Additional shapes available for testing against quadtrees or elsewhere:
square, ellipse, circle. Square is a simplified version of the
rectangle, circle is a simplified version of the ellipse.
Cleaned up existing code for shapes (rectangles), should be better
documented and easier to read.
Where possible converted existing RECT usage to SQUARE (everything was
squares anyway).
Where possible loosened defined types to the base `/datum/shape` so
shape types can be swapped as desired.

<!-- 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

Should (currently) have no effect on the game, but paves the way for
more interesting uses for quadtrees. Circular areas of effect, for
instance.
# Testing Photographs and Procedure
Boots. Sentries, flamer sentries, bell towers, flags, sentry laptop,
motion detector, and egg morpher all appear to work as intended.


# Changelog
No player facing changes.
  • Loading branch information
Doubleumc committed Jul 16, 2024
1 parent 073b3b0 commit 6a7fc16
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 84 deletions.
3 changes: 3 additions & 0 deletions code/_macros.dm
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
#define GENERATE_DEBUG_ID "[rand(0, 9)][rand(0, 9)][rand(0, 9)][rand(0, 9)][pick(alphabet_lowercase)][pick(alphabet_lowercase)][pick(alphabet_lowercase)][pick(alphabet_lowercase)]"

#define RECT new /datum/shape/rectangle
#define SQUARE new /datum/shape/rectangle/square
#define ELLIPSE new /datum/shape/ellipse
#define CIRCLE new /datum/shape/ellipse/circle
#define QTREE new /datum/quadtree
#define SEARCH_QTREE(qtree, shape_range, flags) qtree.query_range(shape_range, null, flags)

Expand Down
3 changes: 1 addition & 2 deletions code/controllers/subsystem/sound.dm
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ SUBSYSTEM_DEF(sound)
if(!run_hearers) // Initialize for handling next template
run_hearers = run_queue[run_template] // get base hearers
if(run_template.range) // ranging
var/datum/shape/rectangle/zone = RECT(run_template.x, run_template.y, run_template.range * 2, run_template.range * 2)
run_hearers |= SSquadtree.players_in_range(zone, run_template.z)
run_hearers |= SSquadtree.players_in_range(SQUARE(run_template.x, run_template.y, run_template.range), run_template.z)
if(MC_TICK_CHECK)
return
while(length(run_hearers)) // Output sound to hearers
Expand Down
145 changes: 113 additions & 32 deletions code/datums/quadtree.dm
Original file line number Diff line number Diff line change
Expand Up @@ -49,43 +49,124 @@
..()
return QDEL_HINT_IWILLGC

/datum/shape //Leaving rectangles as a subtype if anyone decides to add circles later
/// A simple geometric shape for testing collisions and intersections. This one is a single point.
/datum/shape
/// Horizontal position of the shape's center point.
var/center_x = 0
/// Vertical position of the shape's center point.
var/center_y = 0
/// Distance from the shape's leftmost to rightmost extent.
var/bounds_x = 0
/// Distance from the shape's topmost to bottommost extent.
var/bounds_y = 0

/datum/shape/proc/intersects()
return
/datum/shape/proc/contains()
return
/datum/shape/New(center_x, center_y)
set_shape(center_x, center_y)

/// Assign shape variables.
/datum/shape/proc/set_shape(center_x, center_y)
src.center_x = center_x
src.center_y = center_y

/// Returns TRUE if the coordinates x, y are in or on the shape, otherwise FALSE.
/datum/shape/proc/contains_xy(x, y)
return center_x == x && center_y == y

/// Returns TRUE if the coord datum is in or on the shape, otherwise FALSE.
/datum/shape/proc/contains_coords(datum/coords/coords)
return contains_xy(coords.x_pos, coords.y_pos)

/// Returns TRUE if the atom is in or on the shape, otherwise FALSE.
/datum/shape/proc/contains_atom(atom/atom)
return contains_xy(atom.x, atom.y)

/// Returns TRUE if this shape's bounding box intersects the provided shape's bounding box, otherwise FALSE. Generally faster than a full intersection test.
/datum/shape/proc/intersects_aabb(datum/shape/aabb)
return (abs(src.center_x - aabb.center_x) <= (src.bounds_x + aabb.bounds_x) * 0.5) && (abs(src.center_y - aabb.center_y) <= (src.bounds_x + aabb.bounds_x) * 0.5)

/// Returns TRUE if this shape intersects the provided rectangle shape, otherwise FALSE.
/datum/shape/proc/intersects_rect(datum/shape/rectangle/rect)
return rect.contains_xy(src.center_x, src.center_y)

/// A simple geometric shape for testing collisions and intersections. This one is an axis-aligned rectangle.
/datum/shape/rectangle
/// Distance from the shape's leftmost to rightmost extent.
var/width = 0
/// Distance from the shape's topmost to bottommost extent.
var/height = 0

/datum/shape/rectangle/New(center_x, center_y, width, height)
set_shape(center_x, center_y, width, height)

/datum/shape/rectangle/set_shape(center_x, center_y, width, height)
..()
src.bounds_x = width
src.bounds_y = height
src.width = width
src.height = height

/datum/shape/rectangle/contains_xy(x, y)
return (abs(center_x - x) <= width * 0.5) && (abs(center_y - y) <= height * 0.5)

/datum/shape/rectangle/intersects_rect(datum/shape/rectangle/rect)
return intersects_aabb(rect)

/// A simple geometric shape for testing collisions and intersections. This one is an axis-aligned square.
/datum/shape/rectangle/square
/// Distance between the shape's opposing extents.
var/length = 0

/datum/shape/rectangle/square/New(center_x, center_y, length)
set_shape(center_x, center_y, length)

/datum/shape/rectangle/square/set_shape(center_x, center_y, length)
..(center_x, center_y, length, length)
src.length = length

/// A simple geometric shape for testing collisions and intersections. This one is an axis-aligned ellipse.
/datum/shape/ellipse
/// Distance from the shape's leftmost to rightmost extent.
var/width = 0
/// Distance from the shape's topmost to bottommost extent.
var/height = 0
VAR_PROTECTED/_axis_x_sq = 0
VAR_PROTECTED/_axis_y_sq = 0

/datum/shape/ellipse/New(center_x, center_y, width, height)
set_shape(center_x, center_y, width, height)

/datum/shape/rectangle/New(x, y, w, h)
/datum/shape/ellipse/set_shape(center_x, center_y, width, height)
..()
center_x = x
center_y = y
width = w
height = h

/datum/shape/rectangle/intersects(datum/shape/rectangle/range)
return !(range.center_x + range.width/2 < center_x - width / 2|| \
range.center_x - range.width/2 > center_x + width / 2|| \
range.center_y + range.height/2 < center_y - height / 2|| \
range.center_y - range.height/2 > center_y + height / 2)

/datum/shape/rectangle/contains(datum/coords/coords)
return (coords.x_pos >= center_x - width / 2 \
&& coords.x_pos <= center_x + width / 2 \
&& coords.y_pos >= center_y - height /2 \
&& coords.y_pos <= center_y + height / 2)

/datum/shape/rectangle/proc/contains_atom(atom/A)
return (A.x >= center_x - width / 2 \
&& A.x <= center_x + width / 2 \
&& A.y >= center_y - height /2 \
&& A.y <= center_y + height / 2)
src.bounds_x = width
src.bounds_y = height
src.width = width
src.height = height
src._axis_x_sq = (width * 0.5)**2
src._axis_y_sq = (height * 0.5)**2

/datum/shape/ellipse/contains_xy(x, y)
return ((center_x - x)**2 / _axis_x_sq + (center_y - y)**2 / _axis_y_sq <= 1)

/datum/shape/ellipse/intersects_rect(datum/shape/rectangle/rect)
if(..())
return TRUE

var/nearest_x = clamp(src.center_x, rect.center_x - rect.width * 0.5, rect.center_x + rect.width * 0.5)
var/nearest_y = clamp(src.center_y, rect.center_y - rect.height * 0.5, rect.center_y + rect.height * 0.5)

return src.contains_xy(nearest_x, nearest_y)

/// A simple geometric shape for testing collisions and intersections. This one is a circle.
/datum/shape/ellipse/circle
/// Distance from the shape's center to edge.
var/radius = 0

/datum/shape/ellipse/circle/New(center_x, center_y, radius)
set_shape(center_x, center_y, radius)

/datum/shape/ellipse/circle/set_shape(center_x, center_y, radius)
..(center_x, center_y, radius * 2, radius * 2)
src.radius = radius

/datum/quadtree/proc/subdivide()
//Warning: this might give you eye cancer
Expand All @@ -96,7 +177,7 @@
is_divided = TRUE

/datum/quadtree/proc/insert_player(datum/coords/qtplayer/p_coords)
if(!boundary.contains(p_coords))
if(!boundary.contains_coords(p_coords))
return FALSE

if(!player_coords)
Expand All @@ -118,11 +199,11 @@
player_coords.Add(p_coords)
return TRUE

/datum/quadtree/proc/query_range(datum/shape/rectangle/range, list/found_players, flags = 0)
/datum/quadtree/proc/query_range(datum/shape/range, list/found_players, flags = 0)
if(!found_players)
found_players = list()
. = found_players
if(!range?.intersects(boundary))
if(!range?.intersects_rect(boundary))
return
if(is_divided)
nw_branch.query_range(range, found_players, flags)
Expand All @@ -136,7 +217,7 @@
continue
if((flags & QTREE_EXCLUDE_OBSERVER) && P.is_observer)
continue
if(range.contains(P))
if(range.contains_coords(P))
if(flags & QTREE_SCAN_MOBS)
found_players.Add(P.player.mob)
else
Expand Down
20 changes: 10 additions & 10 deletions code/game/camera_manager/camera_manager.dm
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/datum/component/camera_manager
var/map_name
var/obj/structure/machinery/camera/current
var/datum/shape/rectangle/current_area
var/datum/shape/current_area
var/atom/movable/screen/map_view/cam_screen
var/atom/movable/screen/background/cam_background
var/list/range_turfs = list()
Expand Down Expand Up @@ -86,7 +86,7 @@
RegisterSignal(parent, COMSIG_CAMERA_UNREGISTER_UI, PROC_REF(unregister))
RegisterSignal(parent, COMSIG_CAMERA_SET_NVG, PROC_REF(enable_nvg))
RegisterSignal(parent, COMSIG_CAMERA_CLEAR_NVG, PROC_REF(disable_nvg))
RegisterSignal(parent, COMSIG_CAMERA_SET_AREA, PROC_REF(set_camera_rect))
RegisterSignal(parent, COMSIG_CAMERA_SET_AREA, PROC_REF(set_camera_area))
RegisterSignal(parent, COMSIG_CAMERA_SET_TARGET, PROC_REF(set_camera))
RegisterSignal(parent, COMSIG_CAMERA_CLEAR, PROC_REF(clear_camera))
RegisterSignal(parent, COMSIG_CAMERA_REFRESH, PROC_REF(refresh_camera))
Expand Down Expand Up @@ -133,18 +133,18 @@
RegisterSignal(current, COMSIG_PARENT_QDELETING, PROC_REF(show_camera_static))
update_target_camera()

/datum/component/camera_manager/proc/set_camera_rect(source, x, y, z, w, h)
/datum/component/camera_manager/proc/set_camera_area(source, datum/shape/new_area, z)
SIGNAL_HANDLER
render_mode = RENDER_MODE_AREA
if(current)
UnregisterSignal(current, COMSIG_PARENT_QDELETING)
current = null
current_area = RECT(x, y, w, h)
target_x = x
target_y = y
current_area = new_area
target_x = current_area.center_x
target_y = current_area.center_y
target_z = z
target_width = w
target_height = h
target_width = current_area.bounds_x
target_height = current_area.bounds_y
update_area_camera()

/datum/component/camera_manager/proc/enable_nvg(source, power, matrixcol)
Expand Down Expand Up @@ -221,8 +221,8 @@
// Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
last_camera_turf = new_location

var/x_size = current_area.width
var/y_size = current_area.height
var/x_size = current_area.bounds_x
var/y_size = current_area.bounds_y
var/turf/target = locate(current_area.center_x, current_area.center_y, target_z)

var/list/visible_things = isXRay ? range("[x_size]x[y_size]", target) : view("[x_size]x[y_size]", target)
Expand Down
3 changes: 1 addition & 2 deletions code/game/machinery/computer/dropship_weapons.dm
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,8 @@
var/obj/structure/machinery/defenses/sentry/defense = sentry.deployed_turret
if(defense.has_camera)
defense.set_range()
var/datum/shape/rectangle/current_bb = defense.range_bounds
camera_area_equipment = sentry
SEND_SIGNAL(src, COMSIG_CAMERA_SET_AREA, current_bb.center_x, current_bb.center_y, defense.loc.z, current_bb.width, current_bb.height)
SEND_SIGNAL(src, COMSIG_CAMERA_SET_AREA, defense.range_bounds, defense.loc.z)
return TRUE

if("clear-camera")
Expand Down
11 changes: 3 additions & 8 deletions code/game/objects/items/devices/motion_detector.dm
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
var/iff_signal = FACTION_MARINE
actions_types = list(/datum/action/item_action)
var/scanning = FALSE // controls if MD is in process of scan
var/datum/shape/rectangle/range_bounds
var/datum/shape/rectangle/square/range_bounds
var/long_range_locked = FALSE //only long-range MD
var/ping_overlay

Expand All @@ -48,7 +48,7 @@

/obj/item/device/motiondetector/Initialize()
. = ..()
range_bounds = new //Just creating a rectangle datum
range_bounds = new //Just creating a square datum
update_icon()

/obj/item/device/motiondetector/Destroy()
Expand Down Expand Up @@ -215,12 +215,7 @@
if(!istype(cur_turf))
return

if(!range_bounds)
range_bounds = new/datum/shape/rectangle
range_bounds.center_x = cur_turf.x
range_bounds.center_y = cur_turf.y
range_bounds.width = detector_range * 2
range_bounds.height = detector_range * 2
range_bounds.set_shape(cur_turf.x, cur_turf.y, detector_range * 2)

var/list/ping_candidates = SSquadtree.players_in_range(range_bounds, cur_turf.z, QTREE_EXCLUDE_OBSERVER | QTREE_SCAN_MOBS)

Expand Down
6 changes: 3 additions & 3 deletions code/modules/cm_aliens/structures/special/egg_morpher.dm
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
var/huggers_to_grow_max = 12
var/huggers_reserved = 0
var/mob/captured_mob
var/datum/shape/rectangle/range_bounds
var/datum/shape/range_bounds

appearance_flags = KEEP_TOGETHER
layer = FACEHUGGER_LAYER

/obj/effect/alien/resin/special/eggmorph/Initialize(mapload, hive_ref)
. = ..()
range_bounds = RECT(x, y, EGGMORPG_RANGE, EGGMORPG_RANGE)
range_bounds = SQUARE(x, y, EGGMORPG_RANGE)

/obj/effect/alien/resin/special/eggmorph/Destroy()
if (stored_huggers && linked_hive)
Expand Down Expand Up @@ -158,7 +158,7 @@

/obj/effect/alien/resin/special/eggmorph/proc/check_facehugger_target()
if(!range_bounds)
range_bounds = RECT(x, y, EGGMORPG_RANGE, EGGMORPG_RANGE)
range_bounds = SQUARE(x, y, EGGMORPG_RANGE)

var/list/targets = SSquadtree.players_in_range(range_bounds, z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
if(isnull(targets) || !length(targets))
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 @@ -257,7 +257,7 @@
STOP_PROCESSING(SSobj, src)
return

var/list/targets = SSquadtree.players_in_range(RECT(M.x, M.y, area_range, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
var/list/targets = SSquadtree.players_in_range(SQUARE(M.x, M.y, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
if(!targets)
return

Expand Down
10 changes: 5 additions & 5 deletions code/modules/defenses/planted_flag.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
desc = "A planted flag with the iconic USCM flag plastered all over it, you feel a burst of energy by its mere sight."
handheld_type = /obj/item/defenses/handheld/planted_flag
disassemble_time = 10
var/datum/shape/rectangle/range_bounds
var/datum/shape/range_bounds
var/area_range = PLANTED_FLAG_RANGE
var/buff_intensity = PLANTED_FLAG_BUFF
health = 200
Expand All @@ -33,7 +33,7 @@
apply_area_effect()
start_processing()

range_bounds = RECT(x, y, PLANTED_FLAG_RANGE, PLANTED_FLAG_RANGE)
range_bounds = SQUARE(x, y, PLANTED_FLAG_RANGE)
update_icon()

/obj/structure/machinery/defenses/planted_flag/Destroy()
Expand Down Expand Up @@ -70,9 +70,9 @@

/obj/structure/machinery/defenses/planted_flag/proc/apply_area_effect()
if(!range_bounds)
range_bounds = RECT(x, y, area_range, area_range)
range_bounds = SQUARE(x, y, area_range)

var/list/targets = SSquadtree.players_in_range(RECT(x, y, area_range, area_range), z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
var/list/targets = SSquadtree.players_in_range(SQUARE(x, y, area_range), z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
if(!targets)
return

Expand Down Expand Up @@ -180,7 +180,7 @@
if(!M.x && !M.y && !M.z)
return

var/list/targets = SSquadtree.players_in_range(RECT(M.x, M.y, area_range, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
var/list/targets = SSquadtree.players_in_range(SQUARE(M.x, M.y, area_range), M.z, QTREE_SCAN_MOBS | QTREE_EXCLUDE_OBSERVER)
targets |= M

for(var/mob/living/carbon/human/H in targets)
Expand Down
Loading

0 comments on commit 6a7fc16

Please sign in to comment.