Skip to content

Commit

Permalink
[MIRROR] Makes AI tracking more snappy, improves API (AI movement cha…
Browse files Browse the repository at this point in the history
…nge) (#962)

* Makes AI tracking more snappy, improves API (AI movement change) (#81401)

## About The Pull Request

Ok so tracking (from the datum) worked, but when used to follow someone
it had a noticable delay from the datum needing to wait for process to
fire to do its work

This would be an expensive proc to run constantly, but we don't really
have to (there are not that many ai eyes in the world). So rather then
only processing to keep step, let's track the target mob by its
movement, and then fall back on a process loop to handle rechecking in
case of camera memes.

This does technically mean you won't "break" the track if the cameras go
out until the tracked mob moves, but I think that's a reasonable price
to pay for more responsive movement. I think I could make our current
system work with it too, though it would be a bit more wasteful. John if
you have opinions just lay into me.

I've also renamed/pulled apart the helper procs for the trackable datum,
with the hope of making how they are used more understandable at a
glance

Oh and rather then holding a weakref since I needed MOVED anyway I just
use QDELETING to free the ref if the mob goes away

### Edit:

#### Glide size touchups
Implements glide size mirroring so we move at the same speed as our
target

Also moves the existing signal to send to the trackable datum itself, as
appears intended from the doc comment

#### AI behavior changes

Rewrites ai movement to be less dumb

OK so 2 things here. One is a behavior change, the other is a visual QOL
thing.

The way ai movement works is we move graduated "steps". Either moving 1,
2, or 3 steps per tick.
We do this by, so long as input is held down, incrementing a number
called "sprint"
Currently it'll go from 10 to 50 (formula effectively looks like steps =
(sprint / 20) + 1))

Anyway, this is... not fine but ok, but the way we handle deceleration
is ass IMO. It's literally just wait 0.5 seconds and sprint resets.
I think this feels crummy, so instead I've made it decay depending on
how long you go between inputs, at 7x greater rate then it increases.

That's the behavior change. Visual change is a lot easier.
Ais were not gliding properly. They assumed they had 4 ticks to move a
tile, rather then 1. This meant they'd jump around constantly, to catch
up to where we expect them to be.
I've fixed this by giving them 1 tick instead. Should feel a lot better

## Why It's Good For The Game

Snappier response times, cleaner code

## Changelog
:cl:
add: AI's acceleration now smoothly decays, instead of just falling back
down to 0 after 0.5 seconds
fix: AI's standard movement (non accelerated) is smooth now, instead of
constantly jumping around
fix: AIs will now follow their targets more closely, shouldn't have any
issues with them lagging behind anymore
/:cl:

* Makes AI tracking more snappy, improves API (AI movement change)

---------

Co-authored-by: LemonInTheDark <[email protected]>
  • Loading branch information
2 people authored and StealsThePRs committed Feb 15, 2024
1 parent fcba208 commit d90cbb2
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 98 deletions.
4 changes: 3 additions & 1 deletion code/__DEFINES/dcs/signals/signals_camera.dm
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
///Signal sent when a /datum/trackable found a target: (datum/trackable/source, mob/living/target)
///Signal sent when a /datum/trackable found a target: (mob/living/target)
#define COMSIG_TRACKABLE_TRACKING_TARGET "comsig_trackable_tracking_target"
///Signal sent when the mob a /datum/trackable is actively following changes glide size: mob/living/target, new_glide_size)
#define COMSIG_TRACKABLE_GLIDE_CHANGED "comsig_trackable_glide_changed"
2 changes: 1 addition & 1 deletion code/_onclick/ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
return

if(ismob(A))
ai_tracking_tool.set_tracked_mob(src, A.name)
ai_tracking_tool.track_mob(src, A)
else
A.move_camera_by_click()

Expand Down
178 changes: 125 additions & 53 deletions code/game/machinery/camera/trackable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
#define CAMERA_TICK_LIMIT 10

/datum/trackable
///Boolean on whether or not we are currently trying to track something.
var/tracking = FALSE
///Reference to the atom that owns us, used for tracking.
var/atom/tracking_holder

///If there is a mob currently being tracked, this will be the weakref to it.
var/datum/weakref/tracked_mob
///What mob are we currently tracking, if any
var/mob/living/tracked_mob
///If we're currently rechecking our target's trackability in hopes of something changing
var/rechecking = FALSE
///How many times we've failed to locate our target.
var/cameraticks = 0

