I published a post on how to use Behavior Trees to create artificial Intelligence for NPC in which I talked about What is a Behavior Tree, the type of BT nodes, commonly used nodes, and the functioning of a Behavior Tree. I also talked about using different available implementations of Behavior Trees in Godot. I am using the implmentation which is available here
In that post I used an example of an ant where the ant will move towards the mouse position if the mouse is closer than a specified distance. Recently, I have been very inspired by the work of Sebastian Lague on YouTube and in one of his videos he made an ant simulation in Unity Engine, so I thought why not extend my simple ant example and make a little ant simulation of my own.
Just to set the right expectations, my simulation is not going to be exactly like his simulation and I will be using Godot Engine 3.4. (the latest stable release at the time of writing this post) instead of Unity Engine.
So, lets revisit the tree diagram I used to make the ant follow the mouse position.
graph TD
BTR[1. Root Node] --> SEL1[2. Sequence]
SEL1 --> CCOND1{{3. Is_Mouse_Close?}}
SEL1 --> CACTION1[4. Look At Mouse]
SEL1 --> DEC1[/Inverter\]
DEC1 --> CACTION3{{6. Is Ant at Mouse?}}
SEL1 --> CACTION4[Move To Mouse]
style BTR fill:#7D6B7D,color:#fff
style SEL1 fill:#FF8C64,color:#222
style CACTION1 fill:#3EB595,color:#fff
style CCOND1 fill:#3EB595,color:#fff
style DEC1 fill:#D29922,color:#fff
style CACTION3 fill:#3EB595,color:#fff
style CACTION4 fill:#3EB595,color:#fff
Replicating this in Godot Engine using the Behavior Tree Plugin will look like this:
I have a new 2D scene with a KinematicBody2D as the root node with the following structure.
- KinematicBody2D
- Animated Sprite (because my ant will be animated when walking)
- CollisionShape2D (my ant will be colliding with objects and detecting collisions)
- Behavior Tree Root Node
- Sequence Composite
- Condition Leaf
- Action Leaf
- Selector Composite
- Inverter Decorator
- Condition Leaf
- Action Leaf
- Inverter Decorator
- Sequence Composite
I have attached a new script on my KinematicBody2D called Ant_AI.gd
extends KinematicBody2D
export(NodePath) onready var animSprite = get_node(animSprite)
export(float) var move_speed : float = 3
export(float) var distance_threshold = 300
# Custom Functions
func is_mouse_close(target_position):
var distance = (target_position - position).length()
if abs(distance) > distance_threshold:
animSprite.animation = "idle"
return false
return true
func move_towards_position(target, delta) -> void:
# Set animation to animate
animSprite.animation = "walk"
# find direction and normalized direction
var dir = target - position
var dir_n = dir.normalized()
# Get distance between self and target
var distance = dir.length() - (dir_n * move_speed).length()
# If distance is more than the move_speed then move
if distance > move_speed:
position += dir_n * move_speed
# Else move self to target position and change animation to idle.
else:
position = target
animSprite.animation = "idle"
I have two animations in my AnimatedSprite called "walk
" and "idle
" and my code will switch between these 2 animations whenever needed.
Using the above functions, and the behavior tree, I am moving the ant, towards the mouse position, but only if the mouse pointer is less than a certain amount of pixels away.
Now let’s make the ants smarter and wander autonomously.
The Wandering Ant
Let’s try and modify the behavior tree to:
- Move in random directions
- Detect collisions and change direction till a clear path is found.
I will duplicate my existing ant scene and rename it to WanderingAnt
. Then I will create a new 2D scene and add my WanderingAnt
to this new scene.
In this scenario, the ant doesn’t need to move towards any target, but just wander on it’s own. To achieve this, I will move the ant in just the forward direction of the ant. In Godot, i can move it towards the transform.x
. If now I rotate the ant, the ant will still move forward towards it’s transform.x
direction making the ant always move in the direction it is facing.
In the behavior tree, I will add a Sequence node under the root and make an action Leaf node to move the ant forward. I also want the ant to move in random directions so, if the ant is moving, then the ant should rotate as well every now and then.
graph TD
R[Root] --> SEQ1[Sequence]
SEQ1 --> ACTION1[Move Forward]
SEQ1 --> ACTION2[Rotate]
style R fill:#7D6B7D,color:#fff
style SEQ1 fill:#FF8C64,color:#222
style ACTION1 fill:#3EB595,color:#fff
style ACTION2 fill:#3EB595,color:#fff
Move Forward
Moving forward is pretty straight forward. As long as this node is called, the ant should move in it’s transform.x
direction.
func tick(actor, blackboard) -> int:
actor.move_forward(blackboard.get("delta"))
return SUCCESS
The move_forward()
method will take care of moving the ant forward.
After the Move Forward node is processed and returns a SUCCESS, the Rotate Node will be processed. I don’t want to rotate on every frame, so I am using the blackboard to set and get a variable value to call the rotate function only once every few frames. I can write my own Decorator node for this, but for now I will let it be embedded in this rotation code.
func tick(actor, blackboard) -> int:
var delta = blackboard.get("delta")
rng.randomize()
var random_time = blackboard.get("random_rotation_time")
var tick_count = blackboard.get("tick_count")
if tick_count == null:
tick_count = 0
if random_time == null:
random_time = rng.randf_range(actor.rotate_timer.x, actor.rotate_timer.y)
tick_count += 1
var frame_count : float = random_time * 60
if tick_count >= frame_count:
tick_count = 0
actor.rotate_to_random(true, delta)
blackboard.set("tick_count", tick_count)
blackboard.set("random_rotation_time", null)
return SUCCESS
actor.rotate_to_random(false, delta)
blackboard.set("tick_count", tick_count)
blackboard.set("random_rotation_time", random_time)
return SUCCESS
In the above code, the rotation function is called every tick, but if it is called after a specified count, it will send true
as a parameter, whereas, every other tick, it will send false
as the parameter. The true
and false
values will be help the rotate_to_random()
method behave appropriately.
The rotate_to_random
function is quite simple. If it receives true, it randomly generates a Vector2. If it receives false it rotates the ant towards direction randomly generated in the previous call. The code in the WanderingAnt_AI.gd
is :
extends KinematicBody2D
export(NodePath) onready var animSprite = get_node(animSprite)
export(float) var move_speed : float = 200
export(Vector2) var rotate_timer : Vector2 = Vector2(2, 6)
export(Vector2) var min_target : Vector2 = Vector2(-500,-500)
export(Vector2) var max_target : Vector2 = Vector2(500,500)
export(float) var rotate_speed : float = 5
var angle := 0.0
var random_dir := Vector2.ZERO
var velocity = Vector2.ZERO
# Custom Functions
func move_forward(delta):
velocity = transform.x * move_speed
move_and_slide(velocity, Vector2.UP)
animateWalk()
func rotate_to_random(flag, delta):
if flag:
var f1 := rand_range(min_target.x, max_target.x)
var f2 := rand_range(min_target.y, max_target.y)
random_dir = Vector2(f1, f2)
else:
angle = transform.x.angle_to(random_dir)
if abs(angle) > .01:
rotate(sign(angle) * min((delta * rotate_speed), abs(angle)))
func animateWalk():
animSprite.animation = "walk"
func animateIdle():
animSprite.animation = "idle"
Now our ant is wandering around the area. It will keep walking forward with forward animation and will never stop. At random intervals, it will rotate to a random direction. It will keep wandering even outside the area of the screen. Now we need to make sure that it stays within the bounds of the screen. To keep the ant within the bounds of the screen, I will make a static body with collision shapes all around the screen in the Main Scene
. The ant is KinematicBody2D
Node which has a CollisionShape
and will not be able to walk past the boundaries.
Once we put the collision shapes, the kinematic body will not pass through the colliders. This is good enough to hold the ant inside the bounds of the screen, but it doesn’t look realistic. If the ant has intelligence, it should be able to detect these colliders and move away before it even hits them, as if it is able to sense them with it’s antennas. We can achieve this using a few ray casts
Add RayCast2D
For detecting if the ant is about to collide with something or not, we want to simulate ant antennas using a ray cast 1. If the ray is collides with an obstacle it will check if that object is in the obstacles
group. If yes, then the ant will try to rotate a few degrees to either left or right till the ray does not collide with anything any more. This will make the ant act as if it is able to detect obstacles in front of it and try and change it’s path before it’s body collides with the obstacles.
To achieve this, I will add a Raycast2D node in the WandererAnt
scene. We need to make a cast in the x axis in the positive direction. A value between 15 to 25 is good enough, depending on the size of your ant. Make sure you enable the ray-cast because it is disabled by default.
Now I will duplicate the RayCast2D node 2 times to make two more ray casts. I will use in total 3 RayCast2D nodes to detect upcoming collisions. One will cast a ray straight ahead of the head as we have seen. the second one will cast a ray at 45 degrees to the right and the third one will cast a ray 45 degrees to the left of the ant. the left and right ray-casts will be a little shorter than the first one. If the front and left rays both collide with an object, this means that there is an obstacle in the front of the ant but it is closer on the left side, so the ant will rotate to the right. If on the other hand, the right and the front rays collide with an obstacle, then the ant will know that the obstacle is towards the right side, and it will rotate to the left. if only front collides, which can happen if the ant is walking straight into some obstacle, it will just rotate towards any side, it doesn’t matter. The ant will keep rotating to a side till all three ray-casts stop colliding with any obstacle. To make sure that the ant knows what it is colliding with, I will put obstacles in the obstacles group.
Before we make any changes in the code, let us modify the behavior tree a bit. It should look like this:
graph TD
R[Root] --> SEQ1[Sequence]
SEQ1 --> ACTION1[Move Forward]
SEQ1 --> DEC1[/Inverter\]
SEQ1 --> ACTION2[Rotate]
DEC1 --> ACTION3[Check for Collisions]
style R fill:#7D6B7D,color:#fff
style SEQ1 fill:#FF8C64,color:#222
style ACTION1 fill:#3EB595,color:#fff
style ACTION2 fill:#3EB595,color:#fff
style ACTION3 fill:#3EB595,color:#fff
style DEC1 fill:#FEA443,color:#fff
Once I have made changes to my Behavior Tree Nodes in the scene, I will add the following function to the Ant’s script.
export(NodePath) onready var ray_collider = get_node(ray_collider)
export(NodePath) onready var ray_collider_l = get_node(ray_collider_l)
export(NodePath) onready var ray_collider_r = get_node(ray_collider_r)
func check_for_collisions() -> int:
var hit_f := false
var hit_l := false
var hit_r := false
if ray_collider.is_colliding():
var obstacle = ray_collider.get_collider()
if obstacle.is_in_group("obstacles") or obstacle.is_in_group("ant"):
hit_f = true
if ray_collider_l.is_colliding():
var obstacle = ray_collider_l.get_collider()
if obstacle.is_in_group("obstacles") or obstacle.is_in_group("ant"):
hit_l = true
if ray_collider_r.is_colliding():
var obstacle = ray_collider_r.get_collider()
if obstacle.is_in_group("obstacles") or obstacle.is_in_group("ant"):
hit_r = true
if hit_f and hit_r:
return -1
elif hit_f and hit_l:
return 1
elif hit_f:
return 0
elif hit_l:
return 1
elif hit_r:
return -1
else:
return 2
The Check collisions leaf node code will be something like this:
extends ActionLeaf
func tick(actor, blackboard) -> int:
var delta = blackboard.get("delta")
var result = actor.check_for_collisions()
if result == -1 or result == 1:
actor.rotate_till_clear(true, result, delta)
return SUCCESS
if result == 0:
actor.rotate_till_clear(true, 1, delta)
return SUCCESS
if result == 2:
actor.rotate_till_clear(false, 1, delta)
return FAILURE
print("reaching till end")
return FAILURE
I will add a few static objects to the scene and those static objects to the "obstacles" node. I will add the ant parent node to the "ant" group. If I run the simulation now, the ant will walk aimlessly around, towards random directions, rotating every now and then, but also avoiding any obstacles.
When you now run the simulation, the ant will wander around on the screen. It will either rotate on it’s own at random intervals, or turn around just as it is about to collide with an obstacle.
I will be adding more features to the ant simulation in the upcoming posts. The full updated project is available on github for download and experimenting. The wandering ant example is available under the Wanderer
folder.
Photo by hybridnighthawk on Unsplash
-
A ray cast in game development is a ray that is sent out in a straight line forward from any origin to a target or direction and is of a specified length. If the ray collides with an object(s), it will return some details, like the objects it has collided with, the positions it collided with each object etc. ↩
Comments