Making a Simple AI
Simple AI Example
This is a very simple AI entity aimed at beginners that follows and kills nearby players. Copy and paste this for a good starting point and/or read on to understand more about how it works.
Note that this example uses the Player
class, which is technically deprecated. Therefore, you might want to swap in your own player class. Or not.
The map you're spawning this on needs to have its navigation mesh built. See other tutorials for how to do that if you're using your own map. If you're using someone else's map it probably has that already.
The Code
The Explanation
Class Information
Here we add some class attributes so that the entity shows up in Hammer. We inherit from AnimatedEntity
because most AI will probably use animations at some point, but you could use ModelEntity
, or something else.
Variables
We'll see what these do in a second. Note that it is not standard practice to capitalise variables LIKE_THIS in C#. However, it's just how I like to do it when it comes to constants that don't change.
We set the class variables to protected because these are not usually things that would be of interest to other classes. Then again, there are circumstances where they would be, so feel free to make them public or whatever if you need to.
Spawn()
Basically just set the model of this entity to a melon.
Tick()
One of the biggest dilemmas when it comes to writing NPC logic is how to avoid writing nonsense spaghetti code. There is no "correct" way to implement NPC logic, but a good way is to create some kind of state machine.
A state machine means that at any point in time, the NPC is in one of many pre-defined states (in this case we have "idle" and "attacking player"). Each state is responsible for defining its own behaviour as well as under what circumstances it can switch to other states. Here we simply implement all that with a basic switch statement. We call this on the server tick event, so this code will only run on the server, where it probably belongs.
PerformStateIdle()
Each state is its own function, and this is one of them. In the idle state, we check to see if there are any players within CHASE_DISTANCE
. If there are, we switch our state to "attacking player". If not, we do nothing.
PerformStateAttackingPlayer()
First, we get the closest player we can find. If we didn't find a player, or they were further than CHASE_DISTANCE
, we set our state to idle and give up.
If it's been at least one second since we generated a path to the player, then we generate one. We do this every second because if we generated a path every tick then that wouldn't be very efficient.
We then call TraversePath()
, which makes the NPC move along the path.
If the player is within ATTACK_RANGE
, we inflict damage on them.
GetClosestPlayer()
This quite literally gets the closest player, or null if no players were found.
GeneratePath()
Here we use s&box's navigation mesh API to generate a path. Let's go through each bit of it:
PathBuilder( Position )
We initialise thePathBuilder
starting from our current position.WithMaxClimbDistance( 16f )
We don't want the melon to climb walls etc so we set a low value. Typically 16f is the size of a human-walkable step.WithMaxDropDistance( 16f )
We don't want the melon to jump down walls etc so we set a low value.WithStepHeight( 16f )
As mentioned, 16f is the size of a typical step, so that's what we set here.WithMaxDistance( 99999999 )
Make the path as long as possible.WithPartialPaths()
Partial paths means that the navigation generator can generate incomplete paths to the player (e.g. if it can only figure out how to get halfway or something).Build( target.Position )
Build a path to the player's position.Segments
A path is split into segments. We access that here.Select( x => x.Position )
We get the position from each segment.ToArray()
Return it as an array, becausePath
is an array.
The end result is just that we get an array of Vector3
s representing our newly-generated path.
We set CurrentPathSegment
to 0 because we have generated a new path and so we need to start at the beginning (0) of it.
TraversePath()
This actually moves the NPC along its path.
If there is no path, we return and give up.
We need to move MOVEMENT_SPEED
every tick (NB: this function is called every tick). However, this is not as simple as just taking the next segment in the path and moving towards it. We cannot assume that we will only ever move towards one segment per tick. For example, if our movement speed is 100f, but each path segment is only 10f away from the next, then we will actually move through 10 segments per tick (this is an exaggerated example, but you get the idea).
So, first we check to see if the next path segment is further away than the amount we can move this tick (distanceToTravel
). If it is, then we simply move towards it and return, because we have expended the amount we can move this tick.
If the next path segment is closer than the amount we can move, then we move to the next segment and decrease distanceToTravel
by the amount we have moved. We move on to the next segment by doing CurrentPathSegment++
and repeat over again, until distanceToTravel
is zero.
If we reach the end of the path, we set the path to null so we don't use it anymore and we return.
Challenges
- The melon doesn't always touch the floor properly. Using
Trace.Ray
, can you trace a ray to the floor and adjust its position based on its distance to the ground? - The melon has no collision. Can you use
SetupPhysicsFromModel()
to give it some? - The melon is immortal. Can you implement
TakeDamage()
to make it killable? - The melon can move through objects. Can you implement
MoveHelper
in order to make it collide with stuff? - The melon is a bit boring. Can you add sound effects, e.g. when the melon attacks?