WB Coding Questions about "try_for_agents" performance and "agent_set_attack_action" function.

Users who are viewing this thread

Cokjan

Squire
WBNW
1. Try for agents
I've seen a lot posts that said try_for_agents can burden the engine and cause quite the stutter when there's a lot of npcs. Question is, is it caused just from the try_for_agents operation, or is it caused by the data gathered from the follow up operations (like agent_get_wielded_item etc) under try_for_agents? I got the impression that its the latter because of this code I found from this mod but still unsure because it still iterates every agent in the scene right?
common_battle_morale_check = (
0.1, 0, 0, [
(store_mission_timer_a_msec,":mission_time_ms"),
(ge,":mission_time_ms",10000),
(store_div,":mission_time_s",":mission_time_ms",1000),
(store_div,":mission_time_ticks",":mission_time_ms",100),
(try_for_agents, ":agent_no"),
(agent_is_active, ":agent_no"),
(agent_is_human, ":agent_no"),
(agent_is_alive, ":agent_no"),
# Only check every 50th agent each tick to reduce stutter
(store_mod,":ticks_mod",":mission_time_ticks",50),
(store_mod,":agent_mod",":agent_no",50),
(eq,":agent_mod",":ticks_mod"),
(call_script, "script_decide_run_away_or_not", ":agent_no", ":mission_time_s"),
(try_end),
], [])

2. agent_set_attack_action
From the enhanced header operation, I read that when agent_set_attack_action's dir value is set to -2 (per 1.153 patch), the agent will cancel their action. But for some reason it doesn't work, was it removed or something?
 
Solution
1. It still iterates through all agents in the scene, yes. With the modulo filter the author only reduces latter appearing stutter which happens through the script script_decide_run_away_or_not (which probably contains multiple try_for_agents loops).

2. I have seen scripts with (agent_set_attack_action, ":agents", -2, -2), and with (agent_set_attack_action, ":rider", -2, 1), which seems to have worked normally. How does your script look like?

Eärendil Ardamírë

Subforum Moderator
WBWF&SM&B
1. It still iterates through all agents in the scene, yes. With the modulo filter the author only reduces latter appearing stutter which happens through the script script_decide_run_away_or_not (which probably contains multiple try_for_agents loops).

2. I have seen scripts with (agent_set_attack_action, ":agents", -2, -2), and with (agent_set_attack_action, ":rider", -2, 1), which seems to have worked normally. How does your script look like?
 
Upvote 1
Solution

Cokjan

Squire
WBNW
1. Ah, thank you for the answer Earendil. Seems like setting up a radius around a set position like the player's cam is the best way to go, whoever added that is a lifesaver :razz:. Or maybe experiment with dividing the map into four quadrants for a more global approach

