Despite my creature AI working (for the most part), I feel like the way I've set it up is terribly inefficient and likely committing some programming sins. I want to rewrite it to be more clean, efficient, and easier to maintain but I'm not exactly sure where to begin.
In my creature AI, I have a list of triggers, such as OnSpawn, OnDeath, or OnCollisionEnter. Within each trigger is a list of actions such as "Cast a Spell" or "Play an Animation". When a trigger's conditions are met, its list of actions are processed to check if it's not already in our processing list, adds it, and then plays their associated actions. When the trigger's conditions are not met, the list of actions are removed from this process list, and similarly processes through some remove functions to clean up behavior.
Some code that I've simplified:
void Update()
{
if (canAct && !dead)
{
CheckTriggers();
PlayAllActions();
}
}
private void CheckTriggers()
{
for (int i = 0; i < actions.Length; i++)
{
switch (actions[i].trigger)
{
case ActionTrigger.Trigger.OnCollisionEnter:
if (isColliding)
AddActionList(actions[i].actionSetList);
else
RemoveActionList(actions[i].actionSetList);
break;
case ActionTrigger.Trigger.UponBeingAttacked:
if (hasBeenAttacked)
AddActionList(actions[i].actionSetList);
break;
}
}
}
public void AddActionList(ActionSetList actionSetList)
{
bool containsItem = existingActionsList.Any(item => item == actionSetList);
if (containsItem)
return;
existingActionsList.Add(actionSetList);
}
private void PlayAllActions()
{
if (existingActionsList.Count > 0)
for (int i = 0; i < existingActionsList.Count; i++)
ActionPlayEffect(existingActionsList[i]);
}
public void ActionPlayEffect(ActionSetList actionSetList)
{
for (int i = 0; i < actionSetList.Length; i++)
{
switch (actionSetList[i].type)
{
case ActionSet.Type.CastSpell:
if (spellRoutine == null && actionSetList[i].cooldownTimeRemaining <= 0)
spellRoutine = StartCoroutine(Cast(actionSetList[i]));
break;
case ActionSet.Type.PlayAnim:
if (!isInActionPose)
{
animator.SetTrigger("ActionTrigger");
animator.SetInteger("Action", (int)actionSetList[i].animToPlay+1);
isInActionPose = true;
}
break;
}
}
}
public void RemoveActionList(ActionSetList actionSetList)
{
bool containsItem = existingActionsList.Any(item => item == actionSetList);
if (containsItem)
{
ActionRemoveEffect(actionSetList);
existingActionsList.Remove(actionSetList);
}
}
public void ActionRemoveEffect(ActionSetList actionSetList)
{
for (int i = 0; i < actionSetList.Length; i++)
{
switch (actionSetList[i].type)
{
case ActionSet.Type.CastSpell:
CancelCast();
break;
case ActionSet.Type.PlayAnim:
animator.SetTrigger("ActionTrigger");
animator.SetInteger("Action", 0);
isInActionPose = false;
break;
}
}
}
What can I do to build a more efficient creature AI?
I would probably write a similar system using delegates.
A delegate can to some extent be considered a variable holding a list of methods. If you execute that delegate you then execute all the methods it holds.
This would allow you to add methods like this, to a list of methods that you then call when desired.
delegate void OnSpawn(GameObject gameObject); //Create delegate type
public OnSpawn onSpawn; //Create variable from delegate type
void SetUpStats(Gameobject gameObject){
//Set hp, initialize spells
}
void SetUpAnimations(GameObject gameObject){
//Initialize animations
}
void PlaySpawnSound(GameObject gameObject){
//Play a spawn sound
}
void Start(){
if (onSpawn == null) //Add content to delegate
{
onSpawn = new OnSpawn(SetUpStats); //You may be able to write onSpawn = SetUpStats; instead, for shorter code. But please test it.
onSpawn += SetUpAnimations;
onSpawn += PlaySpawnSound;
}
}
void Spawn(){
onSpawn(gameObject);
//Call delegate, invoking all methods stored in it.
//Note that they all receive the same input. In this case the gameObject.
//You can give them any input you want, so long as you define it in the delegate type.
//I chose a gameObject as you can get all components and more from it.
}
Let me know if you have any questions or things you wonder about.
Related
I have this code that lets me handle button combinations in Unity:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InputBufferScriptTest: MonoBehaviour {
public enum FightInput {
Up,
UpRight,
Right,
DownRight,
Down,
DownLeft,
Left,
UpLeft,
LightPunch,
MediumPunch,
HeavyPunch,
LightKick,
MediumKick,
HeavyKick
}
List<FightInput> recentInputs = new List<FightInput>();
public List<FightInput> Special1Inputs = new List<FightInput>();
public List<FightInput> Special2Inputs = new List<FightInput>();
// Update is called once per frame
void Update() {
if (Input.GetKeyDown(KeyCode.W))
{
recentInputs.Add(FightInput.Up);
Debug.Log("UP");
}
if (Input.GetKeyDown(KeyCode.S))
{
recentInputs.Add(FightInput.Down);
Debug.Log("DOWN");
}
if (Input.GetKeyDown(KeyCode.A))
{
recentInputs.Add(FightInput.Left);
Debug.Log("LEFT");
}
if (Input.GetKeyDown(KeyCode.D))
{
recentInputs.Add(FightInput.Right);
Debug.Log("RIGHT");
}
if (Input.GetKeyDown(KeyCode.U))
{
recentInputs.Add(FightInput.LightPunch);
Debug.Log("LIGHT PUNCH");
}
if (Input.GetKeyDown(KeyCode.I))
{
recentInputs.Add(FightInput.MediumPunch);
Debug.Log("MEDIUM PUNCH");
}
if (Input.GetKeyDown(KeyCode.O))
{
recentInputs.Add(FightInput.HeavyPunch);
Debug.Log("HEAVY PUNCH");
}
if (Input.GetKeyDown(KeyCode.J))
{
recentInputs.Add(FightInput.LightKick);
Debug.Log("LIGHT KICK");
}
if (Input.GetKeyDown(KeyCode.K))
{
recentInputs.Add(FightInput.MediumKick);
Debug.Log("MEDIUM KICK");
}
if (Input.GetKeyDown(KeyCode.L))
{
recentInputs.Add(FightInput.HeavyKick);
Debug.Log("HEAVY KICK");
}
for (int i = recentInputs.Count - 1, j = Special1Inputs.Count - 1; i >= 0 && j >= 0; --i, --j) {
FightInput input = recentInputs[i];
FightInput nextSpecial1Input = Special1Inputs[j];
if (input != nextSpecial1Input) {
break;
} else if (j == 0) {
//Combo was entered, do Special1 then empty the list
Debug.Log("Special1!");
recentInputs.Clear();
}
}
for (int i = recentInputs.Count - 1, j = Special2Inputs.Count - 1; i >= 0 && j >= 0; --i, --j) {
FightInput input = recentInputs[i];
FightInput nextSpecial2Input = Special2Inputs[j];
if (input != nextSpecial2Input) {
break;
} else if (j == 0) {
//Combo was entered, do Special2 then empty the list
Debug.Log("Special2!");
recentInputs.Clear();
}
}
float secondsSinceLastInput = 0;
secondsSinceLastInput += Time.deltaTime;
if (secondsSinceLastInput > 1) {
recentInputs.Clear();
}
}
}
It works by me making a special attack list (public List<FightInput> Special1Inputs = new List<FightInput>(); for example). And then in the editor I can choose how many buttons are needed to trigger the special attack, then I get a drop down list to choose which button should be pressed for each step in the chain. The issue is if I wanted to have 5 special attacks, I'd have to repeat public List<FightInput> Special1Inputs = new List<FightInput>(); 5 times, name them individually, and copy and paste the for loop 5 times and make them correspond to the right special attack. Is there a way I can add special attacks in the editor? I was thinking of it going something like getting an "Add a special attack" field in the editor, and then Once I do I can name the special attack, and give it its number of inputs and its required buttons. Or maybe there's a better way to handle doing this that I haven't realised yet
Probably, you need a list of List<FightInput> like List<List<FightInput>>.
Sadly for this to work in unity editor, you would need to write a wrapper to be able to use it:
[Serializable]
public class InputList
{
public List<FightInput> fightInputs;
}
public List<InputList> specialAttacks;
This way, you will be able to assign multiple special attacks without creating multiple variables. Just be sure to change loops - you would need to put your current loops inside a bigger loop that iterates over specialAttakcs list.
https://imgur.com/a/g5u9qtg - result in the editor
Of course you can create custom editor to add special attacks, if you wish so. If you want to go this path, maybe you should consider using ScriptableObject.
I have an array of toggles defined in script. They are all turned off in the beginning. When the user clicks on one of the toggles, that toggle should be turned on and the other toggles should be switched to off state. Basically there could be only "one" on state toggle. How do I achieve this?
Currently with this script, all the toggles are getting turned off when the user clicks on one of it.
public Toggle[] toggle;
void Start () {
for (int i = 0; i < toggle.Length; i++) {
int idx = i;
toggle[idx].onValueChanged.AddListener (delegate {
ToggleValueChanged (toggle[idx], idx);
});
}
}
public void ToggleValueChanged (Toggle change, int index) {
for (int i = 0; i < toggle.Length; i++) {
if (i == index) {
return;
} else {
if (toggle[i].isOn) {
toggle[i].isOn = false;
}
}
}
}
Unity has a component called ToggleGroup that allow you to do just that.
ToggleContainer Parent Object:
Reference ToggleGroup object in every Toggle component as following:
Scene Hierarchy:
Overview:
https://gfycat.com/bossyscenteddorado
Change your ToggleValueChanged function like this :
public void ToggleValueChanged (Toggle change, int index)
{
for (int i = 0; i < toggle.Length; i++)
{
if (i == index) continue;
if (toggle[i].isOn) {
toggle[i].isOn = false;
}
}
}
When you return in you first if statement, other toggles won't get off. you have to continue iterating your loop.
And instead getting the index and passing it to the delegate, you can use RefrenceEqual
EDIT
Actually each time you manipulate the toggle[i].isOn, you are changing the value of it. So each time, You are calling your function.
EDIT
try this :
public void ToggleValueChanged (Toggle change, int index)
{
for (int i = 0; i < toggle.Length; i++)
{
if (i == index) continue;
if (toggle[i].isOn)
{
toggle[i].SetIsOnWithoutNotify(false);
}
}
}
Why do you even need the index for this?
Simply do
public void ToggleValueChanged (Toggle change)
{
// add a little safety to actually only disable all others if
// this one ws actually enabled
if(!change.isOn) return;
foreach(var toggle in toggles)
{
if (toggle == change) continue;
if (toggle.isOn)
{
// I would actually specifically NOT use "SetIsOnWithoutNotify"
// because you never know who else is actually listening to the state change
// so if you simply set it to false but don't invoke the callback events
// things might behave different than expected
toggle[i].isOn = false;
}
}
}
and accordingly
foreach(var t in toggle)
{
var currentToggle = t;
currentToggle .onValueChanged.AddListener(value => ToggleValueChanged(currentToggle));
}
There is no need to either return or continue, just don't handle the toggle if it's the indexth one:
public void ToggleValueChanged (Toggle change, int index)
{
for (int i = 0; i < toggle.Length; i++)
{
if (i != index)
{
toggle[i].isOn = false;
}
}
}
Also you can assume that all others are off, so no reason to check that either.
If they are mutually exclusive, they aren't "boolean states", it's just one state, supported by an enum and including the null value. If your UI is a set of checkboxes, you should switch it to a set of radio buttons. A dropdown box would do as well.
I've tried to make a script that if all the lights in my scene tagged "Light" are active at the same time, the game proceeds. None of the answers I've found have helped so far. I always end up with it only scanning the active objects, just randomly jumping out of the loop, or stopping when it has found one object that's active. Here is a simple piece of code that should have worked according to other posts
void Update()
{
bool allActive = false;
GameObject[] allLights = GameObject.FindGameObjectsWithTag("Light");
if(currenObjective > 0 && currenObjective < 3)
{
for (int i = 0; i < allLights.Length; i++)
{
if (allLights[i].activeInHierarchy)
{
allActive = true;
break;
}
}
if (allActive)
{
currentObjective = 2;
}
}
}
This code just sets the allActive variable to true at the moment one light is turned on.
You would need to invert the check:
private void Update()
{
// Since Find is always a bit expensive I would do the cheapest check first
if(currenObjective > 0 && currenObjective < 3)
{
var allLights = GameObject.FindGameObjectsWithTag("Light");
var allActive = true;
for (int i = 0; i < allLights.Length; i++)
{
if (!allLights[i].activeInHierarchy)
{
allActive = false;
break;
}
}
if (allActive)
{
currentObjective = 2;
}
}
}
Or You can do this in one line using Linq All
using System.Linq;
...
private void Update()
{
// Since Find is always a bit expensive I would do the cheapest check first
if(currenObjective > 0 && currenObjective < 3)
{
var allLights = GameObject.FindGameObjectsWithTag("Light");
if (allLights.All(light => light.activeInHierarchy))
{
currentObjective = 2;
}
}
}
As a general note: You should avoid using FindGameObjectsWithTag every frame! Either store these references ONCE at start, or if you spawn more lights on runtime implement it event driven and add the newly spawned lights to a list and then use that list to check.
If i understant you want to know if all objects are active:
using Linq does the job
you have to add using system.Linq to your script.
GameObject[] allLights = GameObject.FindGameObjectsWithTag("Light");
bool result = allLights.All(p => p.activeInHierarchy);
you could simplify your code like this:
private void Update()
{
if(currenObjective > 0 && currenObjective < 3 && GameObject.FindGameObjectsWithTag("Light").All(p => p.activeInHierarchy))
{
currentObjective = 2;
}
}
As says derHugo, FindGameObjectsWithTag is very expensive in each frame...
In a script :
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class NaviDialogue : MonoBehaviour
{
public ObjectsManipulation op;
public bool scaling = true;
public Scaling scale;
public ConversationTrigger conversationTrigger;
private bool ended = false;
private bool startConversation = false;
private void Update()
{
if (scaling == true && DOFControl.hasFinished == true)
{
DOFControl.hasFinished = false;
scaling = false;
op.Scaling();
PlayerController.disablePlayerController = true;
ConversationTrigger.conversationsToPlay.Add(0);
ConversationTrigger.conversationsToPlay.Add(1);
ConversationTrigger.conversationsToPlay.Add(2);
StartCoroutine(conversationTrigger.PlayConversations());
}
}
And in the top of ConversationTrigger :
public static List<int> conversationsToPlay = new List<int>();
In the method PlayConversations :
public IEnumerator PlayConversations()
{
for (int i = 0; i < conversationsToPlay.Count; i++)
{
yield return StartCoroutine(PlayConversation(conversationsToPlay[i]));
}
}
And the Play Conversation method :
public IEnumerator PlayConversation(int index)
{
isRunning = true;
if (conversations.Count > 0 &&
conversations[index].Dialogues.Count > 0)
{
for (int i = 0; i < conversations[index].Dialogues.Count; i++)
{
if (dialoguemanager != null)
{
dialoguemanager.StartDialogue(conversations[index].Dialogues[i]);
}
while (DialogueManager.dialogueEnded == false)
{
yield return null;
}
}
conversationIndex = index;
conversationEnd = true;
canvas.SetActive(false);
Debug.Log("Conversation Ended");
conversationsToPlay.Remove(index);
}
}
In the last method Play Conversation I'm removing the current played item :
conversationsToPlay.Remove(index);
The problem is that in the PlayConversations method now I value is 1 so it will play next the last item. So if there are 3 items it will play the first and the last but the middle one will not be played.
You should never modify a collection you are currently iterating over (the problem you encountered is one of the reasons for that). In your case, there are a few options, a simple solution could be to copy the list of conversations and at the same time clear the original list:
public IEnumerator PlayConversations()
{
var conversations = conversationsToPlay.ToArray(); // Copy the list
conversationsToPlay.Clear(); // Immediately clear the original list
for (int i = 0; i < conversations.Length; i++) // iterate over the array
{
// Now you also don't need to remove items anymore,
// since you already cleared the list
yield return StartCoroutine(PlayConversation(conversations[i]));
}
}
The array you create stays local to the coroutine, so you can clear the original list and work with the copy.
Alternatively, you could just change the loop to a while-loop and process the list from the start until it's empty:
public IEnumerator PlayConversations()
{
while (conversationsToPlay.Count > 0)
{
// Better remove the item right here, close to the loop condition.
// Makes things easier to understand.
var conversationIndex = conversationsToPlay[0];
conversationsToPlay.RemoveAt(0);
yield return StartCoroutine(PlayConversation(conversationIndex));
}
}
When going with the second example, you might just as well use a Queue<T> instead of a List<T> for the conversations, as a queue is designed specifically with first-in, first-out access in mind.
If you dont have a business requirment to maintain order, then Always iterate the collection in reverse order when you are planning to remove items. This you can remove items without breaking the sequence of the array. So here
public IEnumerator PlayConversations()
{
for (int i = conversationsToPlay.Count-1; i >=0; i++)
{
yield return StartCoroutine(PlayConversation(conversationsToPlay[i]));
}
}
This method works in general for all the situations where we remove something from the collection. However on a side note, Removing the conversation in Playconversation method is just a bad practice. you will end up with hard to maintain code. Remove it in some method which is specifically for this purpose. otherwise you are violating SRP
I have several variations of the following code within methods that are being used for Selenium testing (waits for certain events before returning) and I would like to refactor it and make it reusable so I have the logic controlling the delay & try/catch as a generic method but be able to swap in and out conditions depending on situation.
Is there an easy way to achieve this?
Code:
for (int second = 0; second <= 10; second++)
{
try
{
// bit that needs to vary
matchedAddresses = driver.FindElements(By.ClassName("addresslookup"));
if (matchedAddresses.Count > 0)
{
break;
}
}
catch (Exception)
{
}
Thread.Sleep(1000);
}
return matchedAddresses.Count;
You want function that takes argument of something like Func<int> - method that returns number of elements (or enumerable Func<IEnumerable<sometype>>)
public int GetCountOfElementsWithWait(Func<int> test)
{
.....
var count = test();
....
}
Seems a bit too obvious, but would this work?
public int GetCountOfElementsByClassName(string className)
{
for (int second = 0; second <= 10; second++)
{
try
{
// bit that needs to vary
matchedElements = driver.FindElements(By.ClassName(className));
if (matchedElements.Count > 0)
{
break;
}
}
catch (Exception)
{
}
Thread.Sleep(1000);
}
return matchedElements.Count;
}