As you all know I’ve been working on End of Days for quite some time and one of the biggest hurdles has been with AI. I recently began diving into AI on a deeper level and there are some concepts I want to look into like Utility AI , UE4’s crowd AI controller, as well as dig into some of my own hunches for optimization. I’ll be writing about my findings to see if I can help the community progress in the same direction as I noticed it’s hard to find good set ups for large quantities of AI.
Originally, I was using AI behavior toolkit for my AI and having even 25 of them on a map can reduce my fps to 40 or less. I just switched over to some very simple c++ AI using pawn sensing, behavior tress, and a custom movement component ( I’ll go over that in a moment) and I’ve found that I can now get over 100 AI running at 120 fps with worst case scenario being about 90 fps. I want to crank those numbers as high as possible.
The custom movement component essentially allows for occlusion and does not render the movement updates when the AI aren’t on screen. I got this nice improvement from a user ColdSteel48 on the UE4 forums back in 2017 and didn’t understand a bit of it, until recently, as I am now more acquainted with the engine and c++.
Here is a sample of this in action. This is not End of Days, but rather a modified version of Tom Looman’s survival game example
For those interested, the tweak looks a bit like this :
class YOUR_GAME_NAME_API UCM: public UCharacterMovementComponent
virtual void InitializeComponent() override;
//Useful to make sure that your characters are using the correct Component.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = StubCheck)
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
m_nTickGroup = FMath::RandRange(3, 4);
m_DeltaTime = 0.0f;
m_SkipFloorChecks = FMath::RandBool();
m_nTicksCount = 0;
void UCM::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
m_DeltaTime += DeltaTime;
if (++ m_nTicksCount % m_nTickGroup == 0)
m_nTicksCount = 0;
DeltaTime = m_DeltaTime;
m_DeltaTime = 0.0f;
//Call Default CharacterMovementComponent
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
All this is well and good, but not good enough for the numbers that I want to push. The next step to is find a way to stagger the tick of AI in groups sorted by distance. This is easy in single player, but how does this work in a multiplayer environment when players can be at 4 corners of a map? I’ll be investigating this in the coming week.
Other things to consider:
- pathing in such a way that not all the characters run in a straight line after their target. A simple solution to this is to generate offsets to the left and right of the player and set those as the destination until the AI is close enough to the player to run straight at them.
- These AI are quite simple and do not do that great in an open world environment, let alone a procedural one. This is where I believe the Utility AI concept will come in handy.
- AI should avoid colliding with each other and also should know how to path with other AI of varying sizes
- The number of AI controllers becomes a problem at a certain point, so I am considering setting up a fixed number of AI controllers and having the densest cluster near the players so that the AI seem more intelligent and all AI behind those AI fall into some sort of group AI controller ( almost RTS style ) as they won’t need to make any choices other than follow the AI in front of them and avoid colliding with each other. That doesn’t even need to be that precise when the distance and fog end up hiding it naturally. Perhaps this is also how I will control the staggered tick.