Creating simple AI interactions with environment objects


When creating artificial intelligence for video games, one of the most important aspects is its location. The position of the AI ​​character can completely change his types of behavior and future decisions. In this tutorial we will understand how the game environment can affect AI and how to use it correctly.

This article is taken from the book Practical Game AI Programming , written by Michael Dagrack and published by Packt Publishing. This book allows you to learn how to create a game AI and from scratch implement the most advanced AI algorithms.

Visual interactions are basic, they do not directly affect the gameplay, but allow you to improve the video game and its characters by making them part of the environment we create, which greatly affects the player’s immersion in the game. This proves to us the importance of the environment being part of the game, and not just helping to fill the space on the screen. Similar interactions are increasingly found in games, and players expect to see them. If there is an object in the game, then it must fulfill some function, albeit not the most important one.

One of the first examples of interaction with environments can be found in the first Castlevania, released in 1986 for the Nintendo Entertainment System. From the very beginning, the player can use the whip to destroy candles and fires, which were originally part of the background.


This and some games of that time opened up many doors and opportunities in terms of the modern perception of the backgrounds and environments of characters in the game. Obviously, due to the hardware limitations of that generation of consoles, it was much more difficult to create simple things that are generally accepted by current standards. But each generation of consoles brought new features and developers used them to create amazing games.

So, our first example of visual interaction is an object on the background that can be destroyed without directly affecting the gameplay. This type of interaction is found in many games. Implementing it is simple, just animate the object when it is attacked. After that, we can decide whether any points or objects should be dropped from the object that reward the player for exploring the game.

Now we can move on to the next example - objects in the game that are animated or move when characters pass through them. The principle here is the same as with destructible objects, but this time the interaction is more subtle - it requires the character to move to the point where the object is. This can be applied to various elements of the game, from the movement of grass, dust or water, to flying birds or people making funny gestures; the possibilities are endless.

When analyzing these interactions, we can easily determine that they do not necessarily use AI, and in most cases it is just a Boolean function that is activated according to some given action. But they are part of the environment and therefore should be taken into account when implementing high-quality interaction between the environment and AI.

Create simple interactions with the environment


As we have already seen, the environment once became part of the gameplay, and this gave rise to many new concepts and ideas for future games. The next step was the integration of these small changes in the gameplay and their use to change the player’s behavior in the game. This definitely positively affected the history of video games, because all the elements of the scene gradually began to come to life and the player began to realize how rich the environment was. Using environments to achieve in-game goals has become part of the gameplay.


To demonstrate one example of an environment that directly affects gameplay, we’ll take an excellent example - the Tomb Raider franchise. In this example, our character, Lara Croft, must push the cube until it lands on the marked area. This will change the environment and open a new path, allowing the player to move further in level.

Such puzzles can be found in many games: you must perform an action at a certain point on the map so that something happens in another part of it, and this can be used to accomplish some goal in the game. Usually we need to change the environment itself in order to advance further in level. Therefore, when developers plan a map or level, they take this into account and create all the rules related to each interaction. For instance:

if(cube.transform.position == mark.transform.position)
{
  openDoor = true;
}

Now imagine for a moment that Lara Croft has an ally character whose main task is to help her put this box in its place. And in this chapter we will consider just this type of interaction: the AI ​​character understands how the environment works and how to use it.

Moving Environment in Tomb Raider


Let's go straight to this scenario and try to recreate a situation in which there is an AI character that can help the player achieve his goal. In this example, we will imagine that a player is trapped from which he cannot gain access to an interactive object that can free him. The character we create must be able to find the cube and push it in the right direction.


So now we have all the characters and objects. Let's plan how the AI ​​character should behave in this situation. First, he should see that the player is close, so that he can begin to search and move the cube to the desired position. Assume that if the cube is at the mark, then a new block appears from the sand, allowing the player to advance further in level. An AI character can push a cube in four directions: left, right, forward and backward, so that it fits perfectly with the position mark.


The AI ​​character must verify and validate every action shown in this behavior tree. The first and most important thing for continuing the task is that the character must be sure that the player is located on his mark.

If the player has not yet reached there, then our character must wait and stay in place. If the player has already arrived at the mark, then the AI ​​character continues execution and asks himself how far he is from the cube object. If not, then the character should move towards the cube, and once this action is confirmed, he should ask the same question. When the answer becomes positive and the character is next to the cube, he needs to figure out which way you must first push the cube.

