From 9a7e74d69935ed146c3758b0da759481b7b50a8d Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Thu, 15 Feb 2024 23:19:30 +0100 Subject: [PATCH] [MIRROR] Makes AI tracking more snappy, improves API (AI movement change) (#26508) * 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 <58055496+LemonInTheDark@users.noreply.github.com> --- code/__DEFINES/dcs/signals/signals_camera.dm | 4 +- code/_onclick/ai.dm | 2 +- code/game/machinery/camera/trackable.dm | 178 ++++++++++++------ code/game/machinery/computer/crew.dm | 2 +- code/modules/mob/living/silicon/ai/ai.dm | 28 +-- .../mob/living/silicon/ai/freelook/eye.dm | 51 ++--- code/modules/mob/mob_movement.dm | 3 +- .../file_system/programs/secureye.dm | 12 +- 8 files changed, 183 insertions(+), 97 deletions(-) diff --git a/code/__DEFINES/dcs/signals/signals_camera.dm b/code/__DEFINES/dcs/signals/signals_camera.dm index 6ec142f54fa..92e9b94f35b 100644 --- a/code/__DEFINES/dcs/signals/signals_camera.dm +++ b/code/__DEFINES/dcs/signals/signals_camera.dm @@ -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" diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm index 3d632a99f73..c8c35b30847 100644 --- a/code/_onclick/ai.dm +++ b/code/_onclick/ai.dm @@ -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() diff --git a/code/game/machinery/camera/trackable.dm b/code/game/machinery/camera/trackable.dm index 884f38f750b..b1c4b46da4b 100644 --- a/code/game/machinery/camera/trackable.dm +++ b/code/game/machinery/camera/trackable.dm @@ -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 @@ -24,7 +24,7 @@ /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 @@ -32,27 +32,6 @@ 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) @@ -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 -///Called by Signals, used to cancel tracking of a target. -/datum/trackable/proc/cancel_target_tracking(atom/source) +/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 + + 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()) - set_tracking(TRUE) +/** + * 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()) + +/** + * 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 diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm index e269dc46ad9..be15e3dde6c 100644 --- a/code/game/machinery/computer/crew.dm +++ b/code/game/machinery/computer/crew.dm @@ -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 diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 096afab907d..211cf5c81a7 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -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 @@ -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) @@ -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") @@ -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 ..() @@ -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 @@ -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) @@ -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" @@ -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 @@ -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)) @@ -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()) diff --git a/code/modules/mob/living/silicon/ai/freelook/eye.dm b/code/modules/mob/living/silicon/ai/freelook/eye.dm index 416bbb19912..e8c1919b020 100644 --- a/code/modules/mob/living/silicon/ai/freelook/eye.dm +++ b/code/modules/mob/living/silicon/ai/freelook/eye.dm @@ -154,35 +154,44 @@ return var/mob/living/silicon/ai/AI = usr if(AI.eyeobj && (AI.multicam_on || (AI.client.eye == AI.eyeobj)) && (AI.eyeobj.z == z)) - if(AI.ai_tracking_tool.tracking) - AI.ai_tracking_tool.set_tracking(FALSE) + AI.ai_tracking_tool.reset_tracking() if (isturf(loc) || isturf(src)) AI.eyeobj.setLoc(src) // This will move the AIEye. It will also cause lights near the eye to light up, if toggled. // This is handled in the proc below this one. +#define SPRINT_PER_TICK 0.5 +#define MAX_SPRINT 50 +#define SPRINT_PER_STEP 20 +/mob/living/silicon/ai/proc/AIMove(direction) + if(last_moved && last_moved + 1 < world.timeofday) + // Decay sprint based off how long it took us to input this next move + var/missed_sprint = max((world.timeofday + 1) - last_moved, 0) * SPRINT_PER_TICK + sprint = max(sprint - missed_sprint * 7, initial(sprint)) -/client/proc/AIMove(n, direct, mob/living/silicon/ai/user) - - var/initial = initial(user.sprint) - var/max_sprint = 50 - - if(user.cooldown && user.cooldown < world.timeofday) // 3 seconds - user.sprint = initial - - for(var/i = 0; i < max(user.sprint, initial); i += 20) - var/turf/step = get_turf(get_step(user.eyeobj, direct)) + // We move a full step, at least. Can't glide more with our current movement mode, so this is how I have to live + var/step_count = 0 + for(var/i = 0; i < max(sprint, initial(sprint)); i += SPRINT_PER_STEP) + step_count += 1 + var/turf/step = get_turf(get_step(eyeobj, direction)) if(step) - user.eyeobj.setLoc(step) + eyeobj.setLoc(step) - user.cooldown = world.timeofday + 5 - if(user.acceleration) - user.sprint = min(user.sprint + 0.5, max_sprint) + // I'd like to make this scale with the steps we take, but it like, just can't + // So we're doin this instead + eyeobj.glide_size = world.icon_size + + last_moved = world.timeofday + if(acceleration) + sprint = min(sprint + SPRINT_PER_TICK, MAX_SPRINT) else - user.sprint = initial + sprint = initial(sprint) - if(user.ai_tracking_tool.tracking) - user.ai_tracking_tool.set_tracking(FALSE) + ai_tracking_tool.reset_tracking() + +#undef SPRINT_PER_STEP +#undef MAX_SPRINT +#undef SPRINT_PER_TICK // Return to the Core. /mob/living/silicon/ai/proc/view_core() @@ -191,8 +200,8 @@ H.clear_holo(src) else current = null - if(ai_tracking_tool && ai_tracking_tool.tracking) - ai_tracking_tool.set_tracking(FALSE) + if(ai_tracking_tool) + ai_tracking_tool.reset_tracking() unset_machine() if(isturf(loc) && (QDELETED(eyeobj) || !eyeobj.loc)) diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm index 6660639b400..bac29902347 100644 --- a/code/modules/mob/mob_movement.dm +++ b/code/modules/mob/mob_movement.dm @@ -76,7 +76,8 @@ return mob.remote_control.relaymove(mob, direct) if(isAI(mob)) - return AIMove(new_loc,direct,mob) + var/mob/living/silicon/ai/smoovin_ai = mob + return smoovin_ai.AIMove(direct) if(Process_Grab()) //are we restrained by someone's grip? return diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm index c9e4fc087f3..2b3b8f6ea8c 100644 --- a/code/modules/modular_computers/file_system/programs/secureye.dm +++ b/code/modules/modular_computers/file_system/programs/secureye.dm @@ -61,7 +61,6 @@ cam_background = new cam_background.assigned_map = map_name cam_background.del_on_map_removal = FALSE - RegisterSignal(src, COMSIG_TRACKABLE_TRACKING_TARGET, PROC_REF(on_track_target)) /datum/computer_file/program/secureye/Destroy() QDEL_NULL(cam_screen) @@ -138,8 +137,8 @@ playsound(computer, get_sfx(SFX_TERMINAL_TYPE), 25, FALSE) if(isnull(camera_ref)) return TRUE - if(internal_tracker && internal_tracker.tracking) - internal_tracker.set_tracking(FALSE) + if(internal_tracker) + internal_tracker.reset_tracking() update_active_camera_screen() return TRUE @@ -147,7 +146,8 @@ if("start_tracking") if(!internal_tracker) internal_tracker = new(src) - internal_tracker.set_tracked_mob(usr) + RegisterSignal(internal_tracker, COMSIG_TRACKABLE_TRACKING_TARGET, PROC_REF(on_track_target)) + internal_tracker.track_input(usr) return TRUE /datum/computer_file/program/secureye/proc/on_track_target(datum/trackable/source, mob/living/target) @@ -169,8 +169,8 @@ /datum/computer_file/program/secureye/ui_close(mob/user) . = ..() //don't track anyone while we're shutting off. - if(internal_tracker && internal_tracker.tracking) - internal_tracker.set_tracking(FALSE) + if(internal_tracker) + internal_tracker.reset_tracking() var/user_ref = REF(user) var/is_living = isliving(user) // Living creature or not, we remove you anyway.