I’m trying to convert my enemy AI to an FSM, and am having trouble wrapping my head around where in my script to put the actual decision logic. I built this basic template:
enum AIState{
wandering,
chasing,
attacking
}
var state : AIState;
function MakeDecision(){
switch(state){
case AIState.wandering:
Wander();
break;
case AIState.chasing:
Chase();
break;
case AIState.attacking:
Attack();
break;
}
}
function Wander(){
//wander
}
function Chase(){
//chase
}
function Attack(){
//attack
}
But I don’t know where to put my actual decision-making logic. Should I just call MakeDecision once in Start, and do a check inside each of the coroutines for my states to see if the condition that I required to be satisfied for this state is still satisfied, and if not, return?
Is there a way I could separate the decision making logic from the coroutines for each state, so I don’t have to pepper every function with checks to make sure whatever condition got me there is still satisfied?
It really depends on where you want to decisions to be made. In my case I made the StateMachine a class, and the states their own classes with their own logic.The StateMachine just holds data and points to the current state. Note that these are not MonoBehaviors in my case, they subclass from System.Object. I then use delegates to inform any observers(my game entity logic entity, the animation driving system, or the network code) when the state has changed.
Let me know if you’d like to see the code, it’s in C# but might be of help to you.
You got me beat, I just made a big mud puddle and threw them in all willy nilly like. Actually it makes a decision on what to do next givin one of two events pass… Either it accomplishes it’s objective, or it time outs. Furthermore, I added a Aggression variable and a Concentration variable. which basically ask, do you have enough concentration to continue doing what you are doing, or should we go find something else for you to do.
I have 3 basic states, Race, Kill and Explore. I make decisions based on what I am currently doing and what I can see vs what’s closest to me.
I am making a race game, so I use waypoints for my track. I developed several way point paths for my racers to follow. In my logic, I pick a random path, if I can see any of the points, I chose the closest one and start racing on it. If I am feeling aggressive, I will find the nearest vehicle (other than myself) and go try to kill it.
most of the time racing and killing is all they do, but they do wind up in a predicament that they cannot find a path, nor can they find an enemy, so I set them to wander around.
I guess I could setup a nice class system and have it deal with it that way… lol
Here’s an update, on how I imagine implementing what I’m trying to achieve.
This code works, but causes a massive FPS drop. I’m assuming its because of the while…yield coroutines, but I’m not exactly sure how else I could pull this off:
var target : Transform;
var chaseDistance : float = 6.0;
var attackDistance : float = 3.0;
var state : aiState;
private var lastState : aiState;
enum aiState{
wandering,
chasing,
attacking
}
function Start(){
MakeDecision();
}
function Update(){
var distanceToTarget = Vector3.Distance(target.position, transform.position);
if(distanceToTarget <= attackDistance){
state = aiState.attacking;
}
else if(distanceToTarget <= chaseDistance){
state = aiState.chasing;
}
else{
state = aiState.wandering;
}
if(lastState != state){
MakeDecision();
}
}
function MakeDecision(){
switch(state){
case aiState.wandering:
Wander();
break;
case aiState.chasing:
Chase();
break;
case aiState.attacking:
Attack();
break;
}
lastState = state;
}
function Wander(){
while(state == aiState.wandering){
print("I am wandering");
yield;
}
}
function Chase(){
while(state == aiState.chasing){
print("I am chasing");
yield;
}
}
function Attack(){
while(state == aiState.attacking){
print("I am attacking");
yield;
}
}
OK, I took your code and ran with it a bit. Added a couple new states, Wait and Evade (they will make some sense)
I used the concept of if someone is closer than certain distances from your code, but put them in the right places. I also added the ability to find a wandering spot. (it will have to be updated to be able to see the spot, so you aren’t walking through walls and such.)
I dumped the lastState as it really didnt have much of a use. I am using the wander position as the evade position, so when the creature is out run then it will go back to the last wandering position.
I added a OnTrigger set. What this object will need is a view radius. So it can’t “see” anything over a certain distance, and the OnTrigger should keep track of if the creature is still in the area, and what to do. If it can see the enemy, it can chase the enemy. It is up to you to figure out how to chase him, and what paths to follow.
You will need to complete all the movement, attacking, checking and other stuff that you would have still had to do.
Hope it helps.
var chaseDistance : float = 6.0;
var attackDistance : float = 3.0;
var evadeDistance : float = 30.0;
var state : aiState;
private var lastLocation : Vector3;
private var target : Transform;
private var targetEnemy : Transform;
private var targetingEnemy=false;
private var waitTil : float;
enum aiState{
waiting,
wandering,
chasing,
attacking,
evading
}
function Start(){
// create a visible target
target = GameObject.CreatePrimitive(PrimitiveType.Cube).transform;
DestroyImmediate(target.collider);
target.localScale.y = 200.0;
target.renderer.enabled = false; // comment out for debug.
MakeChoice();
}
function Update(){
switch(state){
case aiState.waiting:
Wait();
break;
case aiState.wandering:
Wander();
break;
case aiState.chasing:
Chase();
break;
case aiState.attacking:
Attack();
break;
case aiState.evading:
Evade();
break;
}
}
function MakeChoice(){
// wait or wander
var rndValue=Random.value;
if(rndValue > 0.5){
newWander();
} else {
state = aiState.waiting;
waitTil = Time.time + 2.0 + Random.value * 5.0;
}
}
function newWander(){
var newPosition : Vector2 = Random.insideUnitCircle * 20;
var testPosition : Vector3 = Vector3(newPosition.x, transform.position.y + 5.0, newPosition.y);
var hit : RaycastHit;
if(!Physics.Linecast(testPosition, testPosition - Vector3.up * 50.0)){
newWander();
}else{
target.position = hit.point;
lastLocation = target.position;
state = aiState.wandering;
}
}
function Wait(){
if(Time.time >= waitTil)
MakeChoice();
print("I am waiting");
}
function Wander(){
if(distanceToTarget() < 5.0){
MakeChoice();
}
print("I am wandering");
}
function Chase(){
target.position = targetEnemy.position;
if(distanceToTarget() <= attackDistance){
state = aiState.attacking;
return;
}
if(distanceToTarget() > evadeDistance){
state = aiState.evading;
target.position = lastLocation;
return;
}
print("I am chasing");
}
function Attack(){
print("I am attacking");
}
function Evade(){
// moving to lastLocation (cannot be attacked)
print("I am evading");
}
function isVisible(object : Transform){
return (Physics.Linecast (transform.position, target.position));
}
function isEnemy(object : Transform){
// fill code in here to identify if the target should be attacked
return true;
}
function distanceToTarget(){
return Vector3.Distance(target.position, transform.position);
}
// if someone walks into the trigger check if you can see it, if you can chase it.
function OnTriggerEnter(other : Collider) {
if(targetingEnemy)
return;
if(state == aiState.evading)
return;
if(!isEnemy(other.transform))
return;
if(isVisible(other.transform)){
state = aiState.chasing;
targetEnemy = other.transform;
targetingEnemy=true;
}
}
function OnTriggerStay(other : Collider) {
OnTriggerEnter(other);
}
Your code looks solid, but that’s essentially what I had before I decided to try and switch to a coroutine-based FSM.
My goal was to try and do the entire AI without doing any of the actual action functions (coroutines) in Update, so I can use WaitForSeconds yields instead of doing Time.time based timers and whatnot.
For instance, if you look at the robot AI in the FPS tutorial assets from Unity, I am basically trying to shoehorn that into a cleaner look state-switch.
Yep, that is what I figured that was what you were after. I wouldn’t do the wait time though. That would be why I put the Wait state in there. The thing is that the OnTriggerEnter should be able to interrupt the waiting process… if you use a yield the script, it would stop some portion of the update.
Here’s what I came up with fiddling around this morning, works pretty good. I have 30 cubes on screen with rigidbodies and this script and i get around 500 fps… good enough for me
var target : Transform;
//wandering vars
public var wanderSpeed = 2.0;
public var wanderRotSpeed = 5.0;
public var wanderRadius = 10.0;
public var wanderRayDistance = 5.0;
public var wanderPauseMin = 2.0;
public var wanderPauseMax = 6.0;
private var basePosition : Vector3;
private var currentDestination : Vector3;
//chase vars
var chaseDistance : float = 10.0;
var chaseSpeed : float = 3.0;
var chaseRotSpeed : float = 5.0;
//attack vars
var attackDistance : float = 3.0;
var attackRate : float = 0.25;
//state setup
enum aiState{ wandering, chasing, attacking }
var state : aiState;
InvokeRepeating("StateLogic", 0.0, 0.01);
function Start(){
if(target == null)
target = GameObject.FindWithTag("Player").transform;
ChooseNextDestination();
yield StateMachine();
}
function StateLogic(){
var distanceToTarget = (target.position - transform.position).sqrMagnitude;
if(distanceToTarget <= attackDistance*attackDistance)
state = aiState.attacking;
else if(distanceToTarget <= chaseDistance*chaseDistance)
state = aiState.chasing;
else
state = aiState.wandering;
}
function StateMachine(){
while(true){
switch(state){
case aiState.wandering:
yield Wander();
break;
case aiState.chasing:
Chase();
break;
case aiState.attacking:
yield Attack();
break;
}
yield;
}
}
function Wander(){
RotateToward(currentDestination, wanderRotSpeed);
MoveForward(wanderSpeed);
//BroadcastMessage("PlayAnimation", "walk");
var destPosZeroY = currentDestination;
var currentPosZeroY = transform.position;
destPosZeroY.y = 0;
currentPosZeroY.y = 0;
if((destPosZeroY - currentPosZeroY).magnitude < 1.0){
yield WaitForSeconds(Random.Range(wanderPauseMin, wanderPauseMax));
ChooseNextDestination();
}
}
function ChooseNextDestination(){
var randOffset : Vector2 = Random.insideUnitCircle * wanderRadius;
currentDestination = basePosition + new Vector3(randOffset.x, transform.position.y, randOffset.y);
Debug.DrawLine(transform.position, currentDestination, Color.white);
}
function Chase(){
RotateToward(target.position, chaseRotSpeed);
MoveForward(chaseSpeed);
}
function Attack(){
target.GetComponent(PlayerStatus).TakeDamage(20.0);
yield WaitForSeconds(attackRate);
}
function RotateToward(targetPos : Vector3, rotSpeed : float){
targetPos.y = transform.position.y;
var rotation = Quaternion.LookRotation(targetPos - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * rotSpeed);
}
function MoveForward(moveSpeed : float){
transform.Translate(Vector3.forward*Time.deltaTime*moveSpeed);
}
I made a FSM project last yeart too, without using switch, the states acre classes with a method to reason about the NPC current state and to Act on the environment. If you want to check another idea (it’s in C#)
Dart, I’ve seen your project before, it looks great. My only problem is I don’t really use C#. I read on your forum thread about it that someone mentioned he was going to start a js version, but it seems he never finished it.