Then he begins to push the cube along the Y or X axis until it matches the marking position and the task is completed.

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

private bool playerOnMark;


void Start () {

}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueYplayerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  {
     playerOnMark = true;
  }
}

We are starting to add information to the code that allows the character to check whether the player is next to his marked position. To do this, we create all the variables necessary to calculate the distances between the player and the position in which he should be. playerMeshrefers to the 3D model of the player, from which we extract his position and use it as currentPlayerPosition.

To know whether it is close to the mark, we need a variable representing the position of the mark, and in our example we created a variable playerMarkin which we can write the position in which the player should be. Then we added three variables that let us know if the player is nearby. proximityValueXwill calculate the distance between the player and the X-axis mark. proximityValueYcalculates the distance between the player and the Y-axis mark.

Next, we have nearValue, in which we can determine how far the player can be from the position of the mark, when the AI ​​character can begin to work on achieving the goal. As soon as the player is near the mark, the Boolean variable playerOnMarkchanges the value to true.

To calculate the distance between the player and the mark, we used the following: the distance between the player and his mark, similar (mark.position - player.position).

Now, to determine if the AI ​​character is near the cube, we calculate the same equation by calculating the distance between the AI ​​and the cube. In addition, we supplemented the code with positions on both marks (player and cube):

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

public float cubeProximityX;
public float cubeProximityY;
public float nearCube;

private bool playerOnMark;
private bool cubeIsNear;


void Start () {

   Vector3 playerMark = new Vector3(81.2f, 32.6f, -31.3f);
   Vector3 cubeMark = new Vector3(81.9f, -8.3f, -2.94f);
   nearValue = 0.5f;
   nearCube = 0.5f;
}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueY = playerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  {
     playerOnMark = true;
  }

  cubeProximityX = currentCubePosition.transform.position.x - this.transform.position.x;
  cubeProximityY = currentCubePosition.transform.position.y - this.transform.position.y;

  if((cubeProximityX + cubeProximityY) < nearCube)
  {
     cubeIsNear = true;
  }

  else
  {
     cubeIsNear = false;
  }
}

Now that our AI character knows if he is next to the cube, this allows us to answer the question and determine if he can move on to the next branch we planned. But what happens when the character is not next to the cube? He will need to approach the cube. Therefore, we will add this to the code:

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform cubeMesh;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

public float cubeProximityX;
public float cubeProximityY;
public float nearCube;

private bool playerOnMark;
private bool cubeIsNear;

public float speed;
public bool Finding;


void Start () {

   Vector3 playerMark = new Vector3(81.2f, 32.6f, -31.3f);
   Vector3 cubeMark = new Vector3(81.9f, -8.3f, -2.94f);
   nearValue = 0.5f;
   nearCube = 0.5f;
   speed = 1.3f;
}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueY = playerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  { 
      playerOnMark = true;
  }

  cubeProximityX = currentCubePosition.transform.position.x - this.transform.position.x;
  cubeProximityY = currentCubePosition.transform.position.y - this.transform.position.y;

  if((cubeProximityX + cubeProximityY) < nearCube)
  {
      cubeIsNear = true;
  }

  else
  {
      cubeIsNear = false;
  }

  if(playerOnMark == true && cubeIsNear == false && Finding == false)
  {
     PositionChanging();
  }

  if(playerOnMark == true && cubeIsNear == true)
  {
     Finding = false;
  }

}

void PositionChanging () {

  Finding = true;
  Vector3 positionA = this.transform.position;
  Vector3 positionB = cubeMesh.transform.position;
  this.transform.position = Vector3.Lerp(positionA, positionB, Time.deltaTime * speed);
}

So far, our AI character is able to calculate the distance between himself and the cube; if they are too far apart, then he will go to the cube. After completing this task, he can proceed to the next phase and begin to push the cube. The last thing he needs to calculate is how far the cube is from the position of the mark, after which he decides which way to push, taking into account which side of the cube the mark is closer to.


The cube can only be pushed along the X and Z axes, and its rotation is not yet important to us, because the button is activated when the cube is installed on it. Given all this, the AI ​​character must calculate how far the cube is from the position of the mark in X and the position of the mark in Z.

Then he compares the two values ​​in two axes and selects which one is farther from the desired position, and then begins to push along this axis. The character will continue to push in this direction until the cube is aligned with the position of the mark, and then switches to the other side, and will push it until it is completely over the mark position:

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform cubeMesh;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

