Spawn UCC prefab, switch between AI and playable

bbjones

Member
This script is for simple testing and is the result of resolving multiple issues with Justin's help.

The script demonstrates how you can spawn a playable UCC prefab into a scene and attach it to the UCC camera.
Then swap that character back and forth between being AI or human playable.
It also shows how you can setup multiple inventory loadouts using item pickup prefabs.

This is one way to make a simple test with the script:
  • Either create a new project or use an existing one that has the Opsive UCC demo asset
  • If you have Opsive UCC version 2.1.8 or lower, replace ObjectdFader.cs and ItemSetManager.cs with the attached scripts
    • Justin provided these to fix some issues related to these tests, and suggested the changes will make their way into the upcoming UCC update
  • Create a new empty scene
  • Using the Opsive Manager
    • Create the Setup objects for Scene (Managers, Camera, UI) and Project (Key bindings, Layers)
    • Make a UCC character normally, then make a new prefab of that character. Repeat so you have multiple different characters.
    • Create item pickup objects, as many as you like.
    • You should now have prefabs in your project assets, the scene should still be empty
  • Create a new empty game object named TestManager, attach the script below.
  • Create a new camera, position somewhere useful, this will be the non-Opsive camera that is activated when you switch to AI
  • Select the TestManager in your scene, and setup the script properties, drag in the prefabs you created
    • Note this test can be done with as little as a single UCC character prefab. You don't need inventory pickups.
  • Disable the Opsive camera
  • Add a plane or simple terrain for the character to stand on, bake a nav mesh (Window->AI->Navigation)
  • Play the scene
    • Scene should start with no characters or item pickups, and the non-opsive camera active.
    • Numpad 1 - Spawns the character prefab into the scene as a playable character, attaches and activates the UCC camera
    • Numpad 2 - Swaps the active character from playable to AI Agent, disables Opsive camera, enables non-Opsive camera
    • Numpad 3 - Swaps the AI agent back to playable, swaps cameras again
You can then go through any combinations of those keys swapping and spawning.
If you have multiple character prefabs, change the Character Type To Spawn at runtime to change which character will spawn.
Note the script is setup to only have a single character in the scene.

Example of the TestManager properties and default scene hierarchy
1573312737111.png

Script:

C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Opsive.UltimateCharacterController.Inventory;
using Opsive.UltimateCharacterController.Objects.CharacterAssist;
using Opsive.UltimateCharacterController.Utility.Builders;

[System.Serializable]
public class SpawnCharacter : MonoBehaviour
{
    [Tooltip("At run-time, select the type of character you want to spawn. Then call the SpawnACharacter() method, easy way is to wire that up to a UI Button click event.")]
    [Header("Testing")]
    public ECharacterType characterTypeToSpawn;
    //public bool makeAiAgent;
    //public bool doNotDestroyAiAgents;
    [Tooltip("Drag in the Opsive camera you created in your scene with the Opsive Setup Manager.  Be sure to uncheck Init Character On Awake.")]
    public GameObject opsiveCamera;
    public GameObject nonOpsiveCamera;
    //[Tooltip("If false the Opsive camera will not reference the newly spawned UCC character.")]
    //public bool assignSpawnToCamera;

    [Tooltip("One entry for every combination of character type and default loadout")]
    [Header("Default Kits")]
    public CharacterDefaultKits[] characterDefaultKits;

