|
|
|
|
@ -0,0 +1,787 @@
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using Convai.Scripts.Runtime.Core;
|
|
|
|
|
using Convai.Scripts.Runtime.LoggerSystem;
|
|
|
|
|
using Service;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.AI;
|
|
|
|
|
using UnityEngine.Serialization;
|
|
|
|
|
|
|
|
|
|
namespace Convai.Scripts.Runtime.Features
|
|
|
|
|
{
|
|
|
|
|
// STEP 1: Add the enum for your custom action here.
|
|
|
|
|
public enum ActionChoice
|
|
|
|
|
{
|
|
|
|
|
None,
|
|
|
|
|
Jump,
|
|
|
|
|
Crouch,
|
|
|
|
|
MoveTo,
|
|
|
|
|
PickUp,
|
|
|
|
|
Drop
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// DISCLAIMER: The action API is in experimental stages and can misbehave. Meanwhile, feel free to try it out and play
|
|
|
|
|
/// around with it.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[DisallowMultipleComponent]
|
|
|
|
|
[AddComponentMenu("Convai/Convai Actions Handler")]
|
|
|
|
|
public class ConvaiActionsHandler : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
[SerializeField] public ActionMethod[] actionMethods;
|
|
|
|
|
public List<string> actionResponseList = new();
|
|
|
|
|
private readonly List<ConvaiAction> _actionList = new();
|
|
|
|
|
public readonly ActionConfig ActionConfig = new();
|
|
|
|
|
private List<string> _actions = new();
|
|
|
|
|
private ConvaiNPC _currentNPC;
|
|
|
|
|
private ConvaiInteractablesData _interactablesData;
|
|
|
|
|
private Coroutine _playActionListCoroutine;
|
|
|
|
|
|
|
|
|
|
// Awake is called when the script instance is being loaded
|
|
|
|
|
private void Awake()
|
|
|
|
|
{
|
|
|
|
|
// Find the global action settings object in the scene
|
|
|
|
|
_interactablesData = FindObjectOfType<ConvaiInteractablesData>();
|
|
|
|
|
|
|
|
|
|
// Check if the global action settings object is missing
|
|
|
|
|
if (_interactablesData == null)
|
|
|
|
|
// Log an error message to indicate missing Convai Action Settings
|
|
|
|
|
ConvaiLogger.Error("Convai Action Settings missing. Please create a game object that handles actions.",
|
|
|
|
|
ConvaiLogger.LogCategory.Character);
|
|
|
|
|
|
|
|
|
|
// Check if this GameObject has a ConvaiNPC component attached
|
|
|
|
|
if (TryGetComponent(out ConvaiNPC npc))
|
|
|
|
|
// If it does, set the current NPC to this GameObject
|
|
|
|
|
_currentNPC = npc;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Iterate through each action method and add its name to the action configuration
|
|
|
|
|
foreach (ActionMethod actionMethod in actionMethods) ActionConfig.Actions.Add(actionMethod.action);
|
|
|
|
|
|
|
|
|
|
if (_interactablesData != null)
|
|
|
|
|
{
|
|
|
|
|
// Iterate through each character in global action settings and add them to the action configuration
|
|
|
|
|
foreach (ConvaiInteractablesData.Character character in _interactablesData.Characters)
|
|
|
|
|
{
|
|
|
|
|
ActionConfig.Types.Character rpcCharacter = new()
|
|
|
|
|
{
|
|
|
|
|
Name = character.Name,
|
|
|
|
|
Bio = character.Bio
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ActionConfig.Characters.Add(rpcCharacter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Iterate through each object in global action settings and add them to the action configuration
|
|
|
|
|
foreach (ConvaiInteractablesData.Object eachObject in _interactablesData.Objects)
|
|
|
|
|
{
|
|
|
|
|
ActionConfig.Types.Object rpcObject = new()
|
|
|
|
|
{
|
|
|
|
|
Name = eachObject.Name,
|
|
|
|
|
Description = eachObject.Description
|
|
|
|
|
};
|
|
|
|
|
ActionConfig.Objects.Add(rpcObject);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Reset()
|
|
|
|
|
{
|
|
|
|
|
actionMethods = new ActionMethod[]
|
|
|
|
|
{
|
|
|
|
|
new() { action = "Move To", actionChoice = ActionChoice.MoveTo },
|
|
|
|
|
new() { action = "Pick Up", actionChoice = ActionChoice.PickUp },
|
|
|
|
|
new() { action = "Dance", animationName = "Dance", actionChoice = ActionChoice.None },
|
|
|
|
|
new() { action = "Drop", actionChoice = ActionChoice.Drop },
|
|
|
|
|
new() { action = "Jump", actionChoice = ActionChoice.Jump }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start is called before the first frame update
|
|
|
|
|
private void Start()
|
|
|
|
|
{
|
|
|
|
|
// Set up the action configuration
|
|
|
|
|
|
|
|
|
|
#region Actions Setup
|
|
|
|
|
|
|
|
|
|
// Set the classification of the action configuration to "multistep"
|
|
|
|
|
ActionConfig.Classification = "multistep";
|
|
|
|
|
|
|
|
|
|
// Log the configured action information
|
|
|
|
|
ConvaiLogger.DebugLog(ActionConfig, ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
// Start playing the action list using a coroutine
|
|
|
|
|
_playActionListCoroutine = StartCoroutine(PlayActionList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnEnable() {
|
|
|
|
|
if ( _playActionListCoroutine != null ) {
|
|
|
|
|
_playActionListCoroutine = StartCoroutine(PlayActionList());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDisable() {
|
|
|
|
|
if ( _playActionListCoroutine != null ) {
|
|
|
|
|
StopCoroutine(_playActionListCoroutine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
if (actionResponseList.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
ParseActions(actionResponseList[0]);
|
|
|
|
|
actionResponseList.RemoveAt(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ParseActions(string actionsString)
|
|
|
|
|
{
|
|
|
|
|
actionsString = actionsString.Trim();
|
|
|
|
|
ConvaiLogger.DebugLog($"Parsing actions from: {actionsString}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
_actions = actionsString.Split(", ").ToList();
|
|
|
|
|
_actionList.Clear();
|
|
|
|
|
|
|
|
|
|
foreach (string action in _actions)
|
|
|
|
|
{
|
|
|
|
|
List<string> actionWords = action.Split(' ').ToList();
|
|
|
|
|
ConvaiLogger.Info($"Processing action: {action}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
ParseSingleAction(actionWords);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Parses a single action from a list of action words.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="actionWords">The list of words representing the action.</param>
|
|
|
|
|
private void ParseSingleAction(List<string> actionWords)
|
|
|
|
|
{
|
|
|
|
|
for (int j = 0; j < actionWords.Count; j++)
|
|
|
|
|
{
|
|
|
|
|
// Split the action into verb and object parts
|
|
|
|
|
string[] verbPart = actionWords.Take(j + 1).ToArray();
|
|
|
|
|
string[] objectPart = actionWords.Skip(j + 1).ToArray();
|
|
|
|
|
|
|
|
|
|
// Remove trailing 's' from verb words
|
|
|
|
|
verbPart = verbPart.Select(word => word.TrimEnd('s')).ToArray();
|
|
|
|
|
string actionString = string.Join(" ", verbPart);
|
|
|
|
|
|
|
|
|
|
// Find the best matching action using Levenshtein distance
|
|
|
|
|
ActionMethod matchingAction = actionMethods
|
|
|
|
|
.OrderBy(a => LevenshteinDistance(a.action.ToLower(), actionString.ToLower()))
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
// If no close match is found, continue to the next iteration
|
|
|
|
|
if (matchingAction == null || LevenshteinDistance(matchingAction.action.ToLower(), actionString.ToLower()) > 2) continue;
|
|
|
|
|
|
|
|
|
|
// Find the target object for the action
|
|
|
|
|
GameObject targetObject = FindTargetObject(objectPart);
|
|
|
|
|
LogActionResult(verbPart, objectPart, targetObject);
|
|
|
|
|
|
|
|
|
|
// Add the parsed action to the action list
|
|
|
|
|
_actionList.Add(new ConvaiAction(matchingAction.actionChoice, targetObject, matchingAction.animationName));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Finds the target object based on the object part of the action.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="objectPart">The array of words representing the object.</param>
|
|
|
|
|
/// <returns>The GameObject that best matches the object description, or null if no match is found.</returns>
|
|
|
|
|
private GameObject FindTargetObject(string[] objectPart)
|
|
|
|
|
{
|
|
|
|
|
string targetName = string.Join(" ", objectPart);
|
|
|
|
|
|
|
|
|
|
// Try to find a matching object
|
|
|
|
|
ConvaiInteractablesData.Object obj = _interactablesData.Objects
|
|
|
|
|
.OrderBy(o => LevenshteinDistance(o.Name.ToLower(), targetName.ToLower()))
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
if (obj != null && LevenshteinDistance(obj.Name.ToLower(), targetName.ToLower()) <= 2)
|
|
|
|
|
return obj.gameObject;
|
|
|
|
|
|
|
|
|
|
// If no object is found, try to find a matching character
|
|
|
|
|
ConvaiInteractablesData.Character character = _interactablesData.Characters
|
|
|
|
|
.OrderBy(c => LevenshteinDistance(c.Name.ToLower(), targetName.ToLower()))
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
if (character != null && LevenshteinDistance(character.Name.ToLower(), targetName.ToLower()) <= 2)
|
|
|
|
|
return character.gameObject;
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Calculates the Levenshtein distance between two strings.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="s">The first string.</param>
|
|
|
|
|
/// <param name="t">The second string.</param>
|
|
|
|
|
/// <returns>The Levenshtein distance between the two strings.</returns>
|
|
|
|
|
private int LevenshteinDistance(string s, string t)
|
|
|
|
|
{
|
|
|
|
|
int[][] d = new int[s.Length + 1][];
|
|
|
|
|
for (int index = 0; index < s.Length + 1; index++) d[index] = new int[t.Length + 1];
|
|
|
|
|
|
|
|
|
|
// Initialize the first row and column
|
|
|
|
|
for (int i = 0; i <= s.Length; i++)
|
|
|
|
|
d[i][0] = i;
|
|
|
|
|
for (int j = 0; j <= t.Length; j++)
|
|
|
|
|
d[0][j] = j;
|
|
|
|
|
|
|
|
|
|
// Calculate the distance
|
|
|
|
|
for (int j = 1; j <= t.Length; j++)
|
|
|
|
|
for (int i = 1; i <= s.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
int cost = s[i - 1] == t[j - 1] ? 0 : 1;
|
|
|
|
|
d[i][j] = Math.Min(Math.Min(d[i - 1][j] + 1, d[i][j - 1] + 1), d[i - 1][j - 1] + cost);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return d[s.Length][t.Length];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void LogActionResult(string[] verbPart, string[] objectPart, GameObject targetObject)
|
|
|
|
|
{
|
|
|
|
|
string verb = string.Join(" ", verbPart).ToLower();
|
|
|
|
|
string obj = string.Join(" ", objectPart).ToLower();
|
|
|
|
|
|
|
|
|
|
if (targetObject != null)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog($"Active Target: {obj}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
ConvaiLogger.DebugLog($"Found matching target: {targetObject.name} for action: {verb}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.Warn($"No matching target found for action: {verb}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Event that is triggered when an action starts.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This event can be subscribed to in order to perform custom logic when an action starts.
|
|
|
|
|
/// The event provides the name of the action and the GameObject that the action is targeting.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
public event Action<string, GameObject> ActionStarted;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Event that is triggered when an action ends.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This event can be subscribed to in order to perform custom logic when an action ends.
|
|
|
|
|
/// The event provides the name of the action and the GameObject that the action was targeting.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
public event Action<string, GameObject> ActionEnded;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This coroutine handles playing the actions in the action list.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private IEnumerator PlayActionList()
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
// Check if there are actions in the action list
|
|
|
|
|
if (_actionList.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// Call the DoAction function for the first action in the list and wait until it's done
|
|
|
|
|
yield return DoAction(_actionList[0]);
|
|
|
|
|
|
|
|
|
|
// Remove the completed action from the list
|
|
|
|
|
_actionList.RemoveAt(0);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// If there are no actions in the list, yield to wait for the next frame
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IEnumerator DoAction(ConvaiAction action)
|
|
|
|
|
{
|
|
|
|
|
// STEP 2: Add the function call for your action here corresponding to your enum.
|
|
|
|
|
// Remember to yield until its return if it is an Enumerator function.
|
|
|
|
|
|
|
|
|
|
// Use a switch statement to handle different action choices based on the ActionChoice enum
|
|
|
|
|
switch (action.Verb)
|
|
|
|
|
{
|
|
|
|
|
case ActionChoice.MoveTo:
|
|
|
|
|
// Call the MoveTo function and yield until it's completed
|
|
|
|
|
yield return MoveTo(action.Target);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ActionChoice.PickUp:
|
|
|
|
|
// Call the PickUp function and yield until it's completed
|
|
|
|
|
yield return PickUp(action.Target);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ActionChoice.Drop:
|
|
|
|
|
// Call the Drop function
|
|
|
|
|
Drop(action.Target);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ActionChoice.Jump:
|
|
|
|
|
// Call the Jump function
|
|
|
|
|
Jump();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ActionChoice.Crouch:
|
|
|
|
|
// Call the Crouch function and yield until it's completed
|
|
|
|
|
yield return Crouch();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ActionChoice.None:
|
|
|
|
|
// Call the AnimationActions function and yield until it's completed
|
|
|
|
|
yield return AnimationActions(action.Animation);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentOutOfRangeException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Yield once to ensure the coroutine advances to the next frame
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This method is a coroutine that handles playing an animation for Convai NPC.
|
|
|
|
|
/// The method takes in the name of the animation to be played as a string parameter.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="animationName"> The name of the animation to be played. </param>
|
|
|
|
|
/// <returns> A coroutine that plays the animation. </returns>
|
|
|
|
|
private IEnumerator AnimationActions(string animationName)
|
|
|
|
|
{
|
|
|
|
|
// Logging the action of initiating the animation with the provided animation name.
|
|
|
|
|
ConvaiLogger.DebugLog("Doing animation: " + animationName, ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Attempting to get the Animator component attached to the current NPC object.
|
|
|
|
|
// The Animator component is responsible for controlling animations on the GameObject.
|
|
|
|
|
Animator animator = _currentNPC.GetComponent<Animator>();
|
|
|
|
|
|
|
|
|
|
// Converting the provided animation name to its corresponding hash code.
|
|
|
|
|
// This is a more efficient way to refer to animations and Animator states.
|
|
|
|
|
int animationHash = Animator.StringToHash(animationName);
|
|
|
|
|
|
|
|
|
|
// Check if the Animator component has a state with the provided hash code.
|
|
|
|
|
// This is a safety check to prevent runtime errors if the animation is not found.
|
|
|
|
|
if (!animator.HasState(0, animationHash))
|
|
|
|
|
{
|
|
|
|
|
// Logging a message to indicate that the animation was not found.
|
|
|
|
|
ConvaiLogger.DebugLog("Could not find an animator state named: " + animationName, ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Exiting the coroutine early since the animation is not available.
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Playing the animation with a cross-fade transition.
|
|
|
|
|
// The second parameter '0.1f' specifies the duration of the cross-fade.
|
|
|
|
|
animator.CrossFadeInFixedTime(animationHash, 0.1f);
|
|
|
|
|
|
|
|
|
|
// Waiting for a short duration (just over the cross-fade time) to allow the animation transition to start.
|
|
|
|
|
// This ensures that subsequent code runs after the animation has started playing.
|
|
|
|
|
yield return new WaitForSeconds(0.11f);
|
|
|
|
|
|
|
|
|
|
// Getting information about the current animation clip that is playing.
|
|
|
|
|
AnimatorClipInfo[] clipInfo = animator.GetCurrentAnimatorClipInfo(0);
|
|
|
|
|
|
|
|
|
|
// Checking if there is no animation clip information available.
|
|
|
|
|
if (clipInfo == null || clipInfo.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
// Logging a message to indicate that there are no animation clips associated with the state.
|
|
|
|
|
ConvaiLogger.DebugLog("Animator state named: " + animationName + " has no associated animation clips",
|
|
|
|
|
ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Exiting the coroutine as there is no animation to play.
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Defining variables to store the length and name of the animation clip.
|
|
|
|
|
float length = 0;
|
|
|
|
|
string animationClipName = "";
|
|
|
|
|
|
|
|
|
|
// Iterating through the array of animation clips to find the one that is currently playing.
|
|
|
|
|
foreach (AnimatorClipInfo clipInf in clipInfo)
|
|
|
|
|
{
|
|
|
|
|
// Logging the name of the animation clip for debugging purposes.
|
|
|
|
|
ConvaiLogger.DebugLog("Clip name: " + clipInf.clip.name, ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Storing the current animation clip in a local variable for easier access.
|
|
|
|
|
AnimationClip clip = clipInf.clip;
|
|
|
|
|
|
|
|
|
|
// Checking if the animation clip is valid.
|
|
|
|
|
if (clip != null)
|
|
|
|
|
{
|
|
|
|
|
// Storing the length and name of the animation clip.
|
|
|
|
|
length = clip.length;
|
|
|
|
|
animationClipName = clip.name;
|
|
|
|
|
|
|
|
|
|
// Exiting the loop as we've found the information we need.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checking if a valid animation clip was found.
|
|
|
|
|
if (length > 0.0f)
|
|
|
|
|
{
|
|
|
|
|
// Logging a message indicating that the animation is now playing.
|
|
|
|
|
ConvaiLogger.DebugLog(
|
|
|
|
|
"Playing the animation " + animationClipName + " from the Animator State " + animationName +
|
|
|
|
|
" for " + length + " seconds", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Waiting for the duration of the animation to allow it to play out.
|
|
|
|
|
yield return new WaitForSeconds(length);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Logging a message to indicate that no valid animation clips were found or their length was zero.
|
|
|
|
|
ConvaiLogger.DebugLog(
|
|
|
|
|
"Animator state named: " + animationName +
|
|
|
|
|
" has no valid animation clips or they have a length of 0", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Exiting the coroutine early.
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transitioning back to the idle animation.
|
|
|
|
|
// It is assumed that an "Idle" animation exists and is set up in your Animator Controller.
|
|
|
|
|
animator.CrossFadeInFixedTime(Animator.StringToHash("Idle"), 0.1f);
|
|
|
|
|
|
|
|
|
|
// Yielding to wait for one frame to ensure that the coroutine progresses to the next frame.
|
|
|
|
|
// This is often done at the end of a coroutine to prevent issues with Unity's execution order.
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Registers the provided methods to the ActionStarted and ActionEnded events.
|
|
|
|
|
/// This allows external code to subscribe to these events and react when they are triggered.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="onActionStarted">
|
|
|
|
|
/// The method to be called when an action starts. It should accept a string (the action
|
|
|
|
|
/// name) and a GameObject (the target of the action).
|
|
|
|
|
/// </param>
|
|
|
|
|
/// <param name="onActionEnded">
|
|
|
|
|
/// The method to be called when an action ends. It should accept a string (the action name)
|
|
|
|
|
/// and a GameObject (the target of the action).
|
|
|
|
|
/// </param>
|
|
|
|
|
public void RegisterForActionEvents(Action<string, GameObject> onActionStarted,
|
|
|
|
|
Action<string, GameObject> onActionEnded)
|
|
|
|
|
{
|
|
|
|
|
ActionStarted += onActionStarted;
|
|
|
|
|
ActionEnded += onActionEnded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Unregisters the provided methods from the ActionStarted and ActionEnded events.
|
|
|
|
|
/// This allows external code to unsubscribe from these events when they are no longer interested in them.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="onActionStarted">
|
|
|
|
|
/// The method to be removed from the ActionStarted event. It should be the same method that
|
|
|
|
|
/// was previously registered.
|
|
|
|
|
/// </param>
|
|
|
|
|
/// <param name="onActionEnded">
|
|
|
|
|
/// The method to be removed from the ActionEnded event. It should be the same method that was
|
|
|
|
|
/// previously registered.
|
|
|
|
|
/// </param>
|
|
|
|
|
public void UnregisterForActionEvents(Action<string, GameObject> onActionStarted,
|
|
|
|
|
Action<string, GameObject> onActionEnded)
|
|
|
|
|
{
|
|
|
|
|
ActionStarted -= onActionStarted;
|
|
|
|
|
ActionEnded -= onActionEnded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
public class ActionMethod
|
|
|
|
|
{
|
|
|
|
|
[FormerlySerializedAs("Action")] [SerializeField]
|
|
|
|
|
public string action;
|
|
|
|
|
|
|
|
|
|
[SerializeField] public string animationName;
|
|
|
|
|
[SerializeField] public ActionChoice actionChoice;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class ConvaiAction
|
|
|
|
|
{
|
|
|
|
|
public ConvaiAction(ActionChoice verb, GameObject target, string animation)
|
|
|
|
|
{
|
|
|
|
|
Verb = verb;
|
|
|
|
|
Target = target;
|
|
|
|
|
Animation = animation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region 04. Public variables
|
|
|
|
|
|
|
|
|
|
public readonly string Animation;
|
|
|
|
|
public readonly GameObject Target;
|
|
|
|
|
public readonly ActionChoice Verb;
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// STEP 3: Add the function for your action here.
|
|
|
|
|
|
|
|
|
|
#region Action Implementation Methods
|
|
|
|
|
|
|
|
|
|
private IEnumerator Crouch()
|
|
|
|
|
{
|
|
|
|
|
ActionStarted?.Invoke("Crouch", _currentNPC.gameObject);
|
|
|
|
|
ConvaiLogger.DebugLog("Crouching!", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
Animator animator = _currentNPC.GetComponent<Animator>();
|
|
|
|
|
animator.CrossFadeInFixedTime(Animator.StringToHash("Crouch"), 0.1f);
|
|
|
|
|
|
|
|
|
|
// Wait for the next frame to ensure the Animator has transitioned to the new state.
|
|
|
|
|
yield return new WaitForSeconds(0.11f);
|
|
|
|
|
|
|
|
|
|
AnimatorClipInfo[] clipInfo = animator.GetCurrentAnimatorClipInfo(0);
|
|
|
|
|
if (clipInfo == null || clipInfo.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog("No animation clips found for crouch state!", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float length = clipInfo[0].clip.length;
|
|
|
|
|
|
|
|
|
|
_currentNPC.GetComponents<CapsuleCollider>()[0].height = 1.2f;
|
|
|
|
|
_currentNPC.GetComponents<CapsuleCollider>()[0].center = new Vector3(0, 0.6f, 0);
|
|
|
|
|
|
|
|
|
|
if (_currentNPC.GetComponents<CapsuleCollider>().Length > 1)
|
|
|
|
|
{
|
|
|
|
|
_currentNPC.GetComponents<CapsuleCollider>()[1].height = 1.2f;
|
|
|
|
|
_currentNPC.GetComponents<CapsuleCollider>()[1].center = new Vector3(0, 0.6f, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(length);
|
|
|
|
|
animator.CrossFadeInFixedTime(Animator.StringToHash("Idle"), 0.1f);
|
|
|
|
|
|
|
|
|
|
yield return null;
|
|
|
|
|
ActionEnded?.Invoke("Crouch", _currentNPC.gameObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IEnumerator MoveTo(GameObject target)
|
|
|
|
|
{
|
|
|
|
|
if (!IsTargetValid(target)) yield break;
|
|
|
|
|
|
|
|
|
|
ConvaiLogger.DebugLog($"Moving to Target: {target.name}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
ActionStarted?.Invoke("MoveTo", target);
|
|
|
|
|
|
|
|
|
|
Animator animator = _currentNPC.GetComponent<Animator>();
|
|
|
|
|
NavMeshAgent navMeshAgent = _currentNPC.GetComponent<NavMeshAgent>();
|
|
|
|
|
|
|
|
|
|
SetupAnimationAndNavigation(animator, navMeshAgent);
|
|
|
|
|
|
|
|
|
|
Vector3 targetDestination = CalculateTargetDestination(target);
|
|
|
|
|
navMeshAgent.SetDestination(targetDestination);
|
|
|
|
|
yield return null;
|
|
|
|
|
|
|
|
|
|
yield return MoveTowardsTarget(target, navMeshAgent);
|
|
|
|
|
|
|
|
|
|
FinishMovement(animator, target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsTargetValid(GameObject target)
|
|
|
|
|
{
|
|
|
|
|
if (target == null || !target.activeInHierarchy)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog("MoveTo target is null or inactive.", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetupAnimationAndNavigation(Animator animator, NavMeshAgent navMeshAgent)
|
|
|
|
|
{
|
|
|
|
|
animator.CrossFade(Animator.StringToHash("Walking"), 0.01f);
|
|
|
|
|
animator.applyRootMotion = false;
|
|
|
|
|
navMeshAgent.updateRotation = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Vector3 CalculateTargetDestination(GameObject target)
|
|
|
|
|
{
|
|
|
|
|
Vector3 targetDestination = target.transform.position;
|
|
|
|
|
if (target.TryGetComponent(out Renderer rendererComponent))
|
|
|
|
|
{
|
|
|
|
|
float zOffset = rendererComponent.bounds.size.z;
|
|
|
|
|
targetDestination += zOffset * target.transform.forward;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
targetDestination += 0.5f * target.transform.forward;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return targetDestination;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IEnumerator MoveTowardsTarget(GameObject target, NavMeshAgent navMeshAgent)
|
|
|
|
|
{
|
|
|
|
|
float rotationSpeed = 5;
|
|
|
|
|
while (navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance)
|
|
|
|
|
{
|
|
|
|
|
if (!target.activeInHierarchy)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog("Target deactivated during movement.", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (navMeshAgent.velocity.sqrMagnitude < Mathf.Epsilon) yield return null;
|
|
|
|
|
|
|
|
|
|
RotateTowardsMovementDirection(navMeshAgent, rotationSpeed);
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RotateTowardsMovementDirection(NavMeshAgent navMeshAgent, float rotationSpeed)
|
|
|
|
|
{
|
|
|
|
|
Quaternion rotation = Quaternion.LookRotation(navMeshAgent.velocity.normalized);
|
|
|
|
|
rotation.x = 0;
|
|
|
|
|
rotation.z = 0;
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, rotationSpeed * Time.deltaTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FinishMovement(Animator animator, GameObject target)
|
|
|
|
|
{
|
|
|
|
|
animator.CrossFade(Animator.StringToHash("Idle"), 0.1f);
|
|
|
|
|
if (_actions.Count == 1 && Camera.main != null) StartCoroutine(RotateTowardsCamera());
|
|
|
|
|
animator.applyRootMotion = true;
|
|
|
|
|
ActionEnded?.Invoke("MoveTo", target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IEnumerator RotateTowardsCamera()
|
|
|
|
|
{
|
|
|
|
|
if (Camera.main != null)
|
|
|
|
|
{
|
|
|
|
|
Vector3 direction = (Camera.main.transform.position - transform.position).normalized;
|
|
|
|
|
Quaternion targetRotation = Quaternion.LookRotation(direction);
|
|
|
|
|
float elapsedTime = 0f;
|
|
|
|
|
float rotationTime = 2f;
|
|
|
|
|
|
|
|
|
|
while (elapsedTime < rotationTime)
|
|
|
|
|
{
|
|
|
|
|
targetRotation.x = 0;
|
|
|
|
|
targetRotation.z = 0;
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, elapsedTime / rotationTime);
|
|
|
|
|
elapsedTime += Time.deltaTime;
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Coroutine to pick up a target GameObject, adjusting the NPCs' rotation and playing animations.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="target">The target GameObject to pick up.</param>
|
|
|
|
|
private IEnumerator PickUp(GameObject target)
|
|
|
|
|
{
|
|
|
|
|
// Invoke the ActionStarted event with the "PickUp" action and the target GameObject.
|
|
|
|
|
ActionStarted?.Invoke("PickUp", target);
|
|
|
|
|
|
|
|
|
|
// Check if the target GameObject is null. If it is, log an error and exit the coroutine.
|
|
|
|
|
if (target == null)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog("Target is null! Exiting PickUp coroutine.", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the target GameObject is active. If not, log an error and exit the coroutine.
|
|
|
|
|
if (!target.activeInHierarchy)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog($"Target: {target.name} is inactive! Exiting PickUp coroutine.",
|
|
|
|
|
ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate the direction from the NPC to the target, ignoring the vertical (y) component.
|
|
|
|
|
Vector3 direction = (target.transform.position - transform.position).normalized;
|
|
|
|
|
direction.y = 0;
|
|
|
|
|
|
|
|
|
|
// Calculate the target rotation to face the target direction.
|
|
|
|
|
Quaternion targetRotation = Quaternion.LookRotation(direction);
|
|
|
|
|
float elapsedTime = 0f;
|
|
|
|
|
float rotationTime = 0.5f;
|
|
|
|
|
|
|
|
|
|
// Smoothly rotate the NPC towards the target direction over a specified time.
|
|
|
|
|
while (elapsedTime < rotationTime)
|
|
|
|
|
{
|
|
|
|
|
targetRotation.x = 0;
|
|
|
|
|
targetRotation.z = 0;
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, elapsedTime / rotationTime);
|
|
|
|
|
|
|
|
|
|
elapsedTime += Time.deltaTime;
|
|
|
|
|
yield return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Log the action of picking up the target along with its name.
|
|
|
|
|
ConvaiLogger.DebugLog($"Picking up Target: {target.name}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
|
|
|
|
|
// Retrieve the Animator component from the current NPC.
|
|
|
|
|
Animator animator = _currentNPC.GetComponent<Animator>();
|
|
|
|
|
|
|
|
|
|
// Start the "Picking Up" animation with a cross-fade transition.
|
|
|
|
|
animator.CrossFade(Animator.StringToHash("Picking Up"), 0.1f);
|
|
|
|
|
|
|
|
|
|
// Wait for one second to ensure that the Animator has had time to transition to the "Picking Up" animation state.
|
|
|
|
|
yield return new WaitForSeconds(1);
|
|
|
|
|
|
|
|
|
|
// Define the time it takes for the hand to reach the object in the "Picking Up" animation.
|
|
|
|
|
// This is a specific point in time during the animation that we are interested in.
|
|
|
|
|
float timeToReachObject = 1f;
|
|
|
|
|
|
|
|
|
|
// Wait for the time it takes for the hand to reach the object.
|
|
|
|
|
yield return new WaitForSeconds(timeToReachObject);
|
|
|
|
|
|
|
|
|
|
// Check again if the target is still active before attempting to pick it up.
|
|
|
|
|
if (!target.activeInHierarchy)
|
|
|
|
|
{
|
|
|
|
|
ConvaiLogger.DebugLog(
|
|
|
|
|
$"Target: {target.name} became inactive during the pick up animation! Exiting PickUp coroutine.",
|
|
|
|
|
ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Once the hand has reached the object, set the target's parent to the NPCs' transform,
|
|
|
|
|
// effectively "picking up" the object, and then deactivate the object.
|
|
|
|
|
target.transform.parent = gameObject.transform;
|
|
|
|
|
target.SetActive(false);
|
|
|
|
|
|
|
|
|
|
// Transition back to the "Idle" animation.
|
|
|
|
|
animator.CrossFade(Animator.StringToHash("Idle"), 0.4f);
|
|
|
|
|
|
|
|
|
|
// Invoke the ActionEnded event with the "PickUp" action and the target GameObject.
|
|
|
|
|
ActionEnded?.Invoke("PickUp", target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Drop(GameObject target)
|
|
|
|
|
{
|
|
|
|
|
ActionStarted?.Invoke("Drop", target);
|
|
|
|
|
|
|
|
|
|
if (target == null) return;
|
|
|
|
|
|
|
|
|
|
ConvaiLogger.DebugLog($"Dropping Target: {target.name}", ConvaiLogger.LogCategory.Actions);
|
|
|
|
|
target.transform.parent = null;
|
|
|
|
|
target.SetActive(true);
|
|
|
|
|
|
|
|
|
|
ActionEnded?.Invoke("Drop", target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Jump()
|
|
|
|
|
{
|
|
|
|
|
ActionStarted?.Invoke("Jump", _currentNPC.gameObject);
|
|
|
|
|
|
|
|
|
|
float jumpForce = 5f;
|
|
|
|
|
GetComponent<Rigidbody>().AddForce(new Vector3(0f, jumpForce, 0f), ForceMode.Impulse);
|
|
|
|
|
_currentNPC.GetComponent<Animator>().CrossFade(Animator.StringToHash("Dance"), 1);
|
|
|
|
|
|
|
|
|
|
ActionEnded?.Invoke("Jump", _currentNPC.gameObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// STEP 3: Add the function for your action here.
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|