Firstborn/Assets/RPG Creation Kit/Scripts/AI/AI Scripts/AIBehaviourTreed.cs
Schaken-Mods f35c8a6d6e 6/21/23 update
Upgraded the framework which includes the Distant cell rendering. I have been merging meshes to cut down the Draw calls, so far gaining on average 20FPS everywhere. New bugs are the magic fails (Again) and the environment presets fail to call when outside. Currently trying to build new lightmaps for the combined meshes.
2023-06-21 11:09:52 -05:00

644 lines
16 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPGCreationKit;
using RPGCreationKit.BehaviourTree;
using System.Linq;
namespace RPGCreationKit.AI
{
/// <summary>
/// Manages the Behaviour Trees and the interactions with the AI agent from a tree.
/// </summary>
public class AIBehaviourTreed : AICombatSystem
{
public TreeTickRate tickRate;
public int xFrames = 5;
public float xSeconds = 1f;
public PurposeState purposeState;
[Space(10)]
[Header("Behaviour Tree Control")]
public bool useBT = true;
public bool pauseBT = false;
public RPGCK_BT currentBehaviour;
public bool isUsingPurposeBehaviour = true;
public RPGCK_BT purposeBehaviourTree;
public RPGCK_BT combatBehaviourTree;
[Header("Settings")]
public bool keepTicking = true;
public override void Start()
{
base.Start();
StartCoroutine(IEStart());
}
IEnumerator IEStart()
{
if (!isAlive)
Die();
else
if (useBT)
{
purposeBehaviourTree = purposeBehaviourTree.RPGCK_BTCopy(this.gameObject, GetComponent<RckAI>());
combatBehaviourTree = combatBehaviourTree.RPGCK_BTCopy(this.gameObject, GetComponent<RckAI>());
// Always start with the NormalBehaviourTree
SwitchBehaviourTree(false);
StartTicking();
}
yield return null;
}
public void StartTicking()
{
keepTicking = true;
StopCoroutine(TickBehaviourTree());
StartCoroutine(TickBehaviourTree());
}
IEnumerator TickBehaviourTree()
{
while (keepTicking)
{
while (pauseBT)
yield return null;
switch(tickRate)
{
case TreeTickRate.EveryFrame:
yield return new WaitForEndOfFrame();
break;
case TreeTickRate.EveryXFrames:
int framesPassed = 0;
while(framesPassed <= xFrames)
{
yield return new WaitForEndOfFrame();
framesPassed++;
}
break;
case TreeTickRate.EveryXSeconds_Realtime:
yield return new WaitForSecondsRealtime(xSeconds);
break;
case TreeTickRate.EveryXSeconds_GameTime:
yield return new WaitForSeconds(xSeconds);
break;
}
while (pauseBT || currentBehaviour == null || currentBehaviour.nodes.Count <= 0 || currentBehaviour.nodes[0] == null)
yield return null;
try
{
(currentBehaviour.nodes[0] as BTNode).Execute();
}catch { }
/*
if (firstTick == false)
{
// Tick
(currentBehaviour.nodes[0] as BTNode).Execute();
firstTick = true;
}
else
{
// Find all the nodes that are still running and execute them
((currentBehaviour.nodes[0] as BTNode).GetOutputPort("firstNode").Connection.node as BTNode).Execute();
}
*/
yield return null;
}
}
public void ChangeTickRate(TreeTickRate newTickRate)
{
tickRate = newTickRate;
}
protected bool anyWaitForSetRunning = false;
protected bool anyWaitForSwitchRunning = false;
public IEnumerator QueueSwitch(bool _combat)
{
if (anyWaitForSwitchRunning)
yield return null;
SwitchBehaviourTree(_combat);
}
[AIInvokable]
public void SwitchBehaviourTree(bool _combat)
{
if (anyWaitForSwitchRunning)
{
StartCoroutine(QueueSwitch(_combat));
return;
}
// Execute OnExit node if present
if (currentBehaviour != null && currentBehaviour.nodes.Count > 0 && ((currentBehaviour.nodes[0] as BTNode).GetOutputPort("onExitNode").Connection != null && ((currentBehaviour.nodes[0] as BTNode).GetOutputPort("onExitNode").Connection.node != null)))
{
anyWaitForSwitchRunning = true;
// Execute OnExit before changing the tree
StartCoroutine(SwitchBehaviourTree_WaitForExitNodeTask(_combat));
}
else
{
if (_combat)
{
currentBehaviour = combatBehaviourTree;
isUsingPurposeBehaviour = false;
}
else
{
currentBehaviour = purposeBehaviourTree;
isUsingPurposeBehaviour = true;
}
}
}
IEnumerator SwitchBehaviourTree_WaitForExitNodeTask(bool _combat)
{
pauseBT = true;
yield return new WaitForEndOfFrame();
var node = (((currentBehaviour.nodes[0] as BTNode).GetOutputPort("onExitNode").Connection.node) as BTNode);
node.ReEvaluate();
while (node.m_NodeState == NodeState.Null || node.m_NodeState == NodeState.Running)
{
node.Execute();
yield return new WaitForEndOfFrame();
}
if (anyWaitForSetRunning)
{
yield return new WaitForEndOfFrame();
}
if (_combat)
{
currentBehaviour = combatBehaviourTree;
isUsingPurposeBehaviour = false;
}
else
{
currentBehaviour = purposeBehaviourTree;
isUsingPurposeBehaviour = true;
}
anyWaitForSwitchRunning = false;
pauseBT = false;
yield break;
}
[AIInvokable]
public void SwitchBehaviourTreeToAI(string _rckID, bool _combat)
{
// Get the RckAI if possible
RckAI sTo = null;
CellsSystem.CellInformation.TryToGetAI(_rckID, out sTo);
if (sTo != null)
{
sTo.SwitchBehaviourTree(_combat);
}
}
public override void OnEnterInCombat()
{
base.OnEnterInCombat();
if (combatBehaviourTree.resetVariablesUponStartResume)
combatBehaviourTree.ResetVariables();
if (isUsingActionPoint && !leavingActionPoint)
StopUsingNPCActionPoint();
SwitchBehaviourTree(true);
}
[AIInvokable]
public override void LeaveCombat()
{
base.LeaveCombat();
if (purposeBehaviourTree.resetVariablesUponStartResume)
purposeBehaviourTree.ResetVariables();
// reset ishostile
isHostile = false;
isHostileAgainstPC = false;
SwitchBehaviourTree(false);
// Reset PurposeState
if (purposeState.IsAssigned)
{
SetTarget(purposeState.Target);
purposeState.ResumePurpose();
}
}
public override void Damage(DamageContext context)
{
if (context.sender != null && !context.sender.CompareTag("Player"))
{
// If they're both members of the same factions chances are they didn't meant to attach each other, so ignore
for (int i = 0; i < belongsToFactions.Count; i++)
{
if ((context.sender as EntityFactionable).GetInFaction(belongsToFactions[i].ID))
return;
}
}
base.Damage(context);
if (aiSounds != null && isAlive)
{
if (aiSounds.takeDamageSounds.Length > 0)
{
int n = Random.Range(0, aiSounds.takeDamageSounds.Length);
if (aiSounds.takeDamageSounds[n] != null)
{
audioSource.clip = aiSounds.takeDamageSounds[n];
audioSource.Play();
}
}
}
if (context.sender != null)
{
// If they're both members of the same factions chances are they didn't meant to attach each other, so ignore
if (isInCombat)
{
for (int i = 0; i < belongsToFactions.Count; i++)
{
if ((context.sender as EntityFactionable).GetInFaction(belongsToFactions[i].ID))
return;
}
}
if (!enemyTargets.Any(t => t.m_entity == context.sender))
{
if (isInConversation)
StopConversation();
if (!m_isInCombat)
EnterInCombatAgainst(context.sender);
enemyTargets.Add(new VisibleEnemy(context.sender, context.sender.GetComponent<EntityAttributes>(), new AggroInfo(50)));
}
else // this enemy was already in combat with this agent so just update the aggro
{
var entTarget = enemyTargets.Find(x => x.m_entity == context.sender);
entTarget.m_aggro.AlterAggroValue(AggroSettings.Modifier.Damage, context.amount);
float weaponAggroModifier = 0.0f;
if (context.weaponItem != null)
weaponAggroModifier = context.weaponItem.aggroModifier;
else if (context.spell != null)
weaponAggroModifier = context.spell.aggroModifier;
entTarget.m_aggro.AlterAggroValue(AggroSettings.Modifier.Weapon, weaponAggroModifier);
}
}
}
public override void Die()
{
base.Die();
attributes.CurHealth = -1;
attributes.activeEffects.Clear();
keepTicking = false;
useBT = false;
pauseBT = true;
for (int i = 0; i < inventory.Items.Count; i++) {
inventory.Items[i].metadata.IsOwnedByNPC = false;
}
}
#region Methods and Functions specifically for Behaviour Trees
public bool hasARangedWeapon = false;
[AIInvokable]
public void HasRangedWeaponCheck()
{
WeaponItem weaponItem = null;
for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++)
{
weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item;
if (weaponItem.weaponType == WeaponType.Bow || weaponItem.weaponType == WeaponType.Crossbow)
{
if (HasAmmoForWeapon(weaponItem))
{
hasARangedWeapon = true;
break;
}
else
{
Debug.Log("don't have ammo for " + weaponItem.ItemID);
}
}
}
}
public bool hasAMeleeWeapon = false;
[AIInvokable]
public void HasMeleeWeaponCheck()
{
WeaponItem weaponItem = null;
for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++)
{
weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item;
if (weaponItem.weaponType == WeaponType.BladeOneHand || weaponItem.weaponType == WeaponType.BluntOneHand ||
weaponItem.weaponType == WeaponType.BladeTwoHands || weaponItem.weaponType == WeaponType.BluntTwoHands ||
weaponItem.weaponType == WeaponType.DaggerOneHand)
{
hasAMeleeWeapon = true;
break;
}
}
}
public float distanceFromMainTarget;
[AIInvokable]
public void GetDistanceFromMainTarget()
{
if(mainTarget == null)
{
Debug.LogWarning("Called GetDistanceFromMainTarget() but no MainTarget has been set");
return;
}
distanceFromMainTarget = Vector3.Distance(this.transform.position, mainTarget.transform.position);
}
[AIInvokable]
public void Weapon_SwitchToMelee()
{
isSwitchingWeapon = true;
WeaponItem weaponItem = null;
for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++)
{
weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item;
if (weaponItem.weaponType == WeaponType.BladeOneHand || weaponItem.weaponType == WeaponType.BluntOneHand ||
weaponItem.weaponType == WeaponType.BladeTwoHands || weaponItem.weaponType == WeaponType.BluntTwoHands
|| weaponItem.weaponType == WeaponType.DaggerOneHand)
{
ChangeWeapon(weaponItem.ItemID);
break;
}
}
}
[AIInvokable]
public void Weapon_SwitchToRanged()
{
isSwitchingWeapon = true;
WeaponItem weaponItem = null;
for (int i = 0; i < inventory.subLists[(int)ItemTypes.WeaponItem].Count; i++)
{
weaponItem = (WeaponItem)inventory.subLists[(int)ItemTypes.WeaponItem][i].item;
if (weaponItem.weaponType == WeaponType.Bow || weaponItem.weaponType == WeaponType.Crossbow)
{
// We got a weapon, check for ammo
if (EquipAmmoForWeapon(weaponItem))
{
ChangeWeapon(weaponItem);
break;
}
}
}
}
[AIInvokable]
public void Weapon_SwitchToDefault()
{
ChangeWeapon(defaultWeapon.ItemID);
}
public float currentWeaponReach = RCKSettings.DEFAULT_WEAPON_REACH;
[AIInvokable]
public void GetWeaponsReach()
{
if (equipment.currentWeapon != null)
currentWeaponReach = equipment.currentWeapon.Reach;
else
currentWeaponReach = RCKSettings.DEFAULT_WEAPON_REACH; // 4 is default value
}
[AIInvokable]
public void SpeakToTarget()
{
if (mainTarget.CompareTag("Player"))
RPGCreationKit.Player.RckPlayer.instance.StartDialogue(this.GetComponent<IDialoguable>());
else
{
IDialoguable[] targetDialoguable = new IDialoguable[2];
targetDialoguable[0] = mainTarget.GetComponent<IDialoguable>();
if (targetDialoguable == null)
targetDialoguable[0] = mainTarget.GetComponentInParent<IDialoguable>();
// Set us
targetDialoguable[1] = this;
if (targetDialoguable[0] != null)
{
DialogueLogic(targetDialoguable, GetCurrentDialogueGraph());
}
}
}
[AIInvokable]
public void SetRckAIAsTarget(string _rckID)
{
// Get the RckAI if possible
RckAI sTo = null;
CellsSystem.CellInformation.TryToGetAI(_rckID, out sTo);
if (sTo != null)
SetTarget(sTo.gameObject);
}
public override void OnDialogueEnds(GameObject target)
{
//// PURPOSE_CALLBACK: ClearOnTeleportToCell \\\\\
if (purposeState != null && purposeState.IsAssigned && purposeState.clearsOn == PurposeClearTypes.ClearOnFinishTalkingWith)
{
if (purposeState.clearsOnData.stringData == speakingTo.GetEntityGameObject().GetComponent<Entity>().entityID)
{
// We did it
purposeState.CompletePurpose();
}
}
base.OnDialogueEnds(target);
}
/// <summary>
/// Forces the AI to stop the dialogue
/// </summary>
public void StopConversation()
{
if (entitiesTalkingWith != null)
{
// Copy before destroying entitiesTalkingWith
List<IDialoguable> dial = new List<IDialoguable>();
for (int i = 0; i < entitiesTalkingWith.Length; i++)
dial.Add(entitiesTalkingWith[i]);
for (int i = 0; i < dial.Count; i++)
dial[i].ForceToStopDialogue();
}
}
bool goingToSpeak = false;
[AIInvokable]
public void SendAIToSpeakToPlayer(bool run)
{
if (goingToSpeak)
return;
goingToSpeak = true;
StopCoroutine(SendAIToSpeakToPlayerTask());
if ( isReachingActionPoint || isUsingActionPoint)
{
ResetActionPointAgentState();
StopUsingNPCActionPoint(true);
}
if (run)
Movements_SwitchToRun();
else
Movements_SwitchToWalk();
// Set maintarget
SetTarget(Player.RckPlayer.instance.gameObject);
// Follow until he reaches it
StartCoroutine(SendAIToSpeakToPlayerTask());
}
public IEnumerator SendAIToSpeakToPlayerTask()
{
if (!isLoaded)
{
goingToSpeak = false;
yield break;
}
while (Vector3.Distance(this.gameObject.transform.position, Player.RckPlayer.instance.transform.position) > RCKSettings.NPC_TO_NPC_CONVERSATION_DISTANCE || !CanDialogue() ||
!Player.RckPlayer.instance.CanDialogue() || !WorldManager.instance.isLoading && !Player.RckPlayer.instance.IsControlledByPlayer() || isInOfflineMode || !isLoaded)
yield return new WaitForEndOfFrame();
Player.RckPlayer.instance.StartDialogue(this);
goingToSpeak = false;
yield break;
}
[AIInvokable]
public void AssignPurpose(RckAI _AI, GameObject _target, PurposeClearTypes _clearsOn, PurposeStateClearsOnData _clearOnData, string _nextPurposeBehaviourTree = null)
{
purposeState.AssignPurpose(_AI, _target, _clearsOn, _clearOnData, _nextPurposeBehaviourTree);
}
[AIInvokable]
public void Purpose_AssignToFollowCurrentPath()
{
if(aiPath != null && aiPath.gameObject != null)
purposeState.AssignPurpose(GetComponent<RckAI>(), aiPath.gameObject, PurposeClearTypes.ClearOnDeath, null, null);
}
[AIInvokable]
public void Magic_SwitchToHealingSpell()
{
// Check if the AI already has an healing spell
if (spellsKnowledge.spellInUse != null && spellsKnowledge.spellInUse.tag == SpellTag.RestoreHealth)
return;
for(int i = 0; i < spellsKnowledge.Spells.Count; i++)
{
if(spellsKnowledge.Spells[i].tag == SpellTag.RestoreHealth)
{
spellsKnowledge.spellInUse = spellsKnowledge.Spells[i];
return;
}
}
}
[AIInvokable]
public void Magic_SwitchToDamageSpell()
{
// Check if the AI already has an healing spell
if (spellsKnowledge.spellInUse != null && spellsKnowledge.spellInUse.tag == SpellTag.DamageProjectile || spellsKnowledge.spellInUse.tag == SpellTag.DamageTouch)
return;
for (int i = 0; i < spellsKnowledge.Spells.Count; i++)
{
if (spellsKnowledge.Spells[i].tag == SpellTag.DamageProjectile || spellsKnowledge.Spells[i].tag == SpellTag.DamageTouch)
{
spellsKnowledge.spellInUse = spellsKnowledge.Spells[i];
return;
}
}
}
[AIInvokable]
public void Magic_SetSpell(string _spellIDToSet)
{
Spell _spellToSet = SpellsDatabase.GetSpell(_spellIDToSet);
if (_spellToSet != null)
spellsKnowledge.spellInUse = _spellToSet;
}
[AIInvokable]
public void Magic_AddSpellToKnowledge(string _spellIDToAdd)
{
Spell _spellToAdd = SpellsDatabase.GetSpell(_spellIDToAdd);
if (_spellToAdd != null)
spellsKnowledge.Spells.Add(_spellToAdd);
}
#endregion
}
}