Expand All @@ -24,35 +24,14 @@
/datum/trackable/New(atom/source)
. = ..()
tracking_holder = source
RegisterSignal(tracking_holder, COMSIG_MOB_RESET_PERSPECTIVE, PROC_REF(cancel_target_tracking))
RegisterSignal(tracking_holder, COMSIG_MOB_RESET_PERSPECTIVE, PROC_REF(perspective_reset))

/datum/trackable/Destroy(force)
tracking_holder = null
tracked_mob = null
STOP_PROCESSING(SSprocessing, src)
return ..()

/datum/trackable/process()
var/mob/living/tracked_target = tracked_mob?.resolve()
if(!tracked_target || !tracking)
set_tracking(FALSE)
return

if(tracked_target.can_track(tracking_holder))
cameraticks = initial(cameraticks)
SEND_SIGNAL(tracking_holder, COMSIG_TRACKABLE_TRACKING_TARGET, tracked_target)
return

if(cameraticks < CAMERA_TICK_LIMIT)
if(!cameraticks)
to_chat(tracking_holder, span_warning("Target is not near any active cameras. Attempting to reacquire..."))
cameraticks++
return

to_chat(tracking_holder, span_warning("Unable to reacquire, cancelling track..."))
cameraticks = initial(cameraticks)
set_tracking(FALSE)

///Generates a list of trackable people by name, returning a list of Humans + Non-Humans that can be tracked.
/datum/trackable/proc/find_trackable_mobs()
RETURN_TYPE(/list)
Expand Down Expand Up @@ -82,47 +61,140 @@
var/list/targets = sort_list(humans) + sort_list(others)
return targets

///Toggles whether or not we're tracking something. Arg is whether it's on or off.
/datum/trackable/proc/set_tracking(on = FALSE)
if(on)
/// Takes a mob to track, resets our state and begins trying to follow it
/// Best we can at least
/datum/trackable/proc/set_tracked_mob(mob/living/track)
set_rechecking(FALSE)
if(tracked_mob)
UnregisterSignal(tracked_mob, list(COMSIG_QDELETING, COMSIG_MOVABLE_MOVED, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE))
if(track && !isliving(track))
tracked_mob = null
return
tracked_mob = track
if(tracked_mob)
RegisterSignal(tracked_mob, COMSIG_QDELETING, PROC_REF(target_deleted))
RegisterSignal(tracked_mob, COMSIG_MOVABLE_MOVED, PROC_REF(target_moved))
RegisterSignal(tracked_mob, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, PROC_REF(glide_size_changed))
attempt_track()

/datum/trackable/proc/target_deleted(datum/source)
SIGNAL_HANDLER
reset_tracking()

/datum/trackable/proc/perspective_reset(atom/source)
SIGNAL_HANDLER
reset_tracking()

/datum/trackable/proc/target_moved(datum/source)
SIGNAL_HANDLER
if(attempt_track())
return
set_rechecking(TRUE)

/// Controls if we're processing to recheck the conditions that prevent tracking or not
/datum/trackable/proc/set_rechecking(should_check)
if(should_check)
START_PROCESSING(SSprocessing, src)
tracking = TRUE
cameraticks = initial(cameraticks)
rechecking = TRUE
else
STOP_PROCESSING(SSprocessing, src)
tracking = FALSE
tracked_mob = null
rechecking = FALSE

/datum/trackable/process()
if(!rechecking)
return PROCESS_KILL

if(attempt_track())
set_rechecking(FALSE)
return

if(cameraticks < CAMERA_TICK_LIMIT)
if(!cameraticks)
to_chat(tracking_holder, span_warning("Target is not near any active cameras. Attempting to reacquire..."))
cameraticks++
return

///Called by Signals, used to cancel tracking of a target.
/datum/trackable/proc/cancel_target_tracking(atom/source)
to_chat(tracking_holder, span_warning("Unable to reacquire, cancelling track..."))
reset_tracking()

/// Tries to track onto our target mob. Returns true if it succeeds, false otherwise
/datum/trackable/proc/attempt_track()
if(!tracked_mob)
reset_tracking()
return FALSE

