Initialer Upload neues Unity-Projekt
This commit is contained in:
8
Assets/Convai/Scripts/Editor.meta
Normal file
8
Assets/Convai/Scripts/Editor.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a639f0b163b7a7449151715dc4fa978
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Editor/Character.meta
Normal file
8
Assets/Convai/Scripts/Editor/Character.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95bdc04365ba55b42bf020ffdc22e71f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,367 @@
|
||||
#if READY_PLAYER_ME
|
||||
using ReadyPlayerMe.Core;
|
||||
using ReadyPlayerMe.Core.Editor;
|
||||
#endif
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Convai.Scripts.Runtime.Utils;
|
||||
using Convai.Scripts.Utils;
|
||||
using Convai.Scripts.Utils.LipSync;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Convai.Scripts.Editor.Character
|
||||
{
|
||||
public class ConvaiCharacterImporter : EditorWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// The color palette used for the character text.
|
||||
/// </summary>
|
||||
private static readonly Color[] ColorPalette =
|
||||
{
|
||||
new(1f, 0f, 0f), new(0f, 1f, 0f), new(0f, 0f, 1f),
|
||||
new(1f, 1f, 0f), new(0f, 1f, 1f), new(1f, 0f, 1f),
|
||||
new(1f, 0.5f, 0f), new(0.5f, 0f, 0.5f), new(0f, 0.5f, 0f),
|
||||
new(0.5f, 0.5f, 0.5f), new(1f, 0.8f, 0.6f), new(0.6f, 0.8f, 1f),
|
||||
new(0.8f, 0.6f, 1f), new(1f, 0.6f, 0.8f), new(0.7f, 0.4f, 0f),
|
||||
new(0f, 0.7f, 0.7f), new(0.7f, 0.7f, 0f), new(0f, 0.7f, 0.4f),
|
||||
new(0.7f, 0f, 0.2f), new(0.9f, 0.9f, 0.9f)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates the GUI for the Character Importer window.
|
||||
/// </summary>
|
||||
public void CreateGUI()
|
||||
{
|
||||
VisualElement root = rootVisualElement;
|
||||
|
||||
ScrollView page2 = new();
|
||||
|
||||
root.Add(new Label(""));
|
||||
|
||||
Image convaiLogoImage = new()
|
||||
{
|
||||
image = AssetDatabase.LoadAssetAtPath<Texture>(ConvaiImagesDirectory.CONVAI_LOGO_PATH),
|
||||
style =
|
||||
{
|
||||
height = 100,
|
||||
paddingBottom = 10,
|
||||
paddingTop = 10,
|
||||
paddingRight = 10,
|
||||
paddingLeft = 10
|
||||
}
|
||||
};
|
||||
|
||||
root.Add(convaiLogoImage);
|
||||
|
||||
Label convaiCharacterIDLabel = new("Enter your Character ID: ")
|
||||
{
|
||||
style = { fontSize = 16 }
|
||||
};
|
||||
|
||||
TextField characterIDTextField = new();
|
||||
|
||||
Button downloadButton = new(() => DownloadCharacter(characterIDTextField.text))
|
||||
{
|
||||
text = "Import!",
|
||||
style =
|
||||
{
|
||||
fontSize = 16,
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
alignSelf = Align.Center,
|
||||
paddingBottom = 10,
|
||||
paddingLeft = 30,
|
||||
paddingRight = 30,
|
||||
paddingTop = 10
|
||||
}
|
||||
};
|
||||
|
||||
Button docsLink = new(() => Application.OpenURL(
|
||||
"https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/importing-a-character-from-convai-playground"))
|
||||
{
|
||||
text = "How do I create a character?",
|
||||
style =
|
||||
{
|
||||
alignSelf = Align.Center,
|
||||
paddingBottom = 5,
|
||||
paddingLeft = 50,
|
||||
paddingRight = 50,
|
||||
paddingTop = 5
|
||||
}
|
||||
};
|
||||
|
||||
page2.Add(convaiCharacterIDLabel);
|
||||
page2.Add(new Label(""));
|
||||
page2.Add(characterIDTextField);
|
||||
page2.Add(new Label(""));
|
||||
page2.Add(downloadButton);
|
||||
page2.Add(new Label(""));
|
||||
page2.Add(docsLink);
|
||||
|
||||
page2.style.marginBottom = 20;
|
||||
page2.style.marginLeft = 20;
|
||||
page2.style.marginRight = 20;
|
||||
page2.style.marginTop = 20;
|
||||
|
||||
root.Add(page2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the Character Importer window.
|
||||
/// </summary>
|
||||
[MenuItem("Convai/Character Importer", false, 5)]
|
||||
public static void CharacterImporter()
|
||||
{
|
||||
ConvaiCharacterImporter wnd = GetWindow<ConvaiCharacterImporter>();
|
||||
wnd.titleContent = new GUIContent("Character Importer");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the character from the Convai API and sets up the character in the scene.
|
||||
/// </summary>
|
||||
/// <param name="characterID"> The characterID of the character to download.</param>
|
||||
private async void DownloadCharacter(string characterID)
|
||||
{
|
||||
#if READY_PLAYER_ME
|
||||
|
||||
if (!ConvaiAPIKeySetup.GetAPIKey(out string apiKey)) return;
|
||||
|
||||
|
||||
GetRequest getRequest = new(characterID);
|
||||
string stringGetRequest = JsonConvert.SerializeObject(getRequest);
|
||||
|
||||
WebRequest request = WebRequest.Create("https://api.convai.com/character/get");
|
||||
EditorUtility.DisplayProgressBar("Connecting", "Collecting resources...", 0f);
|
||||
request.Method = "post";
|
||||
request.ContentType = "application/json";
|
||||
request.Headers.Add("CONVAI-API-KEY", apiKey);
|
||||
|
||||
byte[] jsonBytes = Encoding.UTF8.GetBytes(stringGetRequest);
|
||||
await using Stream requestStream = await request.GetRequestStreamAsync();
|
||||
await requestStream.WriteAsync(jsonBytes, 0, jsonBytes.Length);
|
||||
|
||||
try
|
||||
{
|
||||
using HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
|
||||
await using Stream streamResponse = response.GetResponseStream();
|
||||
if (streamResponse == null) return;
|
||||
using StreamReader reader = new(streamResponse);
|
||||
string responseContent = await reader.ReadToEndAsync();
|
||||
GetResponse getResponseContent = JsonConvert.DeserializeObject<GetResponse>(responseContent);
|
||||
string modelLink = getResponseContent.ModelDetail.ModelLink;
|
||||
string characterName = getResponseContent.CharacterName.Trim();
|
||||
|
||||
AvatarObjectLoader avatarLoader = new()
|
||||
{
|
||||
AvatarConfig = Resources.Load<AvatarConfig>("ConvaiRPMAvatarConfig")
|
||||
};
|
||||
|
||||
DirectoryUtility.DefaultAvatarFolder = $"Convai/Characters/Mesh Data/{characterName}";
|
||||
EditorUtility.DisplayProgressBar("Downloading Character", "Initializing download...", 0f);
|
||||
|
||||
avatarLoader.OnProgressChanged += (_, progressArgs) =>
|
||||
EditorUtility.DisplayProgressBar("Downloading Character", $"Downloading character model {characterName}: {progressArgs.Progress * 100f}%",
|
||||
progressArgs.Progress);
|
||||
|
||||
avatarLoader.OnCompleted += (_, args) =>
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
AvatarLoaderSettings avatarLoaderSettings = Resources.Load<AvatarLoaderSettings>("ConvaiAvatarLoaderSettings");
|
||||
string path =
|
||||
$"{DirectoryUtility.GetRelativeProjectPath(args.Avatar.name, AvatarCache.GetAvatarConfigurationHash(avatarLoaderSettings.AvatarConfig))}/{args.Avatar.name}";
|
||||
GameObject avatar = PrefabHelper.CreateAvatarPrefab(args.Metadata, path, avatarConfig: avatarLoaderSettings.AvatarConfig);
|
||||
|
||||
SetupCharacter(characterID, characterName, avatar, args);
|
||||
|
||||
Debug.Log($"Character '{characterName}' downloaded and set up successfully.");
|
||||
};
|
||||
|
||||
avatarLoader.OnFailed += (_, error) =>
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
Debug.LogError($"Failed to download character: {error}");
|
||||
};
|
||||
|
||||
avatarLoader.LoadAvatar(modelLink);
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
Debug.LogError(e.Message + "\nPlease check if Character ID is correct.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
Debug.LogError(e);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if READY_PLAYER_ME
|
||||
/// <summary>
|
||||
/// Sets up the character in the scene with the downloaded character model.
|
||||
/// </summary>
|
||||
/// <param name="characterID"> The character ID.</param>
|
||||
/// <param name="characterName"> The name of the character.</param>
|
||||
/// <param name="avatar"> The avatar GameObject to set up.</param>
|
||||
/// <param name="args"> The completion event arguments.</param>
|
||||
private void SetupCharacter(string characterID, string characterName, GameObject avatar, CompletionEventArgs args)
|
||||
{
|
||||
SetupCharacterMetadata(characterName, avatar);
|
||||
SetupCollision(avatar);
|
||||
avatar.AddComponent<AudioSource>();
|
||||
SetupAnimator(args, avatar);
|
||||
ConvaiNPC convaiNPCComponent = SetupConvaiComponents(characterID, characterName, avatar);
|
||||
SetupLipsync(avatar);
|
||||
SetupChatUI(convaiNPCComponent);
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(avatar, $"Assets/Convai/Characters/Prefabs/{avatar.name}.prefab");
|
||||
DestroyImmediate(args.Avatar, true);
|
||||
Selection.activeObject = avatar;
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
/// <summary>Setups the lipsync.</summary>
|
||||
/// <param name="avatar">The avatar.</param>
|
||||
private static void SetupLipsync(GameObject avatar)
|
||||
{
|
||||
ConvaiLipSync convaiLipSync = avatar.AddComponent<ConvaiLipSync>();
|
||||
convaiLipSync.BlendshapeType = ConvaiLipSync.LipSyncBlendshapeType.ARKit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the metadata for the character.
|
||||
/// </summary>
|
||||
/// <param name="characterName">The name of the character.</param>
|
||||
/// <param name="avatar">The avatar GameObject.</param>
|
||||
private static void SetupCharacterMetadata(string characterName, GameObject avatar)
|
||||
{
|
||||
avatar.tag = "Character";
|
||||
avatar.name = $"Convai NPC {characterName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the chat UI for the character.
|
||||
/// </summary>
|
||||
/// <param name="convaiNPCComponent">The ConvaiNPC component.</param>
|
||||
private void SetupChatUI(ConvaiNPC convaiNPCComponent)
|
||||
{
|
||||
ConvaiChatUIHandler chatUIHandler = FindObjectOfType<ConvaiChatUIHandler>();
|
||||
|
||||
if (chatUIHandler != null && convaiNPCComponent.characterName != null &&
|
||||
!chatUIHandler.HasCharacter(convaiNPCComponent.characterName))
|
||||
{
|
||||
Utils.Character newCharacter = new()
|
||||
{
|
||||
characterGameObject = convaiNPCComponent,
|
||||
characterName = convaiNPCComponent.characterName,
|
||||
CharacterTextColor = GetRandomColor()
|
||||
};
|
||||
chatUIHandler.AddCharacter(newCharacter);
|
||||
EditorUtility.SetDirty(chatUIHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the Convai components for the character.
|
||||
/// </summary>
|
||||
/// <param name="characterID">The character ID.</param>
|
||||
/// <param name="characterName">The name of the character.</param>
|
||||
/// <param name="avatar">The avatar GameObject.</param>
|
||||
/// <returns>The ConvaiNPC component.</returns>
|
||||
private static ConvaiNPC SetupConvaiComponents(string characterID, string characterName, GameObject avatar)
|
||||
{
|
||||
ConvaiNPC convaiNPCComponent = avatar.AddComponent<ConvaiNPC>();
|
||||
convaiNPCComponent.sessionID = "-1";
|
||||
convaiNPCComponent.characterID = characterID;
|
||||
convaiNPCComponent.characterName = characterName;
|
||||
|
||||
avatar.AddComponent<ConvaiHeadTracking>();
|
||||
|
||||
|
||||
return convaiNPCComponent;
|
||||
}
|
||||
|
||||
#if READY_PLAYER_ME
|
||||
/// <summary>
|
||||
/// Sets up the animator for the character.
|
||||
/// </summary>
|
||||
/// <param name="args">The completion event arguments.</param>
|
||||
/// <param name="avatar">The avatar GameObject.</param>
|
||||
private static void SetupAnimator(CompletionEventArgs args, GameObject avatar)
|
||||
{
|
||||
AvatarAnimationHelper.SetupAnimator(args.Metadata, avatar);
|
||||
Animator animator = avatar.GetComponent<Animator>();
|
||||
// Determine avatar type based on Avatar field in Animator component
|
||||
bool isMasculine = animator.avatar.name.Contains("Masculine");
|
||||
|
||||
// Set the appropriate animator controller
|
||||
string animatorPath = isMasculine ? "Masculine NPC Animator" : "Feminine NPC Animator";
|
||||
animator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>(animatorPath);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the collision for the character.
|
||||
/// </summary>
|
||||
/// <param name="avatar">The avatar GameObject.</param>
|
||||
private static void SetupCollision(GameObject avatar)
|
||||
{
|
||||
CapsuleCollider capsuleColliderComponent = avatar.AddComponent<CapsuleCollider>();
|
||||
capsuleColliderComponent.center = new Vector3(0, 0.9f, 0);
|
||||
capsuleColliderComponent.radius = 0.3f;
|
||||
capsuleColliderComponent.height = 1.8f;
|
||||
capsuleColliderComponent.isTrigger = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a random color from the predefined palette.
|
||||
/// </summary>
|
||||
/// <returns>A random color from the predefined palette.</returns>
|
||||
private Color GetRandomColor()
|
||||
{
|
||||
return ColorPalette[Random.Range(0, ColorPalette.Length)];
|
||||
}
|
||||
|
||||
private class GetRequest
|
||||
{
|
||||
[JsonProperty("charID")] public string CharID;
|
||||
|
||||
public GetRequest(string charID)
|
||||
{
|
||||
CharID = charID;
|
||||
}
|
||||
}
|
||||
|
||||
private class GetResponse
|
||||
{
|
||||
[JsonProperty("backstory")] public string Backstory;
|
||||
[JsonProperty("character_actions")] public string[] CharacterActions;
|
||||
[JsonProperty("character_emotions")] public string[] CharacterEmotions;
|
||||
[JsonProperty("character_id")] public string CharacterID;
|
||||
[JsonProperty("character_name")] public string CharacterName;
|
||||
[JsonProperty("model_details")] public ModelDetails ModelDetail;
|
||||
[JsonProperty("timestamp")] public string Timestamp;
|
||||
[JsonProperty("user_id")] public string UserID;
|
||||
[JsonProperty("voice_type")] public string VoiceType;
|
||||
|
||||
#region Nested type: ModelDetails
|
||||
|
||||
internal class ModelDetails
|
||||
{
|
||||
[JsonProperty("modelLink")] public string ModelLink;
|
||||
[JsonProperty("modelPlaceholder")] public string ModelPlaceholder;
|
||||
[JsonProperty("modelType")] public string ModelType;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 387cdd849d1d17342bd74800041fbc7f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Editor/Custom Package.meta
Normal file
8
Assets/Convai/Scripts/Editor/Custom Package.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 597bd5d03de1f16428d8240a3cb00d23
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,588 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
|
||||
namespace Convai.Scripts.Editor.Custom_Package
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom package installer for Convai's Custom Packages in Unity Editor.
|
||||
/// </summary>
|
||||
public class ConvaiCustomPackageInstaller : EditorWindow, IActiveBuildTargetChanged
|
||||
{
|
||||
// Enum to represent different setup types
|
||||
private enum SetupTypes
|
||||
{
|
||||
None,
|
||||
ARAndroid,
|
||||
ARiOS,
|
||||
VR,
|
||||
Uninstaller
|
||||
}
|
||||
|
||||
// Paths to different Convai packages
|
||||
private const string AR_PACKAGE_PATH = "Assets/Convai/Custom Packages/ConvaiARUpgrader.unitypackage";
|
||||
private const string IOS_BUILD_PACKAGE_PATH = "Assets/Convai/Custom Packages/ConvaiiOSBuild.unitypackage";
|
||||
private const string TMP_PACKAGE_PATH = "Assets/Convai/Custom Packages/ConvaiCustomTMP.unitypackage";
|
||||
private const string URP_CONVERTER_PACKAGE_PATH = "Assets/Convai/Custom Packages/ConvaiURPConverter.unitypackage";
|
||||
private const string VR_PACKAGE_PATH = "Assets/Convai/Custom Packages/ConvaiVRUpgrader.unitypackage";
|
||||
|
||||
// Index to keep track of the current package installation step
|
||||
private int _currentPackageInstallIndex;
|
||||
|
||||
// Current setup type
|
||||
private SetupTypes _currentSetup;
|
||||
|
||||
// Request object for package installations/uninstallations
|
||||
private Request _request;
|
||||
|
||||
/// <summary>
|
||||
/// GUI method to display the window and buttons.
|
||||
/// </summary>
|
||||
private void OnGUI()
|
||||
{
|
||||
// Loading Convai logo
|
||||
Texture2D convaiLogo = AssetDatabase.LoadAssetAtPath<Texture2D>(ConvaiImagesDirectory.CONVAI_LOGO_PATH);
|
||||
GUI.DrawTexture(new Rect(115, 0, 256, 80), convaiLogo);
|
||||
|
||||
GUILayout.BeginArea(new Rect(165, 100, Screen.width, Screen.height));
|
||||
GUILayout.BeginVertical();
|
||||
|
||||
// Button to install AR package
|
||||
if (GUILayout.Button("Install AR Package", GUILayout.Width(170), GUILayout.Height(30)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Which Platform",
|
||||
"Which platform do you want to install AR package for?", "Android", "iOS"))
|
||||
{
|
||||
// Display confirmation dialog before installation
|
||||
if (EditorUtility.DisplayDialog("Confirm Android AR Package Installation",
|
||||
"This step will install AR-related packages and integrate Convai's AR package into your project. " +
|
||||
"This process will affect your project. Do you want to proceed?\n\n" +
|
||||
"The following operations will be performed:\n" +
|
||||
"- Universal Render Pipeline (URP)\n" +
|
||||
"- ARCore Plugin\n" +
|
||||
"- Convai Custom AR Package\n" +
|
||||
"- Convai URP Converter\n\n" +
|
||||
"* If these packages are not present in your project, they will be installed.\n" +
|
||||
"* If the target build platform is not Android, it will be switched to Android.", "Yes, Proceed", "No, Cancel"))
|
||||
{
|
||||
EditorApplication.LockReloadAssemblies();
|
||||
StartPackageInstallation(SetupTypes.ARAndroid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Display confirmation dialog before installation
|
||||
if (EditorUtility.DisplayDialog("Confirm iOS AR Package Installation",
|
||||
"This step will install AR-related packages and integrate Convai's AR package into your project. " +
|
||||
"This process will affect your project. Do you want to proceed?\n\n" +
|
||||
"The following operations will be performed:\n" +
|
||||
"- Universal Render Pipeline (URP)\n" +
|
||||
"- ARKit Plugin\n" +
|
||||
"- Convai Custom AR Package\n" +
|
||||
"- Convai URP Converter\n\n" +
|
||||
"* If these packages are not present in your project, they will be installed.\n" +
|
||||
"* If the target build platform is not iOS, it will be switched to iOS.", "Yes, Proceed", "No, Cancel"))
|
||||
{
|
||||
EditorApplication.LockReloadAssemblies();
|
||||
StartPackageInstallation(SetupTypes.ARiOS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Button to install VR package
|
||||
if (GUILayout.Button("Install VR Package", GUILayout.Width(170), GUILayout.Height(30)))
|
||||
// Display confirmation dialog before installation
|
||||
if (EditorUtility.DisplayDialog("Confirm VR Package Installation",
|
||||
"This step will install VR-related packages and integrate Convai's VR package into your project. " +
|
||||
"This process will affect your project. Do you want to proceed?\n\n" +
|
||||
"The following operations will be performed:\n" +
|
||||
"- Universal Render Pipeline (URP)\n" +
|
||||
"- OpenXR Plugin\n" +
|
||||
"- XR Interaction Toolkit\n" +
|
||||
"- Convai Custom VR Package\n" +
|
||||
"- Convai URP Converter\n\n" +
|
||||
"* If these packages are not present in your project, they will be installed.\n" +
|
||||
"* If the target build platform is not Android, it will be switched to Android.", "Yes, Proceed", "No, Cancel"))
|
||||
{
|
||||
EditorApplication.LockReloadAssemblies();
|
||||
StartPackageInstallation(SetupTypes.VR);
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Button to uninstall XR package
|
||||
if (GUILayout.Button("Uninstall XR Package", GUILayout.Width(170), GUILayout.Height(30)))
|
||||
// Display confirmation dialog before uninstallation
|
||||
if (EditorUtility.DisplayDialog("Confirm Package Uninstallation",
|
||||
"This process will uninstall the Convai package and revert changes made by AR or VR setups in your project. " +
|
||||
"It may affect your project. Are you sure you want to proceed?\n\n" +
|
||||
"The following packages will be uninstalled.\n" +
|
||||
"- ARCore Plugin or ARKit\n" +
|
||||
"- OpenXR Plugin\n" +
|
||||
"- XR Interaction Toolkit\n" +
|
||||
"- Convai Custom AR or VR Package\n\n" +
|
||||
"* The Convai Uninstaller Package will be installed. This process will revert scripts modified for XR to their default states.",
|
||||
"Yes, Uninstall", "No, Cancel"))
|
||||
{
|
||||
_currentSetup = SetupTypes.Uninstaller;
|
||||
EditorApplication.update += Progress;
|
||||
EditorApplication.LockReloadAssemblies();
|
||||
HandleUninstallPackage();
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (GUILayout.Button("Install iOS Build Package", GUILayout.Width(170), GUILayout.Height(30)))
|
||||
{
|
||||
InstallConvaiUnityPackage(IOS_BUILD_PACKAGE_PATH);
|
||||
TryToDownloadiOSDLL();
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (GUILayout.Button("Install URP Converter", GUILayout.Width(170), GUILayout.Height(30))) InstallConvaiUnityPackage(URP_CONVERTER_PACKAGE_PATH);
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (GUILayout.Button("Install TMP Package", GUILayout.Width(170), GUILayout.Height(30))) InstallConvaiUnityPackage(TMP_PACKAGE_PATH);
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
// IActiveBuildTargetChanged callback
|
||||
public int callbackOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the active build target is changed.
|
||||
/// </summary>
|
||||
/// <param name="previousTarget">The previous build target.</param>
|
||||
/// <param name="newTarget">The new build target.</param>
|
||||
public void OnActiveBuildTargetChanged(BuildTarget previousTarget, BuildTarget newTarget)
|
||||
{
|
||||
// Check if the new build target is iOS and trigger the download of iOS DLL.
|
||||
if (newTarget == BuildTarget.iOS) TryToDownloadiOSDLL();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the Convai Custom Package Installer window.
|
||||
/// </summary>
|
||||
[MenuItem("Convai/Custom Package Installer", false, 10)]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
ConvaiCustomPackageInstaller window = GetWindow<ConvaiCustomPackageInstaller>("Convai Custom Package Installer", true);
|
||||
window.minSize = new Vector2(500, 370);
|
||||
window.maxSize = window.minSize;
|
||||
window.titleContent.text = "Custom Package Installer";
|
||||
window.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Progress method to handle the installation/uninstallation progress.
|
||||
/// </summary>
|
||||
private void Progress()
|
||||
{
|
||||
Debug.Log("<color=cyan>Process in progress... Please wait.</color>");
|
||||
|
||||
// Check if the request object is initialized
|
||||
if (_request == null) return;
|
||||
if (_request.IsCompleted)
|
||||
{
|
||||
switch (_request.Status)
|
||||
{
|
||||
case StatusCode.InProgress:
|
||||
// Do nothing while the request is still in progress
|
||||
break;
|
||||
case StatusCode.Success:
|
||||
// Handle the successful completion of the package request
|
||||
HandlePackageRequest();
|
||||
break;
|
||||
case StatusCode.Failure:
|
||||
// Log an error message in case of failure
|
||||
Debug.LogError("Error: " + _request.Error.message);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the Progress method from the update event
|
||||
EditorApplication.UnlockReloadAssemblies();
|
||||
EditorApplication.update -= Progress;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to handle the completion of the package request.
|
||||
/// </summary>
|
||||
private void HandlePackageRequest()
|
||||
{
|
||||
switch (_currentSetup)
|
||||
{
|
||||
case SetupTypes.None:
|
||||
// Do nothing for SetupTypes.None
|
||||
break;
|
||||
case SetupTypes.ARiOS:
|
||||
// Handle iOS AR package installation
|
||||
HandleARPackageInstall();
|
||||
Debug.Log("<color=lime>The request for package installation from the Package Manager has been successfully completed.</color>");
|
||||
break;
|
||||
case SetupTypes.ARAndroid:
|
||||
// Handle Android AR package installation
|
||||
HandleARPackageInstall();
|
||||
Debug.Log("<color=lime>The request for package installation from the Package Manager has been successfully completed.</color>");
|
||||
break;
|
||||
case SetupTypes.VR:
|
||||
// Handle VR package installation
|
||||
HandleVRPackageInstall();
|
||||
Debug.Log("<color=lime>The request for package installation from the Package Manager has been successfully completed.</color>");
|
||||
break;
|
||||
case SetupTypes.Uninstaller:
|
||||
// Handle uninstallation package completion
|
||||
HandleUninstallPackage();
|
||||
Debug.Log("<color=lime>The request for package uninstallation from the Package Manager has been successfully completed.</color>");
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the Progress method back to the update event
|
||||
EditorApplication.update += Progress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to handle the uninstallation of packages.
|
||||
/// </summary>
|
||||
private void HandleUninstallPackage()
|
||||
{
|
||||
// Check if the request object is not initialized
|
||||
if (_request == null)
|
||||
{
|
||||
// Define asset paths to delete
|
||||
string[] deleteAssetPaths =
|
||||
{
|
||||
"Assets/Samples",
|
||||
"Assets/Convai/ConvaiAR",
|
||||
"Assets/Convai/ConvaiVR",
|
||||
"Assets/XR",
|
||||
"Assets/XRI"
|
||||
};
|
||||
|
||||
List<string> outFailedPaths = new();
|
||||
// Delete specified asset paths
|
||||
AssetDatabase.DeleteAssets(deleteAssetPaths, outFailedPaths);
|
||||
|
||||
// Log errors if any deletion fails
|
||||
if (outFailedPaths.Count > 0)
|
||||
foreach (string failedPath in outFailedPaths)
|
||||
Debug.LogError("Failed to delete : " + failedPath);
|
||||
}
|
||||
|
||||
// Define package names for uninstallation
|
||||
string ARCorePackageName = "com.unity.xr.arcore";
|
||||
string ARKitPackageName = "com.unity.xr.arkit";
|
||||
string OpenXRPackageName = "com.unity.xr.openxr";
|
||||
string XRInteractionToolkitPackageName = "com.unity.xr.interaction.toolkit";
|
||||
|
||||
// Check if ARCore is installed and initiate removal
|
||||
if (IsPackageInstalled(ARCorePackageName)) _request = Client.Remove(ARCorePackageName);
|
||||
|
||||
// Check if ARKit is installed and initiate removal
|
||||
if (IsPackageInstalled(ARKitPackageName))
|
||||
{
|
||||
_request = Client.Remove(ARKitPackageName);
|
||||
}
|
||||
// Check if OpenXR is installed and initiate removal
|
||||
else if (IsPackageInstalled(OpenXRPackageName))
|
||||
{
|
||||
_request = Client.Remove(OpenXRPackageName);
|
||||
}
|
||||
// Check if XR Interaction Toolkit is installed and initiate removal
|
||||
else if (IsPackageInstalled(XRInteractionToolkitPackageName))
|
||||
{
|
||||
_request = Client.Remove(XRInteractionToolkitPackageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop the update event if the request is not initialized
|
||||
EditorApplication.update -= Progress;
|
||||
EditorApplication.UnlockReloadAssemblies();
|
||||
}
|
||||
|
||||
// Remove the Progress method from the update event if the request is not initialized
|
||||
if (_request == null) EditorApplication.update -= Progress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to start the installation of a specific package setup.
|
||||
/// </summary>
|
||||
private void StartPackageInstallation(SetupTypes setupType)
|
||||
{
|
||||
// Log a message indicating the start of the package installation
|
||||
Debug.Log($"<color=cyan>Installation of {setupType} package has started... This process may take 3-5 minutes.</color>");
|
||||
|
||||
// Warn the user about the possibility of 'Failed to Resolve Packages' error
|
||||
Debug.LogWarning("<color=yellow>If you encounter with 'Failed to Resolve Packages' error, there's no need to be concerned.</color>");
|
||||
|
||||
// Reset the package installation index
|
||||
_currentPackageInstallIndex = 0;
|
||||
|
||||
// Set the current setup type
|
||||
_currentSetup = setupType;
|
||||
// Initialize the Universal Render Pipeline (URP) setup
|
||||
InitializeURPSetup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to handle the installation of AR-related packages.
|
||||
/// </summary>
|
||||
private void HandleARPackageInstall()
|
||||
{
|
||||
// Check the current package installation index
|
||||
if (_currentPackageInstallIndex == 0)
|
||||
{
|
||||
switch (_currentSetup)
|
||||
{
|
||||
case SetupTypes.ARAndroid:
|
||||
// Initialize the ARCore setup
|
||||
InitializeARCoreSetup();
|
||||
break;
|
||||
case SetupTypes.ARiOS:
|
||||
// Initialize the ARKit setup
|
||||
InitializeARKitSetup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Install AR-related packages and perform necessary setup
|
||||
InstallConvaiUnityPackage(AR_PACKAGE_PATH);
|
||||
InstallConvaiUnityPackage(URP_CONVERTER_PACKAGE_PATH);
|
||||
switch (_currentSetup)
|
||||
{
|
||||
case SetupTypes.ARAndroid:
|
||||
TryToChangeEditorBuildTargetToAndroid();
|
||||
break;
|
||||
case SetupTypes.ARiOS:
|
||||
TryToChangeEditorBuildTargetToiOS();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to handle the installation of VR-related packages.
|
||||
/// </summary>
|
||||
private void HandleVRPackageInstall()
|
||||
{
|
||||
// Check the current package installation index
|
||||
if (_currentPackageInstallIndex == 0)
|
||||
{
|
||||
// Initialize the OpenXR setup
|
||||
InitializeOpenXRSetup();
|
||||
}
|
||||
else if (_currentPackageInstallIndex == 1)
|
||||
{
|
||||
// Initialize the XR Interaction Toolkit setup
|
||||
InitializeXRInteractionToolkitSetup();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Install VR-related packages and perform necessary setup
|
||||
InstallConvaiUnityPackage(VR_PACKAGE_PATH);
|
||||
InstallConvaiUnityPackage(URP_CONVERTER_PACKAGE_PATH);
|
||||
TryToChangeEditorBuildTargetToAndroid();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to initialize the URP setup.
|
||||
/// </summary>
|
||||
private void InitializeURPSetup()
|
||||
{
|
||||
// Define the URP package name
|
||||
const string URPPackageName = "com.unity.render-pipelines.universal@14.0.11";
|
||||
|
||||
// Check if the URP package is already installed
|
||||
if (IsPackageInstalled(URPPackageName))
|
||||
{
|
||||
// If installed, handle the successful package request
|
||||
HandlePackageRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not installed, send a request to the Package Manager to add the URP package
|
||||
_request = Client.Add(URPPackageName);
|
||||
Debug.Log($"<color=orange>{URPPackageName} Package Installation Request Sent to Package Manager.</color>");
|
||||
|
||||
// Add the Progress method to the update event to monitor the installation progress
|
||||
EditorApplication.update += Progress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to initialize the ARCore setup.
|
||||
/// </summary>
|
||||
private void InitializeARCoreSetup()
|
||||
{
|
||||
// Set the current package installation index for ARCore
|
||||
_currentPackageInstallIndex = 1;
|
||||
|
||||
// Define the ARCore package name
|
||||
string ARCorePackageName = "com.unity.xr.arcore@5.1.4";
|
||||
|
||||
// Check if the ARCore package is already installed
|
||||
if (IsPackageInstalled(ARCorePackageName))
|
||||
{
|
||||
// If installed, handle the AR package installation
|
||||
HandleARPackageInstall();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not installed, send a request to the Package Manager to add the ARCore package
|
||||
_request = Client.Add(ARCorePackageName);
|
||||
Debug.Log($"<color=orange>{ARCorePackageName} Package Installation Request sent to Package Manager.</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to initialize the ARKit setup.
|
||||
/// </summary>
|
||||
private void InitializeARKitSetup()
|
||||
{
|
||||
// Set the current package installation index for AR Setup
|
||||
_currentPackageInstallIndex = 1;
|
||||
|
||||
// Define the ARKit package name
|
||||
string ARKitPackageName = "com.unity.xr.arkit@5.1.4";
|
||||
|
||||
// Check if the ARKit package is already installed
|
||||
if (IsPackageInstalled(ARKitPackageName))
|
||||
{
|
||||
// If installed, handle the AR package installation
|
||||
HandleARPackageInstall();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not installed, send a request to the Package Manager to add the ARKit package
|
||||
_request = Client.Add(ARKitPackageName);
|
||||
Debug.Log($"<color=orange>{ARKitPackageName} Package Installation Request sent to Package Manager.</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to initialize the OpenXR setup.
|
||||
/// </summary>
|
||||
private void InitializeOpenXRSetup()
|
||||
{
|
||||
// Set the current package installation index for OpenXR
|
||||
_currentPackageInstallIndex = 1;
|
||||
|
||||
// Define the OpenXR package name
|
||||
string OpenXRPackageName = "com.unity.xr.openxr@1.10.0";
|
||||
|
||||
// Check if the OpenXR package is already installed
|
||||
if (IsPackageInstalled(OpenXRPackageName))
|
||||
{
|
||||
// If installed, handle the VR package installation
|
||||
HandleVRPackageInstall();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not installed, send a request to the Package Manager to add the OpenXR package
|
||||
_request = Client.Add(OpenXRPackageName);
|
||||
Debug.Log($"<color=orange>{OpenXRPackageName} Package Installation Request sent to Package Manager.</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to initialize the XR Interaction Toolkit setup.
|
||||
/// </summary>
|
||||
private void InitializeXRInteractionToolkitSetup()
|
||||
{
|
||||
// Set the current package installation index for XR Interaction Toolkit
|
||||
_currentPackageInstallIndex = 2;
|
||||
|
||||
// Define the XR Interaction Toolkit package name
|
||||
string XRInteractionToolkitPackageName = "com.unity.xr.interaction.toolkit@2.5.4";
|
||||
|
||||
// Check if the XR Interaction Toolkit package is already installed
|
||||
if (IsPackageInstalled(XRInteractionToolkitPackageName))
|
||||
{
|
||||
// If installed, handle the VR package installation
|
||||
HandleVRPackageInstall();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not installed, send a request to the Package Manager to add the XR Interaction Toolkit package
|
||||
_request = Client.Add(XRInteractionToolkitPackageName);
|
||||
Debug.Log($"<color=orange>{XRInteractionToolkitPackageName} Package Installation Request sent to Package Manager.</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to install a custom Convai Unity package.
|
||||
/// </summary>
|
||||
private void InstallConvaiUnityPackage(string packagePath)
|
||||
{
|
||||
// Import the Unity package
|
||||
AssetDatabase.ImportPackage(packagePath, false);
|
||||
|
||||
// Get the package name without extension
|
||||
string packageName = Path.GetFileNameWithoutExtension(packagePath);
|
||||
Debug.Log($"<color=lime>{packageName} Custom Unity Package Installation Completed.</color>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to check if a package is already installed.
|
||||
/// </summary>
|
||||
private bool IsPackageInstalled(string packageName)
|
||||
{
|
||||
// Iterate through all registered packages
|
||||
foreach (PackageInfo packageInfo in PackageInfo.GetAllRegisteredPackages())
|
||||
// Check if the package name matches
|
||||
if (packageInfo.name == packageName)
|
||||
// Return true if the package is installed
|
||||
return true;
|
||||
|
||||
// Return false if the package is not installed
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try changing the editor build target to Android.
|
||||
/// </summary>
|
||||
private void TryToChangeEditorBuildTargetToAndroid()
|
||||
{
|
||||
// Check if the current build target is not Android
|
||||
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android)
|
||||
{
|
||||
// Switch the active build target to Android
|
||||
EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.Android, BuildTarget.Android);
|
||||
Debug.Log("<color=lime>Build Target Platform is being Changed to Android...</color>");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try changing the editor build target to iOS.
|
||||
/// </summary>
|
||||
private void TryToChangeEditorBuildTargetToiOS()
|
||||
{
|
||||
// Check if the current build target is not iOS
|
||||
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.iOS)
|
||||
{
|
||||
// Switch the active build target to iOS
|
||||
EditorUserBuildSettings.SwitchActiveBuildTargetAsync(BuildTargetGroup.iOS, BuildTarget.iOS);
|
||||
Debug.Log("<color=lime>Build Target Platform is being Changed to iOS...</color>");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to download the iOS DLL using the IOSDLLDownloader class.
|
||||
/// </summary>
|
||||
private void TryToDownloadiOSDLL()
|
||||
{
|
||||
// Call the TryToDownload method from the IOSDLLDownloader class.
|
||||
iOSDLLDownloader.TryToDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53fd043db40ab5b4798e83e7f3204952
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
173
Assets/Convai/Scripts/Editor/Custom Package/IOSDLLDownloader.cs
Normal file
173
Assets/Convai/Scripts/Editor/Custom Package/IOSDLLDownloader.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using Convai.Scripts.Runtime.Utils;
|
||||
using Convai.Scripts.Utils;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace Convai.Scripts.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window for downloading and extracting iOS DLL from a specified URL.
|
||||
/// </summary>
|
||||
public class iOSDLLDownloader
|
||||
{
|
||||
private const string DOWNLOAD_ENDPOINT_URL = "https://api.convai.com/user/downloadAsset";
|
||||
private const string RELATIVE_PATH = "Convai/Plugins/Grpc.Core/runtimes";
|
||||
private static string _targetDirectory;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to download the iOS DLL if it doesn't already exist.
|
||||
/// </summary>
|
||||
public static void TryToDownload()
|
||||
{
|
||||
if (CheckFileExistence()) return;
|
||||
Debug.Log("<color=lime>The iOS DLL download has started...</color>");
|
||||
DownloadAndExtract(GetTargetDirectory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine to download and extract the ZIP file from the specified URL.
|
||||
/// </summary>
|
||||
/// <param name="url">URL of the ZIP file to download.</param>
|
||||
/// <param name="outputPath">Directory to extract the contents to.</param>
|
||||
/// <returns></returns>
|
||||
private static void DownloadAndExtract(string outputPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
string downloadURL = GetDownloadURL();
|
||||
|
||||
if (downloadURL == null) Debug.LogError("Failed to get download URL. Please check the API key and try again.");
|
||||
|
||||
using UnityWebRequest webRequest = UnityWebRequest.Get(downloadURL);
|
||||
webRequest.SendWebRequest();
|
||||
|
||||
while (!webRequest.isDone)
|
||||
{
|
||||
float progress = webRequest.downloadProgress;
|
||||
EditorUtility.DisplayProgressBar("Downloading required iOS DLL...",
|
||||
"Please wait for the download to finish and do not close Unity. " + (int)(progress * 100) + "%", progress);
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
|
||||
if (webRequest.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError)
|
||||
{
|
||||
Debug.LogError($"Error downloading file: {webRequest.error}");
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] results = webRequest.downloadHandler.data;
|
||||
string zipPath = Path.Combine(Path.GetTempPath(), "downloaded.zip");
|
||||
File.WriteAllBytes(zipPath, results);
|
||||
ExtractZipFile(zipPath, outputPath);
|
||||
File.Delete(zipPath);
|
||||
Debug.Log($"Downloaded and extracted to {outputPath}" + "/ios/libgrpc.a");
|
||||
|
||||
// Refresh the asset database to make sure the new files are recognized
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Log(e.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the download URL from Convai API.
|
||||
/// </summary>
|
||||
/// <returns>The download URL or null.</returns>
|
||||
private static string GetDownloadURL()
|
||||
{
|
||||
if(!ConvaiAPIKeySetup.GetAPIKey(out string apiKey)) return null;
|
||||
|
||||
string body = @"{""service_name"": ""unity-builds"",""version"":""ios""}";
|
||||
|
||||
WebRequest request = WebRequest.Create(DOWNLOAD_ENDPOINT_URL);
|
||||
request.Method = "POST";
|
||||
request.ContentType = "application/json";
|
||||
request.Headers.Add("CONVAI-API-KEY", apiKey);
|
||||
|
||||
using (StreamWriter streamWriter = new StreamWriter(request.GetRequestStream()))
|
||||
{
|
||||
streamWriter.Write(body);
|
||||
}
|
||||
|
||||
using (WebResponse response = request.GetResponse())
|
||||
using (Stream dataStream = response.GetResponseStream())
|
||||
using (StreamReader reader = new(dataStream))
|
||||
{
|
||||
JObject responseJson = JObject.Parse(reader.ReadToEnd());
|
||||
return (string)responseJson["download_link"];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the contents of a ZIP file to the specified output folder.
|
||||
/// </summary>
|
||||
/// <param name="zipFilePath">Path to the ZIP file.</param>
|
||||
/// <param name="outputFolder">Directory to extract the contents to.</param>
|
||||
private static void ExtractZipFile(string zipFilePath, string outputFolder)
|
||||
{
|
||||
if (!Directory.Exists(outputFolder)) Directory.CreateDirectory(outputFolder);
|
||||
|
||||
using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
|
||||
{
|
||||
float totalEntries = archive.Entries.Count;
|
||||
float currentEntry = 0;
|
||||
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
string fullPath = Path.Combine(outputFolder, entry.FullName);
|
||||
|
||||
// Ensure the directory exists
|
||||
string directoryName = Path.GetDirectoryName(fullPath);
|
||||
if (!Directory.Exists(directoryName))
|
||||
if (directoryName != null)
|
||||
Directory.CreateDirectory(directoryName);
|
||||
|
||||
// Extract the entry to the output directory
|
||||
entry.ExtractToFile(fullPath, true);
|
||||
|
||||
// Update the progress bar
|
||||
currentEntry++;
|
||||
float progress = currentEntry / totalEntries;
|
||||
EditorUtility.DisplayProgressBar("Extracting", $"Extracting file {entry.Name}...", progress);
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target directory for extracting the files.
|
||||
/// </summary>
|
||||
/// <returns>Target directory path.</returns>
|
||||
private static string GetTargetDirectory()
|
||||
{
|
||||
_targetDirectory = Path.Combine(Application.dataPath, RELATIVE_PATH);
|
||||
if (!Directory.Exists(_targetDirectory)) Directory.CreateDirectory(_targetDirectory);
|
||||
return _targetDirectory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the iOS DLL file already exists.
|
||||
/// </summary>
|
||||
/// <returns>True if the file exists, otherwise false.</returns>
|
||||
private static bool CheckFileExistence()
|
||||
{
|
||||
string fullPath = Path.Combine(Application.dataPath, RELATIVE_PATH + "/ios/libgrpc.a");
|
||||
bool fileExists = File.Exists(fullPath);
|
||||
if (fileExists) Debug.Log("<color=orange>iOS DLL already exists. No need to download.</color>");
|
||||
|
||||
return fileExists;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 526eafca5259e824591be577e0305054
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,47 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEditor.PackageManager;
|
||||
#if !READY_PLAYER_ME
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
#endif
|
||||
|
||||
|
||||
namespace Convai.Scripts.Editor.Custom_Package
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
public class ReadyPlayerMeImporter
|
||||
{
|
||||
static AddRequest _request;
|
||||
static ReadyPlayerMeImporter()
|
||||
{
|
||||
#if !READY_PLAYER_ME
|
||||
Debug.Log("Ready Player Me is not installed, importing it");
|
||||
_request = Client.Add("https://github.com/readyplayerme/rpm-unity-sdk-core.git");
|
||||
EditorUtility.DisplayProgressBar("Importing Ready Player Me", "Importing.....", Random.Range(0,1f));
|
||||
EditorApplication.update += UnityEditorUpdateCallback;
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !READY_PLAYER_ME
|
||||
private static void UnityEditorUpdateCallback()
|
||||
{
|
||||
if (_request == null) return;
|
||||
if (!_request.IsCompleted) return;
|
||||
switch (_request.Status)
|
||||
{
|
||||
case StatusCode.Success:
|
||||
Debug.Log($"Successfully installed: {_request.Result.name}");
|
||||
break;
|
||||
case StatusCode.Failure:
|
||||
Debug.Log($"Failure: {_request.Error.message}");
|
||||
break;
|
||||
}
|
||||
EditorApplication.update -= UnityEditorUpdateCallback;
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2854596b13f48fa428a24015c498a0bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Editor/Logger.meta
Normal file
8
Assets/Convai/Scripts/Editor/Logger.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f95efface998ad4190a0c4b87fe42d4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
211
Assets/Convai/Scripts/Editor/Logger/LoggerSettingsManager.cs
Normal file
211
Assets/Convai/Scripts/Editor/Logger/LoggerSettingsManager.cs
Normal file
@ -0,0 +1,211 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Convai.Scripts.Utils;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Logger = Convai.Scripts.Utils.Logger;
|
||||
|
||||
namespace Convai.Scripts.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the settings for the Logger, including loading, creating, and modifying LoggerSettings.
|
||||
/// </summary>
|
||||
public class LoggerSettingsManager
|
||||
{
|
||||
// Path to the LoggerSettings asset
|
||||
private const string SETTINGS_PATH = "Assets/Convai/Resources/LoggerSettings.asset";
|
||||
|
||||
/// <summary>
|
||||
/// Mapping between the row names and the field names in the LoggerSettings class.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> CategoryMapping = new()
|
||||
{
|
||||
{ "Character", "characterResponse" },
|
||||
{ "LipSync", "lipSync" },
|
||||
{ "Actions", "actions" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The LoggerSettings instance.
|
||||
/// </summary>
|
||||
private LoggerSettings _loggerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Property accessor for _loggerSettings. If _loggerSettings is null, it attempts to load it from the asset path.
|
||||
/// If the asset does not exist, it creates a new LoggerSettings instance.
|
||||
/// </summary>
|
||||
public LoggerSettings loggerSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_loggerSettings == null)
|
||||
{
|
||||
_loggerSettings = AssetDatabase.LoadAssetAtPath<LoggerSettings>(SETTINGS_PATH);
|
||||
if (_loggerSettings == null)
|
||||
{
|
||||
CreateLoggerSettings();
|
||||
Logger.Warn("LoggerSettings ScriptableObject not found. Creating one...",
|
||||
Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
return _loggerSettings;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new LoggerSettings instance with default values and saves it as an asset
|
||||
/// </summary>
|
||||
private void CreateLoggerSettings()
|
||||
{
|
||||
_loggerSettings = ScriptableObject.CreateInstance<LoggerSettings>();
|
||||
|
||||
// Set default values for Character
|
||||
_loggerSettings.characterResponseDebug = true;
|
||||
_loggerSettings.characterResponseInfo = true;
|
||||
_loggerSettings.characterResponseWarning = true;
|
||||
_loggerSettings.characterResponseError = true;
|
||||
_loggerSettings.characterResponseException = true;
|
||||
|
||||
// Set default values for LipSync
|
||||
_loggerSettings.lipSyncDebug = true;
|
||||
_loggerSettings.lipSyncInfo = true;
|
||||
_loggerSettings.lipSyncWarning = true;
|
||||
_loggerSettings.lipSyncError = true;
|
||||
_loggerSettings.lipSyncException = true;
|
||||
|
||||
// Set default values for Actions
|
||||
_loggerSettings.actionsDebug = true;
|
||||
_loggerSettings.actionsInfo = true;
|
||||
_loggerSettings.actionsWarning = true;
|
||||
_loggerSettings.actionsError = true;
|
||||
_loggerSettings.actionsException = true;
|
||||
|
||||
// Check if the Convai folder exists and create if not
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Convai/Resources"))
|
||||
AssetDatabase.CreateFolder("Assets/Convai", "Resources");
|
||||
|
||||
// Check if the Settings folder exists and create if not
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Convai/Resources/Settings"))
|
||||
AssetDatabase.CreateFolder("Assets/Convai/Resources", "Settings");
|
||||
|
||||
AssetDatabase.CreateAsset(_loggerSettings, SETTINGS_PATH);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all flags for a given row are set.
|
||||
/// </summary>
|
||||
/// <param name="rowName">The name of the row to check.</param>
|
||||
/// <returns>True if all flags for the given row are set, false otherwise.</returns>
|
||||
public bool GetAllFlagsForRow(string rowName)
|
||||
{
|
||||
bool allSelected = true;
|
||||
|
||||
foreach (string logType in new[] { "Debug", "Error", "Exception", "Info", "Warning" })
|
||||
{
|
||||
string baseFieldName = CategoryMapping.TryGetValue(rowName, out string value) ? value : string.Empty;
|
||||
if (string.IsNullOrEmpty(baseFieldName))
|
||||
{
|
||||
Debug.LogError($"No mapping found for row {rowName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
string fieldName = $"{baseFieldName}{logType}";
|
||||
FieldInfo field = _loggerSettings.GetType().GetField(fieldName);
|
||||
if (field != null)
|
||||
{
|
||||
bool currentValue = (bool)field.GetValue(_loggerSettings);
|
||||
allSelected &= currentValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Field {fieldName} does not exist in LoggerSettings");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return allSelected;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Renders a checkbox for a given log type and handles changes to its value.
|
||||
/// </summary>
|
||||
/// <param name="rowName">The name of the row to render the checkbox for.</param>
|
||||
/// <param name="logType">The type of log to handle.</param>
|
||||
public void RenderAndHandleCheckbox(string rowName, string logType)
|
||||
{
|
||||
// Using the mapping to get the base name for the fields
|
||||
string baseFieldName = CategoryMapping.TryGetValue(rowName, out string value) ? value : string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(baseFieldName))
|
||||
{
|
||||
Debug.LogError($"No mapping found for row {rowName}");
|
||||
return;
|
||||
}
|
||||
|
||||
string fieldName = $"{baseFieldName}{logType}";
|
||||
|
||||
FieldInfo field = _loggerSettings.GetType().GetField(fieldName);
|
||||
if (field != null)
|
||||
{
|
||||
bool currentValue = (bool)field.GetValue(_loggerSettings);
|
||||
bool newValue = EditorGUILayout.Toggle(currentValue, GUILayout.Width(100));
|
||||
if (currentValue != newValue) field.SetValue(_loggerSettings, newValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"Field {fieldName} does not exist in LoggerSettings");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets all flags for a given row to the provided value.
|
||||
/// </summary>
|
||||
/// <param name="rowName">The name of the row to set the flags for.</param>
|
||||
/// <param name="value">The value to set all flags to.</param>
|
||||
public void SetAllFlagsForRow(string rowName, bool value)
|
||||
{
|
||||
foreach (string logType in new[] { "Debug", "Error", "Exception", "Info", "Warning" })
|
||||
{
|
||||
string baseFieldName = CategoryMapping.TryGetValue(rowName, out string value1) ? value1 : string.Empty;
|
||||
if (string.IsNullOrEmpty(baseFieldName))
|
||||
{
|
||||
Debug.LogError($"No mapping found for row {rowName}");
|
||||
return;
|
||||
}
|
||||
|
||||
string fieldName = $"{baseFieldName}{logType}";
|
||||
FieldInfo field = _loggerSettings.GetType().GetField(fieldName);
|
||||
if (field != null)
|
||||
field.SetValue(_loggerSettings, value);
|
||||
else
|
||||
Debug.LogError($"Field {fieldName} does not exist in LoggerSettings");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets all flags to the provided value.
|
||||
/// </summary>
|
||||
/// <param name="value"> The value to set all flags to.</param>
|
||||
public void SetAllFlags(bool value)
|
||||
{
|
||||
string[] categories = { "characterResponse", "lipSync", "actions" };
|
||||
string[] logTypes = { "Debug", "Info", "Error", "Exception", "Warning" };
|
||||
|
||||
foreach (string category in categories)
|
||||
foreach (string logType in logTypes)
|
||||
{
|
||||
string fieldName = $"{category}{logType}";
|
||||
FieldInfo field = _loggerSettings.GetType().GetField(fieldName);
|
||||
if (field != null && field.FieldType == typeof(bool))
|
||||
field.SetValue(_loggerSettings, value);
|
||||
else
|
||||
Debug.LogWarning($"Field {fieldName} not found or not boolean.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6aa5925780d46299eacd79ab8115332
|
||||
timeCreated: 1701853621
|
||||
87
Assets/Convai/Scripts/Editor/Logger/LoggerSettingsWindow.cs
Normal file
87
Assets/Convai/Scripts/Editor/Logger/LoggerSettingsWindow.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Editor
|
||||
{
|
||||
public class LoggerSettingsWindow : EditorWindow
|
||||
{
|
||||
private readonly LoggerSettingsManager _loggerSettingsManager = new();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_ = _loggerSettingsManager.loggerSettings;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
// Setting window size
|
||||
minSize = new Vector2(850, 250);
|
||||
maxSize = minSize;
|
||||
if (_loggerSettingsManager.loggerSettings == null) return;
|
||||
EditorGUILayout.Space(20);
|
||||
// Create a custom GUIStyle based on EditorStyles.wordWrappedLabel
|
||||
GUIStyle customLabelStyle = new(EditorStyles.wordWrappedLabel)
|
||||
{
|
||||
fontSize = 15,
|
||||
normal = { textColor = Color.grey }
|
||||
};
|
||||
// Display a label with a custom style
|
||||
GUILayout.Label(
|
||||
"These loggerSettings only affect log loggerSettings related to the Convai plugin. Changes made here will not affect other parts of your project.",
|
||||
customLabelStyle);
|
||||
EditorGUILayout.Space(20);
|
||||
// Headers for the table
|
||||
string[] headers =
|
||||
{ "Select All", "Category", "Debug", "Info", "Error", "Exception", "Warning" };
|
||||
// Names of the rows in the table
|
||||
string[] rowNames = { "Character", "LipSync", "Actions" };
|
||||
// Style for the headers
|
||||
GUIStyle headerStyle = new(GUI.skin.label)
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
alignment = TextAnchor.MiddleLeft
|
||||
};
|
||||
// Draw the headers
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
foreach (string header in headers) GUILayout.Label(header, headerStyle, GUILayout.Width(95));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
// Slightly increased spacing between rows
|
||||
EditorGUILayout.Space(5);
|
||||
// Draw the rows
|
||||
foreach (string row in rowNames)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
// 'Select All' checkbox for each row
|
||||
bool allSelectedForRow = _loggerSettingsManager.GetAllFlagsForRow(row);
|
||||
bool newAllSelectedForRow = EditorGUILayout.Toggle(allSelectedForRow, GUILayout.Width(100));
|
||||
if (newAllSelectedForRow != allSelectedForRow)
|
||||
_loggerSettingsManager.SetAllFlagsForRow(row, newAllSelectedForRow);
|
||||
GUILayout.Label(row, GUILayout.Width(100));
|
||||
// Individual checkboxes for each log type
|
||||
foreach (string logType in new[] { "Debug", "Info", "Error", "Exception", "Warning" })
|
||||
_loggerSettingsManager.RenderAndHandleCheckbox(row, logType);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
// Increased spacing before global actions
|
||||
EditorGUILayout.Space(20);
|
||||
// Global actions
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Select All", GUILayout.Width(150), GUILayout.Height(30))) // Slightly bigger button
|
||||
_loggerSettingsManager.SetAllFlags(true);
|
||||
if (GUILayout.Button("Clear All", GUILayout.Width(150), GUILayout.Height(30))) // Slightly bigger button
|
||||
_loggerSettingsManager.SetAllFlags(false);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
// Additional space at the end for cleaner look
|
||||
EditorGUILayout.Space(20);
|
||||
// If the GUI has changed, mark _settings as dirty so it gets saved
|
||||
if (GUI.changed) EditorUtility.SetDirty(_loggerSettingsManager.loggerSettings);
|
||||
}
|
||||
|
||||
[MenuItem("Convai/Logger Settings")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
GetWindow<LoggerSettingsWindow>("Logger Settings");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1cfc7ccc4a8a584459a48d2851faa587
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Editor/NPC.meta
Normal file
8
Assets/Convai/Scripts/Editor/NPC.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b384a5a1424381d4483a96496fef88b1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,141 @@
|
||||
using System.IO;
|
||||
using Convai.Scripts.Utils;
|
||||
using Convai.Scripts.Utils.LipSync;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace Convai.Scripts.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Editor window for managing Convai NPC components.
|
||||
/// </summary>
|
||||
public class ConvaiNPCComponentSettingsWindow : EditorWindow
|
||||
{
|
||||
private ConvaiNPC _convaiNPC;
|
||||
|
||||
/// <summary>
|
||||
/// Handles GUI events for the window.
|
||||
/// </summary>
|
||||
private void OnGUI()
|
||||
{
|
||||
titleContent = new GUIContent("Convai NPC Components");
|
||||
Vector2 windowSize = new(300, 180);
|
||||
minSize = windowSize;
|
||||
maxSize = windowSize;
|
||||
if (_convaiNPC == null)
|
||||
{
|
||||
EditorGUILayout.LabelField("No ConvaiNPC selected");
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.BeginVertical(GUI.skin.box);
|
||||
EditorGUIUtility.labelWidth = 200f; // Set a custom label width
|
||||
|
||||
_convaiNPC.IncludeActionsHandler = EditorGUILayout.Toggle(new GUIContent("NPC Actions", "Decides if Actions Handler is included"), _convaiNPC.IncludeActionsHandler);
|
||||
_convaiNPC.LipSync = EditorGUILayout.Toggle(new GUIContent("Lip Sync", "Decides if Lip Sync is enabled"), _convaiNPC.LipSync);
|
||||
_convaiNPC.HeadEyeTracking = EditorGUILayout.Toggle(new GUIContent("Head & Eye Tracking", "Decides if Head & Eye tracking is enabled"), _convaiNPC.HeadEyeTracking);
|
||||
_convaiNPC.EyeBlinking = EditorGUILayout.Toggle(new GUIContent("Eye Blinking", "Decides if Eye Blinking is enabled"), _convaiNPC.EyeBlinking);
|
||||
_convaiNPC.NarrativeDesignManager = EditorGUILayout.Toggle(new GUIContent("Narrative Design Manager", "Decides if Narrative Design Manager is enabled"), _convaiNPC.NarrativeDesignManager);
|
||||
_convaiNPC.ConvaiGroupNPCController = EditorGUILayout.Toggle(new GUIContent("Group NPC Controller", "Decides if this NPC can be a part of Convai NPC to NPC Conversation"), _convaiNPC.ConvaiGroupNPCController);
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (GUILayout.Button("Apply Changes", GUILayout.Height(40)))
|
||||
{
|
||||
ApplyChanges();
|
||||
EditorUtility.SetDirty(_convaiNPC);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the component states when the window gains focus.
|
||||
/// </summary>
|
||||
private void OnFocus()
|
||||
{
|
||||
RefreshComponentStates();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the Convai NPC Component Settings window.
|
||||
/// </summary>
|
||||
/// <param name="convaiNPC">The Convai NPC to manage.</param>
|
||||
public static void Open(ConvaiNPC convaiNPC)
|
||||
{
|
||||
ConvaiNPCComponentSettingsWindow window = GetWindow<ConvaiNPCComponentSettingsWindow>();
|
||||
window.titleContent = new GUIContent("Convai NPC Component Settings");
|
||||
window._convaiNPC = convaiNPC;
|
||||
window.RefreshComponentStates();
|
||||
window.Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the states of the components.
|
||||
/// </summary>
|
||||
private void RefreshComponentStates()
|
||||
{
|
||||
if (_convaiNPC != null)
|
||||
{
|
||||
_convaiNPC.IncludeActionsHandler = _convaiNPC.GetComponent<ConvaiActionsHandler>() is not null;
|
||||
_convaiNPC.LipSync = _convaiNPC.GetComponent<ConvaiLipSync>() is not null;
|
||||
_convaiNPC.HeadEyeTracking = _convaiNPC.GetComponent<ConvaiHeadTracking>() is not null;
|
||||
_convaiNPC.EyeBlinking = _convaiNPC.GetComponent<ConvaiBlinkingHandler>() is not null;
|
||||
_convaiNPC.NarrativeDesignManager = _convaiNPC.GetComponent<NarrativeDesignManager>() is not null;
|
||||
_convaiNPC.ConvaiGroupNPCController = _convaiNPC.GetComponent<ConvaiGroupNPCController>() is not null;
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies changes based on the user's selection in the inspector.
|
||||
/// </summary>
|
||||
private void ApplyChanges()
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("Confirm Apply Changes",
|
||||
"Do you want to apply the following changes?", "Yes", "No"))
|
||||
{
|
||||
ApplyComponent<ConvaiActionsHandler>(_convaiNPC.IncludeActionsHandler);
|
||||
ApplyComponent<ConvaiLipSync>(_convaiNPC.LipSync);
|
||||
ApplyComponent<ConvaiHeadTracking>(_convaiNPC.HeadEyeTracking);
|
||||
ApplyComponent<ConvaiBlinkingHandler>(_convaiNPC.EyeBlinking);
|
||||
ApplyComponent<NarrativeDesignManager>(_convaiNPC.NarrativeDesignManager);
|
||||
ApplyComponent<ConvaiGroupNPCController>(_convaiNPC.ConvaiGroupNPCController);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies or removes a component based on the specified condition.
|
||||
/// If the component is to be removed, its state is saved. If it's added, its state is restored if previously saved.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the component.</typeparam>
|
||||
/// <param name="includeComponent">Whether to include the component.</param>
|
||||
private void ApplyComponent<T>(bool includeComponent) where T : Component
|
||||
{
|
||||
T component = _convaiNPC.GetComponent<T>();
|
||||
|
||||
string savedDataFileName = Path.Combine(StateSaver.ROOT_DIRECTORY, _convaiNPC.characterID,
|
||||
$"{SceneManager.GetActiveScene().name}_{_convaiNPC.characterID}_{nameof(T)}_State.data");
|
||||
|
||||
if (includeComponent)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
component = _convaiNPC.gameObject.AddComponentSafe<T>();
|
||||
|
||||
if (File.Exists(savedDataFileName))
|
||||
component.RestoreStateFromFile(savedDataFileName);
|
||||
}
|
||||
}
|
||||
else if (component != null)
|
||||
{
|
||||
component.SaveStateToFile(savedDataFileName);
|
||||
DestroyImmediate(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f84be99651944b69262e8060b8ac451
|
||||
timeCreated: 1696842374
|
||||
254
Assets/Convai/Scripts/Editor/NPC/ConvaiNPCEditor.cs
Normal file
254
Assets/Convai/Scripts/Editor/NPC/ConvaiNPCEditor.cs
Normal file
@ -0,0 +1,254 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Convai.Scripts.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor for the ConvaiNPC component.
|
||||
/// Provides functionalities to cache and restore states of all convai scripts whenever a scene is saved.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ConvaiNPC))]
|
||||
[HelpURL("https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/scripts-overview")]
|
||||
public class ConvaiNPCEditor : UnityEditor.Editor
|
||||
{
|
||||
private ConvaiNPC _convaiNPC;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_convaiNPC = (ConvaiNPC)target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the default inspector GUI to add custom buttons and functionality.
|
||||
/// </summary>
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
// Add Components button to add necessary components and assign a random color to the character.
|
||||
if (GUILayout.Button(new GUIContent(
|
||||
"Add Components",
|
||||
"Adds necessary components to the NPC and assigns a random color to the character's text"
|
||||
),
|
||||
GUILayout.Width(120)
|
||||
)
|
||||
) AddComponentsToNPC();
|
||||
|
||||
if (GUILayout.Button(new GUIContent(
|
||||
"Copy Debug",
|
||||
"Copies the session id and other essential properties to clipboard for easier debugging"
|
||||
),
|
||||
GUILayout.Width(120)
|
||||
)
|
||||
) CopyToClipboard();
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void CopyToClipboard()
|
||||
{
|
||||
StringBuilder stringBuilder = new();
|
||||
stringBuilder.AppendLine($"Endpoint: {_convaiNPC.GetEndPointURL}");
|
||||
stringBuilder.AppendLine($"Character ID: {_convaiNPC.characterID}");
|
||||
stringBuilder.AppendLine($"Session ID: {_convaiNPC.sessionID}");
|
||||
|
||||
GUIUtility.systemCopyBuffer = stringBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds components to the NPC and assigns a random color to the character's text.
|
||||
/// </summary>
|
||||
private void AddComponentsToNPC()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConvaiNPCComponentSettingsWindow.Open(_convaiNPC);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Unexpected error occurred when applying changes. Error: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility class to save the state of Convai scripts.
|
||||
/// </summary>
|
||||
public abstract class StateSaver
|
||||
{
|
||||
public const string ROOT_DIRECTORY = "Assets/Convai/Settings/Script State/";
|
||||
|
||||
/// <summary>
|
||||
/// Save the state of all Convai scripts in the current scene.
|
||||
/// </summary>
|
||||
[MenuItem("Convai/Save Script State", false, 100)]
|
||||
public static void SaveScriptState()
|
||||
{
|
||||
Scene scene = SceneManager.GetActiveScene();
|
||||
ConvaiNPC[] convaiObjects = Object.FindObjectsOfType<ConvaiNPC>();
|
||||
|
||||
foreach (ConvaiNPC convaiNPC in convaiObjects)
|
||||
{
|
||||
Debug.Log($"Saving state for character: {convaiNPC.characterName}");
|
||||
MonoBehaviour[] scripts = convaiNPC.GetComponentsInChildren<MonoBehaviour>();
|
||||
|
||||
string characterFolder = Path.Combine(ROOT_DIRECTORY, convaiNPC.characterID);
|
||||
if (!Directory.Exists(characterFolder)) Directory.CreateDirectory(characterFolder);
|
||||
|
||||
foreach (MonoBehaviour script in scripts)
|
||||
{
|
||||
string fullName = script.GetType().FullName;
|
||||
if (fullName != null && !fullName.StartsWith("Convai.Scripts")) continue;
|
||||
|
||||
string assetPath = script.GetSavePath(characterFolder, scene.name, convaiNPC.characterID);
|
||||
File.WriteAllText(assetPath, JsonUtility.ToJson(script));
|
||||
}
|
||||
}
|
||||
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
#region Nested type: SaveSceneHook
|
||||
|
||||
/// <summary>
|
||||
/// Restore the state of all Convai scripts in the current scene.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public class SaveSceneHook
|
||||
{
|
||||
static SaveSceneHook()
|
||||
{
|
||||
EditorSceneManager.sceneSaved += SceneSaved;
|
||||
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
|
||||
}
|
||||
|
||||
private static void OnBeforeAssemblyReload()
|
||||
{
|
||||
// Unsubscribe from the sceneSaved event to prevent memory leaks.
|
||||
EditorSceneManager.sceneSaved -= SceneSaved;
|
||||
// Unsubscribe from the beforeAssemblyReload event.
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
|
||||
}
|
||||
|
||||
private static void SceneSaved(Scene scene)
|
||||
{
|
||||
SaveScriptState();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for Unity editor components to facilitate saving and restoring state, as well as safely
|
||||
/// adding components.
|
||||
/// </summary>
|
||||
public static class EditorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the state of a component to a file in JSON format.
|
||||
/// </summary>
|
||||
/// <param name="component">The component whose state is to be saved.</param>
|
||||
/// <param name="path">The file path where the state will be saved.</param>
|
||||
/// <typeparam name="T">The type of the component derived from UnityEngine.Component.</typeparam>
|
||||
public static void SaveStateToFile<T>(this T component, string path) where T : Component
|
||||
{
|
||||
try
|
||||
{
|
||||
string serializedComponentData = JsonUtility.ToJson(component);
|
||||
File.WriteAllText(path, serializedComponentData);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
Debug.LogError($"Access to the path '{path}' is denied. Error: {ex.Message}");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Debug.LogError($"An I/O error occurred while writing to the file at '{path}'. Error: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to save component state for {typeof(T).Name}. Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the state of a component from a file containing JSON data.
|
||||
/// </summary>
|
||||
/// <param name="component">The component whose state is to be restored.</param>
|
||||
/// <param name="path">The file path from which the state will be restored.</param>
|
||||
/// <typeparam name="T">The type of the component derived from UnityEngine.Component.</typeparam>
|
||||
public static void RestoreStateFromFile<T>(this T component, string path) where T : Component
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Debug.LogWarning($"No saved state file found at '{path}' for component {typeof(T).Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
string savedData = File.ReadAllText(path);
|
||||
JsonUtility.FromJsonOverwrite(savedData, component);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
Debug.LogError($"Access to the path '{path}' is denied. Error: {ex.Message}");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Debug.LogError($"An I/O error occurred while reading from the file at '{path}'. Error: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to restore component data for {typeof(T).Name}. Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a component to the GameObject safely, catching any exceptions that occur during the process.
|
||||
/// </summary>
|
||||
/// <param name="go">The GameObject to which the component will be added.</param>
|
||||
/// <typeparam name="T">The type of the component to be added, derived from UnityEngine.Component.</typeparam>
|
||||
/// <returns>The newly added component, or null if the operation failed.</returns>
|
||||
public static T AddComponentSafe<T>(this GameObject go) where T : Component
|
||||
{
|
||||
try
|
||||
{
|
||||
return go.AddComponent<T>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to add component of type {typeof(T).Name}, Error: {ex}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a file path for saving the state of a MonoBehaviour script, based on the character folder, scene name,
|
||||
/// and character ID.
|
||||
/// </summary>
|
||||
/// <param name="script">The MonoBehaviour script for which the save path is being constructed.</param>
|
||||
/// <param name="characterFolder">The folder path associated with the character.</param>
|
||||
/// <param name="sceneName">The name of the current scene.</param>
|
||||
/// <param name="characterID">The unique identifier of the character from Convai Playground.</param>
|
||||
/// <returns>A string representing the constructed file path for saving the script's state.</returns>
|
||||
public static string GetSavePath(this MonoBehaviour script, string characterFolder, string sceneName,
|
||||
string characterID)
|
||||
{
|
||||
return Path.Combine(characterFolder, $"{sceneName}_{characterID}_{script.GetType().FullName}_State.data");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
11
Assets/Convai/Scripts/Editor/NPC/ConvaiNPCEditor.cs.meta
Normal file
11
Assets/Convai/Scripts/Editor/NPC/ConvaiNPCEditor.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f2ab5d11b144b3aa5d8c371ea664c44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Assets/Convai/Scripts/Editor/NarrativeDesign.meta
Normal file
3
Assets/Convai/Scripts/Editor/NarrativeDesign.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25397e28c8a1443e8ed01b939b22a538
|
||||
timeCreated: 1715597900
|
||||
@ -0,0 +1,115 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Narrative_Design.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Narrative_Design.Editor
|
||||
{
|
||||
[CustomEditor(typeof(NarrativeDesignManager))]
|
||||
public class NarrativeDesignManagerEditor : UnityEditor.Editor
|
||||
{
|
||||
/// Dictionary to keep track of which sections are expanded in the editor
|
||||
private readonly Dictionary<string, bool> _sectionIdExpanded = new();
|
||||
|
||||
/// Reference to the NarrativeDesignManager that this editor is modifying
|
||||
private NarrativeDesignManager _narrativeDesignManager;
|
||||
|
||||
/// SerializedProperty for the section change events in the NarrativeDesignManager
|
||||
private SerializedProperty _sectionChangeEvents;
|
||||
|
||||
/// Whether the section events are expanded in the editor
|
||||
private bool _sectionEventsExpanded = true;
|
||||
|
||||
/// SerializedObject for the target object
|
||||
private SerializedObject _serializedObject;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_serializedObject = new SerializedObject(target);
|
||||
_narrativeDesignManager = target as NarrativeDesignManager;
|
||||
|
||||
if (_narrativeDesignManager != null) FindProperties();
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
_serializedObject.Update();
|
||||
|
||||
if (GUILayout.Button("Check for Updates")) OnUpdateNarrativeDesignButtonClicked();
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (_narrativeDesignManager.sectionDataList.Count > 0)
|
||||
{
|
||||
_sectionEventsExpanded = EditorGUILayout.Foldout(_sectionEventsExpanded, "Section Events", true, EditorStyles.foldoutHeader);
|
||||
|
||||
if (_sectionEventsExpanded)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
|
||||
for (int i = 0; i < _narrativeDesignManager.sectionDataList.Count; i++)
|
||||
{
|
||||
SectionData sectionData = _narrativeDesignManager.sectionDataList[i];
|
||||
string sectionId = sectionData.sectionId;
|
||||
|
||||
SectionChangeEventsData sectionChangeEventsData = _narrativeDesignManager.sectionChangeEventsDataList.Find(x => x.id == sectionId);
|
||||
|
||||
if (sectionChangeEventsData == null)
|
||||
{
|
||||
sectionChangeEventsData = new SectionChangeEventsData { id = sectionId };
|
||||
_narrativeDesignManager.sectionChangeEventsDataList.Add(sectionChangeEventsData);
|
||||
}
|
||||
|
||||
_sectionIdExpanded.TryAdd(sectionId, false);
|
||||
|
||||
GUIStyle sectionIdStyle = new(EditorStyles.foldoutHeader)
|
||||
{
|
||||
fontStyle = FontStyle.Bold,
|
||||
fontSize = 14
|
||||
};
|
||||
|
||||
string sectionIdText = $"{sectionData.sectionName} - {sectionId}";
|
||||
_sectionIdExpanded[sectionId] = EditorGUILayout.Foldout(_sectionIdExpanded[sectionId], sectionIdText, true, sectionIdStyle);
|
||||
|
||||
if (_sectionIdExpanded[sectionId])
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
SerializedProperty sectionChangeEventsProperty = _sectionChangeEvents.GetArrayElementAtIndex(i);
|
||||
EditorGUILayout.PropertyField(sectionChangeEventsProperty, GUIContent.none, true);
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
_serializedObject.ApplyModifiedProperties();
|
||||
_narrativeDesignManager.OnSectionEventListChange();
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
}
|
||||
|
||||
_serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private async void OnUpdateNarrativeDesignButtonClicked()
|
||||
{
|
||||
await Task.WhenAll(_narrativeDesignManager.UpdateSectionListAsync(), _narrativeDesignManager.UpdateTriggerListAsync());
|
||||
_serializedObject.ApplyModifiedProperties();
|
||||
_narrativeDesignManager.OnSectionEventListChange();
|
||||
}
|
||||
|
||||
private void FindProperties()
|
||||
{
|
||||
_serializedObject.FindProperty(nameof(NarrativeDesignManager.sectionDataList));
|
||||
_serializedObject.FindProperty(nameof(NarrativeDesignManager.triggerDataList));
|
||||
_sectionChangeEvents = _serializedObject.FindProperty(nameof(NarrativeDesignManager.sectionChangeEventsDataList));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1537f32558f6e8042878f1a95c1fe984
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,43 @@
|
||||
using Convai.Scripts.Narrative_Design.Models;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Narrative_Design.Editor
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(SectionChangeEventsData))]
|
||||
public class NarrativeDesignSectionChangeEventsDataPropertyDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
SerializedProperty sectionIdProperty = property.FindPropertyRelative("id");
|
||||
SerializedProperty onSectionStartProperty = property.FindPropertyRelative("onSectionStart");
|
||||
SerializedProperty onSectionEndProperty = property.FindPropertyRelative("onSectionEnd");
|
||||
|
||||
Rect sectionIdRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||
EditorGUI.LabelField(sectionIdRect, "Section ID", sectionIdProperty.stringValue);
|
||||
|
||||
position.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
EditorGUI.PropertyField(position, onSectionStartProperty, true);
|
||||
position.y += EditorGUI.GetPropertyHeight(onSectionStartProperty) + EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
EditorGUI.PropertyField(position, onSectionEndProperty, true);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
SerializedProperty onSectionStartProperty = property.FindPropertyRelative("onSectionStart");
|
||||
SerializedProperty onSectionEndProperty = property.FindPropertyRelative("onSectionEnd");
|
||||
|
||||
float height = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||
height += EditorGUI.GetPropertyHeight(onSectionStartProperty) + EditorGUIUtility.standardVerticalSpacing;
|
||||
height += EditorGUI.GetPropertyHeight(onSectionEndProperty);
|
||||
|
||||
return height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23fcdcc9acc44804e991455dcd85953d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,19 @@
|
||||
using UnityEditor;
|
||||
|
||||
namespace Convai.Scripts.Narrative_Design.Editor
|
||||
{
|
||||
[CustomEditor(typeof(NarrativeDesignTrigger))]
|
||||
public class NarrativeDesignTriggerEditor : UnityEditor.Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
NarrativeDesignTrigger narrativeDesignTrigger = (NarrativeDesignTrigger)target;
|
||||
|
||||
DrawDefaultInspector();
|
||||
|
||||
if (narrativeDesignTrigger.availableTriggers is { Count: > 0 })
|
||||
narrativeDesignTrigger.selectedTriggerIndex =
|
||||
EditorGUILayout.Popup("Trigger", narrativeDesignTrigger.selectedTriggerIndex, narrativeDesignTrigger.availableTriggers.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdf68c1817bd4102879052a18d773fec
|
||||
timeCreated: 1706873993
|
||||
8
Assets/Convai/Scripts/Editor/Setup.meta
Normal file
8
Assets/Convai/Scripts/Editor/Setup.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2ef013e23cfeea43972daeb3a881509
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
406
Assets/Convai/Scripts/Editor/Setup/ConvaiSetup.cs
Normal file
406
Assets/Convai/Scripts/Editor/Setup/ConvaiSetup.cs
Normal file
@ -0,0 +1,406 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEditor.VSAttribution;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using UnityEditor;
|
||||
using Convai.Scripts.Utils;
|
||||
using Convai.Scripts.Runtime.Utils;
|
||||
|
||||
public class ConvaiSetup : EditorWindow
|
||||
{
|
||||
private const string API_KEY_PATH = "Assets/Resources/ConvaiAPIKey.asset";
|
||||
private const string API_URL = "https://api.convai.com/user/referral-source-status";
|
||||
|
||||
[MenuItem("Convai/Convai Setup", false, 1)]
|
||||
public static void ShowConvaiSetupWindow()
|
||||
{
|
||||
ConvaiSetup wnd = GetWindow<ConvaiSetup>();
|
||||
}
|
||||
|
||||
[MenuItem("Convai/Documentation")]
|
||||
public static void OpenDocumentation()
|
||||
{
|
||||
Application.OpenURL("https://docs.convai.com/plugins-and-integrations/unity-plugin");
|
||||
}
|
||||
|
||||
public class UpdateSource
|
||||
{
|
||||
[JsonProperty("referral_source")] public string referral_source;
|
||||
|
||||
public UpdateSource(string referral_source)
|
||||
{
|
||||
this.referral_source = referral_source;
|
||||
}
|
||||
}
|
||||
|
||||
public class referralSourceStatus
|
||||
{
|
||||
[JsonProperty("referral_source_status")] public string referral_source_status;
|
||||
[JsonProperty("status")] public string status;
|
||||
}
|
||||
|
||||
async Task<string> CheckReferralStatus(string url, string apiKey)
|
||||
{
|
||||
// Create a new HttpWebRequest object
|
||||
var request = WebRequest.Create(url);
|
||||
request.Method = "post";
|
||||
|
||||
// Set the request headers
|
||||
request.ContentType = "application/json";
|
||||
|
||||
string bodyJsonString = "{}";
|
||||
|
||||
// Convert the json string to bytes
|
||||
byte[] jsonBytes = Encoding.UTF8.GetBytes(bodyJsonString);
|
||||
|
||||
referralSourceStatus referralStatus;
|
||||
|
||||
request.Headers.Add("CONVAI-API-KEY", apiKey);
|
||||
|
||||
// Write the data to the request stream
|
||||
using (Stream requestStream = await request.GetRequestStreamAsync())
|
||||
{
|
||||
await requestStream.WriteAsync(jsonBytes, 0, jsonBytes.Length);
|
||||
}
|
||||
|
||||
// Get the response from the server
|
||||
try
|
||||
{
|
||||
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
|
||||
{
|
||||
using (Stream streamResponse = response.GetResponseStream())
|
||||
{
|
||||
using (StreamReader reader = new(streamResponse))
|
||||
{
|
||||
string responseContent = reader.ReadToEnd();
|
||||
|
||||
referralStatus = JsonConvert.DeserializeObject<referralSourceStatus>(responseContent);
|
||||
}
|
||||
}
|
||||
return referralStatus.referral_source_status;
|
||||
}
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
Debug.LogError(e.Message + "\nPlease check if API Key is correct.");
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<bool> SendReferralRequest(string url, string bodyJsonString, string apiKey)
|
||||
{
|
||||
// Create a new HttpWebRequest object
|
||||
var request = WebRequest.Create(url);
|
||||
request.Method = "post";
|
||||
|
||||
// Set the request headers
|
||||
request.ContentType = "application/json";
|
||||
|
||||
// Convert the json string to bytes
|
||||
byte[] jsonBytes = Encoding.UTF8.GetBytes(bodyJsonString);
|
||||
|
||||
request.Headers.Add("CONVAI-API-KEY", apiKey);
|
||||
|
||||
// Write the data to the request stream
|
||||
using (Stream requestStream = await request.GetRequestStreamAsync())
|
||||
{
|
||||
await requestStream.WriteAsync(jsonBytes, 0, jsonBytes.Length);
|
||||
}
|
||||
|
||||
// Get the response from the server
|
||||
try
|
||||
{
|
||||
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
|
||||
{
|
||||
using (Stream streamResponse = response.GetResponseStream())
|
||||
{
|
||||
using (StreamReader reader = new(streamResponse))
|
||||
{
|
||||
string responseContent = reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if ((int)response.StatusCode == 200)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebException e)
|
||||
{
|
||||
Debug.LogError(e.Message + "\nPlease check if API Key is correct.");
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> BeginButtonTask(string apiKey)
|
||||
{
|
||||
|
||||
ConvaiAPIKeySetup aPIKeySetup = CreateInstance<ConvaiAPIKeySetup>();
|
||||
|
||||
aPIKeySetup.APIKey = apiKey;
|
||||
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
string referralStatus =
|
||||
await CheckReferralStatus(API_URL, apiKey);
|
||||
|
||||
if (referralStatus != null)
|
||||
{
|
||||
CreateOrUpdateAPIKeyAsset(aPIKeySetup);
|
||||
|
||||
if (referralStatus.Trim().ToLower() == "undefined" || referralStatus.Trim().ToLower() == "")
|
||||
{
|
||||
EditorUtility.DisplayDialog("Warning", "[Step 1/2] API Key loaded successfully!",
|
||||
"OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
EditorUtility.DisplayDialog("Success", "API Key loaded successfully!", "OK");
|
||||
|
||||
// if the status is already set, do not show the referral dialog
|
||||
return false;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error", "Something went wrong. Please check your API Key. Contact support@convai.com for more help. ", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EditorUtility.DisplayDialog("Error", "Please enter a valid API Key.", "OK");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CreateOrUpdateAPIKeyAsset(ConvaiAPIKeySetup aPIKeySetup)
|
||||
{
|
||||
string assetPath = "Assets/Resources/ConvaiAPIKey.asset";
|
||||
|
||||
if (!File.Exists(assetPath))
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Resources"))
|
||||
AssetDatabase.CreateFolder("Assets", "Resources");
|
||||
|
||||
AssetDatabase.CreateAsset(aPIKeySetup, assetPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssetDatabase.DeleteAsset(assetPath);
|
||||
AssetDatabase.CreateAsset(aPIKeySetup, assetPath);
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
// Each editor window contains a root VisualElement object
|
||||
VisualElement root = rootVisualElement;
|
||||
|
||||
VisualElement page1 = new ScrollView();
|
||||
|
||||
VisualElement page2 = new ScrollView();
|
||||
|
||||
root.Add(new Label(""));
|
||||
|
||||
Image convaiLogoImage = new()
|
||||
{
|
||||
image = AssetDatabase.LoadAssetAtPath<Texture>(ConvaiImagesDirectory.CONVAI_LOGO_PATH)
|
||||
};
|
||||
convaiLogoImage.style.height = 100;
|
||||
|
||||
convaiLogoImage.style.paddingBottom = 10;
|
||||
convaiLogoImage.style.paddingTop = 10;
|
||||
convaiLogoImage.style.paddingRight = 10;
|
||||
convaiLogoImage.style.paddingLeft = 10;
|
||||
|
||||
root.Add(convaiLogoImage);
|
||||
|
||||
Label convaiSetupLabel = new("Enter your API Key:");
|
||||
convaiSetupLabel.style.fontSize = 16;
|
||||
|
||||
TextField APIKeyTextField = new("", -1, false, true, '*');
|
||||
|
||||
Button beginButton = new(async () =>
|
||||
{
|
||||
bool isPage2 = await BeginButtonTask(APIKeyTextField.text);
|
||||
|
||||
if (isPage2)
|
||||
{
|
||||
root.Remove(page1);
|
||||
root.Add(page2);
|
||||
}
|
||||
else
|
||||
{
|
||||
Close();
|
||||
}
|
||||
})
|
||||
{
|
||||
text = "Begin!"
|
||||
};
|
||||
|
||||
beginButton.style.fontSize = 16;
|
||||
beginButton.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
beginButton.style.alignSelf = Align.Center;
|
||||
|
||||
beginButton.style.paddingBottom = 10;
|
||||
beginButton.style.paddingLeft = 30;
|
||||
beginButton.style.paddingRight = 30;
|
||||
beginButton.style.paddingTop = 10;
|
||||
|
||||
Button docsLink = new(() =>
|
||||
{
|
||||
Application.OpenURL("https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/setting-up-unity-plugin");
|
||||
})
|
||||
{
|
||||
text = "How do I find my API key?"
|
||||
};
|
||||
docsLink.style.alignSelf = Align.Center;
|
||||
docsLink.style.paddingBottom = 5;
|
||||
docsLink.style.paddingLeft = 50;
|
||||
docsLink.style.paddingRight = 50;
|
||||
docsLink.style.paddingTop = 5;
|
||||
|
||||
page1.Add(convaiSetupLabel);
|
||||
page1.Add(new Label("\n"));
|
||||
page1.Add(APIKeyTextField);
|
||||
page1.Add(new Label("\n"));
|
||||
page1.Add(beginButton);
|
||||
page1.Add(new Label("\n"));
|
||||
page1.Add(docsLink);
|
||||
|
||||
page1.style.marginBottom = 20;
|
||||
page1.style.marginLeft = 20;
|
||||
page1.style.marginRight = 20;
|
||||
page1.style.marginTop = 20;
|
||||
|
||||
Label attributionSourceLabel = new("[Step 2/2] Where did you discover Convai?");
|
||||
|
||||
attributionSourceLabel.style.fontSize = 14;
|
||||
attributionSourceLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
|
||||
List<string> attributionSourceOptions = new()
|
||||
{
|
||||
"Search Engine (Google, Bing, etc.)",
|
||||
"Youtube",
|
||||
"Social Media (Facebook, Instagram, TikTok, etc.)",
|
||||
"Friend Referral",
|
||||
"Unity Asset Store",
|
||||
"Others"
|
||||
};
|
||||
|
||||
|
||||
TextField otherOptionTextField = new();
|
||||
|
||||
string currentChoice = "";
|
||||
int currentChoiceIndex = -1;
|
||||
|
||||
DropdownMenu dropdownMenu = new();
|
||||
|
||||
ToolbarMenu toolbarMenu = new() { text = "Click here to select option..." };
|
||||
|
||||
foreach (string choice in attributionSourceOptions)
|
||||
{
|
||||
toolbarMenu.menu.AppendAction(choice,
|
||||
action =>
|
||||
{
|
||||
currentChoice = choice;
|
||||
toolbarMenu.text = choice;
|
||||
});
|
||||
}
|
||||
|
||||
toolbarMenu.style.paddingBottom = 10;
|
||||
toolbarMenu.style.paddingLeft = 30;
|
||||
toolbarMenu.style.paddingRight = 30;
|
||||
toolbarMenu.style.paddingTop = 10;
|
||||
|
||||
Button continueButton = new(async () =>
|
||||
{
|
||||
UpdateSource updateSource;
|
||||
|
||||
currentChoiceIndex = attributionSourceOptions.IndexOf(toolbarMenu.text);
|
||||
|
||||
if (currentChoiceIndex < 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Error", "Please select a valid referral source!", "OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
updateSource = new UpdateSource(attributionSourceOptions[currentChoiceIndex]);
|
||||
|
||||
if (attributionSourceOptions[currentChoiceIndex] == "Others")
|
||||
{
|
||||
updateSource.referral_source = otherOptionTextField.text;
|
||||
}
|
||||
|
||||
ConvaiAPIKeySetup apiKeyObject = AssetDatabase.LoadAssetAtPath<ConvaiAPIKeySetup>("Assets/Resources/ConvaiAPIKey.Asset");
|
||||
|
||||
await SendReferralRequest("https://api.convai.com/user/update-source", JsonConvert.SerializeObject(updateSource), apiKeyObject.APIKey);
|
||||
|
||||
if (attributionSourceOptions[currentChoiceIndex] == "Unity Asset Store")
|
||||
{
|
||||
// VS Attribution
|
||||
VSAttribution.SendAttributionEvent("Initial Setup", "Convai Technologies, Inc.", apiKeyObject.APIKey);
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
})
|
||||
{
|
||||
text = "Continue"
|
||||
};
|
||||
|
||||
continueButton.style.fontSize = 16;
|
||||
continueButton.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
continueButton.style.alignSelf = Align.Center;
|
||||
|
||||
continueButton.style.paddingBottom = 5;
|
||||
continueButton.style.paddingLeft = 30;
|
||||
continueButton.style.paddingRight = 30;
|
||||
continueButton.style.paddingTop = 5;
|
||||
|
||||
page2.Add(new Label("\n"));
|
||||
page2.Add(attributionSourceLabel);
|
||||
page2.Add(new Label("\n"));
|
||||
|
||||
page2.Add(toolbarMenu);
|
||||
page2.Add(new Label("\nIf selected Others above, please specifiy from where: "));
|
||||
|
||||
page2.Add(otherOptionTextField);
|
||||
page2.Add(new Label("\n"));
|
||||
page2.Add(continueButton);
|
||||
|
||||
page2.style.marginBottom = 20;
|
||||
page2.style.marginLeft = 20;
|
||||
page2.style.marginRight = 20;
|
||||
page2.style.marginTop = 20;
|
||||
root.Add(page1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Assets/Convai/Scripts/Editor/Setup/ConvaiSetup.cs.meta
Normal file
11
Assets/Convai/Scripts/Editor/Setup/ConvaiSetup.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 839f8dfaec43fac4aa4f6b776bc083c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Editor/Tutorial.meta
Normal file
8
Assets/Convai/Scripts/Editor/Tutorial.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87108192e81b561468407969a263b958
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1 @@
|
||||
TutorialInitialized
|
||||
41
Assets/Convai/Scripts/Editor/Tutorial/PostImportProcess.cs
Normal file
41
Assets/Convai/Scripts/Editor/Tutorial/PostImportProcess.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.IO;
|
||||
|
||||
[InitializeOnLoad]
|
||||
public class PostImportProcess
|
||||
{
|
||||
private static readonly string processedFilePath = Path.Combine(Application.dataPath, "Convai/Scripts/Editor/Tutorial/.TutorialInitialized");
|
||||
|
||||
static PostImportProcess()
|
||||
{
|
||||
if (HasAlreadyProcessed())
|
||||
return;
|
||||
|
||||
string settingsPath = Path.Combine(Application.dataPath.Substring(0, Application.dataPath.LastIndexOf('/')),
|
||||
"ProjectSettings/Packages/com.unity.learn.iet-framework/Settings.json");
|
||||
|
||||
if (!File.Exists(settingsPath)) return;
|
||||
File.Delete(settingsPath);
|
||||
DestroySelf();
|
||||
MarkAsProcessed();
|
||||
}
|
||||
|
||||
private static void DestroySelf()
|
||||
{
|
||||
string path = Path.Combine(Application.dataPath, "Convai/Scripts/Editor/Tutorial/PostImportProcess.cs");
|
||||
File.Delete(path);
|
||||
File.Delete(path + ".meta");
|
||||
File.Delete(processedFilePath);
|
||||
}
|
||||
|
||||
private static bool HasAlreadyProcessed()
|
||||
{
|
||||
return File.Exists(processedFilePath);
|
||||
}
|
||||
|
||||
private static void MarkAsProcessed()
|
||||
{
|
||||
File.WriteAllText(processedFilePath, "TutorialInitialized");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e74fa33131314ea40900b3c1f43352bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
|
||||
public class RemoveTutorialAssets : EditorWindow
|
||||
{
|
||||
private const string TUTORIAL_ASSETS_PATH = "Assets/Convai/Tutorials";
|
||||
private const string TUTORIAL_PACKAGE_NAME = "com.unity.learn.iet-framework";
|
||||
|
||||
[MenuItem("Convai/Remove Tutorial Assets")]
|
||||
private static void StartUninstallProcess()
|
||||
{
|
||||
if (Directory.Exists(TUTORIAL_ASSETS_PATH))
|
||||
{
|
||||
AssetDatabase.DeleteAsset(TUTORIAL_ASSETS_PATH);
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
foreach (PackageInfo packageInfo in PackageInfo.GetAllRegisteredPackages())
|
||||
{
|
||||
// Check if the package name matches
|
||||
if (packageInfo.name == TUTORIAL_PACKAGE_NAME)
|
||||
{
|
||||
Client.Remove(TUTORIAL_PACKAGE_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 884304efde7585442b24b38748460394
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Editor/UI.meta
Normal file
8
Assets/Convai/Scripts/Editor/UI.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b52a71c671cc4147939ccc623dc577d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
4
Assets/Convai/Scripts/Editor/UI/ConvaiImagesDirectory.cs
Normal file
4
Assets/Convai/Scripts/Editor/UI/ConvaiImagesDirectory.cs
Normal file
@ -0,0 +1,4 @@
|
||||
public struct ConvaiImagesDirectory
|
||||
{
|
||||
public const string CONVAI_LOGO_PATH = "Assets/Convai/Art/UI/Logos/Convai Logo.png";
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0e07e10d99ea164ca7da91a9aaa1a4b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/Convai/Scripts/Editor/UI/ReadOnlyDrawer.cs
Normal file
16
Assets/Convai/Scripts/Editor/UI/ReadOnlyDrawer.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Editor
|
||||
{
|
||||
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
|
||||
public class ReadOnlyDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
GUI.enabled = false; // Disable the property field
|
||||
EditorGUI.PropertyField(position, property, label, true);
|
||||
GUI.enabled = true; // Re-enable the property field
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Convai/Scripts/Editor/UI/ReadOnlyDrawer.cs.meta
Normal file
3
Assets/Convai/Scripts/Editor/UI/ReadOnlyDrawer.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b829967cd7e4a4880a40bb6653e26d9
|
||||
timeCreated: 1701082874
|
||||
8
Assets/Convai/Scripts/Runtime.meta
Normal file
8
Assets/Convai/Scripts/Runtime.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3758aa3aebee524cafef043d0dc87ee
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Runtime/Addons.meta
Normal file
8
Assets/Convai/Scripts/Runtime/Addons.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5e535a2ca4f1fe488b8e9bc5958d726
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2668e173130e8d448b53fd765d6ff11
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Convai.Scripts.Utils;
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Controls player input to trigger a notification if there is no active NPC available for conversation.
|
||||
/// </summary>
|
||||
public class ActiveNPCChecker : MonoBehaviour
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
/// <summary>
|
||||
/// Subscribes to the talk key input action when the script starts.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
ConvaiInputManager.Instance.GetTalkKeyAction().started += ConvaiInputManager_TalkKeyActionStarted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from the talk key input action when the script is destroyed.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
ConvaiInputManager.Instance.GetTalkKeyAction().started -= ConvaiInputManager_TalkKeyActionStarted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the talk key action and triggers a notification if no active NPC is available.
|
||||
/// </summary>
|
||||
/// <param name="input">The input context of the talk key action.</param>
|
||||
private void ConvaiInputManager_TalkKeyActionStarted(InputAction.CallbackContext input)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!input.action.WasPressedThisFrame() || UIUtilities.IsAnyInputFieldFocused() || ConvaiNPCManager.Instance.activeConvaiNPC == null ||
|
||||
ConvaiNPCManager.Instance.CheckForNPCToNPCConversation(ConvaiNPCManager.Instance.activeConvaiNPC))
|
||||
if (ConvaiNPCManager.Instance.activeConvaiNPC == null && ConvaiNPCManager.Instance.nearbyNPC == null)
|
||||
NotificationSystemHandler.Instance.NotificationRequest(NotificationType.NotCloseEnoughForConversation);
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
Debug.Log("No active NPC available for conversation");
|
||||
}
|
||||
}
|
||||
#elif ENABLE_LEGACY_INPUT_MANAGER
|
||||
private void Update()
|
||||
{
|
||||
if (ConvaiInputManager.Instance.WasTalkKeyPressed())
|
||||
{
|
||||
if (ConvaiNPCManager.Instance.activeConvaiNPC == null)
|
||||
NotificationSystemHandler.Instance.NotificationRequest(NotificationType.NotCloseEnoughForConversation);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd477455ea76a6c46b64614d87aa55b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,93 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
public class MicrophoneInputChecker : MonoBehaviour
|
||||
{
|
||||
// Duration for microphone input check.
|
||||
private const float INPUT_CHECK_DURATION = 3f;
|
||||
|
||||
// Microphone sensitivity, adjust as needed.
|
||||
private const float SENSITIVITY = 10f;
|
||||
|
||||
// Threshold level to detect microphone issues.
|
||||
private const float THRESHOLD = 0.1f;
|
||||
|
||||
// Reference to the TalkButtonDurationChecker script to check the talk button status.
|
||||
private TalkButtonDurationChecker _talkButtonDurationChecker;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Find and assign the TalkButtonDurationChecker instance in the scene.
|
||||
_talkButtonDurationChecker = FindObjectOfType<TalkButtonDurationChecker>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the microphone is working by analyzing the provided AudioClip.
|
||||
/// </summary>
|
||||
/// <param name="audioClip">The audio clip to analyze.</param>
|
||||
public void IsMicrophoneWorking(AudioClip audioClip)
|
||||
{
|
||||
// Stop any existing coroutines to ensure a clean start.
|
||||
StopAllCoroutines();
|
||||
|
||||
// Start the coroutine to check the microphone device.
|
||||
StartCoroutine(CheckMicrophoneDevice(audioClip));
|
||||
}
|
||||
|
||||
// Coroutine to check the microphone device after a specified duration.
|
||||
private IEnumerator CheckMicrophoneDevice(AudioClip audioClip)
|
||||
{
|
||||
// Check if the provided AudioClip is null.
|
||||
if (audioClip == null)
|
||||
{
|
||||
// Log an error and abort the microphone check.
|
||||
Logger.Error("AudioClip is null!", Logger.LogCategory.Character);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Wait for the specified duration before analyzing microphone input.
|
||||
yield return new WaitForSeconds(INPUT_CHECK_DURATION);
|
||||
|
||||
// If the talk button was released prematurely, abort the microphone check.
|
||||
if (_talkButtonDurationChecker.isTalkKeyReleasedEarly) yield break;
|
||||
|
||||
// Calculate the range of audio samples to analyze based on the duration.
|
||||
int sampleStart = 0;
|
||||
int sampleEnd = (int)(INPUT_CHECK_DURATION * audioClip.frequency * audioClip.channels);
|
||||
|
||||
// Initialize an array to store audio samples.
|
||||
float[] samples = new float[sampleEnd - sampleStart];
|
||||
int samplesLength = samples.Length;
|
||||
|
||||
// Attempt to retrieve audio data from the AudioClip.
|
||||
if (audioClip.GetData(samples, sampleStart) == false)
|
||||
{
|
||||
Logger.Error("Failed to get audio data!", Logger.LogCategory.Character);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Initialize a variable to store the total absolute level of audio samples.
|
||||
float level = 0;
|
||||
|
||||
// Calculate the total absolute level of audio samples.
|
||||
for (int i = 0; i < samplesLength; i++) level += Mathf.Abs(samples[i] * SENSITIVITY);
|
||||
|
||||
// Normalize the calculated level by dividing it by the number of samples and then multiply by sensitivity.
|
||||
level = level / samplesLength * SENSITIVITY;
|
||||
|
||||
// Check if the microphone level is below the threshold, indicating a potential issue.
|
||||
if (level < THRESHOLD)
|
||||
{
|
||||
Logger.Warn("Microphone Issue Detected!", Logger.LogCategory.Character);
|
||||
NotificationSystemHandler.Instance.NotificationRequest(NotificationType.MicrophoneIssue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log that the microphone is working fine.
|
||||
Logger.Info("Microphone is working fine.", Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 596b00c62fa88c645938df61b488e084
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
public class NetworkReachabilityChecker : MonoBehaviour
|
||||
{
|
||||
private void Start()
|
||||
{
|
||||
// Variable to store the debug text for network reachability status
|
||||
string networkStatusDebugText = "";
|
||||
|
||||
switch (Application.internetReachability)
|
||||
{
|
||||
// Check the current internet reachability status
|
||||
case NetworkReachability.NotReachable:
|
||||
// If the device is not reachable over the internet, set debug text and send a notification.
|
||||
networkStatusDebugText = "Not Reachable";
|
||||
NotificationSystemHandler.Instance.NotificationRequest(NotificationType.NetworkReachabilityIssue);
|
||||
break;
|
||||
case NetworkReachability.ReachableViaCarrierDataNetwork:
|
||||
// Reachable via mobile data network
|
||||
networkStatusDebugText = "Reachable via Carrier Data Network";
|
||||
break;
|
||||
case NetworkReachability.ReachableViaLocalAreaNetwork:
|
||||
// Reachable via local area network
|
||||
networkStatusDebugText = "Reachable via Local Area Network";
|
||||
break;
|
||||
}
|
||||
|
||||
// Log the network reachability status for debugging
|
||||
Logger.Info("Network Reachability: " + networkStatusDebugText, Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0001b07d59270994ba1cacc80c615eb4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,70 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// Handles the activation status of the notification system based on Settings Panel Toggle.
|
||||
public class NotificationSystemActiveStatusHandler : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Toggle _notificationSystemActiveStatusToggle;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Subscribe to the toggle's value change event.
|
||||
_notificationSystemActiveStatusToggle.onValueChanged.AddListener(SetActiveStatus);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to events when this component is enabled.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
// Subscribe to the event when saved data is loaded.
|
||||
UISaveLoadSystem.Instance.OnLoad += UISaveLoadSystem_OnLoad;
|
||||
|
||||
// Subscribe to the event when data is saved.
|
||||
UISaveLoadSystem.Instance.OnSave += UISaveLoadSystem_OnSave;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from events when this component is disabled.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
// Subscribe to the event when saved data is loaded.
|
||||
UISaveLoadSystem.Instance.OnLoad -= UISaveLoadSystem_OnLoad;
|
||||
|
||||
// Subscribe to the event when data is saved.
|
||||
UISaveLoadSystem.Instance.OnSave -= UISaveLoadSystem_OnSave;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event handler for when saved data is loaded.
|
||||
/// </summary>
|
||||
private void UISaveLoadSystem_OnLoad()
|
||||
{
|
||||
// Retrieve the saved notification system activation status.
|
||||
bool newValue = UISaveLoadSystem.Instance.NotificationSystemActiveStatus;
|
||||
|
||||
// Update the UI and internal status based on the loaded value.
|
||||
SetActiveStatus(newValue);
|
||||
_notificationSystemActiveStatusToggle.isOn = newValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event handler for when data is saved.
|
||||
/// </summary>
|
||||
private void UISaveLoadSystem_OnSave()
|
||||
{
|
||||
// Save the current notification system activation status.
|
||||
UISaveLoadSystem.Instance.NotificationSystemActiveStatus = _notificationSystemActiveStatusToggle.isOn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the activation status of the notification system.
|
||||
/// </summary>
|
||||
/// <param name="value"> The new activation status. </param>
|
||||
public void SetActiveStatus(bool value)
|
||||
{
|
||||
// Call the NotificationSystemHandler to update the activation status.
|
||||
NotificationSystemHandler.Instance.SetNotificationSystemActiveStatus(value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c5289bfc72186f40b90ff7b9d45894a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the notification system's behavior and interactions.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public class NotificationSystemHandler : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Array containing predefined notification configurations.
|
||||
/// This array can be modified in the Unity Editor to define different types of notifications.
|
||||
/// </summary>
|
||||
[SerializeField] private SONotificationGroup _notificationGroup;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a notification is requested.
|
||||
/// </summary>
|
||||
public Action<SONotification> OnNotificationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating whether the notification system is currently active.
|
||||
/// </summary>
|
||||
private bool _isNotificationSystemActive = true;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the NotificationSystemHandler.
|
||||
/// </summary>
|
||||
public static NotificationSystemHandler Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ensure there is only one instance of NotificationSystemHandler.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null)
|
||||
{
|
||||
Debug.Log("<color=red> There's More Than One NotificationSystemHandler </color> " + transform + " - " +
|
||||
Instance);
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests a notification of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="notificationType">The type of notification to request.</param>
|
||||
public void NotificationRequest(NotificationType notificationType)
|
||||
{
|
||||
// Check if the notification system is currently active.
|
||||
if (!_isNotificationSystemActive) return;
|
||||
|
||||
// Search for the requested notification type in the predefined array.
|
||||
SONotification requestedSONotification = null;
|
||||
foreach (SONotification notification in _notificationGroup.SONotifications)
|
||||
if (notification.NotificationType == notificationType)
|
||||
{
|
||||
requestedSONotification = notification;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the requested notification is not found, log an error.
|
||||
if (requestedSONotification == null)
|
||||
{
|
||||
Debug.LogError("There is no Notification defined for the selected Notification Type!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoke the OnNotificationRequested event with the requested notification.
|
||||
OnNotificationRequested?.Invoke(requestedSONotification);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the activation status of the notification system.
|
||||
/// </summary>
|
||||
/// <param name="value">The new activation status.</param>
|
||||
public void SetNotificationSystemActiveStatus(bool value)
|
||||
{
|
||||
_isNotificationSystemActive = value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d911410153c6d594098cac3c3bfa456d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,36 @@
|
||||
/// <summary>
|
||||
/// Enumeration defining various types of in-app notifications.
|
||||
/// Each enum value represents a specific scenario or issue that can trigger a notification.
|
||||
/// </summary>
|
||||
public enum NotificationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a notification related to microphone problems.
|
||||
/// </summary>
|
||||
MicrophoneIssue,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a notification related to network reachability issues.
|
||||
/// </summary>
|
||||
NetworkReachabilityIssue,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a notification when the user is not in close proximity to initiate a conversation.
|
||||
/// </summary>
|
||||
NotCloseEnoughForConversation,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a notification when a user releases the talk button prematurely during a conversation.
|
||||
/// </summary>
|
||||
TalkButtonReleasedEarly,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that no microphone device was detected in the system
|
||||
/// </summary>
|
||||
NoMicrophoneDetected,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that no API key was found.
|
||||
/// </summary>
|
||||
APIKeyNotFound,
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18458e12a4b7457da0eb049ea8d56d4c
|
||||
timeCreated: 1698156821
|
||||
@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents a notification in the game.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Convai/Notification System/Notification", fileName = "New Notification")]
|
||||
public class SONotification : ScriptableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the notification.
|
||||
/// </summary>
|
||||
[Tooltip("The type of the notification.")]
|
||||
public NotificationType NotificationType;
|
||||
|
||||
/// <summary>
|
||||
/// The icon to be displayed with the notification.
|
||||
/// </summary>
|
||||
[Tooltip("The icon to be displayed with the notification.")]
|
||||
public Sprite Icon;
|
||||
|
||||
/// <summary>
|
||||
/// The notification title.
|
||||
/// </summary>
|
||||
[Tooltip("The notification title.")]
|
||||
public string NotificationTitle;
|
||||
/// <summary>
|
||||
/// The text content of the notification.
|
||||
/// </summary>
|
||||
[TextArea(10, 10)] [Tooltip("The text content of the notification.")]
|
||||
public string NotificationMessage;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b33bf54ff467c742a84ac58d34105ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,15 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a group of notifications as a ScriptableObject.
|
||||
/// This allows for easy configuration and management of different notifications in the Unity Editor.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Convai/Notification System/Notification Group", fileName = "New Notification Group")]
|
||||
public class SONotificationGroup : ScriptableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Array of SONotification objects.
|
||||
/// Each object represents a unique notification that can be triggered in the application.
|
||||
/// </summary>
|
||||
public SONotification[] SONotifications;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73c98f07d31af334ba49c31a867600b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0323cdb3f17fa914cae382e617430dd2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/API Key Not Found Notification.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/API Key Not Found Notification.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87748cd0f7abedf4e8dd7cf60e5fb99a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Convai Default Notification Group.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Convai Default Notification Group.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe9295a7cd110d545b49f77fcc49c489
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Microphone Issue Notification.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Microphone Issue Notification.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e8394cce5330644594a848783844973
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Network Reachability Issue Notification.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Network Reachability Issue Notification.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a58608b2e0aa77418e15e4b4ef0a1fa
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/No Microphone Detected Notification.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/No Microphone Detected Notification.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee7d034a751672c449ab90856e05919c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Not Close Enough For Conversation Notification.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Not Close Enough For Conversation Notification.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 374f6f70a1f7d9546926f20184467b32
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Talk Button Released Early Notification.asset
(Stored with Git LFS)
Normal file
BIN
Assets/Convai/Scripts/Runtime/Addons/NotificationSystem/Scriptable Objects/Talk Button Released Early Notification.asset
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1dbb77ab53e0d714a9f00cba95a25a46
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,115 @@
|
||||
using Convai.Scripts;
|
||||
using Convai.Scripts.Utils;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors the duration of the talk button press and notifies the Notification System if released prematurely.
|
||||
/// </summary>
|
||||
public class TalkButtonDurationChecker : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum duration required for a valid talk action.
|
||||
/// </summary>
|
||||
private const float MIN_TALK_DURATION = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating whether the talk button was released prematurely.
|
||||
/// </summary>
|
||||
[HideInInspector] public bool isTalkKeyReleasedEarly;
|
||||
|
||||
private TMP_InputField _activeInputField;
|
||||
|
||||
/// <summary>
|
||||
/// Timer to track the duration of the talk button press.
|
||||
/// </summary>
|
||||
private float _timer;
|
||||
|
||||
private UIAppearanceSettings _uiAppearanceSettings;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_uiAppearanceSettings = FindObjectOfType<UIAppearanceSettings>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update is called once per frame.
|
||||
/// It checks if the talk button is being held down or released.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
// Check if the talk button is being held down.
|
||||
if (ConvaiInputManager.Instance.IsTalkKeyHeld() && !UIUtilities.IsAnyInputFieldFocused())
|
||||
// Increment the timer based on the time passed since the last frame.
|
||||
_timer += Time.deltaTime;
|
||||
|
||||
// Check if the talk button is released.
|
||||
if (ConvaiInputManager.Instance.WasTalkKeyReleased() && !UIUtilities.IsAnyInputFieldFocused())
|
||||
{
|
||||
if (_activeInputField != null && _activeInputField.isFocused)
|
||||
{
|
||||
_timer = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
CheckTalkButtonRelease();
|
||||
// Reset the timer for the next talk action.
|
||||
_timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged += ConvaiNPCManager_OnActiveNPCChanged;
|
||||
_uiAppearanceSettings.OnAppearanceChanged += UIAppearanceSettings_OnAppearanceChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged -= ConvaiNPCManager_OnActiveNPCChanged;
|
||||
_uiAppearanceSettings.OnAppearanceChanged -= UIAppearanceSettings_OnAppearanceChanged;
|
||||
}
|
||||
|
||||
private void ConvaiNPCManager_OnActiveNPCChanged(ConvaiNPC convaiNpc)
|
||||
{
|
||||
if (convaiNpc == null)
|
||||
{
|
||||
_activeInputField = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_activeInputField = convaiNpc.playerInteractionManager.FindActiveInputField();
|
||||
}
|
||||
|
||||
private void UIAppearanceSettings_OnAppearanceChanged()
|
||||
{
|
||||
ConvaiNPC convaiNpc = ConvaiNPCManager.Instance.activeConvaiNPC;
|
||||
if (convaiNpc == null)
|
||||
{
|
||||
_activeInputField = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_activeInputField = convaiNpc.playerInteractionManager.FindActiveInputField();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the talk button was released prematurely and triggers a notification if so.
|
||||
/// </summary>
|
||||
private void CheckTalkButtonRelease()
|
||||
{
|
||||
// Initialize the flag to false.
|
||||
isTalkKeyReleasedEarly = false;
|
||||
|
||||
// Trigger a notification if the talk button is released before reaching the minimum required duration.
|
||||
if (_timer < MIN_TALK_DURATION)
|
||||
{
|
||||
// Check if there is an active ConvaiNPC.
|
||||
if (ConvaiNPCManager.Instance.activeConvaiNPC == null) return;
|
||||
|
||||
// Set the flag to true and request a notification.
|
||||
isTalkKeyReleasedEarly = true;
|
||||
NotificationSystemHandler.Instance.NotificationRequest(NotificationType.TalkButtonReleasedEarly);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce5db0c0354de754f99bd35c9f7fb96a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a UI notification element that can be activated or deactivated.
|
||||
/// </summary>
|
||||
public class UINotification : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The RectTransform of the notification UI element.
|
||||
/// </summary>
|
||||
public RectTransform NotificationRectTransform;
|
||||
|
||||
/// <summary>
|
||||
/// The image component for displaying the notification icon.
|
||||
/// </summary>
|
||||
[SerializeField] private Image _notificationIcon;
|
||||
|
||||
/// <summary>
|
||||
/// The TextMeshProUGUI component for displaying the notification title.
|
||||
/// </summary>
|
||||
[SerializeField] private TextMeshProUGUI _notificationTitleText;
|
||||
|
||||
/// <summary>
|
||||
/// The TextMeshProUGUI component for displaying the notification text.
|
||||
/// </summary>
|
||||
[SerializeField] private TextMeshProUGUI _notificationMessageText;
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the notification UI element on awake.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the UI notification with the provided Notification data.
|
||||
/// </summary>
|
||||
/// <param name="soNotification">The notification data to initialize the UI notification with.</param>
|
||||
public void Initialize(SONotification soNotification)
|
||||
{
|
||||
if (soNotification == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(soNotification), "SONotification is null.");
|
||||
}
|
||||
|
||||
// Set the notification icon and text based on the provided Notification.
|
||||
_notificationIcon.sprite = soNotification.Icon;
|
||||
_notificationTitleText.text = soNotification.NotificationTitle;
|
||||
_notificationMessageText.text = soNotification.NotificationMessage;
|
||||
|
||||
// Activate the notification UI element.
|
||||
SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the active state of the notification UI element.
|
||||
/// </summary>
|
||||
/// <param name="value">The new active state.</param>
|
||||
public void SetActive(bool value)
|
||||
{
|
||||
gameObject.SetActive(value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 894cc2b4c2298fb4a98bd3d9f2e8d6ba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,316 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// This class is responsible for controlling the UI notifications in the game.
|
||||
/// It handles the creation, activation, deactivation, and animation of notifications.
|
||||
/// </summary>
|
||||
public class UINotificationController : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of notifications that can be displayed at the same time.
|
||||
/// </summary>
|
||||
private const int MAX_NUMBER_OF_NOTIFICATION_AT_SAME_TIME = 3;
|
||||
|
||||
/// <summary>
|
||||
/// References to the UI notification prefab and other necessary components.
|
||||
/// </summary>
|
||||
[Header("References")]
|
||||
[SerializeField] private UINotification _uiNotificationPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// Spacing between Notifications
|
||||
/// </summary>
|
||||
[Header("Configurations")]
|
||||
[SerializeField] private int _spacing = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Position for Active Notification
|
||||
/// </summary>
|
||||
[Tooltip("Starting position for the first notification; Y value adjusts sequentially for each subsequent notification.")]
|
||||
[SerializeField] private Vector2 _activeNotificationPos;
|
||||
|
||||
/// <summary>
|
||||
/// Position for Deactivated Notification
|
||||
/// </summary>
|
||||
[SerializeField] private Vector2 _deactivatedNotificationPos;
|
||||
|
||||
|
||||
[Header("UI Notification Animation Values")]
|
||||
[SerializeField] private float _activeDuration = 4f;
|
||||
[SerializeField] private float _slipDuration = 0.3f;
|
||||
[SerializeField] private float _delay = 0.3f;
|
||||
[SerializeField] private AnimationCurve _slipAnimationCurve;
|
||||
private readonly float _fadeInDuration = 0.35f;
|
||||
private readonly float _fadeOutDuration = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating whether a UI notification movement animation is currently in progress.
|
||||
/// Used to prevent overlapping animation coroutines for UI notifications.
|
||||
/// </summary>
|
||||
private bool _isNotificationAnimationInProgress;
|
||||
private Queue<UINotification> _activeUINotifications;
|
||||
private Queue<UINotification> _deactivatedUINotifications;
|
||||
private CanvasGroup _canvasGroup;
|
||||
private FadeCanvas _fadeCanvas;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// List to keep track of the order in which pending notifications were requested.
|
||||
/// </summary>
|
||||
private readonly List<SONotification> _pendingNotificationsOrder = new();
|
||||
|
||||
/// <summary>
|
||||
/// Awake is called when the script instance is being loaded.
|
||||
/// It is used to initialize any variables or game state before the game starts.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
// Get necessary components and initialize UI notifications.
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
_fadeCanvas = GetComponent<FadeCanvas>();
|
||||
InitializeUINotifications();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function is called when the object becomes enabled and active.
|
||||
/// It is used to subscribe to the OnNotificationRequested event.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
NotificationSystemHandler.Instance.OnNotificationRequested += NotificationSystemHandler_OnNotificationRequested;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function is called when the behaviour becomes disabled or inactive.
|
||||
/// It is used to unsubscribe from the OnNotificationRequested event.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
NotificationSystemHandler.Instance.OnNotificationRequested -= NotificationSystemHandler_OnNotificationRequested;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a new notification request by adding it to the order list and attempting to initialize it.
|
||||
/// If a notification animation is already in progress, waits for it to complete before processing the new request.
|
||||
/// </summary>
|
||||
/// <param name="SONotification">The requested SONotification to be processed.</param>
|
||||
private void NotificationSystemHandler_OnNotificationRequested(SONotification SONotification)
|
||||
{
|
||||
// Add the requested notification to the order list and try to initialize it.
|
||||
_pendingNotificationsOrder.Add(SONotification);
|
||||
|
||||
// If a notification animation is already in progress, wait for it to complete before processing the new request.
|
||||
if (_isNotificationAnimationInProgress) return;
|
||||
|
||||
// If initialization fails, return
|
||||
if (TryInitializeNewNotification(SONotification, out UINotification uiNotification) == false) return;
|
||||
|
||||
// Start the coroutine for UI notification animations
|
||||
StartNotificationUICoroutine(uiNotification);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function is used to initialize the UI notifications.
|
||||
/// It initializes the queues for active and deactivated UI notifications and instantiates and enqueues deactivated UI
|
||||
/// notifications.
|
||||
/// </summary>
|
||||
private void InitializeUINotifications()
|
||||
{
|
||||
// Initialize the queues for active and deactivated UI notifications.
|
||||
_activeUINotifications = new Queue<UINotification>();
|
||||
_deactivatedUINotifications = new Queue<UINotification>();
|
||||
|
||||
// Instantiate and enqueue deactivated UI notifications.
|
||||
for (int i = 0; i < MAX_NUMBER_OF_NOTIFICATION_AT_SAME_TIME; i++)
|
||||
{
|
||||
UINotification uiNotification = Instantiate(_uiNotificationPrefab, transform);
|
||||
|
||||
// Initialize Position
|
||||
uiNotification.NotificationRectTransform.anchoredPosition = _deactivatedNotificationPos;
|
||||
_deactivatedUINotifications.Enqueue(uiNotification);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to initialize a new UI notification using the provided SONotification.
|
||||
/// Tries to get an available UI notification and initializes it with the given SONotification.
|
||||
/// </summary>
|
||||
/// <param name="SONotification">The SONotification to be used for initializing the UI notification.</param>
|
||||
/// <param name="uiNotification">The initialized UINotification if successful, otherwise null.</param>
|
||||
/// <returns>True if initialization is successful, false otherwise.</returns>
|
||||
private bool TryInitializeNewNotification(SONotification SONotification, out UINotification uiNotification)
|
||||
{
|
||||
// Try to get an available UI notification and initialize it with the given SONotification.
|
||||
uiNotification = GetAvailableUINotification();
|
||||
if (uiNotification == null) return false;
|
||||
|
||||
uiNotification.Initialize(SONotification);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the coroutine for UI notification animations and adds the notification to the active queue.
|
||||
/// </summary>
|
||||
/// <param name="uiNotification">The UINotification to be animated and added to the active queue.</param>
|
||||
private void StartNotificationUICoroutine(UINotification uiNotification)
|
||||
{
|
||||
// Define additional delay for smoother notification end transition
|
||||
float extraDelayForNotificationEndTransition = 0.5f;
|
||||
|
||||
// Calculate the total duration including fadeIn, activeDuration, slipDuration (for both activation and deactivation), delay, and extra delay
|
||||
float totalAnimationDuration = _fadeInDuration + _activeDuration + (2 * _slipDuration) + _delay + extraDelayForNotificationEndTransition;
|
||||
|
||||
// Start the fade animation for the canvas group
|
||||
_fadeCanvas.StartFadeInFadeOutWithGap(_canvasGroup, _fadeInDuration, _fadeOutDuration, totalAnimationDuration);
|
||||
|
||||
// Enqueue the notification to the active queue
|
||||
_activeUINotifications.Enqueue(uiNotification);
|
||||
|
||||
// Start the coroutine for individual UI notification animations
|
||||
StartCoroutine(StartNotificationUI(uiNotification));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine for managing the lifecycle of a UI notification, including its activation, display duration, and deactivation.
|
||||
/// </summary>
|
||||
/// <param name="uiNotification">The UINotification to be managed.</param>
|
||||
private IEnumerator StartNotificationUI(UINotification uiNotification)
|
||||
{
|
||||
// Remove the notification from the pending list
|
||||
int firstIndex = 0;
|
||||
_pendingNotificationsOrder.RemoveAt(firstIndex);
|
||||
|
||||
// Move to the active position
|
||||
yield return MoveUINotificationToActivePosition(uiNotification);
|
||||
|
||||
// Wait for the active duration
|
||||
yield return new WaitForSeconds(_activeDuration);
|
||||
|
||||
UpdateUINotificationPositions();
|
||||
|
||||
// Move to the hidden position
|
||||
yield return MoveUINotificationToHiddenPosition(uiNotification);
|
||||
|
||||
// Deactivate the UI notification, update positions, and check for pending notifications.
|
||||
DeactivateAndEnqueueUINotification(uiNotification);
|
||||
|
||||
// If there are pending notifications, initialize and start a new one
|
||||
if (AreTherePendingNotifications())
|
||||
{
|
||||
TryInitializeAndStartNewNotification();
|
||||
}
|
||||
|
||||
// Update UI notification positions after the lifecycle is complete
|
||||
UpdateUINotificationPositions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the UI notification to its active position.
|
||||
/// </summary>
|
||||
private IEnumerator MoveUINotificationToActivePosition(UINotification uiNotification)
|
||||
{
|
||||
float targetY = _activeNotificationPos.y - _spacing * (_activeUINotifications.Count - 1);
|
||||
Vector2 targetPos = new Vector2(_activeNotificationPos.x, targetY);
|
||||
yield return StartCoroutine(MoveUINotificationToTargetPos(uiNotification, targetPos));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the UI notification to its hidden position.
|
||||
/// </summary>
|
||||
private IEnumerator MoveUINotificationToHiddenPosition(UINotification uiNotification)
|
||||
{
|
||||
Vector2 targetPos = _deactivatedNotificationPos;
|
||||
yield return StartCoroutine(MoveUINotificationToTargetPos(uiNotification, targetPos));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the UI notification, updates positions, and enqueues it for later use.
|
||||
/// </summary>
|
||||
private void DeactivateAndEnqueueUINotification(UINotification uiNotification)
|
||||
{
|
||||
uiNotification.SetActive(false);
|
||||
_activeUINotifications.Dequeue();
|
||||
_deactivatedUINotifications.Enqueue(uiNotification);
|
||||
UpdateUINotificationPositions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are pending notifications and initializes and starts a new one if available.
|
||||
/// </summary>
|
||||
private void TryInitializeAndStartNewNotification()
|
||||
{
|
||||
if (TryInitializeNewNotification(_pendingNotificationsOrder[0], out UINotification newUiNotification))
|
||||
{
|
||||
StartNotificationUICoroutine(newUiNotification);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smoothly moves the UI notification to the target position over a specified duration.
|
||||
/// </summary>
|
||||
/// <param name="uiNotification">The UINotification to be moved.</param>
|
||||
/// <param name="targetPos">The target position to move the UINotification to.</param>
|
||||
private IEnumerator MoveUINotificationToTargetPos(UINotification uiNotification, Vector2 targetPos)
|
||||
{
|
||||
// Set flag to indicate that a notification animation is in progress
|
||||
_isNotificationAnimationInProgress = true;
|
||||
|
||||
float elapsedTime = 0f;
|
||||
Vector2 startPos = uiNotification.NotificationRectTransform.anchoredPosition;
|
||||
|
||||
// Move the UI notification smoothly to the target position over the specified duration
|
||||
while (elapsedTime <= _slipDuration + _delay)
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
float percent = Mathf.Clamp01(elapsedTime / _slipDuration);
|
||||
float curvePercent = _slipAnimationCurve.Evaluate(percent);
|
||||
uiNotification.NotificationRectTransform.anchoredPosition = Vector2.Lerp(startPos, targetPos, curvePercent);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Reset the flag once the animation is complete
|
||||
_isNotificationAnimationInProgress = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the positions of active UI notifications along the Y-axis.
|
||||
/// </summary>
|
||||
private void UpdateUINotificationPositions()
|
||||
{
|
||||
float targetX = _activeNotificationPos.x;
|
||||
float targetY = _activeNotificationPos.y;
|
||||
|
||||
// Iterate through active UI notifications and move them to their respective positions
|
||||
foreach (UINotification activeUINotification in _activeUINotifications)
|
||||
{
|
||||
Vector2 targetPos = new Vector2(targetX, targetY);
|
||||
StartCoroutine(MoveUINotificationToTargetPos(activeUINotification, targetPos));
|
||||
targetY -= _spacing;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an available UI notification from the deactivated queue.
|
||||
/// </summary>
|
||||
/// <returns>The available UI notification, or null if the deactivated queue is empty.</returns>
|
||||
private UINotification GetAvailableUINotification()
|
||||
{
|
||||
// Check if there are available deactivated UI notifications
|
||||
if (_deactivatedUINotifications.Count == 0) return null;
|
||||
|
||||
// Dequeue and return an available UI notification
|
||||
return _deactivatedUINotifications.Dequeue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there are pending notifications in the order list.
|
||||
/// </summary>
|
||||
/// <returns>True if there are pending notifications, false otherwise.</returns>
|
||||
private bool AreTherePendingNotifications()
|
||||
{
|
||||
// Check if there are any pending notifications in the order list
|
||||
return _pendingNotificationsOrder.Count >= 1;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad1fae26184a1504bbf417585440fe12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Runtime/Addons/Player.meta
Normal file
8
Assets/Convai/Scripts/Runtime/Addons/Player.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe6af3c5595f83b4aaf1e1c05ef9b819
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem.UI;
|
||||
|
||||
#elif ENABLE_LEGACY_INPUT_MANAGER
|
||||
using UnityEngine.EventSystems;
|
||||
#endif
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
public class ConvaiDynamicInputSystem : MonoBehaviour
|
||||
{
|
||||
private void Awake()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
if (FindObjectOfType<InputSystemUIInputModule>() == null) gameObject.AddComponent<InputSystemUIInputModule>();
|
||||
#elif ENABLE_LEGACY_INPUT_MANAGER
|
||||
if (FindObjectOfType<StandaloneInputModule>() == null)
|
||||
{
|
||||
gameObject.AddComponent<StandaloneInputModule>();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7611c6ad1a67fed44afca249d0bcd288
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,134 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for handling player movement including walking, running, jumping, and looking around.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Convai/Player Movement")]
|
||||
[HelpURL("https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/scripts-overview")]
|
||||
public class ConvaiPlayerMovement : MonoBehaviour
|
||||
{
|
||||
[Header("Movement Parameters")] [SerializeField] [Tooltip("The speed at which the player walks.")] [Range(1, 10)]
|
||||
private float walkingSpeed = 3f;
|
||||
|
||||
[SerializeField] [Tooltip("The speed at which the player runs.")] [Range(1, 10)]
|
||||
private float runningSpeed = 8f;
|
||||
|
||||
[SerializeField] [Tooltip("The speed at which the player jumps.")] [Range(1, 10)]
|
||||
private float jumpSpeed = 4f;
|
||||
|
||||
[Header("Gravity & Grounding")] [SerializeField] [Tooltip("The gravity applied to the player.")] [Range(1, 10)]
|
||||
private float gravity = 9.8f;
|
||||
|
||||
[Header("Camera Parameters")] [SerializeField] [Tooltip("The main camera the player uses.")]
|
||||
private Camera playerCamera;
|
||||
|
||||
[SerializeField] [Tooltip("Speed at which the player can look around.")] [Range(0, 1)]
|
||||
private float lookSpeedMultiplier = 0.05f;
|
||||
|
||||
[SerializeField] [Tooltip("Limit of upwards and downwards look angles.")] [Range(1, 90)]
|
||||
private float lookXLimit = 45.0f;
|
||||
|
||||
[HideInInspector] public bool canMove = true;
|
||||
|
||||
private CharacterController _characterController;
|
||||
private Vector3 _moveDirection = Vector3.zero;
|
||||
private float _rotationX;
|
||||
|
||||
//Singleton Instance
|
||||
public static ConvaiPlayerMovement Instance { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Singleton pattern to ensure only one instance exists
|
||||
if (Instance == null)
|
||||
Instance = this;
|
||||
else
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_characterController = GetComponent<CharacterController>();
|
||||
LockCursor();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Handle cursor locking/unlocking
|
||||
HandleCursorLocking();
|
||||
|
||||
// Check for running state and move the player
|
||||
MovePlayer();
|
||||
|
||||
// Handle the player and camera rotation
|
||||
RotatePlayerAndCamera();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlock the cursor when the ESC key is pressed, Re-lock the cursor when the left mouse button is pressed
|
||||
/// </summary>
|
||||
private void HandleCursorLocking()
|
||||
{
|
||||
if (ConvaiInputManager.Instance.WasCursorLockKeyPressed())
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
|
||||
if (ConvaiInputManager.Instance.WasMouseLeftButtonPressed() && !EventSystem.current.IsPointerOverGameObject()) LockCursor();
|
||||
}
|
||||
|
||||
private static void LockCursor()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
|
||||
private void MovePlayer()
|
||||
{
|
||||
Vector3 horizontalMovement = Vector3.zero;
|
||||
|
||||
if (canMove && !EventSystem.current.IsPointerOverGameObject())
|
||||
{
|
||||
Vector3 forward = transform.TransformDirection(Vector3.forward);
|
||||
Vector3 right = transform.TransformDirection(Vector3.right);
|
||||
|
||||
float speed = ConvaiInputManager.Instance.IsRunKeyHeld() ? runningSpeed : walkingSpeed;
|
||||
|
||||
Vector2 moveVector = ConvaiInputManager.Instance.GetPlayerMoveVector();
|
||||
float curSpeedX = speed * moveVector.x;
|
||||
float curSpeedY = speed * moveVector.y;
|
||||
|
||||
horizontalMovement = forward * curSpeedY + right * curSpeedX;
|
||||
|
||||
if (_characterController.isGrounded && ConvaiInputManager.Instance.WasJumpKeyPressed()) _moveDirection.y = jumpSpeed;
|
||||
}
|
||||
|
||||
if (canMove && !_characterController.isGrounded)
|
||||
// Apply gravity only when canMove is true
|
||||
_moveDirection.y -= gravity * Time.deltaTime;
|
||||
|
||||
// Move the character
|
||||
_characterController.Move((_moveDirection + horizontalMovement) * Time.deltaTime);
|
||||
}
|
||||
|
||||
private void RotatePlayerAndCamera()
|
||||
{
|
||||
if (Cursor.lockState != CursorLockMode.Locked) return;
|
||||
|
||||
// Vertical rotation
|
||||
_rotationX -= ConvaiInputManager.Instance.GetMouseYAxis() * lookSpeedMultiplier;
|
||||
_rotationX = Mathf.Clamp(_rotationX, -lookXLimit, lookXLimit);
|
||||
playerCamera.transform.localRotation = Quaternion.Euler(_rotationX, 0, 0);
|
||||
|
||||
// Horizontal rotation
|
||||
float rotationY = ConvaiInputManager.Instance.GetMouseXAxis() * lookSpeedMultiplier;
|
||||
transform.rotation *= Quaternion.Euler(0, rotationY, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: adc3b3c371ebd1543ad6696b74dbbe9f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Convai/Scripts/Runtime/Attributes.meta
Normal file
8
Assets/Convai/Scripts/Runtime/Attributes.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 189248cf557957840a0084f28183b3f9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts
|
||||
{
|
||||
public class ReadOnlyAttribute : PropertyAttribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4af0f963530e4aeca5f5747085ac74fb
|
||||
timeCreated: 1701083156
|
||||
8
Assets/Convai/Scripts/Runtime/Core.meta
Normal file
8
Assets/Convai/Scripts/Runtime/Core.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d932a943c13cad4381fdb6714489c14
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
235
Assets/Convai/Scripts/Runtime/Core/ConvaiBlinkingHandler.cs
Normal file
235
Assets/Convai/Scripts/Runtime/Core/ConvaiBlinkingHandler.cs
Normal file
@ -0,0 +1,235 @@
|
||||
using System.Collections;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
// TODO: Change URL to point to the blinking script documentation after it is created
|
||||
/// <summary>
|
||||
/// Controls the blinking behavior of a character model in Unity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Instructions to find the index of left / right eyelids in BlendShapes:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>Select your character model in the scene which has the SkinnedMeshRenderer component.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>Look for the blend shapes in the SkinnedMeshRenderer component in the Inspector window.</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// The count (from 0) of blend shape until "EyeBlink_L" or similar is the index of the lef
|
||||
/// eyelid.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// The count (from 0) of blend shape until "EyeBlink_R" or similar is the index of the right
|
||||
/// eyelid.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Convai/Character Blinking")]
|
||||
[HelpURL("https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/scripts-overview")]
|
||||
public class ConvaiBlinkingHandler : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("The SkinnedMeshRenderer for the character's face")]
|
||||
private SkinnedMeshRenderer faceSkinnedMeshRenderer;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The index of the left eyelid blend shape in the SkinnedMeshRenderer")]
|
||||
private int indexOfLeftEyelid = -1;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The index of the right eyelid blend shape in the SkinnedMeshRenderer")]
|
||||
private int indexOfRightEyelid = -1;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Maximum value of the blendshape of the eye lid")]
|
||||
private float maxBlendshapeWeight = 1;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The minimum amount of time, in seconds, for a blink. Positive values only.")]
|
||||
[Range(0.1f, 1f)]
|
||||
private float minBlinkDuration = 0.2f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip(
|
||||
"The maximum amount of time, in seconds, for a blink. Must be greater than the minimum blink duration.")]
|
||||
[Range(0.1f, 1f)]
|
||||
private float maxBlinkDuration = 0.3f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The minimum amount of time, in seconds, between blinks. Positive values only.")]
|
||||
[Range(1f, 10f)]
|
||||
private float minBlinkInterval = 2;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip(
|
||||
"The maximum amount of time, in seconds, between blinks. Must be greater than the minimum blink interval.")]
|
||||
[Range(1f, 10f)]
|
||||
private float maxBlinkInterval = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the settings for eyelid blinking on a character's SkinnedMeshRenderer blend shapes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method executes the following sequence of operations:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// Checks if the SkinnedMeshRenderer is associated with the character's face. If it is not found,
|
||||
/// it logs an error and returns.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// If the indices of the left and right eyelids are not set (i.e., they are -1), it iterates over
|
||||
/// the blend shapes of the SkinnedMeshRenderer to find these indices. It uses regex to match blend shapes'
|
||||
/// names, looking for "eye" and "blink" in combination with either "_l" for left or "_r" for right
|
||||
/// indicators. The appropriate indices found are stored in PlayerPrefs for caching purposes.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private void Start()
|
||||
{
|
||||
string npcName = GetComponent<ConvaiNPC>().characterName; // fetch NPC name from ConvaiNPC script
|
||||
string leftBlinkKey = npcName + "LeftEyelid";
|
||||
string rightBlinkKey = npcName + "RightEyelid";
|
||||
|
||||
if (indexOfLeftEyelid == -1)
|
||||
indexOfLeftEyelid = PlayerPrefs.GetInt(leftBlinkKey, -1);
|
||||
if (indexOfRightEyelid == -1)
|
||||
indexOfRightEyelid = PlayerPrefs.GetInt(rightBlinkKey, -1);
|
||||
|
||||
if (faceSkinnedMeshRenderer == null)
|
||||
faceSkinnedMeshRenderer = GetSkinnedMeshRendererWithRegex(transform);
|
||||
|
||||
if (faceSkinnedMeshRenderer != null)
|
||||
{
|
||||
// If we couldn't retrieve the indices from cache, we search for them in our mesh
|
||||
if (indexOfLeftEyelid == -1 || indexOfRightEyelid == -1)
|
||||
{
|
||||
for (int i = 0; i < faceSkinnedMeshRenderer.sharedMesh.blendShapeCount; i++)
|
||||
{
|
||||
string blendShapeName = faceSkinnedMeshRenderer.sharedMesh.GetBlendShapeName(i).ToLower();
|
||||
if (indexOfLeftEyelid == -1 && Regex.IsMatch(blendShapeName, @"(eye).*(blink).*(l|left)"))
|
||||
{
|
||||
indexOfLeftEyelid = i;
|
||||
PlayerPrefs.SetInt(leftBlinkKey, i);
|
||||
}
|
||||
else if (indexOfRightEyelid == -1 && Regex.IsMatch(blendShapeName, @"(eye).*(blink).*(r|right)"))
|
||||
{
|
||||
indexOfRightEyelid = i;
|
||||
PlayerPrefs.SetInt(rightBlinkKey, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (indexOfLeftEyelid == -1 || indexOfRightEyelid == -1)
|
||||
{
|
||||
Logger.Error("Left and/or Right eyelid blend shapes not found!", Logger.LogCategory.Character);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error("No SkinnedMeshRenderer found with matching name.", Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
StartCoroutine(BlinkCoroutine());
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
maxBlinkDuration = Mathf.Max(minBlinkDuration, maxBlinkDuration);
|
||||
maxBlinkInterval = Mathf.Max(minBlinkInterval, maxBlinkInterval);
|
||||
}
|
||||
|
||||
private SkinnedMeshRenderer GetSkinnedMeshRendererWithRegex(Transform parentTransform)
|
||||
{
|
||||
SkinnedMeshRenderer findFaceSkinnedMeshRenderer = null;
|
||||
Regex regexPattern = new("(.*_Head|CC_Base_Body)");
|
||||
|
||||
foreach (Transform child in parentTransform)
|
||||
if (regexPattern.IsMatch(child.name))
|
||||
{
|
||||
findFaceSkinnedMeshRenderer = child.GetComponent<SkinnedMeshRenderer>();
|
||||
|
||||
if (findFaceSkinnedMeshRenderer != null) break;
|
||||
}
|
||||
|
||||
return findFaceSkinnedMeshRenderer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine that controls the blinking behavior of the character.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This coroutine is designed to perform a sequence of blinking actions where it:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description>Closes the eyes smoothly over half of the defined 'blinkDuration'</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>Waits for the defined 'blinkDuration'</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>Opens the eyes smoothly over half of the defined 'blinkDuration'</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>Waits for a randomized interval time before repeating the blinking process</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <returns>Enumerator to control the sequence of this coroutine</returns>
|
||||
private IEnumerator BlinkCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
float blinkDuration = Random.Range(minBlinkDuration, maxBlinkDuration);
|
||||
float blinkInterval = Random.Range(minBlinkInterval, maxBlinkInterval);
|
||||
|
||||
// Blink the character's eyes over the course of the blinkDuration
|
||||
for (float t = 0.0f; t < blinkDuration; t += Time.deltaTime)
|
||||
{
|
||||
float normalizedTime = t / blinkDuration;
|
||||
SetEyelidsBlendShapeWeight(maxBlendshapeWeight * normalizedTime); // Increase the weight of the blend shape to affect the character's model
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetEyelidsBlendShapeWeight(maxBlendshapeWeight);
|
||||
|
||||
// Wait for blinkDuration seconds, this gives the impression of the eyelids being naturally closed
|
||||
yield return new WaitForSeconds(blinkDuration);
|
||||
|
||||
|
||||
// Now we 'un-blink' the character's eyes over the course of the blinkDuration
|
||||
|
||||
for (float t = 0.0f; t < blinkDuration; t += Time.deltaTime)
|
||||
{
|
||||
float normalizedTime = t / blinkDuration;
|
||||
SetEyelidsBlendShapeWeight(maxBlendshapeWeight - maxBlendshapeWeight * normalizedTime);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
|
||||
yield return new WaitForSeconds(blinkInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the same weight to both eyelids' blend shape.
|
||||
/// </summary>
|
||||
private void SetEyelidsBlendShapeWeight(float weight)
|
||||
{
|
||||
faceSkinnedMeshRenderer.SetBlendShapeWeight(indexOfLeftEyelid, weight);
|
||||
faceSkinnedMeshRenderer.SetBlendShapeWeight(indexOfRightEyelid, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b64bad04a93295642a4486f9899f8734
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
785
Assets/Convai/Scripts/Runtime/Core/ConvaiGRPCAPI.cs
Normal file
785
Assets/Convai/Scripts/Runtime/Core/ConvaiGRPCAPI.cs
Normal file
@ -0,0 +1,785 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Convai.Scripts.Runtime.Utils;
|
||||
using Convai.Scripts.Utils.LipSync;
|
||||
using Google.Protobuf;
|
||||
using Grpc.Core;
|
||||
using Service;
|
||||
using UnityEngine;
|
||||
using static Service.GetResponseRequest.Types;
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
public class WavHeaderParser
|
||||
{
|
||||
public WavHeaderParser(byte[] wavBytes)
|
||||
{
|
||||
// Ensure the byte array is not null and has enough bytes to contain a header
|
||||
if (wavBytes == null || wavBytes.Length < 44)
|
||||
throw new ArgumentException("Invalid WAV byte array.");
|
||||
|
||||
// Parse the number of channels (2 bytes at offset 22)
|
||||
NumChannels = BitConverter.ToInt16(wavBytes, 22);
|
||||
|
||||
// Parse the sample rate (4 bytes at offset 24)
|
||||
SampleRate = BitConverter.ToInt32(wavBytes, 24);
|
||||
|
||||
// Parse the bits per sample (2 bytes at offset 34)
|
||||
BitsPerSample = BitConverter.ToInt16(wavBytes, 34);
|
||||
|
||||
// Parse the Subchunk2 size (data size) to help calculate the data length
|
||||
DataSize = BitConverter.ToInt32(wavBytes, 40);
|
||||
}
|
||||
|
||||
public int SampleRate { get; }
|
||||
public int NumChannels { get; }
|
||||
public int BitsPerSample { get; }
|
||||
public int DataSize { get; }
|
||||
|
||||
public float CalculateDurationSeconds()
|
||||
{
|
||||
// Calculate the total number of samples in the data chunk
|
||||
int totalSamples = DataSize / (NumChannels * (BitsPerSample / 8));
|
||||
|
||||
// Calculate the duration in seconds
|
||||
return (float)totalSamples / SampleRate;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class is dedicated to manage all communications between the Convai server and plugin, in addition to
|
||||
/// processing any data transmitted during these interactions. It abstracts the underlying complexities of the plugin,
|
||||
/// providing a seamless interface for users. Modifications to this class are discouraged as they may impact the
|
||||
/// stability and functionality of the system. This class is maintained by the development team to ensure compatibility
|
||||
/// and performance.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(ConvaiNPCManager))]
|
||||
[AddComponentMenu("Convai/Convai GRPC API")]
|
||||
[HelpURL(
|
||||
"https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/scripts-overview/convaigrpcapi.cs")]
|
||||
public class ConvaiGRPCAPI : MonoBehaviour
|
||||
{
|
||||
public static ConvaiGRPCAPI Instance;
|
||||
private readonly List<string> _stringUserText = new();
|
||||
private ConvaiNPC _activeConvaiNPC;
|
||||
|
||||
private string _apiKey;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
private ConvaiChatUIHandler _chatUIHandler;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Singleton pattern: Ensure only one instance of this script is active.
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
// Load API key from a ScriptableObject in Resources folder.
|
||||
ConvaiAPIKeySetup.GetAPIKey(out _apiKey);
|
||||
|
||||
// Find and store a reference to the ConvaiChatUIHandler component in the scene.
|
||||
_chatUIHandler = FindObjectOfType<ConvaiChatUIHandler>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged += HandleActiveNPCChanged;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
MainThreadDispatcher.CreateInstance();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// Check if there are pending user texts to display
|
||||
// If chatUIHandler is available, send the first user text in the list
|
||||
if (_stringUserText.Count > 0 && _chatUIHandler != null)
|
||||
{
|
||||
_chatUIHandler.SendPlayerText(_stringUserText[0]);
|
||||
// Remove the displayed user text from the list
|
||||
_stringUserText.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged -= HandleActiveNPCChanged;
|
||||
|
||||
InterruptCharacterSpeech(_activeConvaiNPC);
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle the Exception, which can occur if the CancellationTokenSource is already disposed.
|
||||
Logger.Warn("Exception in OnDestroy: " + ex.Message, Logger.LogCategory.Character);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously initializes a session ID by communicating with a gRPC service and returns the session ID if
|
||||
/// successful.
|
||||
/// </summary>
|
||||
/// <param name="characterName">The name of the character for which the session is being initialized.</param>
|
||||
/// <param name="client">The gRPC service client used to make the call to the server.</param>
|
||||
/// <param name="characterID">The unique identifier for the character.</param>
|
||||
/// <param name="sessionID">The session ID that may be updated during the initialization process.</param>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous operation. The task result contains the initialized session ID if
|
||||
/// successful, or null if the initialization fails.
|
||||
/// </returns>
|
||||
public static async Task<string> InitializeSessionIDAsync(string characterName, ConvaiService.ConvaiServiceClient client, string characterID, string sessionID)
|
||||
{
|
||||
Logger.DebugLog("Initializing SessionID for character: " + characterName, Logger.LogCategory.Character);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
Logger.Error("gRPC client is not initialized.", Logger.LogCategory.Character);
|
||||
return null;
|
||||
}
|
||||
|
||||
using AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call = client.GetResponse();
|
||||
GetResponseRequest getResponseConfigRequest = new()
|
||||
{
|
||||
GetResponseConfig = new GetResponseConfig
|
||||
{
|
||||
CharacterId = characterID,
|
||||
ApiKey = Instance._apiKey,
|
||||
SessionId = sessionID,
|
||||
AudioConfig = new AudioConfig { DisableAudio = true }
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await call.RequestStream.WriteAsync(getResponseConfigRequest);
|
||||
await call.RequestStream.WriteAsync(new GetResponseRequest
|
||||
{
|
||||
GetResponseData = new GetResponseData
|
||||
{
|
||||
TextData = "Repeat the following exactly as it is: [Hii]"
|
||||
}
|
||||
});
|
||||
|
||||
await call.RequestStream.CompleteAsync();
|
||||
|
||||
while (await call.ResponseStream.MoveNext())
|
||||
{
|
||||
GetResponseResponse result = call.ResponseStream.Current;
|
||||
|
||||
if (!string.IsNullOrEmpty(result.SessionId))
|
||||
{
|
||||
Logger.DebugLog("SessionID Initialization SUCCESS for: " + characterName,
|
||||
Logger.LogCategory.Character);
|
||||
sessionID = result.SessionId;
|
||||
return sessionID;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Exception("SessionID Initialization FAILED for: " + characterName, Logger.LogCategory.Character);
|
||||
}
|
||||
catch (RpcException rpcException)
|
||||
{
|
||||
switch (rpcException.StatusCode)
|
||||
{
|
||||
case StatusCode.Cancelled:
|
||||
Logger.Exception(rpcException, Logger.LogCategory.Character);
|
||||
break;
|
||||
case StatusCode.Unknown:
|
||||
Logger.Error($"Unknown error from server: {rpcException.Status.Detail}",
|
||||
Logger.LogCategory.Character);
|
||||
break;
|
||||
default:
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Exception(ex, Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sends text data to the server and processes the response.
|
||||
/// </summary>
|
||||
/// <param name="client">The gRPC client used to communicate with the server.</param>
|
||||
/// <param name="userText">The text data to send to the server.</param>
|
||||
/// <param name="characterID">The ID of the character that is sending the text.</param>
|
||||
/// <param name="isActionActive">Indicates whether actions are active.</param>
|
||||
/// <param name="isLipSyncActive">Indicates whether lip sync is active.</param>
|
||||
/// <param name="actionConfig">The action configuration.</param>
|
||||
/// <param name="faceModel">The face model.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task SendTextData(ConvaiService.ConvaiServiceClient client, string userText, string characterID, bool isActionActive, bool isLipSyncActive,
|
||||
ActionConfig actionConfig, FaceModel faceModel)
|
||||
{
|
||||
AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call =
|
||||
GetAsyncDuplexStreamingCallOptions(client);
|
||||
|
||||
GetResponseRequest getResponseConfigRequest = CreateGetResponseRequest(
|
||||
isActionActive,
|
||||
isLipSyncActive,
|
||||
0,
|
||||
characterID,
|
||||
actionConfig,
|
||||
faceModel);
|
||||
|
||||
try
|
||||
{
|
||||
await call.RequestStream.WriteAsync(getResponseConfigRequest);
|
||||
await call.RequestStream.WriteAsync(new GetResponseRequest
|
||||
{
|
||||
GetResponseData = new GetResponseData
|
||||
{
|
||||
TextData = userText
|
||||
}
|
||||
});
|
||||
await call.RequestStream.CompleteAsync();
|
||||
|
||||
// Store the task that receives results from the server.
|
||||
Task receiveResultsTask = Task.Run(
|
||||
async () => { await ReceiveResultFromServer(call, _cancellationTokenSource.Token); },
|
||||
_cancellationTokenSource.Token);
|
||||
|
||||
// Await the task if needed to ensure it completes before this method returns [OPTIONAL]
|
||||
await receiveResultsTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
// This method will be called whenever the active NPC changes.
|
||||
private void HandleActiveNPCChanged(ConvaiNPC newActiveNPC)
|
||||
{
|
||||
if (newActiveNPC != null)
|
||||
InterruptCharacterSpeech(newActiveNPC);
|
||||
|
||||
// Cancel the ongoing gRPC call
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Handle the Exception, which can occur if the CancellationTokenSource is already disposed.
|
||||
Logger.Warn("Exception in GRPCAPI:HandleActiveNPCChanged: " + e.Message,
|
||||
Logger.LogCategory.Character);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
Logger.Info("The Cancellation Token Source was Disposed in GRPCAPI:HandleActiveNPCChanged",
|
||||
Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource(); // Create a new token for future calls
|
||||
_activeConvaiNPC = newActiveNPC;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts recording audio and sends it to the server for processing.
|
||||
/// </summary>
|
||||
/// <param name="client">gRPC service Client object</param>
|
||||
/// <param name="isActionActive">Bool specifying whether we are expecting action responses</param>
|
||||
/// <param name="isLipSyncActive"></param>
|
||||
/// <param name="recordingFrequency">Frequency of the audio being sent</param>
|
||||
/// <param name="recordingLength">Length of the recording from the microphone</param>
|
||||
/// <param name="characterID">Character ID obtained from the playground</param>
|
||||
/// <param name="actionConfig">Object containing the action configuration</param>
|
||||
/// <param name="faceModel"></param>
|
||||
public async Task StartRecordAudio(ConvaiService.ConvaiServiceClient client, bool isActionActive, bool isLipSyncActive, int recordingFrequency, int recordingLength,
|
||||
string characterID, ActionConfig actionConfig, FaceModel faceModel)
|
||||
{
|
||||
AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call = GetAsyncDuplexStreamingCallOptions(client);
|
||||
|
||||
GetResponseRequest getResponseConfigRequest = CreateGetResponseRequest(isActionActive, isLipSyncActive, recordingFrequency, characterID, actionConfig, faceModel);
|
||||
|
||||
Logger.DebugLog(getResponseConfigRequest.ToString(), Logger.LogCategory.Character);
|
||||
|
||||
try
|
||||
{
|
||||
await call.RequestStream.WriteAsync(getResponseConfigRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, Logger.LogCategory.Character);
|
||||
return; // early return on error
|
||||
}
|
||||
|
||||
AudioClip audioClip = Microphone.Start(MicrophoneManager.Instance.SelectedMicrophoneName, false, recordingLength, recordingFrequency);
|
||||
|
||||
MicrophoneTestController.Instance.CheckMicrophoneDeviceWorkingStatus(audioClip);
|
||||
|
||||
Logger.Info(_activeConvaiNPC.characterName + " is now listening", Logger.LogCategory.Character);
|
||||
OnPlayerSpeakingChanged?.Invoke(true);
|
||||
|
||||
await ProcessAudioContinuously(call, recordingFrequency, recordingLength, audioClip);
|
||||
}
|
||||
|
||||
private AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> GetAsyncDuplexStreamingCallOptions(ConvaiService.ConvaiServiceClient client)
|
||||
{
|
||||
Metadata headers = new()
|
||||
{
|
||||
{ "source", "Unity" },
|
||||
{ "version", "3.0.0" }
|
||||
|
||||
};
|
||||
|
||||
CallOptions options = new(headers);
|
||||
return client.GetResponse(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a GetResponseRequest object configured with the specified parameters for initiating a gRPC call.
|
||||
/// </summary>
|
||||
/// <param name="isActionActive">Indicates whether actions are enabled for the character.</param>
|
||||
/// <param name="isLipSyncActive">Indicates whether lip sync is enabled for the character.</param>
|
||||
/// <param name="recordingFrequency">The frequency at which the audio is recorded.</param>
|
||||
/// <param name="characterID">The unique identifier for the character.</param>
|
||||
/// <param name="actionConfig">The configuration for character actions.</param>
|
||||
/// <param name="faceModel">The facial model configuration for the character.</param>
|
||||
/// <returns>A GetResponseRequest object configured with the provided settings.</returns>
|
||||
private GetResponseRequest CreateGetResponseRequest(bool isActionActive, bool isLipSyncActive, int recordingFrequency, string characterID, ActionConfig actionConfig = null,
|
||||
FaceModel faceModel = FaceModel.OvrModelName)
|
||||
{
|
||||
GetResponseRequest getResponseConfigRequest = new()
|
||||
{
|
||||
GetResponseConfig = new GetResponseConfig
|
||||
{
|
||||
CharacterId = characterID,
|
||||
ApiKey = _apiKey, // Assumes apiKey is available
|
||||
SessionId = _activeConvaiNPC.sessionID, // Assumes _activeConvaiNPC would not be null, else this will throw NullReferenceException
|
||||
|
||||
AudioConfig = new AudioConfig
|
||||
{
|
||||
SampleRateHertz = recordingFrequency,
|
||||
EnableFacialData = isLipSyncActive,
|
||||
FaceModel = faceModel
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isActionActive || _activeConvaiNPC != null) getResponseConfigRequest.GetResponseConfig.ActionConfig = actionConfig;
|
||||
|
||||
return getResponseConfigRequest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes audio data continuously from a microphone input and sends it to the server via a gRPC call.
|
||||
/// </summary>
|
||||
/// <param name="call">The streaming call to send audio data to the server.</param>
|
||||
/// <param name="recordingFrequency">The frequency at which the audio is recorded.</param>
|
||||
/// <param name="recordingLength">The length of the audio recording in seconds.</param>
|
||||
/// <param name="audioClip">The AudioClip object that contains the audio data from the microphone.</param>
|
||||
/// <returns>A task that represents the asynchronous operation of processing and sending audio data.</returns>
|
||||
private async Task ProcessAudioContinuously(AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call, int recordingFrequency, int recordingLength,
|
||||
AudioClip audioClip)
|
||||
{
|
||||
// Run the receiving results from the server in the background without awaiting it here.
|
||||
Task receiveResultsTask = Task.Run(async () => { await ReceiveResultFromServer(call, _cancellationTokenSource.Token); }, _cancellationTokenSource.Token);
|
||||
|
||||
int pos = 0;
|
||||
float[] audioData = new float[recordingFrequency * recordingLength];
|
||||
|
||||
while (Microphone.IsRecording(MicrophoneManager.Instance.SelectedMicrophoneName))
|
||||
{
|
||||
await Task.Delay(200);
|
||||
int newPos = Microphone.GetPosition(MicrophoneManager.Instance.SelectedMicrophoneName);
|
||||
int diff = newPos - pos;
|
||||
|
||||
if (diff > 0)
|
||||
{
|
||||
if (audioClip == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Handle the Exception, which can occur if the CancellationTokenSource is already disposed.
|
||||
Logger.Warn("Exception when Audio Clip is null: " + e.Message,
|
||||
Logger.LogCategory.Character);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
Logger.Info("The Cancellation Token Source was Disposed because the Audio Clip was empty.",
|
||||
Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
audioClip.GetData(audioData, pos);
|
||||
await ProcessAudioChunk(call, diff, audioData);
|
||||
pos = newPos;
|
||||
}
|
||||
}
|
||||
|
||||
// Process any remaining audio data.
|
||||
await ProcessAudioChunk(call,
|
||||
Microphone.GetPosition(MicrophoneManager.Instance.SelectedMicrophoneName) - pos,
|
||||
audioData).ConfigureAwait(false);
|
||||
|
||||
await call.RequestStream.CompleteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops recording and processing the audio.
|
||||
/// </summary>
|
||||
public void StopRecordAudio()
|
||||
{
|
||||
// End microphone recording
|
||||
Microphone.End(MicrophoneManager.Instance.SelectedMicrophoneName);
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Info(_activeConvaiNPC.characterName + " has stopped listening", Logger.LogCategory.Character);
|
||||
OnPlayerSpeakingChanged?.Invoke(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Logger.Error("No active NPC found", Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes each audio chunk and sends it to the server.
|
||||
/// </summary>
|
||||
/// <param name="call">gRPC Streaming call connecting to the getResponse function</param>
|
||||
/// <param name="diff">Length of the audio data from the current position to the position of the last sent chunk</param>
|
||||
/// <param name="audioData">Chunk of audio data that we want to be processed</param>
|
||||
private static async Task ProcessAudioChunk(AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call, int diff, IReadOnlyList<float> audioData)
|
||||
{
|
||||
if (diff > 0)
|
||||
{
|
||||
// Convert audio data to byte array
|
||||
byte[] audioByteArray = new byte[diff * sizeof(short)];
|
||||
|
||||
for (int i = 0; i < diff; i++)
|
||||
{
|
||||
float sample = audioData[i];
|
||||
short shortSample = (short)(sample * short.MaxValue);
|
||||
byte[] shortBytes = BitConverter.GetBytes(shortSample);
|
||||
audioByteArray[i * sizeof(short)] = shortBytes[0];
|
||||
audioByteArray[i * sizeof(short) + 1] = shortBytes[1];
|
||||
}
|
||||
|
||||
// Send audio data to the gRPC server
|
||||
try
|
||||
{
|
||||
await call.RequestStream.WriteAsync(new GetResponseRequest
|
||||
{
|
||||
GetResponseData = new GetResponseData
|
||||
{
|
||||
AudioData = ByteString.CopyFrom(audioByteArray)
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (RpcException rpcException)
|
||||
{
|
||||
if (rpcException.StatusCode == StatusCode.Cancelled)
|
||||
Logger.Error(rpcException, Logger.LogCategory.Character);
|
||||
else
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="newActiveNPC"></param>
|
||||
public void InterruptCharacterSpeech(ConvaiNPC newActiveNPC)
|
||||
{
|
||||
// If the active NPC is speaking, cancel the ongoing gRPC call,
|
||||
// clear the response queue, and reset the character's speaking state, lip-sync, animation, and audio playback
|
||||
if (newActiveNPC != null)
|
||||
{
|
||||
// Cancel the ongoing gRPC call
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Handle the Exception, which can occur if the CancellationTokenSource is already disposed.
|
||||
Logger.Warn("Exception in Interrupt Character Speech: " + e.Message, Logger.LogCategory.Character);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
Logger.Info($"The Cancellation Token Source for {newActiveNPC} was Disposed in ConvaiGRPCAPI:InterruptCharacterSpeech.", Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource(); // Create a new token for future calls
|
||||
|
||||
CharacterInterrupted?.Invoke();
|
||||
|
||||
// Clear the response queue
|
||||
newActiveNPC.ClearResponseQueue();
|
||||
|
||||
// Reset the character's speaking state
|
||||
newActiveNPC.SetCharacterTalking(false);
|
||||
|
||||
// Stop any ongoing audio playback
|
||||
newActiveNPC.StopAllAudioPlayback();
|
||||
|
||||
// Stop any ongoing lip sync for active NPC
|
||||
newActiveNPC.StopLipSync();
|
||||
|
||||
// Reset the character's animation to idle
|
||||
newActiveNPC.ResetCharacterAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodically receives responses from the server and adds it to a static list in streaming NPC
|
||||
/// </summary>
|
||||
/// <param name="call">gRPC Streaming call connecting to the getResponse function</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
private async Task ReceiveResultFromServer(AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call, CancellationToken cancellationToken)
|
||||
{
|
||||
Queue<LipSyncBlendFrameData> lipSyncBlendFrameQueue = new();
|
||||
bool firstSilFound = false;
|
||||
while (!cancellationToken.IsCancellationRequested && await call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
// Get the response from the server
|
||||
GetResponseResponse result = call.ResponseStream.Current;
|
||||
OnResultReceived?.Invoke(result);
|
||||
|
||||
// Process different types of responses
|
||||
|
||||
if (result.UserQuery != null)
|
||||
if (_chatUIHandler != null)
|
||||
// Add user query to the list
|
||||
_stringUserText.Add(result.UserQuery.TextData);
|
||||
|
||||
// Trigger the current section of the narrative design manager in the active NPC
|
||||
if (result.BtResponse != null) TriggerNarrativeSection(result);
|
||||
|
||||
// Add action response to the list in the active NPC
|
||||
if (result.ActionResponse != null)
|
||||
if (_activeConvaiNPC.actionsHandler != null)
|
||||
_activeConvaiNPC.actionsHandler.actionResponseList.Add(result.ActionResponse.Action);
|
||||
|
||||
// Add audio response to the list in the active NPC
|
||||
if (result.AudioResponse != null)
|
||||
{
|
||||
if (result.AudioResponse.AudioData != null)
|
||||
{
|
||||
// Add response to the list in the active NPC
|
||||
if (result.AudioResponse.AudioData.ToByteArray().Length > 46)
|
||||
{
|
||||
byte[] wavBytes = result.AudioResponse.AudioData.ToByteArray();
|
||||
|
||||
// will only work for wav files
|
||||
WavHeaderParser parser = new(wavBytes);
|
||||
if (_activeConvaiNPC.convaiLipSync == null)
|
||||
{
|
||||
Logger.DebugLog($"Enqueuing responses: {result.AudioResponse.TextData}", Logger.LogCategory.LipSync);
|
||||
_activeConvaiNPC.EnqueueResponse(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
LipSyncBlendFrameData.FrameType frameType =
|
||||
_activeConvaiNPC.convaiLipSync.faceModel == FaceModel.OvrModelName
|
||||
? LipSyncBlendFrameData.FrameType.Visemes
|
||||
: LipSyncBlendFrameData.FrameType.Blendshape;
|
||||
lipSyncBlendFrameQueue.Enqueue(
|
||||
new LipSyncBlendFrameData(
|
||||
(int)(parser.CalculateDurationSeconds() * 30),
|
||||
result,
|
||||
frameType
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the response contains visemes data and the active NPC has a LipSync component
|
||||
if (result.AudioResponse.VisemesData != null)
|
||||
if (_activeConvaiNPC.convaiLipSync != null)
|
||||
{
|
||||
// Logger.Info(result.AudioResponse.VisemesData, Logger.LogCategory.LipSync);
|
||||
if (result.AudioResponse.VisemesData.Visemes.Sil == -2 || result.AudioResponse.EndOfResponse)
|
||||
{
|
||||
if (firstSilFound) lipSyncBlendFrameQueue.Dequeue().Process(_activeConvaiNPC);
|
||||
firstSilFound = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
lipSyncBlendFrameQueue.Peek().Enqueue(result.AudioResponse.VisemesData);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the response contains blendshapes data and the active NPC has a LipSync component
|
||||
if (result.AudioResponse.BlendshapesFrame != null)
|
||||
if (_activeConvaiNPC.convaiLipSync != null)
|
||||
{
|
||||
if (lipSyncBlendFrameQueue.Peek().CanProcess() || result.AudioResponse.EndOfResponse)
|
||||
{
|
||||
lipSyncBlendFrameQueue.Dequeue().Process(_activeConvaiNPC);
|
||||
}
|
||||
else
|
||||
{
|
||||
lipSyncBlendFrameQueue.Peek().Enqueue(result.AudioResponse.BlendshapesFrame);
|
||||
|
||||
if (lipSyncBlendFrameQueue.Peek().CanPartiallyProcess()) lipSyncBlendFrameQueue.Peek().ProcessPartially(_activeConvaiNPC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
if (result.AudioResponse == null && result.DebugLog != null)
|
||||
_activeConvaiNPC.EnqueueResponse(call.ResponseStream.Current);
|
||||
|
||||
// Check if the session id of active NPC is -1 then only update it
|
||||
if (_activeConvaiNPC.sessionID == "-1")
|
||||
// Update session ID in the active NPC
|
||||
_activeConvaiNPC.sessionID = call.ResponseStream.Current.SessionId;
|
||||
}
|
||||
}
|
||||
catch (RpcException rpcException)
|
||||
{
|
||||
// Handle RpcExceptions, log or throw if necessary
|
||||
if (rpcException.StatusCode == StatusCode.Cancelled)
|
||||
Logger.Error(rpcException, Logger.LogCategory.Character);
|
||||
else
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.DebugLog(ex, Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
|
||||
if (cancellationToken.IsCancellationRequested) await call.RequestStream.CompleteAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
private void TriggerNarrativeSection(GetResponseResponse result)
|
||||
{
|
||||
// Trigger the current section of the narrative design manager in the active NPC
|
||||
if (result.BtResponse != null)
|
||||
{
|
||||
Debug.Log("Narrative Design SectionID: " + result.BtResponse.NarrativeSectionId);
|
||||
// Get the NarrativeDesignManager component from the active NPC
|
||||
NarrativeDesignManager narrativeDesignManager = _activeConvaiNPC.narrativeDesignManager;
|
||||
if (narrativeDesignManager != null)
|
||||
MainThreadDispatcher.Instance.RunOnMainThread(() => { narrativeDesignManager.UpdateCurrentSection(result.BtResponse.NarrativeSectionId); });
|
||||
else
|
||||
Debug.Log("NarrativeDesignManager component not found in the active NPC");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="characterID"></param>
|
||||
/// <param name="triggerConfig"></param>
|
||||
public async Task SendTriggerData(ConvaiService.ConvaiServiceClient client, string characterID, TriggerConfig triggerConfig)
|
||||
{
|
||||
AsyncDuplexStreamingCall<GetResponseRequest, GetResponseResponse> call = GetAsyncDuplexStreamingCallOptions(client);
|
||||
|
||||
GetResponseRequest getResponseConfigRequest = CreateGetResponseRequest(true, true, 0, characterID);
|
||||
|
||||
try
|
||||
{
|
||||
await call.RequestStream.WriteAsync(getResponseConfigRequest);
|
||||
await call.RequestStream.WriteAsync(new GetResponseRequest
|
||||
{
|
||||
GetResponseData = new GetResponseData
|
||||
{
|
||||
TriggerData = triggerConfig
|
||||
}
|
||||
});
|
||||
await call.RequestStream.CompleteAsync();
|
||||
|
||||
// Store the task that receives results from the server.
|
||||
Task receiveResultsTask = Task.Run(
|
||||
async () => { await ReceiveResultFromServer(call, _cancellationTokenSource.Token); },
|
||||
_cancellationTokenSource.Token);
|
||||
|
||||
// Await the task if needed to ensure it completes before this method returns [OPTIONAL]
|
||||
await receiveResultsTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sends feedback to the server.
|
||||
/// </summary>
|
||||
/// <param name="thumbsUp">Indicates whether the feedback is a thumbs up or thumbs down.</param>
|
||||
/// <param name="interactionID">The ID associated with the interaction.</param>
|
||||
/// <param name="feedbackText">The text content of the feedback.</param>
|
||||
/// <returns>A Task representing the asynchronous operation.</returns>
|
||||
public async Task SendFeedback(bool thumbsUp, string interactionID, string feedbackText)
|
||||
{
|
||||
// Create a FeedbackRequest object with the provided parameters.
|
||||
FeedbackRequest request = new()
|
||||
{
|
||||
InteractionId = interactionID,
|
||||
CharacterId = _activeConvaiNPC.characterID,
|
||||
SessionId = _activeConvaiNPC.sessionID,
|
||||
TextFeedback = new FeedbackRequest.Types.Feedback
|
||||
{
|
||||
FeedbackText = feedbackText,
|
||||
ThumbsUp = thumbsUp
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Send the feedback request asynchronously and await the response.
|
||||
FeedbackResponse response = await _activeConvaiNPC.GetClient().SubmitFeedbackAsync(request, cancellationToken: _cancellationTokenSource.Token);
|
||||
|
||||
// Log the feedback response.
|
||||
Logger.Info(response.FeedbackResponse_, Logger.LogCategory.Character);
|
||||
}
|
||||
catch (RpcException rpcException)
|
||||
{
|
||||
// Log an exception if there is an error in sending the feedback.
|
||||
Logger.Exception(rpcException, Logger.LogCategory.Character);
|
||||
}
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
public event Action CharacterInterrupted; // Event to notify when the character's speech is interrupted
|
||||
public event Action<GetResponseResponse> OnResultReceived; // Event to notify when a response is received from the server
|
||||
public event Action<bool> OnPlayerSpeakingChanged; // Event to notify when the player starts or stops speaking
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
11
Assets/Convai/Scripts/Runtime/Core/ConvaiGRPCAPI.cs.meta
Normal file
11
Assets/Convai/Scripts/Runtime/Core/ConvaiGRPCAPI.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac3bfdb7f1f556540bc41acc9a375817
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
274
Assets/Convai/Scripts/Runtime/Core/ConvaiHeadTracking.cs
Normal file
274
Assets/Convai/Scripts/Runtime/Core/ConvaiHeadTracking.cs
Normal file
@ -0,0 +1,274 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Convai.Scripts.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// This class provides head tracking functionalities for an object (like a character) with an Animator.
|
||||
/// It requires the Animator component to be attached to the same GameObject.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Animator))]
|
||||
[DisallowMultipleComponent]
|
||||
[AddComponentMenu("Convai/Character Head & Eye Tracking")]
|
||||
[HelpURL(
|
||||
"https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/scripts-overview/convaiheadtracking")]
|
||||
public class ConvaiHeadTracking : MonoBehaviour
|
||||
{
|
||||
private const float POSITION_UPDATE_DELAY = 2f;
|
||||
|
||||
[field: Header("Tracking Properties")]
|
||||
[Tooltip("The object that the head should track.")]
|
||||
[field: SerializeField]
|
||||
public Transform TargetObject { get; set; }
|
||||
|
||||
[Range(0.0f, 100.0f)]
|
||||
[Tooltip("The maximum distance at which the head must still track target.")]
|
||||
[SerializeField]
|
||||
private float trackingDistance = 10f;
|
||||
|
||||
[Tooltip("Speed at which character turns towards the target.")]
|
||||
[Range(1f, 10f)]
|
||||
[SerializeField]
|
||||
private float turnSpeed = 5.0f;
|
||||
|
||||
[Header("Look At Weights")]
|
||||
[Range(0f, 1f)]
|
||||
[Tooltip(
|
||||
"Controls the amount of rotation applied to the body to achieve the 'Look At' target. The closer to 1, the more the body will rotate to follow the target.")]
|
||||
[SerializeField]
|
||||
private float bodyLookAtWeight = 0.6f;
|
||||
|
||||
[Range(0f, 1f)]
|
||||
[Tooltip(
|
||||
"Controls the amount of rotation applied to the head to achieve the 'Look At' target. The closer to 1, the more the head will rotate to follow the target.")]
|
||||
[SerializeField]
|
||||
private float headLookAtWeight = 0.8f;
|
||||
|
||||
[Range(0f, 1f)]
|
||||
[Tooltip(
|
||||
"Controls the amount of rotation applied to the eyes to achieve the 'Look At' target. The closer to 1, the more the eyes will rotate to follow the target.")]
|
||||
[SerializeField]
|
||||
private float eyesLookAtWeight = 1f;
|
||||
|
||||
[Space(10)]
|
||||
[Tooltip(
|
||||
"Set this to true if you want the character to look away randomly, false to always look at the target")]
|
||||
[SerializeField]
|
||||
private bool lookAway;
|
||||
|
||||
private Animator _animator;
|
||||
private float _appliedBodyLookAtWeight;
|
||||
private ConvaiActionsHandler _convaiActionsHandler;
|
||||
private float _currentLookAtWeight;
|
||||
private float _desiredLookAtWeight = 1f;
|
||||
private Transform _headPivot;
|
||||
private bool _isActionRunning;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InitializeComponents();
|
||||
InitializeHeadPivot();
|
||||
InvokeRepeating(nameof(UpdateTarget), 0, POSITION_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_convaiActionsHandler != null)
|
||||
_convaiActionsHandler.UnregisterForActionEvents(ConvaiActionsHandler_OnActionStarted, ConvaiActionsHandler_OnActionEnded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity's built-in method called during the IK pass.
|
||||
/// </summary>
|
||||
public void OnAnimatorIK(int layerIndex)
|
||||
{
|
||||
PerformHeadTracking();
|
||||
}
|
||||
|
||||
private void InitializeComponents()
|
||||
{
|
||||
if (!_animator) _animator = GetComponent<Animator>();
|
||||
InitializeTargetObject();
|
||||
|
||||
if (TryGetComponent(out _convaiActionsHandler))
|
||||
_convaiActionsHandler.RegisterForActionEvents(ConvaiActionsHandler_OnActionStarted, ConvaiActionsHandler_OnActionEnded);
|
||||
}
|
||||
|
||||
private void ConvaiActionsHandler_OnActionStarted(string action, GameObject target)
|
||||
{
|
||||
SetActionRunning(true);
|
||||
}
|
||||
|
||||
private void ConvaiActionsHandler_OnActionEnded(string action, GameObject target)
|
||||
{
|
||||
SetActionRunning(false);
|
||||
}
|
||||
|
||||
private void InitializeHeadPivot()
|
||||
{
|
||||
// Check if the pivot already exists
|
||||
if (_headPivot) return;
|
||||
|
||||
// Create a new GameObject for the pivot
|
||||
_headPivot = new GameObject("HeadPivot").transform;
|
||||
|
||||
// Set the new GameObject as a child of this character object
|
||||
_headPivot.transform.parent = transform;
|
||||
|
||||
// Position the pivot appropriately, in this case, it seems like it's a bit above the base (probably around the character's neck/head)
|
||||
_headPivot.localPosition = new Vector3(0, 1.6f, 0);
|
||||
}
|
||||
|
||||
|
||||
private void RotateCharacterTowardsTarget()
|
||||
{
|
||||
Vector3 toTarget = TargetObject.position - transform.position;
|
||||
float distance = toTarget.magnitude;
|
||||
|
||||
// Calculate the angle difference between the character's forward direction and the direction towards the target.
|
||||
float angleDifference = Vector3.Angle(transform.forward, toTarget);
|
||||
|
||||
// Adjust turn speed based on distance to target.
|
||||
float adjustedTurnSpeed = turnSpeed * 4 * (1f / distance);
|
||||
|
||||
// If the angle difference exceeds the limit, we turn the character smoothly towards the target.
|
||||
if (Mathf.Abs(angleDifference) > 0.65f)
|
||||
{
|
||||
Vector3 targetDirection = toTarget.normalized;
|
||||
|
||||
// Zero out the y-component (up-down direction) to only rotate on the horizontal plane.
|
||||
targetDirection.y = 0;
|
||||
|
||||
Quaternion targetRotation = Quaternion.LookRotation(targetDirection);
|
||||
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
|
||||
adjustedTurnSpeed * Time.deltaTime);
|
||||
|
||||
// Ensure that the character doesn't tilt on the X and Z axis.
|
||||
transform.eulerAngles = new Vector3(0, transform.eulerAngles.y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void InitializeTargetObject()
|
||||
{
|
||||
if (TargetObject != null) return;
|
||||
|
||||
Logger.Warn("No target object set for head tracking. Setting default target as main camera",
|
||||
Logger.LogCategory.Character);
|
||||
if (Camera.main != null) TargetObject = Camera.main.transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the target weight for the look-at.
|
||||
/// </summary>
|
||||
private void UpdateTarget()
|
||||
{
|
||||
_desiredLookAtWeight = lookAway ? Random.Range(0.2f, 1.0f) : 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs the head tracking towards the target object.
|
||||
/// </summary>
|
||||
private void PerformHeadTracking()
|
||||
{
|
||||
if (_isActionRunning) return;
|
||||
|
||||
float distance = Vector3.Distance(transform.position, TargetObject.position);
|
||||
DrawRayToTarget();
|
||||
|
||||
// only perform head tracking if within threshold distance
|
||||
if (!(distance < trackingDistance / 2))
|
||||
{
|
||||
_desiredLookAtWeight = 0;
|
||||
if (_currentLookAtWeight > 0)
|
||||
SetCurrentLookAtWeight();
|
||||
}
|
||||
|
||||
SetCurrentLookAtWeight();
|
||||
_headPivot.transform.LookAt(TargetObject); // orient the pivot towards the target object
|
||||
// set the current look at weight based on how much rotation is needed
|
||||
|
||||
// limit the head rotation
|
||||
float headRotation = _headPivot.localRotation.y;
|
||||
if (Mathf.Abs(headRotation) > 0.70f)
|
||||
{
|
||||
// clamp rotation if more than 80 degrees
|
||||
headRotation = Mathf.Sign(headRotation) * 0.70f;
|
||||
Quaternion localRotation = _headPivot.localRotation;
|
||||
localRotation.y = headRotation;
|
||||
_headPivot.localRotation = localRotation;
|
||||
}
|
||||
|
||||
// adjust body rotation weight based on how much the head is rotated
|
||||
float targetBodyLookAtWeight = Mathf.Abs(_headPivot.localRotation.y) > 0.45f
|
||||
? bodyLookAtWeight / 3f
|
||||
: 0f;
|
||||
|
||||
// smooth transition between current and target body rotation weight
|
||||
_appliedBodyLookAtWeight = Mathf.Lerp(_appliedBodyLookAtWeight, targetBodyLookAtWeight, Time.deltaTime);
|
||||
|
||||
// Apply rotation weights to the Animator
|
||||
RotateCharacterTowardsTarget();
|
||||
AdjustAnimatorLookAt();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to set the current look at weight based on the desired look at weight.
|
||||
/// </summary>
|
||||
private void SetCurrentLookAtWeight()
|
||||
{
|
||||
float angleDifference = _headPivot.localRotation.y;
|
||||
|
||||
// Lerp the currentLookAtWeight towards the desiredLookAtWeight or towards 0 if above a certain threshold.
|
||||
_currentLookAtWeight = Mathf.Abs(angleDifference) < 0.55f
|
||||
? Mathf.Lerp(Mathf.Clamp(_currentLookAtWeight, 0, 1), Mathf.Clamp(_desiredLookAtWeight, 0, 1),
|
||||
Time.deltaTime * POSITION_UPDATE_DELAY)
|
||||
: Mathf.Lerp(Mathf.Clamp(_currentLookAtWeight, 0, 1), 0, Time.deltaTime * POSITION_UPDATE_DELAY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to apply rotation weights to the Animator
|
||||
/// </summary>
|
||||
private void AdjustAnimatorLookAt()
|
||||
{
|
||||
// Check if Animator or TargetObject are null
|
||||
if (!_animator || TargetObject == null)
|
||||
{
|
||||
// If either is null, set the look-at weight to 0 and return, effectively ending the method early
|
||||
_animator.SetLookAtWeight(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the look-at weights in the Animator.
|
||||
// This is used to dictate how much the body, head or eyes should turn to "look at" the target.
|
||||
// `Mathf.Clamp` is used to ensure the weight values lie between 0 and 1 (inclusive).
|
||||
// The body weight is clamped between 0 to 0.5 since it's less advisable to rotate the body too much versus the head or eyes.
|
||||
_animator.SetLookAtWeight(Mathf.Clamp(
|
||||
_currentLookAtWeight, 0, 1),
|
||||
Mathf.Clamp(_appliedBodyLookAtWeight, 0, .5f),
|
||||
Mathf.Clamp(headLookAtWeight / 1.25f, 0, .8f),
|
||||
Mathf.Clamp(eyesLookAtWeight, 0, 1));
|
||||
|
||||
// Set the look-at position for the Animator (where the body/head/eyes will turn toward)
|
||||
_animator.SetLookAtPosition(TargetObject.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DebugLog utility to visualize the tracking mechanism
|
||||
/// </summary>
|
||||
private void DrawRayToTarget()
|
||||
{
|
||||
Vector3 pos = transform.position;
|
||||
// Draw a debug ray from our position to the normalized direction towards the target, scaled by half of the tracking distance threshold.
|
||||
// The purpose is to visualize the direction and focus of the head tracking, and it's a useful debug tool in Unity's Scene view.
|
||||
// "Normalized" ensures that the vector has a magnitude (length) of 1, keeping the scaling of the vector consistent.
|
||||
// This ray appears red in the Scene view.
|
||||
Debug.DrawRay(pos,
|
||||
(TargetObject.position - pos).normalized * trackingDistance / 2, Color.red);
|
||||
}
|
||||
|
||||
public void SetActionRunning(bool newValue)
|
||||
{
|
||||
_isActionRunning = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47bf09eafaaeed940ab9e5531a64790c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
336
Assets/Convai/Scripts/Runtime/Core/ConvaiInputManager.cs
Normal file
336
Assets/Convai/Scripts/Runtime/Core/ConvaiInputManager.cs
Normal file
@ -0,0 +1,336 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// The Input Manager class for Convai, allowing you to control inputs in your project through this class.
|
||||
/// It supports both the New Input System and Old Input System.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-105)]
|
||||
public class ConvaiInputManager : MonoBehaviour
|
||||
{
|
||||
#if ENABLE_LEGACY_INPUT_MANAGER
|
||||
[Serializable]
|
||||
public class FourDirectionalMovementKeys
|
||||
{
|
||||
public KeyCode Forward = KeyCode.W;
|
||||
public KeyCode Backward = KeyCode.S;
|
||||
public KeyCode Right = KeyCode.D;
|
||||
public KeyCode Left = KeyCode.A;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
|
||||
/// <summary>
|
||||
/// Input Action for player movement.
|
||||
/// </summary>
|
||||
[Header("Player Related")] public InputAction PlayerMovementKeyAction;
|
||||
|
||||
/// <summary>
|
||||
/// Input Action for player jumping.
|
||||
/// </summary>
|
||||
public InputAction PlayerJumpKeyAction;
|
||||
|
||||
/// <summary>
|
||||
/// Input Action for player running.
|
||||
/// </summary>
|
||||
public InputAction PlayerRunKeyAction;
|
||||
|
||||
/// <summary>
|
||||
/// Input Action for locking the cursor.
|
||||
/// </summary>
|
||||
[Header("General")] public InputAction CursorLockKeyAction;
|
||||
|
||||
/// <summary>
|
||||
/// Input Action for sending text.
|
||||
/// </summary>
|
||||
public InputAction TextSendKeyAction;
|
||||
|
||||
/// <summary>
|
||||
/// Input Action for talk functionality.
|
||||
/// </summary>
|
||||
public InputAction TalkKeyAction;
|
||||
|
||||
/// <summary>
|
||||
/// Action to open the Settings Panel.
|
||||
/// </summary>
|
||||
public InputAction SettingsKeyAction;
|
||||
#elif ENABLE_LEGACY_INPUT_MANAGER
|
||||
/// <summary>
|
||||
/// Key used to manage cursor lock
|
||||
/// </summary>
|
||||
public KeyCode CursorLockKey = KeyCode.Escape;
|
||||
|
||||
/// <summary>
|
||||
/// Key used to manage text send
|
||||
/// </summary>
|
||||
public KeyCode TextSendKey = KeyCode.Return;
|
||||
|
||||
/// <summary>
|
||||
/// Key used to manage text send
|
||||
/// </summary>
|
||||
public KeyCode TextSendAltKey = KeyCode.KeypadEnter;
|
||||
|
||||
/// <summary>
|
||||
/// Key used to manage record user audio
|
||||
/// </summary>
|
||||
public KeyCode TalkKey = KeyCode.T;
|
||||
|
||||
/// <summary>
|
||||
/// Key used to manage setting panel toggle
|
||||
/// </summary>
|
||||
public KeyCode OpenSettingPanelKey = KeyCode.F10;
|
||||
|
||||
/// <summary>
|
||||
/// Key used to manage running
|
||||
/// </summary>
|
||||
public KeyCode RunKey = KeyCode.LeftShift;
|
||||
|
||||
/// <summary>
|
||||
/// Keys used to manage movement
|
||||
/// </summary>
|
||||
public FourDirectionalMovementKeys MovementKeys;
|
||||
#endif
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance providing easy access to the ConvaiInputManager from other scripts.
|
||||
/// </summary>
|
||||
public static ConvaiInputManager Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Awake is called when the script instance is being loaded.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
// Ensure only one instance of ConvaiInputManager exists
|
||||
if (Instance != null)
|
||||
{
|
||||
Debug.LogError("There's more than one ConvaiInputManager! " + transform + " - " + Instance);
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable input actions when the object is enabled.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
PlayerMovementKeyAction.Enable();
|
||||
PlayerJumpKeyAction.Enable();
|
||||
PlayerRunKeyAction.Enable();
|
||||
CursorLockKeyAction.Enable();
|
||||
TextSendKeyAction.Enable();
|
||||
TalkKeyAction.Enable();
|
||||
SettingsKeyAction.Enable();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the left mouse button was pressed.
|
||||
/// </summary>
|
||||
public bool WasMouseLeftButtonPressed()
|
||||
{
|
||||
// Check if the left mouse button was pressed this frame
|
||||
#if ENABLE_INPUT_SYSTEM && (!UNITY_ANDROID || !UNITY_IOS)
|
||||
return Mouse.current.leftButton.wasPressedThisFrame;
|
||||
#else
|
||||
return Input.GetMouseButtonDown(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current mouse position.
|
||||
/// </summary>
|
||||
public Vector2 GetMousePosition()
|
||||
{
|
||||
// Get the current mouse position
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return Mouse.current.position.ReadValue();
|
||||
#else
|
||||
return Input.mousePosition;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the vertical movement of the mouse.
|
||||
/// </summary>
|
||||
public float GetMouseYAxis()
|
||||
{
|
||||
// Get the vertical movement of the mouse
|
||||
#if ENABLE_INPUT_SYSTEM && (!UNITY_ANDROID || !UNITY_IOS)
|
||||
return Mouse.current.delta.y.ReadValue();
|
||||
#else
|
||||
return Input.GetAxis("Mouse Y");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the horizontal movement of the mouse.
|
||||
/// </summary>
|
||||
public float GetMouseXAxis()
|
||||
{
|
||||
// Get the horizontal movement of the mouse
|
||||
#if ENABLE_INPUT_SYSTEM && (!UNITY_ANDROID || !UNITY_IOS)
|
||||
return Mouse.current.delta.x.ReadValue();
|
||||
#else
|
||||
return Input.GetAxis("Mouse X");
|
||||
#endif
|
||||
}
|
||||
|
||||
// General input methods
|
||||
/// <summary>
|
||||
/// Checks if the cursor lock key was pressed.
|
||||
/// </summary>
|
||||
public bool WasCursorLockKeyPressed()
|
||||
{
|
||||
// Check if the cursor lock key was pressed this frame
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return CursorLockKeyAction.WasPressedThisFrame();
|
||||
#else
|
||||
return Input.GetKeyDown(CursorLockKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the text send key was pressed.
|
||||
/// </summary>
|
||||
public bool WasTextSendKeyPressed()
|
||||
{
|
||||
// Check if the text send key was pressed this frame
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return TextSendKeyAction.WasPressedThisFrame();
|
||||
#else
|
||||
return Input.GetKeyDown(TextSendKey) || Input.GetKeyDown(TextSendAltKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the talk key was pressed.
|
||||
/// </summary>
|
||||
public bool WasTalkKeyPressed()
|
||||
{
|
||||
// Check if the talk key was pressed this frame
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return TalkKeyAction.WasPressedThisFrame();
|
||||
#else
|
||||
return Input.GetKeyDown(TalkKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the talk key is being held down.
|
||||
/// </summary>
|
||||
public bool IsTalkKeyHeld()
|
||||
{
|
||||
// Check if the talk key is being held down
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return TalkKeyAction.IsPressed();
|
||||
#else
|
||||
return Input.GetKey(TalkKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
/// <summary>
|
||||
/// Retrieves the InputAction associated with the talk key.
|
||||
/// </summary>
|
||||
/// <returns>The InputAction for handling talk-related input.</returns>
|
||||
public InputAction GetTalkKeyAction() => TalkKeyAction;
|
||||
#endif
|
||||
/// <summary>
|
||||
/// Checks if the talk key was released.
|
||||
/// </summary>
|
||||
public bool WasTalkKeyReleased()
|
||||
{
|
||||
// Check if the talk key was released this frame
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return TalkKeyAction.WasReleasedThisFrame();
|
||||
#else
|
||||
return Input.GetKeyUp(TalkKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the Settings key was pressed.
|
||||
/// </summary>
|
||||
public bool WasSettingsKeyPressed()
|
||||
{
|
||||
// Check if the Settings key was pressed this frame
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return SettingsKeyAction.WasPressedThisFrame();
|
||||
#else
|
||||
return Input.GetKeyDown(OpenSettingPanelKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Player related input methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the jump key was pressed.
|
||||
/// </summary>
|
||||
public bool WasJumpKeyPressed()
|
||||
{
|
||||
// Check if the jump key was pressed this frame
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return PlayerJumpKeyAction.WasPressedThisFrame();
|
||||
#else
|
||||
return Input.GetButton("Jump");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the run key is being held down.
|
||||
/// </summary>
|
||||
public bool IsRunKeyHeld()
|
||||
{
|
||||
// Check if the run key is being held down
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return PlayerRunKeyAction.IsPressed();
|
||||
#else
|
||||
return Input.GetKey(RunKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the player's movement input vector.
|
||||
/// </summary>
|
||||
public Vector2 GetPlayerMoveVector()
|
||||
{
|
||||
// Get the player's movement input vector
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return PlayerMovementKeyAction.ReadValue<Vector2>();
|
||||
#else
|
||||
Vector2 inputMoveDir = new Vector2(0, 0);
|
||||
// Manual input for player movement
|
||||
if (Input.GetKey(MovementKeys.Forward))
|
||||
{
|
||||
inputMoveDir.y += 1f;
|
||||
}
|
||||
|
||||
if (Input.GetKey(MovementKeys.Backward))
|
||||
{
|
||||
inputMoveDir.y -= 1f;
|
||||
}
|
||||
|
||||
if (Input.GetKey(MovementKeys.Left))
|
||||
{
|
||||
inputMoveDir.x -= 1f;
|
||||
}
|
||||
|
||||
if (Input.GetKey(MovementKeys.Right))
|
||||
{
|
||||
inputMoveDir.x += 1f;
|
||||
}
|
||||
|
||||
return inputMoveDir;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a69a6bc2bf58e64883c79cf732d1bfd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
482
Assets/Convai/Scripts/Runtime/Core/ConvaiNPC.cs
Normal file
482
Assets/Convai/Scripts/Runtime/Core/ConvaiNPC.cs
Normal file
@ -0,0 +1,482 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Convai.Scripts.Utils;
|
||||
using Convai.Scripts.Utils.LipSync;
|
||||
using Grpc.Core;
|
||||
using Service;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using Logger = Convai.Scripts.Utils.Logger;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
#if UNITY_ANDROID
|
||||
using UnityEngine.Android;
|
||||
#endif
|
||||
|
||||
namespace Convai.Scripts
|
||||
{
|
||||
/// <summary>
|
||||
/// The ConvaiNPC class is a MonoBehaviour script that gives a GameObject the ability to interact with the Convai API.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Animator), typeof(AudioSource))]
|
||||
[AddComponentMenu("Convai/ConvaiNPC")]
|
||||
[HelpURL(
|
||||
"https://docs.convai.com/api-docs/plugins-and-integrations/unity-plugin/overview-of-the-convainpc.cs-script")]
|
||||
public class ConvaiNPC : MonoBehaviour
|
||||
{
|
||||
private const int AUDIO_SAMPLE_RATE = 44100;
|
||||
private const string GRPC_API_ENDPOINT = "stream.convai.com";
|
||||
private const int RECORDING_FREQUENCY = AUDIO_SAMPLE_RATE;
|
||||
private const int RECORDING_LENGTH = 30;
|
||||
private static readonly int Talk = Animator.StringToHash("Talk");
|
||||
|
||||
[Header("Character Information")]
|
||||
[Tooltip("Enter the character name for this NPC.")]
|
||||
public string characterName;
|
||||
|
||||
[Tooltip("Enter the character ID for this NPC.")]
|
||||
public string characterID;
|
||||
|
||||
[Tooltip("The current session ID for the chat with this NPC.")]
|
||||
[ReadOnly]
|
||||
public string sessionID = "-1";
|
||||
|
||||
[Tooltip("Is this character active?")]
|
||||
[ReadOnly]
|
||||
public bool isCharacterActive;
|
||||
[HideInInspector] public ConvaiActionsHandler actionsHandler;
|
||||
[HideInInspector] public ConvaiLipSync convaiLipSync;
|
||||
|
||||
[Tooltip("Is this character talking?")]
|
||||
[SerializeField]
|
||||
[ReadOnly]
|
||||
private bool isCharacterTalking;
|
||||
|
||||
[Header("Session Initialization")]
|
||||
[Tooltip("Enable/disable initializing session ID by sending a text request to the server")]
|
||||
public bool initializeSessionID = true;
|
||||
|
||||
[HideInInspector] public ConvaiPlayerInteractionManager playerInteractionManager;
|
||||
[HideInInspector] public NarrativeDesignManager narrativeDesignManager;
|
||||
[HideInInspector] public TriggerUnityEvent onTriggerSent;
|
||||
private readonly Queue<GetResponseResponse> _getResponseResponses = new();
|
||||
private bool _animationPlaying;
|
||||
private Channel _channel;
|
||||
private Animator _characterAnimator;
|
||||
private ConvaiService.ConvaiServiceClient _client;
|
||||
private ConvaiChatUIHandler _convaiChatUIHandler;
|
||||
private ConvaiCrosshairHandler _convaiCrosshairHandler;
|
||||
private ConvaiGroupNPCController _convaiGroupNPCController;
|
||||
private TMP_InputField _currentInputField;
|
||||
private bool _groupNPCComponentNotFound;
|
||||
private ConvaiGRPCAPI _grpcAPI;
|
||||
private bool _isActionActive;
|
||||
private bool _isLipSyncActive;
|
||||
private bool _stopAudioPlayingLoop;
|
||||
private bool _stopHandlingInput;
|
||||
public ActionConfig ActionConfig;
|
||||
|
||||
private bool IsInConversationWithAnotherNPC
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_groupNPCComponentNotFound) return false;
|
||||
if (_convaiGroupNPCController == null)
|
||||
{
|
||||
if (TryGetComponent(out ConvaiGroupNPCController component))
|
||||
_convaiGroupNPCController = component;
|
||||
else
|
||||
_groupNPCComponentNotFound = true;
|
||||
}
|
||||
|
||||
return _convaiGroupNPCController != null && _convaiGroupNPCController.IsInConversationWithAnotherNPC;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCharacterTalking
|
||||
{
|
||||
get => isCharacterTalking;
|
||||
private set => isCharacterTalking = value;
|
||||
}
|
||||
|
||||
private FaceModel FaceModel => convaiLipSync == null ? FaceModel.OvrModelName : convaiLipSync.faceModel;
|
||||
|
||||
public string GetEndPointURL => GRPC_API_ENDPOINT;
|
||||
|
||||
// Properties with getters and setters
|
||||
[field: NonSerialized] public bool IncludeActionsHandler { get; set; }
|
||||
[field: NonSerialized] public bool LipSync { get; set; }
|
||||
[field: NonSerialized] public bool HeadEyeTracking { get; set; }
|
||||
[field: NonSerialized] public bool EyeBlinking { get; set; }
|
||||
[field: NonSerialized] public bool NarrativeDesignManager { get; set; }
|
||||
[field: NonSerialized] public bool ConvaiGroupNPCController { get; set; }
|
||||
|
||||
public ConvaiNPCAudioManager AudioManager { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Logger.Info("Initializing ConvaiNPC : " + characterName, Logger.LogCategory.Character);
|
||||
InitializeComponents();
|
||||
Logger.Info("ConvaiNPC component initialized", Logger.LogCategory.Character);
|
||||
}
|
||||
|
||||
private async void Start()
|
||||
{
|
||||
// Assign the ConvaiGRPCAPI component in the scene
|
||||
_grpcAPI = ConvaiGRPCAPI.Instance;
|
||||
|
||||
// Start the coroutine that plays audio clips in order
|
||||
StartCoroutine(AudioManager.PlayAudioInOrder());
|
||||
InvokeRepeating(nameof(ProcessResponse), 0f, 1 / 100f);
|
||||
|
||||
|
||||
// Check if the platform is Android
|
||||
#if UNITY_ANDROID
|
||||
// Check if the user has not authorized microphone permission
|
||||
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
|
||||
// Request microphone permission from the user
|
||||
Permission.RequestUserPermission(Permission.Microphone);
|
||||
#endif
|
||||
// DO NOT EDIT
|
||||
// gRPC setup configuration
|
||||
|
||||
#region GRPC_SETUP
|
||||
|
||||
SslCredentials credentials = new(); // Create SSL credentials for secure communication
|
||||
_channel = new Channel(GRPC_API_ENDPOINT, credentials); // Initialize a gRPC channel with the specified endpoint and credentials
|
||||
_client = new ConvaiService.ConvaiServiceClient(_channel); // Initialize the gRPC client for the ConvaiService using the channel
|
||||
|
||||
#endregion
|
||||
|
||||
if (initializeSessionID)
|
||||
{
|
||||
sessionID = await ConvaiGRPCAPI.InitializeSessionIDAsync(characterName, _client, characterID, sessionID);
|
||||
}
|
||||
_convaiChatUIHandler = ConvaiChatUIHandler.Instance;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if(Input.GetKeyDown(KeyCode.U)) {
|
||||
characterID = "fb0d9902-4fde-11ef-bfa5-42010a7be011";
|
||||
}
|
||||
if (Input.GetKeyDown(KeyCode.I))
|
||||
{
|
||||
characterID = "3e40947a-4e47-11ef-832b-42010a7be011";
|
||||
}
|
||||
playerInteractionManager.UpdateUserInput();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
AudioManager.OnCharacterTalkingChanged += HandleIsCharacterTalkingAnimation;
|
||||
AudioManager.OnAudioTranscriptAvailable += HandleAudioTranscriptAvailable;
|
||||
AudioManager.OnCharacterTalkingChanged += SetCharacterTalking;
|
||||
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged += HandleActiveNPCChanged;
|
||||
|
||||
if (_convaiChatUIHandler != null) _convaiChatUIHandler.UpdateCharacterList();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (AudioManager != null)
|
||||
{
|
||||
AudioManager.OnCharacterTalkingChanged -= HandleIsCharacterTalkingAnimation;
|
||||
AudioManager.OnAudioTranscriptAvailable -= HandleAudioTranscriptAvailable;
|
||||
AudioManager.OnCharacterTalkingChanged -= SetCharacterTalking;
|
||||
AudioManager.PurgeExcessLipSyncFrames -= PurgeLipSyncFrames;
|
||||
}
|
||||
|
||||
ConvaiNPCManager.Instance.OnActiveNPCChanged -= HandleActiveNPCChanged;
|
||||
|
||||
if (_convaiChatUIHandler != null) _convaiChatUIHandler.UpdateCharacterList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unity callback that is invoked when the application is quitting.
|
||||
/// Stops the loop that plays audio in order.
|
||||
/// </summary>
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
AudioManager.StopAudioLoop();
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
_convaiChatUIHandler = ConvaiChatUIHandler.Instance;
|
||||
if (_convaiChatUIHandler != null) _convaiChatUIHandler.UpdateCharacterList();
|
||||
}
|
||||
|
||||
public async void TriggerEvent(string triggerName, string triggerMessage = "")
|
||||
{
|
||||
TriggerConfig trigger = new()
|
||||
{
|
||||
TriggerName = triggerName,
|
||||
TriggerMessage = triggerMessage
|
||||
};
|
||||
|
||||
// Send the trigger to the server using GRPC
|
||||
await ConvaiGRPCAPI.Instance.SendTriggerData(_client, characterID, trigger);
|
||||
|
||||
// Invoke the UnityEvent
|
||||
onTriggerSent.Invoke(triggerMessage, triggerName);
|
||||
}
|
||||
|
||||
private event Action<bool> OnCharacterTalking;
|
||||
|
||||
private void UpdateWaitUntilLipSync(bool value)
|
||||
{
|
||||
AudioManager.SetWaitForCharacterLipSync(value);
|
||||
}
|
||||
|
||||
private void HandleActiveNPCChanged(ConvaiNPC newActiveNPC)
|
||||
{
|
||||
// If this NPC is no longer the active NPC, interrupt its speech
|
||||
if (this != newActiveNPC && !IsInConversationWithAnotherNPC && ConvaiInputManager.Instance.WasTalkKeyPressed()) InterruptCharacterSpeech();
|
||||
}
|
||||
|
||||
|
||||
private void InitializeComponents()
|
||||
{
|
||||
_convaiChatUIHandler = FindObjectOfType<ConvaiChatUIHandler>();
|
||||
_convaiCrosshairHandler = FindObjectOfType<ConvaiCrosshairHandler>();
|
||||
_characterAnimator = GetComponent<Animator>();
|
||||
AudioManager = gameObject.AddComponent<ConvaiNPCAudioManager>();
|
||||
narrativeDesignManager = GetComponent<NarrativeDesignManager>();
|
||||
|
||||
InitializePlayerInteractionManager();
|
||||
InitializeLipSync();
|
||||
StartCoroutine(InitializeActionsHandler());
|
||||
}
|
||||
|
||||
|
||||
private IEnumerator InitializeActionsHandler()
|
||||
{
|
||||
yield return new WaitForSeconds(1);
|
||||
actionsHandler = GetComponent<ConvaiActionsHandler>();
|
||||
if (actionsHandler != null)
|
||||
{
|
||||
_isActionActive = true;
|
||||
ActionConfig = actionsHandler.ActionConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializePlayerInteractionManager()
|
||||
{
|
||||
playerInteractionManager = gameObject.AddComponent<ConvaiPlayerInteractionManager>();
|
||||
playerInteractionManager.Initialize(this, _convaiCrosshairHandler, _convaiChatUIHandler);
|
||||
}
|
||||
|
||||
private void InitializeLipSync()
|
||||
{
|
||||
convaiLipSync = GetComponent<ConvaiLipSync>();
|
||||
if (convaiLipSync != null)
|
||||
{
|
||||
_isLipSyncActive = true;
|
||||
convaiLipSync = GetComponent<ConvaiLipSync>();
|
||||
convaiLipSync.OnCharacterLipSyncing += UpdateWaitUntilLipSync;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAudioTranscriptAvailable(string transcript)
|
||||
{
|
||||
if (isCharacterActive) _convaiChatUIHandler.SendCharacterText(characterName, transcript);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the character's talking animation based on whether the character is currently talking.
|
||||
/// </summary>
|
||||
private void HandleIsCharacterTalkingAnimation(bool isTalking)
|
||||
{
|
||||
if (isTalking)
|
||||
{
|
||||
if (!_animationPlaying)
|
||||
{
|
||||
_animationPlaying = true;
|
||||
_characterAnimator.SetBool(Talk, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_animationPlaying = false;
|
||||
_characterAnimator.SetBool(Talk, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends message data to the server asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="text">The message to send.</param>
|
||||
public async void SendTextDataAsync(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ConvaiGRPCAPI.Instance.SendTextData(_client, text, characterID,
|
||||
_isActionActive, _isLipSyncActive, ActionConfig, FaceModel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, Logger.LogCategory.Character);
|
||||
// Handle the exception, e.g., show a message to the user.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the session in an asynchronous manner and handles the receiving of results from the server.
|
||||
/// Initiates the audio recording process using the gRPC API.
|
||||
/// </summary>
|
||||
public async void StartListening()
|
||||
{
|
||||
if (!MicrophoneManager.Instance.HasAnyMicrophoneDevices())
|
||||
{
|
||||
NotificationSystemHandler.Instance.NotificationRequest(NotificationType.NoMicrophoneDetected);
|
||||
return;
|
||||
}
|
||||
|
||||
await _grpcAPI.StartRecordAudio(_client, _isActionActive, _isLipSyncActive, RECORDING_FREQUENCY,
|
||||
RECORDING_LENGTH, characterID, ActionConfig, FaceModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the ongoing audio recording process.
|
||||
/// </summary>
|
||||
public void StopListening()
|
||||
{
|
||||
// Stop the audio recording process using the ConvaiGRPCAPI StopRecordAudio method
|
||||
_grpcAPI.StopRecordAudio();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add response to the GetResponseResponse Queue
|
||||
/// </summary>
|
||||
/// <param name="response"></param>
|
||||
public void EnqueueResponse(GetResponseResponse response)
|
||||
{
|
||||
if (response == null || response.AudioResponse == null) return;
|
||||
//Logger.DebugLog($"Adding Response for Processing: {response.AudioResponse.TextData}", Logger.LogCategory.LipSync);
|
||||
_getResponseResponses.Enqueue(response);
|
||||
}
|
||||
|
||||
public void ClearResponseQueue()
|
||||
{
|
||||
_getResponseResponses.Clear();
|
||||
}
|
||||
|
||||
private void PurgeLipSyncFrames()
|
||||
{
|
||||
if (convaiLipSync == null) return;
|
||||
convaiLipSync.PurgeExcessFrames();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a response fetched from a character.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 1. Processes audio/message/face data from the response and adds it to _responseAudios.
|
||||
/// 2. Identifies actions from the response and parses them for execution.
|
||||
/// </remarks>
|
||||
private void ProcessResponse()
|
||||
{
|
||||
// Check if the character is active and should process the response
|
||||
if (isCharacterActive || IsInConversationWithAnotherNPC)
|
||||
if (_getResponseResponses.Count > 0)
|
||||
{
|
||||
GetResponseResponse getResponseResponse = _getResponseResponses.Dequeue();
|
||||
|
||||
if (getResponseResponse?.AudioResponse != null)
|
||||
{
|
||||
// Check if text data exists in the response
|
||||
if (getResponseResponse.AudioResponse.AudioData.ToByteArray().Length > 46)
|
||||
{
|
||||
// Initialize empty string for text
|
||||
string textDataString = getResponseResponse.AudioResponse.TextData;
|
||||
|
||||
byte[] byteAudio = getResponseResponse.AudioResponse.AudioData.ToByteArray();
|
||||
|
||||
AudioClip clip = AudioManager.ProcessByteAudioDataToAudioClip(byteAudio,
|
||||
getResponseResponse.AudioResponse.AudioConfig.SampleRateHertz.ToString());
|
||||
|
||||
// Add the response audio along with associated data to the list
|
||||
AudioManager.AddResponseAudio(new ConvaiNPCAudioManager.ResponseAudio
|
||||
{
|
||||
AudioClip = clip,
|
||||
AudioTranscript = textDataString,
|
||||
IsFinal = false
|
||||
});
|
||||
}
|
||||
else if (getResponseResponse.AudioResponse.EndOfResponse)
|
||||
{
|
||||
Logger.DebugLog("We have received end of response", Logger.LogCategory.LipSync);
|
||||
// Handle the case where there is a DebugLog but no audio response
|
||||
AudioManager.AddResponseAudio(new ConvaiNPCAudioManager.ResponseAudio
|
||||
{
|
||||
AudioClip = null,
|
||||
AudioTranscript = null,
|
||||
IsFinal = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetAudioResponseCount()
|
||||
{
|
||||
return AudioManager.GetAudioResponseCount();
|
||||
}
|
||||
|
||||
public void StopAllAudioPlayback()
|
||||
{
|
||||
AudioManager.StopAllAudioPlayback();
|
||||
AudioManager.ClearResponseAudioQueue();
|
||||
}
|
||||
|
||||
public void ResetCharacterAnimation()
|
||||
{
|
||||
if (_characterAnimator != null)
|
||||
_characterAnimator.SetBool(Talk, false);
|
||||
|
||||
if (convaiLipSync != null)
|
||||
convaiLipSync.ConvaiLipSyncApplicationBase.ClearQueue();
|
||||
}
|
||||
|
||||
public void SetCharacterTalking(bool isTalking)
|
||||
{
|
||||
if (IsCharacterTalking != isTalking)
|
||||
{
|
||||
Logger.Info($"Character {characterName} is talking: {isTalking}", Logger.LogCategory.Character);
|
||||
IsCharacterTalking = isTalking;
|
||||
OnCharacterTalking?.Invoke(IsCharacterTalking);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopLipSync()
|
||||
{
|
||||
if (convaiLipSync != null) convaiLipSync.StopLipSync();
|
||||
}
|
||||
|
||||
public void InterruptCharacterSpeech()
|
||||
{
|
||||
_grpcAPI.InterruptCharacterSpeech(this);
|
||||
}
|
||||
|
||||
public ConvaiService.ConvaiServiceClient GetClient()
|
||||
{
|
||||
return _client;
|
||||
}
|
||||
|
||||
public void UpdateSessionID(string newSessionID)
|
||||
{
|
||||
sessionID = newSessionID;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TriggerUnityEvent : UnityEvent<string, string>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user