This is the second part of the ant simulation inspired by a video posted by Sebastian Lague. In this post I wanted to go through adding attributes like collecting food, leaving trails and death in the simulation, but I realized that all of that is not possible in just one post, so I will just stick to finding food and eating food.

The Food Gatherer

Now let’s add the capability to detect food and find food. Right now,, my ant is moving around aimlessly, randomly changing directions. It will also detect any obstacle in front of it and change it’s course. I want that the ant should be able to detect food if the food is present within a certain range from the ant. In real life the ant should be able to detect the smell of the food. I am not sure that from how far away the real ants can smell the food, but I will make around 150-200 pixel diameter circle area, within which if food is present, the ant will be able to detect it.

For this I will add an Area2D to the Ant Scene. Then add a PolygonShape2D under the Area2D node and create new Circle2D shape.

The Food Object

I will create a new scene with a simple Circle Shape and save it as food.tscn then, attach a script to it and name it food.gd. The Food will have a size attribute. The size variable will determine the scale of the food. The Food Node needs to be in the food group so that the ant knows that it is food and not an obstacle. The food.gd script will also make sure that every time the food collides with an ant, it looses some of it’s size value. The physical scale of the circle will also reduce with the size variable. Once the size value of the food is zero, the food will self-destruct using queue_free() method.

For the food I will create a circle in an image editor and saved it as a PNG. I will use this image as the food object. In my food.tscn I will change the type of the root node to Area2D. Then I will add a CollisionShape2D and a Sprite node as the child of the Area2D node. The collision node will have a Circle2D of the same size as the sprite image. I am using 50×50 pixels as the size of the image.
enter image description here enter image description here

I will add the root node of the food scene to a new group called food This will help the ant to identify this object as food.

enter image description here

I will attach a new script to the root node of the food scene. The next thing is to attach a signal that will be emitted when an object enters the area. I select the Root Node and then go to the Node tab and right-click on the body_entered(body: Node) then click Connect.

enter image description here

The next thing is to select the script where you want to the attached function to be created and connected to. I will be using the Same Root Node which has my script food.gd.

enter image description here

In the on_body_entered function I will write the following code.

export(float) var size : float = 1

func _on_Food_body_entered(body: Node) -> void:
    if body.is_in_group("ant"):
        size -= 0.02
        body.objective = "none"
    if size < 0:
        queue_free()

    scale = Vector2(size, size)

I want that whenever the ant touches the food’s body the ant’s objective is set to "none" again and the food is reduced a bit in size. When the size of the food is zero, the food will self destruct itself.

I will now do the same thing for the FoodGathererAnt0 scene except that instead of a on_body_entered I will use on_area_entered instead. I will select the root node of the FoodGathererAnt and go to Node tab and right-click on on_area_entered and click Connect. Just like how I connected the signal to a script for the food scene, I will connect the signal with the FoodGathererAnt Node, since that is the node which is holding the script FoodGathererAnt_AI.gd and write the following code.

func _on_Food_Detector_area_entered(area: Area2D) -> void:
    if (area.is_in_group("food")):
        objective = "food"

The above function is called whenever any object enters the Ant’s Detection Area. In the function I will check if the object that has entered belongs to the group "food" and then set the value of the variable objective to "food".

Let’s revisit the Behavior tree again now. I want that if the ant finds the food it should rotate towards the food. If it collides with the food it should eat a bit of the food and start wandering again. Since the tree now is getting a bit more complex, we can use Backwards Chaining method. First I will make a very high level tree from the root deciding what are the major tasks that the ant needs to perform. I have kind of three states for ant. The ant is either wandering around, or it it has located food and is going towards food and lastly, it eaten the food and is now going back to wander mode. The ? is used to denote a Selector node and >> is used to denote a Sequence node going forward.

graph TD
R[Root] --> SEL1[/?\]
SEL1 --> T2[Go Towards Food]
SEL1 --> T3[Wander]

style R fill:#7D6B7D,color:#fff
style SEL1 fill:#FF8C64,color:#222
style T2 fill:#3EB595,color:#fff
style T3 fill:#3EB595,color:#fff

Now I will create a tree for Go Towards Food

graph TD
R[Go Towards Food] --> SEQ1[/?\]
SEQ1 --> COND1{{Is Food near?}}
SEQ1 --> ACTION1[Rotate towards Food]

style R fill:#7D6B7D,color:#fff
style SEQ1 fill:#FF8C64,color:#222
style COND1 fill:#3EB595,color:#fff
style ACTION1 fill:#3EB595,color:#fff

For the last one, I will modify the previous one to

graph TD
R[Wander] --> SEQ1[/>>\]
SEQ1 --> ACTION1[Rotate to Random]

style R fill:#7D6B7D,color:#fff
style SEQ1 fill:#FF8C64,color:#222
style ACTION1 fill:#3EB595,color:#fff

