Howdy, Stranger!

It looks like you're new here. Sign in or register to get started.

If you have any questions, reports, suggestions, or requests about Live2D, please send them to this forum.
※We cannot guarantee statements or answers from Live2D staff. Thank you for your understanding in advance.
 
Live2D Cubism
Cubism Products and Downloads
Cubism product manuals and tutorials
Cubism Editor Manual    Cubism Editor Tutorial    Cubism SDK Manual    Cubism SDK Tutorial

[Unity] Motion won't play on build

OS Name: Microsoft Windows 11 Pro
OS Version: 10.0.22621 N/A Build 22621
Unity: 6000.0.48f1
Cubism SDK: 5-r.3

I dynamically instantiate GameObject with CubismModel at runtime and the .moc3 file is outside of Unity Editor. I have managed to make it play motions with a simple MotionController.PlayAnimation() and it works on the Unity Editor. When I tried the build version, it somehow won't play the motions.

I've tried to reimport entire project, create a clean project but still didn't work. What I know is, If i use the generated prefab, instantiate it on runtime then play motions, it's work on the build.

Here's my main code's:

CharacterViewer.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using Kiraio.Azure.Utils;
using Live2D.Cubism.Framework;
using Live2D.Cubism.Framework.Expression;
using Live2D.Cubism.Framework.Json;
using Live2D.Cubism.Framework.Motion;
using Live2D.Cubism.Framework.MotionFade;
using Live2D.Cubism.Framework.Pose;
using Live2D.Cubism.Framework.Raycasting;
using Live2D.Cubism.Rendering.Masking;
using UnityEngine;

namespace Kiraio.Azure.Components
{
    [AddComponentMenu("Azure/Components/Character Viewer")]
    public class CharacterViewer : CubismViewerBase
    {
        // TouchBody = Upper Body, TouchHead = Head, TouchSpecial = Bust
        private readonly string[] _touchAreas = { "TouchBody", "TouchHead", "TouchSpecial" };

        private void OnDestroy()
        {
            foreach (var clip in Animations.Select(animationClip => animationClip.Value))
                clip.events = Array.Empty<AnimationEvent>();
        }

