Hi, Are you interested in building an AI to your game? This oldschool way of making game-AI is still useable for many types of games. I also attach a manual that goes a bit more indepth if you dont understand it. I hope to see some interesting AI-behaviour comming out from this base-script! Cheers! /Gustav Code (csharp): using UnityEngine; using System.Collections; public abstract class EnemyBehaviour : MonoBehaviour { /*If you need guidance see the manual.txt, and if that didn't help you can email me at gustavbok@gmail.com!*/ /*Here you can add and remove states for the enemy, see the manual.txt for guidance!*/ public enum EnemyState { initializing, idle, sawPlayer, chasing, attacking, fleeing } /*This is the currentState of the Enemy, this is what you'll change in the child-Class*/ public EnemyState currentState; public GameObject playerReference; void Start () { currentState = EnemyState.initializing; } /*In here there is a switch-statement which handles which method that is going * to be updating, this is chosen by the currentState of the enemy. It is in here that you will add your own EnemyState.yourState-case and call for your own method below*/ public virtual void Update () { switch (currentState) { case EnemyState.initializing: /*filling in the player reference for easier access*/ playerReference = GameObject.Find ("Player"); currentState = EnemyState.idle; break; case EnemyState.idle: Idle(); break; case EnemyState.sawPlayer: SawPlayer(); break; case EnemyState.chasing: Chasing(); break; case EnemyState.attacking: Attacking(); break; case EnemyState.fleeing: Fleeing(); break; default: break; } } /*When you add your own methods here they need to be virtual, this is so you can in override them in your own class*/ public virtual void Idle() { } public virtual void SawPlayer() { } public virtual void Chasing() { } public virtual void Attacking() { } public virtual void Fleeing() { } } Manual(Not completed):
Here is an example class that inherits from the base script. Code (csharp): using UnityEngine; using System.Collections; public class Enemy1 : EnemyBehaviour { // Use this for initialization void Start () { } /*Now you will need to override the methods you want to use for the enemy, these can be * found in the bottom of the EnemyBehaviour-class*/ public override void Idle() { /*Add your idle-behaviour here*/ /*could be:*/ /*Update Patrolling the area*/ /* if in range of the player*/ /*Raycast toward player*/ /*if raycast hit - change currentState to sawPlayer*/ //currentState = EnemyState.sawPlayer; } public override void SawPlayer() { /*Add your SawPlayer-behaviour here*/ /*could be:*/ /* face the player */ /* play surprise animation */ /* when animation is done - change currentState to chasing*/ //currentState = EnemyState.chasing; } public override void Chasing() { /*Add your Chasing-behaviour here*/ /*could be:*/ /* if hp < 30 - change currentState to Flee*/ //currentState = EnemyState.fleeing; /* if not close enough to attack the player - move towards the player. It is here you choose * if the enemy is either melee or ranged */ /*else change currentState to attack*/ //currentState = EnemyState.attacking; } public override void Attacking() { /*Add your Attacking-behaviour here*/ /*could be:*/ /*if still close enough, do attack*/ /*else change to chase*/ } public override void Fleeing() { /*Add your Fleeing-behaviour here*/ /*could be:*/ /* if EnemyHp < 30*/ /*raycast to player and run the other way, simplest to implement*/ /*else*/ /*change state to chase*/ } } And here is a filled one for an test enemy, ORC: Code (csharp): using UnityEngine; using System.Collections; public class EnemyOrc : EnemyBehaviour { AudioSource attackSoundEffect; AudioSource lookSurprisedSoundEffect; bool hasSpottedPlayer; float reactionTime; GameObject weaponHitBox; Vector3 weaponStartPos; Vector3 weaponEndPos; float weaponSwingTimer; float weaponSpeed; float attackTimer; bool weaponsOut; float healthRegeneration; float healthRegenTimer; int hp; // Use this for initialization void Start () { hp = 10; reactionTime = 50; healthRegeneration = 2; healthRegenTimer = 0; weaponSpeed = 3; weaponsOut = false; attackTimer = 100; /*have not yet spotted player*/ hasSpottedPlayer = false; weaponSwingTimer = 0; } public override void Idle() { if(Vector3.Distance(transform.position,playerReference.transform.position) < 5) { currentState = EnemyState.sawPlayer; } } public override void SawPlayer() { if(!hasSpottedPlayer) { /*visualisation of enemy animation*/ transform.localScale = new Vector3(transform.localScale.x*2,transform.localScale.x*2,transform.localScale.x*2); hasSpottedPlayer = true; } if(reactionTime < 0) { reactionTime = 50; currentState = EnemyState.chasing; } { reactionTime -= 1; } } public override void Chasing() { transform.LookAt(playerReference.transform); if(Vector3.Distance(transform.position,playerReference.transform.position) > 2) { Vector3 moveTowards = playerReference.transform.position -transform.position; moveTowards.Normalize(); float dampening = 10; transform.position = new Vector3(transform.position.x+moveTowards.x/dampening, transform.position.y, transform.position.z+moveTowards.z/dampening); } else { currentState = EnemyState.attacking; } } public override void Attacking() { if(weaponHitBox != null) { weaponSwingTimer += Time.deltaTime* weaponSpeed; weaponHitBox.transform.position = Vector3.Slerp(weaponStartPos, weaponEndPos, weaponSwingTimer); } if(weaponsOut) { GameObject.Destroy(weaponHitBox); weaponHitBox = GameObject.CreatePrimitive(PrimitiveType.Cylinder); weaponHitBox.name = "weaponHitBox"; weaponHitBox.AddComponent<CapsuleCollider>(); weaponHitBox.transform.position = transform.position; weaponStartPos = weaponHitBox.transform.position; Vector3 towardsPlayer = playerReference.transform.position - transform.position; towardsPlayer.Normalize(); towardsPlayer *= 2; weaponEndPos = new Vector3( transform.position.x + towardsPlayer.x, transform.position.y + towardsPlayer.y, transform.position.z + towardsPlayer.z); weaponHitBox.transform.parent = transform; weaponHitBox.transform.LookAt(playerReference.transform); weaponHitBox.transform.rotation = Quaternion.Euler( new Vector3(weaponHitBox.transform.rotation.eulerAngles.x+90, weaponHitBox.transform.rotation.eulerAngles.y, weaponHitBox.transform.rotation.eulerAngles.z)); weaponsOut = true; } if(attackTimer < 0) { GameObject.Destroy(weaponHitBox); weaponSwingTimer = 0; currentState = EnemyState.chasing; weaponsOut = false; } else attackTimer -= 1; if(hp < 5) { GameObject.Destroy(weaponHitBox); weaponSwingTimer = 0; currentState = EnemyState.fleeing; } } public override void Fleeing() { Vector3 awayFromPlayer = transform.position - playerReference.transform.position; awayFromPlayer.Normalize(); transform.LookAt(transform.position + awayFromPlayer); if(hp < 5) { float dampening = 30; transform.position = new Vector3(transform.position.x+awayFromPlayer.x/dampening, transform.position.y, transform.position.z+awayFromPlayer.z/dampening); healthRegenTimer += Time.deltaTime; if(healthRegenTimer >= healthRegeneration) { Debug.Log("Enemy new hp: "+ hp); hp++; healthRegenTimer = 0; } } else { currentState = EnemyState.chasing; } } }
This method is actually the base of an OOP. Each of those states simply calls things. You are still going to need a controller to control the OOP stuff. This is just a simple framework that helps do that. Good job, good examples. Perhaps a further addition would be sub classes. So An enemy orc could be a Wizard, or a warrior. Then you could have like a Warrior object or Wizard or archer or whatever with the same stats but simple AI already embedded. Further emphasize could be that an orc, looks like an orc, but then extends both the warrior and orc classes. The warrior class could be designed to only wear certain types of armor and use certain weapons, and the orc always uses specific models. So the combination could easily produce something along the lines of an MMO. (Though I will never suggest anyone to attempt such a thing without 1000 people working on it.)
Yes and no, I guess. Wrapping everything around distinctive object gives you some advantages... Like easy tracking of past and future states, constructor and destructor, polymorphism and so on. On top, it makes for a very easy management of your behaviors, since each state is a different file. Here's the machine we use; Code (csharp): using UnityEngine; using System; using System.Collections; /// <summary> /// Basic state machine. /// Generally used for object that has multiple related or unrelated states. /// </summary> public class StateMachine { private object parent; /// <summary> /// Owner of this StateMachine. /// </summary> public object Parent { get { return parent; } } private State previousState; /// <summary> /// The older destroyed state that was playing previous to the current state. /// </summary> public State PreviousState { get { return previousState; } } private State currentState; /// <summary> /// Currently updated state. /// </summary> public State CurrentState { get { return currentState; } } private State nextState; /// <summary> /// Next State. /// This will be this machine current state next frame. /// </summary> public State NextState { get { return nextState; } } private bool forced = false; public StateMachine(StateMachine parent) { this.parent = parent; } public StateMachine(MonoBehaviour parent) { this.parent = parent; } public void Update() { if (nextState != null) { if (currentState != null) { previousState = currentState; previousState.Destructor(); } currentState = nextState; currentState.Constructor(); nextState = null; forced = false; } if (currentState != null) currentState.Update(); } /// <summary> /// Switch the state on the next frame. /// If null, will remove a previous request. /// </summary> public void SwitchState(State nextState) { if (forced) return; if (nextState == null || (nextState != null this.nextState != null nextState.Priority < this.nextState.Priority)) return; this.nextState = nextState; } /// <summary> /// Forces the state to change, bypassing priorities. /// Any other following request will be ignored. Use with care. /// </summary> public void ForceSwitchState(State nextState) { if (forced) return; this.nextState = nextState; forced = true; } /// <summary> /// Return true is the current or next state is of that type. /// </summary> public bool IsInState(Type type) { return currentState.GetType() == type || (nextState != null nextState.GetType() == type); } } And the base state class; Code (csharp): using UnityEngine; using System.Collections; /// <summary> /// Derive each state of your machine from this. /// </summary> public abstract class State { private StateMachine machine; /// <summary> /// Owner of this state, even if this state is not currently referenced by the machine. /// </summary> public StateMachine Machine { get { return machine; } } protected int priority = 1; /// <summary> /// When two states try to gain control of a machine, the highest priority win. /// </summary> public int Priority { get { return priority; } } public State(StateMachine machine) { this.machine = machine; } /// <summary> /// Called when a state is becoming active. /// </summary> public virtual void Constructor() { } /// <summary> /// Called when a state is no more active. /// </summary> public virtual void Destructor() { } /// <summary> /// Called each frame to update the state internal logic. /// </summary> public virtual void Update() { } }
I agree to not liking this pattern very much. I'd much rather skip the switch statement completely. Also, using an enum is already limiting how many different states you can have. Code (csharp): public class Agent : MonoBehaviour { StateMachine stateMachine; void Update() { stateMachine.CurrentState.Execute(); } } Code (csharp): public class StateMachine { public IState CurrentState { get; private set; } public void ChangeState(IState newState) { if (CurrentState != null) CurrentState.Exit(); CurrentState = newState; CurrentState.Enter(); } } Code (csharp): public interface IState<T> { public T Owner { get; private set; } public void Enter(); public void Execute(); public void Exit(); } public class IdleState : IState<Agent> { public IdleState(Agent owner) { this.Owner = owner; } public void Enter() { } public void Execute() { } public void Exit() { } }
I like your approach LightStriker, my only criticism (that of personal preference) would be with the naming of your methods "Constructor" and "Destructor" for two reasons: The naming is confusing since C# has its own constructor and finalizer/destructor mechanism. So imo the terminology clashes. It is not consistent with Unity's naming "OnEnable" and "OnDisable". In the context of a Unity project I would probably go with "OnEnable" and "OnDisable" and would probably introduce two additional methods "OnEnter" and "OnExit". That's just my 2 cents anyhow
Well, I didn't use Unity's naming for the specific reason to not be confusing with their own method. After all, "OnEnable" is called when an object is loaded, while a state "initiator" is called just before the state become active - once, or again if you reuse states. It would probably be more similar to "Awake" or "Start". I guess "OnEnter" and "OnExit" would make more sense. I like KelsoMRK "Enter, Exit, Execute"... EEE Also, it doesn't clash that much with C# constructor/destructor, because they both uses the class' name. Code (csharp): public class Car { // Constructor public Car() { } // Destructor ~Car() { } } But I can understand the confusion. And I admit that naming in my state machine come from a much older C (not C++) engine structure. Old habits die hard.
Unity executes OnEnable and OnDisable each time an object is activated/deactivated. No, of course it doesn't cause a syntax error, but it does clash with the C# terminology. The "constructor" of a class is still a different concept which shouldn't be confused...