Kinetic #2 - A.I. Baby Steps
- Jared Greiner
- Feb 8, 2021
- 4 min read
After posting my first video last week of my personal game dev project, Kinetic, one of my game designer friends pointed out that it seemed difficult to avoid taking damage from the enemies. That inspired this weeks work on the project. He suggested adding a cover system, which I am interested in doing, but I wanted to start with some far more basic levers that would let me tune the difficulty of the enemies. Here's what I have implemented so far:

FireTicketScore (Calculated Dynamically) - I'll explain this further below, but it determines the priority this enemy should have for receiving permission to fire from within a group of enemies.
MaxBulletsPerFireTask (9) - How many bullets an enemy should fire in one burst. Before they always fired their whole clip until they needed to reload. This lets me limit it to a smaller number. (BulletsFiredThisFireTask simply tracks whether they have reached that limit yet or not.)
MaxFireDistance (20m) - This determined from how far away an enemy can fire at the player. This will be fun to vary across enemy classes so enemies with snipers stay back and enemies with shotguns charge in to close range. Setting this higher for the current enemies with my basic assault rifle actually made them easier to fight as they suffer the effects of bullet spread and recoil more at long distances. Gives the player more of a chance to track them and fight back if they start firing from further away.
MinTimeBetweenBursts (2 sec) - This causes the enemies to wait between firing bursts. Simulates feathering the trigger. When there is a group of enemies, this gives others that chance to claim the available fire tickets.
EnemyRecognitionTime (0.75 sec) - Once the enemy can see the player, they will wait a fraction of a second before firing. This simulates reaction time and really helps the enemies not appear to be cheating by tracking you through walls.
Accuracy (0.33) - This acts as a percent modifier to the bullet spread where 0 would be completely inaccurate and 1 would be normal accuracy. Normal is pulled from the weapon. So this multiplier essentially allows me to determine how much less accurate enemies are than the player with the same weapon in the same circumstances (i.e. standing still, in ADS, firing the first bullet with now recoil).
MuzzleVelocityMultiplier (0.5) - This also acts as a multiplier on the weapons normal muzzle velocity. By slowing down the enemy bullets compared to the players, it give the player a better chance to doge or outrun the bullets. It also visually helps players recognize which direction the bullets are coming from. I want to also add better bullet trail VFX to aid with that.
AimTrackingLag (0.075 sec) - This simulates the enemy trying to keep their aim locked on a moving target. They will aim where the player was a fraction of a second ago. This allows the player to escape dangerous situations by sprinting and ducking and weaving. It quickly feels silly if this is too large.
Ok, let me explain that FireTicketScore. This is based on a little tip shared near the end of this GDC talk from the Doom (2016) combat designers. In Doom enemies need a Token to attack the player. There are a limited number of tokens per attack type (light ranged, heavy ranged, melee). This ensures that even if 10 enemies could attack the player at once, only a limited number do. This is better than a simple cooldown between attacks as the enemies might end up on synchronous cooldowns and effectively attack in unintended waves of projectiles. My enemies need FireTickets to pull the trigger. I have a global pool of fire tickets available so only a limited number of enemies can have a ticket at once. I'll extend this with GrenadeTickets and MeleeTickets in the future. To determine who gets a ticket, I calculate a FireTicketScore per enemy. The enemies with the highest scores get the tickets. Enemies can request a FireTicket whenever their behavior tree reaches the Fire task, but if scores have been calculated within the last half a second, the request to recalculate is delayed.

The first event is called by any behavior tree when that individual enemy wants a FireTicket. The second event only fires when the cooldown met between recalculating FireTicketScores. This prevents the recalculation running multiple times a frame or just too frequently in a way that would impact performance. I might break up the calculation further so that enemies update their score on separate frames to further improve performance if I ever see it cause hitches.

When FireTicketScores are recalculated, enemies will receive a score of zero if they are unable to fire (reloading, out of range, fired too recently, etc.) and will receive a higher score the closer they are to the player, the longer it has been since they last fired, and if they are on screen. This gives some variance where occasionally enemies far away fire at the player, but closer enemies are more likely to be granted fire tickets. I need to implement fallback tasks for the AI like taunting, repositioning, etc. when they are denied a Fire Ticket.
All of that adds up into some nice group behavior. All of these variables will allow me to configure different enemies to behave a bit differently from one another. As I create variants, I want to pull the default values for each of these out of a Data Table linked to an Excel Sheet to allow for easy tuning in the future. The possibilities are exciting! But next, grenades...
Comentários