        public async UniTask Initialize()
        {
            // Load the *.model3.json file
            var baseModelName = Path.GetFileName(ModelJsonFile).Replace(".model3.json", "");
            ModelJson = CubismModel3Json.LoadAtPath(
                StorageHelper.NormalizePath(ModelJsonFile),
                LoadAssetAtPath
            );
            ModelJson.FileReferences.DisplayInfo = $"{baseModelName}.cdi3.json";
            ModelJson.FileReferences.Moc = $"{baseModelName}.moc3";
            // ModelJson.FileReferences.Expressions = $"{baseModelName}.exp3.json";
            // ModelJson.FileReferences.Motions = $"{baseModelName}.motion3.json";
            ModelJson.FileReferences.Physics = $"{baseModelName}.physics3.json";
            // PhysicsJson =
            // CubismPhysics3Json.LoadFrom(await WebRequestHelper.GetTextDataAsync(ModelJson.FileReferences.Physics));

            PoseJson = CubismPose3Json.LoadFrom(await WebRequestHelper.GetTextDataAsync(ModelJson.FileReferences.Pose));

            ModelJson.FileReferences.Pose = $"{baseModelName}.pose3.json";
            ModelJson.HitAreas = _touchAreas
                .Select(area => new CubismModel3Json.SerializableHitArea { Name = area, Id = area }).ToArray();

            Model = ModelJson.ToModel();
            Model.transform.parent = transform;
            Model.gameObject.SetActive(false); // Disable the model gameObject to stop the components being initialized
            gameObject.name = Model.name;

            //! Physics Rig is somehow became null after the initialization, disable the Physics for now
            // Model.gameObject.AddComponent<CubismPhysicsController>();

            Raycaster = Model.gameObject.AddComponent<CubismRaycaster>();
            VoiceSource = Model.gameObject.AddComponent<AudioSource>();
            Animator = Model.gameObject.GetComponent<Animator>();
            // Animator.runtimeAnimatorController = AnimatorController;

            // Add CubismMotionController after assigning CubismFadeMotionList
            MotionController = Model.gameObject.AddComponent<CubismMotionController>();
            FadeController = Model.gameObject.GetComponent<CubismFadeController>();
            // MotionController.enabled = false;
            MotionController.LayerCount = 3;

            // Workaround for the "InstanceId" AnimationEvent error
            Model.gameObject
                .AddComponent<
                    FixAnimationEvent>(); // Fix AnimationEvent errors by bypassing the "InstanceId" AnimationEvent with empty callback

            // Create Mask Texture
            MaskController = Model.gameObject.GetComponent<CubismMaskController>();
            var modelMaskTexture =
                ScriptableObject.CreateInstance<CubismMaskTexture>();
            modelMaskTexture.name = $"{Model.name}MaskTexture";
            MaskController.MaskTexture = modelMaskTexture;

            ExpressionController = Model.gameObject.AddComponent<CubismExpressionController>();
            var expressionList = ScriptableObject.CreateInstance<CubismExpressionList>();
            expressionList.name =
                $"{Path.GetFileNameWithoutExtension(ModelJsonFile).Split(".")[0]}";
            var expressionDataList = new List<CubismExpressionData>();
            foreach (var expression in ModelJson
                         .FileReferences
                         .Expressions)
            {
                var expressionName = expression.File.Split('/', '.')[1];
                var expressionJsonPath = Path.Combine(
                    Path.GetDirectoryName(ModelJsonFile) ?? string.Empty,
                    expression.File
                );
                var expressionJson = CubismExp3Json.LoadFrom(
                    await WebRequestHelper.GetTextDataAsync(expressionJsonPath)
                );

                // Create ExpressionData
                var expressionData = CubismExpressionData.CreateInstance(expressionJson);
                expressionData.name = $"{expressionName}.exp";
                expressionDataList.Add(expressionData);
            }

            expressionList.CubismExpressionObjects = expressionDataList.ToArray();
            ExpressionController.ExpressionsList = expressionList;

            // Create Fade Motion List
            // Ref: https://docs.live2d.com/en/cubism-sdk-manual/motionfade/
            var fadeMotionList = ScriptableObject.CreateInstance<CubismFadeMotionList>();
            fadeMotionList.name =
                $"{Path.GetFileNameWithoutExtension(ModelJsonFile).Split(".")[0]}";
            var motionsData = new Dictionary<int, CubismFadeMotionData>();

            foreach (
                var motion in ModelJson
                    .FileReferences
                    .Motions
                    .Motions
            )
            {
                var motionName = motion[0].File.Split('/', '.')[1];
                var motionJsonPath = Path.Combine(
                    Path.GetDirectoryName(ModelJsonFile) ?? string.Empty,
                    motion[0].File
                );
                var motionJson = CubismMotion3Json.LoadFrom(
                    await WebRequestHelper.GetTextDataAsync(motionJsonPath)
                );
                motionJson.Meta.FadeInTime = 1f;
                motionJson.Meta.FadeOutTime = 1f;

                // Create FadeMotionData
                var fadeMotion = CubismFadeMotionData.CreateInstance(
                    motionJson,
                    Path.GetFileName(motionJsonPath),
                    motionJson.Meta.Duration,
                    false,
                    false,
                    ModelJson
                );
                fadeMotion.name = $"{motionName}.fade";
                fadeMotion.FadeInTime = 1f;
                fadeMotion.FadeOutTime = 1f;

                for (var i = 0; i < fadeMotion.ParameterFadeInTimes.Length; i++)
                {
                    fadeMotion.ParameterFadeInTimes[i] = 1f;
                    fadeMotion.ParameterFadeOutTimes[i] = 1f;
                }

                // Create AnimationClip
                var animationClip = motionJson.ToAnimationClip(false, false, false, PoseJson);
                animationClip.name = motionName;
                animationClip.legacy = false;

                // NOTE: Not clearing animation events first causing fade motion list errors.
                // Create "InstanceId" AnimationEvent at the start
                // var animationClipEvents = animationClip.events;
                animationClip.events = Array.Empty<AnimationEvent>(); // Clear any events
                // for (var i = 0; i < animationClipEvents.Length; i++)
                // {
                var newEvent = new AnimationEvent
                {
                    functionName = "InstanceId",
                    time = 0,
                    intParameter = animationClip.GetInstanceID()
                };
                animationClip.AddEvent(newEvent);
                // }

                motionsData.Add(animationClip.GetInstanceID(), fadeMotion);
                Animations.Add(motionName, animationClip);
            }

            // Assign the CubismFadeMotionList to the CubismFadeController
            fadeMotionList.MotionInstanceIds = motionsData.Keys.ToArray();
            fadeMotionList.CubismFadeMotionObjects = motionsData.Values.ToArray();
            FadeController.CubismFadeMotionList = fadeMotionList;

            // Add touch area
            _touchAreas
                .Select(area => transform.Find($"{transform.name}/Drawables/{area}").gameObject)
                .Where(target => target != null)
                .ToList()
                .ForEach(target =>
                {
                    var touchArea = target.AddComponent<Touch>();
                    touchArea.CharacterViewer = this;

                    switch (touchArea.name)
                    {
                        case "TouchBody":
                            touchArea.PlayData.Add("touch_body", "touch");
                            break;
                        case "TouchHead":
                            touchArea.PlayData.Add("touch_head", "headtouch");
                            break;
                        case "TouchSpecial":
                            touchArea.PlayData.Add("touch_special", "touch2");
                            break;
                        default:
                            Debug.LogError("No touch data.");
                            break;
                    }
                });

            // Fetch voices audio
            if (Directory.Exists(VoicesDirectory))
            {
                var directoryInfo = new DirectoryInfo(VoicesDirectory);
                var files = directoryInfo.GetFiles();
                foreach (var file in files)
                {
                    try
                    {
                        var clip = await WebRequestHelper.GetAudioClip(file.FullName);
                        if (clip != null)
                            Voices.Add(Path.GetFileNameWithoutExtension(file.Name), clip);
                    }
                    catch (Exception ex)
                    {
                        Debug.LogWarning($"Can't load audio: {ex.Message}");
                    }
                }
            }

            // Enable the model gameObject to initialize the components
            Model.gameObject.SetActive(true);

            PlayInitialMotions();
        }

