prime31 / Nez

Nez is a free 2D focused framework that works with MonoGame and FNA
MIT License
1.76k stars 357 forks source link

SpriteAnimator can't SetOriginNormalized in Animation? #817

Open legoeugen opened 1 week ago

legoeugen commented 1 week ago

I create an Entity with a Sprite Animator then I load a texture atlas and add the animations to the Sprite Animator, then I scale it and put it to the right position. After that, I want to set the Origin to the bottom middle. This does not work. I tried it also updating it in the update method, setting the origin before scaling and setting the position or setting the origin before and after calling play but is never works. It only gets the correct Origin when the animation is not playing. After the Animation is done, the code does what it should do and set it to the correct origin.

Is there something I am missing, or is this a bug/issue in Nez?

Here is my code for Creating the Entity

public void CreateLightning()
{
    //Create Entity
    Lightning = GameScene.Instance.CreateEntity("Lightning");

    //Add Components
    SpriteAnimator animator = Lightning.AddComponent<SpriteAnimator>();

    //Set Components
    var texture = GameScene.Instance.Content.LoadTexture(Content.Weather.Storm.LightningStrike);
    var sprites = Sprite.SpritesFromAtlas(texture, 16, 32);

    animator.LayerDepth = 0;
    animator.SetMaterial(new Material(BlendState.Additive));

    //Set Animations
    animator.AddAnimation("Strike", sprites.ToArray(), 30f);

    //Make Impact
    Lightning.Position = chunkTiles.RandomItem().Entity.Position;
    Lightning.Scale = new Vector2(10,10);

    animator.Play("Strike", LoopMode.Once);

    //Set Origin
    animator.SetOriginNormalized(new Vector2(.5f, 1));

    animator.OnAnimationCompletedEvent += LightningCompleted;
}
optimo commented 1 week ago

It might be considered a bug but more likely an oversight for an uncommon usage scenario.

It just happens that when you use SpritesFromAtlas, an array of Sprites is created, with the array filled via new Sprite(texture, sourceRect), rather than by using the alternative constructor that accepts an origin argument. You can see this yourself where the array is created: https://github.com/prime31/Nez/blob/09b190c547aeb2e024df9c755bf058f2950433a0/Nez.Portable/Graphics/Textures/Sprite.cs#L151-L152

That call falls through to a constructor that assumes you mean a default origin for each. It just means that by building the animation from that convenient atlas method all those will have an origin positioned in the default middle of the sprite.

The SetOrigin method and Origin property of SpriteAnimator come from part of the SpriteRenderer that it wraps - the animator manages which sprite should be rendered. The SpriteRenderer usually gets' its origin set when SetSprite is called. And changing it after that would be normal use-case.

An easy fix for you might be to modify or create your own alternative SpritesFromAtlas method in the class, where you use the origin-preserving constructor for generating the new Sprite instances that are added to the atlas. It's only a handful of lines of code anyway. In either case, be sure to set your origin before you add all the sprites so that value can be set in the constructor call.

I am curious though what your objective is for using the origin - it's not clear from your examples. My limited experience with it is how it's used as the pivot point for rotations and scaling. Just to be sure; if your intent was rather to offset the position of the sprite with relation to its entity, you may want to use the component's localoffset.

legoeugen commented 6 days ago

It might be considered a bug but more likely an oversight for an uncommon usage scenario.

It just happens that when you use SpritesFromAtlas, an array of Sprites is created, with the array filled via new Sprite(texture, sourceRect), rather than by using the alternative constructor that accepts an origin argument. You can see this yourself where the array is created:

https://github.com/prime31/Nez/blob/09b190c547aeb2e024df9c755bf058f2950433a0/Nez.Portable/Graphics/Textures/Sprite.cs#L151-L152

That call falls through to a constructor that assumes you mean a default origin for each. It just means that by building the animation from that convenient atlas method all those will have an origin positioned in the default middle of the sprite.

The SetOrigin method and Origin property of SpriteAnimator come from part of the SpriteRenderer that it wraps - the animator manages which sprite should be rendered. The SpriteRenderer usually gets' its origin set when SetSprite is called. And changing it after that would be normal use-case.

An easy fix for you might be to modify or create your own alternative SpritesFromAtlas method in the class, where you use the origin-preserving constructor for generating the new Sprite instances that are added to the atlas. It's only a handful of lines of code anyway. In either case, be sure to set your origin before you add all the sprites so that value can be set in the constructor call.

I am curious though what your objective is for using the origin - it's not clear from your examples. My limited experience with it is how it's used as the pivot point for rotations and scaling. Just to be sure; if your intent was rather to offset the position of the sprite with relation to its entity, you may want to use the component's localoffset.

Hey, Your answer was very helpful, although I figured it out for myself after some time. My code looks now as follows:

public void CreateLightning()
{
        //Create Entity
        Lightning = GameScene.Instance.CreateEntity("Lightning");

        //Add Components
        SpriteAnimator animator = Lightning.AddComponent<SpriteAnimator>();

        //Set Components
        var texture = GameScene.Instance.Content.LoadTexture(Content.Weather.Storm.LightningStrike);
    var sprites = Sprite.SpritesFromAtlas(texture, 16, 32);

        //Set Origin
        foreach (var sprite in sprites)
            sprite.Origin = new Vector2(sprite.SourceRect.Width / 2, sprite.SourceRect.Height);
        animator.SetOriginNormalized(new Vector2(.5f, 1));

    animator.LayerDepth = 0;
        animator.SetMaterial(new Material(BlendState.Additive));

    //Set Animations
    animator.AddAnimation("Strike", sprites.ToArray(), 30f);

    //Make Impact
    Lightning.Position = chunkTiles.RandomItem().Entity.Position;
        Lightning.Scale = new Vector2(10,10);

    animator.Play("Strike", LoopMode.Once);

    animator.OnAnimationCompletedEvent += LightningCompleted;
}

You asked me what the purpose or the function of this was. I have a world build up of little tiles within a chunk (just like Minecraft in 2d top down). The Weather system of my game has different states, one of the is “Storm”, witch crate's lightning strikes every so often. When this happens, the system gets a random tile from a chunk the player is in or near. I wanted it so that the pivot is on the button middle so that I can position the lightning strike end on the place where it is striking the chosen tile. (The lightning strike is animated)

I hope this explanation has helped you understand my problem.

Thank you for your help.