Broken animation in Windows build, fine in play mode

Hi Guys,

I’m stuck for days with an issue on animating my character. In play mode it works perfect, unfortunately whenever I build and start it the results looks like all my characters are trapped in a twister.

I have an object called player with 5 child objects body, hair, eyes, outfit and accessory. The user should be able to change the look, so I have 5 different images rendered in a specific order to ensure the character looks as it should.

Each different visual object such as body, hair or accessory has 48 different sprites, as each animation consists of 6 sprites * 4 for each direction * 2 for standing and walking.

This way I have e.g. 27 hair styles, which additionally have 7 hair colors each.

This issue is happening for each of these objects, so I use the body as the simplest example.

Initially I had created an animation controller and used animations triggered with keyboard inputs to decide if the character is standing or walking, but then I had this issue, so I removed the animations and decided to update the sprites in each FixedUpdate() call instead.

Again working fine in play mode, but not in the built game.

The player looks like each part constantly receives different inputs, resulting in a body looking left, hair from the right looking, equipment from the up looking and so on.

This changes several times per second so it looks the character is in a twister.

The animationIds are ordered and should loop only in a specific way, e.g. 0-5 = looking down, 6-11 looking left and so on.

I have another script called Move which uses keyboard inputs to calculate the offset and loop through the 6 sprites. If I’m e.g. walking right it will set it’s animationId which is referenced in each of the scripts to an integer looping through [42,43,44,45,46,47] every 0.1f seconds.

I have added a Text Mash Pro GUI field to show the current animationId. It’s correct, always resulting in the correct number, in play mode and the built game.

Please take a look at my code, maybe you have an idea

public class Body : UpdateReceiver
{
    private int bodyId = 0;
    private int animationId = 0;

    private Sprite[] spriteList = new Sprite[48];
    private int lastBodyId = 0;
    private int lastAnimationId = 0;
    public bool ui = false;

    protected void Start()
    {
        bodyId = gameObject.GetComponentInParent<Person>().playerLook._bodyId % PersonStatics.bodyList.Length;
        AsyncOperationHandle<Sprite[]> spriteHandle = Addressables.LoadAssetAsync<Sprite[]>("Assets/Tilemaps/Character/Body/" + PersonStatics.bodyList[bodyId % PersonStatics.bodyList.Length] + ".png");
        spriteHandle.Completed += LoadSpritesWhenReady;
        lastBodyId = bodyId;
    }

    protected void LoadSpritesWhenReady(AsyncOperationHandle<Sprite[]> handleToCheck)
    {
        if (handleToCheck.Status == AsyncOperationStatus.Succeeded)
        {
            spriteList = handleToCheck.Result;
            if (ui)
            {
                GetComponent<Image>().sprite = spriteList[animationId];
            }
            else
            {
                GetComponent<SpriteRenderer>().sprite = spriteList[animationId];
            }
        }
    }

    protected void FixedUpdate()
    {
        bodyId = gameObject.GetComponentInParent<Person>().playerLook._bodyId % PersonStatics.bodyList.Length;
        animationId = gameObject.GetComponentInParent<Move>().animationId % 48;
        if (lastBodyId != bodyId)
        {
            AsyncOperationHandle<Sprite[]> spriteHandle = Addressables.LoadAssetAsync<Sprite[]>("Assets/Tilemaps/Character/Body/" + PersonStatics.bodyList[bodyId % PersonStatics.bodyList.Length] + ".png");
            spriteHandle.Completed += LoadSpritesWhenReady;
            lastBodyId = bodyId;
        }
        else if(animationId != lastAnimationId)
        {
            if (ui)
            {
                GetComponent<Image>().sprite = spriteList[animationId];
            }
            else
            {
                GetComponent<SpriteRenderer>().sprite = spriteList[animationId];
            }
        }
        lastAnimationId = animationId;
    }
}

Thanks in advance!

Regards,
Christian

maybe video clip to compare editor vs build would help.

check also editor.log for possible error messages

check if build and editor have same quality settings,
check if vsync makes difference.

Hi mgear,

Thanks for the quick response :slight_smile:

I checked all logs, but unfortunately I was not able to find any suspicious errors.

Both quality settings are the same and modifying vsync didn’t have an effect.

This is how it looks in the editor: https://serverless-works--dev.s3.eu-central-1.amazonaws.com/City+-+AnimationTest+-+Windows%2C+Mac%2C+Linux+-+Unity+2022.2.19+_DX11_+2023-10-18+15-59-51.mp4

