Refactor AI into two subsystems and clean up targeting some

This commit is contained in:
Aronai Sieyes
2020-04-26 23:32:45 -04:00
parent d8cc52070a
commit 8ec31c9aa4
10 changed files with 132 additions and 63 deletions

View File

@@ -74,6 +74,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
#define INIT_ORDER_XENOARCH -20 #define INIT_ORDER_XENOARCH -20
#define INIT_ORDER_CIRCUIT -21 #define INIT_ORDER_CIRCUIT -21
#define INIT_ORDER_AI -22 #define INIT_ORDER_AI -22
#define INIT_ORDER_AI_FAST -23
#define INIT_ORDER_GAME_MASTER -24 #define INIT_ORDER_GAME_MASTER -24
#define INIT_ORDER_TICKER -50 #define INIT_ORDER_TICKER -50
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init. #define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.

View File

@@ -2,7 +2,7 @@ SUBSYSTEM_DEF(ai)
name = "AI" name = "AI"
init_order = INIT_ORDER_AI init_order = INIT_ORDER_AI
priority = FIRE_PRIORITY_AI priority = FIRE_PRIORITY_AI
wait = 5 // This gets run twice a second, however this is technically two loops in one, with the second loop being run every four iterations. wait = 2 SECONDS
flags = SS_NO_INIT|SS_TICKER flags = SS_NO_INIT|SS_TICKER
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
@@ -22,15 +22,11 @@ SUBSYSTEM_DEF(ai)
var/list/currentrun = src.currentrun var/list/currentrun = src.currentrun
while(currentrun.len) while(currentrun.len)
// var/mob/living/L = currentrun[currentrun.len]
var/datum/ai_holder/A = currentrun[currentrun.len] var/datum/ai_holder/A = currentrun[currentrun.len]
--currentrun.len --currentrun.len
if(!A || QDELETED(A) || A.busy) // Doesn't exist or won't exist soon or not doing it this tick if(!A || QDELETED(A) || A.busy) // Doesn't exist or won't exist soon or not doing it this tick
continue continue
if(times_fired % 4 == 0 && A.holder.stat != DEAD)
A.handle_strategicals() A.handle_strategicals()
if(A.holder.stat != DEAD) // The /TG/ version checks stat twice, presumably in-case processing somehow got the mob killed in that instant.
A.handle_tactics()
if(MC_TICK_CHECK) if(MC_TICK_CHECK)
return return

View File

@@ -0,0 +1,32 @@
SUBSYSTEM_DEF(aifast)
name = "AI (Fast)"
init_order = INIT_ORDER_AI_FAST
priority = FIRE_PRIORITY_AI
wait = 5 // This gets run twice a second, but shouldn't do much unless mobs are in combat
flags = SS_NO_INIT|SS_TICKER
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
var/list/processing = list()
var/list/currentrun = list()
/datum/controller/subsystem/aifast/stat_entry(msg_prefix)
var/list/msg = list(msg_prefix)
msg += "P:[processing.len]"
..(msg.Join())
/datum/controller/subsystem/aifast/fire(resumed = 0)
if (!resumed)
src.currentrun = processing.Copy()
//cache for sanic speed (lists are references anyways)
var/list/currentrun = src.currentrun
while(currentrun.len)
var/datum/ai_holder/A = currentrun[currentrun.len]
--currentrun.len
if(!A || QDELETED(A) || A.busy) // Doesn't exist or won't exist soon or not doing it this tick
continue
A.handle_tactics()
if(MC_TICK_CHECK)
return

View File

