/** Gerenciador genérico e principal dos players. * * Versão 2.1 * - Acompanhamento da legenda * Corrigido problema na soletração quando a velocidade ultrapassava ~1. * * Versão 2.2 * - Acompanhamento da legenda * Corrigido problema na soletração quando o estado muda para pausado. * * Versão 2.3 * - Legenda * A letras acentuadas reproduzem a mesma sem o acento. * * Versão 2.4 * - Legenda * "Ç" reproduz "C". * - Reprodução * Quando não há acesso aos bundles dos sinais de pontuação, eles são ignorados. * Ç adicionado como TYPE_WORD. * * Versão 2.5 * - Sincronização * Retira os LOCKERs. * Os LOCKERs não impedem que duas Coroutines entrem na mesma sessão crítica. * A soluação utiliza o estado loading na coroutine LoadAndPlay. * - Código * Retira namespaces não utilizados. * Adiciona logs. */ //Log Dir http://docs.unity3d.com/Manual/LogFiles.html using UnityEngine; using System.Collections; using System.Collections.Generic; using System; using UnityEngine.UI; public abstract class GenericPlayerManager : MonoBehaviour { private const string DEFAULT_ANIMATION = "_default"; private const string DEFAULT_ANIMATION_MIDDLE = "_default_middle"; public float fadeLength = 0.6F; public string gloss = ""; // Referencia para o avatar private GameObject AVATAR; // Referencia para o componente animador do avatar private Animation COMPONENT_ANIMATION; public Text SUBTITLES; // Guarda os nomes das palavras já carregadas private HashSet loadedAssetBundles = new HashSet(); // Guarda os nomes das palavras que não tem assetbundle private HashSet nonexistentAssetBundles = new HashSet(); // Fila de animações para reprodução // Utilizada para alterar velocidade e apresentar a legenda private Queue animQueue = new Queue(); // Sinais de intervalo de animações: não sinaliza reprodução na UI private HashSet intervalAnimations = new HashSet(); // Sinais ignorados na apresentação de legenda private HashSet flags = new HashSet(); // True quando está na função LoadAndPlay private volatile bool loading = false; // True quando está reproduzindo qualquer animação private volatile bool playing = false; // True quando é chamada a função de pausa private volatile bool paused = false; private IEnumerator subtitlesSynchronizer; // Se diferente de null, não está reproduzindo animação de intervalo private AnimationState intervalAnimationState = null; // Usado para pausar quando comandado private AnimationReference animationPlaying = null; // Gerenciador de animações de intervalo public RandomAnimations randomAnimations; // Gerenciados de legendas public Subtitle subtitles = null; private bool[] lastLetterAnimations = new bool[256]; private HashSet allowedCharacters = new HashSet() { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '%', '.', ',', '!', '?', }; public virtual void Start() { // Configuração de velocidade das animações subtitles = new Subtitle(SUBTITLES); subtitles.DefaultWordSpeed = new DefaultSignSpeed(); subtitles.DefaultFirstLetterSpeed = new DefaultSignSpeed(2.1F, 2.8F); subtitles.DefaultLetterSpeed = new DefaultSignSpeed(3F, 4.3F); subtitles.DefaultNumberSpeed = new DefaultSignSpeed(1.5F, 2.9F); subtitlesSynchronizer = SubtitlesSynchronizer(); AVATAR = GameObject.FindGameObjectWithTag("avatar"); COMPONENT_ANIMATION = AVATAR.GetComponent(); // Sinais ignorados na legenda string[] flags = new string[] { "[PONTO]", "[INTERROGAÇÃO]", "[EXCLAMAÇÃO]" }; foreach (string flag in flags) this.flags.Add(flag); string[] preloadedAnims = new string[] { "A", "B", "C", "Ç", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "," }; // Duplica sinais para diferenciar quando há repetidos foreach (string anim in preloadedAnims) COMPONENT_ANIMATION.AddClip(COMPONENT_ANIMATION[anim].clip, "d_" + anim); foreach (string anim in preloadedAnims) this.loadedAssetBundles.Add(anim); // Cria novo _default chamado _default_middle para pausas dentro de uma glosa // Impede que a animação default seja confundida com não-reprodução na UI COMPONENT_ANIMATION.AddClip(COMPONENT_ANIMATION[DEFAULT_ANIMATION].clip, DEFAULT_ANIMATION_MIDDLE); } public bool isPlayingIntervalAnimation() { return intervalAnimationState != null; } public bool isLoading() { return loading; } public bool isPlaying() { return playing; } public bool isPaused() { return paused; } public bool isRepeatable() { return ! String.IsNullOrEmpty(gloss); } public virtual void setSubtitle(string text) { this.subtitles.setText(text); } /* Configura as animações de intervalo */ public void setRandomAnimations(string[] intervalAnimations) { foreach (string name in intervalAnimations) { this.intervalAnimations.Add(name); this.loadedAssetBundles.Add(name); } this.randomAnimations.setAnimations(intervalAnimations); } /* Define a velocidade das animacões com base no slider da GUI */ public void setSlider(float sliderPosition) { subtitles.SliderPosition = sliderPosition; subtitles.updateWordSpeed(); subtitles.updateLetterSpeed(); subtitles.updateNumberSpeed(); // Altera a velocidade de todas as animações em reprodução if ( ! paused) lock (this.animQueue) { foreach (AnimationReference reference in this.animQueue) if (reference.type != Subtitle.TYPE_NONE && reference.state != null) reference.state.speed = getSpeedByType(reference.type); } } /* Retorna a velocidade para o tipo */ private float getSpeedByType(short type) { switch (type) { case Subtitle.TYPE_WORD: return subtitles.WordSpeed; case Subtitle.TYPE_LETTER: return subtitles.LetterSpeed; case Subtitle.TYPE_NUMBER: return subtitles.NumberSpeed; } return 2F; } /* Para carregamento e animações */ public void stopAll() { StopCoroutine("LoadAndPlay"); this.randomAnimations.unlockFor("GPM.LoadAndPlay"); loading = false; stopAnimations(); } /* Para animações */ public void stopAnimations() { StopCoroutine(subtitlesSynchronizer); this.randomAnimations.unlockFor("GPM.SubtitlesSynchronizer"); this.subtitles.setText(""); lock (this.animQueue) { this.animQueue.Clear(); } COMPONENT_ANIMATION.CrossFadeQueued(DEFAULT_ANIMATION, fadeLength, QueueMode.PlayNow); resetStates(); } /* Repete animações */ public void repeat() { repeat(true); } /* Repete animações se now == true ou se não estiver carregando glosa */ public void repeat(bool now) { if (now || ! this.loading) playNow(this.gloss); } /* Manda reproduzir animação e adiciona a file de animações a serem reproduzidas */ private AnimationState playAnimation(short type, string name, string subtitle, float speed) { try { AnimationState state = COMPONENT_ANIMATION.CrossFadeQueued(name, fadeLength, QueueMode.CompleteOthers); state.speed = speed; lock (this.animQueue) { this.animQueue.Enqueue(new AnimationReference(name, subtitle, state, type)); } return state; } catch (NullReferenceException nre) { UnityEngine.Debug.Log("'" + name + "' não foi encontrado!\n" + nre.ToString()); } return null; } private AnimationState playAnimation(short type, string name, string subtitle) { return playAnimation(type, name, subtitle, getSpeedByType(type)); } private AnimationState playAnimation(short type, string name) { return playAnimation(type, name, name); } /* Enfileira em reprodução a animação de intervalo */ private void playDefaultAnimation() { playDefaultAnimation(false); } /* Enfileira em reprodução a animação padrão se now == true, ou reproduz imediatamente */ private void playDefaultAnimation(bool now) { COMPONENT_ANIMATION.CrossFadeQueued(DEFAULT_ANIMATION, fadeLength, now ? QueueMode.PlayNow : QueueMode.CompleteOthers); } /** * Returns the asset bundle named aniName. * * @return AssetBundle - se for encontrado. * null - se ocorrer num erro. */ public abstract WWW loadAssetBundle(string aniName); /** * Listen to changes in the playing status. */ public abstract void onConnectionError(string gloss, string word); /** * Listen to changes in the playing status. */ public abstract void onPlayingStateChange(); /* Pause or continue animations */ public void setPauseState(bool paused) { if (this.paused != paused) { this.paused = paused; lock (this.animQueue) { if (this.animationPlaying != null && this.animationPlaying.state != null) this.animationPlaying.state.speed = paused ? 0F : getSpeedByType(this.animationPlaying.type); foreach (AnimationReference reference in this.animQueue) if (reference.state != null) reference.state.speed = paused ? 0F : getSpeedByType(reference.type); } } onPlayingStateChange(); } public void setAnimationEnabled(bool enabled) { COMPONENT_ANIMATION.enabled = enabled; } /* Pause or continue animations */ public void switchPauseState() { setPauseState( ! this.paused); } /* Play if anything loading or playing */ public bool playIfEmpty(string gloss) { if (this.loading || this.playing) return false; StartCoroutine(LoadAndPlay(gloss)); return true; } /* Enqueue animations */ public void playQueued(string gloss) { Debug.Log("GPM.pQ(" + gloss + ")"); StartCoroutine(LoadAndPlay(gloss)); } /* Stop all and play */ public void playNow(string gloss) { Debug.Log("GPM.pN(" + gloss + ")"); stopAll(); StartCoroutine(LoadAndPlay(gloss)); } /* Reproduz animação de intervalo */ public bool playIntervalAnimation(string name) { playDefaultAnimation(true); this.intervalAnimationState = COMPONENT_ANIMATION.CrossFadeQueued(name, fadeLength, QueueMode.CompleteOthers); playDefaultAnimation(false); return true; } private string nextLetterAnimation(char letter) { string animation = (this.lastLetterAnimations[letter] ? "" : "d_") + letter.ToString(); this.lastLetterAnimations[letter] = ! this.lastLetterAnimations[letter]; return animation; } private static short getType(char c) { // Se for uma letra if (c >= 65 && c <= 90) return Subtitle.TYPE_LETTER; // Se for um número else if (c >= 48 && c <= 57) return Subtitle.TYPE_NUMBER; // Se for uma vírgula else if (c == 44 || c == 'Ç') return Subtitle.TYPE_WORD; else return Subtitle.TYPE_NONE; } /* Enfileira soletração de palavra */ private string spellWord(Queue toPlayQueue, string word) { string lastAnimationSubtitle = ""; bool defaultPlayed = false; // A reprodução da primeira letra deve ser longa para não ser cortada no fade this.subtitles.updateLetterSpeed(); for (int i = 0; i < word.Length; i++) { switch (word[i]) { case 'Á': case 'Â': case 'À': case 'Ã': word = word.Replace(word[i], 'A'); break; case 'É': case 'Ê': word = word.Replace(word[i], 'E'); break; case 'Í': word = word.Replace(word[i], 'I'); break; case 'Ó': case 'Ô': case 'Õ': word = word.Replace(word[i], 'O'); break; case 'Ú': word = word.Replace(word[i], 'U'); break; } if (!allowedCharacters.Contains(word[i])) { Debug.Log(word[i] + " is not allowed"); word = word.Remove(i, 1); i--; } } for (int i = 0; i < word.Length; i++) { lastAnimationSubtitle = Subtitle.highlight(word, i); char anim = word[i]; short type = getType(anim); string animName = nextLetterAnimation(anim); // Não há animação if (type == Subtitle.TYPE_NONE) { // Reproduz animação default apenas uma vez if ( ! defaultPlayed) { defaultPlayed = true; toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_WORD, DEFAULT_ANIMATION_MIDDLE, lastAnimationSubtitle, this)); // A reprodução da próxima letra deve ser longa para não ser cortada no fade this.subtitles.updateLetterSpeed(); } Debug.Log("GPM.sW(" + word + "): Animação \"" + animName + "\" inexistente."); } else { toPlayQueue.Enqueue(new ToPlay(type, animName, lastAnimationSubtitle, this)); defaultPlayed = false; this.subtitles.updateLetterSpeed(); } } return lastAnimationSubtitle; } /* Instruções para reprodução de animação */ private struct ToPlay { private short type; private string name; private string subtitle; private float speed; public ToPlay(short type, string name, string subtitle, float speed) { this.type = type; this.name = name; this.subtitle = subtitle; this.speed = speed; } public ToPlay(short type, string name, string subtitle, GenericPlayerManager context) : this(type, name, subtitle, 0F) { this.speed = context.getSpeedByType(type); } public ToPlay(short type, string name, GenericPlayerManager context) : this(type, name, name, context) { } public void play(GenericPlayerManager context) { context.playAnimation(this.type, this.name, this.subtitle, this.speed); } } /* Carrega animações e reproduz */ private IEnumerator LoadAndPlay(string gloss) { Debug.Log("GPM.LAP(" + gloss + ")"); while (loading) yield return null; this.loading = true; // onPlayingStateChange(); this.randomAnimations.lockFor("GPM.LoadAndPlay"); string lastAnimationSubtitle = ""; bool spelled = false; Debug.Log("GPM.LAP(" + gloss + "): Starting subtitlesSynchronizer"); if (this.subtitlesSynchronizer == null) this.subtitlesSynchronizer = SubtitlesSynchronizer(); StartCoroutine(this.subtitlesSynchronizer); yield return null; string[] stringPos = gloss.Split(' '); Queue toPlayQueue = new Queue(); int wordsCount = 0; toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_NONE, DEFAULT_ANIMATION, "", this)); foreach (string aniName in stringPos) { Debug.Log("GPM.LAP(" + gloss + "): Animation name: " + aniName); wordsCount++; if (String.IsNullOrEmpty(aniName)) continue; bool nonexistent = nonexistentAssetBundles.Contains(aniName); bool loaded = loadedAssetBundles.Contains(aniName); if ( ! nonexistent && ! loaded) { // Função loadAssetBundle é definida pela classe filha WWW www = loadAssetBundle(aniName); if (www != null) { Debug.Log("GPM:LAP(" + gloss + "): www != null"); yield return www; AssetBundle bundle = null; if (www.error == null) { Debug.Log("GPM:LAP(" + gloss + "): www.error == null"); bundle = www.assetBundle; if (bundle != null && ! string.IsNullOrEmpty(bundle.mainAsset.name)) { AnimationClip aniClip = bundle.mainAsset as AnimationClip; bundle.Unload(false); if (aniClip) { COMPONENT_ANIMATION.AddClip(aniClip, aniName); loadedAssetBundles.Add(aniName); loaded = true; Debug.Log("GPM:LAP(" + gloss + "): Bundle \"" + aniName + "\" loaded!"); } else Debug.Log ("GPM:lAP(" + gloss + "): Sinal \"" + aniName + "\" foi não carregado corretamente."); } } else onConnectionError(gloss, aniName); } else onConnectionError(gloss, aniName); } // Reproduz palavra if (loaded) { if (spelled) { // Default toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_NONE, DEFAULT_ANIMATION, lastAnimationSubtitle, this)); spelled = false; } if (this.flags.Contains(aniName) || this.intervalAnimations.Contains(aniName)) { lastAnimationSubtitle = ""; toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_WORD, aniName, "", this)); } else { lastAnimationSubtitle = aniName; toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_WORD, aniName, this)); } } // Soletra palavra else { // Se a animação não foi carregada e nem está marcada como não existente, // adiciona ao set de animações não existentes if ( ! nonexistent) nonexistentAssetBundles.Add(aniName); UnityEngine.Debug.Log("GPM:lAP(" + gloss + "): To spell: " + aniName); if (this.flags.Contains(aniName) || this.intervalAnimations.Contains(aniName)) { toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_NONE, DEFAULT_ANIMATION_MIDDLE, "", 1.6F)); spelled = false; } else { // Se já houve o soletramento de alguma palavra, reproduz animação default if (spelled) toPlayQueue.Enqueue(new ToPlay(Subtitle.TYPE_NONE, DEFAULT_ANIMATION_MIDDLE, "", 1.6F)); else spelled = true; lastAnimationSubtitle = spellWord(toPlayQueue, aniName); } } if (toPlayQueue.Count > 4 || wordsCount == stringPos.Length) while (toPlayQueue.Count > 0) toPlayQueue.Dequeue().play(this); yield return null; } // Default playAnimation(Subtitle.TYPE_NONE, DEFAULT_ANIMATION, ""); this.randomAnimations.unlockFor("GPM.LoadAndPlay"); this.loading = false; // onPlayingStateChange(); } /* Sincroniza as legendas com as animações. */ private IEnumerator SubtitlesSynchronizer() { UnityEngine.Debug.Log("GPM.SS()"); this.randomAnimations.lockFor("GPM.SubtitlesSynchronizer"); this.playing = true; onPlayingStateChange(); bool isNotEmpty; lock (this.animQueue) { isNotEmpty = this.animQueue.Count > 0; } // Animação anterior a atual AnimationReference endedAnimation = null; // Enquanto estiver executando a corotina "loadAndPlay" // ou existir animações na fila de reprodução while (loading || isNotEmpty) { // Se não houver animações na fila, espera if (isNotEmpty) { // Pega primeira animação AnimationReference reference; lock (this.animQueue) { reference = this.animQueue.Peek(); } // Se estiver sendo reproduzida if (COMPONENT_ANIMATION.IsPlaying(reference.name)) { Debug.Log("GPM.SS(): Playing " + reference.name); this.subtitles.setText(reference.subtitle); // Animação seguinte AnimationReference next = null; lock (this.animQueue) { this.animationPlaying = this.animQueue.Dequeue(); if (this.animQueue.Count > 0) next = this.animQueue.Peek(); } while (true) { // Se a próxima animação estiver sendo reproduzida (no fade) if (next != null && COMPONENT_ANIMATION.IsPlaying(next.name)) { // Se a animação anterior a atual não tiver acabado, // espera acabar e só então conta o tempo if (endedAnimation != null) while (COMPONENT_ANIMATION.IsPlaying(endedAnimation.name)) yield return null; // Tempo para pular para a legenda da próxima animação yield return new WaitForSeconds(0.4F); // Deprecated // yield return WaitForContinuousMillis.Wait(this, 300); endedAnimation = reference; break; } else if (COMPONENT_ANIMATION.IsPlaying(reference.name)) yield return null; else break; } reference = null; } // Se a animação não tiver sido liberada e seu AnimationState for nulo, // a animação será liberada if (reference != null && reference.state == null) lock (this.animQueue) { this.animQueue.Dequeue(); } else yield return null; } else yield return null; lock (this.animQueue) { isNotEmpty = this.animQueue.Count > 0; } } UnityEngine.Debug.Log("GPM.SS(): All done."); this.subtitles.setText(""); resetStates(); this.randomAnimations.unlockFor("GPM.SubtitlesSynchronizer"); this.subtitlesSynchronizer = null; } public void resetStates() { this.animationPlaying = null; this.playing = false; this.paused = false; onPlayingStateChange(); } }