Making a Pirate Game #2 - Behavior Trees

After about a month of after-hours work, I finished adding the behavior tree skeleton for NPCs in my game. It is good :D You can see the results with a description in the video below, and for anyone more interested, there is also some technical rambling.

What even are these behavior trees?

For simplicity, I will use the name BT (Behavioral tree). BTs are made of nodes representing different types of behavior, such as conditions, actions, sequences, selectors, decorators, and others. Nodes are connected in a hierarchical structure that defines the order and way behaviors are executed. Behavior trees are easy to create, modify, and debug because they are readable and modular. If you can handle English, I highly recommend the article that helped me understand how all of this works in practice: https://www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work.

How did I implement it?

I am a simple programmer. When something needs to be done, I check whether someone did it before me and whether I can find it on the internet for free. In this simple way, I found several libraries implementing BTs. The most advanced one had documentation in Chinese… Then there was one nice library where everything was described, but unfortunately the way nodes returned information (return true) about whether a task was complete did not suit me.

In the end I found this library: https://github.com/tanema/behaviourtree.lua. It does, however, have one drawback. Nodes can be registered so they can be reused in other trees. Unfortunately, new instances of them are not created, so the same registered node used in several different BTs can cause many unacceptable problems. I was crushed until I read the library code and it turned out to be quite easy to understand. So I changed a lot locally to fix it. I proposed some of those changes to the author as a PR, he accepted them, but unfortunately they need tests before they can be finally merged. So for now the PR is waiting for my update.

Information about whether a task has been completed is provided by the task:success() function, which lets you assign a task reference outside the tree. In the Defold engine, which relies on sending messages between objects, this is priceless.

How does it look in my game?

Right now I have implemented a BT skeleton and added a few simple behaviors. At the moment there is only one type of NPC in the game, so I named its tree SIMPLE_SAILOR. I am dropping a piece of code here with explanations. Maybe someone will find it interesting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
SIMPLE_SAILOR = {
  --[[
    The node type below will run the nodes
    passed into it one by one until one of them
    returns task execution as `failure`.
  ]]--
  type = BehaviourTree.RepeatUntilFail,
  nodes = {
    --[[
      A simple check to see whether the NPC ship
      is alive at all. TASKS represents an object
      with references to nodes containing code.
      Such a `task` can return `success`,
      `failure`, or `running`.
    ]]--
    TASKS.IS_ALIVE, -- 
    {
      --[[
        Below is a sequence. It will execute tasks
        one by one until one of them returns `failure`.
        Its general job is to check whether the NPC ship
        is being attacked. If it is not, the sequence
        returns `failure`, which would interrupt the loop
        above. That is why there is a decorator here that
        does not care about the sequence result and always
        returns `success`, so the loop keeps running.
      --]]
      decorator = BehaviourTree.AlwaysSucceedDecorator, --
      node = {
        type = BehaviourTree.Sequence,
        nodes = {
          TASKS.IS_UNDER_ATTACK,
          TASKS.STOP_SAILING,
          TASKS.SELECT_RANDOM_AGGRESSOR_AS_ATTACKED_ENTITY,
          {
            --[[
              The node above - IS_UNDER_ATTACK - checks
              whether the NPC is being attacked. If it is,
              the next one starts - STOP_SAILING - which
              stops the NPC after 1.5 seconds. Then a random
              enemy attacking the NPC is selected and assigned
              as the attack target. Once that happens, another
              loop starts, where the NPC keeps firing cannons
              as long as the enemy is alive or has not stopped
              attacking.
            --]]
            type = BehaviourTree.RepeatUntilFail,
            nodes = {
              TASKS.SHOOT_FROM_CANNONS_ATTACKED_ENTITY,
              TASKS.IS_ENEMY_ALIVE,
              TASKS.IS_ENEMY_STILL_ATTACKING,
            },
          },
        },
      },
    },
    {
      --[[
        When the NPC has dealt with the enemy,
        or there was no enemy at all, a random
        choice is made about what to do next :D
        The `Random` node is used for that.
        It has two nodes under it: one SAIL_TO sends
        the ship to a random place on the map, while
        the other selects a random enemy and attacks it
        until it dies. The chances of drawing a given node
        are assigned in `chances`, matching the order
        of nodes in `nodes`.
      --]]
      type = BehaviourTree.Random,
      chances = { 85, 15 },
      nodes = {
        TASKS.SAIL_TO,
        {
          type = BehaviourTree.Sequence,
          nodes = {
            TASKS.STOP_SAILING,
            TASKS.SELECT_CLOSEST_SHIP_AS_ATTACKED_ENTITY,
            {
              type = BehaviourTree.RepeatUntilFail,
              nodes = {
                TASKS.SHOOT_FROM_CANNONS_ATTACKED_ENTITY,
                TASKS.IS_ENEMY_ALIVE,
              },
            },
          },
        },
      },
    },
  },
},

Summary

Adding BT is a big step. It will let me add modular behaviors quickly. Implementing the tree itself took me about 3 weeks, and writing the behaviors took a few days. I should add that I work on the game about 1 hour a day, so not much. By default, the game will have more trees, each for a different type of NPC with more or less complex actions.

Now I am moving on to implementing pathfinding. Thanks to that, NPCs will avoid islands and places where ships should not sail.

Talk soon!