Technical Help Needed Integrating UMA into DFU

Discuss coding questions, pull requests, and implementation details.
User avatar
MasonFace
Posts: 543
Joined: Tue Nov 27, 2018 7:28 pm
Location: Tennessee, USA
Contact:

Re: Technical Help Needed Integrating UMA into DFU

Post by MasonFace »

Well, I was never able to get UMA version 2.9 to compile into an assembly. I reached out to the UMA dev team and they said that the error I was getting was due to a bug and that it's fixed in UMA 2.10. So I upgraded and was able to get it to compile without any problems.

Unfortunately, UMA 2.10 requires .NET 4.0... so that won't work with DFU at the moment.

I won't let that stop me from developing, though. I'll keep working with UMA 2.9, then upgrade it to 2.10 once DFU gets upgraded to Unity 2019.4 LTS and .NET4.7.1.

Most of the work of this mod will be clothing/hair models and animations anyhow, so I probably wouldn't have enough assets to release a mod until late Spring anyway.

User avatar
Azteca
Posts: 143
Joined: Tue Mar 26, 2019 12:38 am

Re: Technical Help Needed Integrating UMA into DFU

Post by Azteca »

Awesome news, Mason. Thanks for keeping us posted. So you’ll have to do some waiting to share it with others but it sounds like this is still a very feasible project. I’ll be paying attention!

User avatar
MasonFace
Posts: 543
Joined: Tue Nov 27, 2018 7:28 pm
Location: Tennessee, USA
Contact:

Re: Technical Help Needed Integrating UMA into DFU

Post by MasonFace »

@TheLacus:

I'm having some trouble understanding exactly when the SetPerson method gets called on a MobliePersonAsset.

What is controlling the assignment of the race, gender, variant, and isGuard arguments?

My assumption was that the PopulationManager would assign all of these parameters to the MobileNPC upon initialization, but it doesn't look like that's where the assignment comes from after all.

The reason I ask is because it doesn't seem like the SetPerson method in my MobilePersonCube (yeah, I never renamed it from your example script) is consistently firing off. For debug purposes, I'm renaming the MobilePersonAsset inside the SetPerson method to take on the name in the format of Race Gender Variant (i.e. BretonFemale3), but not all instances of MobilePersonAsset are getting their name reassigned. Likewise, any other code I've put in the SetPerson method is not being ran.
NPCsetPerson.jpg
NPCsetPerson.jpg (50.78 KiB) Viewed 3828 times
If from the Unity Editor I use the "Apply Person Type" on the MobileNPC game object, then it will force it to update and successfully run all the SetPerson code, but of course it sets the race/gender/variant data to whatever was chosen in the inspector.

I'm not sure if it is important or not, but I'm having to delay getting the MeshRenderer because UMA requires a few miliseconds to create the mesh at runtime.

Is there a way to just force SetPerson to refresh its original assignment?

As a related followup questions, where do the portrait assignments come from? I'm just asking in case I want to later expand the MobilePersonAsset to have a face that matches their portrait.

User avatar
TheLacus
Posts: 1305
Joined: Wed Sep 14, 2016 6:22 pm

Re: Technical Help Needed Integrating UMA into DFU

Post by TheLacus »

PopulationManager handles a pool of npcs which are assigned a random race and gender and eventually recicled when outside player's field of view. I don't think there is necessarily something wrong if some npcs are not being assigned immediately.
MasonFace wrote: Sun Apr 05, 2020 3:51 pm I'm not sure if it is important or not, but I'm having to delay getting the MeshRenderer because UMA requires a few miliseconds to create the mesh at runtime.
MeshRenderer is accessed by PopulationManager here:

Code: Select all

// Do not render active mobile until it has made at least 1 full tile move
// This hides skating effect while unit aligning to navigation grid
if (poolItem.active && poolItem.npc.Asset)
{
    MeshRenderer billboardRenderer = poolItem.npc.Asset.GetComponent<MeshRenderer>();
    if (billboardRenderer)
        billboardRenderer.enabled = (poolItem.npc.Motor.MoveCount > 0) ? true : false;
}
But i wouldn't worry about this right now. It shouldn't affect core functionalities so this is something we can improve later if needed.
MasonFace wrote: Sun Apr 05, 2020 3:51 pm Is there a way to just force SetPerson to refresh its original assignment?
I'm not sure i understand, can you elaborate? If the issue is the MeshRender delay you can wait for it with a Coroutine. You can start it from SetPerson and pass all parameters required to setup the mesh after you have waited for it asynchronously.

