|
member
|
OP
member
Joined: Dec 2016
|
There was some interest in the AI improvements that I developed for my upcoming patch to Epic Encounters, which solves the problem of enemies "stalling" during their turns. I will outline here the issues addressed by my modifications and the implementation thereof. There are other improvements that were previously made (Ice Wall destruction, generic priority improvements), as well as various considerations for performance that I will not discuss here (the scripts get quite long)--this is to present the system's logic. Of course, if anyone is interested in the full scripts, I can upload and provide them Suggestions are of course welcome  The AI stalls when it is asked to move and the pathing algorithm cannot find a valid location to satisfy the request. In theory, the scripting should throw a MovementFailed interrupt, which is caught by the behavior to end and block it. However, the MovementFailed interrupt is usually or never thrown by CharacterMoveInRange(); this is the core of the problem. Generally, CharacterMoveInRange() is called from spell wrapper scripts. In my scripting, I chose to address this by building my own interrupt for these behaviors. This is done via timer checks during the character's turn--if the character's AP or position doesn't change between ticks, I assume that it is doing nothing. The timer checks if the character has flagged itself as attempting a movement behavior, if it did, then the timer fires an interrupt. This same timer also ends a character's turn if no activity is detected for too long. Here is the activity detection timer code:
EVENT DefaultActivityDetection
//Used as a means to reliably detect when a character
//is timing out.
VARS
FLOAT:_AP
FLOAT3:_Pos
FLOAT3:_OldPos
ON
OnTimer("AMER_ActivityDetection")
ACTIONS
IF "c1&c2"
GetPosition(__Me, _Pos)
CharacterGetStat(_AP, __Me, ActionPoints)
THEN
IF "c1&c2"
IsEqual(_Pos, _OldPos)
IsEqual(_AP, %AMER_TimeoutOldAP)
THEN
// Need to check if the character is in dialogue, because
//some characters will be in combat while also in dialogue.
IF "!c1"
IsInDialog(__Me)
THEN
Add(%AMER_TimeoutDetection, 1)
//If a behavior has flagged that it may be doing something that
//could stall, interrupt it, if it has started stalling.
IF "!c1"
IsEqual(%AMER_InterruptBehavior, null)
THEN
IF "c1"
IsGreaterThen(%AMER_TimeoutDetection, 2)
THEN
//StatusText(__Me, "AMER_TEST_5") //TEST
Interrupt(%AMER_InterruptBehavior)
ENDIF
ENDIF
//Non-bosses get 4 ticks before timeout. Bosses get 12 ticks before
//timeout because they tend to have long scripted components.
IF "c1"
IsGreaterThen(%AMER_TimeoutDetection, INT:4)
THEN
IF "!c1"
CharacterIsBoss(__Me)
THEN
CharacterPlayEffect(__Me, "FX_GP_Status_SkipTurn_A_Icon", "Dummy_OverheadFX")
StatusText(__Me, "AMER_TEST_TurnTimeout") //TEST
CharacterEndTurn()
ELIF "c1"
IsGreaterThen(%AMER_TimeoutDetection, INT:12) //Bosses have extra time.
THEN
CharacterPlayEffect(__Me, "FX_GP_Status_SkipTurn_A_Icon", "Dummy_OverheadFX")
StatusText(__Me, "AMER_TEST_TurnTimeout") //TEST
CharacterEndTurn()
ENDIF
ENDIF
ELSE
//Give extra time if this character is in a dialogue.
Set(%AMER_TimeoutDetection, INT:-6)
ENDIF
ELSE
Set(%AMER_TimeoutOldAP, _AP)
Set(_OldPos, _Pos)
//External behaviors may set this below zero.
IF "c1"
IsGreaterThen(%AMER_TimeoutDetection, INT:0)
THEN
Set(%AMER_TimeoutDetection, INT:0)
ENDIF
ENDIF
ENDIF To use this interrupt, we must provide the behavior reaction name before using CharacterMoveInRange(): SetVar(__Me, "AMER_InterruptBehavior", FIXEDSTRING:CastOnCharacter_$1)
CharacterMoveInRange(_Target, _minRange, _maxRange, 1)
SetVar(__Me, "AMER_InterruptBehavior", FIXEDSTRING:null) Additionally, code to handle the interrupt must be added to a generic INTERRUPT block of the behavior reaction--because OnManualInterrupt() doesn't appear to work.
INTERRUPT
//OnManualInterrupt doesn't seem to work. Catch the manual
//interupt behavior in this manner instead.
IF "c1&!c2"
GetVar(_Interrupt, __Me, "AMER_InterruptBehavior")
IsEqual(_Interrupt, null)
THEN
//StatusText(__Me, "AMER_TEST_2") //TEST
SetVar(__Me, "AMER_InterruptBehavior", FIXEDSTRING:null)
SetVar(__Me, "AMER_TimeoutDetection", INT:0) //Prevent timeout while deliberating.
DelayReaction("CastOnCharacter_$1", 2)
ENDIF With the former code in place, our enemies will no longer remain stalled for too long. However this is not yet ideal behavior, as they may still end up passing many turns if they continue to attempt to target the same unreachable target. To remedy this problem, we need to implement a means for the AI to recalculate its target priority while ignoring unreachable targets. I have done this with a blacklist infrastructure. The blacklist is composed of enough int variables to handle however many player indices might be present in combat--one for each player and one for each of their possible summons--I use twelve to support up to six players. Additionally, there is one generic character variable to support blacklisting of one NPC (relevant for charmed targets or combat with friendly NPCs). These variables should be reset any time the state of pathing could change; this commonly means at the beginning of a turn, but can also occur when the character destroys an Ice Wall or uses a movement spell. This is the code used to blacklist a target--called from interrupts of movement failure:
INT:%AMER_BlacklistUsed = 0
INT:%AMER_BlacklistIndex0 = 0
INT:%AMER_BlacklistIndex1 = 0
INT:%AMER_BlacklistIndex2 = 0
INT:%AMER_BlacklistIndex3 = 0
INT:%AMER_BlacklistIndex4 = 0
INT:%AMER_BlacklistIndex5 = 0
INT:%AMER_BlacklistIndex6 = 0
INT:%AMER_BlacklistIndex7 = 0
INT:%AMER_BlacklistIndex8 = 0
INT:%AMER_BlacklistIndex9 = 0
INT:%AMER_BlacklistIndex10 = 0
INT:%AMER_BlacklistIndex11 = 0
CHARACTER:%AMER_BlacklistGeneric = null
CHARACTER:%AMER_BlacklistCmp = null
EVENT AttackMovementFailedDeliberation
//If my target is an unreachable player, blacklist it
//and flag to reevaluate targets. If all players are
//blacklisted, check for Ice Walls.
VARS
CHARACTER:_Enemy
CHARACTER:_Player
INT:_Count
INT:_PartySize
STRING:_Str
FIXEDSTRING:_BlacklistVar
ON
OnFunction("AMER_AttackMovementFailedDeliberation")
ACTIONS
//Prevent timeout when deliberation is happening.
//Set(%AMER_TimeoutDetection, INT:0)
IF "c1"
GetPlayerCount(_PartySize)
THEN
//StatusText(__Me, "AMER_ChainOne") //TEST
IF "c1&c2"
CharacterGetEnemy(_Enemy, __Me)
CharacterIsPlayer(_Enemy)
THEN
Set(_Count, INT:0)
WHILE "c1"
IsLessThen(_Count, _PartySize)
DO
IF "c1&c2"
GetPlayerByIndex(_Player, _Count)
IsEqual(_Player, _Enemy)
THEN
//StatusText(_Player, "AMER_ExtraCrit") //TEST
Print(_Str, "AMER_BlacklistIndex[1]", _Count)
Cast(_BlacklistVar, _Str)
SetVar(__Me, _BlacklistVar, INT:1)
Add(%AMER_BlacklistUsed, INT:1)
//Flag to reevaluate targets.
CharacterSetEnemy(__Me, null)
Set(%defaultEvaluateTarget, INT:1)
ENDIF
Add(_Count, INT:1)
ENDWHILE
//If it was an NPC and I haven't blacklisted one
//yet, blacklist it and reevaluate.
ELIF "c1"
IsEqual(%AMER_BlacklistGeneric, null)
THEN
//StatusText(_Enemy, "AMER_ExtraCrit") //TEST
Set(%AMER_BlacklistGeneric, _Enemy)
//Flag to reevaluate targets.
CharacterSetEnemy(__Me, null)
Set(%defaultEvaluateTarget, INT:1)
ENDIF
ENDIF The system checks if the unreachable target was a player and, if it was, obtains the character's playerindex and flags the corresponding blacklist variable. The blacklist variables are considered in the default attack target deliberation routine when any of them have been flagged. Here is the code for this check, called from any target deliberation routine before recording a target:
EVENT DefaultCheckIfBlacklisted
//Compare %AMER_BlacklistCmp against the active player characters
//to see if it has been blacklisted.
VARS
CHARACTER:_Player
INT:_Count
INT:_PartySize
INT:_IsBlacklisted
STRING:_Str
FIXEDSTRING:_BlacklistVar
ON
OnFunction("AMER_CheckIfBlacklisted")
ACTIONS
IF "c1"
GetPlayerCount(_PartySize)
THEN
Set(_Count, INT:0)
WHILE "c1&!c2"
IsLessThen(_Count, _PartySize)
IsEqual(%AMER_BlacklistCmp, null)
DO
IF "c1&c2"
GetPlayerByIndex(_Player, _Count)
IsEqual(_Player, %AMER_BlacklistCmp)
THEN
Print(_Str, "AMER_BlacklistIndex[1]", _Count)
Cast(_BlacklistVar, _Str)
IF "c1&c2"
GetVar(_IsBlacklisted, __Me, _BlacklistVar)
IsEqual(_IsBlacklisted, INT:1)
THEN
//Character is blacklisted, set to null to escape
//loop and denote that character should be ignored.
Set(%AMER_BlacklistCmp, null)
ENDIF
ENDIF
Add(_Count, INT:1)
ENDWHILE
ENDIF With the addition of this "custom" interrupt and the blacklist system, characters will only pass their turns if every potential target is unreachable, and should never be met with a situation that does not generate an interrupt from movement failure. Since the implementation of this AI, I have yet to witness a single creature's turn ever stall, even when considering extremely tight scenarios with many characters. While this AI does greatly improve combat performance of enemies, the implications of this system are grand; one could tailor movement spells and teleport spells to be used only when high-priority targets are unreachable, for example.
Last edited by Ameranth; 29/03/17 09:23 AM.
|
|
|
|
apprentice
|
apprentice
Joined: Feb 2010
|
Are you Sandpie on Steam? If so, I am testing your mod now. OMG is it awesome! I am using XC encounters actually. Can't wait for new version. I'm so glad someone else is making a nice mod for EE.
ok
|
|
|
|
member
|
OP
member
Joined: Dec 2016
|
I am, thanks for playing!
|
|
|
Moderated by Bvs, ForkTong, gbnf, Issh, Kurnster, Larian_QA, LarSeb, Lar_q, Lynn, Monodon, Raze, Stephen_Larian
|
|