Prefab Instantiation Issue or Perhaps iTween?

HI all,

Short time reader, first time poster here. Great resources. Unity and the Unity community continue to impress. Now, on to the madness…

I’ve run into an issue with a proof of concept (which in fairness, is still just a learning process) for a classic memory card matching game. I’ve stripped away everything that’s not relevant into an isolated scene in the hopes that the problem would become more evident, but the only two conclusions I’ve come to I’m not sure how to resolve.

The isolated concept:

Four cards are in a grid, the top two match one another and the bottom two match one another.

Game play sequence of events:

Clicking the first card reveals it’s face and remains visible until a second card is revealed

Clicking the second card reveals its face then performs a check to see if they were a match.

If match, they remain visible, else they hide themselves.

The problem is with the animations. If you click subsequent cards at certain speeds they get “interrupted” and throw off the correct state. My first thought was that I’m using iTween for animation and that I’m possibly not handling those tweens properly, but the more I experiment with it, the less I feel that’s the case; though the way I’m recursively calling iTweens to create a “sequence” may also be my issue. Maybe @pixelplacement will have a thought. Also, if I’m instantiating new prefabs into my grid how can calling the animation method in one instance cause issues in another? So confused as to where I’ve lost track here. I’ll list the scripts in code here, but I’m also going to add a link to a webGL version and the assets of the game below in case nothing stands out in the code and someone feels like really digging in.

Any advice, comments, criticism, etc. are appreciated!

WebGL: http://thisguy.forgot.his.name/isolation/

Assets zipped: http://thisguy.forgot.his.name/isolation/Assets.zip

Scripts:

// Game.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class Game: MonoBehaviour {
    private Button restart;
    public List<GameObject> Target { get; set; }
    public List<int> Match { get; set; }
    void Awake() {
        restart = GameObject.Find ("Restart").GetComponent<Button> ();
        restart.onClick.AddListener (Restart);
        ResetMatches ();
    }
    private void Restart() {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
    private void ResetMatches() {
        Target = new List<GameObject> ();
        Match = new List<int> ();
    }
    public void CheckMatch() {
        if (!IsMatch ()) {
            Target [0].GetComponent<Card> ().FlipCard ();
            Target [1].GetComponent<Card> ().FlipCard ();
        }
        ResetMatches ();
    }
    private bool IsMatch() {
        return Match [0] == Match [1] && Target[0] != Target[1];
    }
}
// Grid.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Grid: MonoBehaviour {
    public GameObject cardPrefab;
    private Transform grid;
    void Awake () {
        grid = gameObject.transform;
    }
    void Start() {
        Generate ();
    }
    private void CreateCard (int i, int matchId, string text) {
        GameObject prefab;
        prefab = (GameObject)Instantiate (cardPrefab);
        prefab.transform.SetParent (grid);
        prefab.GetComponent<RectTransform> ().localScale = Vector3.one;
        prefab.name = "card-" + i.ToString ();
        prefab.GetComponent<Card> ().MatchId = matchId;
        prefab.GetComponentInChildren<Text> ().text = text;
    }
    private void Generate () {
        CreateCard (0, 0, "a");
        CreateCard (1, 0, "A");
        CreateCard (2, 1, "b");
        CreateCard (3, 1, "B");
    }
}
// AnimationSequence.cs
using System;
using UnityEngine;
public struct AnimationSequence {
    public AnimationSequence (string onUpdateHandler, Vector3 origin, Vector3 destination, float duration, string easeType, float delay) {
        OnUpdateHandler = onUpdateHandler;
        Origin = origin;
        Destination = destination;
        Duration = duration;
        EaseType = easeType;
        Delay = delay;
    }
    public string OnUpdateHandler { get; set; }
    public Vector3 Origin { get; set; }
    public Vector3 Destination { get; set; }
    public float Duration { get; set; }
    public string EaseType { get; set; }
    public float Delay { get; set; }
}
// Card.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Card: MonoBehaviour {
    private Game game;
    private Button button;
    private Transform front;
    private Transform back;
    private Dictionary<string, AnimationSequence[]> animationSequences;
    private float sequenceDuration = 1f;
    private float pairShownDuration = 0.75f;
    private string currentSequence = "";
    private string lastSequence = "";
    private int currentSequenceIndex = -1;
    private bool IsFlipped { get; set; }
    public int MatchId { get; set; }
    void Awake () {
        game = GameObject.FindObjectOfType<Game> ();
        front = gameObject.transform.FindChild ("Front");
        back = gameObject.transform.FindChild ("Back");
        button = GetComponent<Button> ();
        button.onClick.AddListener (OnClick);
        iTween.Init (gameObject);
        CreateSequences ();
    }
    public void FlipCard (bool reveal = false) {
        string sequence = (reveal) ? "reveal" : "hide";
        HandleSequence (sequence);
    }
    private void OnClick () {
        // disallow clicks on flipped cards
        if (!IsFlipped) {
            IsFlipped = true;
            FlipCard (true);
            game.Match.Add (MatchId);
            game.Target.Add (gameObject);
        }
    }
    private void CreateSequences () {
        animationSequences = new Dictionary<string, AnimationSequence[]> ();
        // reveal
        AnimationSequence[] reveal = new AnimationSequence[2];
        reveal [0] = new AnimationSequence ("HandleBack", Vector3.zero, new Vector3 (0, -90, 0), sequenceDuration / reveal.Length, "easeInBack", 0f);
        reveal [1] = new AnimationSequence ("HandleFront", new Vector3 (0, 90, 0), Vector3.zero, sequenceDuration / reveal.Length, "easeOutBack", 0f);
        // hide 
        AnimationSequence[] hide = new AnimationSequence[2];
        hide [0] = new AnimationSequence ("HandleFront", Vector3.zero, new Vector3 (0, 90, 0), sequenceDuration / hide.Length, "easeInBack", pairShownDuration);
        hide [1] = new AnimationSequence ("HandleBack", new Vector3 (0, -90, 0), Vector3.zero, sequenceDuration / hide.Length, "easeOutBack", 0f);
        animationSequences.Add ("reveal", reveal);
        animationSequences.Add ("hide", hide);
    }
    private void HandleSequence (string sequence) {
        currentSequence = sequence;
        RunSequence ();
    }
    private void RunSequence () {
        // if the current sequence was cleared below (at end of sequence)
        // check last sequence for "complete" events and return
        if (currentSequence == "") {
            if (lastSequence == "reveal" && game.Match.Count > 1) {
                game.CheckMatch ();
            }
            if (lastSequence == "hide") {
                IsFlipped = false;
            }
            lastSequence = "";
            return;
        }
        currentSequenceIndex++;
        string onUpdateHandler = animationSequences [currentSequence] [currentSequenceIndex].OnUpdateHandler;
        Vector3 origin = animationSequences [currentSequence] [currentSequenceIndex].Origin;
        Vector3 destination = animationSequences [currentSequence] [currentSequenceIndex].Destination;
        float duration = animationSequences [currentSequence] [currentSequenceIndex].Duration;
        string easeType = animationSequences [currentSequence] [currentSequenceIndex].EaseType;
        float delay = animationSequences [currentSequence] [currentSequenceIndex].Delay;
        Hashtable hashParams = iTween.Hash ("from", origin, "to", destination, "time", duration, "onupdate", onUpdateHandler, "onupdatetarget", gameObject, "easetype", easeType, "oncomplete", "RunSequence", "delay", delay);
        iTween.ValueTo (gameObject, hashParams);
        // the sequence is at end of its tweens, clear sequence
        // for the next recursive call of this method
        if (currentSequenceIndex == animationSequences [currentSequence].Length - 1) {
            lastSequence = currentSequence;
            currentSequence = "";
            currentSequenceIndex = -1;
        }
    }
    private void HandleBack(Vector3 rotation) {
        back.localEulerAngles = rotation;
    }
    private void HandleFront(Vector3 rotation) {
        front.localEulerAngles = rotation;
    }
}

I was able to deduce my issue and wanted to share in case others run into a similar issue. Or perhaps someone will find it useful that just runs across this trying to find out how to flip some cards.