vs the whirlwinds in the built game: https://serverless-works--dev.s3.eu-central-1.amazonaws.com/City+2023-10-18+16-03-02.mp4

The white numbers indicate the animationId and it looks the same for both.

Any further idea what might be wrong?

Best Regards,
Christian

is it using custom shader?
any difference if you disable dynamic batching or sprite packer?

how do you get input? (if can show those lines from script)

Hi mgear,

Thanks, I just tried your suggestions. Disabling dynamic batching or changing the sprite packer (was v1 always, tried disabled and v2 always) didn’t change. It also doesn’t use a custom shader.

What kind of input are refering to? My user inputs to move?

This is my script move which is used to move 64px after each input and it also updates the public animationId which is reused in my other scripts to always animate the correct sprite. I also tried to store only the activityType in a public field and do the animationId in the other scripts, but this didn’t help as well:

using System;
using TMPro;
using UnityEngine;
using UnityEngine.Tilemaps;
using static PersonStatics;

public class Move : UpdateReceiver
{
    public Tilemap ground;
    public Tilemap blocked;
    private Vector2 targetPosition;
    public float speedX = 1f;
    public float speedY = 1f;
    private Vector2 movement;
    private bool animationSet = false;
    public float initX = 0;
    public float initY = -1;
    private float prevX = 0;
    private float prevY = 0;
    private Vector2 moveDelta;
    public bool player = false;
    public string remoteControl = "";
    private string instructions = "";
    public PersonActivity initialActivity = PersonActivity.lookDown;
    private PersonActivity currentActivity;
    public int animationId;
    private float timeDelta = 0;
    public float animationSpeed = 0.1f;

    //private BoxCollider2D boxCollider;
    private bool blockUp = false;
    private bool blockDown = false;
    private bool blockLeft = false;
    private bool blockRight = false;
    private Vector2 raySource;
    private bool locked;

  

    private void Awake()
    {
        player = gameObject.GetComponentInParent<Person>().player;
        targetPosition = new Vector2(transform.position.x,transform.position.y);
        transform.position = targetPosition;
        // boxCollider = GetComponent<BoxCollider2D>();
        currentActivity = initialActivity;
    }

    private void Animate()
    {
        timeDelta += Time.deltaTime;
        int framesPassed = Convert.ToInt32(Math.Floor(timeDelta / animationSpeed));
        animationId = framesPassed % 6 + GetAnimationOffset(currentActivity);
        if (player)
        {
            GameObject.Find("AnimationIdText").GetComponent<TextMeshProUGUI>().text = animationId.ToString();
        }
    }

    private void FixedUpdate()
    {
        Animate();
        locked = gameObject.GetComponentInParent<Person>().locked;
        raySource = (Vector2)transform.position + (Vector2.down * 0.5f) + (Vector2.right * 0.5f);
        transform.rotation = Quaternion.AngleAxis(0, Vector3.zero);
        var moving = (Vector2)transform.position != targetPosition;
        if (moving && Vector2.Distance((Vector2)transform.position, targetPosition) <= 0.03f)
        {
            MoveTowardsTargetPosition();
            moving = false;
            switch(currentActivity)
            {
                case PersonActivity.moveUp:
                    currentActivity = PersonActivity.lookUp;
                    break;
                case PersonActivity.moveDown:
                    currentActivity = PersonActivity.lookDown;
                    break;
                case PersonActivity.moveLeft:
                    currentActivity= PersonActivity.lookLeft;
                    break;
                case PersonActivity.moveRight:
                    currentActivity= PersonActivity.lookRight;
                    break;
            }
        }
        if (remoteControl != "")
        {
            instructions = remoteControl;
            remoteControl = "";
        }
        else if (instructions != null && instructions.Length != 0)
        {
            if (moving)
            {
                MoveTowardsTargetPosition();
            }
            else
            {
                SetNewTargetPositionFromControl();
                instructions = instructions.Substring(1);
            }
        }
        else
        {
            if (player)
            {
                movement.x = Input.GetAxisRaw("Horizontal");
                movement.y = Input.GetAxisRaw("Vertical");


                if (moving)
                {
                    MoveTowardsTargetPosition();
                }
                else
                {
                    blockUp = Physics2D.Linecast(raySource, raySource + (Vector2.up * 1.4f), LayerMask.GetMask("Blocking"));
                    blockDown = Physics2D.Linecast(raySource, raySource + (Vector2.down * 1.4f), LayerMask.GetMask("Blocking"));
                    blockLeft = Physics2D.Linecast(raySource, raySource + (Vector2.left * 1.4f), LayerMask.GetMask("Blocking"));
                    blockRight = Physics2D.Linecast(raySource, raySource + (Vector2.right * 1.4f), LayerMask.GetMask("Blocking"));
                    SetNewTargetPositionFromInput();
                }
            }
        }
    }

