Animator/Sprite/Shop script idea question

As of late, I have an app (which is going to be released to the google play store soon), that is pretty much ready to go. However in the future I would like to add in a shop to buy several different “skins” for the character. This is a 2D game and uses a basic animator which basically transitions between three different sprite images based on taps. My question is, what would be the most optimal way of going about making the skins? Obviously I would want the sprites to change dependent on the skins the user selects, but wouldn’t that mean i would also have to either switch out the animator for the character object as well? Or would it be better to make prefabs for each skin/player and switch it out this way?

I’m interesting too.
I guess, the most important thing here, is to not include all skins into the apk, and download them only after the purchase.

1 Like

Very true! Thank you for the reply. Which at the moment I’m using PlayerPrefs for now (just for a simple highscore/volume system), but when I start adding in the shop and such I planned to use A binary formatter or something much more secure. Which is why I’m asking the question in the first place, because based on what I need to do with these skins, pretty much determines which saving method I should go about using. (that and I’m truly curious on the best optimal way of going about this is)

bump

I’d separate the Animator from the sprites.

I’d do this by creating a ScriptableObject that is the animation set. And the Animator which accepts this ScriptableObject as a serialized field on itself.

The animator doesn’t care what the sprites are that are animated, just that it receives them as the anim set. And it plays them as necessary.

Give you an example… this is from my current game, it’s 3d, but the idea is almost the same. In it there are various enemies that can “grapple” you (like a zombie grabs the player, holds on, and bites them repeatedly. The player must shake the thumbstick and press buttons to fight free).

Thing is, each enemy may grab you in a different way. Maybe a dog bites the players arm and shakes it about, or the zombie grabs your neck and chews on it, and the boss picks you up by the throat and swings you around.

The thing is… the animation set follows the same basic steps:

grab
hold
bite/attack
release by force (player successfully released)
release by choice (enemy released player due to some condition)
death (this anim technically isn’t part of the set, but could be)

So here’s my anim set (it uses my own custom stuff, but you get the idea… you could write your own custom collections):

using UnityEngine;
using System.Collections.Generic;

using com.spacepuppy;
using com.spacepuppy.Anim;
using com.spacepuppy.Utils;

namespace com.mansion.Anim
{

    [CreateAssetMenu(fileName = "GrappleAnimSet", menuName = "Spacepuppy/GrappleAnimSet")]
    public class GrappleAnimSet : ScriptableObject
    {

        public const string ANIM_GRAB = "Grab";
        public const string ANIM_HELD = "Held";
        public const string ANIM_BIT = "Bit";
        public const string ANIM_RELEASED = "Released";
        public const string ANIM_FORCED_RELEASED = "ForcedReleased";

        #region Fields

        [SerializeField()]
        [SPAnimClipCollection.Config(DefaultLayer = Constants.ANIMLAYER_GRAPPLE)]
        [SPAnimClipCollection.StaticCollection(ANIM_GRAB,
                                       ANIM_HELD,
                                       ANIM_BIT,
                                       ANIM_RELEASED,
                                       ANIM_FORCED_RELEASED)]
        private SPAnimClipCollection _animations;

        [SerializeField]
        private float _proximity = 1f;

        #endregion

        #region Properties

        public SPAnimClip this[string id]
        {
            get { return _animations[id]; }
        }

        public float Proximity
        {
            get { return _proximity; }
            set { _proximity = value; }
        }

        #endregion
      
    }

}

The animator then takes these in:

using UnityEngine;
using System.Collections.Generic;

using com.spacepuppy;
using com.spacepuppy.Anim;
using com.spacepuppy.Collections;
using com.spacepuppy.Scenario;
using com.spacepuppy.Utils;

using com.mansion.Anim;

namespace com.mansion.Entities.Actors.Player
{

    public class PlayerGrappledAnimator : SPAnimator
    {

        public const string ANIM_GRAB = "Grab";
        public const string ANIM_HELD = "Held";
        public const string ANIM_BIT = "Bit";
        public const string ANIM_RELEASED = "Released";
        public const string ANIM_FORCED_RELEASED = "ForcedReleased";
        public const string HASH = "*plgrap";

        public enum GrappleState
        {
            None,
            Grabbing,
            Grappled,
            Releasing
        }

        #region Fields

        [SerializeField()]
        private GrappleAnimSet _defaultAnimations;

        [SerializeField]
        private GrappleAnimSetDictionary _overrideAnimations;
      
        [Header("Event Triggers")]
        [SerializeField()]
        private Trigger _onGrabbed;
        [SerializeField]
        private Trigger _onBit;
        [SerializeField()]
        private Trigger _onReleased;
        [SerializeField()]
        private Trigger _onForcedReleased;

        [System.NonSerialized]
        private GrappleState _state;

        [System.NonSerialized]
        private ISPAnim _heldAnim;

        #endregion

        #region CONSTRUCTOR

        protected override void Init(SPEntity entity, SPAnimationController controller)
        {

        }

        #endregion

