AI Tutorial: Setting Up a Basic Melee AI

January 7th, 2013 Leave a comment Go to comments

Important Note: I am not using the CryEngine anymore, so this page is up “as-is”. It is probably very flawed and incomplete. And I won’t be able to provide any help about this topic, sorry. But feel free to discuss things in the comments.

This very basic tutorial will help you setting up a basic AI behavior for melee combat. Meaning that it will yield a behavior that you can assign to your entity character. The entity starts in idle mode, then seeks the player when it enters its field of view, starts approaching until a certain distance and starts attacking once it’s in range for its melee attack to make contact.

First I want to thank the_grim and all the community members from crydev.org for making a lot of this information available in the first place. This is only possible because of what I learned from them. Now let’s move on to the topic at hand!

 

The Selection Tree file:

To start with, we need to define a Selection Tree. This file contains a Variables section (user-defined variables), a SignalVariables section (the engine’s own hard-coded signals) and a Priority section, listing all the behaviors and their conditions:

<SelectionTrees>
  <SelectionTree name="Swordsman" type="BehaviorSelectionTree">
  <Variables>
   <Variable name="Alerted"/>
   <Variable name="AwareOfPlayer"/>
   <Variable name="IsAttackRange"/>
  </Variables>
  <SignalVariables>
   <Signal name="OnEnemySeen" variable="AwareOfPlayer" value="true"/>
   <Signal name="OnEnemySeen" variable="Alerted" value="true"/>
   <Signal name="OnLostSightOfTarget" variable="AwareOfPlayer" value="false"/>
   <Signal name="OnCloseContact" variable="IsAttackRange" value="true"/>
   <Signal name="OnCloseContact" variable="Alerted" value="true"/>
   <Signal name="OnCloseContact" variable="AwareOfPlayer" value="true"/>
   <Signal name="OnThreateningSoundHeard" variable="Alerted" value="true"/>
   <Signal name="OnReceivingDamage" variable="Alerted" value="true"/>
  </SignalVariables>
  <LeafTranslations />
  <Priority name="Root">
   <Leaf name="SwordsmanAttack" condition="IsAttackRange"/>
   <Leaf name="SwordsmanApproach" condition="AwareOfPlayer"/>
   <Leaf name="SwordsmanSeek" condition="Alerted"/>
   <Leaf name="SwordsmanIdle"/>
  </Priority>
 </SelectionTree>
</SelectionTrees>

In this case I’ll name the file “Swordsman.xml” and I’ll save it in …/Game/Scripts/AI/SelectionTrees/.

 

The GoalPipes file:

This is where we define what should happen and when. Notice that you’ll recognize the names of the GoalPipes, since they’re the ones we declared as “leaves” in the Selection Tree.

<GoalPipes>   
   <GoalPipe name="swordsman_idle">
      <Locate name="player"/>
   </GoalPipe>
   <GoalPipe name="swordsman_seek">
      <Locate name="player" blocking="false"/>
      <Script code="entity.Behavior:AnalyzeSituation(entity);"/>
   </GoalPipe>
   <GoalPipe name="swordsman_approach">
      <Locate name="player" blocking="false"/>
      <AcqTarget name="player" blocking="false"/>
      <Speed id="Run" blocking="true"/>
      <Stick blocking="true" distance="2.3" AlignBeforeMove="true" UseLastOp="true" LookAtLastOp="true"/>
      <Script code="entity.Behavior:AnalyzeSituation(entity);"/>
   </GoalPipe>
   <GoalPipe name="swordsman_attack">
      <Locate name="player" blocking="false"/>
      <AcqTarget name="player" blocking="false"/>
      <FireCmd mode="MeleeForced" useLastOp="true" timeout="1.5" blocking="true"/>
      <FireCmd mode="Off" blocking="false"/>
      <Approach time="0.25" UseLastOp="true" LookAtLastOp="true" blocking="true"/>
    <Script code="entity.Behavior:AnalyzeSituation(entity);"/>
   </GoalPipe>
</GoalPipes>

I’ll name this file “GoalPipesSwordsman.xml” and I’ll save it in …/Game/Scripts/AI/GoalPipes/.

 

The Personalities file:

Those files correspond to the GoalPipes we just defined. So there is one .lua file for each GoalPipe:

 

SwordsmanApproach.lua

-- SwordsmanApproach behavior.
-- Based on a file created by the_grim