    private void MoveTowardsTargetPosition()
    {
        transform.position = Vector2.MoveTowards(transform.position, targetPosition, speedX * Time.deltaTime);
    }

    private void SetNewTargetPositionFromInput()
    {
        if (!locked)
        {
            float x = Input.GetAxisRaw("Horizontal");
            float y = Input.GetAxisRaw("Vertical");
            if ((x != 0 && x != prevX) || (y != 0 && y != prevY))
            {
                animationSet = true;
            }
            prevX = x;
            prevY = y;
            moveDelta = new Vector2(x * speedX, y * speedY);
            if (y == 1 && !blockUp)
            {
                currentActivity = PersonActivity.moveUp;
                targetPosition += Vector2.up;
            }
            else if (y == -1 && !blockDown)
            {
                currentActivity = PersonActivity.moveDown;
                targetPosition += Vector2.down;
            }
            else if (x == 1 && !blockRight)
            {
                currentActivity = PersonActivity.moveRight;
                targetPosition += Vector2.right;
            }
            else if (x == -1 && !blockLeft)
            {
                currentActivity = PersonActivity.moveLeft;
                targetPosition += Vector2.left;
            }
            else
            {
                animationSet = false;
                switch (currentActivity)
                {
                    case PersonActivity.moveUp:
                        currentActivity = PersonActivity.lookUp;
                        break;
                    case PersonActivity.moveDown:
                        currentActivity = PersonActivity.lookDown;
                        break;
                    case PersonActivity.moveLeft:
                        currentActivity = PersonActivity.lookLeft;
                        break;
                    case PersonActivity.moveRight:
                        currentActivity = PersonActivity.lookRight;
                        break;
                }
            }
        }
        else
        {
            animationSet = false;
            switch (currentActivity)
            {
                case PersonActivity.moveUp:
                    currentActivity = PersonActivity.lookUp;
                    break;
                case PersonActivity.moveDown:
                    currentActivity = PersonActivity.lookDown;
                    break;
                case PersonActivity.moveLeft:
                    currentActivity = PersonActivity.lookLeft;
                    break;
                case PersonActivity.moveRight:
                    currentActivity = PersonActivity.lookRight;
                    break;
            }
        }
    }

    private void SetNewTargetPositionFromControl()
    {
        if (instructions[0].Equals("u".ToCharArray()[0]))
        {
            targetPosition += Vector2.up;
        }
        else if (instructions[0].Equals("d".ToCharArray()[0]))
        {
            targetPosition += Vector2.down;
        }
        else if (instructions[0].Equals("r".ToCharArray()[0]))
        {
            targetPosition += Vector2.right;
        }
        else if (instructions[0].Equals("l".ToCharArray()[0]))
        {
            targetPosition += Vector2.left;
        }
        else if (animationSet)
        {
            animationSet = false;
        }
    }
}

I also have this in a static class PersonStatics:

public enum PersonActivity
    {
        lookUp,
        lookDown,
        lookLeft,
        lookRight,
        moveUp,
        moveDown,
        moveLeft,
        moveRight,
    }

    public static Int32 GetAnimationOffset(PersonActivity activity)
    {
        switch (activity)
        {
            case PersonActivity.lookUp:
                return 6;
            case PersonActivity.lookDown:
                return 18;
            case PersonActivity.lookLeft:
                return 12;
            case PersonActivity.lookRight:
                return 0;
            case PersonActivity.moveUp:
                return 30;
            case PersonActivity.moveDown:
                return 42;
            case PersonActivity.moveLeft:
                return 36;
            case PersonActivity.moveRight:
                return 24;
            default: return 0;
        }
    }

These are my Player settings:



Does this help?

Thanks in advance!

Best Regards,
Christian

