Unity Narrative Design Trigger Implementation

Hello

Can you please share the code for the UpdateActiveNPC function?

Thanks a lot!

Hello @Maya_Bishawi,

Could you please elaborate on your question?

It seems that the camera ray was blocked by an obstacle, preventing the NPC from responding. The script you provided works only for a single NPC, but I have multiple NPCs in the scene. I need a script that allows any NPC to respond when the camera ray is focused on them

using System;
using System.Collections;
using System.Collections.Generic;
using Convai.Scripts.Runtime.Attributes;
using Convai.Scripts.Runtime.Features;
using Convai.Scripts.Runtime.LoggerSystem;
using UnityEngine;

namespace Convai.Scripts.Runtime.Core
{
    [DefaultExecutionOrder(-101)]
    public class ConvaiNPCManager : MonoBehaviour
    {
        private static readonly RaycastHit[] RaycastHits = new RaycastHit[1];

        [Tooltip("Length of the ray used for detecting NPCs.")] [SerializeField]
        private float rayLength = 2.0f;

        [Tooltip("Angle from the ray's direction to keep the NPC active, even if not directly hit by the ray.")] [SerializeField]
        private float visionConeAngle = 45f;

        [Tooltip("Reference to the currently active NPC.")] [ReadOnly]
        public ConvaiNPC activeConvaiNPC;

        [Tooltip("Reference to the NPC that is currently near the player.")] [ReadOnly]
        public ConvaiNPC nearbyNPC;

        // Cache used to store NPC references and avoid redundant GetComponent calls.
        private readonly Dictionary<GameObject, ConvaiNPC> _convaiNPCCache = new();

        // Reference to the NPC that was last hit by the raycast.
        private ConvaiNPC _lastHitNpc;

        // Reference to the main camera used for ray casting.
        private Camera _mainCamera;

        // Singleton instance of the NPC manager.
        public static ConvaiNPCManager Instance { get; private set; }

        private void Awake()
        {
            // Singleton pattern to ensure only one instance exists
            if (Instance == null)
                Instance = this;
            else
                Destroy(gameObject);

            _mainCamera = Camera.main;
        }


        private void LateUpdate()
        {
            Ray ray = new(_mainCamera.transform.position, _mainCamera.transform.forward);
            bool foundConvaiNPC = false;

            if (Physics.RaycastNonAlloc(ray, RaycastHits, rayLength) > 0)
            {
                RaycastHit hit = RaycastHits[0];

                nearbyNPC = GetConvaiNPC(hit.transform.gameObject);

                if (nearbyNPC != null)
                {
                    foundConvaiNPC = true;

                    if (_lastHitNpc != nearbyNPC && !CheckForNPCToNPCConversation(nearbyNPC))
                    {
                        UpdateActiveNPC(nearbyNPC);
                    }
                }
            }

            if (!foundConvaiNPC && _lastHitNpc != null)
            {
                Vector3 toLastHitNPC = _lastHitNpc.transform.position - ray.origin;
                float angleToLastHitNPC = Vector3.Angle(ray.direction, toLastHitNPC.normalized);
                float distanceToLastHitNPC = toLastHitNPC.magnitude;

                if (angleToLastHitNPC > visionConeAngle || distanceToLastHitNPC > rayLength * 1.2f)
                {
                    ConvaiLogger.DebugLog($"Player left {_lastHitNpc.gameObject.name}", ConvaiLogger.LogCategory.Character);
                    UpdateActiveNPC(null);
                }
            }
        }

        private void OnDrawGizmos()
        {
            if (_mainCamera == null)
                _mainCamera = Camera.main;

            if (_mainCamera == null)
                return;

            Transform cameraTransform = _mainCamera.transform;
            Vector3 rayOrigin = cameraTransform.position;
            Vector3 rayDirection = cameraTransform.forward;

            // Drawing the main ray
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(rayOrigin, rayDirection.normalized * rayLength);

            if (_lastHitNpc != null) DrawVisionConeArc(rayOrigin, rayDirection, cameraTransform.up);
        }

        private void DrawVisionConeArc(Vector3 rayOrigin, Vector3 rayDirection, Vector3 up)
        {
            const int arcResolution = 50; // number of segments to use for arc
            float angleStep = 2 * visionConeAngle / arcResolution; // angle between each segment

            Vector3 previousPoint = Quaternion.AngleAxis(-visionConeAngle, up) * rayDirection * rayLength;

            for (int i = 1; i <= arcResolution; i++)
            {
                Vector3 nextPoint = Quaternion.AngleAxis(-visionConeAngle + angleStep * i, up) * rayDirection * rayLength;
                Gizmos.DrawLine(rayOrigin + previousPoint, rayOrigin + nextPoint);
                previousPoint = nextPoint;
            }

            Quaternion leftRotation = Quaternion.AngleAxis(-visionConeAngle, up);
            Quaternion rightRotation = Quaternion.AngleAxis(visionConeAngle, up);

            Vector3 leftDirection = leftRotation * rayDirection;
            Vector3 rightDirection = rightRotation * rayDirection;

            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(rayOrigin, rayOrigin + leftDirection.normalized * rayLength);
            Gizmos.DrawLine(rayOrigin, rayOrigin + rightDirection.normalized * rayLength);
        }