@@ -1,6 +1,14 @@
// This is a datum-based artificial intelligence for simple mobs (and possibly others) to use. // This is a datum-based artificial intelligence for simple mobs (and possibly others) to use.
// The neat thing with having this here instead of on the mob is that it is independant of Life(), and that different mobs // The neat thing with having this here instead of on the mob is that it is independant of Life(), and that different mobs
// can use a more or less complex AI by giving it a different datum. // can use a more or less complex AI by giving it a different datum.
#define AI_NO_PROCESS 0
#define AI_PROCESSING (1<<0)
#define AI_FASTPROCESSING (1<<1)
#define START_AIPROCESSING(Datum) if (!(Datum.process_flags & AI_PROCESSING)) {Datum.process_flags |= AI_PROCESSING;SSai.processing += Datum}
#define STOP_AIPROCESSING(Datum) Datum.process_flags &= ~AI_PROCESSING;SSai.processing -= Datum
#define START_AIFASTPROCESSING(Datum) if (!(Datum.process_flags & AI_FASTPROCESSING)) {Datum.process_flags |= AI_FASTPROCESSING;SSaifast.processing += Datum}
#define STOP_AIFASTPROCESSING(Datum) Datum.process_flags &= ~AI_FASTPROCESSING;SSaifast.processing -= Datum
/mob/living /mob/living
var/datum/ai_holder/ai_holder = null var/datum/ai_holder/ai_holder = null
@@ -27,7 +35,20 @@
var/busy = FALSE // If true, the ticker will skip processing this mob until this is false. Good for if you need the var/busy = FALSE // If true, the ticker will skip processing this mob until this is false. Good for if you need the
// mob to stay still (e.g. delayed attacking). If you need the mob to be inactive for an extended period of time, // mob to stay still (e.g. delayed attacking). If you need the mob to be inactive for an extended period of time,
// consider sleeping the AI instead. // consider sleeping the AI instead.
var/process_flags = 0 // Where we're processing, see flag defines.
var/list/static/fastprocess_stances = list(
STANCE_ALERT,
STANCE_APPROACH,
STANCE_FIGHT,
STANCE_BLINDFIGHT,
STANCE_REPOSITION,
STANCE_MOVE,
STANCE_FOLLOW,
STANCE_FLEE
)
var/list/static/noprocess_stances = list(
STANCE_SLEEP
)
/datum/ai_holder/hostile /datum/ai_holder/hostile
@@ -40,16 +61,34 @@
/datum/ai_holder/New(var/new_holder) /datum/ai_holder/New(var/new_holder)
ASSERT(new_holder) ASSERT(new_holder)
holder = new_holder holder = new_holder
SSai.processing += src
home_turf = get_turf(holder) home_turf = get_turf(holder)
manage_processing(AI_PROCESSING)
GLOB.stat_set_event.register(holder, src, .proc/holder_stat_change)
..() ..()
/datum/ai_holder/Destroy() /datum/ai_holder/Destroy()
holder = null holder = null
SSai.processing -= src // We might've already been asleep and removed, but byond won't care if we do this again and it saves a conditional. manage_processing(AI_NO_PROCESS)
home_turf = null home_turf = null
return ..() return ..()
/datum/ai_holder/proc/manage_processing(var/desired)
if(desired & AI_PROCESSING)
START_AIPROCESSING(src)
else
STOP_AIPROCESSING(src)
if(desired & AI_FASTPROCESSING)
START_AIFASTPROCESSING(src)
else
STOP_AIFASTPROCESSING(src)
/datum/ai_holder/proc/holder_stat_change(var/mob, old_stat, new_stat)
if(old_stat >= DEAD && new_stat <= DEAD) //Revived
manage_processing(AI_PROCESSING)
else if(old_stat <= DEAD && new_stat >= DEAD) //Killed
manage_processing(AI_NO_PROCESS)
/datum/ai_holder/proc/update_stance_hud() /datum/ai_holder/proc/update_stance_hud()
var/image/stanceimage = holder.grab_hud(LIFE_HUD) var/image/stanceimage = holder.grab_hud(LIFE_HUD)
stanceimage.icon_state = "ais_[stance]" stanceimage.icon_state = "ais_[stance]"
@@ -78,7 +117,6 @@
return return
forget_everything() // If we ever wake up, its really unlikely that our current memory will be of use. forget_everything() // If we ever wake up, its really unlikely that our current memory will be of use.
set_stance(STANCE_SLEEP) set_stance(STANCE_SLEEP)
SSai.processing -= src
update_paused_hud() update_paused_hud()
// Reverses the above proc. // Reverses the above proc.
@@ -89,7 +127,6 @@
if(!should_wake()) if(!should_wake())
return return
set_stance(STANCE_IDLE) set_stance(STANCE_IDLE)
SSai.processing += src
update_paused_hud() update_paused_hud()
/datum/ai_holder/proc/should_wake() /datum/ai_holder/proc/should_wake()
@@ -122,12 +159,23 @@
// For setting the stance WITHOUT processing it // For setting the stance WITHOUT processing it
/datum/ai_holder/proc/set_stance(var/new_stance) /datum/ai_holder/proc/set_stance(var/new_stance)
if(stance == new_stance)
ai_log("set_stance() : Ignoring change stance to same stance request.", AI_LOG_INFO)
return
ai_log("set_stance() : Setting stance from [stance] to [new_stance].", AI_LOG_INFO) ai_log("set_stance() : Setting stance from [stance] to [new_stance].", AI_LOG_INFO)
stance = new_stance stance = new_stance
if(stance_coloring) // For debugging or really weird mobs. if(stance_coloring) // For debugging or really weird mobs.
stance_color() stance_color()
update_stance_hud() update_stance_hud()
if(new_stance in fastprocess_stances) //Becoming fast
manage_processing(AI_PROCESSING|AI_FASTPROCESSING)
else if(new_stance in noprocess_stances)
manage_processing(AI_NO_PROCESS) //Becoming off
else
manage_processing(AI_PROCESSING) //Becoming slow
// This is called every half a second. // This is called every half a second.
/datum/ai_holder/proc/handle_stance_tactical() /datum/ai_holder/proc/handle_stance_tactical()
ai_log("========= Fast Process Beginning ==========", AI_LOG_TRACE) // This is to make it easier visually to disinguish between 'blocks' of what a tick did. ai_log("========= Fast Process Beginning ==========", AI_LOG_TRACE) // This is to make it easier visually to disinguish between 'blocks' of what a tick did.
@@ -167,19 +215,6 @@
return return
switch(stance) switch(stance)
if(STANCE_IDLE)
if(should_go_home())
ai_log("handle_stance_tactical() : STANCE_IDLE, going to go home.", AI_LOG_TRACE)
go_home()
else if(should_follow_leader())
ai_log("handle_stance_tactical() : STANCE_IDLE, going to follow leader.", AI_LOG_TRACE)
set_stance(STANCE_FOLLOW)
else if(should_wander())
ai_log("handle_stance_tactical() : STANCE_IDLE, going to wander randomly.", AI_LOG_TRACE)
handle_wander_movement()
if(STANCE_ALERT) if(STANCE_ALERT)
ai_log("handle_stance_tactical() : STANCE_ALERT, going to threaten_target().", AI_LOG_TRACE) ai_log("handle_stance_tactical() : STANCE_ALERT, going to threaten_target().", AI_LOG_TRACE)
threaten_target() threaten_target()
@@ -241,9 +276,23 @@
if(STANCE_IDLE) if(STANCE_IDLE)
if(speak_chance) // In the long loop since otherwise it wont shut up. if(speak_chance) // In the long loop since otherwise it wont shut up.
handle_idle_speaking() handle_idle_speaking()
if(hostile) if(hostile)
ai_log("handle_stance_strategical() : STANCE_IDLE, going to find_target().", AI_LOG_TRACE) ai_log("handle_stance_strategical() : STANCE_IDLE, going to find_target().", AI_LOG_TRACE)
find_target() find_target()
if(should_go_home())
ai_log("handle_stance_tactical() : STANCE_IDLE, going to go home.", AI_LOG_TRACE)
go_home()
else if(should_follow_leader())
ai_log("handle_stance_tactical() : STANCE_IDLE, going to follow leader.", AI_LOG_TRACE)
set_stance(STANCE_FOLLOW)
else if(should_wander())
ai_log("handle_stance_tactical() : STANCE_IDLE, going to wander randomly.", AI_LOG_TRACE)
handle_wander_movement()
if(STANCE_APPROACH) if(STANCE_APPROACH)
if(target) if(target)
ai_log("handle_stance_strategical() : STANCE_APPROACH, going to calculate_path([target]).", AI_LOG_TRACE) ai_log("handle_stance_strategical() : STANCE_APPROACH, going to calculate_path([target]).", AI_LOG_TRACE)
@@ -292,3 +341,6 @@
/mob/living/proc/taunt(atom/movable/taunter, force_target_switch = FALSE) /mob/living/proc/taunt(atom/movable/taunter, force_target_switch = FALSE)
if(ai_holder) if(ai_holder)
ai_holder.receive_taunt(taunter, force_target_switch) ai_holder.receive_taunt(taunter, force_target_switch)
#undef AI_PROCESSING
#undef AI_FASTPROCESSING