local Behavior = CreateAIBehavior("SwordsmanApproach",
{
    Alertness = 2,

    Constructor = function (self, entity)
        Log("============")
        Log("Approaching!");
        Log("============")
        entity:SelectPipe(0, "swordsman_approach");    
    end,

    Destructor = function(self, entity)
    end,

    OnGroupMemberDiedNearest = function ( self, entity, sender,data)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);
    end,

    OnGroupMemberDied = function( self, entity, sender)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);        
    end,

    AnalyzeSituation = function (self, entity, sender, data)
        local range = 2.5;
        local distance = AI.GetAttentionTargetDistance(entity.id);

        Log("Distance in approach:");
        Log(distance);

        if(distance > (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", false);
        elseif(distance < (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", true);
        end

    end,
})

 

SwordsmanAttack.lua

-- SwordsmanAttack behavior by the_grim
-- Based on FogOfWarAttack by Francesco Roccucci

local Behavior = CreateAIBehavior("SwordsmanAttack",
{
    Alertness = 2,

    Constructor = function (self, entity)
        entity:MakeAlerted();
        entity:DrawWeaponNow();
        Log("=======")
        Log("Attack!")
        Log("=======")
        entity:SelectPipe(0,"swordsman_attack");
    end,

    OnGroupMemberDiedNearest = function ( self, entity, sender,data)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);
    end,

    OnGroupMemberDied = function( self, entity, sender)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);        
    end,

    AnalyzeSituation = function (self, entity, sender, data)
        local range = 2.5;
        local distance = AI.GetAttentionTargetDistance(entity.id);

        Log("Distance in attack:")
        Log(distance);

        if(distance > (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", false);
        elseif(distance < (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", true);
        end

    end,    
})

 

SwordsmanIdle.lua

-- SwordsmanIdle behavior
-- Created by the_grim

local Behavior = CreateAIBehavior("SwordsmanIdle",
{
    Alertness = 0,

    Constructor = function (self, entity)
        Log("Idling...");
        AI.SetBehaviorVariable(entity.id, "AwareOfPlayer", false);
        entity:SelectPipe(0,"swordsman_idle");
        entity:DrawWeaponNow();
    end,    

    Destructor = function(self, entity)
    end,

    OnGroupMemberDiedNearest = function ( self, entity, sender,data)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);
        end,
    OnGroupMemberDied = function( self, entity, sender)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);        
    end,

    AnalyzeSituation = function (self, entity, sender, data)
        local range = 2.5;
        local distance = AI.GetAttentionTargetDistance(entity.id);
        if(distance > (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", false);
        elseif(distance < (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", true);
        end

    end,
})

 

SwordsmanSeek.lua

-- SwordsmanSeek behavior
-- Created by the_grim

local Behavior = CreateAIBehavior("SwordsmanSeek",
{
    Alertness = 2,

    Constructor = function (self, entity)
        Log("Seeking!");
        entity:SelectPipe(0, "swordsman_seek");    
    end,

    Destructor = function(self, entity)
    end,

    OnGroupMemberDiedNearest = function ( self, entity, sender,data)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);
    end,
    OnGroupMemberDied = function( self, entity, sender)
        AI.SetBehaviorVariable(entity.id, "Alerted", true);        
    end,

    AnalyzeSituation = function (self, entity, sender, data)
        local range = 2.5;
        local distance = AI.GetAttentionTargetDistance(entity.id);
        if(distance > (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", false);
        elseif(distance < (range)) then
            AI.SetBehaviorVariable(entity.id, "IsAttackRange", true);
    end

    end,
})

We need to save those files in a folder with the name of the Selection Tree (in this case “Swordsman”) …/Game/Scripts/AI/Behaviors/Personalities/Swordsman/.

 

Registering the behavior:

Finally, we need to tell the engine that this new type of behavior should be available in-game, and to do so, we need to register it in the PipesManager.lua file, located in …/Game/Scripts/AI/GoalPipes/. Just find the following section and add a line at the end to point towards your GoalPipes file:

    -- Specific (Xml)
    AI.LoadGoalPipes("Scripts/AI/GoalPipes/GoalPipesFogOfWar.xml");
    AI.LoadGoalPipes("Scripts/AI/GoalPipes/GoalPipesBasicAI.xml");
    AI.LoadGoalPipes("Scripts/AI/GoalPipes/GoalPipesCivilian.xml");    
    AI.LoadGoalPipes("Scripts/AI/GoalPipes/GoalPipesZombie.xml");
    AI.LoadGoalPipes("Scripts/AI/GoalPipes/GoalPipesSwordsman.xml");

 

After that, the Selection Tree will become available in Sandbox once you select an entity:

 

Again, this is a very basic tutorial, but I hope it helps! :)

  1. Garrus
    October 16th, 2012 at 22:30 | #1

    Thank you Seith, all of your tutorials are cool and easy to understand, I ve a little question: this method and the other you did explain in “AI melee by flowgraph” are interchangeable(same function, one does rule out the other and the choice does depend on personal flavour) or they have to work togheter?Thank you

  2. Garrus
    October 17th, 2012 at 02:53 | #2

    Btw I didnt find any PipesManager.xml but Pipemanager.lua in that folder, anyway there is in it “specific Xml” so I did add the line as you did explain.
    I follow all the procedure but when I try to load the custom AI behavior on my AI and I press AI\Physics it compares this error message:
    !AI: Error: SelectPipe: GoalPipe ‘Swordsman_idle’ does not exists. Selecting default goalpipe ‘_first_’ instead. I did check it several time…

  3. Seith
    October 17th, 2012 at 06:55 | #3

    You’re welcome. Yes, those are two separate methods to achieve similar goals (albeit with some differences). So you need to choose which one you prefer.

  4. Seith
    October 17th, 2012 at 07:05 | #4

    As you guessed correctly, it should have read “PipesManager.lua” (I just fixed it). Regarding the error type you meet, it usually comes from a misspelling somewhere. “Swordsman_idle” is very different from “SwordsmanIdle”. And I’m not sure about case sensitivity but just to make certain I’d check that too (“swordsman_idle” =/= “Swordsman_idle”)…

  5. Garrus
    October 17th, 2012 at 09:57 | #5

    Hi seith tank you for reply..I did notice a couple of strangeness; after the first error I ve switched in game and it compares one error for each lua(same error but one for approach, one for attack etc), so in my opinion unlikely the misspelling could be in all of 4 lua.. Most likely it is elsewhere 

  6. Garrus
    October 18th, 2012 at 04:03 | #6

    Hi Seith , I did check lowercase\uppercase but really doesnt work. I m absolutely sure that I did follow the tutorial to the letter. What that seems strange for me is that I did never write Swordsman_idle or _approach etc so this “_” is from reference(it compares in Goalpipe and in luas)..anyway I m not able enough to correct the selection tree or other pro’s scripts etc so I’ll go straight to the other flowgraph way. Thank you!

  7. Max
    November 11th, 2012 at 01:48 | #7

    @Seith

    I have the same problem with your solution. Copy/pasted everything 1:1 and it still gives that error.

    I’m using the FreeSDK if that matters.

  8. Drew
    November 26th, 2012 at 00:26 | #8

    I have fixed said error by renaming the behavior files, the goalpipe names, and the names in the selection tree names. That removed the error, but now I am getting “[Warning] AI: entity Grunt4 failed to change behavior to zombie_idle.” I have gotten this error with any custom AI I have used/ wrote. I am using cryengine free sdk 3.4.0 with a clean project. Can anyone point out what I am doing wrong?

  9. Drew
    November 27th, 2012 at 10:18 | #9

    Let me make it clear. Name the goalpipes in GoalPipesSwordsman.xml to “swordsman_idle” etc.
    Next name the behavior files “SwordsmanIdle” etc.
    In the behavior files, they should both read “swordsman_idle” etc.
    Lastly, the Selection tree leaves should read “SwordsmanIdle” etc.

    that will fix the error claiming no idle goalpipe. The next error reads “[Warning] AI: entity Grunt1 failed to change behavior to swordsman_idle.”

    Im not sure why this comes up. Any Ideas

  10. Alex
    January 6th, 2013 at 23:39 | #10

    I also have the same problem above

    “[Warning] AI: entity Grunt1 failed to change behavior to swordsman_idle.”

    Another problem I have is when using nav meshes inside buildings. They seem to always want to go outside and not follow me. When I’m outside they do follow me.

    Any ideas. Thanks for your help seith 😀

  11. Seith
    January 7th, 2013 at 09:04 | #11

    Hi Alex. Well, this “tutorial” reflects the very little knowledge I had at the time, and it was far from perfect; I could never really get the AI to do exactly what I wanted. I have now moved on to Unity 4 and I’m not using the CryEngine anymore. So I’m afraid I can’t be of any help here, sorry.

  12. Collin Bishop
    March 22nd, 2013 at 17:40 | #12

    This melee system def works. I am not sure what anyone has done here. Maybe they did not switch the file type to lus. Because all my stuff was found and the debug log reports no errors.