I guess my issue was in regard to “instantiation”. What I did to resolve was to abstract the “sequencer” aspects out of my card and into its own “sequencer” game object with it’s on script component. Initially, I was instantiating the new game object and then subsequently trying to find it when i needed to reference it next. THAT was a huge red flag. Anytime the script did a GameObject.Find() for that game object it would lock up Unity entirely and I had to force quit. Eventually, it became apparent (at least this is my best understanding) that I needed to leave the current objects in tact and create new ones that wouldn’t interrupt the current values.

There may still be some concerns, but I feel good about what I’ve got now. With a little more abstraction in the Sequencer script I may be able to turn it into a “timeline” script for building tweens. I wonder what @pixelplacement would think of this? If anyone has any suggestions of better methodologies, comments, questions, etc. feel free. Again, I’ll place the scripts here, but I’ll also updated the WebGL demo (and I’ll leave it up indefinitely) and a new link for the “fixed” assets.

WebGL Demo: http://thisguy.forgot.his.name/isolation/

Assets Fixed: http://thisguy.forgot.his.name/isolation/AssetsFixed.zip

Scripts:

// Game.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class Game: MonoBehaviour {
    private Button restart;
    public List<GameObject> Target { get; set; }
    public List<int> Match { get; set; }
​    public int CardsFlipped { get; set; }
    void Awake() {
        restart = GameObject.Find ("Restart").GetComponent<Button> ();
        restart.onClick.AddListener (Restart);
        ResetMatches ();
    }
    private void Restart() {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
    private void ResetMatches() {
        Target = new List<GameObject> ();
        Match = new List<int> ();
    }
    public void CheckMatch() {
        if (!IsMatch ()) {
            Target [0].GetComponent<Card> ().FlipCard ();
            Target [1].GetComponent<Card> ().FlipCard ();
        }
        ResetMatches ();
    }
    private bool IsMatch() {
        return Match [0] == Match [1] && Target[0] != Target[1];
    }
}
// Grid.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Grid: MonoBehaviour {
    public GameObject cardPrefab;
    private Transform grid;
    void Awake () {
        grid = gameObject.transform;
    }
    void Start() {
        Generate ();
    }
    private void CreateCard (int i, int matchId, string text) {
        GameObject prefab;
        prefab = (GameObject)Instantiate (cardPrefab);
        prefab.transform.SetParent (grid);
        prefab.GetComponent<RectTransform> ().localScale = Vector3.one;
        prefab.name = "card-" + i.ToString ();
        prefab.GetComponent<Card> ().MatchId = matchId;
        prefab.GetComponentInChildren<Text> ().text = text;
    }
    private void Generate () {
        CreateCard (0, 0, "a");
        CreateCard (1, 0, "A");
        CreateCard (2, 1, "b");
        CreateCard (3, 1, "B");
    }
}
// AnimationSequence.cs
using System;
using UnityEngine;
public struct AnimationSequence {
    public AnimationSequence (string onUpdateHandler, Vector3 origin, Vector3 destination, float duration, string easeType, float delay) {
        OnUpdateHandler = onUpdateHandler;
        Origin = origin;
        Destination = destination;
        Duration = duration;
        EaseType = easeType;
        Delay = delay;
    }
    public string OnUpdateHandler { get; set; }
    public Vector3 Origin { get; set; }
    public Vector3 Destination { get; set; }
    public float Duration { get; set; }
    public string EaseType { get; set; }
    public float Delay { get; set; }
}
// Card.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Card: MonoBehaviour {
    private Game game;
    private Button button;
    private Transform front;
    private Transform back;
    public bool IsFlipped { get; set; }
    public int MatchId { get; set; }
    void Awake () {
        game = GameObject.FindObjectOfType<Game> ();
        front = gameObject.transform.FindChild ("Front");
        back = gameObject.transform.FindChild ("Back");
        button = GetComponent<Button> ();
        button.onClick.AddListener (OnClick);
        iTween.Init (gameObject);
    }
    private GameObject GetSequencer(string sequence) {
        Transform[] transforms = new Transform[2];
        transforms [0] = back;
        transforms [1] = front;
        GameObject sequencer = new GameObject ("sequencer-" + gameObject.name);
        sequencer.AddComponent<Sequencer> ();
        sequencer.GetComponent<Sequencer> ().Init (gameObject, transforms, sequence);
        return sequencer;
    }
    public void FlipCard (bool reveal = false) {
        string sequence = (reveal) ? "reveal" : "hide";
        GameObject sequencer = GetSequencer (sequence);
        sequencer.GetComponent<Sequencer> ().Play ();
    }
    private void OnClick () {
        // disallow clicks on flipped cards
        if (!IsFlipped && game.CardsFlipped < 2) {
            ​game.CardsFlipped++;
            IsFlipped = true;
            FlipCard (true);
            game.Match.Add (MatchId);
            game.Target.Add (gameObject);
        }
    }
}
// Sequencer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Sequencer: MonoBehaviour {
    private Game game;
    private GameObject Target { get; set; }
    private Transform[] Transforms { get; set; }
    private string SequenceName { get; set; }
    private AnimationSequence[] sequence;
    private float sequenceDuration = 1f;
    private float pairShownDuration = 0.75f;
    private string lastSequence;
    private int currentSequenceIndex = -1;
    public void Init (GameObject target, Transform[] transforms, string sequenceName) {
        game = GameObject.FindObjectOfType<Game> ();
        Target = target;
        Transforms = transforms;
        SequenceName = sequenceName;
        CreateSequence ();
    }
    public void Play() {
        HandleSequence ();
    }
    private void CreateSequence () {
        int sequenceLength = 2;
        sequence = new AnimationSequence[sequenceLength];
        switch (SequenceName) {
            case "reveal":
                sequence [0] = new AnimationSequence ("HandleBack", Vector3.zero, new Vector3 (0, -90, 0), sequenceDuration / sequenceLength, "easeInBack", 0f);
                sequence [1] = new AnimationSequence ("HandleFront", new Vector3 (0, 90, 0), Vector3.zero, sequenceDuration / sequenceLength, "easeOutBack", 0f);
            break;
            case "hide":
                sequence [0] = new AnimationSequence ("HandleFront", Vector3.zero, new Vector3 (0, 90, 0), sequenceDuration / sequenceLength, "easeInBack", pairShownDuration);
                sequence [1] = new AnimationSequence ("HandleBack", new Vector3 (0, -90, 0), Vector3.zero, sequenceDuration / sequenceLength, "easeOutBack", 0f);
            break;
        }
    }
    private void SequenceComplete(string sequenceName) {
        if (sequenceName == "reveal" && game.Match.Count > 1) {
            game.CheckMatch ();
        }
        if (sequenceName == "hide") {
            Target.GetComponent<Card> ().IsFlipped = false;
            ​game.CardsFlipped--;
        }
        lastSequence = "";
        // destroy the sequencer game object; initially tried keeping it and then finding the relevant
        // sequencer in the card after first creating one, but I think THAT is the issue I had before;
        // completely new instances of sequencers are needed to not corrupt previous values
        Destroy (gameObject);
    }
    private void HandleSequence () {
        // if the current sequence was cleared below (at end of sequence)
        // check last sequence for "complete" events and return
        if (SequenceName == "") {
            SequenceComplete (lastSequence);
            return;
        }
        currentSequenceIndex++;
        string onUpdateHandler = sequence [currentSequenceIndex].OnUpdateHandler;
        Vector3 origin = sequence [currentSequenceIndex].Origin;
        Vector3 destination = sequence [currentSequenceIndex].Destination;
        float duration = sequence [currentSequenceIndex].Duration;
        string easeType = sequence [currentSequenceIndex].EaseType;
        float delay = sequence [currentSequenceIndex].Delay;
        Hashtable hashParams = iTween.Hash ("from", origin, "to", destination, "time", duration, "onupdate", onUpdateHandler, "onupdatetarget", gameObject, "easetype", easeType, "oncomplete", "HandleSequence", "oncompletetarget", gameObject, "delay", delay);
        iTween.ValueTo (Target.gameObject, hashParams);
        // the sequence is at end of its tweens, clear sequence
        // for the next recursive call of this method
        if (currentSequenceIndex == sequence.Length - 1) {
            lastSequence = SequenceName;
            SequenceName = "";
            currentSequenceIndex = -1;
        }
    }
    private void HandleBack(Vector3 rotation) {
        Transforms[0].localEulerAngles = rotation;
    }
    private void HandleFront(Vector3 rotation) {
        Transforms[1].localEulerAngles = rotation;
    }
}