f35c8a6d6e
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.
644 lines
16 KiB
C#
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
|
|
}
|
|
} |