Skip to content

Commit

Permalink
Projectile smooth movement (#4986)
Browse files Browse the repository at this point in the history
# About the pull request

Changes the `projectile`'s visual position to `animate()` across its
projected path, providing smoother visuals, especially at higher client
framerates.


![Bresenham](https://github.com/cmss13-devs/cmss13/assets/14267245/b2cb17d9-3a57-49f3-9faa-39c9b64957fe)
Black squares: tiles actually traversed by the projectile object
Red line: visual path taken by the projectile
Purple marks: equidistant marks corresponding to number of traversed
tiles (black squares)
Blue marks: closest points to center of traversed tiles (black squares)

The projectile's physical path (black squares) is unchanged. The system
calculates how far along the physical path the projectile has travelled,
draws a line from the start point to the end point (red line), then
interpolates where along that line corresponds to that distance
travelled (purple marks). This is slightly less accurate to the
projectile's physical position than the closest point of the line (blue
marks), but this slight potential offset is preferred for its smoother
visuals.

This does not touch any code that actually effects the bullet's
trajectory, this is purely cosmetic.

Tested:
- [x] handheld guns
- [x] scatter
- [x] in-turf targeting (as visually consistent as the current
implementation, at least)
- [x] rockets
- [x] plasma caster
- [x] xeno spit

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

Smoother apparent movement for projectiles gives a more appealing look.
# Testing Photographs and Procedure
<details>
<summary>Screenshots & Videos</summary>

https://streamable.com/h6esir

</details>


# Changelog
:cl:
code: projectiles smoothly animate their movement
/:cl:
  • Loading branch information
Doubleumc committed Dec 7, 2023
1 parent 3df3cfd commit bdbca13
Showing 1 changed file with 82 additions and 23 deletions.
105 changes: 82 additions & 23 deletions code/modules/projectiles/projectile.dm
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
anchored = TRUE //You will not have me, space wind!
flags_atom = NOINTERACT //No real need for this, but whatever. Maybe this flag will do something useful in the future.
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
invisibility = 100 // We want this thing to be invisible when it drops on a turf because it will be on the user's turf. We then want to make it visible as it travels.
alpha = 0 // We want this thing to be transparent when it drops on a turf because it will be on the user's turf. We then want to make it opaque as it travels.
layer = FLY_LAYER
animate_movement = NO_STEPS //disables gliding because it fights against what animate() is doing

var/datum/ammo/ammo //The ammo data which holds most of the actual info.

Expand Down Expand Up @@ -48,6 +49,13 @@
var/vis_travelled = 0
/// Origin point for tracing and visual updates
var/turf/vis_source
var/vis_source_pixel_x = 0
var/vis_source_pixel_y = 0

/// Starting point of projectile before each flight.
var/turf/process_start_turf
var/process_start_pixel_x = 0
var/process_start_pixel_y = 0

var/damage = 0
var/accuracy = 85 //Base projectile accuracy. Can maybe be later taken from the mob if desired.
Expand Down Expand Up @@ -229,9 +237,11 @@
p_x = Clamp(p_x, -16, 16)
p_y = Clamp(p_y, -16, 16)

if(source_turf != vis_source)
if(process_start_turf != vis_source)
vis_travelled = 0
vis_source = source_turf
vis_source = process_start_turf || source_turf
vis_source_pixel_x = process_start_pixel_x
vis_source_pixel_y = process_start_pixel_y

angle = 0 // Stolen from Get_Angle() basically
var/dx = p_x + aim_turf.x * 32 - source_turf.x * 32 // todo account for firer offsets
Expand All @@ -248,13 +258,14 @@
else if(dx < 0)
angle += 360

var/matrix/rotate = matrix() //Change the bullet angle.
rotate.Turn(angle)
apply_transform(rotate)

/obj/projectile/process(delta_time)
. = PROC_RETURN_SLEEP

var/process_start_delta_time = delta_time //easier to take it unaltered than to recalculate it later
process_start_turf = get_turf(src) //obj-level vars so update_angle() can use it without passing it through a ton of procs
process_start_pixel_x = pixel_x
process_start_pixel_y = pixel_y

// Keep going as long as we got speed and time
while(speed > 0 && (speed * ((delta_time + time_carry)/10) >= 1))
time_carry -= 1/speed*10
Expand All @@ -266,8 +277,72 @@
return PROCESS_KILL

time_carry += delta_time

animate_flight(process_start_turf, process_start_pixel_x, process_start_pixel_y, process_start_delta_time)

return FALSE

//#define LERP(a, b, t) (a + (b - a) * CLAMP01(t))
#define LERP_UNCLAMPED(a, b, t) (a + (b - a) * t)

/// Animates the projectile across the process'ed flight.
/obj/projectile/proc/animate_flight(turf/start_turf, start_pixel_x, start_pixel_y, delta_time)
//Get pixelspace coordinates of start and end of visual path

var/pixel_x_source = vis_source.x * world.icon_size + vis_source_pixel_x
var/pixel_y_source = vis_source.y * world.icon_size + vis_source_pixel_y

var/turf/vis_target = path[path.len]
var/pixel_x_target = vis_target.x * world.icon_size + p_x
var/pixel_y_target = vis_target.y * world.icon_size + p_y

//Change the bullet angle to its visual path

var/vis_angle = get_pixel_angle(x = pixel_x_target - pixel_x_source, y = pixel_y_target - pixel_y_source) //naming vars because the proc takes y then x and that's WEIRD
var/matrix/rotate = matrix()
rotate.Turn(vis_angle)
apply_transform(rotate)

//Determine apparent position along visual path, then lerp between start and end positions

var/vis_length = vis_travelled + path.len
var/vis_current = vis_travelled + speed * (time_carry * 0.1) //speed * (time_carry * 0.1) for remainder time movement, visually "catching up" to where it should be
var/vis_interpolant = vis_current / vis_length

var/pixel_x_lerped = LERP_UNCLAMPED(pixel_x_source, pixel_x_target, vis_interpolant)
var/pixel_y_lerped = LERP_UNCLAMPED(pixel_y_source, pixel_y_target, vis_interpolant)

//Convert pixelspace to pixel offset relative to current loc

var/turf/current_turf = get_turf(src)
var/pixel_x_rel_new = pixel_x_lerped - current_turf.x * world.icon_size
var/pixel_y_rel_new = pixel_y_lerped - current_turf.y * world.icon_size

//Set pixel offset as from current loc to old position, so it appears to start in the old position

pixel_x = (start_turf.x - current_turf.x) * world.icon_size + start_pixel_x
pixel_y = (start_turf.y - current_turf.y) * world.icon_size + start_pixel_y

//Determine apparent distance travelled, then lerp for projectile fade-in

var/dist_current = distance_travelled + speed * (time_carry * 0.1) //speed * (time_carry * 0.1) for remainder time fade-in
var/alpha_interpolant = dist_current - 1 //-1 so it transitions from transparent to opaque between dist 1-2
var/alpha_new = LERP_UNCLAMPED(0, 255, alpha_interpolant)

//Animate the visuals from starting position to new position

if(projectile_flags & PROJECTILE_SHRAPNEL) //there can be a LOT of shrapnel especially from a cluster OB, not important enough for the expense of an animate()
alpha = alpha_new
pixel_x = pixel_x_rel_new
pixel_y = pixel_y_rel_new
return

var/anim_time = delta_time * 0.1
animate(src, pixel_x = pixel_x_rel_new, pixel_y = pixel_y_rel_new, alpha = alpha_new, time = anim_time, flags = ANIMATION_END_NOW)

//#undef LERP
#undef LERP_UNCLAMPED

/// Flies the projectile forward one single turf
/obj/projectile/proc/fly()
SHOULD_NOT_SLEEP(TRUE)
Expand All @@ -294,8 +369,6 @@
forceMove(next_turf)
distance_travelled++
vis_travelled++
if(distance_travelled > 1)
invisibility = 0

// Check we're still flying - in the highly unlikely but apparently possible case
// we hit something through forceMove callbacks that we didn't pick up in scan_a_turf
Expand All @@ -320,20 +393,6 @@
p_y *= 2
retarget(aim_turf, keep_angle = TRUE)

// Nowe we update visual offset by tracing the bullet predicted location against real one
//
// Travelled real distance so far
var/dist = vis_travelled * 32 + speed * (time_carry*10)
// Compute where we should be
var/vis_x = vis_source.x * 32 + sin(angle) * dist
var/vis_y = vis_source.y * 32 + cos(angle) * dist
// Get the difference with where we actually are
var/dx = vis_x - loc.x * 32
var/dy = vis_y - loc.y * 32
// Clamp and set this as pixel offsets
pixel_x = Clamp(dx, -16, 16)
pixel_y = Clamp(dy, -16, 16)

/obj/projectile/proc/retarget(atom/new_target, keep_angle = FALSE)
var/turf/current_turf = get_turf(src)
path = getline2(current_turf, new_target)
Expand Down

0 comments on commit bdbca13

Please sign in to comment.