Adding NPC AIs
From VbGORE Visual Basic Online RPG Engine
vbGORE comes with several options for NPC behavior built-in, such as simple random movement, running towards and attacking a player, or running away from a player. For any serious, large-scale MMORPG, however, you'll eventually want to write your own. For more information on NPCs, take a look at Adding NPCs.
Note: This tutorial was written with the beginner in mind. If you've worked with vbGORE and VB6 a lot, you can probably skim through most of it.
[edit] Writing the code
Every NPC AI code is located in the server's NPC_AI routine - GameServer.NPCs.NPC_AI. This is called during the server's game loop, when the NPC has passed all the checks for updating - it's alive, it's on a map with users, its action timer has expired (it's time for it to act again), etc.
For the sake of this tutorial, we're going to add AI for a summoned NPC with a ranged attack - in this case he can be a Summoned Ninja. Find the NPC_AI sub, and find a blank spot before the End Select statement. We're going to add the case for our new AI there, so go ahead and add a comment header describing your AI, and the Case number (for this, we want 8). As so:
'*** Summoned Ranged Attacker *** Case 8 End Select End Sub
All the code under Case 8 will run whenever an NPC with its AI set to 8 acts. So how do we want this NPC to behave? First, as with the summoned melee attacker (Case 7), we want to make sure that it's only run by a summoned NPC with an owner:
'*** Summoned Ranged Attacker *** Case 8 'This routine is for summoned NPCs only! If NPCList(NPCIndex).OwnerIndex = 0 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 5000 Exit Sub End If End Select End Sub
It will reset the action timer and abort the sub if the NPC doesn't have an owner. If the rest of your code is well-designed, this sub should never be called by a non-summoned NPC anyway, but it's always better to be safe than sorry.
Next, we want our NPC to look around for any other NPCs it should be attacking (for now, our NPC will only attack other NPCs, as PvP is a little harder to do).
'*** Summoned Ranged Attacker *** Case 8 'This routine is for summoned NPCs only! If NPCList(NPCIndex).OwnerIndex = 0 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 5000 Exit Sub End If 'Look for a near-by NPC i = NPC_AI_ClosestNPC(NPCIndex, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).OwnerIndex, 1, 1) End Select End Sub
If you're not entirely sure what the NPC_AI_ClosestNPC sub does, it's easy enough to check (it's in the same module). What this sub does is returns the index of the NPC closest to our NPC, inside the range we specify. It also has options for if the NPC has to be hostile, if the NPC has to be attackable, and if the NPC can't be owned by a certain player. If it finds no NPCs, it will return 0, so let's check for that:
'*** Summoned Ranged Attacker *** Case 8 'This routine is for summoned NPCs only! If NPCList(NPCIndex).OwnerIndex = 0 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 5000 Exit Sub End If 'Look for a near-by NPC i = NPC_AI_ClosestNPC(NPCIndex, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).OwnerIndex, 1, 1) 'Check to see if we could find something If i = 0 Then End Select End Sub
So what do we want the NPC to do if there isn't anything to attack? We'll have it follow the user around, because it's a summon. In this case, we can just copy+paste some code from Case 7 again:
'*** Summoned Ranged Attacker *** Case 8 'This routine is for summoned NPCs only! If NPCList(NPCIndex).OwnerIndex = 0 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 5000 Exit Sub End If 'Look for a near-by NPC i = NPC_AI_ClosestNPC(NPCIndex, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).OwnerIndex, 1, 1) 'Check to see if we could find something If i = 0 Then 'There is nothing around to attack, so we want to tell it to move towards the user (we're following a bit behind, don't want to get too close to the action) If Abs(CInt(NPCList(NPCIndex).Pos.X) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.X)) < 4 Then If Abs(CInt(NPCList(NPCIndex).Pos.Y) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.Y)) < 4 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 500 Exit Sub End If End If End Select End Sub
That last bit of code might not make too much sense, because it's probably backwards from what you're thinking. What these If statements are doing is checking if the NPC is already close enough to the user, instead of checking if the NPC needs to move closer. So what we have so far, in pseudocode, is basically:
If there is no valid target in range, Then:
If we're close enough to the user that we don't have to move, Then:
We don't have to do anything, so abort the routine.
However, if we aren't close enough to the user, we have to move towards him. Since it will automatically exit the sub if the NPC is close enough, we can simply add the code without an Else statement.
'*** Summoned Ranged Attacker *** Case 8 'This routine is for summoned NPCs only! If NPCList(NPCIndex).OwnerIndex = 0 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 5000 Exit Sub End If 'Look for a near-by NPC i = NPC_AI_ClosestNPC(NPCIndex, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).OwnerIndex, 1, 1) 'Check to see if we could find something If i = 0 Then 'There is nothing around to attack, so we want to tell it to move towards the user (we're following a bit behind, don't want to get too close to the action) If Abs(CInt(NPCList(NPCIndex).Pos.X) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.X)) < 4 Then If Abs(CInt(NPCList(NPCIndex).Pos.Y) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.Y)) < 4 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 500 Exit Sub End If End If 'So let's set the direction toward the player, and try to move tHeading = Server_FindDirection(NPCList(NPCIndex).Pos, UserList(NPCList(NPCIndex).OwnerIndex).Pos) If NPC_MoveChar(NPCIndex, tHeading) = 0 Then 'Couldn't move so look for alternate paths End Select End Sub
Now what we've done is set a variable called tHeading to the direction the user is in, using the Server_FindDirection function. We then try to move using the NPC_MoveChar function. NPC_MoveChar tries to move, and if it is successful (the NPC can move) it returns 1. Needless to say, if it isn't, it returns 0. So let's assume we can't move in a direct line to the user. We're going to try and move diagonally or to the side now (for the sake of saving space, I've cut off the top part of the code):
If Abs(CInt(NPCList(NPCIndex).Pos.X) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.X)) < 4 Then If Abs(CInt(NPCList(NPCIndex).Pos.Y) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.Y)) < 4 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 500 Exit Sub End If End If 'So let's set the direction toward the player, and try to move tHeading = Server_FindDirection(NPCList(NPCIndex).Pos, UserList(NPCList(NPCIndex).OwnerIndex).Pos) If NPC_MoveChar(NPCIndex, tHeading) = 0 Then 'Couldn't move so look for alternate paths Select Case tHeading Case NORTH t1 = NORTHEAST t2 = NORTHWEST t3 = EAST t4 = WEST Case EAST t1 = NORTHEAST t2 = SOUTHEAST t3 = NORTH t4 = SOUTH Case SOUTH t1 = SOUTHWEST t2 = SOUTHEAST t3 = WEST t4 = EAST Case WEST t1 = SOUTHWEST t2 = NORTHWEST t3 = SOUTH t4 = NORTH Case NORTHEAST t1 = NORTH t2 = EAST t3 = NORTHWEST t4 = SOUTHEAST Case SOUTHEAST t1 = EAST t2 = SOUTH t3 = SOUTHWEST t4 = NORTHEAST Case SOUTHWEST t1 = SOUTH t2 = WEST t3 = SOUTHEAST t4 = NORTHWEST Case NORTHWEST t1 = WEST t2 = NORTH t3 = NORTHEAST t4 = SOUTHWEST End Select End Select End Sub
That's pretty big, but simple. t1, t2, t3, and t4 represent the alternate directions we can move in. If you trace through that code a bit, you shouldn't have much trouble gathering what it does.
Now we're going to actually try and move in those 4 directions.
Case SOUTHWEST t1 = SOUTH t2 = WEST t3 = SOUTHEAST t4 = NORTHWEST Case NORTHWEST t1 = WEST t2 = NORTH t3 = NORTHEAST t4 = SOUTHWEST End Select 'Try the alternate movements If NPC_MoveChar(NPCIndex, t1) = 0 Then If NPC_MoveChar(NPCIndex, t2) = 0 Then If NPC_MoveChar(NPCIndex, t3) = 0 Then If NPC_MoveChar(NPCIndex, t4) = 0 Then 'We still can't move, so just wait NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 1000 End If End If End If End If End Select End Sub
You can probably figure out what this does by now. It tries moving in each alternate direction, and if it still can't move, it will simply wait. We could implement some sort of advanced pathfinding algorithm to get around obstacles, but let's just say our NPC is dumb :P
Case NORTHWEST t1 = WEST t2 = NORTH t3 = NORTHEAST t4 = SOUTHWEST End Select 'Try the alternate movements If NPC_MoveChar(NPCIndex, t1) = 0 Then If NPC_MoveChar(NPCIndex, t2) = 0 Then If NPC_MoveChar(NPCIndex, t3) = 0 Then If NPC_MoveChar(NPCIndex, t4) = 0 Then 'We still can't move, so just wait NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 1000 End If End If End If End If End If Exit Sub Else 'Something was found, so now we get in position and attack! 'Get the position of the NPC tPos = NPCList(i).Pos End Select End Sub
What we've done now is add the trailing End If statement for If NPC_MoveChar(NPCIndex, tHeading) = 0 Then, and then aborted the sub. If we've gotten this far in the routine, we've either moved or not and there are no enemies, so we don't need to do anything else. If you're a bit confused, take a look at this updated pseudocode for our AI so far:
If there is no valid target in range, Then:
If we're close enough to the user that we don't have to move, Then:
We don't have to do anything, so abort the routine.
End If
Try to move directly towards the user. If we can't Then:
Try to move in an alternate path. If we still can't, Then:
We can't do anything else, so set our timer to wait and try again.
End If
End If
We've either moved or not by now, and there's no target, so abort the routine.
Else
We do have a target to attack, so get the target's position.
Now I'm going to post all the code for moving to the correct distance from the NPC. Don't worry, I'll explain it.
Else 'Something was found, so now we get in position and attack! 'Get the position of the NPC tPos = NPCList(i).Pos 'Run away and get in position if we're too close If Server_RectDistance(NPCList(NPCIndex).Pos.X, NPCList(NPCIndex).Pos.Y, tPos.X, tPos.Y, 3, 3) Then tHeading = Server_FindDirection(NPCList(NPCIndex).Pos, tPos) 'We're going away from it so pick the opposite direction Select Case tHeading Case NORTH: tHeading = SOUTH Case NORTHEAST: tHeading = SOUTHWEST Case EAST: tHeading = WEST Case SOUTHEAST: tHeading = NORTHWEST Case SOUTH: tHeading = NORTH Case SOUTHWEST: tHeading = NORTHEAST Case WEST: tHeading = EAST Case NORTHWEST: tHeading = SOUTHEAST End Select 'Now let's attempt to move away If NPC_MoveChar(NPCIndex, tHeading) = 0 Then 'Can't move there, so try alternate directions Select Case tHeading Case NORTH t1 = NORTHEAST t2 = NORTHWEST t3 = EAST t4 = WEST Case EAST t1 = NORTHEAST t2 = SOUTHEAST t3 = NORTH t4 = SOUTH Case SOUTH t1 = SOUTHWEST t2 = SOUTHEAST t3 = WEST t4 = EAST Case WEST t1 = SOUTHWEST t2 = NORTHWEST t3 = SOUTH t4 = NORTH Case NORTHEAST t1 = NORTH t2 = EAST t3 = NORTHWEST t4 = SOUTHEAST Case SOUTHEAST t1 = EAST t2 = SOUTH t3 = SOUTHWEST t4 = NORTHEAST Case SOUTHWEST t1 = SOUTH t2 = WEST t3 = SOUTHEAST t4 = NORTHWEST Case NORTHWEST t1 = WEST t2 = NORTH t3 = NORTHEAST t4 = SOUTHWEST End Select 'Try the alternate moves If NPC_MoveChar(NPCIndex, t1) = 0 Then If NPC_MoveChar(NPCIndex, t2) = 0 Then If NPC_MoveChar(NPCIndex, t3) = 0 Then If NPC_MoveChar(NPCIndex, t4) = 0 Then 'We still can't move (cornered! ack!) so attack! NPC_AI_AttackNPC NPCIndex, NPCList(NPCIndex).OwnerIndex NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + NPCDelayFight End If End If End If End If End If Exit Sub End If End Select End Sub
Lotta code right there. However, if you're keen, you'll have noticed it looks almost like the code we used before (In fact, much of it IS the code we used before. I'm far too lazy to write it all out again. Nothing wrong with a little Ctrl+C action). Let's use some more pseudocode to explain what this is doing. Refer to this little snippet, and then back at the code, and see if you can piece together what the code is doing:
If there is no valid target in range, Then:
If we're close enough to the user that we don't have to move, Then:
We don't have to do anything, so abort the routine.
End If
Try to move directly towards the user. If we can't Then:
Try to move in an alternate path. If we still can't, Then:
We can't do anything else, so set our timer to wait and try again.
End If
End If
We've either moved or not by now, and there's no target, so abort the routine.
Else
We do have a target to attack, so get the target's position.
If we're closer than 3 tiles to the target, Then:
Set tHeading to the opposite direction of where the target is
Try to move in that direction. If we can't Then:
Try to move in an alternate path. If we still can't, Then:
We can't move, but we have a target, so attack.
End If
End If
We've either moved away or attacked, so abort the routine.
End If
Making sense now? We're almost done, but we need some more code. What happens if we're already 3 tiles from the target? We want to go ahead and attack, so:
'Try the alternate moves If NPC_MoveChar(NPCIndex, t1) = 0 Then If NPC_MoveChar(NPCIndex, t2) = 0 Then If NPC_MoveChar(NPCIndex, t3) = 0 Then If NPC_MoveChar(NPCIndex, t4) = 0 Then 'We still can't move (cornered! ack!) so attack! NPC_AI_AttackNPC NPCIndex, NPCList(NPCIndex).OwnerIndex NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + NPCDelayFight End If End If End If End If End If Exit Sub End If 'We're already in position, so just attack b = NPC_AI_AttackNPC(NPCIndex, NPCList(NPCIndex).OwnerIndex) If b Then 'Make sure the attack was successful before applying the delay NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + NPCDelayFight Exit Sub End If 'Nothing to do, so just wait and try again in a second NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 1000 End If End Select End Sub
By now, you probably have a good idea of what those last few lines are. For reference, NPC_AI_AttackNPC returns a 1 if the attack is successful and a 0 if it wasn't. The ActionDelay counter is there to make sure that the NPC attacks on-time - don't want him attacking too fast or too slow.
Believe it or not, that's the end! Here's the completed pseudocode for AI number 8:
If there is no valid target in range, Then:
If we're close enough to the user that we don't have to move, Then:
We don't have to do anything, so abort the routine.
End If
Try to move directly towards the user. If we can't Then:
Try to move in an alternate path. If we still can't, Then:
We can't do anything else, so set our timer to wait and try again.
End If
End If
We've either moved or not by now, and there's no target, so abort the routine.
Else
We do have a target to attack, so get the target's position.
If we're closer than 3 tiles to the target, Then:
Set tHeading to the opposite direction of where the target is
Try to move in that direction. If we can't Then:
Try to move in an alternate path. If we still can't, Then:
We can't move, but we have a target, so attack.
End If
End If
We've either moved away or attacked, so abort the routine.
End If
We're longer than 3 tiles away and we've gotten this far in the routine, so attack.
If the attack was successful, Then:
Update the attack timer.
End If
Update the NPC's AI timer to try again in one second.
And here's the completed AI code. Feel free to copy and paste into your own game, although if all you do is copy+paste it without reading the tutorial you won't learn much.
'*** Summoned Ranged Attacker *** 'Note: only attacks NPCs for now. Fix later kthx. Case 8 'This routine is for summoned NPCs only! If NPCList(NPCIndex).OwnerIndex = 0 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 5000 Exit Sub End If 'Look for a near-by NPC i = NPC_AI_ClosestNPC(NPCIndex, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).AttackRange \ 2, NPCList(NPCIndex).OwnerIndex, 1, 1) 'Check to see if we could find something If i = 0 Then 'There is nothing around to attack, so we want to tell it to move towards the user (we're following a bit behind, don't want to get too close to the action) If Abs(CInt(NPCList(NPCIndex).Pos.X) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.X)) < 4 Then If Abs(CInt(NPCList(NPCIndex).Pos.Y) - CInt(UserList(NPCList(NPCIndex).OwnerIndex).Pos.Y)) < 4 Then NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 500 Exit Sub End If End If 'So let's set the direction toward the player, and try to move tHeading = Server_FindDirection(NPCList(NPCIndex).Pos, UserList(NPCList(NPCIndex).OwnerIndex).Pos) If NPC_MoveChar(NPCIndex, tHeading) = 0 Then 'Couldn't move so look for alternate paths Select Case tHeading Case NORTH t1 = NORTHEAST t2 = NORTHWEST t3 = EAST t4 = WEST Case EAST t1 = NORTHEAST t2 = SOUTHEAST t3 = NORTH t4 = SOUTH Case SOUTH t1 = SOUTHWEST t2 = SOUTHEAST t3 = WEST t4 = EAST Case WEST t1 = SOUTHWEST t2 = NORTHWEST t3 = SOUTH t4 = NORTH Case NORTHEAST t1 = NORTH t2 = EAST t3 = NORTHWEST t4 = SOUTHEAST Case SOUTHEAST t1 = EAST t2 = SOUTH t3 = SOUTHWEST t4 = NORTHEAST Case SOUTHWEST t1 = SOUTH t2 = WEST t3 = SOUTHEAST t4 = NORTHWEST Case NORTHWEST t1 = WEST t2 = NORTH t3 = NORTHEAST t4 = SOUTHWEST End Select 'Try the alternate movements If NPC_MoveChar(NPCIndex, t1) = 0 Then If NPC_MoveChar(NPCIndex, t2) = 0 Then If NPC_MoveChar(NPCIndex, t3) = 0 Then If NPC_MoveChar(NPCIndex, t4) = 0 Then 'We still can't move, so just wait '(we could implement some sort of advanced pathfinding, but let's just say our NPCs failed their IQ tests :P ) NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 1000 End If End If End If End If End If Exit Sub Else 'Something was found, so now we get in position and attack! 'Get the position of the NPC tPos = NPCList(i).Pos 'Run away and get in position if we're too close If Server_RectDistance(NPCList(NPCIndex).Pos.X, NPCList(NPCIndex).Pos.Y, tPos.X, tPos.Y, 3, 3) Then tHeading = Server_FindDirection(NPCList(NPCIndex).Pos, tPos) 'We're going away from it so pick the opposite direction Select Case tHeading Case NORTH: tHeading = SOUTH Case NORTHEAST: tHeading = SOUTHWEST Case EAST: tHeading = WEST Case SOUTHEAST: tHeading = NORTHWEST Case SOUTH: tHeading = NORTH Case SOUTHWEST: tHeading = NORTHEAST Case WEST: tHeading = EAST Case NORTHWEST: tHeading = SOUTHEAST End Select 'Now let's attempt to move away If NPC_MoveChar(NPCIndex, tHeading) = 0 Then 'Can't move there, so try alternate directions Select Case tHeading Case NORTH t1 = NORTHEAST t2 = NORTHWEST t3 = EAST t4 = WEST Case EAST t1 = NORTHEAST t2 = SOUTHEAST t3 = NORTH t4 = SOUTH Case SOUTH t1 = SOUTHWEST t2 = SOUTHEAST t3 = WEST t4 = EAST Case WEST t1 = SOUTHWEST t2 = NORTHWEST t3 = SOUTH t4 = NORTH Case NORTHEAST t1 = NORTH t2 = EAST t3 = NORTHWEST t4 = SOUTHEAST Case SOUTHEAST t1 = EAST t2 = SOUTH t3 = SOUTHWEST t4 = NORTHEAST Case SOUTHWEST t1 = SOUTH t2 = WEST t3 = SOUTHEAST t4 = NORTHWEST Case NORTHWEST t1 = WEST t2 = NORTH t3 = NORTHEAST t4 = SOUTHWEST End Select 'Try the alternate moves If NPC_MoveChar(NPCIndex, t1) = 0 Then If NPC_MoveChar(NPCIndex, t2) = 0 Then If NPC_MoveChar(NPCIndex, t3) = 0 Then If NPC_MoveChar(NPCIndex, t4) = 0 Then 'We still can't move (cornered! ack!) so attack! NPC_AI_AttackNPC NPCIndex, NPCList(NPCIndex).OwnerIndex NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + NPCDelayFight End If End If End If End If End If Exit Sub End If 'We're already in position, so just attack b = NPC_AI_AttackNPC(NPCIndex, NPCList(NPCIndex).OwnerIndex) If b Then 'Make sure the attack was successful before applying the delay NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + NPCDelayFight Exit Sub End If 'Nothing to do, so just wait and try again in a second NPCList(NPCIndex).Counters.ActionDelay = timeGetTime + 1000 End If