View File

@@ -6,7 +6,7 @@
// If our holder is able to do anything. // If our holder is able to do anything.
/datum/ai_holder/proc/can_act() /datum/ai_holder/proc/can_act()
if(!holder) // Holder missing. if(!holder) // Holder missing.
SSai.processing -= src manage_processing(AI_NO_PROCESS)
return FALSE return FALSE
if(holder.stat) // Dead or unconscious. if(holder.stat) // Dead or unconscious.
ai_log("can_act() : Stat was non-zero ([holder.stat]).", AI_LOG_TRACE) ai_log("can_act() : Stat was non-zero ([holder.stat]).", AI_LOG_TRACE)

View File

@@ -57,7 +57,7 @@
ai_log("lose_follow() : Exited.", AI_LOG_DEBUG) ai_log("lose_follow() : Exited.", AI_LOG_DEBUG)
/datum/ai_holder/proc/should_follow_leader() /datum/ai_holder/proc/should_follow_leader()
if(!leader) if(!leader || target)
return FALSE return FALSE
if(follow_until_time && world.time > follow_until_time) if(follow_until_time && world.time > follow_until_time)
lose_follow() lose_follow()

View File

@@ -43,6 +43,8 @@
ai_log("walk_to_destination() : Exiting.",AI_LOG_TRACE) ai_log("walk_to_destination() : Exiting.",AI_LOG_TRACE)
/datum/ai_holder/proc/should_go_home() /datum/ai_holder/proc/should_go_home()
if(stance != STANCE_IDLE)
return FALSE
if(!returns_home || !home_turf) if(!returns_home || !home_turf)
return FALSE return FALSE
if(get_dist(holder, home_turf) > max_home_distance) if(get_dist(holder, home_turf) > max_home_distance)
@@ -139,7 +141,7 @@
return MOVEMENT_ON_COOLDOWN return MOVEMENT_ON_COOLDOWN
/datum/ai_holder/proc/should_wander() /datum/ai_holder/proc/should_wander()
return wander && !leader return (stance == STANCE_IDLE) && wander && !leader
// Wanders randomly in cardinal directions. // Wanders randomly in cardinal directions.
/datum/ai_holder/proc/handle_wander_movement() /datum/ai_holder/proc/handle_wander_movement()

