Forum rules - please read before posting.

Modifying sprite alpha on Click Hold for a special puzzle/minigame.

edited November 2023 in Technical Q&A

Hello, it's me again with a somewhat complex minigame to figure out and implement,

This puzzle was previously working on a different version of Unity and lifetime of the project, it was achieved by some custom scripts that used Yarn Spinner in a complicated manner... but the programmers that made it are no longer communicating with me, so I am jumping to try and make it work on AC. maybe even trying not to use any custom scripts of old but only AC system stuff, which will be really flexible to me since I am not a coder.

To the puzzle:

Basically at a point in the game you pick up an umbrella Item and, when examined in the Inventory, it opens up a Closeup of it where the player has to use the mouse cursor (Left Click) to clean the inside ashes (just a Sprite on a different Layer) off of it and reveal a small etiquette (Hotspot marked in red circle)

Once you examine the etiquette, the puzzle is completed.

I´m thinking that maybe the_ mouse cursor could be used to modify the Sprite alpha of the ashes sprite stuff to slowly "fade it out" as if you were cleaning it, but how can this be achieved?

Also, how can AC activate the etiquette hotspot only once the player has clean exactly on that area?

Here's an old GIF that serves as a reference for what I need. Although in that GIF the cleaning area is too big for the mouse cursor, I would like it to be smaller as well as lowering the sensitivity of mouse on-hold.

Hope it could be achieved somehow.

Thanks in advance!

