Skip to content

Commit

Permalink
projectiles v8 - raycasting (#6544)
Browse files Browse the repository at this point in the history
Atomizing the physics solver out of #5934 

Tldr this is way less overhead, complicated, and far more accurate than
the old one
  • Loading branch information
silicons committed Jul 5, 2024
1 parent 62c23a2 commit 50371db
Show file tree
Hide file tree
Showing 47 changed files with 1,191 additions and 405 deletions.
2 changes: 1 addition & 1 deletion citadel.dme
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@
#include "code\__HELPERS\files\paths.dm"
#include "code\__HELPERS\files\walk.dm"
#include "code\__HELPERS\game\depth.dm"
#include "code\__HELPERS\game\turfs\line.dm"
#include "code\__HELPERS\game\turfs\offsets.dm"
#include "code\__HELPERS\graphs\astar.dm"
#include "code\__HELPERS\icons\alpha.dm"
Expand Down Expand Up @@ -412,7 +413,6 @@
#include "code\__HELPERS\math\angle.dm"
#include "code\__HELPERS\math\distance.dm"
#include "code\__HELPERS\math\fractions.dm"
#include "code\__HELPERS\math\multipliers.dm"
#include "code\__HELPERS\matrices\color_matrix.dm"
#include "code\__HELPERS\matrices\transform_matrix.dm"
#include "code\__HELPERS\misc\sonar.dm"
Expand Down
5 changes: 5 additions & 0 deletions code/__DEFINES/_flags/atom_flags.dm
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,16 @@ DEFINE_BITFIELD(atom_flags, list(
#define MOVABLE_NO_THROW_DAMAGE_SCALING (1<<1)
/// Do not spin when thrown.
#define MOVABLE_NO_THROW_SPIN (1<<2)
/// We are currently about to be yanked by a Moved() triggering a Move()
///
/// * used so things like projectile hitscans know to yield
#define MOVABLE_IN_MOVED_YANK (1<<3)

DEFINE_BITFIELD(movable_flags, list(
BITFIELD(MOVABLE_NO_THROW_SPEED_SCALING),
BITFIELD(MOVABLE_NO_THROW_DAMAGE_SCALING),
BITFIELD(MOVABLE_NO_THROW_SPIN),
BITFIELD(MOVABLE_IN_MOVED_YANK),
))

// Flags for pass_flags. - Used in /atom/movable/var/pass_flags, and /atom/var/pass_flags_self
Expand Down
7 changes: 4 additions & 3 deletions code/__DEFINES/math.dm
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
#define WRAP(val, min, max) ( min == max ? min : (val) - (round(((val) - (min))/((max) - (min))) * ((max) - (min))) )

// Real modulus that handles decimals
#define MODULUS(x, y) ( (x) - (y) * round((x) / (y)) )
#define MODULUS_F(x, y) ( (x) - (y) * round((x) / (y)) )

// Cotangent
#define COT(x) (1 / tan(x))
Expand Down Expand Up @@ -128,12 +128,13 @@

// Will filter out extra rotations and negative rotations
// E.g: 540 becomes 180. -180 becomes 180.
#define SIMPLIFY_DEGREES(degrees) (MODULUS((degrees), 360))
#define SIMPLIFY_DEGREES(degrees) (MODULUS_F((degrees), 360))

#define GET_ANGLE_OF_INCIDENCE(face, input) (MODULUS((face) - (input), 360))
#define GET_ANGLE_OF_INCIDENCE(face, input) (MODULUS_F((face) - (input), 360))

//Finds the shortest angle that angle A has to change to get to angle B. Aka, whether to move clock or counterclockwise.
/proc/closer_angle_difference(a, b)
// todo: optimize this shit
if(!isnum(a) || !isnum(b))
return
a = SIMPLIFY_DEGREES(a)
Expand Down
205 changes: 205 additions & 0 deletions code/__HELPERS/game/turfs/line.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 silicons *//

/**
* line drawing algorithm
*
* basically, takes one pixel,
* and returns all turfs it touches as it passes through a certain angle
* for a certain number of pixels
*
* we can't use DDA or Bresenham because it's, ironically, not as accurate for
* our purposes.
*
* we have to bias full diagonal's (45, 135, 225, 315) to one side
*
* todo: make sure this is consistent with how byond would behave if using step; or not, i don't care lmao
*
* @params
* * starting - starting turf
* * starting_px - pixel x on starting turf. this is not pixel_x on atom, 1 is the bottomleft, 32 is topright.
* * starting_py - pixel y on starting turf. this is not pixel_y on atom. 1 is bottomleft, 32 is topright.
* * angle - angle, clockwise of north
* * distance - pixels to go forwards
* * include_start - include starting turf
* * diagonal_expand_north - get turfs above diagonal if perfect diagonal. if both expand params are null, we use the same priority as SS13's movement handler.
* * diagonal_expand_south - get turfs below diagonal if perfect diagonal. if both expand params are null, we use the same priority as SS13's movement handler.
*/
/proc/pixel_physics_raycast(turf/starting, starting_px, starting_py, angle, distance, include_start, diagonal_expand_north, diagonal_expand_south)
if(starting_px < 0 || starting_py < 0 || starting_px > 33 || starting_py > 33)
CRASH("starting_px or starting_py is not 0 < x < 33")
starting_px = clamp(starting_px, 1, 32)
starting_py = clamp(starting_py, 1, 32)

. = list()

if(include_start)
. += starting

var/remaining_distance = distance
var/safety = world.maxx + world.maxy

// if angle is completely cardinal or completely diagonal
// we use MODULUS_F because it's floating-compatible
if(!MODULUS_F(angle, 45))
// normalize; we already know it's basically diagonal
angle = angle % 360
if(angle < 0)
angle += 360
// go do cardinal / diagonal specials
if(!(angle % 90))
// cardinal
var/c_sdx
var/c_sdy
var/c_dist_to_next
var/turf/c_moving_into = starting
switch(angle)
if(0)
c_sdx = 0
c_sdy = 1
c_dist_to_next = (WORLD_ICON_SIZE + 0.5) - starting_py
if(90)
c_sdx = 1
c_sdy = 0
c_dist_to_next = (WORLD_ICON_SIZE + 0.5) - starting_px
if(180)
c_sdx = 0
c_sdy = -1
c_dist_to_next = starting_py - 0.5
if(270)
c_sdx = -1
c_sdy = 0
c_dist_to_next = starting_px - 0.5

remaining_distance -= c_dist_to_next
while(remaining_distance >= 0)
c_moving_into = locate(c_moving_into.x + c_sdx, c_moving_into.y + c_sdy, c_moving_into.z)
if(!c_moving_into)
break
. += c_moving_into
remaining_distance -= WORLD_ICON_SIZE
return
else
var/is_diagonal_case
var/turf/d_moving_into = starting
var/d_diagonal_distance = (((WORLD_ICON_SIZE ** 2) * 2) ** 0.5)
var/d_dist_to_next
var/d_sdx
var/d_sdy
/// direction to go if we want to include the northern-most cardinal step
var/d_north_dir
/// direction to go if we want to include the southern-most cardinal step
var/d_south_dir
/// direction to go if using ss13 native movement to solve for the cardinal steps
var/d_native_dir
// we're diagonal
switch(angle)
if(45)
is_diagonal_case = round(starting_px, 1) == round(starting_py, 1)
d_sdx = 1
d_sdy = 1
d_dist_to_next = ((((WORLD_ICON_SIZE + 0.5) ** 2) * 2) ** 0.5) - (starting_px / WORLD_ICON_SIZE) * d_diagonal_distance
d_north_dir = WEST
d_south_dir = SOUTH
d_native_dir = WEST
if(135)
is_diagonal_case = round(starting_px, 1) == (WORLD_ICON_SIZE - round(starting_py, 1) + 1)
d_sdx = 1
d_sdy = -1
d_dist_to_next = ((((WORLD_ICON_SIZE + 0.5) ** 2) * 2) ** 0.5) - (starting_px / WORLD_ICON_SIZE) * d_diagonal_distance
d_north_dir = NORTH
d_south_dir = WEST
d_native_dir = WEST
if(225)
is_diagonal_case = round(starting_px, 1) == round(starting_py, 1)
d_sdx = -1
d_sdy = -1
d_dist_to_next = ((starting_px - 0.5) / WORLD_ICON_SIZE) * d_diagonal_distance
d_north_dir = NORTH
d_south_dir = EAST
d_native_dir = EAST
if(315)
is_diagonal_case = round(starting_px, 1) == (WORLD_ICON_SIZE - round(starting_py, 1) + 1)
d_sdx = -1
d_sdy = 1
d_dist_to_next = ((starting_px - 0.5) / WORLD_ICON_SIZE) * d_diagonal_distance
d_north_dir = EAST
d_south_dir = SOUTH
d_native_dir = EAST
// only do special diag stuff if it's a close enough to a perfect diagonal
if(is_diagonal_case)
remaining_distance -= d_dist_to_next
var/use_ss13_default_priority = isnull(diagonal_expand_north) && isnull(diagonal_expand_south)
while(remaining_distance >= 0)
d_moving_into = locate(d_moving_into.x + d_sdx, d_moving_into.y + d_sdy, d_moving_into.z)
if(!d_moving_into)
break
. += d_moving_into
if(use_ss13_default_priority)
. += get_step(d_moving_into, d_native_dir)
else
if(diagonal_expand_north)
// we actually want to get the one behind them; we don't need to null check either because of that
. += get_step(d_moving_into, d_north_dir)
if(diagonal_expand_south)
// we actually want to get the one behind them; we don't need to null check either because of that
. += get_step(d_moving_into, d_south_dir)
remaining_distance -= WORLD_ICON_SIZE
return

// dx, dy for every distance pixel
var/ddx = sin(angle)
var/ddy = cos(angle)

// sign of dx, dy as 1 or -1
var/sdx = ddx > 0? 1 : -1
var/sdy = ddy > 0? 1 : -1

var/cx = starting_px
var/cy = starting_py

var/turf/moving_into = starting
while(safety-- > 0 && remaining_distance > 0)
var/d_next_horizontal = \
(sdx? ((sdx > 0? (WORLD_ICON_SIZE + 0.5) - cx : -cx + 0.5) / ddx) : INFINITY)
var/d_next_vertical = \
(sdy? ((sdy > 0? (WORLD_ICON_SIZE + 0.5) - cy : -cy + 0.5) / ddy) : INFINITY)
var/consumed = 0

if(d_next_horizontal == d_next_vertical)
// we're diagonal
if(d_next_horizontal <= remaining_distance)
moving_into = locate(moving_into.x + sdx, moving_into.y + sdy, moving_into.z)
consumed = d_next_horizontal
if(!moving_into)
break
cx = sdx > 0? 0.5 : (WORLD_ICON_SIZE + 0.5)
cy = sdy > 0? 0.5 : (WORLD_ICON_SIZE + 0.5)
else
break
else if(d_next_horizontal < d_next_vertical)
// closer is to move left/right
if(d_next_horizontal <= remaining_distance)
moving_into = locate(moving_into.x + sdx, moving_into.y, moving_into.z)
consumed = d_next_horizontal
if(!moving_into)
break
cx = sdx > 0? 0.5 : (WORLD_ICON_SIZE + 0.5)
cy = cy + d_next_horizontal * ddy
else if(d_next_vertical < d_next_horizontal)
// closer is to move up/down
if(d_next_vertical <= remaining_distance)
moving_into = locate(moving_into.x, moving_into.y + sdy, moving_into.z)
consumed = d_next_vertical
if(!moving_into)
break
cx = cx + d_next_vertical * ddx
cy = sdy > 0? 0.5 : (WORLD_ICON_SIZE + 0.5)
else
break

remaining_distance -= consumed

// if we need to move
if(moving_into)
. += moving_into
2 changes: 1 addition & 1 deletion code/__HELPERS/game/turfs/offsets.dm
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
// dir2angle is north-zero clockwise
// turn_amount will therefore be how much we need to turn clockwise to get there
//
// we know this will be a whole number so we use native % instead of MODULUS
// we know this will be a whole number so we use native % instead of MODULUS_F
var/turn_amount = (dir2angle(new_dir) - dir2angle(old_dir)) % 360

// rotated x/y is where the entity will be after being rotated in its **current**
Expand Down
11 changes: 0 additions & 11 deletions code/__HELPERS/math/multipliers.dm

This file was deleted.

6 changes: 3 additions & 3 deletions code/__HELPERS/time.dm
Original file line number Diff line number Diff line change
Expand Up @@ -149,21 +149,21 @@ GLOBAL_VAR_INIT(roundstart_hour, pick(2,7,12,17))
if(second < 60)
return "[second] second[(second != 1)? "s":""]"
var/minute = FLOOR(second / 60, 1)
second = MODULUS(second, 60)
second = MODULUS_F(second, 60)
var/secondT
if(second)
secondT = " and [second] second[(second != 1)? "s":""]"
if(minute < 60)
return "[minute] minute[(minute != 1)? "s":""][secondT]"
var/hour = FLOOR(minute / 60, 1)
minute = MODULUS(minute, 60)
minute = MODULUS_F(minute, 60)
var/minuteT
if(minute)
minuteT = " and [minute] minute[(minute != 1)? "s":""]"
if(hour < 24)
return "[hour] hour[(hour != 1)? "s":""][minuteT][secondT]"
var/day = FLOOR(hour / 24, 1)
hour = MODULUS(hour, 24)
hour = MODULUS_F(hour, 24)
var/hourT
if(hour)
hourT = " and [hour] hour[(hour != 1)? "s":""]"
Expand Down
12 changes: 12 additions & 0 deletions code/___compile_options.dm
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,24 @@
// #define AO_USE_LIGHTING_OPACITY

// ## Overlays

/**
* A reasonable number of maximum overlays an object needs.
* If you think you need more, rethink it.
*/
#define MAX_ATOM_OVERLAYS 100

// ## Projectiles

/**
* Enable raycast visuals
*/
// #define CF_PROJECTILE_RAYCAST_VISUALS

#ifdef CF_PROJECTILE_RAYCAST_VISUALS
#warn Visualization of projectile raycast algorithm enabled.
#endif

// ## Timers

// #define TIMER_LOOP_DEBUGGING
9 changes: 8 additions & 1 deletion code/controllers/subsystem/processing/processing.dm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ SUBSYSTEM_DEF(processing)
currentrun = processing.Copy()
//cache for sanic speed (lists are references anyways)
var/list/current_run = currentrun
var/dt = (subsystem_flags & SS_TICKER)? (wait * world.tick_lag) : max(world.tick_lag, wait * 0.1)
// tick_lag is in deciseconds
// in ticker, our wait is that many ds
// in non-ticker, our wait is either wait in ds, or a minimum of tick_lag in ds
// we convert it to seconds with * 0.1
var/dt = (subsystem_flags & SS_TICKER? (wait * world.tick_lag) : max(world.tick_lag, wait)) * 0.1

while(current_run.len)
var/datum/thing = current_run[current_run.len]
Expand All @@ -44,6 +48,9 @@ SUBSYSTEM_DEF(processing)
* - Probability of something happening, do `if(DT_PROB(25, delta_time))`, not `if(prob(25))`. This way, if the subsystem wait is e.g. lowered, there won't be a higher chance of this event happening per second
*
* If you override this do not call parent, as it will return PROCESS_KILL. This is done to prevent objects that dont override process() from staying in the processing list
*
* @params
* * delta_time - time that should have elapsed, in seconds
*/
/datum/proc/process(delta_time)
set waitfor = FALSE
Expand Down
10 changes: 0 additions & 10 deletions code/controllers/subsystem/processing/projectiles.dm
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,3 @@ PROCESSING_SUBSYSTEM_DEF(projectiles)
stat_tag = "PP"
priority = FIRE_PRIORITY_PROJECTILES
subsystem_flags = SS_NO_INIT
var/global_max_tick_moves = 10
var/global_pixel_speed = 2
var/global_iterations_per_move = 16

/datum/controller/subsystem/processing/projectiles/proc/set_pixel_speed(new_speed)
global_pixel_speed = new_speed
for(var/i in processing)
var/obj/projectile/P = i
if(istype(P)) //there's non projectiles on this too.
P.set_pixel_speed(new_speed)
4 changes: 4 additions & 0 deletions code/datums/components/turfs/transition_border.dm
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@
/datum/component/transition_border/proc/transit(datum/source, atom/movable/AM)
if(AM.atom_flags & ATOM_ABSTRACT)
return // nah.
if(AM.movable_flags & MOVABLE_IN_MOVED_YANK)
return // we're already in a yank
var/turf/our_turf = parent
var/z_index = SSmapping.level_index_in_dir(our_turf.z, dir)
if(isnull(z_index))
STACK_TRACE("no z index?! deleting self.")
qdel(src)
return
// todo: this is shit but we have to yield to prevent a Moved() before Moved()
AM.movable_flags |= MOVABLE_IN_MOVED_YANK
spawn(0)
AM.movable_flags &= ~MOVABLE_IN_MOVED_YANK
if(AM.loc != our_turf)
return
var/turf/target
Expand Down
Loading

0 comments on commit 50371db

Please sign in to comment.