    private GameObject spawnedCharacter;
    private bool spawnIsAi;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Keypad1))
        {
            print("Spawn Character...");
            SpawnACharacter();
        }
        if (Input.GetKeyDown(KeyCode.Keypad2))
        {
            print("Convert To AI...");
            ConvertPlaybleToAiAgent();
        }
        if (Input.GetKeyDown(KeyCode.Keypad3))
        {
            print("Convert To Playable...");
            ConvertAiAgentToPlayable();
        }
    }

    public void ConvertPlaybleToAiAgent()
    {
        Opsive.UltimateCharacterController.Camera.CameraController uccCam = opsiveCamera.GetComponent<Opsive.UltimateCharacterController.Camera.CameraController>();
        opsiveCamera.SetActive(false);
        nonOpsiveCamera.SetActive(true);
        uccCam.Character = null;

        var playerInput = this.spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Input.PlayerInput>();
        playerInput.enabled = false;
        var handler = this.spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Character.UltimateCharacterLocomotionHandler>();
        handler.enabled = false;
        var localLookSource = this.spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Character.LocalLookSource>();
        localLookSource.enabled = true;
    }

    public void ConvertAiAgentToPlayable()
    {
        var localLookSource = this.spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Character.LocalLookSource>();
        localLookSource.enabled = false;
        var playerInput = this.spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Input.PlayerInput>();
        playerInput.enabled = true;
        var handler = this.spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Character.UltimateCharacterLocomotionHandler>();
        handler.enabled = true;

        Opsive.UltimateCharacterController.Camera.CameraController uccCam = opsiveCamera.GetComponent<Opsive.UltimateCharacterController.Camera.CameraController>();
        opsiveCamera.SetActive(true);
        nonOpsiveCamera.SetActive(false);
        uccCam.Character = this.spawnedCharacter;
    }

    // The code for adding ItemPickups is taken from the UMA Integration asset scripts found here https://opsive.com/downloads/?pid=923
    // Note that I'm not using anything to do with UMA, and does not require the UMA asset to function.
    // Spawns the character with the typical Unity.GameObject.Instantiate() method.
    public void SpawnACharacter()
    {
        print("Attempt to spawn character...");

        // destroy currently spawned character if exists
        //if (spawnedCharacter != null)
        if (spawnedCharacter != null)
        {
            Destroy(spawnedCharacter);
        }

        Opsive.UltimateCharacterController.Camera.CameraController uccCam = opsiveCamera.GetComponent<Opsive.UltimateCharacterController.Camera.CameraController>();

        // loop through character kits to find the type that matches variable characterTypeToSpawn
        for (int c = 0; c < characterDefaultKits.Length; ++c)
        {
            if (characterDefaultKits[c].ECharacterType == characterTypeToSpawn)
            {
                // find the matching default kit by character type
                spawnedCharacter = GameObject.Instantiate(characterDefaultKits[c].CharacterPrefab);
                var localLookSource = spawnedCharacter.AddComponent<Opsive.UltimateCharacterController.Character.LocalLookSource>();
                localLookSource.enabled = false;

                // attach new character to Opsive camera
                opsiveCamera.SetActive(true);
                nonOpsiveCamera.SetActive(false);
                uccCam.Character = spawnedCharacter;


                // get UCC inventory from new character
                var inventory = spawnedCharacter.GetComponent<InventoryBase>();
                if (inventory == null)
                {
                    return;
                }

                // add all ItemPickups for the new character type
                for (int i = 0; i < characterDefaultKits[c].ItemPickups.Length; ++i)
                {
                    if (characterDefaultKits[c].ItemPickups[i] == null)
                    {
                        continue;
                    }

                    characterDefaultKits[c].ItemPickups[i].DoItemPickup(spawnedCharacter, inventory, -1, true, true);
                }
            }
        }
    }
}

// this class represents all the default items for a single character type
[System.Serializable]
public class CharacterDefaultKits
{
    public ECharacterType ECharacterType;
    public GameObject CharacterPrefab;
    public ItemPickup[] ItemPickups;
}

// this class represents the different types of characters
[System.Serializable]
public enum ECharacterType
{
    CharacterType1,
    CharacterType2,
    CharacterType3
}

Related posts:


 
Last edited:
This code was very useful!


One important thing I found missing from the above mentioned code when using with a behavior tree AI is that the NavMeshAgent ability would need to be added to the character controller as disabled and then enabled for a behavior tree AI. I do something like:

Opsive.UltimateCharacterController.Character.UltimateCharacterLocomotion controller = spawnedCharacter.GetComponent<Opsive.UltimateCharacterController.Character.UltimateCharacterLocomotion>(); var ability = controller.GetAbility<Opsive.UltimateCharacterController.Character.Abilities.AI.NavMeshAgentMovement>(); ability.Enabled = true;

In case anyone reaches this post so they know to do that.
 
