🤸Creating a custom behavior
Behaviors are the most central piece of a NACT NPC.
NACT behaviors utilizes the awesome Classlib library from Timmy so all Behaviors have to be declared as a class, if we take the simplest behavior , NACT_idle
NACT_Idle = BaseClass.Inherit("NACT_Idle")
Constructor interface
When the NPC will instanciate your behavior it will call your constructor with the following parameters:
function NACT_Idle:Constructor(NpcInstance, tBehaviorConfig)
The first parameter is always the NPC that is invoking the behavior, the second parameter is the configuration of the behavior defined by the user.
As a convention in NACT, we store the NpcInstance in the npc field of the class:
self.npc = NpcInstance
Finally, most of the time your behavior will periodically check for stuff in the Main function, hence, we create a new Timer calling the Main function of your class
self.timerHandle = Timer.SetInterval(function()
self:Main()
end, NACT.ValueOrDefault(tBehaviorConfig.intervalTime, 1000), self)
As anorther convetion, we suggest you support the intervalTime
field in your behavior, this allows gamemode creators to fine tune the refresh rate depending of their needs.
There are use cases where your behavior will only react to events, in those cases, you do not need a timer or a Main function, refer to the Event delegation section on this page.
Main function
NACT_Idle main behavior
The main function holds the periodical checks of your behaviors, if we take the Main function of the idle behaviors:
function NACT_Idle:Main()
if (#self.npc.territory:GetEnemiesInZone() > 0) then
self.npc:GoNextBehavior()
else
if (not self.preventReturnToInitialPos) then
self.npc:MoveToPoint(self.npc.initialPosition)
end
end
end
First, we call the Territory function of GetEnemiesInZone and we check that there is at least one enemy. If there is one enemy, then we move to the next defined behavior with GoNextBehavior
Ortherwise, if no enemies are present, we move the NPC back to it's initial spawned position, if in the config we don't prevent it.
As you can see, since the NPC is available in your behavior, you can access all the NPC function with self.npc
and all the Territory functions with self.npc.territory
NACT_Engage main behavior
Idle is very straightforward, we can see the Engage behavior main function, wich is the behavior used when the NPC is actively firing at you
function NACT_Engage:Main()
local weapon = self.npc:GetWeapon()
if (self.npc:ShouldReload()) then
self.npc:SetBehavior(self.mainBehavior)
return
end
self.npc:MoveToFocused()
local bFocusedVisible = self.npc:IsFocusedVisible()
if (bFocusedVisible) then
self.npc:TurnToFocused(self.innacuracy)
if (weapon) then
weapon:PullUse(0)
end
end
if (not bFocusedVisible) then
self.npc:SetBehavior(self.mainBehavior)
end
if (self:TimeElapsed() > self.maxTimeEngaged) then
self.npc:SetBehavior(self.mainBehavior)
end
end
As you can see, the logic is quite similar, a few things differ:
Main Behavior is configurable. The main behavior is the Decision behavior to go back to when the NPC needs to take a decision because he can not continue Engaging for various reasons
The NPC has a few conditions in wich he can no longer keep firing at the focused character (no more ammo, character is not visible anymore, too much time firing). Thoose represent the relations between the nodes of the Behavior Tree
Event delegation
With or without periodic checks, your Behavior can react to events. Thoose are handled by a callback function in your behavior class
The convention used by nact is defining an On with the event name you need to react to. All the parameters of the event will be passed to the callback For example when an NPC takes a damage while Engaging:
function NACT_Engage:OnTakeDamage(_, damage, bone, type, from_direction, instigator, causer)
local decision = math.random(0, 10)
local causerCharacter = NACT.GetCharacterFromCauserEntity(causer)
if (causerCharacter) then
self.npc:SetFocused(causerCharacter)
end
if (decision > 5) then
self.npc.character:SetStanceMode(StanceMode.Crouching)
Timer.SetTimeout(function()
self.npc.character:SetStanceMode(StanceMode.Standing)
end, 500)
end
if (decision == 2) then
self.npc:SetBehavior(self.mainBehavior)
end
end
As you can see, the parameters are exactly the same as the TakeDamage event of Nanos World. We first call the random function to roll a dice on what the NPC will take as a prevenive action. Then, we find the culprit from the causer entity with GetCharacterFromCauserEntity, if the causer is a character and was found, we immediatly focus him. Finally, with the RNG either we will crouch to make aiming more difficult to the causer, or return to the decision behavior.
During the alpha, most events are not delegated since we are thinking on wich are useful or not.
If you miss an event that is important for you, please open an issue or pull request.
You can find all the delegated events in NACT/Server/npc/Index.lua
NACT_PROVISORY_REGISTERED_EVENTS = {
"TakeDamage",
"MoveComplete"
}
Destructor
Remember, behaviors are very volatile some may only exist for a few seconds if not less. As such remember to clear everything you are using in a Destructor function.
function NACT_Engage:Destructor()
Timer.ClearInterval(self.timerHandle)
local weapon = self.npc:GetWeapon()
if (weapon) then
weapon:ReleaseUse()
end
end
If you forget to clear some ressources, your NPC will start to behave really wierdly pretty quickly don't worry !
Using your custom behavior
Finally, to use your behavior, you have to put it in the behavior config of an NPC:
NACT.RegisterNpc(cPatrollingNPC, "MyTerritory", {
behaviors = {
{ class = NACT_Idle },
{ class = MyAwesomeBehavior, config = {
configKey = "configValue"
}}
}})
Last updated