User avatar
TheLacus
Posts: 1305
Joined: Wed Sep 14, 2016 6:22 pm

Re: Technical Help Needed Integrating UMA into DFU

Post by TheLacus »

Here's an example if it can be helpful. :)

Code: Select all

public override void SetPerson(Races race, Genders gender, int personVariant, bool isGuard)
{
    StartCoroutine(SetPersonAsync(race, gender, personVariant, isGuard));
}

private IEnumerator SetPersonAsync(Races race, Genders gender, int personVariant, bool isGuard)
{
    // Wait for MeshRenderer.
    // If you have a better way to know when UMA has finished is better...
    float timeout = 3;
    MeshRenderer meshRenderer = null;
    while (!meshRenderer && timeout  > 0)
    {
        yield return null;
        meshRenderer = GetComponent<MeshRenderer>();
        timeout -= Time.unscaledDeltaTime;
    }

    // do something with MeshRenderer
}

User avatar
TheLacus
Posts: 1305
Joined: Wed Sep 14, 2016 6:22 pm

Re: Technical Help Needed Integrating UMA into DFU

Post by TheLacus »

MasonFace wrote: Sun Apr 05, 2020 3:51 pm As a related followup questions, where do the portrait assignments come from? I'm just asking in case I want to later expand the MobilePersonAsset to have a face that matches their portrait.
TalkManager:

Code: Select all

DaggerfallUI.Instance.TalkWindow.SetNPCPortrait(DaggerfallTalkWindow.FacePortraitArchive.CommonFaces, targetMobileNPC.PersonFaceRecordId);
MobilePersonNPC:

Code: Select all

// get face record id to use (randomize portrait for current person outfit variant)
int personFaceVariant = Random.Range(0, numPersonFaceVariants);
this.personFaceRecordId = recordIndices[personOutfitVariant] + personFaceVariant;

// set billboard to correct race, gender and outfit variant
Asset = GetComponentInChildren<MobilePersonAsset>();
Asset.SetPerson(race, gender, personOutfitVariant, IsGuard);
If you want to match the face with the body you should pick the body for the given personOutfitVariant or, even better, for the resulting personFaceRecordId (transform.parent.GetComponent<MobilePersonNPC>().PersonFaceRecordId).

User avatar
MasonFace
Posts: 543
Joined: Tue Nov 27, 2018 7:28 pm
Location: Tennessee, USA
Contact:

Re: Technical Help Needed Integrating UMA into DFU

Post by MasonFace »

Thank you for the responses. That does help clarify a few things for me, but I'm still a little stuck.

The crux of my problem is that SetPerson doesn't seem to be initializing consistently. I've tried delaying the SetPerson using "Invoke" and added some delays using Coroutines, and that helped to a large degree, but most of the NPCs are acting as if their SetPerson method is never even getting called.

In my SetPerson code, I have it set so that the name of the MobilePersonAsset game object will change its name based on its Race, Gender, and Variant. It will also change its gender and set its height to the max value of 1 (very tall). But what's actually happening is that only some of them are getting their names changes, and only some are getting their heights changed.

For debug purposes, I've set my "H" key to call SetPerson with race = breton, gender = female, variant = 1, and isGuard = false. When I press "H", it does properly set all the MobileNPCs to very tall Breton Females, but of course I don't want all the villagers to be Amazon women; I'd rather they maintain their vanilla assignments.

So how do I get SetPerson to work consistently? Am I supposed to call it in my code somewhere? If so, am I supposed to randomize the override arguments? Am I approaching this totally wrong?

I hope this makes sense.

Note that I'm currently setting the return of GetSize() to a constant Vector3 of (1, 2, 1); I'll fix that later.
Also note that UMA distinguishes male and female archetypes as different "races". They use very misleading terminology...

Code below:
Spoiler!

Code: Select all

using UnityEngine;
using DaggerfallWorkshop;
using DaggerfallWorkshop.Game.Entity;
using DaggerfallWorkshop.Game.Utility.ModSupport;
using UMA.CharacterSystem;
using System.Collections;
using System.Collections.Generic;
using UMA.CharacterSystem;
using UMA;