not sure whats happening…
some notes,

  • Input.getaxis shouldn’t be inside FixedUpdate (it shouldnt work there properly)
  • try printing out if somehow input values are getting set in build?
  • do you unsubscribe from all events when done
  • theres input.getaxis in 2 places?

or,
if you disable those input.getaxis lines or set to 0, is the character still bugged?
if you set monitor refresh rate to 60, any difference?

Hi mgear,

Sorry for the delay, had some busy days at work.

Thanks for your findings, removed one input.getAxis.

I now also replaced input.getAxis with player inputs from the Rewired package, didn’t help.

Setting movement x and y to 0 doesn’t change a thing, still twister.

My monitor already is 60fps.

When setting the animationId to 0 the animation stops but the person has the correct sprites in use and there is no twister.

I added additional text boxes for displaying the animationId and now it gets even more weird.

In the editor the debugged values look strange, as if they would not change based on the activity, they only look correct when looking down (default state)

9431207--1322297--upload_2023-10-25_21-37-39.png

And in the windows built it’s exactly the opposite, there the values are always correct, but the displayed sprites look totally different.(body looking left, outfit moving left, eyes in between looking left and looking right, accessory looking down, hair looking down in this screenshot)

In each script I retrieve the correct animationId like this:

animationId = gameObject.GetComponentInParent().animationId;

Any further ideas what might be the reason?

Thanks in advance!

Best Regards,
Christian

yup, hard to say at this point…
need to try to debug & track down what changes those animations.

could also test runtime inspector, if they would show something in build
https://github.com/yasirkula/UnityRuntimeInspector
https://github.com/ManlyMarco/RuntimeUnityEditor

or if all fails, try to rewrite the script in more simpler way, starting from single object maybe.
(it seems bit complicated for “just” handling movement and animations?)

its also possible that script execution order matters, if you do things in Awake/Start with many scripts.

Hi mgear,

Thanks for the hint, the Runtime Inspector is great, added it, stopped the animation and manually set the animationId in the move component to different values.

It seems like every single animationId is fixed to one look, but the sprites are different than in the editor as if they would pick a different one over there. But it still keeps the correct style, just the style is different.

Therefore I guessed the error is somewhere in my logic where I decide which sprite to use.

If we take the body as easiest example there we have a static class PersonStatics with this static string array:

public static string[] bodyList = new string[]
    {
        "Body_01",
        "Body_02",
        "Body_03",
        "Body_04",
        "Body_05",
        "Body_06",
        "Body_07",
        "Body_08",
        "Body_09",
    };

In the body script I retrieve the current body and load the sprites and add a handler for when it’s completed in the start function:

bodyId = gameObject.GetComponentInParent<Person>().playerLook._bodyId % PersonStatics.bodyList.Length;
        AsyncOperationHandle<Sprite[]> spriteHandle = Addressables.LoadAssetAsync<Sprite[]>("Assets/Tilemaps/Character/Body/" + PersonStatics.bodyList[bodyId % PersonStatics.bodyList.Length] + ".png");
        spriteHandle.Completed += LoadSpritesWhenReady;

In the handler I assign the results to the sprite list and set the sprite in the sprite renderer to the correct sprite:

protected void LoadSpritesWhenReady(AsyncOperationHandle<Sprite[]> handleToCheck)
    {
        if (handleToCheck.Status == AsyncOperationStatus.Succeeded)
        {
            spriteList = handleToCheck.Result;
            if (ui)
            {
                GetComponent<Image>().sprite = spriteList[animationId];
            }
            else
            {
                GetComponent<SpriteRenderer>().sprite = spriteList[animationId];
            }
        }
    }

Therefore e.g. if the bodyId is 0 and animationId is 5 then it loads all sprites from Assets/Tilemaps/Character/Body/Body_01.png which is correct, but then it should load the 6th element from the spritelist.

In editor therefore the sprites are assigned:

animationId 0 -->Body_01_0
animationId 1 -->Body_01_1

while in the built it’s

animationId 0 -->Body_01_44
animationId 1 -->Body_01_26

So “spriteList = handleToCheck.Result;” returns an ordered list in editor but an unsorted one in the built game.

So adding another line for sorting solved this issue, now it’s finally working :slight_smile:

Array.Sort(spriteList, delegate (Sprite x, Sprite y) { return Int32.Parse(x.name.Split("_")[2]).CompareTo(Int32.Parse(y.name.Split("_")[2])); });

Thank you very much for your support :slight_smile:

Best Regards,
Christian

1 Like