Comments

  • edited November 2023

    It'll still need a custom script as the gameplay is very specific, but it should be fairly straightforward - provided you can forego the fading effect and have it be similar to the GIF.

    Essentially, you can use Unity's Sprite Masks feature to remove portions of the ash sprite.

    Make the ash all one sprite, and then set its Mask Interaction to Visible Outside Mask. Then, add a new circle sprite into the scene (whose size should match the "cleaning area". Attach to it the Sprite Mask component, and you should find that this removes ash of the same size when placed over it.

    The next step would involve a script that creates more such masks automatically when the mouse is moved.

    For the Hotspot becoming activated only when clean, you could have the script look out for this - turning it on manually if the mouse is close enough. Let's sort out the cleaning effect first, though.

    Have a go with the above, and if it's on the right track, we can look into the scripting side of things.

  • edited November 2023

    Hey Chris! Thanks for jumping in to help me with this.

    I can see where you are taking me with this Sprite Mask approach, already made some progress following your instructions:

    Waiting for your instructions on the scripting side of things. B) :)

    Just to recap:

    • The ashes should clean only when the player is holding the mouse button down.

    • I would also like to diminish the mouse sensitivity a bit on-hold (compared to normal in-game), just to make the cleaning process a little bit more thorough and the puzzle a little bit "slower" and involved

    • The etiquette hotspot should enable itself only after that portion has been cleaned, and the etiquette sprite can be seen, so the player can interact with it and allow me to Action- List it to finish the minigame off.

    Thanks again! You are awesome!

  • This should handle the ashes and Hotspot. Attach to an object in the scene and fill in its Inspector:

    using UnityEngine;
    using AC;
    
    public class DrawMask : MonoBehaviour
    {
    
        public Hotspot hotspot;
        public GameObject maskToCopy;
        public float zDepth = 10f;
        public float minDistance = 0.5f;
        private Vector2 lastSpawnPosition;
    
        void Update ()
        {
            if (KickStarter.playerInput.GetMouseState () == MouseState.HeldDown)
            {
                Vector2 screenPosition = KickStarter.playerInput.GetMousePosition ();
                Vector3 worldPosition = KickStarter.CameraMain.ScreenToWorldPoint (new Vector3 (screenPosition.x, screenPosition.y, zDepth));
    
                if (Vector3.Distance (lastSpawnPosition, worldPosition) > minDistance)
                {
                    Instantiate (maskToCopy).transform.position = worldPosition;
                    lastSpawnPosition = worldPosition;
    
                    if (hotspot && Vector3.Distance (hotspot.GetIconPosition (), worldPosition) <= minDistance)
                    {
                        hotspot.TurnOn ();
                    }
                }
            }
        }
    }
    

    I would also like to diminish the mouse sensitivity a bit on-hold (compared to normal in-game), just to make the cleaning process a little bit more thorough and the puzzle a little bit "slower" and involved

    That's a bit more tricky, as Mouse And Keyboard input (which I'm assuming you're using) reads the raw mouse position - not the change in position since the previous frame.

    What you'd need to do is switch over to Keyboard Or Controller input (even if only temporarily), which uses the CursorHorizontal / CursorVertical input values to offset the cursor each frame.

    Try the script above first though and let's deal with it last.

  • edited November 2023

    Thanks Chris, so far so good, managed to get the cleaning part working:

    I don't think the mouse sensitivity part is gonna be necessary, I find that altering the Sprite mask shape to a small circle kinda serves the same purpose.

    Now to the Hotspot part... how can it activate itself when that small zone is cleaned. I populated the Inspector and put the Etiquette Hotspot there, but it still remains active. Should I use Remember Hotspot? What are the Z Depth and Min Distance used for?

    On a side note:

    I found that when cleaning, it creates several instances of a Sprite Mask clone every time you Click, with time, this instances add up and make the engine slow down a bit, this is not a good way to approach from an optimization perspective I believe, could the script also kill this old instances so to keep the puzzle efficient?

    Thanks again, this is looking great so far!

  • I populated the Inspector and put the Etiquette Hotspot there, but it still remains active.

    The script only turns on the Hotspot when you click close enough - but it won't make the Hotspot off to start with. To do this, attach Remember Hotspot and set its State on start to Off.

    this instances add up and make the engine slow down a bit, this is not a good way to approach from an optimization perspective I believe

    The approach measures the distance between the last placement and the current mouse position, and places down a new mask once this distance exceeds "Min Distance". This'll be tied to the radius of your circle sprite (in world-space coordinates), so performance will be tied to the size of your circle.

    Personally, I'd increase the radius to increase the performance. You could remove or pool "old" instances, but this'd cause the ash to be shown again in their place.

    This technique isn't involving AC - it's just the one I know. There may be other approaches out there, but I'd expect they'd involve a lot more scripting / use of a dedicated asset.

  • edited November 2023

    Hi Chris!

    I attached Remember Hotspot, but the Hotspot won't activate itself once the label/etiquette is fully visible. Am I doing something wrong? Should I mess with the Z position of the Hotspot in regards to the Ashes Sprite?

    Regarding efficiency, I made the Circle Mask bigger, and it seems a bit better now. Hope older machines would be able to cope. I noticed something weird tough, if you clean inside an already "cleaned" area, it still creates a Sprite Mask Clone...

    I realize now that there's more to the puzzle I need to make it feel complete, for example:

    Could there be a way to:

    • Play a "cleaning" SFX while the player keeps the mouse Held down and working on the cleaning itself? I assume it will involve some added line on the Script but don´t know what.

    • Save the state of the puzzle automatically if/when the player leaves the CloseUp? The CloseUp can be accessed from every Scene on the game from the inventory, so it would be realistic if it "saved" the player´s cleaning progress, I suspect saving such Sprite Masks is the trick here...

    As always, thanks for the awesome aid provided...

  • This variant provides a dedicated "Distance to Hotspot" field that affects how close you need to click for the Hotspot to turn on, as well as a way to avoid repeated spawns:

    using UnityEngine;
    using System.Collections.Generic;
    using AC;
    
    public class DrawMask : MonoBehaviour
    {
    
        public Hotspot hotspot;
        public GameObject maskToCopy;
        public float zDepth = 10f;
        public float distanceToHotspot = 0.5f;
        private List<Vector3> spawnedGridPositions = new List<Vector3> ();
        private List<Vector3> spawnedPositions = new List<Vector3> ();
        public float gridSize = 0.5f;
    
        void Update ()
        {
            if (KickStarter.playerInput.GetMouseState () == MouseState.HeldDown)
            {
                Vector2 screenPosition = KickStarter.playerInput.GetMousePosition ();
                Vector3 worldPosition = KickStarter.CameraMain.ScreenToWorldPoint (new Vector3 (screenPosition.x, screenPosition.y, zDepth));
    
                Vector3 gridPosition = GetGridPosition (worldPosition);
                if (spawnedGridPositions.Contains (gridPosition)) return;
    
                Instantiate (maskToCopy).transform.position = worldPosition;
                spawnedGridPositions.Add (gridPosition);
                spawnedPositions.Add (worldPosition);
    
                if (hotspot && Vector2.Distance (hotspot.GetIconPosition (), worldPosition) <= distanceToHotspot)
                {
                    hotspot.TurnOn ();
                }
            }
        }
    
        private Vector3 GetGridPosition (Vector3 position)
        {
            return new Vector3
            (
                gridSize * Mathf.Round(position.x / gridSize),
                gridSize * Mathf.Round(position.y / gridSize),
                position.z
            );
        }
    
    }
    

    This replaces the Min Distance field with Grid Size - you'll need to play around with it.

    Play a "cleaning" SFX while the player keeps the mouse Held down and working on the cleaning itself?

    How would that work, exactly? I'd imagine a sound that's played when you create a new mask, and stops playing if you don't create a new mask within a set time.

    Save the state of the puzzle automatically if/when the player leaves the CloseUp?

    Yes, but let's get the rest sorted first as it may affect the way that's done.

  • Thanks, Chris! This new script variant seems to work like a charm. It even is more efficient, at least on my machine.

    I'd imagine a sound that's played when you create a new mask, and stops playing if you don't create a new mask within a set time.

    Yes, this could work, it's just a simple and short sound effect that accompanies the player's cleaning process.

    Yes, but let's get the rest sorted first as it may affect the way that's done.

    OK, ready when you are!

    :)

  • edited November 2023
    using UnityEngine;
    using System.Collections.Generic;
    using AC;
    
    public class DrawMask : MonoBehaviour
    {
    
        public AudioSource _audioSource;
        public Hotspot hotspot;
        public GameObject maskToCopy;
        public float zDepth = 10f;
        public float minDistance = 0.5f;
        private List<Vector3> spawnedGridPositions = new List<Vector3> ();
        private List<Vector3> spawnedPositions = new List<Vector3> ();
        private List<GameObject> spawnedObjects = new List<GameObject> ();
        public float gridSize = 0.05f;
    
        void Update ()
        {
            if (KickStarter.playerInput.GetMouseState () == MouseState.HeldDown)
            {
                Vector2 screenPosition = KickStarter.playerInput.GetMousePosition ();
                Vector3 worldPosition = KickStarter.CameraMain.ScreenToWorldPoint (new Vector3 (screenPosition.x, screenPosition.y, zDepth));
    
                Vector3 gridPosition = GetGridPosition (worldPosition);
                if (spawnedGridPositions.Contains (gridPosition)) return;
    
                GameObject newMask = Instantiate (maskToCopy);
                newMask.transform.position = worldPosition;
                spawnedGridPositions.Add (gridPosition);
                spawnedPositions.Add (worldPosition);
                spawnedObjects.Add (newMask);
                if (!_audioSource.isPlaying) _audioSource.Play ();
    
                if (hotspot && Vector2.Distance (hotspot.GetIconPosition (), worldPosition) <= minDistance * 2f)
                {
                    hotspot.TurnOn ();
                }
            }
        }
    
        private Vector3 GetGridPosition (Vector3 position)
        {
            return new Vector3
            (
                gridSize * Mathf.Round(position.x / gridSize),
                gridSize * Mathf.Round(position.y / gridSize),
                position.z
            );
        }
    
    
        public MaskData Save (MaskData data)
        {
            MaskData.MyVector3[] _spawnedPositions = new MaskData.MyVector3[spawnedPositions.Count];
            for (int i = 0; i < _spawnedPositions.Length; i++)
            {
                _spawnedPositions[i] = new MaskData.MyVector3 (spawnedPositions[i]);
            }
            data.spawnedPositions = _spawnedPositions;
    
            MaskData.MyVector3[] _spawnedGridPositions = new MaskData.MyVector3[spawnedGridPositions.Count];
            for (int i = 0; i < _spawnedGridPositions.Length; i++)
            {
                _spawnedGridPositions[i] = new MaskData.MyVector3 (spawnedGridPositions[i]);
            }
            data.spawnedGridPositions = _spawnedGridPositions;
    
            return data;
        }
    
    
        public void Load (MaskData data)
        {
            spawnedPositions.Clear ();
            spawnedGridPositions.Clear ();
    
            while (spawnedObjects.Count > 0)
            {
                if (spawnedObjects[0]) Destroy (spawnedObjects[0]);
                spawnedObjects.RemoveAt (0);
            }
            spawnedObjects.Clear ();
    
            for (int i = 0; i < data.spawnedPositions.Length; i++)
            {
                GameObject newMask = Instantiate (maskToCopy);
                newMask.transform.position = data.spawnedPositions[i].ToVector3 ();
                spawnedPositions.Add (newMask.transform.position);
                spawnedObjects.Add (newMask);
            }
    
            for (int i = 0; i < data.spawnedGridPositions.Length; i++)
            {
                spawnedGridPositions.Add (data.spawnedGridPositions[0].ToVector3 ());
            }
        }
    
    }
    

    And:

    using UnityEngine;
    using AC;
    
    public class RememberMask : Remember
    {
    
        public override string SaveData ()
        {
            MaskData data = new MaskData();
            data.objectID = constantID;
            data.savePrevented = savePrevented;
    
            data = GetComponent<DrawMask> ().Save (data);
    
            return Serializer.SaveScriptData <MaskData> (data);
        }
    
    
        public override void LoadData (string stringData)
        {
            MaskData data = Serializer.LoadScriptData <MaskData> (stringData);
            if (data == null) return;
            SavePrevented = data.savePrevented; if (savePrevented) return;
    
            GetComponent<DrawMask> ().Load (data);
        }
    
    }
    
    
    [System.Serializable]
    public class MaskData : RememberData
    {
    
        public MyVector3[] spawnedPositions;
        public MyVector3[] spawnedGridPositions;
    
        public MaskData () { }
    
    
        [System.Serializable]
        public class MyVector3
        {
            public float x, y, z;
    
            public MyVector3 (Vector3 vector)
            {
                x = vector.x;
                y = vector.y;
                z = vector.z;
            }
    
            public Vector3 ToVector3 ()
            {
                return new Vector3 (x, y, z);
            }
        }
    
    }
    
  • I created both scripts but I am getting some console errors when compiling:

    Assets\Noir Storm\Scripts\DrawMask.cs(28,34): error CS0029: Cannot implicitly convert type 'UnityEngine.Vector3' to 'UnityEngine.GameObject'
    
    Assets\Noir Storm\Scripts\DrawMask.cs(72,34): error CS0029: Cannot implicitly convert type 'UnityEngine.Vector3' to 'UnityEngine.GameObject'
    
    Assets\Noir Storm\Scripts\DrawMask.cs(52,21): error CS0161: 'DrawMask.Save(MaskData)': not all code paths return a value
    
  • I've updated the DrawMask script above - try it now.

  • I have both scripts working now without compile errors. But some new problems have arise:

    -No sound is played when clicking, even tough I have assigned the AudioSource correctly on the inspector:

    -I can´ quite check that the script saves the state cause I cannot switch to the previous scene, it´s giving me another console error:

    **Here´s the full console error: **

    SerializationException: Type 'UnityEngine.Vector3' in Assembly 'UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
    System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers (System.RuntimeType type) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.FormatterServices+<>c__DisplayClass9_0.b__0 (System.Runtime.Serialization.MemberHolder _) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Collections.Concurrent.ConcurrentDictionary2[TKey,TValue].GetOrAdd (TKey key, System.Func2[T,TResult] valueFactory) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.FormatterServices.GetSerializableMembers (System.Type type, System.Runtime.Serialization.StreamingContext context) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo () (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize (System.Type objectType, System.Runtime.Serialization.ISurrogateSelector surrogateSelector, System.Runtime.Serialization.StreamingContext context, System.Runtime.Serialization.Formatters.Binary.SerObjectInfoInit serObjectInfoInit, System.Runtime.Serialization.IFormatterConverter converter, System.Runtime.Serialization.SerializationBinder binder) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize (System.Type objectType, System.Runtime.Serialization.ISurrogateSelector surrogateSelector, System.Runtime.Serialization.StreamingContext context, System.Runtime.Serialization.Formatters.Binary.SerObjectInfoInit serObjectInfoInit, System.Runtime.Serialization.IFormatterConverter converter, System.Runtime.Serialization.SerializationBinder binder) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArray (System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo objectInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo memberNameInfo, System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo memberObjectInfo) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write (System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo objectInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo memberNameInfo, System.Runtime.Serialization.Formatters.Binary.NameInfo typeNameInfo) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize (System.Object graph, System.Runtime.Remoting.Messaging.Header[] inHeaders, System.Runtime.Serialization.Formatters.Binary.__BinaryWriter serWriter, System.Boolean fCheck) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph, System.Runtime.Remoting.Messaging.Header[] headers, System.Boolean fCheck) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph, System.Runtime.Remoting.Messaging.Header[] headers) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize (System.IO.Stream serializationStream, System.Object graph) (at <695d1cc93cca45069c528c15c9fdd749>:0)
    AC.FileFormatHandler_Binary.SerializeObject[T] (System.Object dataObject) (at Assets/AdventureCreator/Scripts/Save system/FileFormat/FileFormatHandler_Binary.cs:37)
    AC.Serializer.SerializeObject[T] (System.Object dataObject, System.Boolean addMethodName, AC.iFileFormatHandler fileFormatHandler) (at Assets/AdventureCreator/Scripts/Save system/Serializer.cs:153)
    AC.Serializer.SaveScriptData[T] (System.Object dataObject) (at Assets/AdventureCreator/Scripts/Save system/Serializer.cs:345)
    RememberMask.SaveData () (at Assets/Noir Storm/Scripts/RememberMask.cs:15)
    AC.LevelStorage.PopulateScriptData (UnityEngine.SceneManagement.Scene scene) (at Assets/AdventureCreator/Scripts/Save system/LevelStorage.cs:780)
    AC.LevelStorage.SaveSceneData (AC.SubScene subScene) (at Assets/AdventureCreator/Scripts/Save system/LevelStorage.cs:452)
    AC.LevelStorage.StoreAllOpenLevelData () (at Assets/AdventureCreator/Scripts/Save system/LevelStorage.cs:333)
    AC.SceneChanger.PrepareSceneForExit (System.Boolean isInstant, System.Boolean saveRoomData, System.Boolean doOverlay) (at Assets/AdventureCreator/Scripts/Game engine/SceneChanger.cs:1170)
    AC.SceneChanger.ChangeScene (System.String nextSceneName, System.Boolean saveRoomData, System.Boolean forceReload, System.Boolean doOverlay) (at Assets/AdventureCreator/Scripts/Game engine/SceneChanger.cs:304)
    AC.ActionSceneSwitchPrevious.Run () (at Assets/AdventureCreator/Scripts/Actions/ActionSceneSwitchPrevious.cs:87)
    AC.ActionList+d__45.MoveNext () (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:460)
    UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at :0)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.ActionList:ProcessAction(Int32) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:410)
    AC.ActionList:ProcessActionEnd(ActionEnd, Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:609)
    AC.ActionList:EndAction(Action) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:568)
    AC.d__45:MoveNext() (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:532)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    AC.ActionList:ProcessAction(Int32) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:410)
    AC.ActionList:ProcessActionEnd(ActionEnd, Int32, Boolean) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:609)
    AC.ActionList:EndAction(Action) (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:568)
    AC.d__45:MoveNext() (at Assets/AdventureCreator/Scripts/ActionList/ActionList.cs:532)
    UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)

  • Try the updated above once more.

  • Now the errors are gone and the sound works! Thanks.

    Still not saving the sprite mask postion tough, I am coming from the previous scene using a different Player Start, that seems to work fine.

    Not sure how exactly the Remember Mask should be used, should I place it as as a script component onthe Sprite Mask GameObject or...?

  • Attach a single instance of it to the DrawMask component.

  • Everything seems to work fine now! :) The Maks are saved if you exit the CloseUp...

    There´s only one little detail that is bothering me, if the player succesfully cleans and uncovers the label, I realized that if you leave the CloseUp and come back, the label hotspot remains inactive, even though the label is right there fully visible. You have to "clean" again in the already cleaned area to "enable" it again. Which is weird.

    Just a small detail.

  • Your Hotspot still has a "Remember Hotspot" component on it?

    Does it specifically mark itself as "Off" in its Inspector upon returning?

  • edited December 2023

    Nevermind, I realized I was making a mistake on the OnStart cutscene that purposefully would turn it OFF.

    Have to say huge thanks! This turned out better than expected. You are awesome!

  • You're welcome, Asset Store reviews are always appreciated.

  • You're welcome, Asset Store reviews are always appreciated.

    Of course.
    Done!

Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.