I stuck as close as I could to UCC coding standards and wrote a component/manager version without any of the spawn code. A manager is still needed for keeping track of the CurrentPlayer. I'm sure I've missed a few things, the RequireComponents are there mainly to make sure PlayerInput/UCLH/ItemHandler get added back in (feel free to remove this section if you want). The component runs two lists of Behaviours. The lists are treated as toggles and are disabled when the opposite state is set. They are exposed in the inspector so you can add/remove from them as you wish. You may or may not wish to create an Event for swapping controls. Setting an Action<GameObject> OnMakePlayer/OnMakeAI in the Manager would be pretty easy.


TLDR - For a Component/Manager version:

Component
Code:
using BehaviorDesigner.Runtime.UltimateCharacterController;
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Input;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(PlayerInput))]
[RequireComponent(typeof(UltimateCharacterLocomotionHandler))]
[RequireComponent(typeof(ItemHandler))]
[RequireComponent(typeof(LocalLookSource))]
[RequireComponent(typeof(BehaviorTreeAgent))]
[RequireComponent(typeof(NavMeshAgent))]
public class HotSwap : MonoBehaviour
{
    [Tooltip("The tag to give this GameObject when switching to AI.")]
    [SerializeField] protected string m_AITag = "Enemy";
    [Tooltip("List of Behaviours enabled as a Player. (Disabled as AI)")]
    [SerializeField] protected List<Behaviour> m_EnableOnMakePlayer = new List<Behaviour>();
    [Tooltip("List of Behaviours enabled as an AI. (Disabled as Player)")]
    [SerializeField] protected List<Behaviour> m_EnableOnMakeAI = new List<Behaviour>();

    public string AITag { get => m_AITag; set => m_AITag = value; }
    public List<Behaviour> EnableOnMakePlayer 
    {
        get
        {
            if (m_EnableOnMakePlayer == null)
                m_EnableOnMakePlayer = new List<Behaviour>();
            return m_EnableOnMakePlayer;
        }
    }
    public List<Behaviour> EnableOnMakeAI
    {
        get
        {
            if (m_EnableOnMakeAI == null)
                m_EnableOnMakeAI = new List<Behaviour>();
            return m_EnableOnMakeAI;
        }
    }

    #region Public Functions
    /// <summary>
    /// Enables/Disables Components to toggle between AI -> Player controlled.
    /// </summary>
    public virtual void MakePlayer() => HotSwapManager.ConvertToPlayer(this);

    /// <summary>
    /// Enables/Disables Components to toggle between Player -> AI controlled.
    /// </summary>
    public virtual void MakeAI() => HotSwapManager.ConvertToAI(this);

    /// <summary>
    /// Sets up a Behaviour to be toggled on enable/disable as Player/AI.
    /// </summary>
    /// <param name="behaviour"></param>
    /// <param name="enableOnMakePlayer"></param>
    /// <param name="enableOnMakeAI"></param>
    public virtual void SetBehaviour(Behaviour behaviour, bool enableOnMakePlayer, bool enableOnMakeAI)
    {
        // Argument Validation
        if (behaviour == null)
            return;

        // Set or Clear MakePlayer()
        if (enableOnMakePlayer)
            AddEnableOnMakePlayer(behaviour);
        else
            RemoveEnableOnMakePlayer(behaviour);

        // Set or Clear MakeAI()
        if (enableOnMakeAI)
            AddEnableOnMakeAI(behaviour);
        else
            RemoveEnableOnMakeAI(behaviour);
    }
    #endregion

    #region Protected Functions
    /// <summary>
    /// Adds a Behaviour Component to switch "ON" when MakePlayer() is called.
    /// </summary>
    /// <param name="behaviour">The Behaviour to add.</param>
    protected virtual void AddEnableOnMakePlayer(Behaviour behaviour)
    {
        if (!m_EnableOnMakePlayer.Contains(behaviour))
            m_EnableOnMakePlayer.Add(behaviour);
    }

    /// <summary>
    /// Removes a Behaviour Component from being toggled "ON" when MakePlayer() is called.
    /// </summary>
    /// <param name="behaviour">The Behaviour to remove.</param>
    protected virtual void RemoveEnableOnMakePlayer(Behaviour behaviour)
    {
        if (m_EnableOnMakePlayer.Contains(behaviour))
            m_EnableOnMakePlayer.Remove(behaviour);
    }