View File

@@ -25,8 +25,8 @@
// Step 1, find out what we can see. // Step 1, find out what we can see.
/datum/ai_holder/proc/list_targets() /datum/ai_holder/proc/list_targets()
. = hearers(vision_range, holder) - holder // Remove ourselves to prevent suicidal decisions. ~ SRC is the ai_holder. . = ohearers(vision_range, holder)
. -= dview_mob // Not the dview mob either, nerd. . -= dview_mob // Not the dview mob!
var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/blob)) var/static/hostile_machines = typecacheof(list(/obj/machinery/porta_turret, /obj/mecha, /obj/structure/blob))
@@ -43,13 +43,8 @@
if(!has_targets_list) if(!has_targets_list)
possible_targets = list_targets() possible_targets = list_targets()
for(var/possible_target in possible_targets) for(var/possible_target in possible_targets)
var/atom/A = possible_target if(can_attack(possible_target)) // Can we attack it?
if(found(A)) // In case people want to override this. . += possible_target
. = list(A)
break
if(can_attack(A)) // Can we attack it?
. += A
continue
var/new_target = pick_target(.) var/new_target = pick_target(.)
give_target(new_target) give_target(new_target)
@@ -57,7 +52,7 @@
// Step 3, pick among the possible, attackable targets. // Step 3, pick among the possible, attackable targets.
/datum/ai_holder/proc/pick_target(list/targets) /datum/ai_holder/proc/pick_target(list/targets)
if(target != null) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets. if(target) // If we already have a target, but are told to pick again, calculate the lowest distance between all possible, and pick from the lowest distance targets.
targets = target_filter_distance(targets) targets = target_filter_distance(targets)
else else
targets = target_filter_closest(targets) targets = target_filter_closest(targets)
@@ -88,32 +83,31 @@
// Filters return one or more 'preferred' targets. // Filters return one or more 'preferred' targets.
// This one is for closest targets. // This one is for targets closer than our current one.
/datum/ai_holder/proc/target_filter_distance(list/targets) /datum/ai_holder/proc/target_filter_distance(list/targets)
var/target_dist = get_dist(holder, target)
var/list/better_targets = list()
for(var/possible_target in targets) for(var/possible_target in targets)
var/atom/A = possible_target var/atom/A = possible_target
var/target_dist = get_dist(holder, target)
var/possible_target_distance = get_dist(holder, A) var/possible_target_distance = get_dist(holder, A)
if(target_dist < possible_target_distance) if(possible_target_distance < target_dist)
targets -= A better_targets += A
return targets return better_targets
// Returns the closest target and anything tied with it for distance
/datum/ai_holder/proc/target_filter_closest(list/targets) /datum/ai_holder/proc/target_filter_closest(list/targets)
var/lowest_distance = -1 var/lowest_distance = 1e6 //fakely far
var/list/sorted_targets = list() var/list/closest_targets = list()
for(var/possible_target in targets) for(var/possible_target in targets)
var/atom/A = possible_target var/atom/A = possible_target
var/current_distance = get_dist(holder, A) var/current_distance = get_dist(holder, A)
if(lowest_distance == -1) if(current_distance < lowest_distance)
closest_targets.Cut()
lowest_distance = current_distance lowest_distance = current_distance
sorted_targets += A closest_targets += A
else if(current_distance < lowest_distance)
targets.Cut()
lowest_distance = current_distance
sorted_targets += A
else if(current_distance == lowest_distance) else if(current_distance == lowest_distance)
sorted_targets += A closest_targets += A
return sorted_targets return closest_targets
/datum/ai_holder/proc/can_attack(atom/movable/the_target, var/vision_required = TRUE) /datum/ai_holder/proc/can_attack(atom/movable/the_target, var/vision_required = TRUE)
if(!can_see_target(the_target) && vision_required) if(!can_see_target(the_target) && vision_required)
@@ -163,11 +157,6 @@
return TRUE return TRUE
// return FALSE // return FALSE
// Override this for special targeting criteria.
// If it returns true, the mob will always select it as the target.
/datum/ai_holder/proc/found(atom/movable/the_target)
return FALSE
// 'Soft' loss of target. They may still exist, we still have some info about them maybe. // 'Soft' loss of target. They may still exist, we still have some info about them maybe.
/datum/ai_holder/proc/lose_target() /datum/ai_holder/proc/lose_target()
ai_log("lose_target() : Entering.", AI_LOG_TRACE) ai_log("lose_target() : Entering.", AI_LOG_TRACE)

