Get Non-Default Avatar Texture

Using Naninovel C# APIs: adding custom actor implementations or commands, overriding engine services, integrating with other systems, etc.
Post Reply
Nysalie
Posts: 29
Joined: 24 Jun 2020 20:28

Get Non-Default Avatar Texture

Post by Nysalie »

My intention is to access a non Default avatar texture set at the Characters configuration. This is related to my message system but since the question is not specific to the project I decided to create a new thread to facilitate searching if anyone has the same question.

As an example, I have a "Player" character, that char has 2 different avatars: "Player/Default" and "Player/Mobile".
The first one has a specific alpha channel set to fit my dialogue textprinter design, the second has a simple circle alpha to be used as a message profile picture with the chat textprinter.

I'm calling ICharacterManager.GetAvatarTextureFor(characterId) to get the Texture reference. As expected, this works for the Default avatar but how do I specify a different avatar path?
I tried ICharacterManager.GetAvatarTextureFor(characterId + "/Mobile") but since the Interface takes an actor id that doesn't work, it would result in a non existent Character ID.

Elringus
admin
Posts: 538
Joined: 11 May 2020 18:03

Re: Get Non-Default Avatar Texture

Post by Elringus »

There are two ways you can handle this.

  1. Don't use default avatars ("CharacterId/Default" avatar path) and instead set the avatar textures per character manually either with @char CharId avatar:AvatarPath command or via C# with ICharacterManager.SetAvatarTexturePathFor(characterId, avatarPath). In your message UI get avatar textures assigned to specific character with ICharacterManager.GetAvatarTexturePathFor(characterId).

  2. Directly access avatar resources (textures) when you need them. For that, use ResourceLoader; you can find in example in CharacterManager implementation. Basically, it would look something like the following:

Code: Select all

var avatarTextureLoader = charactersConfiguration.AvatarLoader.CreateLocalizableFor<Texture2D>(providerMngr, localizationManager);
var avatarTexture = await  avatarTextureLoader.LoadAsync(avatarTexturePath);
Nysalie
Posts: 29
Joined: 24 Jun 2020 20:28

Re: Get Non-Default Avatar Texture

Post by Nysalie »

Cool! In the meantime I've found a good practical example on the ModifyCharacter Command class and I see how the SetAvatarTexturePathFor() would work.

I have a custom UI implementing the IBacklog interface to keep a log of the messages sent from each contact. The messages are stored in a dictionary where the keys are contact's authorId and the value a list of message.states sent to and from the contact. The message state holds 2 fields, one with the authorId and the other with the text that is supposed to be printed.

When the user opens the conversation panel by clicking on a character on the contact list I iterate through the list assigned to that particular contact key and instantiate all the messages that are stored.

