Best way to approach NPC AI?

I recently did a bit of messing around with creating an NPC and the AI that goes along with it, however, a big problem I faced was the absurd amount of performance issues my methods caused. After some digging around, I found the root of my problem, a while true do loop.

This while true do loop pretty much looped through the script and called the functions that correlated to attacking, finding the target, etc, whenever it was needed.

So my question is, how do I go about creating a more event-driven approach to creating the AI for it? One that doesn’t use while true do loops while seemingly connecting all necessary functions together as well as something that could be used with CollectionService?

1 Like

Im assuming each AI has it’s own loop.

Try having one master controller script that controls ALL AI. Also, try making a ā€œschedulerā€ of sorts. Perhaps the more distant they are, the less the AI updates or something. That way, closest AI would be the most responsive.

You would also benefit from making the AI ā€œsleepā€ when it doesn’t need to do anything; i.e not processing that AI at all if it’s just standing there. Only process what’s needed. Processing one at a time in this fashion would yield the best performance gain, provided each AI can wait 0.03 seconds between the other. 0.03 * 100 = 30 seconds for all AI to process one task cycle, that’s no good. Try to make it so each AI is processed every 1 second if its not marked ā€œasleepā€ (see below). In this case, we would have to do 3 AI task cycles per wait() to keep all 100 AI doing something every 1 second, which is still a lot better than each of them doing it every wait(), in fact it’s exactly 33.333x more efficient in this particular example.

For calculating distances, calculate the distances for all AI and put them into a table, do this once per loop on the master controller. You can then use the distance to judge whether you want them to sleep/wake up, delay their next action for X cycles, something like that. The more distant the AI is, the less often you can check its magnitude for even better performance, then every AI doesn’t need to check magnitude every task cycle.

The idea here is for one, cutting down on the number of threads with loops, and two, cutting down on the number of things your doing with these AI per loop cycle.

Also, any constant variables, store them outside of the loop, including functions i.e math.random

It’s much faster if you have them pre-stored like so:

local workspace = game:GetService"Workspace"
local dummies = workspace:WaitForChild"Dummies"
local random = math.random
... etc

Also, if you use the same thing more than once inside the loop, make sure you that is a variable too. i.e:

while true do
local t = os.time()
end

instead of spamming os.time() many times. Same goes for magnitude checks, health checks, anything of the sort. Check it once, store the result, keep on processing the data.

These are all good places to start. Lmk if I did a poor job explaining and I’ll clear it up for you, I’ve ran hundreds of AI in this manor, if you schedule them right, take advantage of putting AI you cant see to sleep, and the other methods described, it’s a night and day difference in performance.

1 Like

Sounds interesting, however I planned on creating a scheduler utilizing CollectionService and modules, therefore, as I said before, I was looking for a more event driven approach that doesn’t utilize loops in that sort of way.

Although I do want to keep this in consideration just in case I can’t find a solution to my original goal, therefore I do have a question to ask.

  1. When considering if an NPC is ā€œdistantā€ from a player, would this take into account multiple players and their distance relative to each NPC? So you would check if there is ANY player within range, and if there is, the NPC becomes active? If this is the case, wouldn’t this be more performance heavy when compared to my original method that utilized while loops?
{
decision = function -- decide what task to do
taskA = funtion -- do whatever, trigger decision when we're done, also trigger decision when an interruption (cancel) occurs, like coming close to something, the environment changing, etc.

}

For a truly event-driven approach, you could have a totally different setup where each AI is appended to a table, and each ā€œAIā€ in the code is a table full of properties and methods and events for various actions of your choosing.

When the AI is first started, it could fire an event we’ll call ā€œdecideā€ that would decide what the first action is.

It can then start that action, lets say it chose to walk towards a player using pathfind or moveto or whatever. It would start doing that, and it would have a mini loop that detects if they got stuck (the action is cancelled), or they reached their destination (the action was completed, aka they are in X range). This would fire the ā€œdecideā€ event again.

Now, the decide event sees the player is close enough (you could even cache that magnitude in the AI table so it doesnt have to check every decision in case theres rapid decisions like combat. It could re-fetch and ignore cache after 1 second. Then, no two entities magnitude is checked more than one second. That could be tuned however. - So, now you’d decide to do combat stuff or whatever, like moving rapidly with math.random() offsets if a sword fight, in order to simulate them trying to slice n’ dice you. Each time it does this movement, and swings the sword, it would fire ā€˜decide’ event again.

Rinse and repeat for whatever actions you want.
Each action should support being cancelled (interruption), in which case it goes back to ā€˜decision’ event again.
Each action should fire ā€˜decision’ on success, and should schedule its own decision so long as its not interrupted.

forgive me if this is totally flawed logic, I just thought of all this off the top of my head… but it could work. The only thing stopping you from full event based is checking magnitudes which would have to be triggered by a loop.

What do you think?

1 Like

From what I’ve read and understood, this definitely does seem like it could work, however I would have to put this into actual code and try it out for myself, although I’m not experienced at all with OOP, from my extremely limited knowledge about it, your first solution regarding tables and functions do seem pretty similar to a more OOP based solution. However I could be mistaken so please correct me if I’m wrong.

1 Like

Yes, definitely. I prefer to make stuff pretty well entirely OOP. A main task fires off some stuff and the game just reacts to events being fired. With something like this, it’s especially useful because rather than having a loop constantly doing things, you can start performing actions on an as-needed basis for the AI. That’ll have undoubtedly the best performance, albeit it’s a little more complex than your traditional loop-based AI scheduler. The only way to really optimize performance further would be to scrap humanoids in favor of custom methods.

The creator of Store Empire [ Tycoon ] - Roblox switched to such a system - I don’t recall the exact statistic I was told but the performance gain was massive, I do know that for certain. Should you end up getting this system working, and you desired more optimization, this could be something you could demo and test the gains of. The AI there are also handled client-side I believe, but that may not work as well for you as Store Empire loads in and out stores as you walk around - it seems like you want just a combat-based AI.

2 Likes

Yep, I’m currently trying to create AI thats reminiscent of something you would find from the Souls series (Dark Souls, BloodBorne, etc), therefore this AI is heavily centered around combat in relation to my already functioning Combat System and doesn’t need to be constantly loaded and unloaded (with some exceptions of course)

And honestly, from your description of OOP and how the game reacts to events being fired is exactly what I’m looking for, it even matches the way I create my frameworks. I honestly need to look more into this in the near future so I thank you for that.

1 Like

Good luck, hope you get the results you’re after!

1 Like

Thanks! I appreciate it.

I’ll be leaving this thread open in case anyone else wants to give their feedback regarding this topic since I’m still looking for as much help as I can get.

2 Likes