I will now replace the first high level tree with all the sub trees and it will look like this:

graph TD
R[Root] --> SEL1[/?\]

SEL1 --> T2[/>>\]
SEL1 --> T3[/>>\]

T2 --> T2COND1{{Is Food near?}}
T2 --> T2ACTION1[Rotate towards Food]

T3 ---> T3ACTION1[Rotate to Random]

style R fill:#7D6B7D,color:#fff
style SEL1 fill:#FF8C64,color:#222

style T2 fill:#FF8C64,color:#222
style T3 fill:#FF8C64,color:#222
style T2COND1 fill:#3EB595,color:#fff
style T2ACTION1 fill:#3EB595,color:#fff
style T3ACTION1 fill:#3EB595,color:#fff

In the earlier tree, there was a node which made the ant move forward and another made the ant check for collisions. You can see that they have gone missing. It is because those two nodes need to run irrespective of any of the above conditions. I will add them to tree now.

graph TD
R[Root] --> SL1[/?\]
R --> SQ2[/>>\]

SL1 ---> SQ1.1[/>>\]
SQ1.1 --> T1.1.1{{Is Going Home?}}
SQ1.1 --> T1.1.2[Rotate Towards Home]

SL1 --> SQ1.2[/>>\]
SQ1.2 --> T1.2.1{{Is Near Food?}}
SQ1.2 --> T1.2.2[Rotate Towards Food]

SL1 ----> SQ1.3[/>>\]

SQ1.3 --> INV1.3.1[/INVERTER\]
INV1.3.1 --> T1.3.1{{Is Going to Food?}}

SQ1.3 --> T1.3.2[Rotate to Random]

SQ2 --> T2.1[Move Forward]
SQ2 --> T2.2[/Is Colliding?\]
SQ2 --> T2.3[Rotate Till Clear]

style R fill:#7D6B7D,color:#fff

style SL1 fill:#FF8C64,color:#222
style SQ1.1 fill:#FF8C64,color:#222
style SQ1.2 fill:#FF8C64,color:#222
style SQ1.3 fill:#FF8C64,color:#222
style SQ2 fill:#FF8C64,color:#222

style T1.1.1 fill:#3EB595,color:#fff
style T1.1.2 fill:#3EB595,color:#fff
style T1.2.1 fill:#3EB595,color:#fff
style T1.2.2 fill:#3EB595,color:#fff

style T1.3.1 fill:#3EB595,color:#fff
style T1.3.2 fill:#3EB595,color:#fff
style T2.1 fill:#3EB595,color:#fff
style T2.2 fill:#3EB595,color:#fff
style T2.3 fill:#3EB595,color:#fff

style INV1.3.1 fill:#D29922,color:#fff

Now we have this new behavior tree which will take care of the food gathering ant’s AI. Now let’s get into the code. The script in Move Forward node will stay the same as before.

Is Colliding (Condition Leaf)

func tick(actor, blackboard) -> int:
    var delta = blackboard.get("delta")
    var result = actor.check_for_collisions()
    var pos_neg = 1

    if result == -1 or result == 1:
#       actor.rotate_till_clear(true, result, delta)
        blackboard.set("pos_neg", result)
        return SUCCESS

    if result == 0:
#       actor.rotate_till_clear(true, 1, delta)
        blackboard.set("pos_neg", 1)
        return SUCCESS

    if result == 2:
        return FAILURE

    return FAILURE

Rotate Till Clear (Action Leaf)

func tick(actor, blackboard) -> int:
    var delta = blackboard.get("delta")
    var pos_neg = blackboard.get("pos_neg")

    actor.rotate_till_clear(true, pos_neg, delta)
    return SUCCESS

The pos_neg is the value that decides that which direction the ant should rotate towards when colliding. Now I am using blackboard to store and retrieve this value.

Is Near Food? (Condition Leaf)

func tick(actor, blackboard) -> int:
    if actor.objective == "food":
        return SUCCESS
    else:
        return FAILURE

As you may have noticed now that a few more things in the behavior tree have changed. This is because the ant needs to now also know if it is just wandering or is it going towards the food. If the ant has not been to the food and the food is nearby, the ant needs to go towards the food and eat it it. If the ant has been to the food once, it needs to wander again till it finds another food or same food again.

To achieve this, I redesigned the tree a bit and added an additional check which checks the value of a variable. If that variable is in free mode then the ant is searching for food whereas, if the variable value is in none mode then the ant is just wandering again.

The complete project is available on github. This was just to show an example of the basic ant AI built using a behavior tree. I am not sure if I will take this ant simulation project any further or not. Maybe I will, maybe not. But if I do, I will post my update here.

Hope this article was helpful to understand how to build Artificial Intelligence or simulations in Godot engine. Thanks for reading.

Photo by NeONBRAND on Unsplash

Last modified: January 22, 2022

Author

Comments

Write a Reply or Comment

Your email address will not be published.