    /// <summary>
    /// Adds a Behaviour Component to switch "ON" when MakeAI() is called.
    /// </summary>
    /// <param name="behaviour">The Behaviour to add.</param>
    protected virtual void AddEnableOnMakeAI(Behaviour behaviour)
    {
        if (!m_EnableOnMakeAI.Contains(behaviour))
            m_EnableOnMakeAI.Add(behaviour);
    }

    /// <summary>
    /// Removes a Behaviour Component from being toggled "ON" when MakeAI() is called.
    /// </summary>
    /// <param name="behaviour">The Behaviour to remove.</param>
    protected virtual void RemoveEnableOnMakeAI(Behaviour behaviour)
    {
        if (m_EnableOnMakeAI.Contains(behaviour))
            m_EnableOnMakeAI.Remove(behaviour);
    }
    #endregion

    #region Unity Messages
    /// <summary>
    /// This component has been Reset.
    /// </summary>
    protected virtual void Reset()
    {
        // Clear Lists
        EnableOnMakePlayer.Clear();
        EnableOnMakeAI.Clear();

        // Add Default Behaviours
        if (gameObject.CompareTag("Player"))
        {
            SetBehaviour(GetComponent<PlayerInput>(), true, false);
            SetBehaviour(GetComponent<UltimateCharacterLocomotionHandler>(), true, false);
            SetBehaviour(GetComponent<ItemHandler>(), true, false);
            SetBehaviour(GetComponent<LocalLookSource>(), false, true);
            SetBehaviour(GetComponent<BehaviorTreeAgent>(), false, true);
            SetBehaviour(GetComponent<NavMeshAgent>(), false, true);
        }
        else
        {
            SetBehaviour(GetComponent<PlayerInput>(), false, true);
            SetBehaviour(GetComponent<UltimateCharacterLocomotionHandler>(), false, true);
            SetBehaviour(GetComponent<ItemHandler>(), false, true);
            SetBehaviour(GetComponent<LocalLookSource>(), true, false);
            SetBehaviour(GetComponent<BehaviorTreeAgent>(), true, false);
            SetBehaviour(GetComponent<NavMeshAgent>(), true, false);
        }
    }
    #endregion
}
 
Manager
Code:
using Opsive.UltimateCharacterController.Camera;
using UnityEngine;
using UnityEngine.SceneManagement;

public class HotSwapManager : MonoBehaviour
{
    #region Singleton
    private static HotSwapManager s_Instance;
    private static HotSwapManager Instance
    {
        get
        {
            if (!s_Initialized)
            {
                // Could be added to the "Game" GameObject with the rest of the Opsive Managers.
                s_Instance = new GameObject("ControlSwitchManager").AddComponent<HotSwapManager>();
                s_Initialized = true;
            }
            return s_Instance;
        }
    }
    private static bool s_Initialized;
    #endregion

    // TODO:
    // Maybe we can just disable the OpsiveCamera Scripts?
    // Maybe we can toggle CinemachineBrain if it exists?
    [Tooltip("The camera responsible for the Player character.")]
    [SerializeField] protected CameraController m_OpsiveCamera;
    [Tooltip("The camera used when no Player character exists.")]
    [SerializeField] protected Camera m_FreeCamera;
    [Tooltip("The current Player character.")]
    [SerializeField] protected HotSwap m_CurrentPlayer;

