Blog Tworzę grę o piratach #2 - Drzewa behawioralne
Post
Anuluj

Tworzę grę o piratach #2 - Drzewa behawioralne

Po około miesiącu pracy po godzinach, skończyłem dodawanie szkieletu drzewa behawioralnego dla NPC w mojej grze. Jest dobrze :D Efekty wraz z opisem można zobaczyć na poniższym wideo, a dla zainteresowanych bardziej, jest też trochę technicznego pierdololo.

Czym w ogóle są te całe drzewa behawioralne?

Dla uproszczenia będę używał nazwy BT (Behavioral tree). BT składają się z węzłów, które reprezentują różne typy zachowań, takie jak: warunki, akcje, sekwencje, selektory, dekoratory i inne. Węzły są połączone w hierarchiczną strukturę, która określa kolejność i sposób wykonywania zachowań. Drzewa behawioralne są łatwe do tworzenia, modyfikowania i debugowania, ponieważ są czytelne i posiadają modularną formę. Jeśli umisz w angielski, to gorąco polecam artykuł który pomógł mi zrozumieć jak to wszystko działa w praktyce https://www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work.

Jak to zaimplementowałem?

Jestem prostym programistą, gdy trzeba coś zrobić, sprawdzam czy ktoś to zrobił przede mną i czy znajdę to w internecie za friko. Tym prostym sposobem, trafiłem na kilka bibliotek z implementacją BT. Najbardziej rozbudowaną była taka, której dokumentacja jest po chińsku… Dalej była jedna fajna, gdzie wszystko było odpisane, niestety sposób w jaki węzły zwracały informację (return true), czy zadanie jest wykonane, nie odpowiadał mi.

Na końcu znalazłem tę bibliotekę https://github.com/tanema/behaviourtree.lua. Jednakże, posiada ona jeden minus. Węzły można zarejestrować, tak aby używać je ponownie w innych drzewach. Niestety, nie są tworzone ich nowe instancje, dlatego taki zarejestrowanych węzeł w kilku różnych BT potrafi sprawić wiele nieakceptowalnych problemów. Byłem załamany, do momentu przeczytania kodu biblioteki, okazało się że jest całkiem prosty do zrozumienia. Dlatego zmieniłem lokalnie dużo rzeczy aby to naprawić, niektóre zaproponowałem twórcy jako PR, zgodził się na nie, niestety wymagają one dodania testów aby zostać finalnie połączone. Więc póki co PR czeka na moją aktualizację.

Informacja czy zadanie zostało wykonane jest podawana przez funkcję task:success(), dzięki czemu można przypisać referencję do zadania poza drzewem. W silniku Defold, który polega na wysyłaniu wiadomości między obiektami, jest to nieocenione rozwiązanie.

Jak to wygląda w mojej grze?

Obecnie zaimplementowałem tak zwany szkielet BT i dodałem parę prostych zachowań. Póki co, w grze występuje tylko jeden typ NPC, dlatego jego drzewu nadałem nazwę SIMPLE_SAILOR. Wrzucam tu kawałek kodu z objaśnieniami, może kogoś to zainteresuje.

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
92
93
94
95
SIMPLE_SAILOR = {
  --[[
    Typ węzła poniżej będzie po kolei
    odpalał wrzucone mu węzły, dopóki
    któryś z nich zwróci wykonanie zadanie
    jako `failure`.
  ]]--
  type = BehaviourTree.RepeatUntilFail,
  nodes = {
    --[[
      Proste sprawdzenie czy statek NPC
      w ogóle żyje. TASKS reprezentuje
      obiekt z referencjami do węzłów
      gdzie jest kod. Taki `task` może
      zwrócić `success`, `failure` lub
      `running`.
    ]]--
    TASKS.IS_ALIVE, -- 
    {
      --[[
        Poniżej jest sekwencja, będzie wykonywać
        zadania po kolei, dopóki któreś z nich
        nie zwróci `failure`. Ogólnie ma ona za zadanie
        sprawdzić, czy okręt NPC jest atakowany. Jeśli
        nie jest, sekwencja zwróci `failure`, przez co
        przerwie działanie pętli wyżej. Dlatego jest
        tu dodany dekorator, który wynik sekwencji
        ma gdzieś i zawsze zwróci `success`, dzięki
        czemu pętla działa dalej.
      --]]
      decorator = BehaviourTree.AlwaysSucceedDecorator, --
      node = {
        type = BehaviourTree.Sequence,
        nodes = {
          TASKS.IS_UNDER_ATTACK,
          TASKS.STOP_SAILING,
          TASKS.SELECT_RANDOM_AGGRESSOR_AS_ATTACKED_ENTITY,
          {
            --[[
              Węzeł wyżej - IS_UNDER_ATTACK sprawdza czy
              NPC jest atakowany, jeśli tak, odpala się
              kolejny - STOP_SAILING, który zatrzymuje
              NPC po okresie 1.5 sekundy. Następnie
              jest wybierany losowy przeciwnik który
              atakuje NPC i jest przypisany jako cel
              ataku. Gdy to się stanie, odpala się
              kolejna pętla, gdzie NPC będzie strzelało
              z armat, dopóki przeciwnik żyje, lub
              przestał atakować.
            --]]
            type = BehaviourTree.RepeatUntilFail,
            nodes = {
              TASKS.SHOOT_FROM_CANNONS_ATTACKED_ENTITY,
              TASKS.IS_ENEMY_ALIVE,
              TASKS.IS_ENEMY_STILL_ATTACKING,
            },
          },
        },
      },
    },
    {
      --[[
        W momencie gdy NPC uporało się z wrogiem,
        albo w ogóle go nie było, odbędzie się
        losowanie, co robić dalej :D Do tego
        służy węzeł `Random`. Ma pod sobą dwa
        węzły, jeden SAIL_TO który wysyła okręt
        w losowe miejsce na mapie, drugi natomiast,
        wybiera losowego przeciwnika i atakuje go aż
        do jego śmierci. Szanse na wylosowanie
        konkretnego węzła, są przypisane w `chances`
        odpowiednio do kolejności węzłów w `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,
              },
            },
          },
        },
      },
    },
  },
},

Podsumowanie

Dodanie BT to duży krok, pozwoli dodawać modularne zachowania w szybkim czasie. Samo zaimplementowanie drzewa zajęło mi z jakieś 3 tygodnie a napisanie zachowań to kilka dni. Dodam, że pracuje nad grą z 1h dziennie, czyli niewiele. Domyślnie w grze będzie więcej drzew, każde dla innego typu NPC z bardziej lub mniej złożonymi akcjami.

Teraz zabieram się za wdrożenie wyszukiwania ścieżek, dzięki temu NPC będą omijały wyspy i miejsca na które nie powinno się wpływać statkiem.

Do usłyszenia!

Ten post jest udostępniony na licencji CC BY-NC-ND 2.0 przez autora.

Komentarze