View File

@@ -256,14 +256,10 @@
for(var/possible_target in possible_targets) for(var/possible_target in possible_targets)
var/atom/A = possible_target var/atom/A = possible_target
if(found(A))
. = list(A)
break
if(istype(A, /mob/living) && !can_pick_mobs) if(istype(A, /mob/living) && !can_pick_mobs)
continue continue
if(can_attack(A)) // Can we attack it? if(can_attack(A)) // Can we attack it?
. += A . += A
continue
for(var/obj/item/I in .) for(var/obj/item/I in .)
last_search = world.time last_search = world.time

View File

@@ -225,6 +225,7 @@
#include "code\controllers\verbs.dm" #include "code\controllers\verbs.dm"
#include "code\controllers\observer_listener\atom\observer.dm" #include "code\controllers\observer_listener\atom\observer.dm"
#include "code\controllers\subsystems\ai.dm" #include "code\controllers\subsystems\ai.dm"
#include "code\controllers\subsystems\aifast.dm"
#include "code\controllers\subsystems\air.dm" #include "code\controllers\subsystems\air.dm"
#include "code\controllers\subsystems\airflow.dm" #include "code\controllers\subsystems\airflow.dm"
#include "code\controllers\subsystems\alarm.dm" #include "code\controllers\subsystems\alarm.dm"