public float cubeProximityX;
public float cubeProximityY;
public float nearCube;

public float cubeMarkProximityX;
public float cubeMarkProximityZ;

private bool playerOnMark;
private bool cubeIsNear;

public float speed;
public bool Finding;


void Start () {

        Vector3 playerMark = new Vector3(81.2f, 32.6f, -31.3f);
        Vector3 cubeMark = new Vector3(81.9f, -8.3f, -2.94f);
        nearValue = 0.5f;
        nearCube = 0.5f;
        speed = 1.3f;
}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueY = playerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  {
     playerOnMark = true;
  }

  cubeProximityX = currentCubePosition.transform.position.x - this.transform.position.x;
  cubeProximityY = currentCubePosition.transform.position.y - this.transform.position.y;

  if((cubeProximityX + cubeProximityY) < nearCube)
  {
     cubeIsNear = true;
  }

  else
  {
     cubeIsNear = false;
  }

  if(playerOnMark == true && cubeIsNear == false && Finding == false)
  {
      PositionChanging();
  }

  if(playerOnMark == true && cubeIsNear == true)
  {
      Finding = false;
    }

   cubeMarkProximityX = cubeMark.transform.position.x - currentCubePosition.transform.position.x;
   cubeMarkProximityZ = cubeMark.transform.position.z - currentCubePosition.transform.position.z;

   if(cubeMarkProximityX > cubeMarkProximityZ)
   {
     PushX();
   }

   if(cubeMarkProximityX < cubeMarkProximityZ)
   {
     PushZ();
   }

}

void PositionChanging () {

  Finding = true;
  Vector3 positionA = this.transform.position;
  Vector3 positionB = cubeMesh.transform.position;
  this.transform.position = Vector3.Lerp(positionA, positionB, Time.deltaTime * speed);
}

After adding the latest actions to the code, the character must learn to determine his goal, find and push the cube to the desired position so that the player can go through and finish the level. In this example, we focused on how to calculate distances between scene objects and characters. This will help us to create similar types of interactions in which we need to place the game object in a certain position.

The example demonstrates a friendly AI character that helps the player, but the same principles can be applied if we need the opposite effect (if the character is an enemy), in which the character needs to find the cube as soon as possible to stop the player.

Obstacle objects in environments using the example of Age of Empires


As we saw earlier, you can use or move objects in the game to accomplish goals, but what happens if some object obstructs the character’s path? The object can be placed by the player or simply located by the designer in this position of the map. In any case, the AI ​​character should be able to determine what needs to be done in this situation.

We can observe this behavior, for example, in a strategy called Age of Empires II developed by Ensemble Studios. Each time a game character cannot reach enemy territory due to the fact that it is surrounded by fortified walls, the AI ​​switches to destroying part of the wall to move on.

This type of interaction is also very smart and important, because otherwise the characters would just wander along the wall in search of an entrance, and this would not look like reasonable behavior. Since the reinforced wall is created by the player, it can be placed anywhere and have any shape. Therefore, you need to think about this when developing an enemy AI.


This example also relates to the topic of our article, because at the planning stage, when we create behavior trees, we need to think about what will happen if something gets in the way of the character and he cannot fulfill his goals. We will consider this aspect in detail in the next chapters of the book, but for now we simplify the situation and analyze how the AI ​​character should behave if the environment object prevents him from fulfilling his goal.


In our example, the AI ​​character must enter the house, but when he approaches, he realizes that he is surrounded by a wooden fence, through which you can not pass. We want the character to choose a target at this stage and begin to attack until this part of the fence is destroyed and he can enter the house.

In this example, we need to calculate which fence the character should attack, given the distance and the current health status of the fence. A fence with low HP should have a higher priority for attack than a fence with full HP, so we will take this into account in our calculations.


We want to set the neighborhood around the character, within which the nearest fences transmit their information to artificial intelligence so that it can decide which one is easier to destroy. This can be implemented in various ways, either using the recognition of collisions of fences with the player, or forcing them to calculate the distance between fences / objects and the player; we set the value of the distance at which the player begins to perceive the state of the fence. In our example, we will calculate the distance and use it to notify the character about HP fences.

Let's start by creating the code that will be applied to the fence object; all of them will have the same script:

public float HP;
public float distanceValue;
private Transform characterPosition;
private GameObject characterMesh;

private float proximityValueX;
private float proximityValueY;
private float nearValue;