2. Something like this (edit: oh god the indentation isn't copied over, I'll bold it for ease of reading)
(
0.05, 0, 0, [(eq, "$cox_ai", 1)], [
# player setup
(mission_cam_get_position, pos22),
(get_player_agent_no, ":pla"),
(store_mission_timer_b, ":timer_b"),

# agent setup
(try_for_agents, ":ags", pos22, 5000), # Radius of 50 metres.
(neq, ":ags", ":pla"), #Agent is not player
(agent_is_active, ":ags"),
(agent_is_alive, ":ags"),
(agent_is_human, ":ags"),
(agent_get_horse, ":ags_horse", ":ags"),
(eq, ":ags_horse", -1), # isn't a horseman
(agent_get_wielded_item, ":ags_weap", ":ags"),
# (item_get_type, ":ags_weap_type", ":ags_weap"),

(agent_slot_eq, ":ags", slot_agent_is_running_away, 0), #isn't fleeing
(agent_get_troop_id, ":agstrp",":ags"),

(agent_get_team, ":ags_team", ":ags"),

(agent_ai_get_look_target,":possible",":ags"),
(gt,":possible",0), #there is at least someone

(agent_is_active, ":possible"),
(agent_is_alive, ":possible"),
(agent_is_human, ":possible"),
(agent_get_horse, ":pos_horsed", ":possible"),

(agent_get_team, ":possible_team", ":possible"),
(neq, ":possible_team", ":ags_team"), #unfriendly teams

(agent_get_animation, ":ags_anim", ":ags"),
(agent_get_animation, ":pos_anim", ":possible"),

(agent_get_position, pos11, ":ags"),
(agent_get_position, pos12, ":possible"),
(neg|position_is_behind_position, pos12, pos11), #enemy cannot be behind agent
(get_distance_between_positions, ":dist", pos11, pos12),
(set_fixed_point_multiplier, 100), # everything is in centimeters

(agent_get_defend_action, ":ags_def", ":ags"), #defend action free = 0, parrying = 1, blocking = 2.
(agent_get_action_dir, ":ags_atkdir", ":ags"),
(agent_get_attack_action, ":ags_atk", ":ags"), #attack action free = 0, readying_attack = 1, releasing_attack = 2, completing_attack_after_hit = 3, attack_parried = 4, reloading = 5, after_release = 6,

(agent_get_defend_action, ":pos_def", ":possible"), #defend action
(agent_get_action_dir, ":pos_atkdir", ":possible"),
(agent_get_attack_action, ":pos_atk", ":possible"), #attack action

# FOOTWORK
(try_begin),
(lt, ":dist", 500), # Get info only when under 5 metres
(agent_slot_ge, ":ags", slot_agent_cox_polearm, 1),
(agent_get_slot, ":spear_length", ":ags", slot_agent_cox_polearm),
(agent_get_speed, pos13, ":ags"),
(position_get_x, ":leftright", pos13),
(position_get_y, ":frontback", pos13),
(try_begin),
(le, ":dist", ":spear_length"),
(eq, ":frontback", 0),
(try_begin),
(neg|agent_slot_ge, ":ags", slot_agent_cox_athletics, 3),
(agent_set_animation, ":ags", "anim_cox_backward_polearm_slow", 0),
(else_try),
(agent_slot_ge, ":ags", slot_agent_cox_athletics, 4),
(agent_set_animation, ":ags", "anim_cox_backward_polearm_fast", 0),
(try_end),
(else_try),
(le, ":dist", ":spear_length"),
(gt, ":frontback", 0),
(agent_set_animation, ":ags", "anim_cox_stop_polearm", 0),
(try_end),
(try_end),

(try_begin), #CHAMBERING
(le, ":dist", 200),
(neq, ":pos_atk", 1),
(eq, ":pos_atk", 2),
(agent_set_attack_action, ":ags", -2, 0),
(try_begin),
(eq, ":pos_atkdir", 0),
(agent_set_attack_action, ":ags", 0, 0),
(display_message, "@ Chamber"), #debug
(else_try),
(eq, ":pos_atkdir", 1),
(agent_set_attack_action,":ags", 2, 0),
(display_message, "@ Chamber"), #debug
(else_try),
(eq, ":pos_atkdir", 2),
(agent_set_attack_action, ":ags", 1, 0),
(display_message, "@ Chamber"), #debug
(else_try),
(eq, ":pos_atkdir", 3),
(agent_set_attack_action, ":ags", 3, 0),
(display_message, "@ Chamber"), #debug
(try_end),
(try_end),

(try_end), #TRY FOR AGENT END
]),

Something else I remember that bugs me to hell is that, when a weapon can't attack at certain direction (like pike not able to attack left/right), the agent will get stuck which is funny. Current method I found is by checking the current wielded weapon, do you know if there's another less complicated solution?
 
Upvote 0
Question is, is it caused just from the try_for_agents operation, or is it caused by the data gathered from the follow up operations (like agent_get_wielded_item etc) under try_for_agents?

Its the latter. Most individual operations take the same amount of time to execute, and the engine will cut a loop short if one of the operations is false, so if you can drastically limit the amount of stuff inside an average loop by putting the modulo check first. There is no point checking if the agent is alive, if 49/50 times it's going to fail the modulo check anyway.
 
Upvote 1

Eärendil Ardamírë

Subforum Moderator
WBWF&SM&B
Ah, yes, you should also take the comment of Jacobhinds above into account, it improves the loop.

