Fandom

Surfpup's tConfig Mod Wiki

How to create a custom AI for NPCs

124pages on
this wiki
Add New Page
Comments78 Share


Under Contruction

Under Construction
This page has been marked as under construction, which means information may change rapidly as it updated, or that the page is awaiting updates.



Difficulty: Difficulty-4
Clock: 30+ minutes


.

RequirementsEdit

To accomplish this, we'll need a working NPC. This tutorial will explain ONLY how to create a working and simple flying AI. Every other explanation (about NPC's stats, sprites and such) is treated in the 'Enemy NPC Tutorial'.

Getting StartedEdit

First of all, we need a working NPC. Every kind of NPC will be fine, but I suggest a hostile and, possibly, flying one.balch blah no one cares

For getting started, we need a .cs file for our NPC. In it, we write the AI method, like this.

public void AI()
{
}

In here, we will write every NPC's behavior. If we want to use our codes along the vanilla AI, we need to add this:

npc.AI(true);

And our code will look like this:

public void AI()
{
npc.AI(true);
}

Anyway, this is just an example, we actually don't need the vanilla code to be executed, so we will not add it to our AI.

Note : Using the vanilla AI can cause a lot of problems while using the npc.ai array (we'll talk about it in the second part of this tutorial), including messed up values, behaviors and random crashes.

Part 1 : Basic MovementEdit

First of all, our NPC will just stand there if we don't code its movements. Before doing that, we need our NPC to actually target a player. For that we can give it a target easily using this:

npc.TargetClosest(true);

Every time this is called, the NPC will target the closest player - meaning that it will set npc.target to a player index. We can use this index to look up the player in the Main.player[] array. Since we're mostly interested in the position of the targeted player, let's set a local variable to make the rest of our code clearer (and to avoid the cost of looking up the player each time):

Vector2 targetPosition = Main.player[npc.target].position; // get a local copy of the targeted player's position

Now for movement! Let's make the NPC go left if the targeted player is on its left:

if (targetPosition.X < npc.position.X) // if the target is to my left
{
  npc.velocity.X = -8; // then go left full throttle!
}

targetPosition.X is actually the targeted player's X position in the world, while npc.position.X is the npc X position in the world. If the player's X coordinate is lower than the NPC's one, it will change the NPC's velocity. The velocity determines how much the X coordinate will decrease every time the AI method is called (pretty much like 30-60 times in a second). In this case, the npc.velocity.X will be set to -8, so it will decrease the X coordinate by 8 units (half a tile) every time, so the NPC will actually move to the left every time the condition is satisfied.

But now, there's a problem. Setting the npc.velocity.X to -8 will just make the npc move into a direction without any kind of acceleration. We can easily fix this, editing the code like this:

if (targetPosition.X < npc.position.X // IF the target is to my left
    && npc.velocity.X > -8) // AND I'm not at max "left" velocity
{
  npc.velocity.X -= 0.22f; // accelerate to the left
}

Pretty much everything works the same, except for the velocity value's change. Now, if the npc.velocity.X is larger than -8, it will subtract 0.22 from the actual NPC's velocity. Simply, in this case, -8 is the max speed and 0.22 the acceleration value. This will progressively increase the NPC's velocity instead of setting the value instantly.

Now that we're done with this, we can do the same for make the npc follow the player to the right, just changing some values:

if (targetPosition.X < npc.position.X // IF the target is to my left
    && npc.velocity.X > -8) // AND I'm not at max "left" velocity
{
  npc.velocity.X -= 0.22f; // accelerate to the left
}

if (targetPosition.X > npc.position.X // IF the target is to my right
    && npc.velocity.X < 8) // AND I'm not at max "right" velocity
{
  npc.velocity.X += 0.22f; // accelerate to the right
}

As we can see, in the second case, it will check if the player's position is greater than the npc's one and if the npc velocity is smaller than 8, it will increase the X velocity.

Now that we're done with this, the NPC will actually move to the left and right following the player, but it can't actually fly higher or lower, so now, we'll work on that.

The idea is the same, but actually, Rathalos doesn't directly charge into the player, but it flies above him at a certain distance. First of all, we do the same code, but using Y instead of X.

if (targetPosition.Y < npc.position.Y + 300 // IF the target is higher than "300 below my height"
    && npc.velocity.Y > -4) // AND I'm not at max "up" velocity
{
  npc.velocity.Y -= 0.7f; // accelerate up
}

if (targetPosition.Y > npc.position.Y + 300 // IF the target is lower than "300 below my height"
    && npc.velocity.Y < 4) // AND I'm not at max "down" velocity
{
  npc.velocity.Y += 0.7f; // accelerate down
}

As you can see, the concept is the same as the X movement, but this time, we have npc.position.Y+300, this means that the NPC will go, instead to the player's Y position, a bit over his head.

This is it for the Y movement, but there's still a small problem. Testing it, you'll surely notice that its vertical movement is quite messy. It will never stop at a certain height, instead, it will continuously move up and down in a wavy-like pattern. We can fix this easily enough.

if (targetPosition.Y < npc.position.Y + 300) // IF the target is higher than "300 below my height"
{
  if (npc.velocity.Y < 0 // IF I'm already moving up 
      && npc.velocity.Y > -4) // AND I'm not at max "up" velocity
    npc.velocity.Y -= 0.7f; // THEN accelerate up
  else // otherwise, I'm not moving up already
    npc.velocity.Y -= 0.8f; // THEN accelerate up (faster)
}

if (targetPosition.Y > npc.position.Y + 300) // IF the target is lower than "300 below my height"
{
  if (npc.velocity.Y > 0 // IF I'm already moving down 
      && npc.velocity.Y < 4) // AND I'm not at max "down" velocity
    npc.velocity.Y += 0.7f; // THEN accelerate down
  else // otherwise, I'm not moving down already
    npc.velocity.Y += 0.8f; // THEN accelerate down (faster)
}

As you can notice, I've added two new conditions. These work pretty simply. If the NPC's target is under him, and his Y velocity is higher than 0 (it's moving down), the velocity increase is the same. If the NPC's target is under him but his Y velocity is smaller than 0 (it's moving up), the velocity increases more than the normal amount. Doing this, the NPC will fly on a straight line, with pretty much less Y velocity alteration, so he won't "move in a wavy pattern" anymore.

So, this is pretty much about everything about basic NPC movements, so, let's put everything together.

public void AI()
{
  npc.TargetClosest(true);
  Vector2 targetPosition = Main.player[npc.target].position; // get a local copy of the targeted player's position
  if (targetPosition.X < npc.position.X // IF the target is to my left
      && npc.velocity.X > -8) // AND I'm not at max "left" velocity
  {
    npc.velocity.X -= 0.22f; // accelerate to the left
  }
  if (targetPosition.X > npc.position.X // IF the target is to my right
      && npc.velocity.X < 8) // AND I'm not at max "right" velocity
  {
    npc.velocity.X += 0.22f; // accelerate to the right
  }
   
  if (targetPosition.Y < npc.position.Y + 300) // IF the target is higher than "300 below my height"
  {
    if (npc.velocity.Y < 0 // IF I'm already moving up 
        && npc.velocity.Y > -4) // AND I'm not at max "up" velocity
      npc.velocity.Y -= 0.7f; // THEN accelerate up
    else // otherwise, I'm not moving up already
      npc.velocity.Y -= 0.8f; // THEN accelerate up (faster)
  }

  if (targetPosition.Y > npc.position.Y + 300) // IF the target is lower than "300 below my height"
  {
    if (npc.velocity.Y > 0 // IF I'm already moving down 
        && npc.velocity.Y < 4) // AND I'm not at max "down" velocity
      npc.velocity.Y += 0.7f; // THEN accelerate down
    else // otherwise, I'm not moving down already
      npc.velocity.Y += 0.8f; // THEN accelerate down (faster)
  } 

  npc.position += npc.velocity; // update our position using the velocity
}

Congrats! You just did your first NPC's AI! This is all about NPC's basic movements. You can use this knowledge to actually create basic NPCs. The next part can be tricky to understand, so, it can probably require some practice first.

Part 2 : Simple firing codeEdit

Now, we start with something a bit more messy. We have our boss, the code works and he moves as he must, but he just flies above our character, doing nothing at all. How we fix this? By just making it fire at us!

First of all, we are perfectionists, guys, so we want our AI to sync perfectly. We need to introduce "npc.ai", a useful float array that allows us to sync the values we want through clients (if the var becomes "1" in a client, it becomes like that for every player). There's a limit, though. The array can't go over the 4 vars, so we can sync only a maximum of 4 vars, but for simple AIs, they're pretty much enough. There's actually a way to break this limit, but I'm actually not aware of how to do it. Here, we're talking about my Rathalos AI, so the final result must be its code. When I understand how to break that limit, I'll add an extra note at the end on how to do it.

So, here we start. We need to decide "when" it will actually fire. My choice, in this case, is to make it spawn every time with a short delay between every shot. To do this, we need a var that will work like a timer, increasing it at every AI call and then reset itself when reaches a certain number, executing what we actually need to execute (in this case, fire a projectiles to the player). Here's a simple example.

npc.ai[0]++; //or "npc.ai[0] += 1;", works the same way

if (npc.ai[0] >= 90)
{
// Executed code
npc.ai[0] = 0;
}

As you can see, it's pretty simple. Increases the npc.ai[0] by one every time (I used 0 in this case, remember you can only have 4 vars like these, this pretty much means that npc.ai[3] is the limit), checks if npc.ai[0] is higher or equals to 90, runs the code we need and resets the timer to 0, and so everything repeats itself. We can use this to create a basic projectile code that travels toward the NPC's direction.

Now, for the projectile's code, here's what I actually used.

​float Speed = 12f;
Vector2 vector8 = new Vector2(npc.position.X + (npc.width / 2), npc.position.Y + (npc.height / 2));
int damage = 30;
int type = Config.projectileID["Flame Shot"];
Main.PlaySound(2, (int) npc.position.X, (int) npc.position.Y, 17);
float rotation = (float) Math.Atan2(vector8.Y-(Main.player[npc.target].position.Y+(Main.player[npc.target].height * 0.5f)), vector8.X-(Main.player[npc.target].position.X+(Main.player[npc.target].width * 0.5f)));
int num54 = Projectile.NewProjectile(vector8.X, vector8.Y,(float)((Math.Cos(rotation) * Speed)*-1),(float)((Math.Sin(rotation) * Speed)*-1), type, damage, 0f, 0);

This can be pretty tricky to understand.

It creates a float value, Speed, that will actually be the projectile's velocity. Takes the NPC's position (npc.width and height are just added for an offset purpose, adding them you will be sure that the projectile will be created on the NPC's center), takes the projectile's type id (I used a projectile called Flame Shot, feel free to use whatever projectile you want. Just make sure to create it into your modpack's projectiles folder), plays a sound on the NPC's position (You can change the "17" with the sound's id you want to play every time the projectile is shot. You can have a look at the List of Sounds on this wiki. If you don't want it to play a sound, just delete the entire Main.PlaySound function), does some calculations to make the projectile travel into the player's direction and creates the projectile itself. (Atan2, Sin and Cos, you can look at any online guide to know how they actually work, it's too hard to explain for me).

Now, let's add it to our timer.

npc.ai[0]++; //or "npc.ai[0] += 1;", works the same way

if (npc.ai[0] >= 90)
{
​float Speed = 12f;
Vector2 vector8 = new Vector2(npc.position.X + (npc.width / 2), npc.position.Y + (npc.height / 2));
int damage = 30;
int type = Config.projectileID["Flame Shot"];
Main.PlaySound(2, (int) npc.position.X, (int) npc.position.Y, 17);
float rotation = (float) Math.Atan2(vector8.Y-(Main.player[npc.target].position.Y+(Main.player[npc.target].height * 0.5f)), vector8.X-(Main.player[npc.target].position.X+(Main.player[npc.target].width * 0.5f)));
int num54 = Projectile.NewProjectile(vector8.X, vector8.Y,(float)((Math.Cos(rotation) * Speed)*-1),(float)((Math.Sin(rotation) * Speed)*-1), type, damage, 0f, 0);
npc.ai[0] = 0;
}

And now, let's add it into our entire AI code.

public void AI()
{
npc.TargetClosest(true);

if (Main.player[npc.target].position.X < npc.position.X)
{
if (npc.velocity.X > -8) npc.velocity.X -= 0.22f;
}

if (Main.player[npc.target].position.X > npc.position.X)
{
if (npc.velocity.X < 8) npc.velocity.X += 0.22f;
}

if (Main.player[npc.target].position.Y < npc.position.Y+300)
{
if (npc.velocity.Y < 0) // <--
{
if (npc.velocity.Y > -4) npc.velocity.Y -= 0.7f;
}
else npc.velocity.Y -= 0.8f;
}

if (Main.player[npc.target].position.Y > npc.position.Y+300)
{
if (npc.velocity.Y > 0) // <--
{
if (npc.velocity.Y < 4) npc.velocity.Y += 0.7f;
}
else npc.velocity.Y += 0.8f;
}

npc.ai[0]++;

if (npc.ai[0] >= 90)
{
​float Speed = 12f;
Vector2 vector8 = new Vector2(npc.position.X + (npc.width / 2), npc.position.Y + (npc.height / 2));
int damage = 30;
int type = Config.projectileID["Flame Shot"];
Main.PlaySound(2, (int) npc.position.X, (int) npc.position.Y, 17);
float rotation = (float) Math.Atan2(vector8.Y-(Main.player[npc.target].position.Y+(Main.player[npc.target].height * 0.5f)), vector8.X-(Main.player[npc.target].position.X+(Main.player[npc.target].width * 0.5f)));
int num54 = Projectile.NewProjectile(vector8.X, vector8.Y,(float)((Math.Cos(rotation) * Speed)*-1),(float)((Math.Sin(rotation) * Speed)*-1), type, damage, 0f, 0);
npc.ai[0] = 0;
}
}

Done! Now our AI will fire a simple projectile at us while he flies above our head! This is pretty much everything about the NPC firing code. This can be pretty useful, once you understand how it works, to create some new and unique NPC behaviors. Just try it by yourself, change some values, and have fun playing with it!

Part 3 : Creating AI PhasesEdit

This is the last part of this tutorial. This will talk about how to create different AIs and cycle through them.

This part shows a simple and useful method to use the npc.ai array for cycle through different AIs, that can come pretty handy for complex AIs or Bosses. There are two ways to accomplish this. While the first one is more well formatted and easy to handle, the second one is smaller and uses only one npc.ai, while the first one uses two of them. Let's start about the first one.

AI Phase change using other variablesEdit

First of all, we need to define a var that will be our "Phase Definer", or better, a var that we'll use for define the current NPC's phase. In this case, we'll use AI[1].

public void AI()
{
npc.TargetClosest(true);

if (npc.ai[1] == 0) // First AI
{
if (Main.player[npc.target].position.X < npc.position.X)
{
if (npc.velocity.X > -8) npc.velocity.X -= 0.22f;
}

if (Main.player[npc.target].position.X > npc.position.X)
{
if (npc.velocity.X < 8) npc.velocity.X += 0.22f;
}

if (Main.player[npc.target].position.Y < npc.position.Y+300)
{
if (npc.velocity.Y < 0)
{
if (npc.velocity.Y > -4) npc.velocity.Y -= 0.7f;
}
else npc.velocity.Y -= 0.8f;
}

if (Main.player[npc.target].position.Y > npc.position.Y+300)
{
if (npc.velocity.Y > 0)
{
if (npc.velocity.Y < 4) npc.velocity.Y += 0.7f;
}
else npc.velocity.Y += 0.8f;
}

npc.ai[0]++;

if (npc.ai[0] >= 90)
{
float Speed = 12f;
Vector2 vector8 = new Vector2(npc.position.X + (npc.width / 2), npc.position.Y + (npc.height / 2));
int damage = 30;
int type = Config.projectileID["Flame Shot"];
Main.PlaySound(2, (int) npc.position.X, (int) npc.position.Y, 17);
float rotation = (float) Math.Atan2(vector8.Y-(Main.player[npc.target].position.Y+(Main.player[npc.target].height * 0.5f)), vector8.X-(Main.player[npc.target].position.X+(Main.player[npc.target].width * 0.5f)));
int num54 = Projectile.NewProjectile(vector8.X, vector8.Y,(float)((Math.Cos(rotation) * Speed)*-1),(float)((Math.Sin(rotation) * Speed)*-1), type, damage, 0f, 0);
npc.ai[0] = 0;
}
}

if (npc.ai[1] == 1) // Second AI
{
// NPC AI HERE
}
}

As you can see, now there is a condition check for the first AI's execution, so that AI will run only if npc.ai[1] is equal to 0. If you don't change them, the npc.ai array will be 0, so if that var is not changed, the first AI will be executed. Also, I've added a second condition at the end, that will execute second AI if the npc.ai[1] is equal to 1.

Now, we need to decide when the NPC phase will have to change. The positive point of this method, is that you can switch AI every time you want. My choice is generally a timer. We can do something like this.

npc.ai[2] += 1;
if (npc.ai[2] >= 600)
{
if (npc.npc.ai[1] == 0) npc.ai[1] = 1;
else npc.ai[1] = 0;
}

This code will make our AI phase switch through 0 to 1 or 1 to 0 every time the timer var reaches 600 (that is supposed to be something like 10 seconds, as apparently 60 = 1 second, but I need a confirm for this). Now, let's add it to the entire code.

public void AI()
{
npc.TargetClosest(true);

if (npc.ai[1] == 0) // First AI
{
if (Main.player[npc.target].position.X < npc.position.X)
{
if (npc.velocity.X > -8) npc.velocity.X -= 0.22f;
}

if (Main.player[npc.target].position.X > npc.position.X)
{
if (npc.velocity.X < 8) npc.velocity.X += 0.22f;
}

if (Main.player[npc.target].position.Y < npc.position.Y+300)
{
if (npc.velocity.Y < 0)
{
if (npc.velocity.Y > -4) npc.velocity.Y -= 0.7f;
}
else npc.velocity.Y -= 0.8f;
}

if (Main.player[npc.target].position.Y > npc.position.Y+300)
{
if (npc.velocity.Y > 0)
{
if (npc.velocity.Y < 4) npc.velocity.Y += 0.7f;
}
else npc.velocity.Y += 0.8f;
}

npc.ai[0]++;

if (npc.ai[0] >= 90)
{
float Speed = 12f;
Vector2 vector8 = new Vector2(npc.position.X + (npc.width / 2), npc.position.Y + (npc.height / 2));
int damage = 30;
int type = Config.projectileID["Flame Shot"];
Main.PlaySound(2, (int) npc.position.X, (int) npc.position.Y, 17);
float rotation = (float) Math.Atan2(vector8.Y-(Main.player[npc.target].position.Y+(Main.player[npc.target].height * 0.5f)), vector8.X-(Main.player[npc.target].position.X+(Main.player[npc.target].width * 0.5f)));
int num54 = Projectile.NewProjectile(vector8.X, vector8.Y,(float)((Math.Cos(rotation) * Speed)*-1),(float)((Math.Sin(rotation) * Speed)*-1), type, damage, 0f, 0);
npc.ai[0] = 0;
}
}

if (npc.ai[1] == 1) // Second AI
{
// NPC AI HERE
}

npc.ai[2] += 1;
if (npc.ai[2] >= 600)
{
if (npc.ai[1] == 0) npc.ai[1] = 1;
else npc.ai[1] = 0;
}
}


(Work In Progress)


Editor's Notes:

Bullseye55: How to flip the sprite when the NPC's direction changes:


Use:: npc.spriteDirection = 1 ///// npc.spriteDirection = -1.


A positive 1 means the sprite will turn right, a negative 1 means it will turn left. Here is a picture showing a Bee NPC what I made that uses this: Bee Swarm

Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.