namespace MobilePersonCubeMod
{
    [ImportedComponent]
    public class MobilePersonCube : MobilePersonAsset
    {

        public float waitTime = 0.5f;
        MeshRenderer meshRenderer;
        DynamicCharacterAvatar avatar;
        Animator anim;
        Transform player;
        private Dictionary<string, DnaSetter> dna;
        

        private void Awake()
        {

            anim = GetComponent<Animator>();
            player = GameObject.FindGameObjectWithTag("Player").transform;

            //Must wait some amount of time for the UMA character to initialize.
            Invoke("GetRenderer", waitTime);
           
        }

        public override bool IsIdle
        {
            get; set;
        }

        public override void SetPerson(Races race, Genders gender, int personVariant, bool isGuard)
        {

            StartCoroutine(SetPersonAsync(race, gender, personVariant, isGuard));

        }

        private IEnumerator SetPersonAsync(Races race, Genders gender, int personVariant, bool isGuard)
        {

            yield return new WaitForSeconds(waitTime);

            this.name = race.ToString() + gender.ToString() + personVariant.ToString();
            this.transform.localPosition = new Vector3(0f, -1f, 0f);

            Vector3 size = GetSize();
            Trigger.height = size.y * 1.2f;
            Trigger.radius = size.x * 1.2f;

            if (gender == Genders.Male)
            {
                StartCoroutine(ChangeGender("Male"));
            }
            else
            {
                StartCoroutine(ChangeGender("Female"));
            }

            StartCoroutine(ChangeDNA());
 
        }

        public void Update()
        {
            if (IsIdle)
            {
                anim.SetFloat("Speed", 0f);
                Vector3 playerPosition = new Vector3(player.transform.position.x, this.transform.position.y, player.transform.position.z);
                this.transform.LookAt(playerPosition);
            }
            else
            {
                anim.SetFloat("Speed", 1f);
                this.transform.localEulerAngles = Vector3.zero;
            }

            if(Input.GetKeyDown(KeyCode.H))
            {
                SetPerson(Races.Breton, Genders.Female, 1, false);
            }
        }


        public override Vector3 GetSize()
        {
            //This needs to be variable size depending on the size of the NPC.
            //Change this to be based on MeshRenderer.size in the future.
            return new Vector3(1f, 2f, 1f);

            //return meshRenderer.bounds.size;
        }

        private IEnumerator ChangeGender(string gender)
        {

            if (gender == "Male" && avatar.activeRace.name != "HumanMaleDCS")
            {
                avatar.ChangeRace("HumanFemaleDCS");
            }

            if (gender == "Female" && avatar.activeRace.name != "HumanFemaleDCS")
            {
                avatar.ChangeRace("HumanFemaleDCS");
            }

            yield return null;

        }

        private IEnumerator ChangeDNA()
        {


            yield return new WaitForSeconds(waitTime);

            dna = avatar.GetDNA();
           
            dna["height"].Set(1f);

            avatar.BuildCharacter();

        }

        public void GetRenderer()
        {
            avatar = GetComponentInChildren<DynamicCharacterAvatar>();
            meshRenderer = GetComponentInChildren<MeshRenderer>();
            
            dna = avatar.GetDNA();

        }

    }
}

User avatar
TheLacus
Posts: 1305
Joined: Wed Sep 14, 2016 6:22 pm

Re: Technical Help Needed Integrating UMA into DFU

Post by TheLacus »

Can you try to disable colliders and see if it makes any difference? I suspect they might interfer with MobilePersonMotor.IsDirectionClear() and cause them to be stuck in place.

I can confirm that you don't need to call SetPerson(), what you're doing now seems to be fine.

EDIT: yes, broken with commit prevent mobile NPCs from entering stairs.

User avatar
MasonFace
Posts: 543
Joined: Tue Nov 27, 2018 7:28 pm
Location: Tennessee, USA
Contact:

Re: Technical Help Needed Integrating UMA into DFU

Post by MasonFace »

Okay, thank you for looking into that! It was driving me crazy trying to figure out what was going on. :)

User avatar
TheLacus
Posts: 1305
Joined: Wed Sep 14, 2016 6:22 pm

Re: Technical Help Needed Integrating UMA into DFU

Post by TheLacus »

@MasonFace would it work for you if we use a layer mask to ignore colliders on npcs? I understand that you are using multiple layers, so this mask could be provided by the implementation at runtime if needed.

Post Reply