if(!tracked_mob.can_track(tracking_holder))
return FALSE
// In case we've been checking
set_rechecking(FALSE)
SEND_SIGNAL(src, COMSIG_TRACKABLE_TRACKING_TARGET, tracked_mob)
return TRUE

/datum/trackable/proc/glide_size_changed(datum/source, new_glide_size)
SIGNAL_HANDLER
set_tracking(FALSE)
SEND_SIGNAL(src, COMSIG_TRACKABLE_GLIDE_CHANGED, tracked_mob, new_glide_size)

/**
* set_tracked_mob
* reset_tracking
*
* Sets a mob as being tracked, if a target is already provided then it will track that directly,
* otherwise it will give a tgui input list to find targets to track.
* Resets our tracking
*/
/datum/trackable/proc/reset_tracking()
set_tracked_mob(null)

/**
* track_input
*
* Sets a mob as being tracked, will give a tgui input list to find targets to track.
* Args:
* tracker - The person trying to track, used for feedback messages. This is not the same as tracking_holder
* tracked_mob_name - (Optional) The person being tracked, to skip the input list.
*/
/datum/trackable/proc/set_tracked_mob(mob/living/tracker, tracked_mob_name)
/datum/trackable/proc/track_input(mob/living/tracker)
if(!tracker || tracker.stat == DEAD)
return

if(tracked_mob_name)
find_trackable_mobs() //this is in case the tracked mob is newly/no-longer in camera field of view.
tracked_mob = isnull(humans[tracked_mob_name]) ? others[tracked_mob_name] : humans[tracked_mob_name]
if(isnull(tracked_mob))
to_chat(tracker, span_notice("Target is not on or near any active cameras. Tracking failed."))
return
to_chat(tracker, span_notice("Now tracking [tracked_mob_name] on camera."))
else
var/target_name = tgui_input_list(tracker, "Select a target", "Tracking", find_trackable_mobs())
if(!target_name || isnull(target_name))
return
tracked_mob = isnull(humans[target_name]) ? others[target_name] : humans[target_name]
var/target_name = tgui_input_list(tracker, "Select a target", "Tracking", find_trackable_mobs())
if(!target_name || isnull(target_name))
return
var/datum/weakref/mob_ref = isnull(humans[target_name]) ? others[target_name] : humans[target_name]
if(isnull(mob_ref))
to_chat(tracker, span_notice("Target is not on or near any active cameras. Tracking failed."))
return
set_tracked_mob(mob_ref.resolve())

/**
* track_name
*
* Sets a mob as being tracked, will track the passed in target name's target
* Args:
* tracker - The person trying to track, used for feedback messages. This is not the same as tracking_holder
* tracked_mob_name - The person being tracked.
*/
/datum/trackable/proc/track_name(mob/living/tracker, tracked_mob_name)
if(!tracker || tracker.stat == DEAD)
return

find_trackable_mobs() //this is in case the tracked mob is newly/no-longer in camera field of view.
var/datum/weakref/mob_ref = isnull(humans[tracked_mob_name]) ? others[tracked_mob_name] : humans[tracked_mob_name]
if(isnull(mob_ref))
to_chat(tracker, span_notice("Target is not on or near any active cameras. Tracking failed."))
return
to_chat(tracker, span_notice("Now tracking [tracked_mob_name] on camera."))
set_tracked_mob(mob_ref.resolve())

set_tracking(TRUE)
/**
* track_mob
*
* Sets a mob as being tracked, will track the passed in target
* Args:
* tracker - The person trying to track, used for feedback messages. This is not the same as tracking_holder
* tracked - The person being tracked.
*/
/datum/trackable/proc/track_mob(mob/living/tracker, mob/living/tracked)
if(!tracker || tracker.stat == DEAD)
return
// Need to make sure the tracked mob is in our list
track_name(tracked.name)

#undef CAMERA_TICK_LIMIT
2 changes: 1 addition & 1 deletion code/game/machinery/computer/crew.dm
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new)
var/mob/living/silicon/ai/AI = usr
if(!istype(AI))
return
AI.ai_tracking_tool.set_tracked_mob(AI, params["name"])
AI.ai_tracking_tool.track_name(AI, params["name"])

