Wednesday, August 20, 2014

Turn based RPG with Unity 2D - Part 4: Enemy/NPC AI (moving)

Part 1: Grid movement
Part 2: Camera control
Part 3: Lighting
Part 4: NPC AI

---


Unity version: 4.5
Language used: C#

Making the NPC


For the purpose of this tutorial, let's copy the player sprite and just change the tint to something else. Like red. This will be our NPC.

Drag the player sprite to your hierarchy and name the new GameObject as First NPC. Or anything you like. Now, select the object and change its tint from the inspector:


Now we need to give the NPC a tag. In the top of the inspector click the box that says Tag and add a new tag called "npc":



Don't forget to select your npc GameObject again and assign that tag to it.

Scripting the NPC


Next create a new interface called NPCinterface (just create a new C# script and remove everything in it, then write the interface):


1
2
3
public interface NPCinterface {
   void ExecuteAI();
}

This is where you can define methods for the NPC that can be used from other scripts. Next create another script and call it NPCController. This will be our abstract class that all NPC inherit from. We can define common methods in this class:


1
2
3
4
5
6
7
8
9
using UnityEngine;
using System.Collections;

abstract public class NPCController : MonoBehaviour, NPCinterface {

 public void ExecuteAI() {
 
 }
}


Notice the keyword abstract and also the NPCinterface reference. This means we implement NPCinterface and must define the contents of ExecuteAI( ) method here.

Select the NPC from your hierarchy window and from the inspector, select Add component / new script. Name this script FirstNPC. Hit Create and Add. Open the script and change it like so:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using UnityEngine;
using System.Collections;

public class FirstNPC : NPCController {

 void Start () {
  pos = transform.position;
 }
 
 void Update () {
  if (moving) {
   transform.position = pos;
   moving = false;
  } 
 }
}


You will get some errors because pos and moving are not defined but we will do that next. Notice that we added NPCController here instead of MonoBehaviour (because MonoBehaviour is implemented in NPCController already).

Go back to your NPCController script and add these properties:


protected Vector2 pos;
protected bool  moving = false;


Protected means these properties will be accessible to the child objects (in this case our FirstNPC script). Next add this piece of code to the ExecuteAI -method:


int direction = Random.Range (1, 5);

if (direction == 1) {
 pos += Vector2.up;
 moving = true;
}
else if (direction == 2) {
 pos += Vector2.right;
 moving = true;
}
else if (direction == 3) {
 pos -= Vector2.up;
 moving = true;
}
else if (direction == 4) {
 pos -= Vector2.right;
 moving = true;
}
else {
 ; // do nothing
}


In this script, first we get a random direction. We have 4 directions so the range starts from 1 and goes up to 5 (the range starts from 0 so the fifth element is actually 4) . We then set the position depending on this random direction. The Update -method in our FirstNPC class will handle the actual moving.

So... how do we call the ExecuteAI? Our player object is the center of attention in this type of game, the turns are pretty much dependent on what the player does. So let's open up our PlayerController -script and do some changes.

OK, our game only has 1 NPC but let's look to the future and determine that there will be more. Add these properties to the PlayerController -script:


private GameObject[] npcObjects;
private NPCController[] npcControllers;


In the Start() -method, add these lines:


// Find all NPCs with the tag "npc":
npcObjects = GameObject.FindGameObjectsWithTag ("npc");

npcControllers = new NPCController[npcObjects.Length];

// Iterate through them and store their NPCController to
// npcControllers array:
for(int i = 0; i < npcObjects.Length; i++) {
 npcControllers[i] = (NPCController) npcObjects[i]
  .GetComponent(typeof(NPCController));
}


The code comments describe what this code snippet does. Next let's do a convenience method called ExecuteNPCAI where we iterate through the npcController array and execute the AI for each NPC:


private void ExecuteNPCAI() {
 foreach(NPCController npc in npcControllers) {
  npc.ExecuteAI();
 }
}


Now call this method from the CheckInput -method in each keystroke check like so:


if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) {
 pos += Vector2.right;
 moving = true;
 ExecuteNPCAI();
}


Now test your game, the NPC should move randomly whenever you move the player character. Of course there's no collision detection yet so the character might go under the player or off the grid.

GIF of the movement:



Stay tuned for more tutorials.

All of the scripts as a whole:

NPCinterface:


1
2
3
public interface NPCinterface {
 void ExecuteAI();
}


NPCController:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using UnityEngine;
using System.Collections;

abstract public class NPCController : MonoBehaviour, NPCinterface {

 protected Vector2 pos;
 protected bool  moving = false;

 public void ExecuteAI() {
  int direction = Random.Range (1, 5);

  if (direction == 1) {
   pos += Vector2.up;
   moving = true;
  }
  else if (direction == 2) {
   pos += Vector2.right;
   moving = true;
  }
  else if (direction == 3) {
   pos -= Vector2.up;
   moving = true;
  }
  else if (direction == 4) {
   pos -= Vector2.right;
   moving = true;
  }
  else {
   ; // do nothing
  }
 }
}


FirstNPC:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using UnityEngine;
using System.Collections;

public class FirstNPC : NPCController {

 void Start () {
  pos = transform.position;
 }
 
 void Update () {
  if (moving) {
   transform.position = pos;
   moving = false;
  } 
 }
}


PlayerController:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour {
 
 private Vector2 pos;
 private GameObject[] npcObjects;
 private NPCController[] npcControllers;
 private bool moving = false;
 
 void Start () {
  // First store our current position when the
  // script is initialized.
  pos = transform.position;
  
  // Find all NPCs with the tag "npc":
  npcObjects = GameObject.FindGameObjectsWithTag ("npc");
  
  npcControllers = new NPCController[npcObjects.Length];
  
  // Iterate through them and store their NPCController to
  // npcControllers array:
  for(int i = 0; i < npcObjects.Length; i++) {
   npcControllers[i] = (NPCController) npcObjects[i]
    .GetComponent(typeof(NPCController));
  }
 }
 
 void Update () {
  
  CheckInput();
  
  if(moving) {
   transform.position = pos;
   moving = false;
  }
  
 }
 
 private void CheckInput() {
  // WASD control
  // We add the direction to our position,
  // this moves the character 1 unit (32 pixels)
  if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) {
   pos += Vector2.right;
   moving = true;
   ExecuteNPCAI();
  }
  
  // For left, we have to substract the direction
  else if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow)) {
   pos -= Vector2.right;
   moving = true;
   ExecuteNPCAI();
  }
  else if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow)) {
   pos += Vector2.up;
   moving = true;
   ExecuteNPCAI();
  }
  
  // Same as for the left, substraction for down
  else if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow)) {
   pos -= Vector2.up;
   moving = true;
   ExecuteNPCAI();
  }
  
  // Then change the transform to the new position with
  // the given speed:
 }
 
 private void ExecuteNPCAI() {
  foreach(NPCController npc in npcControllers) {
   npc.ExecuteAI();
  }
 }
}


---
Part 1: Grid movement
Part 2: Camera control
Part 3: Lighting
Part 4: NPC AI

Code is highlighted with hilite.me

4 comments:

  1. Replies
    1. Hi, thanks for the interest and sorry for late reply. Really swamped with work at the moment. I'm planning on updates and more articles throughout the next year after xmas.

      Delete
    2. Hey man, I just wanted to thank you for the great content and let you know that I'm looking forward to more content in the new year :)

      Delete
    3. Thanks to you for reading! Hope I can cook up more articles this year (working on it).

      Delete