// Use this for initialization
void Start () {

  HP = 100f;
  distanceValue = 1.5f;

  // Find the Character Mesh
  characterMesh = GameObject.Find("AICharacter");
}

// Update is called once per frame
void Update () {

  // Obtain the Character Mesh Position
  characterPosition = characterMesh.transform;

  //Calculate the distance between this object and the AI Character
  proximityValueX = characterPosition.transform.position.x - this.transform.position.x;
  proximityValueY = characterPosition.transform.position.y - this.transform.position.y;

  nearValue = proximityValueX + proximityValueY;
}

In this script, we added basic information about HP and distances, which will be used to connect with the AI ​​character. This time we add the distance-calculating script not to the character, but to the environment objects; this gives the object more dynamism and allows us to create more opportunities.

For example, if the characters of the game are also engaged in the creation of fences, then they will have different states, for example, “under construction”, “completed” or “damaged”; then the character will be able to receive this information and use it for their own purposes.

Let's set the character interacting with the environment object. His main goal will be to gain access to the house, but when he approaches it, he realizes that he cannot get inside due to the fact that he is surrounded by wooden fences. We want that, having analyzed the situation, our character would destroy the fence to fulfill his goal and get into the house.

In the character script, we will add a static function, to the input of which fences will be able to transmit information about their current "health"; this will help the character choose the most suitable fence for destruction.

public static float fenceHP;
public static float lowerFenceHP;
public static float fencesAnalyzed;
public static GameObject bestFence;

private Transform House;

private float timeWasted;
public float speed;



void Start () {

        fenceHP = 100f;
        lowerFenceHP = fenceHP;
        fencesAnalyzed = 0;
        speed = 0.8;

        Vector3 House = new Vector3(300.2f, 83.3f, -13.3f);

}

void Update () {

        timeWasted += Time.deltaTime;

        if(fenceHP > lowerFenceHP)
        {
            lowerFenceHP = fenceHP;
        }

        if(timeWasted > 30f)
        {
            GoToFence();  
        }
}

void GoToFence() {

        Vector3 positionA = this.transform.position;
        Vector3 positionB = bestFence.transform.position;
        this.transform.position = Vector3.Lerp(positionA, positionB, Time.deltaTime * speed);
}


We have already added the most basic information to the character. fenceHPwill be a static variable in which each fence that falls within the radius of the character’s neighborhood will record information about the current HP. Then the AI ​​character analyzes the information received and compares it with the fence with the least HP presented lowerFenceHP.

The character has a variable timeWastedrepresenting the number of seconds that he has already spent searching for a suitable fence to destroy. fencesAnalyzedwill be used to find out if there is already a fence in the code, and if not, the first fence found by the character is added; in case fences have the same HP value, the character attacks them first. Now let's add the code of the fences so that they can access the character’s script and enter useful information.

public float HP;
public float distanceValue;
private Transform characterPosition;
private GameObject characterMesh;

private float proximityValueX;
private float proximityValueY;
private float nearValue;
void Start () {

        HP = 100f;
        distanceValue = 1.5f;

        // Find the Character Mesh
        characterMesh = GameObject.Find("AICharacter");
}

void Update () {

        // Obtain the Character Mesh Position
        characterPosition = characterMesh.transform;

        //Calculate the distance between this object and the AI Character
        proximityValueX = characterPosition.transform.position.x - this.transform.position.x;
        proximityValueY = characterPosition.transform.position.y - this.transform.position.y;

        nearValue = proximityValueX + proximityValueY;

        if(nearValue <= distanceValue){
            if(AICharacter.fencesAnalyzed == 0){
                AICharacter.fencesAnalyzed = 1;
                AICharacter.bestFence = this.gameObject;
            }

            AICharacter.fenceHP = HP;

            if(HP < AICharacter.lowerFenceHP){
                AICharacter.bestFence = this.gameObject;
            }
        }
}

We finally completed this example. Now the fence compares its current HP with the character's data ( lowerFenceHP), and if its HP is lower than the lowest value the character has, then this fence will be considered bestFence.

This example demonstrates how to adapt an AI character to various dynamic objects in the game; the same principle can be extended and used to interact with almost any object. It is also applicable and useful when using objects to interact with a character, linking information between them.

In this post, we explored various ways of interacting with the environment. The techniques demonstrated in this chapter can be extended to many different genres of games and used to perform simple and complex interactions between AI characters and the environment.

All Articles