        private void PlayInitialMotions()
        {
            // Play the effect motion on layer 2
            PlayMotion("effect", true, 2, 1);

            // Play the login motion on layer 0
            PlayMotion("login", onComplete: OnLoginComplete);

            // PlayVoice("login");
        }

        private void OnLoginComplete(int instanceId)
        {
            // Play the idle motion on layer 0 after login completes
            PlayMotion("idle", true, priority: 1);

            // Unregister the completion handler
            MotionController.AnimationEndHandler -= OnLoginComplete;

            // Allow interactions
            AllowInteraction = true;
        }
    }
}


CubismViewerBase.cs:

using System;
using System.Reflection;
using Gilzoide.SerializableCollections;
using Kiraio.Azure.Core;
using Kiraio.Azure.Utils;
using Live2D.Cubism.Core;
// using Live2D.Cubism.Framework;
using Live2D.Cubism.Framework.Expression;
using Live2D.Cubism.Framework.Json;
using Live2D.Cubism.Framework.Motion;
using Live2D.Cubism.Framework.MotionFade;
// using Live2D.Cubism.Framework.Pose;
using Live2D.Cubism.Framework.Raycasting;
// using Live2D.Cubism.Rendering;
using Live2D.Cubism.Rendering.Masking;
using UnityEngine;

namespace Kiraio.Azure.Components
{
    public class CubismViewerBase : MonoBehaviour
    {
        public string ModelJsonFile { get; set; }
        public string VoicesDirectory { get; set; }

        protected CubismModel3Json ModelJson { get; set; }
        // protected CubismPhysics3Json PhysicsJson { get; set; }
        protected CubismPose3Json PoseJson { get; set; }
        protected CubismModel Model { get; set; }
        public CubismRaycaster Raycaster { get; set; }

        public CubismMotionController MotionController { get; set; }
        // protected CubismPoseController PoseController { get; set; }
        protected CubismExpressionController ExpressionController { get; set; }
        protected CubismMaskController MaskController { get; set; }
        protected CubismFadeController FadeController { get; set; }
        // protected CubismUpdateController UpdateController { get; set; }
        // protected CubismRenderController RenderController { get; set; }

        public Animator Animator { get; set; }
        public RuntimeAnimatorController AnimatorController { get; set; }
        public AudioSource VoiceSource { get; set; }

        // public SerializableDictionary<string, AudioClip> AnimationsVoices { get; set; } = new();
        public SerializableDictionary<string, AudioClip> Voices { get; set; } = new();
        public SerializableDictionary<string, AnimationClip> Animations { get; set; } = new();

        public MainControl MainControl { get; set; }
        public InputManager InputManager { get; set; }
        public bool AllowInteraction { get; set; }