    public static CameraController OpsiveCamera
    {
        get
        {
            if (Instance.m_OpsiveCamera == null)
            {
                // Try to find the OpsiveCamera by Type.
                Instance.m_OpsiveCamera = GameObject.FindObjectOfType<CameraController>();
            }

            return Instance.m_OpsiveCamera;
        }
        
        set => Instance.m_OpsiveCamera = value;
    }
    public static Camera FreeCamera
    {
        get
        {
            if (Instance.m_FreeCamera == null)
            {
                // Grab all the Cameras
                var cameras = GameObject.FindObjectsOfType<Camera>();
                foreach (var camera in cameras)
                {
                    // Don't pick an OpsiveCamera.
                    if (camera.GetComponent<CameraController>() == null)
                    {
                        Instance.m_FreeCamera = camera;
                        break;
                    }
                }
            }

            return Instance.m_FreeCamera;
        }

        set => Instance.m_FreeCamera = value;
    }
    public static HotSwap CurrentPlayer
    {
        get
        {
            if (Instance.m_CurrentPlayer == null)
            {
                // Try to find the character tagged Player.
                Instance.m_CurrentPlayer = GameObject.FindGameObjectWithTag("Player").GetComponent<HotSwap>();
            }
            return Instance.m_CurrentPlayer;
        }

        set => Instance.m_CurrentPlayer = value;
    }

    #region Public Functions
    public static void ConvertToAI(HotSwap hotSwap)
    {
        // Argument Validation
        if (hotSwap == null)
            return;

        // Clear Current Player
        if (hotSwap == CurrentPlayer)
            CurrentPlayer = null;

        // Disable Player Components
        foreach (var behaviour in hotSwap.EnableOnMakePlayer)
        {
            behaviour.enabled = false;
        }

        // Enable AI Components
        foreach (var behaviour in hotSwap.EnableOnMakeAI)
        {
            behaviour.enabled = true;
        }

        // Change Character Tag
        hotSwap.gameObject.tag = hotSwap.AITag;

        // Setup FreeCamera
        if (OpsiveCamera != null)
            OpsiveCamera.gameObject.SetActive(false);
        if (FreeCamera != null)
            FreeCamera.gameObject.SetActive(true);
    }

    public static void ConvertToPlayer(HotSwap hotSwap)
    {
        // Argument Validation
        if (hotSwap == null || hotSwap == CurrentPlayer)
            return;

        // We can only control a max of one Character at a time.
        if (CurrentPlayer != null)
            ConvertToAI(CurrentPlayer);

        // Store Current Player
        CurrentPlayer = hotSwap;

        // Disable AI Components
        foreach (var behaviour in hotSwap.EnableOnMakeAI)
        {
            behaviour.enabled = false;
        }

        // Enable Player Components
        foreach (var behaviour in hotSwap.EnableOnMakePlayer)
        {
            behaviour.enabled = true;
        }

        // Change Character Tag
        CurrentPlayer.gameObject.tag = "Player";

        // Setup OpsiveCamera
        if (OpsiveCamera != null)
        {
            OpsiveCamera.Character = CurrentPlayer.gameObject;
            OpsiveCamera.gameObject.SetActive(true);
        }
        if (FreeCamera != null)
            FreeCamera.gameObject.SetActive(false);
    }
    #endregion

    #region Unity Messages
    /// <summary>
    /// Validate the lists.
    /// </summary>
    protected virtual void Awake()
    {
        foreach (var behaviour in EnableOnMakePlayer)
        {
            if (EnableOnMakeAI.Contains(behaviour))
                Debug.LogWarning($"Behaviour({behaviour.name}) is found in both EnableOnMakePlayer and EnableOnMakeAI.");
        }
    }

    /// <summary>
    /// The object has been enabled.
    /// </summary>
    private void OnEnable()
    {
        // The object may have been enabled outside of the scene unloading.
        if (s_Instance == null)
        {
            s_Instance = this;
            s_Initialized = true;
            SceneManager.sceneUnloaded -= SceneUnloaded;
        }
    }

    /// <summary>
    /// The object has been disabled.
    /// </summary>
    private void OnDisable()
    {
        SceneManager.sceneUnloaded += SceneUnloaded;
    }

    /// <summary>
    /// Reset the initialized variable when the scene is no longer loaded.
    /// </summary>
    /// <param name="scene">The scene that was unloaded.</param>
    private void SceneUnloaded(Scene scene)
    {
        s_Initialized = false;
        s_Instance = null;
    }
    #endregion

#if UNITY_2019_3_OR_NEWER
    /// <summary>
    /// Reset the static variables for domain reloading.
    /// </summary>
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void DomainReset()
    {
        s_Initialized = false;
        s_Instance = null;
    }
#endif
}
 
Top