At your code (please don't use spoilers, use the code environment) try it with -2 or 1 being the second value as in my examples mentioned above.
Code:
(try_begin), #CHAMBERING
  (le, ":dist", 200),
  (neq, ":pos_atk", 1),
  (eq, ":pos_atk", 2),
  (agent_set_defend_action, ":ags", 0, 0),
  (agent_set_attack_action, ":ags", -2, 0),
  (try_begin),
    (eq, ":pos_atkdir", 0),
    (agent_set_attack_action, ":ags", 0, 0),
    (display_message, "@ Chamber"), #debug
  (else_try),
    (eq, ":pos_atkdir", 1),
    (agent_set_attack_action,":ags", 2, 0),
    (display_message, "@ Chamber"), #debug
  (else_try),
    (eq, ":pos_atkdir", 2),
    (agent_set_attack_action, ":ags", 1, 0),
    (display_message, "@ Chamber"), #debug
  (else_try),
    (eq, ":pos_atkdir", 3),
    (agent_set_attack_action, ":ags", 3, 0),
    (display_message, "@ Chamber"), #debug
  (try_end),
(try_end),
 
Upvote 1

Cokjan

Squire
WBNW
Thanks a lot for the help again Jacob. I can rest more easily now with the confirmation :smile:
Hmm setting the second value to -2 or 1 doesn't seem to work either
Code:
        # Cancel test
        (try_begin),
        (le, ":dist", 200),
        (this_or_next|eq, ":ags_atk", 0), #just for testing
        (this_or_next|eq, ":ags_atk", 1),
        (eq, ":ags_atk", 2),
        (agent_set_attack_action, ":ags", -2, -2),
        (display_message, "@ Cancel"), #debug
        (try_end),
 
Upvote 0

Vetrogor

Squire
First read about try_for_agents operator.

Besides using position with radius you can use cashed enemies.
(agent_ai_get_cached_enemy, <destination>, <agent_no>, <cache_index>),

You can also use more frames. Triggers are evaluating each frame.

This is very basic optimisation and should be used most of the time. If you want more challenge for your brains - create dynamic agents lists based on Lav's agent finder.
 
Upvote 1

Cokjan

Squire
WBNW
Ooh thanks for the link, Vetrogor. There are information there for try_for_agents not available in lav's header operation file.

I didn't quite understand the part with cached enemies, do you mean something like this?
Code:
something_something_combat_ai = (
  0.01, 0, 0, [
  (store_mission_timer_a_msec,":mission_time_ms"),
  (ge,":mission_time_ms",10000),
  (store_div,":mission_time_s",":mission_time_ms",1000),
  (store_div,":mission_time_ticks",":mission_time_ms",100),

    (try_for_agents, ":agent_no"),
        # Only check every 50th agent each tick to reduce stutter
        (store_mod,":ticks_mod",":mission_time_ticks",50),
        (store_mod,":agent_mod",":agent_no",50),
        (eq,":agent_mod",":ticks_mod"),
        (agent_is_human, ":agent_no"),
        (agent_is_alive, ":agent_no"),
        # insert code for gathering agent info

        (agent_ai_get_cached_enemy, ":cached_enemy", ":agent_no", 1), #i'm not sure what cache index is, does 1 means the nearest cached enemy?
        (agent_is_human, ":cached_enemy"),
        (agent_is_alive, ":cached_enemy"),
        # insert code for gathering enemy info

        (try_begin),
            # consequence block
        (try_end),

    (try_end),   
      ], [])

So in essence, each 0.01 seconds, a random agent chosen by modulo is checked, and 1 cached enemy is checked too. So that means 2 agents checked each 0.01 seconds, which also meant that in a full 1 second, 200 agents would be checked? Do I get that right? I mean its definitely an improvement over checking all agents each 0.01 seconds, albeit with a tradeoff for consistency.
 
Upvote 0
So in essence, each 0.01 seconds, a random agent chosen by modulo is checked, and 1 cached enemy is checked too. So that means 2 agents checked each 0.01 seconds, which also meant that in a full 1 second, 200 agents would be checked? Do I get that right? I mean its definitely an improvement over checking all agents each 0.01 seconds, albeit with a tradeoff for consistency.
In theory yes, but avoid setting triggers that run less than once every frame (0.015), I think some of them can get skipped. 0.1 is better in my experience.

Also you could put the ticks_mod bit outside the loop since it isnt going to change while the loop is running. I would also replace the raw mission timer with a global variable that increments by 1 every time the loop runs. Mission time might be inconsistent with trigger and ingame time, leading to the modulo failing sometimes if youre just reading from the mission timer.
 
Upvote 1

Vetrogor

Squire
I didn't quite understand the part with cached enemies, do you mean something like this?

Python:
something_something_combat_ai = [
    (ti_before_mission_start, 0, 0, [],[
        (assign, "$prev_ai_time", 0),
        (assign, "$time_ticks", 0),
    ]),

    (0, 0, 0, [],[
        (assign, ":delay", 300), #0.3 msec
        (assign, ":mod", 10), #number of frames
        (store_mission_timer_a_msec, ":time_msec"),
        (val_sub, ":time_msec", "$prev_ai_time"), #Need to assign to 0 in ti_before_mission_start
        (gt, ":time_msec", ":delay"), #Trigger delay optional
        (val_add, "$time_ticks", 1),
        (try_begin),
            (eq, "$time_ticks", ":mod"), #The last frame
            (store_mission_timer_a_msec, "$prev_ai_time"),
        (try_end),
        (val_mod, "$time_ticks", ":mod"),
        (try_for_agents, ":agent_no"),
            # Only check every 10th agent each tick to reduce stutter
            (store_mod,":agent_mod",":agent_no", ":mod"),
            (eq,":agent_mod","$time_ticks"),
            (agent_is_human, ":agent_no"),
            (agent_is_alive, ":agent_no"),
            (agent_ai_get_behavior_target, ":enemy", ":agent_no"),
            (gt, ":enemy", -1),
            (agent_is_alive, ":enemy"),
            #Your melee AI block
        (try_end),
    ]),
]

Python:
something_something_combat_ai = [
    (ti_before_mission_start, 0, 0, [],[
        (assign, "$prev_ai_time", 0),
        (assign, "$time_ticks", 0),
    ]),

    (0, 0, 0, [],[
        (assign, ":delay", 300), #0.3 msec
        (assign, ":mod", 10), #number of frames
        (store_mission_timer_a_msec, ":time_msec"),
        (val_sub, ":time_msec", "$prev_ai_time"), #Need to assign to 0 in ti_before_mission_start
        (gt, ":time_msec", ":delay"), #Trigger delay 0.5 seconds - optional
        (val_add, "$time_ticks", 1),
        (try_begin),
            (eq, "$time_ticks", ":mod"), #The last frame
            (store_mission_timer_a_msec, "$prev_ai_time"),
        (try_end),
        (val_mod, "$time_ticks", ":mod"),
        (try_for_agents, ":agent_no"),
            # Only check every 10th agent each tick to reduce stutter
            (store_mod,":agent_mod",":agent_no", ":mod"),
            (eq,":agent_mod","$time_ticks"),
            (agent_is_human, ":agent_no"),
            (agent_is_alive, ":agent_no"),
            (agent_ai_get_behavior_target, ":enemy", ":agent_no"),
            #Example of using cahced enemies.
            (try_begin),
                (this_or_next|eq, ":enemy", -1),
                (neg|agent_is_alive, ":enemy"),
                (agent_ai_get_num_cached_enemies, ":cached_num", ":agent_no"),
                (assign, ":closest_enemy", -1),
                (assign, ":closest_distance", 1000000),
                (agent_get_position, pos1, ":agent_no"),
                (try_for_range, ":cached_enemy", 0, ":cached_num"),
                    (agent_get_position, pos2, ":cached_enemy"),
                    (get_distance_between_positions, ":distance", pos1, pos2),
                    (lt, ":distance", ":closest_distance"),
                    (agent_is_alive, ":closest_enemy"),
                    (assign, ":distance", ":closest_distance"),
                    (assign, ":closest_enemy", ":cached_enemy"),
                (try_end),
                (assign, ":enemy", ":closest_enemy"),
            (try_end),
            (gt, ":enemy", -1),
            #Your melee AI block
        
        (try_end),
    ]),
]

The engine creates a cache of 16 enemies every 2 seconds. This cache is sorted from nearest to farthest.
Use parameter "code=python" to show code.
 
Upvote 1

Cokjan

Squire
WBNW
In theory yes, but avoid setting triggers that run less than once every frame (0.015), I think some of them can get skipped. 0.1 is better in my experience.

I would also replace the raw mission timer with a global variable that increments by 1 every time the loop runs.
Alright, thanks for the suggestions! I did notice there's pretty much no "major" difference between 0.1 and 0, seems like setting it to 0 would make it a waste of computing power. And yes I forgot to put the ticks_mod outside the try agent loop lol

Python:
something_something_combat_ai = [
    (ti_before_mission_start, 0, 0, [],[
        (assign, "$prev_ai_time", 0),
        (assign, "$time_ticks", 0),
    ]),

    (0, 0, 0, [],[
        (assign, ":delay", 300), #0.3 msec
        (assign, ":mod", 10), #number of frames
        (store_mission_timer_a_msec, ":time_msec"),
        (val_sub, ":time_msec", "$prev_ai_time"), #Need to assign to 0 in ti_before_mission_start
        (gt, ":time_msec", ":delay"), #Trigger delay 0.5 seconds - optional
        (val_add, "$time_ticks", 1),
        (try_begin),
            (eq, "$time_ticks", ":mod"), #The last frame
            (store_mission_timer_a_msec, "$prev_ai_time"),
        (try_end),
        (val_mod, "$time_ticks", ":mod"),
        (try_for_agents, ":agent_no"),
            # Only check every 10th agent each tick to reduce stutter
            (store_mod,":agent_mod",":agent_no", ":mod"),
            (eq,":agent_mod","$time_ticks"),
            (agent_is_human, ":agent_no"),
            (agent_is_alive, ":agent_no"),
            (agent_ai_get_behavior_target, ":enemy", ":agent_no"),
            #Example of using cahced enemies.
            (try_begin),
                (this_or_next|eq, ":enemy", -1),
                (neg|agent_is_alive, ":enemy"),
                (agent_ai_get_num_cached_enemies, ":cached_num", ":agent_no"),
                (assign, ":closest_enemy", -1),
                (assign, ":closest_distance", 1000000),
                (agent_get_position, pos1, ":agent_no"),
                (try_for_range, ":cached_enemy", 0, ":cached_num"),
                    (agent_get_position, pos2, ":cached_enemy"),
                    (get_distance_between_positions, ":distance", pos1, pos2),
                    (lt, ":distance", ":closest_distance"),
                    (agent_is_alive, ":closest_enemy"),
                    (assign, ":distance", ":closest_distance"),
                    (assign, ":closest_enemy", ":cached_enemy"),
                (try_end),
                (assign, ":enemy", ":closest_enemy"),
            (try_end),
            (gt, ":enemy", -1),
            #Your melee AI block
    
        (try_end),
    ]),
]

The engine creates a cache of 16 enemies every 2 seconds. This cache is sorted from nearest to farthest.
Use parameter "code=python" to show code.
Wow, thank you for writing that code! I just tested it and it works. I did read about the engine automatically making a cache of enemies somewhere, but have no idea how to call the variables before.

Oh and by the way, it seems like using agent_ai_get_behavior_target lead to these errors. It can sometimes randomly disappear though.

Code:
 At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences.
SCRIPT WARNING ON OPCODE -2147481946: Invalid Agent ID: 6503696; LINE NO: 19:
 At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences.
SCRIPT WARNING ON OPCODE -2147481946: Invalid Agent ID: 0; LINE NO: 19:
 At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences.
SCRIPT WARNING ON OPCODE -2147481946: Invalid Agent ID: 6503696; LINE NO: 19:
 At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences. At Mission Template mst_lead_charge trigger no: 9 consequences.
SCRIPT WARNING ON OPCODE -2147481946: Invalid Agent ID: 0; LINE NO: 19:

Using agent_ai_get_look_target removes those errors, but a new one pops out saying error opcode 1702 (agent_is_alive). Which I assume is caused by (neg|agent_is_alive, ":enemy"), I'll play around to try and fix these lil bugs.

Thanks again, both of you.
 
Last edited:
Upvote 0

Vetrogor

Squire
After retrieving agent from agent_ai_get_look_target or agent_ai_get_behavior_target check agent_is_active (valid reference) and agent_is_alive.
 
Upvote 0
Top Bottom