        #region Methods

        public override void Play(string id, QueueMode queuMode = QueueMode.PlayNow, PlayMode playMode = PlayMode.StopSameLayer)
        {

        }



        private GrappleAnimSet GetAnimSet(string overrideAnimSet)
        {
            GrappleAnimSet set;
            if (overrideAnimSet != null && _overrideAnimations.TryGetValue(overrideAnimSet, out set) && set != null)
                return set;
            else
                return _defaultAnimations;
        }


        public ISPAnim PlayGrabbed(string overrideAnimSet = null)
        {
            _onGrabbed.ActivateTrigger(this, null);

            GrappleAnimSet set = GetAnimSet(overrideAnimSet);
            if (set == null) return SPAnim.Null;

            var clip1 = set[ANIM_GRAB];
            var clip2 = set[ANIM_HELD];

            if (clip1 == null || clip2 == null) return SPAnim.Null;

            _state = GrappleState.Grabbing;
            clip1.WrapMode = WrapMode.Clamp;
            clip1.Layer = Constants.ANIMLAYER_GRAPPLE;
            var grabAnim = this.Controller.PlayAuxiliary(clip1, QueueMode.PlayNow);
            grabAnim.Schedule((a) =>
            {
                _state = GrappleState.Grappled;
            });

            clip2.WrapMode = WrapMode.Loop;
            clip2.Layer = Constants.ANIMLAYER_GRAPPLE;
            _heldAnim = this.Controller.PlayAuxiliary(clip2, QueueMode.CompleteOthers);

            return grabAnim;
        }

        public ISPAnim PlayBit(string overrideAnimSet = null)
        {
            _onBit.ActivateTrigger(this, null);

            GrappleAnimSet set = GetAnimSet(overrideAnimSet);
            if (set == null) return SPAnim.Null;
          
            var clip = set[ANIM_BIT];
            if (clip == null) return SPAnim.Null;

            clip.WrapMode = WrapMode.Once;
            clip.Layer = Constants.ANIMLAYER_GRAPPLE + 1;
            return this.Controller.CrossFadeAuxiliary(clip, Constants.DEFAULT_CROSSFADE_DUR, QueueMode.PlayNow);
        }

        public ISPAnim PlayRelease(string overrideAnimSet = null)
        {
            _onReleased.ActivateTrigger(this, null);
            if(_heldAnim != null)
            {
                _heldAnim.Stop();
                _heldAnim = null;
            }

            _state = GrappleState.None; //incase we don't play anim

            GrappleAnimSet set = GetAnimSet(overrideAnimSet);
            if (set == null) return SPAnim.Null;
          
            var clip = set[ANIM_RELEASED];
            if (clip == null) return SPAnim.Null;

            _state = GrappleState.Releasing;

            clip.WrapMode = WrapMode.Once;
            clip.Layer = Constants.ANIMLAYER_GRAPPLE;
            var anim = this.Controller.CrossFadeAuxiliary(clip, Constants.DEFAULT_CROSSFADE_DUR, QueueMode.PlayNow);
            anim.Schedule((a) =>
            {
                _state = GrappleState.None;
            });
            return anim;
        }

        public ISPAnim PlayForcedRelease(string overrideAnimSet = null)
        {
            _onForcedReleased.ActivateTrigger(this, null);
            if (_heldAnim != null)
            {
                _heldAnim.Stop();
                _heldAnim = null;
            }

            _state = GrappleState.None; //incase we don't play anim

            GrappleAnimSet set = GetAnimSet(overrideAnimSet);
            if (set == null) return SPAnim.Null;
          
            var clip = set[ANIM_FORCED_RELEASED];
            if (clip == null) return SPAnim.Null;

            _state = GrappleState.Releasing;

            clip.WrapMode = WrapMode.Once;
            clip.Layer = Constants.ANIMLAYER_GRAPPLE;
            var anim = this.Controller.CrossFadeAuxiliary(clip, Constants.DEFAULT_CROSSFADE_DUR, QueueMode.PlayNow);
            anim.Schedule((a) =>
            {
                _state = GrappleState.None;
            });
            return anim;
        }

        #endregion

        #region Special Types

        [System.Serializable]
        private class GrappleAnimSetDictionary : SerializableDictionaryBase<string, GrappleAnimSet>
        {

        }

        #endregion

    }

}

Of course, in this scenario the animations exist as default and overrides.

In yours… you’d just set the default to whatever the downloaded animations were.

Again though… the code is written ambiguously so that it doesn’t care what anims are played… just that a specific set of anims exists.

Though not relevant to 2d animations, if you wanted to see the code behind the various animation classes of mine:

1 Like

First off thank you for a response! I see what you are getting at here, using a scriptable object for this situation could be fairly useful, I really respect you for sharing the code as well. I’ll have to take a deeper look into this and see if i can come up with anything in the future! Now the question is, how does making the skin a downloadable product work with saving data? And how would i make it know how to add it into the base framework?

AssetBundles?

1 Like

Thank you for the help!