        protected virtual void Awake()
        {
            MainControl = FindObjectsByType<MainControl>(FindObjectsSortMode.None)[0];
            InputManager = FindObjectsByType<InputManager>(FindObjectsSortMode.None)[0];
        }

        /// <summary>
        ///     Loads asset.
        /// </summary>
        /// <param name="absolutePath">Path to asset.</param>
        /// <returns>The asset on success; <see langword="null" /> otherwise.</returns>
        public static T LoadAsset<T>(string absolutePath) where T : class
        {
            return LoadAssetAtPath(typeof(T), absolutePath) as T;
        }

        /// <summary>
        ///     A helper method provided by the Live2D itself to load assets from the path.
        /// </summary>
        /// <param name="assetType"></param>
        /// <param name="absolutePath"></param>
        /// <returns></returns>
        /// <exception cref="NotSupportedException"></exception>
        protected static object LoadAssetAtPath(Type assetType, string absolutePath)
        {
            if (assetType == typeof(byte[]))
                return WebRequestHelper.GetBinaryData(absolutePath);
            if (assetType == typeof(string))
                return WebRequestHelper.GetTextData(absolutePath);
            if (assetType != typeof(Texture2D)) throw new NotSupportedException();
            var texture = new Texture2D(1, 1);
            texture.LoadImage(WebRequestHelper.GetBinaryData(absolutePath));
            return texture;
        }

        /// <summary>
        ///     A helper method to play the motion.
        /// </summary>
        /// <param name="motionName">The animation name.</param>
        /// <param name="isLoop">Loop the animation?</param>
        /// <param name="layerIndex">Which layer to play the animation?</param>
        /// <param name="priority">How important is the animation? Scale from 0 ~ 3.</param>
        /// <param name="onComplete">The callback to invoke when the animation completes.</param>
        public void PlayMotion(
            string motionName,
            bool isLoop = false,
            int layerIndex = 0,
            int priority = CubismMotionPriority.PriorityNormal,
            Action<int> onComplete = null
        )
        {
            if (!Animations.TryGetValue(motionName, out var animationClip))
            {
                Debug.LogWarning($"Motion {motionName} not found.");
                return;
            }

            // Stop the current motion on the target layer (if not looped)
            // if (MotionController.IsPlayingAnimation(layerIndex) && !isLoop)
            // {
            //     MotionController.StopAnimation(0, layerIndex);
            // }

            // Play the motion with the specified priority
            MotionController.PlayAnimation(
                animationClip,
                layerIndex,
                priority,
                isLoop
            );

            // Register completion handler
            if (onComplete != null) MotionController.AnimationEndHandler += onComplete;
        }

        public void PlayVoice(string voiceName)
        {
            if (!string.IsNullOrEmpty(voiceName))
            {
                VoiceSource.clip = Voices[voiceName];
                VoiceSource.Play();
            }
            else
            {
                Debug.LogWarning("No voice clip with the name provided.");
            }
        }

        /// <summary>
        ///     Resets the motion priorities for all layers.
        /// </summary>
        /// <param name="motionController">The CubismMotionController instance.</param>
        public static void ResetMotionPriorities(CubismMotionController motionController)
        {
            if (motionController == null)
            {
                Debug.LogWarning("MotionController is null. Cannot reset priorities.");
                return;
            }

            // Access the private _motionPriorities field using reflection
            var motionPrioritiesField = typeof(CubismMotionController).GetField(
                "_motionPriorities",
                BindingFlags.NonPublic | BindingFlags.Instance
            );

            if (motionPrioritiesField == null)
            {
                Debug.LogError("Failed to access _motionPriorities field. Has the Cubism SDK changed?");
                return;
            }

            // Get the current _motionPriorities array
            var motionPriorities = (int[])motionPrioritiesField.GetValue(motionController);

            if (motionPriorities == null || motionPriorities.Length == 0)
            {
                Debug.LogWarning("_motionPriorities array is null or empty. Cannot reset priorities.");
                return;
            }

            // Reset all priorities to 0 (PriorityNone)
            for (var i = 0; i < motionPriorities.Length; i++) motionPriorities[i] = CubismMotionPriority.PriorityNone;

            // Update the _motionPriorities field
            motionPrioritiesField.SetValue(motionController, motionPriorities);
        }
    }
}

Tagged:

Comments

Sign In or Register to comment.