        /// <summary>
        ///     Checks if the specified NPC is in conversation with another NPC.
        /// </summary>
        /// <param name="npc">The NPC to check.</param>
        /// <returns>True if the NPC is in conversation with another NPC; otherwise, false.</returns>
        public bool CheckForNPCToNPCConversation(ConvaiNPC npc)
        {
            return npc.TryGetComponent(out ConvaiGroupNPCController convaiGroupNPC) && convaiGroupNPC.IsInConversationWithAnotherNPC;
        }

        private void UpdateActiveNPC(ConvaiNPC newActiveNPC)
        {
            // Check if the new active NPC is different from the current active NPC.
            if (activeConvaiNPC != newActiveNPC)
            {
                // Deactivate the currently active NPC, if any.
                if (activeConvaiNPC != null) activeConvaiNPC.isCharacterActive = false;

                // Update the reference to the new active NPC.
                activeConvaiNPC = newActiveNPC;
                _lastHitNpc = newActiveNPC; // Ensure the _lastHitNpc reference is updated accordingly.

                // Activate the new NPC, if any.
                if (newActiveNPC != null)
                {
                    newActiveNPC.isCharacterActive = true;
                    ConvaiLogger.DebugLog($"Active NPC changed to {newActiveNPC.gameObject.name}", ConvaiLogger.LogCategory.Character);
                }

                // Invoke the OnActiveNPCChanged event, notifying other parts of the system of the change.
                OnActiveNPCChanged?.Invoke(newActiveNPC);
            }
        }

        /// <summary>
        ///     Sets the active NPC to the specified NPC.
        /// </summary>
        /// <param name="newActiveNPC">The NPC to set as active.</param>
        /// <param name="updateLastHitNPC"> Whether to update the last hit NPC reference.</param>
        public void SetActiveConvaiNPC(ConvaiNPC newActiveNPC, bool updateLastHitNPC = true)
        {
            if (activeConvaiNPC != newActiveNPC)
            {
                if (activeConvaiNPC != null)
                    // Deactivate the previous NPC
                    activeConvaiNPC.isCharacterActive = false;

                activeConvaiNPC = newActiveNPC;
                if (updateLastHitNPC)
                    _lastHitNpc = newActiveNPC;

                if (newActiveNPC != null)
                {
                    // Activate the new NPC
                    newActiveNPC.isCharacterActive = true;
                    ConvaiLogger.DebugLog($"Active NPC changed to {newActiveNPC.gameObject.name}", ConvaiLogger.LogCategory.Character);
                }

                OnActiveNPCChanged?.Invoke(newActiveNPC);
            }
        }

        /// <summary>
        ///     Event that's triggered when the active NPC changes.
        /// </summary>
        public event Action<ConvaiNPC> OnActiveNPCChanged;

        private ConvaiNPC GetConvaiNPC(GameObject obj)
        {
            if (!_convaiNPCCache.TryGetValue(obj, out ConvaiNPC npc))
            {
                npc = obj.GetComponent<ConvaiNPC>();
                if (npc != null)
                    _convaiNPCCache[obj] = npc;
            }

            return npc;
        }

        /// <summary>
        ///     Gets the currently active ConvaiNPC.
        /// </summary>
        /// <returns>The currently active ConvaiNPC.</returns>
        public ConvaiNPC GetActiveConvaiNPC()
        {
            return activeConvaiNPC;
        }
    }
}

Thank you!
But I would like to ask about something related to the narrative design in the project. If the character is only able to travel to one out of three locations they are supposed to visit in the story, what are the possible reasons or justifications for this limitation?

I don’t understand the question.

After the welcome section, the NPC should follow a tour route through three designated trigger locations. However, it only moves to the first location and stops there, failing to continue to the second and third locations.

From what you described, it seems like the NPC stops after reaching the first location because there isn’t an explicit instruction or trigger directing it to continue. In other words, unless a new section is triggered or a user input prompts further movement, the NPC won’t automatically proceed on its own.

I created 3 different trigger objects placed in separate locations. For each of them, I used the Interactable Move To component. The idea is for the NPC to move from the welcome section to the first trigger, and then continue to the second and third triggers automatically in sequence. However, the NPC stops after reaching the first location and doesn’t proceed to the next ones. I was expecting the movement to continue without needing additional player interaction after the first trigger.

Could you please advise on how to make the NPC move through all three locations in sequence automatically?

To achieve this, you should add the MoveTo function to the OnSectionEnd Unity event of your first section. Then, once the NPC reaches the designated location, you should trigger the next section manually to continue the sequence.

Do u mean change this function


?
to clarify This screenshot represents the event that should occur after the first event is completed.

You need to add the corresponding MoveTo function here as you did in the first section.

this is what I did in the first section, I repeated for the rest of the sections. In the first section, after the ‘Welcome’, it moves correctly, but after that, it doesn’t move

I don’t see any related function, you just disable gameobject.

where should i add the function exactly

There are 2 ways, OnSectionEnd of the previous Section or OnSectionStart of the New Section.
But you still need to trigger the new section.

This is the end of the first section i have already added the trigger of the next location at the end of the first one do i need to add another function. (To the end section) ?

This way you don’t trigger anything. It only activates a game object.

What do you mean by “trigger the new section”

Like you trigger the welcome section. I suspect that you watch and understand the Narrative Design video and Documentation. Please watch and follow it. Try to understand the logic.