#undef SENSORS_UPDATE_PERIOD
#undef UNKNOWN_JOB_ID
28 changes: 15 additions & 13 deletions code/modules/mob/living/silicon/ai/ai.dm
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@

var/mob/camera/ai_eye/eyeobj
var/sprint = 10
var/cooldown = 0
var/last_moved = 0
var/acceleration = TRUE

var/obj/structure/ai_core/deactivated/linked_core //For exosuit control
Expand Down Expand Up @@ -191,7 +191,8 @@
builtInCamera.network = list("ss13")

ai_tracking_tool = new(src)
RegisterSignal(src, COMSIG_TRACKABLE_TRACKING_TARGET, PROC_REF(on_track_target))
RegisterSignal(ai_tracking_tool, COMSIG_TRACKABLE_TRACKING_TARGET, PROC_REF(on_track_target))
RegisterSignal(ai_tracking_tool, COMSIG_TRACKABLE_GLIDE_CHANGED, PROC_REF(tracked_glidesize_changed))

add_traits(list(TRAIT_PULL_BLOCKED, TRAIT_HANDS_BLOCKED), ROUNDSTART_TRAIT)

Expand All @@ -211,8 +212,7 @@
switch(_key)
if("`", "0")
if(cam_prev)
if(ai_tracking_tool.tracking)
ai_tracking_tool.set_tracking(FALSE)
ai_tracking_tool.reset_tracking()
eyeobj.setLoc(cam_prev)
return
if("1", "2", "3", "4", "5", "6", "7", "8", "9")
Expand All @@ -223,8 +223,7 @@
return
if(cam_hotkeys[_key]) //if this is false, no hotkey for this slot exists.
cam_prev = eyeobj.loc
if(ai_tracking_tool.tracking)
ai_tracking_tool.set_tracking(FALSE)
ai_tracking_tool.reset_tracking()
eyeobj.setLoc(cam_hotkeys[_key])
return
return ..()
Expand All @@ -250,7 +249,6 @@
if(ai_voicechanger)
ai_voicechanger.owner = null
ai_voicechanger = null
UnregisterSignal(src, COMSIG_TRACKABLE_TRACKING_TARGET)
return ..()

/// Removes all malfunction-related abilities from the AI
Expand Down Expand Up @@ -401,7 +399,7 @@
set name = "track"
set hidden = TRUE //Don't display it on the verb lists. This verb exists purely so you can type "track Oldman Robustin" and follow his ass

ai_tracking_tool.set_tracked_mob(src)
ai_tracking_tool.track_input(src)

///Called when an AI finds their tracking target.
/mob/living/silicon/ai/proc/on_track_target(datum/trackable/source, mob/living/target)
Expand All @@ -411,6 +409,12 @@
else
view_core()

/// Keeps our rate of gliding in step with the mob we're following
/mob/living/silicon/ai/proc/tracked_glidesize_changed(datum/trackable/source, mob/living/target, new_glide_size)
SIGNAL_HANDLER
if(eyeobj)
eyeobj.glide_size = new_glide_size

/mob/living/silicon/ai/verb/toggle_anchor()
set category = "AI Commands"
set name = "Toggle Floor Bolts"
Expand Down Expand Up @@ -525,7 +529,7 @@
else
to_chat(src, span_notice("Unable to project to the holopad."))
if(href_list["track"])
ai_tracking_tool.set_tracked_mob(src, href_list["track"])
ai_tracking_tool.track_name(src, href_list["track"])
return
if (href_list["ai_take_control"]) //Mech domination
var/obj/vehicle/sealed/mecha/M = locate(href_list["ai_take_control"]) in GLOB.mechas_list
Expand Down Expand Up @@ -568,8 +572,7 @@
view_core()
return

if(ai_tracking_tool.tracking)
ai_tracking_tool.set_tracking(FALSE)
ai_tracking_tool.reset_tracking()

// ok, we're alive, camera is good and in our network...
eyeobj.setLoc(get_turf(C))
Expand Down Expand Up @@ -643,8 +646,7 @@
set category = "AI Commands"
set name = "Jump To Network"
unset_machine()
if(ai_tracking_tool.tracking)
ai_tracking_tool.set_tracking(FALSE)
ai_tracking_tool.reset_tracking()
var/cameralist[0]

if(incapacitated())
Expand Down
Loading

0 comments on commit d90cbb2

Please sign in to comment.