Each instantiated GameObject is a slightly tweaked ChatMessage prototype prefab and it serializes similar fields (avatar image (Texture) and text of message (TMP_Text), i don't feel that the author name is necessary in this case so I removed it from the prefab). After the prefab is instantiated I initialize it through a simple method where I pass the message text and the avatar image values and assign them to the corresponding components.

So this means that accessing the Loader directly is probably the way to go, I don't need to mess with avatar paths and worry about the printers, I just need that particular texture from the provider :D

Thanks a lot for the guidance once again, I'm making some progress with this little project.

Nysalie
Posts: 29
Joined: 24 Jun 2020 20:28

Re: Get Non-Default Avatar Texture

Post by Nysalie »

By the way, do you have any tips on debugging a null state on deserialization? I'm having some trouble deserializing the dictionary I mentioned earlier. I've attached VS to Unity and was able to confirm that serialization seems to be working properly, the state is populated whenever I push a snapshot. However, on load I'm getting a null state for some reason.

I'm not sure if the problem comes from another place in my code but I think that these 2 files are relevant to the issue. I would be extremely thankful if you could have a quick look at them and see if you can spot any blatant mistake.

ConversationUI: (acts like a backlog to store and populate messages on a panel. Was also implementing IBacklog but just inheriting CustomUI is enough)

Code: Select all

using System.Collections.Generic;
using System.Text.RegularExpressions;
using Naninovel;
using Naninovel.UI;
using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;

namespace NaninovelPhone.UI
{
    public class ConversationUI : CustomUI
    {
        [System.Serializable]
        public new class GameState
        {
            public Dictionary<string, List<ConversationMessage.State>> Messages;
        }

    protected bool StripTags => stripTags;

    [SerializeField] private RectTransform messagesContainer = default;
    [SerializeField] private ScrollRect scrollRect = default;
    [SerializeField] private ConversationMessage contactMessagePrefab = default;
    [SerializeField] private ConversationMessage playerMessagePrefab = default;
    [Tooltip("Whether to strip formatting content (content inside `<` `>` and the angle brackets themselves) from the added messages.")]
    [SerializeField] private bool stripTags = true;

    private ICharacterManager charManager;
    private PhoneManager phoneManager;
    private IStateManager stateManager;
    private Dictionary<string, List<ConversationMessage.State>> messages;

    private static readonly Regex formattingRegex = new Regex(@"<.*?>");

    protected override void Awake()
    {
        base.Awake();
        this.AssertRequiredObjects(messagesContainer, scrollRect, contactMessagePrefab, playerMessagePrefab);

        charManager = Engine.GetService<ICharacterManager>();
        phoneManager = Engine.GetService<PhoneManager>();
        stateManager = Engine.GetService<IStateManager>();
        messages = new Dictionary<string, List<ConversationMessage.State>>();
    }

    public void AddMessage(string message, string actorId, string contactId)
    {
        if (StripTags) message = StripFormatting(message);

        var messageInstance = SpawnMessage(message, actorId);
        var contactName = charManager.GetDisplayName(contactId);

        List<ConversationMessage.State> smsList;

        if (!messages.TryGetValue(contactName, out smsList)) 
        {
            smsList = new List<ConversationMessage.State>();
            messages[contactName] = smsList;
        }

        smsList.Add(messageInstance.GetState());

        PopulateContainer(contactId);
        stateManager.PushRollbackSnapshot();
    }

    public void Clear()
    {
        ObjectUtils.DestroyAllChilds(messagesContainer);
        messages.Clear();
    }

    protected virtual ConversationMessage SpawnMessage (string messageText, string authorId)
    {
        var message = default(ConversationMessage);
        var actorNameText = charManager.GetDisplayName(authorId);
        var avatarImage = phoneManager.GetMobileAvatar(authorId);

        if (authorId == "Player")
        {
            message = Instantiate(playerMessagePrefab);
        }
        else
        {
            message = Instantiate(contactMessagePrefab);
        }
        message.transform.SetParent(messagesContainer.transform, false);
        message.AuthorId = authorId;
        message.InitializeMessage(messageText, actorNameText, avatarImage);

        return message;
    }

    public void PopulateContainer(string contactId)
    {
        ObjectUtils.DestroyAllChilds(messagesContainer);
        
        if(!messages.ContainsKey(contactId)) return;

        var contactMessages = messages[contactId];

        foreach (var message in contactMessages)
        {
            SpawnMessage(message.PrintedText, message.AuthorId);
        }
    }

    protected virtual string StripFormatting (string content) => formattingRegex.Replace(content, string.Empty);

    protected override void SerializeState(GameStateMap stateMap)
    {
        base.SerializeState(stateMap);

        var state = new GameState() {
            Messages = messages
        };
        stateMap.SetState(state);
    }

    protected override async UniTask DeserializeState(GameStateMap stateMap)
    { 
        await base.DeserializeState(stateMap);

        Clear();

        var state = stateMap.GetState<GameState>();
        if (state is null) return;

        messages = state.Messages;
    }
}
}

ConversationMessage: (the message state that is supposed to be serialized on the values of the dictionary)

Code: Select all

using Naninovel;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace NaninovelPhone.UI
{
    public class ConversationMessage : ScriptableUIBehaviour
    {
        [System.Serializable]
        public struct State
        {
            public string AuthorId;
            public string PrintedText;
        }

    public virtual string MessageText { get => printedText.text; set => printedText.text = value; }
    public virtual string AuthorId { get; set; }
    public virtual Color MessageColor { get => messageFrameImage.color; set => messageFrameImage.color = value; }
    public virtual Texture AvatarTexture { get => avatarImage.texture; set { avatarImage.texture = value; avatarImage.gameObject.SetActive(value); } }


    protected TMP_Text PrintedText => printedText;
    protected Image MessageFrameImage => messageFrameImage;
    protected RawImage AvatarImage => avatarImage;

    [SerializeField] private TMP_Text printedText = default;
    [SerializeField] private Image messageFrameImage = default;
    [SerializeField] private RawImage avatarImage = default;

    public virtual ConversationMessage.State GetState () => new ConversationMessage.State { PrintedText = MessageText, AuthorId = AuthorId };

    protected override void Awake ()
    {
        base.Awake();
        this.AssertRequiredObjects(printedText, messageFrameImage, avatarImage);
    }

    public void InitializeMessage(string message, string authorName, Texture authorImage)
    {
        MessageText = message;
        AvatarTexture = authorImage;
    }
}
}
Elringus
admin
Posts: 538
Joined: 11 May 2020 18:03

Re: Get Non-Default Avatar Texture

Post by Elringus »

Unity can't serialize Dictionaries: https://docs.unity3d.com/Manual/script- ... ation.html Consider using a custom type instead.

Nysalie
Posts: 29
Joined: 24 Jun 2020 20:28

Re: Get Non-Default Avatar Texture

Post by Nysalie »

Oh man that was a big facepalm. I was so focused on pulling my hair out with this issue that I didn't even consider that as a possibility...

I'm sure there is a plausible explanation for it (performance perhaps) but Unity really should offer a standard solution to serialize dictionaries, I would guess that it is quite a common thing to do.

Anyway, found this thread that helped a lot. Used the SerializableDictionary class from OP and it worked great, plus since it essentially mimics a default Dictionary it fits nicely with my already implementation of the dict. Keep in mind that OP's solution is not the most efficient (mainly due to the serialization of all boilerplate variables) and there are some free assets that offer a similar solution, in my case, unless I run into some issues, i'll keep it, it's simple to implement and there is no need to refactor my code to accommodate the new class, however if anyone want's to do something similar and is concerned about performance, check the store.

Also had some trouble serializing the Lists as values, turns out that we need to subclass them. This is the base ideia of the implementation.

Code: Select all

[System.Serializable]
public class ContactsDict : SerializableDictionary<string, ContactMessages> { }

[System.Serializable]
public class ContactMessages
{
    public List<ConversationMessage.State> msgs;

public ContactMessages()
{
    msgs = new List<ConversationMessage.State>();
}
}

And here's my final ConversationUI.cs file:

Code: Select all

using System.Collections.Generic;
using System.Text.RegularExpressions;
using Naninovel;
using Naninovel.UI;
using NaninovelPhone.UI;
using UniRx.Async;
using UnityEngine;
using UnityEngine.UI;

[System.Serializable]
public class ContactsDict : SerializableDictionary<string, ContactMessages> { }

[System.Serializable]
public class ContactMessages
{
    public List<ConversationMessage.State> msgs;

public ContactMessages()
{
    msgs = new List<ConversationMessage.State>();
}
}

namespace NaninovelPhone.UI
{
    public class ConversationUI : CustomUI
    {
        [System.Serializable]
        public new class GameState
        {
            public ContactsDict Messages;
        }

    protected bool StripTags => stripTags;

    [SerializeField] private RectTransform messagesContainer = default;
    [SerializeField] private ScrollRect scrollRect = default;
    [SerializeField] private ConversationMessage contactMessagePrefab = default;
    [SerializeField] private ConversationMessage playerMessagePrefab = default;
    [Tooltip("Whether to strip formatting content (content inside `<` `>` and the angle brackets themselves) from the added messages.")]
    [SerializeField] private bool stripTags = true;

    private ICharacterManager charManager;
    private PhoneManager phoneManager;
    private IStateManager stateManager;
    private ContactsDict messages;

    private static readonly Regex formattingRegex = new Regex(@"<.*?>");

    protected override void Awake()
    {
        base.Awake();
        this.AssertRequiredObjects(messagesContainer, scrollRect, contactMessagePrefab, playerMessagePrefab);

        charManager = Engine.GetService<ICharacterManager>();
        phoneManager = Engine.GetService<PhoneManager>();
        stateManager = Engine.GetService<IStateManager>();
        messages = new ContactsDict();
    }

    public void AddMessage(string message, string actorId, string contactId)
    {
        if (StripTags) message = StripFormatting(message);

        var messageInstance = SpawnMessage(message, actorId);
        var contactName = charManager.GetDisplayName(contactId);

        ContactMessages smsList;

        if (!messages.TryGetValue(contactName, out smsList)) 
        {
            smsList = new ContactMessages();
            messages[contactName] = smsList;
        }

        smsList.msgs.Add(messageInstance.GetState());

        PopulateContainer(contactId);
        stateManager.PushRollbackSnapshot();
    }

    public void Clear()
    {
        ObjectUtils.DestroyAllChilds(messagesContainer);
        messages.Clear();
    }

    protected virtual ConversationMessage SpawnMessage (string messageText, string authorId)
    {
        var message = default(ConversationMessage);
        var actorNameText = charManager.GetDisplayName(authorId);
        var avatarImage = phoneManager.GetMobileAvatar(authorId);

        if (authorId == "Player")
        {
            message = Instantiate(playerMessagePrefab);
        }
        else
        {
            message = Instantiate(contactMessagePrefab);
        }
        message.transform.SetParent(messagesContainer.transform, false);
        message.AuthorId = authorId;
        message.InitializeMessage(messageText, actorNameText, avatarImage);

        return message;
    }

    public void PopulateContainer(string contactId)
    {
        ObjectUtils.DestroyAllChilds(messagesContainer);
        
        if(!messages.ContainsKey(contactId)) return;

        var contactMessages = messages[contactId].msgs;

        foreach (var message in contactMessages)
        {
            SpawnMessage(message.PrintedText, message.AuthorId);
        }
    }

    protected virtual string StripFormatting (string content) => formattingRegex.Replace(content, string.Empty);

    protected override void SerializeState(GameStateMap stateMap)
    {
        base.SerializeState(stateMap);

        var state = new GameState() {
            Messages = messages
        };
        stateMap.SetState(state);
    }

    protected override async UniTask DeserializeState(GameStateMap stateMap)
    { 
        await base.DeserializeState(stateMap);

        Clear();

        var state = stateMap.GetState<GameState>();
        if (state is null) return;

        messages = state.Messages;
    }
}
}
Elringus
admin
Posts: 538
Joined: 11 May 2020 18:03

Re: Get Non-Default Avatar Texture

Post by Elringus »

Naninovel also has a serializable dictionary, it's called SerializableMap (sorry I didn't mention it earlier). I don't think it's a performance issue, more like they don't use dict themselves and didn't bother implementing. Polymorphic and dict serialization was requested for a long time now; they're adding polymorphic support in Unity 2020, not sure about dictionaries, though.

Nysalie
Posts: 29
Joined: 24 Jun 2020 20:28

Re: Get Non-Default Avatar Texture

Post by Nysalie »

No worries, this solution also works and is relatively simple to implement.
Yes that's a very probable cause as well. The thread I mentioned earlier is 5 years old now and I've come across quite a few older than that, this might not be one of their priorities but it sure feels like an often requested feature.

Post Reply